最近在学习 AngularJS,真是无力吐槽…… 感觉 js 真的很罗索,一层一层,我尤其总是搞不懂各种 return 之后还有 return,各种名字各种长,还有层层的{});……
他们说 AngularJS 只要思路理解了,其实也不难…… 大概我还在看 codeschool 的水平,总觉套各种 directive 写起来还不如用普通的 html,快在哪里?controller 啊 routes 啊,各种罗索……
#1 楼 @dandananddada 其实我不太想回复这个问题,因为这是逼着你写长篇的节奏,而且容易被撕逼……先占个位吧,看工作时间安排。
我已经对 Angular 吐过很多槽了,但那也是在拥有过 4 年实际开发经验,确切了解过它的好与坏的前提之下才能做出的反应。这一次我不想吐槽了,我也不想为 Angular 到底是好还是不好去做争辩,就只以我的观点和见识来谈谈 Angular 都做了些什么事情,好在哪里不好又在哪里,如果不用 Angular(及类似框架)的话可以怎么做。我没有要说教或布道的意愿,我会努力减少主观上的评价——但是我也没那么多闲工夫去找散落于 internet 各处的引用,判断留给看官自己。
如果你对 SPA 到底是怎样一种存在没有根底的话,我建议你还是先读一读:Single Page Apps in Depth。虽说这本小书对 SPA 的阐述还不够全面且一些内容已然落后于 2015 年前端发展的现状,但作为基础的理论性科普还是很有价值的(特别是在 View 和 Data Model 方面)。
之所以要先推介这本书,是因为我接下来要谈的东西都是基于 SPA 这个大前提下的。尽管 Angular 等并非只能做 SPA,然而离开了这个前提就一定会让你觉得里面有些东西是废物,是冗余,是复杂,是脱了裤子放屁多此一举。所以如果你一定要拿嵌套在经典后端 MVC 框架里去使用 SPA 框架的经验与感受来抬扛,那我只能说:何苦为难自己,你不用不就完了?
还有一个深层次的原因,由于全栈型工程师的激增(且不论全栈的 level 要如何界定),有一些人觉得前端所做的事情“不过如此”——我这里并没有看不起谁的意思,只是如果你能近距离审视他们所做过的前端工作,也的确能得出“不过如此”的结论,所以我其实是非常理解的——然而 UI 编程的复杂性并非如他们所想的那么简单。过去,以 DOM 为中心的 UI 编程占据前端开发的主流,而现在这个天平正在逐渐倾斜,另一端可以称之为“以数据为中心的 UI 编程”,这种变化正是因为 UI 编程的复杂性在前端领域已经逼近了一个瓶颈。如果你的日常工作不足以让你深刻体会到这一点,那么你没有必要去讨论 Angular 的好与坏,因为“杀鸡焉用牛刀,何谈牛刀锋利与否”?同样的,还有一些前端开发可能更看重别的领域:比如超快速响应的即时数据传输(即时通讯,在线协作)或者离线式数据管理与操作(应对环境变化的移动式应用)或者即时图形处理与数据同步(游戏),那么在他们眼里所看到的 Angular 自然也会有别的样子。所以,在你对我的描述表示赞同或否认之前,请先想想你究竟是做什么的,因为这世界上不存在完美和万能的解决方案。
模块化在 ES2015 尘埃落定之前一直是 JavaScript in browsers 的痛:没有标准的模块化方案就没有标准化的代码组织、管理(特别是依赖关系)、分发,尤其是代码组织更是身为“框架”所要承担的基本职责之一。于是 Angular 不能免俗的造了一个轮子 angular.module
,而且还是一个偷工减料的轮子——公平地说,不是他们没能力或是懒得做,而是因为:
所以,angular.module
仅仅就是一个命名空间(同时包含了 controller/filter/service/config/run... 等机制的 hooks,后面再讲),足以省却 IIFE 的使用但不会提供 async loader 等特性,如果你要更多可以选择 require.js 或者许多现存的 shims(比如 system.js & es6 module loader,详见我挖了却没填完的坑)和一些后起之秀的工具(比如 jspm / browserify / webpack 等)。
模块化机制是必要的(即使你不用 Angular),angular.module
做的不够好也是情有可原的,这毕竟是语言及其运行环境层面上的锅,时间会消除这个痛点。
依赖注入则是 Angular 内部必须的机制,与模块层面上的依赖管理不同,它的 level 略低一层但却没有继承那样强的耦合作用。或许今天我们可以说 Mixin 的解决方案更清爽一些,但在实际使用之中依赖注入依然有它的用武之地。尽管 Angular 在框架层面没有提供继承和 Mixin 的实现机制,可由于 JavaScript 本身就是 OO 语言,开发者自行实现也不难(扩展阅读:Learning JavaScript Module Patterns),唯独 DI 略有难度,因其本质上是 JavaScript 里的元编程,在语言更完备之前需要开发者有相当的经验和一些 hacks 技巧,所以 Angular 实现了一套。以今天的观点来看,实现的有点难看,但好在有辅助工具可用。
Tips:在阅读下文之前先得理解一件事情,在 Angular 中,几乎所有的内部组件都是 services(除了像
$injector
这样极个别的特例),而所有的 services 归根结底都是由$provider
扩展出来的——这就是为什么你在文档中看到的大多数$xxx
都会有对应的$xxxProvider
的缘故。但要命的是:Angular 有一个 service 的名字就叫$service
,它也是$provider
的一种“变体”(针对特定需要封装的语法糖),所以经常搞的初学者头昏脑胀,“此 service 非彼$service
“的误会坑了不少人。我不清楚当初设计 API 的时候是怎么想的,大概是词穷了?反正到最后官方文档里特意为此道了歉,然恶果已生徒呼奈何……作为补救,后文会用service(s)
和$service
来区分二者,不带$
的一概是泛指
什么是依赖注入?几乎所有的 Angular Services 在定义工厂函数的时候都可以用特殊的语法把其他的 services 引用到自己的作用域内,最典型的例子如:
angular.module('demo', []).controller('SomeController', ['$http', function SomeController($http) {
$http.get('...').then(function(response) {
// do anything with async response...
})
}])
这是大多数初学者最开始碰到的问题:SomeController
如何知道 $http
可用?难道 $http
是全局的?
当然不是,答案就是 DI,Dependencies Injection。那个数组里面写一个工厂函数的古怪语法已经饱受批评,我告诉你为何。每一个 service 都有一个字符串形式的名字,Angular 内置的 service 名字开头都有 $
,如果两个 $$
代表它是 private(还是语言自身不够明了的锅)。DI 有一个用于提取 services 实例的 $injector
,当你定义的 service 实例化的时候,$injector
会去 $provider
里面找你需要的其他 services ——所以你要给它个名字。技术上讲,工厂函数里的形参足矣,但坑爹的是代码压缩工具会把形式参数压缩成尽可能少的随机字符($injector
旁白:臣妾做不到啊~),所以 DI 要求你显式提供字符串形式的 service name。如果你提供了,后面的形式参数可以随便写,只要顺序一致即可;如果你不提供,那就不要压缩代码(目前的现实条件不允许你不压缩,HTTP2 或许会改变这一现状,但那时 Angular 2 应该代替他爹了)。
鱼与熊掌如何得兼?ngAnnotate,请把它加入你的构建环节并且放在代码压缩之前。
Angular 就是这样帮你组织代码的,由于 DI 可以极度松散的特性,所以 Angular 对于项目架构如何安排并没有官方性的指导——这到底是好是坏?如果你是极度自由主义的程序员,十有八九会认为这挺不错;如果你是明白“自由无绝对”,更崇尚井然有序的“强迫症患者”,大概会有心怀不满吧——别丧气,Ember 欢迎您~
或许是由于 angular.module
的“天残”特质,官方并没有详尽地给出使用它的最佳实践准则,以至于在现实中“一个 Module 统束一个 App”的做法屡见不鲜,可笑的是这样做的人恰恰最喜欢抱怨“directives 复用性不佳”——你就定义一个 Module 何来的复用性?每次复用一整个 App?请多去看看那些开源 directives 的源代码,也不用深究实现原理,看明白人家是怎么实现复用的先吧。
路由对于 SPA 的作用和意义我不知道说过多少遍了,在这里不再赘述,换个角度说点别的。之前带实习生的时候,对于 Angular 我讲的最多的就是路由,而且特别强调了一个真实的案例:如何把一个 400+ 行的 controller 瘦身成 90 行,在这个案例里,对于路由的恰当使用是达成目的的关键之处。源代码是公司私有的,我不便照搬,只捡其中关键之处来说说对于路由的一些使用。
先说为何初始版本写了 400 多行?(相信我这不算多的,喜欢堆积代码的程序员比比皆是)因为那个页面是一个复杂性较高的 crud 场景,增删改也就罢了,我把它们在数据层面封装了 $resource
减掉了少量冗余,最繁琐的地方是在“查”上面。
查虽然只有一字,但在应用程序设计上往往是最费心思的(你看看各种电商页面上眼花缭乱的搜索/过滤/排序机制就明白了)。搜索、过滤、排序,此三者是“查”这个动作的经典 UI 体现,很多应用程序被认为难用的毛病往往就出自这里。原始的版本仅在这三样功能上就写了 400 多行里的三分有二,可以理解的,因为条件多,还有依赖关系,复杂性确实高。可问题在于原始版本还在用老套的思维在写这些逻辑,除了省去了对 DOM 的直接操作,看起来和 jQuery 的写法殊途同归。更致命的是:组合条件所产生的结果并没有相应的“映射体现”,你无法还原(比如前进后退),无法复制和重现(比如分享链接地址给别人直接看到一样的结果),验收测试很困难(不得不模拟所有可能的用户交互操作,因为每一个条件都是靠绑定一个属性去获取的——这也是代码变长的根源)。
为了让初学者容易理解,我先用传统的实现方式描述其中一个场景:假设有订单列表页面,要按若干条件筛选列表结果,这些条件分别是:全部、预售订购、直接订购、团体订购、未支付、已支付、退单、废单……你可以继续想象。在 UI 设计上这些条件未必是同级的,但它们是可以组合的。我们可以先把它们简化的想象成一堆“开关”式的按钮,每一个按钮对应一个查询条件,每点击一次都会发出新的请求(让我们假设频繁请求是可接受的,客户认为全选完了再提交很啰嗦)。你如何去做(用 jQuery)?
用选择符去找每一个按钮当然太 low,我们可以把所有按钮放在一个 wrapper 里然后用事件委托来获取它的——好比说 data-query-type
属性
这个属性的值是类布尔值,由于 HTML 属性只能是字符串,所以用 "0"/"1"
或 "t"/"f"
这样的东东来表示选中与否——当然你可以用 checkbox,样式上或许要下一些功夫
为了在 UI 上表征 toggle 的状态,配合 data-query-type
还得 toggleClass
——当然你可以使用 attribute level selector,如果你会的话
你需要一个数据结构去保存所有条件的当前状态,因为每次点击都要请求,而请求参数是复合的——单纯的 array 可能不够,因为你要去重——或许你可以用 ES2015 的 Set
,这样到可以省却类布尔形式的属性值,只记属性的名字就好(Set
里的值必须唯一,所以不会有重复值,切换存在与否即可,但是要想想如何与 UI 上的状态同步)
请求可以封装成一个函数调用,每次点击后传当前的 paramsSet
进去即可——当然,你得做数据结构转换,视 API 调用的类型而定
返回的结果 JSON.parse
后传给 compiled template function,将生成的 DOM 替换
完了……吗?别忘了这只是筛选,还有多条件搜索和排序等着你
那个 400 多行的 controller 和上述思路类似,唯一的差别就是把所有和 DOM 相关的部分用双向绑定取代罢了。
这就是一大票 Angular Developers 日常反复做的事情,毫无疑问 Angular 是垃圾,这么用要不算垃圾才怪了!
够啰嗦了,现在我们轻装上阵看看路由的正确使用姿势(以下代码使用了 ui.router,ngRoute 不好用——然后我惊喜的发现 ui.router 的文档竟然更新了……):
提前声明:本文内的代码都是虚构的例子手写的,完全没有语法检查或测试,如果你发现错误——不许嘲笑!快快 @nightire 让我更正~
angular.module('demo', ['ui.router'])
.config(OrdersRoute)
.service('Order', OrderModel)
.controller('OrdersController', OrdersController) // 1
function OrdersRoute($stateProvider) {
$stateProvider.state('orders', {
url: '/orders?filters', // 2
params: {
filters: { array: true, value: 'all', squash: true } // 3
},
data: {
filters: [ // 4
{ name: '全部', value: 'all' },
{ name: '预售购买', value: 'preorder' },
// 后略
]
},
resolve: { // 5
initialRequest: function(Order, $stateParams) {
return Order.query($stateParams).$promise
}
},
reloadOnSearch: true, // 6
templateUrl: 'app/orders/index', // 7
controller: 'OrdersController as vm', // 8
})
}
function OrdersModel() { /* 待续 */ }
function OrdersController() { /* 待续 */ }
我按照注释里的序号逐行解释以上代码:
Module 级别依赖注入 'ui.router' 组件;另外演示了使用命名函数把各 services 拆开写的办法,由此,你可以把它们分别写到单独的文件里去最后加以合并,也可以很方便的加上其他的模块化组件,如 require.js 等;
路由在 url
上的模式,举例:http://my-angular-app.com/orders?filters=preorder&filters=paid
,这样的 url
映射出当前是 预售订购+已支付
的复合筛选条件;它很容易还原(router 会记住它及其前后路由,通过 push/pop states 或 History API——这也是 turbolinks 的实现原理,Rails App 如何不刷新页面,嗯哼?),它很容易复制/重现(你可以随意分享 url——当然它得是公开的,或者对方可以登录且不受限),它很容易测试(你不用模拟用户点击,因为 ui.router 可以保证状态的映射——只要你信得过它,如果它出错了,恭喜你!下一个 open source contribution 即将诞生);
filters
这个 url params 的定义:它是一个数组,默认值是 all
(全部),如果是默认值,url 上不显示出来(为了干净);
data
向 OrdersController
提供了筛选按钮们的数据结构形态——这是为方便你构建视图层,手写很累的(后面会讲到用法);
resolve
是为路由激活时提供异步数据请求的 hook,为什么要用这个?因为:一旦进入 Controller,就意味着视图已经渲染了——Controller 提供了 ViewModel,也就是 $scope
对象——很多时候,我们需要初始数据先于视图渲染发生,然后 resolve 到 ViewModel 实例化的阶段,如果你觉得难以理解,请对比一下把这个请求放在 Controller 里初始化的效果,总有一天你会体悟到的!顺便一提,server render app 不存在这个考量因为浏览器拿到页面的时候请求已经完成,但 SPA 获取视图的速度通常都比请求数据要快得多,特别是模版被 pre-render 之后;此外,使用 resolve 还有一个好处就是你不必在 Controller 那里考虑列表数据的更新问题,路由变化就会自动更新数据——在这里你要考量的是应用程序的状态变化会带动什么,而不是去做什么才能改变应用程序的状态;至于 initialRequest
,现在简单的说就是返回一个 promise 给 Controller,它的实现细节涉及到 Data Model 那一层,我们留待后面讲;
前面提到路由更新就会带动 resolve hook 的执行,但有时候不想要频繁请求怎么办?(特别是只有 params 更新的时候,因为有的 params 只代表 UI 交互的变化,和数据请求没有关系)reloadOnSearch
就是控制这个的上层开关,当它为 false
(默认值)时,params 的变化不会引起 resolving;不过 $state
service 还能提供更细粒度的控制,详情见 ui.router 的文档;
模版的指向,这个很简单。对于初学者而言,你需要了解一下模版的预编译是怎么做的,因为在上例中我实际上填写的是一个 $templateCache
的名字,而不是物理的 HTML 文件;
对应的 Controller 是谁,此处使用了 "controller as namespace" 的语法,这是 Angular 曾经有失考量的设计所带来的“补丁式解决方案”,详情请看 $controller
的文档。扼要盖之:$scope
s 是一颗树,其根节点叫做 $rootScope
,以下的每个子节点都是 $rootScope
创建出来的,它们和 controllers 一一绑定(其实不仅仅是 controllers,还包括 new scoped directives,另外还有一些封闭的 isolated scope……directives 的锅咱们后面分),为了避免属性命名冲突所以允许你用此语法为 $scope
的实例绑定一个命名空间,等价于 $scope.namespace.xxx
的手动补丁,但应用了元编程技巧使得在 Controller 内部,this
终于不再是废物一个(把 this
指向 $scope.namespace
,这个 namespace
就是 Controller 的实例,后面会演示)。
我是真的很想少罗嗦一些,不过这些是 Angular,不!是 SPA 的精华之一,其他的 SPA 框架或许用了不同的架构和方法,但做不到这些就没法开发 SPA 应用。Angular 的确不是必须的,也不是最好的(在这个层面上,ui.router 脚踩 ngRoute 就是明证),我必须要说:这是初学者最难迈过的一道坎儿,可一旦明白过来了,你对 SPA 的理解会升华的。
Router 就像交警根据红绿灯(应用状态)指挥交通,从哪儿来往哪儿去就听他的;而一旦确认了目的地,接下来的事情就要交给 Controller 了。Controller 到底做了什么?在 Angular 的世界里,Controller 为模版提供了视图模型(MVVM 后面的 VM 就是指代 ViewModel),模版与视图模型的合体就是 View 了(当然不能忘了其中还有 directives,不过后面再说)。
Rails 里难道不是如此?否则 View 如何拿到 Controller 里的对象?只不过 Rails 直接把 Controller 的实例对象暴露给 View(我不是 Rails 专家,如有误但请指教),没有所谓的 ViewModel 罢了。那么为什么 Angular 要搞一个 ViewModel 而不是直接用 Controller 的实例对象呢?
原因就是 Angular 从一开始就当卖点的“数据双向绑定”。JavaScript 自身并没有提供数据绑定的机制(ES5 的 getter/setter 勉强算一个,但局限性很大;ES6 的 Proxy 可用,ES7 maybe 的 Observer 可用,可这都是 Angular 实现之前没影的事儿),所以 Angular 得想个法子实现这个,他们采用了 dirty checking 的方案(延伸阅读),有利有弊吧,反正那时候能抓住老鼠的猫不多,结果最重要。
我们在 Controller 里离不开的 $scope
就是 dirty checking 方案的实际履行者,这也是为什么要把它暴露给模版去实现视图 <=> 数据双向绑定的原因。以今天的观念来看,双向绑定并不是万能的“灵丹妙药”,如今 flux 引领的 DDAU 正大行其道,而 Ember 实现了 Glimmer 引擎之后更是发扬光大并让其与双向绑定“融洽相处”(可能也会让“选择性困难”患者头疼不已吧?),Angular 2 会掀起新浪潮吗?大家等着看戏呗。
剩下的事情就是把业务逻辑在 Controller 里来实现,Controller 大概是使用 DI 最频繁的地方,究其根本就是业务逻辑几乎全堆在这里,为了抽象和复用 DI 自然是第一选择。前面那个例子到了这里要怎么发展呢?
function OrdersController(
Order,
initialRequest,
$state,
$stateParams
) {
var vm = this
initialRequest.then(function(response) {
vm.orders = _.map(response, function(order) {
return new Order(order) // 1
})
})
vm.filters = $state.data.filters // 2
vm.isActive = function(name) { // 3
return _.includes($stateParams.filters, name)
}
vm.applyFilters = function(name) { // 4
vm.isActive(name) ? _.pull($stateParams.filters, name) : $stateParams.filters.push(name)
$state.reload($state.current, $stateParams)
}
}
另外,还得把模版也放出来才好逐行解释:
<div class="filters">
<button type="button" class="filter-button"
ng-click="vm.applyFilters(filter.value)"
ng-class="{'active': vm.isActive(filter.value) }"
ng-repeat="filter in vm.filters track by $index">
{{filter.name}}
</button>
</div>
路由 resolve 过来的 promise 在这里赋给了 vm.orders
,也就是订单列表的数据来源。但是为什么要 new Order(order)
——这还是得留着 Data Model 再说不迟。在现实中 map
做的事情其实是封装在 OrderModel
里而不是写在 OrdersController
里的,类似于 vm.orders = Order.createCollection(response)
这样——此处仅为解释用;
路由的 data
所提供的数据赋给了 vm.filters
用来渲染所有的过滤器。你可以用别的方式来提供,比如说 angular.module.value
或者 angular.module.constant
,甚至可以写死在 OrdersController
里;
这是我们用来绑定 UI 状态的 predicate,ng-class
在渲染时会自动调用它从而决定要不要应用这个 css class;
这是我们响应用户交互行为的方法,最终就是改变 $stateParams
的值,然后 reload 当前状态并传递新的 $stateParams
给路由。此后,路由会自动同步 url,记住状态变更,重新 resolve(因为 reloadOnSearch
让它这么做;如果 reloadOnSearch = false
,你也可以在此处传第三个 options
参数来强制路由去 resolve)……以上的过程重演一遍,唯一的区别是:应用程序的状态更新了(数据+视图)
还有一个很常见的问题,经常有人问:在 Angular 里 Controllers 之间如何互相通信?基本上最后能给出的答案都是 services 然后 DI,或者是事件机制。
Services 固然是充当“中间人”的合适角色,但如果这种通信会涉及到数据变化,那么你还得让 Services 自身能够管理这些数据,包括增删改查、缓存等等。有的时候这些数据就在 Controller 里,你搭这个桥难免会破坏“双向绑定”(除非你把整个 $scope
对象丢来丢去,实际上 Directives 里面去 require Controllers 的时候就是在传递 $scope
对象的引用)。
事件机制也是同样的道理啊,通信如果产生了数据负载就得顾及数据本身是 POJO 还是 scoped object。
分析一下,Controllers 相互通信等价于 ViewModels 相互通信(这其实非常合乎情理,做这种相互通信的目的就是为了让不同的 ViewModel 可以同步指定的数据,否则的话没必要在 Controllers 里做这种通信——这也暗示了一个让 Controllers 保持“干净”的原则:但凡不涉及 ViewModel 的东东都可以考虑从 Controller 里抽出去),产生问题的根源在于你有一份数据(比如一个用户)但是却复制给了两个不同的 $scope
(比如一个是详情页面的 VM,一个是编辑页面的 VM,由于 UI 上的设计让两个视图出现在一起了),于是你没办法让它们同步了。
其实前面我已经谈到了 $scope
们就是一颗树上的节点(们),所以任意两个子节点一定都能找到一个共同的父节点——最差的情况也不过就是 $rootScope
而已啦。所以本质上两个 $scope
之间天然就可以通信(这里排除掉 isolated scopes,属于 Directives 的特例),只是看你把原始数据放在哪一层的父节点上罢了(这是根据 UI 的信息结构来决定的)。比如说有很多 Angular 应用在登录之后都会把 currentUser
保存在 $rootScope
下,这样便可以随取随用(DI 可直接获取到 $rootScope
,当它是一个特别的 service 好了),只不过 $rootScope
太重的话是要付出性能损耗的代价的,因而频繁使用 $rootScope
来做事件通信也不可取(有些人喜欢把大量 event handlers 注册在 $rootScope
上,因为它在最顶上,只要不阻止事件冒泡它总能捕获到)。所以你需要一些预先设计,仔细考虑应用的场景,合理安排 $scope
们的层级。这也是 controller as
补丁大法的诞生原因——除了 $rootScope
,别的 $scope
没有 unique name,所以没法 DI。这个补丁允许你安排设置 $scope
下的 namespace,于是可以通过视图层的 $scope
层级关系进行传递和通信。
这个方法固然取巧(可还是补丁呀),但是它也体现出 Angular 早期设计上的缺陷,即:一些关键性的机制不太透明,里面弯弯绕绕有点多,有点繁。搞懂了会觉得也就那么回事,可是还没搞懂的人真是要被折腾死了。你可以观察一下程序员问答社区,关于 Angular 的问题来来回回就是那么几个:scope/service/directive,一旦爬过这几座山就没什么好问的了,瞬间醍醐灌顶一通百通的感觉有木有?以前 Angular 的程序员会嘲讽 Ember 的学习曲线太陡峭,要我说,Angular 是不陡——最下面和最上面几乎都是水平的,唯独中间一截子都快 90 度了吧!这就是为啥总有人说一旦你理解了 Angular 就不难的缘故。
现在请你想想 DDAU,那不就是所有的数据都只存在于一个 Data Store 中(好比那个
$rootScope
),数据可以向下传递,但你不可以在下层直接修改它(Immutable Data),所以它也不会产生很多$scope
——因为不需要双向绑定;状态变更要向上发出 actions(事件冒泡分发),于是真正变化的数据只有 Data Store 里的 "Only Truth",接着 Components 重新渲染,React 在渲染时去 diff VirtualDOM,然后 update once for all;Glimmer(Ember)再进一步直接 diff & update 数据了(Tom Dale 在前几天一个 fullstack 什么会议上讲了一些细节,还没顾上仔细琢磨)。所以不是 Angular 不能这么玩,只是$rootScope
不适合做 Data Store,还有就是数据同步+视图渲染的机制落后了,这些都要等 Angular 2 来解决了。
回到前面埋的两个小坑,现在我来说一下 OrderModel
是干嘛的。
一直以来,前端心中的数据模型就是 JSON.parse(response)
后得到的 Plain Object(s),其实它们顶多称得上是数据实体,但和模型还相去甚远。想想看 Backend Model 吧,拿 Rails 来对比至少还得有两个机制:
如何定义数据实体中的业务逻辑?比如说 validations。讲究一点的前端程序员不会直接用 JSON.parse
来做数据转换,而是封装成 Model 其目的就是为了拿到像在 Rails 里 @user
@posts
这样的对象,它们知道如何处理自身相关的逻辑,比如 @user#greeting
之类的;
如何与数据源做关系映射?比如说 Rails Model 实际上是一个 ORM,它知道如何取出集合,如何保存/更新/删除实例等等。虽然前端不会和数据库直接沟通(但也不绝对哦)但 API 就是我们的数据源,JSON.parse
出来的对象可不懂这种映射,你需要单独做请求然后把对象(的属性)当做 request body 传进去。
不要轻视这种差异,处理得当的话会有很大的变化。
Angular 的生态圈里有一些现成的 Data Model 模块可以拿来用,比如说 restangular,如果你很幸运后端的小伙伴把 RESTful API 处理的妥妥的,那么你来直接用吧。然而或许有人得不幸面对那些没头没脑的 legacy api,或许有人需要更轻量化的选择……等等,总之到了一定阶段你总是会考虑如何自己造个轮子玩玩。
那么接下来就以前例留下的 OrderModel
为例简单说说可以做些什么(以下代码都是 demo,和上面的例子毫无关系):
function OrderModel(API, $resource) {
this.url = API + '/your/api/endpoint/to/orders/if/restful/enough/:id'
this.params = { id: '@id' }
this.actions = {}
this.actions.search = {
method: 'POST',
url: API + '/not/very/restful/ordersList',
isArray: true
}
var Model = $resource.call(this, this.url, this.params, this.actions)
Model.createCollection = function(data) {
// this is how class method been defined
}
Object.defineProperties(Model.prototype, {
instanceMethod: {
enumerable: true,
get: function() {
// whatever model instance needs to get value
},
set: function(value) {
this.instanceProperty = value
// or whatever model instance needs to set value
// return model instance is always a good idea, e.g: methods chaining
return this
}
}
})
return Model
}
我就不一句一句分析了,毕竟学到这个份上的已经不算是初学者了吧?简单概括几个重点:
本质上,上面那一堆就是返回了一个 Order
数据模型的构造器,只不过在实现了基本的 Model 之外,它继承了 $resource
服务所能提供的一切——针对 REST API 的异步调用,其底层是对 $http
的封装——这么做的好处是,你不需要显式去调用 $http
service,特别是对那些没谱的 API services,光是记住和管理那些莫名其妙的 url endpoint 就够烦人的了
最开始的 url
params
actions
在 $resource
的文档里有详尽的描述,但是文档组织的不好,有很多细节要去 $http
里找。基本上你掌握的顺序应该是 $http
-> $resource
actions
hash 里定义的种种方法既可以作为静态方法调用,也可以作为实例方法调用,这是 $resource
为你处理好的细节,唯一的区别是:当作为实例方法调用的时候,方法前面要加 $
,比如 anOrder.$save()
actions
里的方法远不止上面所示的用法,它其实是 $http
service 里对于 config 的 definition。举例来说,如果 API 返回的数据很奇葩(命名或是结构等),你可以利用 transformer 进行数据层面的 serialization(Ember 用户们:这里就是 Adapter 和 Serializer 在 Angular 中的体现)
此外,要记住上述这些方法根本上都是对于 $http
的封装,所以返回的总是 promise,请善加利用而不总是抱怨 JavaScript 嵌套多,异步编程也是可以玩得很潇洒的
如果对 Object.defineProperties
这样的东东不熟,说明你需要补补 JavaScript 的基本功,请查阅 ES5 的新增 API,它们中有很多都是实现 JavaScript 元编程的基石
最后回答前面 Controller 那里提到的问题,为什么要 new Order(order)
?看到这里相信你也明白了,这就是把 JSON.parse
返回的 POJO 变成自体完备的 model instance 的过程而已。至此,你不再需要手动去处理数据实体的属性挑拣,API 调用传参(即使 API service 让你很无语,你也有办法预先做好适配而不是用到的时候才骂骂咧咧),属性校验(是的,当你新建的时候可以 new Model({这里面可以是缺省属性值})
,当你更新的时候视图返回给你的对象就是 model instance,它们自己就拥有校验自己的能力)等等。Even Better,你可以使用第三方的 ORM 库来处理在客户端的模型关系!
别问我后端有了 Model 干嘛前端还要 Model,每次谈到 Data Model 都有这样的问题出来实在让人受不了,我认为你需要想想自己的思维是不是有着很大的局限性?如果一定要回答,以后我的答案总是:可以不要,统统交给后端吧。
到这里可以先做一个小结了,Angular 到底好不好?我真的真的是毫不带偏见的说一句:好,但却不够好。纵然 directives filters 等其他 services 都还没谈,但只看目前已经讲到的几个部分吧(5 分制):
controller as
的语法布丁严格来说不是控制器的锅。4 分;$scope.apply
)竟使得用户不去了解 $scope
的实现机制就没法好好做朋友了……Angular 当初拿一堆双向绑定的简单 Demo 做宣传可真是“居心险恶”呀,容易上手?呵呵~ 2 分;$resource
不算难用,但官方是不是高看了前端的整体水平?也不搞个教程教一下大家怎么玩儿!3 分;综合评价就先跳过,心中估算了一下:加上还没讲到的部分,最后得分也就在 3 分上下。但是——以 Angular 诞生时的环境和它对整个前端开发所起到的推动作用来看,这个 3 分真的不算低了,这可是以今天的见识在评价好几年前的作品啊!所以我说它好,只是到了今天已不足够好罢了,然而各位看官需要扪心自问:就算众口一辞都说它是垃圾吧,你对它的了解又能有几分?嘴巴是别人的,见识是自己的。
看来今晚是写不完了,来到新公司比较繁忙白天不好偷懒,有兴趣往后看的恐怕要等周日了。虽然我说我不太想回复,其实就是怕麻烦偷懒,后来想想反正也暂别 Angular 了,索性就当一次回顾总结吧……所以我会写完它。
此坑就填到这里吧,本来还想着把其他的 services,比如 filters directives 等等也谈一谈,可后来一想也没什么好说的,都是看文档就能学会的东西,实操的经验和官方的说明也没有太大的区别,就到此为止吧。
又及:刚才粗略回看了一遍,忽然想起一个事,$stateParams.filters
这个数组里,'all'
的出现应该是排它的——也就是 'all'
等于剩下所有的条件之和。因此:
'all'
就不管其他条件,但 UI 视图上那些按钮的状态却是有瑕疵的;$scope.$watch
一下这个数组,用来处理这一 edge case;ng-class
能否保持后续的双向绑定,毕竟 $stateParams
是一个 POJO……八成不行。不过没关系,你可以提前这样做:vm.params = _.clone($stateParams)
,把 $stateParams
克隆一份丢到 $scope
对象上去,这可以保证双向绑定——记住 vm
其实就是 $scope.vm
,前面讲过的。为什么要这样,我觉得就是因为通用性的问题。 我可以服务器只提供数据接口,而不需要管展现方式,甚至可以几个不同的服务器来 host 不同的页面展现,但是核心的数据是一致的。
AngularJs 可以让我们放心的做数据绑定,在前端做数据的展现。 前端样式和 html 完成后,将数据塞进去就可以了。完全不需要改 API 服务器。
否则,你为了改页面就需要改数据的提取,耦合太狠。谨记解耦。
确实比较麻烦,小项目还是别用了。。。最近在看 backbone,不过比较起来还是感觉 Angular 舒服些。。。现在不是好多走 JS 解决方案的,MEAN,A 就是 Angular。
在各种小外包和小项目里用 angular 一年了,也用过 ember、react,给我的感觉就是 angular 上手很快,想清楚有哪些部分要用 angular 进行处理,哪些不用,然后再选择用那些 angular 的模块来实现,这个过程是很快的,弹性很大。
就我自己的实践经验来看,大多数项目使用 controller 这一层就够了,route 多用于 spa,并不用纠结分别的写法…directive 这些,可以理解成 rails 里的 helper,比较典型的例子就是基于 angular 写的 ionic 框架,基于 directive 实现的各个组件,然后就能体会到了 directive 的优势…
答案很简单,Angular 1 不好,不要再学了,浪费时间。 Angular 2 没用过,不评价。 如果是初学,直接上 React,你的视野会一下子扩大很多,以后的路越走越宽。
#21 楼 @cassiuschen 其实我正是在学 ionic,才要先学的 AngularJS. 听说年底 Angular 2.0 和 Ionic 2.0 都会出来,不知道到时会不会清爽一点……
我觉得 Angular 是成也双向绑定,败也双向绑定。 双向绑定好处不多说了,大家都知道,写代码确实可以方便很多。 坏处就是整个页面的加载逻辑不再是“面向过程”的了,更接近一个反馈环。一些页面元素依赖另一些页面元素,并在不断调整平衡。页面并没有一个执行完 js 达到稳定的状态,而是不停地检查各个变量之间的关系。我们在用的时候确实为此碰到了一些很麻烦的问题,主要是第三方服务方面的。
另外我个人觉得 Angular 在前端再建立一整套 MVC 并没有什么必要。听说 React 很不错,但是我们现在用着 Angular 了,换起来比较麻烦。
看项目吧。 小项目怎么来怎么好。快速就可以 但是对 1 个逐渐庞大的项目 如何平稳的增加功能。如何保持代码的条理性。复杂度增长的曲线很陡峭的。
前端各种框架目的就是为了解决这个问题。。降低复杂度随项目增长而带来的陡峭曲线增长。
没用过除 angular 之外的 framework. 要点 @nightire 都讲了。router 真的是最关键的部分。如果觉得 js 是垃圾语言的话,试试看 typescript,至少心情会舒服点。$resource 以及替代的 restangular 都不顶用,restmod 算是勉勉强强。等 restmod 都见肘的时候可以自己用 $http 写个数据封装。这样以后即使不用 angular 了也能照样拿来在其他 framework 下用。比如我最近给新出的 jsonapi.org 的第一版 json 协议 写的封装 json-model 。没多少代码,算是小广告下。 (赶进度所以代码质量堪忧,谨慎使用)
angular 说难不难,对后端出来的人来说,能忽略 js 很多啃爹的地方很快弄出个 demo。 但是出来混迟早要还的。后期越用越多坑,最终还是得把 js 的屎吃干净。等你屎吃饱后,用啥 framework 都不是很重要了。
设计思路的确很好,网站上说 学习路径比较陡啊。学会了,就好了。不过最近 angular2.0 出来了,非正式版,代码风格大变,思路也靠近 reactjs。