在过去的几个月里,我一直遨游于Angular的世界。如今回想起来,很难想象在没有类似于Angular.js, Backbone.js以及其伙伴Underscore.js这些数据绑定框架下我每天如何去编写一个大型前端应用。我不敢相信我已经用它们完成了那件工作。
可能我有点小偏见,但考虑到我一直在做的应用是在浏览器中实现Photoshop类型的编辑器,它呈现相同的数据有几种完全不一样的方式。
- 图层以图形化的形式呈现,占据了屏幕的大部份。它们列于一个面板内,你可以删除它们。
- 当你选中一个图层时,它的边缘会被虚线包围,同时会高亮显示于列表中。
- 类似地,图层在面板中的尺寸和它们的大小这些属性取决于画布。
- 我上面提到过的面板是可以拖拽,折叠,关闭的。
如果不是一个像Augular的框架,这一种类的互动、数据连接和视图同步很容易变成一个持续的噩梦。有能力修正一个地方的模型和用Augular修正所有相关的视图听起来几乎像在骗人。添加、消除或者改动一个层次只是一个改变对象的问题。层次,x+=10,完成。并没有地方需要手动作废视图、手工地修改在DOM的层次中的每一个实例,甚至是因为这个问题而去与DOM互动。
Augular使我们可以去到我们从未想过的地方,像设置一串使我们能够在现有的环境下做出申请的键盘捷径。举个例子,文件编辑捷径(像?B:用于切换黑体文本)只是使我们能够编辑一个文件层面。
同样地,我们为这些快捷键附加了一个描述(通过一个我们创建的服务进行注册),然后我们可以显示一个快捷键的列表,同时还有它们的描述,在一个便利条上。此外,我们写了一个指令使得我们可以将单独的DOM元素与它们的快捷键绑定在一起,当你的鼠标在元素上停留一会,会出现一个提示,让你知道此时可用的快捷键。
- Angular可以使我们做到我们做梦也想不到的事情。
老实说,这就好像我们已经不是在编写一个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'];</div>
这段代码没有包含在闭包中,或者说,所有的声明都在根作用域,全局的 window 对象上,混蛋啊。用正宗的 Angular 方式来写的话是使用它提供的模块 api ( module API)。但是如你所见,即使是文档和建议步骤任然过时地建议你使用全局作用域:
这样做,极棒的事情将出现。
// A Controller for your app var XmplController = function($scope, greeter, user) { $scope.greeting = greeter.greet(user.name); }</div>
-- 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 }; } ]);</div>
我发现使用 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'); }); });</div>
然而,我们可以用我们期望的方法来使用Angular,替代之。
angular.module('foo').controller('fooCtrl', function ($scope, $element) { $element.addClass('foo-init'); $scope.$watch('something', function () { $element.toggleClass('foo-something-else'); }); });</div>
最后一行你不能直接,或者通过jQuery来操作DOM(改变属性,增添事件监听器)。你应该使用指令来替代。那篇文章很棒,去读读看。
如果你仍然jQuery化了,有许多文章可以一读,例如这篇迁移指南,还有我的关于怎样使用jQuery的批判性思考 这篇文章。
我不是要声明我们准备完