本節書摘來自華章出版社《angularjs深度剖析與最佳實踐》一書中的第1章,第1.4節,作者 雪狼 破狼 彭洪偉,更多章節内容可以通路雲栖社群“華章計算機”公衆号檢視
接下來,我們開始實作第一個疊代的第一個功能:10.注冊。
我們把能夠通過url獨立通路的一項功能簡稱為“一個路由”,這裡為注冊功能配置設定一個叫作/reader/create的路由。
之是以不使用/register的形式,是希望在各個url之間保持統一,這也是我們在整個項目中将貫穿的一個約定。
如同後端開發一樣,我們将reader稱為controller,create稱作action,中間還可以有一個id,是以,典型的url是這樣的:/$controller/:id?/$action,其中的id字段是可以省略的,取決于具體的action。
這樣,我們在url和檔案所在的路徑之間就可以建立一個簡單的映射關系:拿到一個url,如/reader/1/edit,其中reader是$controller,edit是$action,于是我們知道它的代碼位于app/controllers/reader目錄下,其模闆為edit.html,控制器為edit.js,樣式為edit.scss。
這有什麼好處呢?比如測試人員報告說一個頁面存在bug,其url是/book/1/preview,我們一看bug報告,判斷出其錯誤位于控制器中,于是,我們直接開始修改app/controllers/book/preview.js。
如果使用的是intellij/webstorm,那麼我們隻要按下cmd-shift-n(navigate|file...,mac下)組合鍵,輸入preview.js,然後回車,就可以直接開始編輯了。其他ide也有類似的功能。
不要小看這一點點約定,它可以節省很多不必要的時間浪費,特别是在多人協作開發時,你的代碼可能會被很多人修改,如果連這個都需要溝通或者閱讀代碼,那麼浪費的時間和精力也是很可觀的。
配置設定完url,我們還需要把這個url和控制器、模闆對應起來。雖然我們有了一個約定,但是程式并不知道,我們還需要找個地方聲明一下,這個地方就是app/configs/router.js。
範例工程會給它生成一個代碼骨架,為了友善加注釋,我對一些語句進行了額外換行,實際代碼中是不需要這麼多換行的:
定義完路由,我們仍然不能直接通過url通路它,如果通路則會在浏覽器控制台中發現一個錯誤資訊:angular.js:10126 error: [ng:areq] argument 'readercreatectrl' is not a function, got undefined(angular.js:10126 錯誤:[ng:areq]參數'readercreatectrl'不是函數,而是未定義。),其原因是沒有找到名為readercreatectrl的控制器。
接下來,我們就對它所引用的模闆和控制器進行實作。
首先我們要建立一個app/controllers/reader/create.js檔案,和一個app/controllers/reader/create.html檔案,我們來看一個空白的create.js檔案:
接下來,我們就要開始實作它們了。
如果已經有ux(使用者體驗設計師)或ba(業務分析師)給出的原型圖,那麼建議從設計model的資料結構開始,這樣有助于更深入的了解angular開發中最顯著的特點:模型驅動。如果是從零開始,也可以先設計html。我們這個項目的開發是單人項目,是以我們先直接設計html。到本節的最後,我再來講解根據原型圖做模型驅動開發的過程。
我們要設計一個html,但不用設計一個“漂亮的”html。注意,在一個項目組中,不同的角色是需要分工的,要把ux擅長的工作留給ux;即使是單人項目,也需要把不同類型的工作分開完成。
在注冊頁,我們需要一個表單,它具有如下業務意義上的字段:郵箱、昵稱、密碼、确認密碼;還需要一些技術和法律意義上的字段:圖形驗證碼(captcha)、網站服務協定、“同意服務協定”複選框。
由于我們的業務并不需要手機号、年齡之類的字段,那麼我們就不要收集它。這種“最小資訊”原則,可以幫助你在受到安全攻擊的時候把損失控制在最小。同時,把需要填寫的内容控制在最小範圍内,也有利于提升使用者體驗。
我們的第一個html頁面如下:
注意,用來在input和label之間建立關聯的id字段都是用下劃線開頭的,這并不是随意為之,而是要把id留給寫“端到端測試”的人員。我們把這些不能不用的id全用下劃線開頭,有助于防止潛在的沖突。
現在,我們切到浏覽器中,會看到一個很難看的表單,如圖1-1所示。

