使用AngularJS編寫較為優美的JavaScript代碼指南,angularjs
本文範例程式碼下載:modulePattern.zip - 所有的 4 個 HTML 檔案 以及 panacea.js - 1.6 KB
介紹
AngularJS 的庫裡面有很多東西,但本文中我只想專註於小的,針對特定主題的庫,我相信通過它們能對Angular有一個較好的介紹. 理解這篇文章並不需要你有任何Angular相關的,甚至是JavaScript的經驗。希望你能從本文中看到一些使用Angular的好處,並樂於動手嘗試.
背景
我使用Angular有一段時間了,而在學習Angular的時候,我也喜歡構建一些範例,所以當我一開始深入進去的時候,對於模組或者JavaScript的設計模式,我也沒有多想,那樣對保持程式碼群組織和條理性有協助. 那就是所有的重點:保持代碼的組織和條理性. 因此,現在我回過頭來,建立了這個極其小巧的範例,以展示使用模組可以有多簡單. 一路走來,我希望它能夠成為一篇好的對Angular的介紹.
(大多數)文章在闡述模式時的問題
大多數時候人們都會嘗試去在讀者知道模式是啥概念之前就開始闡述一個模式,而這基本上誤導了每一個人. 這裡要努力使得本文盡量簡單,讓我們首先來看一看這個問題吧。哪個問題呢?就是有關預設會在全域記憶體空間被建立的所有東西的Javascript的問題.
下面就是我所說的意思.
JavaScript 預設的全域問題
設想你的HTML中有下面這樣一段指令碼.
<script> var isDoingWork = false;</script>
範圍?
你清楚這個變數的範圍麼?
是的,它是全域的。這個布爾值實際上被添加到了瀏覽器的全域視窗對象中.
把它設定到Action中
這裡你可以看到它在Action中是怎樣的.
- 下載本文的代碼範例.
- 在你的瀏覽器中開啟 modulePattern.htm .
- 開啟瀏覽器開發工具 -- F12(Chrome, IE) or Ctrl-Shift-I (Opera) -- (那樣就可以看見控制台了)
- 在瀏覽器工具控制台下,輸入: isDoingWork,然後斷行符號<ENTER>
- 你會看到輸出的值為false.
- 現在輸入 : isDoingWork = true,然後斷行符號<ENTER>
- 如此下載的值就為true了. 你已經改變了這個值.
- 你可以看到這個值已經通過輸入doingwindow.isDoingWork = true然後斷行符號<ENTER>,被添加到了全域視窗對象之中.
這可能會造成一些名字衝突,也會導致一些嚴重的bug. 這也許對你而言有點杞人憂天了,是不? 但是請設想你是決定要去實現某一個新的JS庫,它每分每秒都可以被建立出來. 假設你發現了這個叫做 Panacea.js 的很棒的庫,它將解決你所有的問題.
因此你向下面這樣在你的頁面中引用了它:
<script src="panacea.js"></script>
如此簡單,你就已經解決之前你遇到的所有問題. 然而,因為它是一個龐大的庫,而你只想要解決方案,卻不回去深挖這個龐大(幾千行代碼)源檔案裡的每一行代碼. 而深埋在 Panacea.js 裡面某個角落的確實下面這樣的代碼:
var isDoingWork = false; setInterval(function(){isDoingWork = !isDoingWork;}, 3000);
這代碼真是酷,你知道嗎?
每個3秒,它都會將這個布爾值設定成相對的值。啊!
自己動手看看
如果你想要自己動手驗證下這個東西,你可以做下面這幾步:
- 下載本文的範例代碼.
- 在你的瀏覽器中開啟 modulePattern2.htm .
- 開啟瀏覽器開發工具 -- F12(Chrome, IE) 或者 Ctrl-Shift-I (Opera) -- (這樣你就可以看到控制台了)
- 在瀏覽器開發工具的控制台下,輸入 : isDoingWork 然後斷行符號<ENTER>
- 將第4步多重複幾次,你將會發現isDoingWork的值會每個大約3秒鐘變化一次.
那這是不是很棒呢?
我的第一個觀點 : 模組模式是很有用的
我需要為此做出解釋,為了要向你展示為什麼 JavaScript 的模組模式是很有用的. 我得想你展示 JavaScript 的模組模式,那樣我就可以告訴你它是如何在AngularJS中被使用或實現的了.
模組模式:封裝
如此,實際就是,模組模式基本上就是封裝了. 封裝聽起來很熟悉,如果你有點物件導向編程經曆的話 -- 而我也希望你能有點這個經驗. 封裝是物件導向編程的三原則之一。封裝的另外一個說法就是資料隱藏。在經典的物件導向編程中——它不同於JavaScript所依賴的原型化OOP -- 資料隱藏是構建一個類模板的內在組成部分.
例如在 C#中, Animal 類的封裝 -- 隱藏資料 -- 特定的值被關聯到Animal對象. 那樣,如果某人決定變更那些值,他或他必須明確的通過初始化一個Animal對象並設定這個對象的值來達到目的. 在JavaScript中,我們則可以隨意的在全域視窗對象中設定值.
public class Animal { // constructor allows user to set the commonName public animal(string name) { this.commonName = name; } // making commonName private hides (encapsulates) it in the class private string commonName; //explicitly exposing the value for reading only public string CommonName get { return this.commonName }}
在JavaScript中,模組已經被建立用來類比這種封裝行為了,如此我們就不會去將我們的變數組織到一個全域的命名空間中,並造成了隱藏很深的難以被發現和修複的問題.
現在你知道為什麼了,讓我們來看看如何會是這樣的.
函數被立即調用的運算式(IIFE)
看上去就好像每次我們向前推進一步,我們都要走點旁門左道. 因為要獲得能讓我們建立模組模式的JavaScript文法,我們就得去瞭解一種叫做函數被立即調用的運算式文法,也叫做IIFE ( IIFE 發音是 "iffy").
最基礎的 IIFE 看起來像這樣:
(function(){ // lines // of // code}());
如果你從來沒有看到過像這樣的東西,那你就有點說不過去了.
立即被調用
首先,這個名稱的第一部分叫做立即被調用的原因是,一般包含這個特殊函數的源檔案被載入好了,那麼包含在這個函數中的代碼就會運行.
對IIFE文法更加仔細的觀察
你可以看到這個文法的最中心是一個函數。看一下這個代碼塊,我已經將代碼分段並將一些行標上了號,如此我們就可以探討它了.
( // 1. function() //2. { // 3. // 一行一行 // 的 // 代碼 }() // 4.); // 5.
首先,看看上面指令碼的第2行。這一行通常看來就是一個匿名(也就是沒有命名)的函式宣告. 而後,第3一直到第4則是這個函數的主題部分。最後,第4行最後以一對括弧結束,這對 括弧會告訴JavaScript解譯器去調用這個函數。最終, 所有這些都會被包在一個不歸屬任何部分的括弧(第1和第5行)中, 而這對括弧會告訴解譯器要調用這個外部的匿名函數,它包含了我們所定義的函數.
IIFE 可以帶上參數
這段奇怪的文法會在帶上參數之後,看起來會更加的奇怪. 它看起來會像是下面這樣
(function(thing1, thing2){ // lines // of // code}("in string", 382));
現在,你可以看到這個函數可以帶上兩個會被內部的函數引用的thing1, thing2參數. 被傳入值,在樣本中是 "in string" 和 382.
現在我們理解了IIFE文法,讓我們來建立另外一個程式碼範例,我們將運行這段代碼來看看封裝是如何運作的.
(function(){ var isDoingWork = false; console.log("isDoingWork value : " + isDoingWork);}());
自己動手看看
為了看看是怎麼啟動並執行,你可以做下面這幾步:
- 下載本文的原始碼.
- 在你的瀏覽器中開啟 modulePattern3.htm.
- 開啟瀏覽器的開發工具 -- F12(Chrome, IE) 或者 Ctrl-Shift-I (Opera) -- (這樣你就可以看到控制台了)
- 你可以看到很像下面這樣圖片中所展示出來的東西
當方法被調用時 -- 這會在代碼被JavaScript解譯器載入支護立即發生 -- 而後函數會建立 isDoingWork 變數,並調用console.log()來在控制台輸出這個變數的值.
現在,讓我們使用開發工具中的控制台來試試我們之前所嘗試過的步驟:
輸入: isDoingWork然後斷行符號<ENTER>
當你這樣做了之後,你將會看到 瀏覽器不再相信isDoingWork這個值被定義過。即使是你嘗試從全域視窗對象中擷取這個值, 瀏覽器也不認為 isDoingWork 這個值在此對象中被定義了. 你所看到的錯誤訊息看起來會像接下來這張圖片中所展示的這樣.
函數是一個對象:它建立了範圍
這是因為現在你已經把isDoingWork這個變數建立在了一個函數裡面 -- 也就是我們們的匿名 IIFE 中 -- 而如此這個變數就只能通過這個函數才能訪問到. 有趣的是Javascript中的所有函數都是第一類對象. 那很簡明的意味著函數是一個對象,它可能通過一個變數被訪問到. 或者說,另外一種描述的方式是你儲存了指向 函數的一個引用,並在稍後的某個時間擷取其變數.
在我們第一個樣本中,我們的問題是並沒有儲存一個指向我們匿名函數的引用,所以我們永遠也不能再擷取到isDoingWork這個值。這就是我們下一個樣本要改進的地方.
函數是一個對象 : 使用this
因為每一個函數都是一個對象,所以每個函數都會有一個this變數,這個變數向開發人員提供了指向當前對象的引用. 為了提供在從外部大我們的函數及其範圍的訪問,我們可以返回這個this變數 -- 而它將會提供一個指向當前對象的引用.
然後,除非我們將這個私人的isDoingWork變數添加到函數引用(this)上,我們也不能夠引用這個變數。為此我們要對之前的樣本做一下輕微的改動。它看起來會像下面這樣:
thing = (function(){ // 1. this.isDoingWork = false; // 2. console.log("isDoingWork value : " + isDoingWork); return this; // 3.}());
你可以看到第一行我們加入了一個新的全域變數thing,它包含了從匿名函數返回的值。從範例程式碼的開頭跳到第三行,你可以看到我們返回了this變數。那就意味著我們返回了一個指向匿名函數的引用.
在第二行我們也已經將isDoingWork加入了this引用中,那樣我們就可以使用文法thing.isDoingWork來從外部參考到這個值了.
自己動手看看
為了看看的運行,你可以做下面這幾步:
- 下載本文的範例程式碼.
- 在你的瀏覽器中開啟 modulePattern4.htm.
- 開啟瀏覽器開發工具 -- F12(Chrome, IE) 或者 Ctrl-Shift-I (Opera) -- (那樣你就可以看到控制台了)
- 你將會看到isDoingWork的值會輸出到控制台,就像最開始那個樣本中你看到的那樣.
- 不過,現在你得輸入thing.isDoingWork才能或者這個值.
模組模式總結
在最後這個樣本中,變數值被成功的封裝了,而其他的JavaScript庫則可以明確的引用thing對象來擷取這個值. 好像不大可能,而這協助了我們保持全域命名空間的乾淨,並且在看起看來是更好的程式碼群組織形式. 這也使得我們代碼的維護更容易.
最終,我們用上了 AngularJS
因為使用模組模式是一個最佳實務,AngularJS的開發人員就將一個模組系統構建到了庫中.
Plunker 代碼
首先你可以通過到這個Plunker上 (http://plnkr.co/edit/js8rbKpIuAuePzLF2DcP?p=preview - 在一個新的視窗或Tab頁開啟)擷取整個AngularJS樣本.
而我們在這裡展示出代碼,那樣我們就可以更方便的談論它了.
首先,讓我們看看這個 HTML.
<!DOCTYPE html><html ng-app="mainApp"> <head> <meta charset="utf-8" /> <title>Angular Module Example</title> <script data-require="angular.js@1.2.x" src="https://code.angularjs.org/1.2.20/angular.js" data-semver="1.2.20"></script> <script src="mainCtrl.js"></script> <script src="secondCtrl.js"></script> </head> <body> <div ng-controller="MainCtrl as mc"> <p>mc refers to MainCtrl which has been added to the angular app module</p> <p>Hello {{mc.name}}!</p> <ol><li ng-repeat="a in mc.allThings">{{a}}</li></ol> </div> <div ng-controller="SecondCtrl as sc"> <p> Hello {{sc.name}} </p> <ol><li ng-repeat="a in sc.allThings">{{a}}</li></ol> </div> </body> </html>
Angular 指令 : ng-app
Angular 所定義和使用的東西叫做指令。這些指令基本上就是由Angular定義屬性,而AngularJS編譯器(Angular的JavaScript)會將它們轉換成其他的東西.
我們應用了ng-app指令,為我們的Angular應用定義了一個名稱,叫做mainApp.
mainApp 就是我們稍後會看到的模組模式的起點.
被引入的指令碼 : 每個都是一個模組
現在,請注意有三個指令碼被引入到了這個HTML中.
第一個是必須的AngularJS庫.
而其他兩個則是作為模組被實現的Angular控制器.
它們被作為模組實現以保持代碼彼此,還有從這個應用上看,都是獨立的.
AngularJS : 建立 score
在往下看,你將會看到兩個以如下代碼開頭的div:
<div ng-controller="MainCtrl as mc"> <div ng-controller="SecondCtrl as sc">
這是在為div的每一個都設定上ng-controller. 這些div中的每一個都有其各自的範圍. 第一個控制器的名字叫做 MainCtrl,第二個叫做 SecondCtrl.
AngularJS 編譯器會在你提供(引入)的代碼中用這兩個名稱尋找對應的函數.
如果AngularJS編譯器沒有找個這兩個名稱對應的函數,它就會拋出一個錯誤.
mainCtrl.js : 第一個控制器
讓我們來看看mainCtrl.js檔案裡面有些啥東西.
你可以在Plunker頁面的左側點擊它在Plunker中將其開啟.
當你開啟了它,你將會看到一些看上去很熟悉的代碼。好吧,你至少會看出來它們都是被包在一個IIFE中的.
(function() { var app = angular.module('mainApp', []); app.controller('MainCtrl', function() { console.log("in MainCtrl..."); // vt = virtual this - just shorthand vt = this; vt.name = 'MainCtrl'; vt.allThings = ["first", "second", "third"]; });})();
那是因為我們需要這些代碼在檔案mainCtrl.js被載入時就運行.
現在,請注意在這個IIFE中的第一行代碼.
var app = angular.module('mainApp', []);
這行代碼是Angular將一個模組添加到其命名空間的方式. 在這裡,我們添加了一個將用來展示我們應用程式的模組. 這是應用程式的模組,而我們已經將其命名為 itmainApp, 它跟HTML頁面上ng-app所指定的值是一樣的.
我們也建立了一個叫做app的(只在IIFE本地可見的)本地變數,以便我們將可以在這個函數內部用來再次添加一個控制器.
奇怪的 Angular 文法
請你也要再仔細看看第一行。你會注意到我們是首次建立mainApp模組,而如果是首次,則我們必須提供以字串數組的形式提供其可能需要的任何依賴(,表示出依賴庫的名稱). 不過,在這裡對於這個簡單的樣本而言,我們不需要任何的依賴。但Angular仍然需要我們傳入一個空的數組,以便它知曉我們正在建立新的模組,而不是去試圖載入一個已經被建立好了的模組.
提示: 你將會看到我們會在secondCtrl.js裡載入mainApp模組,而上面所提的數組將會有更多的作用.
我們一把mainApp建立好,就需要向其添加我們的控制器. 這些就是Angular預期我們在HTML(的div中)加入的控制器.
將控制器添加到App模組
添加控制器的代碼看起來像下面這樣:
app.controller('MainCtrl', function() { console.log("in MainCtrl..."); // vt = virtual this - just shorthand vt = this; vt.name = 'MainCtrl'; vt.allThings = ["first", "second", "third"]; });
為了添加我們的控制器函數,我們向app.controller()函數提供了一個控制器名稱和一個函數. 在此處我們提供了一個匿名函數.
所以,我們的控制器主體代碼就是下面這幾行了:
console.log("in MainCtrl..."); // vt = virtual this - just shorthand vt = this; vt.name = 'MainCtrl'; vt.allThings = ["first", "second", "third"];
這裡,當我們的控制器運行時,會向控制台輸出一行. 然後,我們將this變數重新命名為vt(方便起見,就叫他虛擬this) ,而後我天為其添加了一個name屬性和一個叫做allThings 的字串數組.
控制器和封裝
那就是當控制器被Angular調用時會啟動並執行代碼. 那個控制器會在檔案被載入時運行起來,也就是一開始HTML被載入的時候. 這意味著控制器會被載入到app模組中,而這些屬性會被添加到控制器對象(函數)中。因為我們想this變數添加了屬性,我們就可以在稍後擷取這些屬性,但它們是被封裝了起來的,因此它們不可以被每個人隨意的更改.
現在,讓我們跳到HTML中控制器被引用和使用的地方.
第一個Div
這是我們的MainCtrl控制器被引用和使用的第一個Div。它看起來就像下面這樣:
<div ng-controller="MainCtrl as mc"> <p>mc refers to MainCtrl which has been added to the angular app module</p> <p>Hello {{mc.name}}!</p> <ol><li ng-repeat="a in mc.allThings">{{a}}</li></ol> </div>
這個div輸出我們的web頁面的如下部分,看起來就是接下來這張圖片上所展示的那樣.
輸出被使用Angular指令來建立
不過,它使用了一種特殊的方式建立那個輸出,它使用了兩種Angular指令:
{{mc.name}} ng-repeat
第一個指令被關聯到了Div那一行上面MainCtrl的聲明和引用. 我們告訴Angular,說我們想以mc這個名稱引用我們的MainCtrl函數(對象)。那就是Angular提供的一個很棒的縮寫功能.
現在,因為我們將一個屬性放到了MainCtrl的this對象上,我們現在就可以通過mc和屬性的名稱來引用那些東西了。我們將那些東西包含特殊的雙大括弧{{ }}裡面,如此Angular編譯器就懂得那是可以啟動並執行代碼,你就會瞧見Angular將其轉換成了HTML:
<p>Hello {{mc.name}}!</p>
編程了下面這一:
Hello MainCtrl!
之後,我們設定了一個漂亮的無需列表,並使用了ng-repeat指令來迭代輸出數組中的每一行.
然後Angular跌倒了整個allThings數組,並將其裝換成了下面的HTML
<li ng-repeat="a in mc.allThings">{{a}}</li>
變成了如下的輸出
1. first2. second3. third
就那麼簡單。這就是模組化的所有東西,我們的值再也不會被任何人動手動腳了.
SecondCtrl : 幾乎就是同樣的東西
這裡有SecondCtrl的代碼. 代碼機會就是一樣的,除了我們擷取我滿原來的app模組處有點不一樣——不是第一次建立它了.
(function() { var app = angular.module('mainApp'); app.controller('SecondCtrl', function() { console.log("in SecondCtrl..."); // vt = virtual this - just shorthand vt = this; vt.name = 'SecondCtrl'; vt.allThings = ["bacon", "lettuce", "tomato"]; });})();
仔細看看下面這一行:
var app = angular.module('mainApp');
唯一的不同就是我們沒有提供引用數組.
那是因為mainApp已經是存在了的,而我們只是想向其添加另外一個新模組 (SecondCtrl) .
總結:最佳實務
所有其它的指令碼中的代碼,以及HTML基本上是一樣的,而此處最重要的是所有的代碼都被模組化了,資料也被封裝了起來,以便更好的組織我們的代碼. 這是Google軟體開發人員遵循的一個最佳實務,也是我們應該遵循的。請學習他,運用它,並與它同在吧(阿門).