AngularJS 是一個 Web 應用框架,它實現了前端的 MVC 架構,能讓開發人員很方便地實現業務邏輯。
舉個栗子,要做到下面的效果,以前可能需要寫一連串的 JavaScript 代碼綁定 N 多事件。而使用 AngularJS 框架,一句 JavaScript 都不用寫就能實現了,神奇吧?查看演示。
這得益於 AngularJS 中的雙向數據綁定特性(Two Way Data-Binding),將 Model 和 View 自動關聯了起來,在更複雜的業務場景下,還有代碼分離的好處,將 DOM 操作和應用邏輯解耦,非常實用。
不過沒有銀彈,和其他框架一樣,AngularJS 也有它的局限。CRUD 類型的操作是它所擅長的,想想看以前寫過的管理後台,幾乎大部分都是從數據庫中讀取數據,然後呈現在頁面上,進行各種增刪改查。AngularJS 約定了一套規範(約定優於配置),於是你可以很便捷地操作數據。而在其他方面,例如開發複雜的 Web 遊戲,AngularJS 則是無用武之地了。
一、AngularJS 中的精美特性
雙向綁定
上面的例子已經說明了,我們可以像 PHP Smarty 模板一樣在 HTML 中寫表達式,用 {{ 和 }} 包起來。在 AngularJS 里,View 和 Model 是在 Controller 裡面綁定的,所以無論你在 View 的表單中修改了內容,還是在 Controller 里通過代碼修改了 Model 值,兩邊都會即時發生變化,同步更新。因為 AngularJS 會監控 (watch) Model 對象的變化,隨時反映到 View 中。
Filter
Filter 類似 Unix 裡面的 | 管道概念,AngularJS 把它搬到了前端。還是舉個例子,你們感受一下——
輸出結果:
二、AngularJS 中的一些“坑”
由於過去寫 JavaScript 的習慣使然,人們很容易掉進一些 AngularJS 的陷阱里。下面的內容假設你已經了解前端 MVC 概念,並對 AngularJS 有了一定經驗,初學者讀起來可能比較艱深晦澀。
DOM 操作
避免使用 jQuery 來操作 DOM,包括增加元素節點,移除元素節點,獲取元素內容,隱藏或顯示元素。你應該使用 directives 來實現這些動作,有必要的話你還要編寫自己的 directives。
如果你感到很難改變習慣,那麼考慮從你的網頁中移除 jQuery 吧。真的,AngularJS 中的
$http
服務非常強大,基本可以替代 jQuery 的 ajax 函數,而且 AngularJS 內嵌了 jQLite —— 它內部實現的一個 jQuery 子集,包含了常用的 jQuery DOM 操作方法,事件綁定等等。但這並不是說用了AngularJS 就不能用 jQuery 了。如果你的網頁有載入 jQuery 那麼 AngularJS 會優先採用你的 jQuery,否則它會 fall back 到 jQLite。
需要自己編寫 directives 的情況通常是當你使用了第三方的 jQuery 插件。因為插件在 AngularJS 之外對錶單值進行更改,並不能即時反應到 Model 中。例如我們用得比較多的 jQueryUI datepicker 插件,當你選中一個日期後,插件會將日期字符串填到 input 輸入框中。View 改變了,卻並沒有更新 Model,因為
$('.datepicker').datepicker();
這段代碼不屬於 AngularJS 的管理範圍。我們需要編寫一個directive 來讓 DOM 的改變即時更新到 Model 里。
然後在 HTML 中引入這個 direcitve
說白了 directive 就是在 HTML 里寫自定義的標籤屬性,達到插件的作用。這種聲明式的語法擴展了 HTML。
需要說明的是,有一個 AngularUI 項目提供了大量的 directive 給我們使用,包括 Bootstrap 框架中的插件以及基於 jQuery 的其他很熱門的 UI 組件。我之前說過 AngularJS 的社區很活躍嘛,生態系統健全。
ngOption 中的 value
這是個大坑。如果你去查看 ngOption 生成的
<select>
中的 <option>
的選項值(每個 <option value="xxx">
的 value 部分),那絕對是枉費心機。因為這裡的值永遠都會是 AngularJS 內部元素的索引,並不是你所指定的表單選項值。
還是要轉變觀念,AngularJS 已經不再用表單進行數據交互了,而是用 Model。使用 $http 來提交 Model,在 php 中則使用
file_get_contents('php://input')
來獲取前端提交的數據。{{ }} 的問題
在頁面初始化的時候,用戶可能會看到 {{ }},然後閃爍一下才出現真正的內容。
解決辦法:
解決辦法:
- 使用 ng-cloak directive 來隱藏它
- 使用 ng-bind 替代 {{ }}
將界面與業務邏輯分離
Controller 不應該直接引用 DOM,而應該控制 view 的行為。例如“如果用戶操作了 X,應該發生什麼事情”,“我從哪裡可以獲得 X?”
Service 在大部分情況下也不應該直接引用 DOM,它應該是一個單例(singletons),獨立於界面,與 view 的邏輯無關。它的角色只是“做 X 操作”。
DOM 操作應該放在 directives 裡面。
盡量復用已有功能
你所寫的功能很可能 AngularJS 已經實現了,有一些代碼是可以抽象出來複用的,使用更 Angular 的方式。總之就是很多 jQuery 的繁瑣代碼可以被替代。
1.
ng-repeat
ng-repeat 很有用。當 Ajax 從服務器獲得數據後,我們經常使用 jQuery (比如上面講過的例子) 向某些 HTML 容器節點中添加更多的元素,這在 AngularJS 里是不好的做法。有了 ng-repeat 一切就變得非常簡單了。在你的 $scope 中定義一個數組 (model) 來保存從服務器拉取的數據,然後使用 ng-repeat 將它與 DOM 綁定即可。下面的例子初始化定義了 friends 這個 model
顯示結果
2.
ng-show
ng-show 也很有用。使用 jQuery 來根據條件控制界面元素的顯示隱藏,這很常見。但是 Angular 有更好的方式來做到這一點。ng-show (以及 ng-hide) 可以根據布爾表達式來決定隱藏和顯示。在 $scope 中定義一個變量:
類似的內置 directives 還有 ng-disabled, ng-switch 等等,用於條件控制,語法簡潔,都很強大。
3.
ng-class
ng-class 用於條件性地給元素添加 class,以前我們也經常用 jQuery 來實現。Angular 中的 ng-class 當然更好用了,例子:
在這裡 ng-class 接受一個 object 對象,key 為 CSS class 名,值為 $scope 變量控制的條件表達式,其他類似的內置 directives 還有 ng-class-even 和 ng-class-odd,很實用。
$watch 和 $apply
AngularJS 的雙向數據綁定是最令人興奮的特性了,然而它也不是全能的魔法,在某些情況下你需要做一些小小的修正。
當你使用 ng-model, ng-repeat 等等來綁定一個元素的值時, AngularJS 為那個值創建了一個 $watch,只要這個值在 AngularJS 的範圍內有任何改變,所有的地方都會同步更新。而你在寫自定義的 directive 時,你需要定義你自己的 $watch 來實現這種自動同步。
有時候你在代碼中改變了 model 的值,view 卻沒有更新,這在自定義事件綁定中經常遇到。這時你就需要手動調用 scope.$apply() 來觸發界面更新。上面 datepicker 的例子已經說明了這一點。第三方插件可能會有 call back,我們也可以把回調函數寫成匿名函數作為參數傳入$apply()中。
將 ng-repeat 和其他 directives 結合起來
ng-repeat 很有用,不過它和 DOM 綁定了,很難在同一個元素上使用其他 directives (比如 ng-show, ng-controller 等等)。
如果你想對整個循環使用某個 directive,你可以在 repeat 外再包一層父元素把 directive 寫在那兒;如果你想對循環內部的每一個元素使用某個 directive,那麼把它放到 ng-repeat 的一個子節點上即可。
Scope
Scope 在 templates 模板中應該是 read-only 的,而在 controller 里應該是 write-only 的。Scope 的目的是引用 model,而不是成為 model。model 就是我們定義的 JavaScript 對象。
$rootScope 是可以用的,不過很可能被濫用
Scopes 在 AngularJS 中形成一定的層級關係,樹狀結構必然有一個根節點。通常我們用不到它,因為幾乎每個 view 都有一個 controller 以及相對應的自己的 scope。
但偶爾有一些數據我們希望全局應用在整個 app 中,這時我們可以將數據注入 $rootScope。因為其他 scope 都會繼承 root scope,所以那些注入的數據對於 ng-show 這類 directive 都是可用的,就像是在本地 $scope 中的變量一樣。
當然,全局變量是邪惡的,你必須很小心地使用 $rootScope。特別是不要用於代碼,而僅僅用於注入數據。如果你非常希望在 $rootScope 寫一個函數,那最好把它寫到 service 里,這樣只有用到的時候它才會被注入,測試起來也方便些。
相反,如果一個函數的功能僅僅是存儲和返回一些數據,就不要把它創建成一個 service。
三、AngularJS 項目的目錄結構
怎樣組織代碼文件和目錄?這恐怕是初學者一開始就會遇到的問題。AngularJS 應用開發的官方入門項目 angular-seed,其文件結構是這樣的:
- css/
- img/
- js/
- app.js
- controllers.js
- directives.js
- filters.js
- services.js
- lib/
- partials/
這種結構對於一個簡單的單頁 app 來說是可行的,只是一旦代碼中存在多個 Controller 或者 Service,就很難找到想要尋找的對象了。我們可以對文件按照業務邏輯進行拆分,就像下面這樣:
- controllers/
- LoginController.js
- RegistrationController.js
- ProductDetailController.js
- SearchResultsController.js
- directives.js
- filters.js
- models/
- CartModel.js
- ProductModel.js
- SearchResultsModel.js
- UserModel.js
- services/
- CartService.js
- UserService.js
- ProductService.js
這種結構把不同的業務功能拆分為獨立的文件,條理清晰,但是仍有一定的局限性。最大的問題是一個業務功能的代碼分布在controllers, models, servers 三個不同目錄下,要從中挑出正確的文件,建立起代碼關聯,還是有些麻煩。按照功能進行模塊化劃分目錄結構,應該要更為合理一些:
- cart/
- CartModel.js
- CartService.js
- common/
- directives.js
- filters.js
- product/
- search/
- SearchResultsController.js
- SearchResultsModel.js
- ProductDetailController.js
- ProductModel.js
- ProductService.js
- search/
- user/
- LoginController.js
- RegistrationController.js
- UserModel.js
- UserService.js
這樣也是適合 RequireJS 等模塊加載器的自然直觀的代碼組織方式。
參考鏈接:
From: https://www.lovelucy.info/angularjs-best-practices.html