雖然簡陋,但已經足夠表現我們的html骨架了。
接下來,我們需要把它修到可正常互動的級别。
首先,我們需要給每個輸入型字段(input/textarea/select等)綁定一個model變量。僅以郵箱字段為例,我們把它修改為:
這裡的ng-model就是angular中一系列“魔法”的關鍵。它是一個angular指令,其作用是把所在的input元素和ng-model=""中的表達式建立雙向綁定,這種雙向綁定意味着,當表達式的值發生變化時,input的value會跟着變化,反過來,當input中的value 由于使用者操作而發生變化時,綁定表達式的值也會相應跟着變化。而且這兩者的資料格式并不需要保持一緻,ng-model指令提供了一系列機制在兩者之間進行轉換。
ng-model并不限于用在input/textarea/select元素中,事實上,它幾乎可以用在任何元素中。但隻有這幾個元素可以直接使用ng-model,用于其他元素時需要自己寫自定義指令來實作雙向綁定。這是因為angular自帶了對input/textarea/select的重寫指令,重定義了它們的行為,使其可以支援ng-model。在後面的章節,我們會看到如何在自定義指令中支援ng-model。
這裡還涉及另一項約定和最佳實踐:把目前表單的所有字段綁定到一個叫作vm.form的對象中,這樣我們就可以很友善的把表單資料作為一個整體進行處理,比如送出或重置。
除了“确認密碼”和“同意協定”之外的其他的字段可以以此類推,不再摘引源碼。
“确認密碼”字段之是以特殊,是因為它并不需要最終送出給服務端,我們隻是為了防止使用者輸入錯誤,靠前端來校驗就已經足夠了。我們設想一下攻擊場景,發現對它隻做前端校驗并不會構成安全漏洞。是以,我們不需要把它綁定到vm.form對象的屬性中去,用個獨立的vm.retypedpassword變量就可以了。
而“同意協定”字段也同樣可以依靠純前端驗證。在法律上,我們隻要盡到了提醒和詢問的義務即可。如果使用者通過非正規手段繞過前端直接通路服務端,不能作為未曾同意協定的借口。
注意,前面我們隻是修改了html就已經完成了雙向綁定,我們并沒有在控制器中定義vm.form.email變量,甚至連vm.form對象都沒有定義。這是因為在angular中做了容錯處理,發現一個變量沒有定義時,它會自動幫我們定義一下,而不會觸發錯誤,這個特性在寫模闆時非常有用。
“圖形驗證碼”的字段也比較特殊,我們把它留到下一節去處理。現在,我們的界面就已經到了最初的可互動級别。
接下來我們有兩個分支可以走:套用bootstrap類進行初步美化(ux分支),或者開始實作表單送出代碼(程式員分支),兩者可以同時進行。
為了盡快看到“可行走骨架”,我們選擇程式員的分支:實作表單送出代碼。要在收集了表單資料的基礎上進一步實作表單送出,我們就要借助另一個指令了,這個指令叫作ng-submit。
直覺上,我們可能希望在送出按鈕上綁定一個事件來完成表單送出,但更好的方式是在form上綁定一個ng-submit事件。這是因為觸發表單送出并不是隻有點選“送出”按鈕這一種方式,使用者還可以在input中敲Enter鍵來直接送出表單,在大多數場景下,這是更為友好的方式。但更重要的是,這樣的html,其表意性更強一些。
這個步驟對代碼的影響很小,在html中,我們隻要把
改為`javascript
-submit="vm.submit(vm.form)">即可,在javascript中增加幾句指令即可:
vm.submit = function(form) {
};
module.exports = function (config) {
angular.module('com.ngnice.app').factory('reader', function readerfactory ($resource) {
});
angular.module('com.ngnice.app').controller('readercreatectrl', function readercreatectrl($resource) {
angular.module('com.ngnice.app').controller('readercreatectrl', function readercreatectrl(reader) {
reader.save(form,
);
{
}
$$validitystate: validitystate
$dirty: false
$error: object
$formatters: array[2]
$invalid: true
$isempty: function (value) {...
$modelvalue: undefined
$name: "email"
$parsers: array[2]
$pristine: true
$render: function () {...
$setpristine: function () {...
$setvalidity: function (validationerrorkey, isvalid) {...
$setviewvalue: function (value) {...
$valid: false
$viewchangelisteners: array[0]
$viewvalue: undefined
angular.module('com.ngnice.app').directive(
// 指令名稱,它會按照約定轉換成減号分隔的辨別符後才能在模闆中使用:<code>bf-field-error</code>,這裡的bf是book-forum的縮寫,用這個字首來防止和其他指令沖突。
'bffielderror', function bffielderror($compile) {
var hint = $compile('
{{name}}')(subscope);
{{name | error}}')(subscope);
angular.module('com.ngnice.app').filter('error', function () {
1)app/constants/errors.js:
angular.module('com.ngnice.app').constant('errors', {
2)app/filters/error.js:
angular.module('com.ngnice.app').filter('error', function (errors) {
angular.module('com.ngnice.app').directive('bfassertsameas', function bfassertsameas() {
...
angular.module('com.ngnice.app').directive('bfcaptcha', function bfcaptcha() {