在過去的幾個月裡,我一直遨遊於Angular的世界。如今回想起來,很難想像在沒有類似於Angular.js, Backbone.js以及其夥伴Underscore.js這些資料綁定框架下我每天如何去編寫一個大型前端應用。我不敢相信我已經用它們完成了那件工作。
可能我有點小偏見,但考慮到我一直在做的應用是在瀏覽器中實現Photoshop類型的編輯器,它呈現相同的數據有幾種完全不一樣的方式。
如果不是像Augular的框架,這種種類的互動、資料連結和視圖同步很容易變成一個持續的惡夢。有能力修正一個地方的模型和用Augular修正所有相關的視圖聽起來幾乎像在騙人。新增、消除或改動一個層次只是一個改變物件的問題。層次,x =10,完成。並沒有地方需要手動作廢棄視圖、手工修改在DOM的層次中的每一個實例,甚至是因為這個問題而去與DOM互動。
Augular讓我們可以去到我們從未想過的地方,像設定一串使我們能夠在現有的環境下做出申請的鍵盤捷徑。舉個例子,文件編輯捷徑(像?B:用於切換黑體文本)只是使我們能夠編輯一個文件層面。
同樣地,我們為這些快捷鍵附加了一個描述(透過一個我們創建的服務進行註冊),然後我們可以顯示一個快捷鍵的列表,同時還有它們的描述,在一個便利條上。此外,我們寫了一個指令使得我們可以將單獨的DOM元素與它們的快捷鍵綁在一起,當你的滑鼠在元素上停留一會,會出現一個提示,讓你知道此時可用的快捷鍵。
老實說,這就好像我們已經不是在寫一個web應用程式。 web只是媒介。當我們增進了我們對Angular的理解後,程式碼變得更加模組化,更加獨立,並且更加連接互動。它很自然地變得更Angular了。
然後透過Augular,我的意思是在Augular背後的那些高度互動的豐富的應用開發哲學。 javascript,一個讓我們能夠開發那些一段時間前我們還覺得不可能的一部分軟體的相似的東西。
我們甚至有能力去開發一個成熟的用於修改DOM變成歷史中現在選中的點的歷史控制板,並讓它工作得很好。至少可以這麼說,當你興奮的返回歷史控制板查看那些與Augular能力相關的數據在你的視圖工作中完美的更新每一個微小的細節。
那並不總是容易的,基本程式碼總是變成一場無可控制的混亂。
的確,在過去幾週我們一直在更新並且將我們的前端整個架構重寫。在我們開始重新編寫以前,看看自從0.10.6以來,將Angular更新得有優勢的過程。如果你看了變更日誌,你就知道這是一個相當長的過程。
在這個重構的過程裡,我們從以錯誤的方法對待Angular,轉變為以Angular的方式對待Angular。
在我們的案例中,錯誤的方法包含了許多的問題,我們不得不在此時,在使我們的程式碼基礎到達可愛狀態之前,解決它們。
在全域作用域宣告控制器(Controllers)
這是一個 Angular 初學者容易做的例子。如果你熟悉 Angular,你也會熟悉這種模式。
// winds up on window.LoginCtrl ... var LoginCtrl = function ($scope, dep1, dep2) { // scope defaults }; LoginCtrl.prototype.resetPassword = function () { // reset password button click handler }; // more on this one later LoginCtrl.$inject = ['$scope', dep1', 'dep2'];
這段程式碼沒有包含在閉包中,或者說,所有的聲明都在根作用域,全域的 window 物件上,混蛋啊。用正宗的 Angular 方式來寫的話是使用它提供的模組 api ( module API)。但如你所見,即使是文件和建議步驟任然過時地建議你使用全域作用域:
這樣做,極棒的事情將會出現。
// A Controller for your app var XmplController = function($scope, greeter, user) { $scope.greeting = greeter.greet(user.name); }
-- Angular.js文档
使用模块(modules)允许我们以下面的方式重写控制器(controllers):
angular.module('myApp').controller('loginCtrl', [ '$scope', 'dep1', 'dep2', function ($scope, dep1, dep2) { 'use strict'; // scope defaults $scope.resetPassword = function () { // reset password button click handler }; } ]);
我发现使用 Angular 控制器的漂亮做法是你必须在所有地方使用控制器方法(controller function),因为你需要控器的依赖注入,而且控制器提供了新的作用域,绑定我们从需求到封装我们所有的脚本文件成为自调用函数表达式( self-invoking function expressions),像这样 (function(){})()。
依赖$injection
在最早的例子中你可能已经注意到了, 依赖是使用$inject注入的. 另一方面,大部份的模块API, 允许你传入一个函数作为参数, 或者一个包含了依赖的数组作为参数, 其后面跟着一个依赖于这些依赖的函数. 这是在Angular中我不喜欢的一点 , 但这应该是它文档的过错. 在文档中的大部份例子认为你并不需要一个数组形式的参数; 但现实是,你是需要的。 如果你在使用一个压缩器压缩你的代码之前, 没有运行ngmin , 事情将会变得糟糕.
由于你没有使用数组格式['$scope',...]明确声明你的依赖包,你看上去简洁的方法参数将会被缩略成类似于b,c,d,e的样子,有效地扼杀了Angular的依赖注入能力。我认为他们构建框架的思路存在了重大的失误,这与我在非常不喜欢 Require.js 和他们麻烦的 AMD 模块最后的推论是相似的。
如果他不能在产品中使用,它还有什么用?
我的这种态度是因为你在产品中所使用的框架里,有一部分代码是已经写死了的。这对于开发中经常用到、产品中偶尔用到的实用工具,诸如控制台和错误报告,是很好的。如果语法上的甜头(可读性)只用在开发中,就会变得没有任何意义。
这些破事让我很愤怒, 现在发泄完了. 谈谈$符吧...
减少 jQuery扩散
深入的讲, 这个应用是 "类Angular程序", 也就是说它只是包裹于Angular之中, 大多数DOM 交互是经由jQuery处理的, 这给Angular带来相当多的争论。
如果今天我要从头开始写一款Angular.js应用,我不会立即包含进jQuery。我会强迫自己使用 angular.element 来代替。
如果jQuery存在的话,angular.element这个API将包装它,同时它给Angular团队实现 jQuery的API提供了可以替代的选择,名为jqLite。这并不是说 jQuery不好,或者说我们需要另一个某种实现,来映射它们的API。只是因为使用jQuery显得不是那么有Angular的思想。
让我们来看一个具体的,愚蠢的,例子。在controller被声明的地方,它使用jQuery来做元素之上的类操作。
div.foo(ng-controller='fooCtrl') angular.module('foo').controller('fooCtrl', function ($scope) { $('.foo').addClass('foo-init'); $scope.$watch('something', function () { $('.foo').toggleClass('foo-something-else'); }); });
然而,我们可以用我们期望的方法来使用Angular,替代之。
angular.module('foo').controller('fooCtrl', function ($scope, $element) { $element.addClass('foo-init'); $scope.$watch('something', function () { $element.toggleClass('foo-something-else'); }); });
最后一行你不能直接,或者通过jQuery来操作DOM(改变属性,增添事件监听器)。你应该使用指令来替代。那篇文章很棒,去读读看。
如果你仍然jQuery化了,有许多文章可以一读,例如这篇迁移指南,还有我的关于怎样使用jQuery的批判性思考 这篇文章。
我不是要声明我们准备完全移除 jQuery 。我们有其他更重要的目标,例如,发布我们的产品。这个时候,删除 jQuery 的依赖还是很有意义的。这样做能够使我们的控制器得到简化,我们创建处理 DOM 的指令,使用 angular.element 即使它实际上映射着 jQuery 。
我们依赖着有点恶心的 jQuery UI,我们当然不只是为了它的对话框而使用它,它还有很多用途。例如,拖动一个列表项然后把它放到一个已排序的列表中,如果不使用 jQuery UI,这将牵涉到一大堆代码。因此,实际上,对于 jQuery UI 来说,并没有真正很好的替代品。拖拽的功能可以通过一个轻量级的拖拽库 angular-dragon-drop 来替代,但是,对于元素排序插件,还是得依赖 jQuery UI 。
管理代码库
还有一个我们在迁移中需要解决的问题是整个代码库都挤在一个单一的大文件中。这个文件包含了所有控制器、所有服务、所有指令以及每个控制器的特定代码。我指出一点使得我们可以准确地把每个文件只包含一个组件。目前,我们有很少的文件,却包含了不知一个组件。大多数是因为一个指令使用一个服务来与外界共享数据。
尽管和 Angular 无关,我们还是把我们的 CSS 样式表(stylesheet)模块化。我们为每个组件中使用的 CSS 类名前面都加上了两个字的前缀。例如, .pn- 作为前缀,代表面板(panel); .ly- 前缀,代表着图层(layer)等等。这样做的直接好处就是,你不需要再费劲地想哪个组件的 CSS 类是怎样的了。因为你已经为它们设置了命名空间,你就很少会重复用到某一个 CSS 类名了。另一个好处就是减少了嵌套,我们以前曾经用 #layoutEditor div.layer .handle div 这样复杂的选择器表达式,而现在,我们只需要 .ly-handle-content 就可以了。深度的嵌套现在只发生在额外的选择器覆盖上,例如 .foobar[disabled]:hover,或者,最坏的情况下,像 .foo-bar .br-baz 。
下面是一些我们定下的 CSS 类命名规则:
在实现了这套面向组件的 CSS 声明方法后,我又想了很久“the class soup way”。
Angular 强制你写好的代码,但是更深一层说,它强制你去思考。一会儿后,它就像一个服务器端的实现,或者成为一个不堪忍受的“黑客大会”。这些都取决于你这么选择。
接近完美
让我们来解析一下我们应用程序的各部件的其中之一,层。
div.cv-layer( ng-repeat="layer in page.layers | reverse", ap-layer, ng-mousedown="selectLayer(layer.id)", ng-mouseup="selectLayer(layer.id)", ng-dblclick="doubleClickLayer(layer)", ng-hide="layer.invisible" )
這裡,我們使用了cv-layer類,也就是說這個元素是canvas組件的一部分(canvas指的使我們繪製層的地方,不要和HTML5canvas混淆)。然後,我們 在foreach類似的循環裡面 使用ngRepeat標籤來為每一個層的建立一個相似的元素。並且透過一個我們所寫的反向的filter來傳遞,所以,最後一個層位於最上部,而且對用戶可見。 apLayer標籤,其實是為了繪製層的任務所採用的,不論是一個圖片,或者是某些文字,HTML,或別的東西。 event標籤(ng-mousedown, ng-mouseup, ng-dblclick) 只是簡單的為事件做代理而用,這些事件將被我們的層選擇服務來處理。最後,ngHide這個標籤,我想就不必多言了吧。
這麼一大堆功能(譯者註:有點誇張了),而Angular成功的使它看上去如此簡單,用可讀的HTML,從某種程度上就告訴了你它們是怎麼回事。更多的是,它使得你可以分解開不同的需要考慮的問題,從而你能夠寫出來簡潔的程式碼,不需要一次把所有的事情都考慮在內。簡言之,它降低了複雜度(譯者註:其實Angular本身就很複雜,呵呵),讓複雜變的簡單。而讓“難以簡單度量的問題”,變的可能。
我期待不久會有更多關於Angular程式碼的文章。特別是,我樂於探討一些在升級我的程式碼的時候,所遇到的一些邊緣的案例,如何解決其中的問題,同時讓其餘的部分同樣運作。