问个样式方面的问题,我看了这个项目的源码,所有的组件自身的样式都太简单了,不具有代表性。如果项目在样式方面用了很多第三方的东西,比如 BS,Bourbon,Susy 这样的东西,并且组件本身的样式也很复杂,这样写在一个文件里的方式还足够方便吗?如果用了 BS 这样的框架,组件本身的样式怎么引用?特别是用到了 @extend
或 @include
这样的场景,还方便否?
可能是你的 pull 过程还没结束,应该是有冲突需要你解决。解决之后有可能需要 commit,也有可能需要 rebase continue,这取决于 default branch update type 是什么。如果你不确定,可以 git status 看看 git 怎么说,它会告诉你下一步需要你做什么。
你的代码在 stash 里,不担心会丢,但是不要轻易 pop 出来,否则会越来越乱,先把 pull 解决干净再说。
#4 楼 @dongli1985 基本如此。不过这个过程其实也可以反过来,在很多时候往往客户端开发者要比服务端开发者更理解 API 应该是什么样子的(两边的理解高度一致那最好,不过这是理想化的结果),所以你完全可以先从客户端开始,无非就是借助假数据(就好像后端测试用的 fixtures)先实现客户端的 UI 功能,然后以此为导向实现真正的 API,最后两边一对接就好了。这是一种可行的办法,而且挺适合两人组合干活的。
#2 楼 @dongli1985 我想请你回顾自己说的这句话:
不过 EmberJS 是一个 MVC 框架,里面也有自己的 model,那么它如何和后端 Rails 保持一致呢?不会要手动修改吧?
然后重新问问自己:为什么前端的 Model 要和后端保持一致(同步)呢?
之后推荐你看看这个视频:https://www.youtube.com/watch?v=gk-xyLM7R4g
Yehuda 用 Ember 仿做 Github(部分)的视频,在这段视频里 Yehuda 没有使用 Ember Data,也就不存在你原本顾虑的 Model 问题。然而从另外一个角度来讲,前端开发往往是对后端的 MVC 一无所知的(就好像你不是 Github 的员工,你怎么知道它后端的 MVC 细节如何?),我们能知道的最靠谱的东西就是 API 了。
所以你顾虑的问题是和具体的技术/框架(比如 Ember)无关的,它的终极命题是:客户端在不知道服务端具体实现的情况下,如何保持和服务端的业务模型/逻辑一致(同步)?
简单的答案是:客户端是不是 MVC 不重要,不是 MVC 也可以有 Data Model Layer,只不过它 mapping 的对象是 API 而不是服务端的 MVC 层。所以设计好你的 API,底层的实现你随意变不要紧,API 保持一致客户端就很 OK。
如果嫌快……这样真的好吗?觉得 Rails 酱很可怜兮兮的啊……
#24 楼 @iwege "你可以说 xxx”……这是一种行文手法而已,类似于英文里的 "You can say...",这里的“你”或者“You“都不是特指你,而是一种泛指。所以我说的并不是你……
关于 model,部分取代也不行,不是说你把数据接收到了然后可以在每一个组件的 local 使用,那么这个数据就是 model 或者部分是 model 了。仔细对比一下 Ember 接受数据也是靠 components 的 attrs,也是通过 yield 来做像 React 提供的 context 这样的事情,但这些都和 model 无关。Ember data 才是和 model 有关的部分,而这部分在 React 里是没有的,需要其他组件加入来支持,所以说 React 是 View Engine 或者 Components Layer 一点没错。
你不应该往 hbs 文件里写 script 标签,诚然 handlebars 编译的时候也可以去处理这种情况,但是没有必要,因为这种需求几乎就是废的……如果你真心想用 script 标签,写到 index.html 里。
最好还是想想为啥要写 script
View Engine 的说法是 React 自己强调的,在历届 React Conf 里都反复的被强调。你提到的那几点:
所以没必要觉得怪怪的吧,当然内行相互谈话说起 React 一般都是隐式的指代 React 及其生态圈相关,所以我们当然知道 data store 该怎么搞,知道怎么把 flux/redux/relay 这一系列串到一起,只不过这么一来其实和 Angular/Ember 等等也是半斤八两的,并不总是人们以为的那么轻量。
诚然,你可以说这样更灵活,选择更多,每一个独立的组件其质量都更有保障等等等等,然而无非就是另外一种选择罢了。就好像 JSX 到底好不好,最终只不过就是一种选择而已。
综上,尽管 Ember 没有 React/Angular 等等那么引人注目,但我这些年大家都用下来的感觉是,Ember 没有那么那么不好,其他的也没有那么那么好,现在对待它们的心态很平和,不管用哪个都能用到和用对它们的优势和特色,这才是最重要的吧。
同感,很不错,好用!
补充一点相关细节:
webpack 和 broccoli 其实不适合横向对比,webpack 不只是构建工具,它真正的诉求是模块加载方案,底下的构建只是必须要经历的过程;broccoli 则是专门的构建工具,从构建角度来讲 broccoli 和 gulp 不相上下(虽然流行度没法相比)甚至有些地方比 gulp 还出色。ember cli 是可以和 webpack 对比的,目前的情况,webpack 的尖端科技追的更紧一些,而 ember cli 则主要专注于为 ember 服务。作为一个套件,ember cli 采用了 broccoli 作为构建层,而且其实不用太担心 broccoli 的流行度,因为 broccoli 的作者 jo liss 也算是 ember 的核心圈子里的,其它核心开发者比如 rwj,steffan 等都是 broccoli 及其生态圈的维护者,他们似乎不怎么 care broccoli 流行与否,慢慢的有点 broccoli 专门为 ember cli 服务的意思了。broccoli-babel-transpiler 是我写的,很荣幸也在 ember cli 套件中,现在的主要维护已经是 ember core team 接手了
bower 之死其实早在预料之中,ember 社区已经开始把 bower 安装的依赖迁移到 npm 去了,也就是说现在用 bower 安装的依赖其实都可以用 npm 来安装(除了用户自己装的)。之所以现在版本的 ember cli 还有遗留,一是因为 npm 升级到 3 以后还有一些相关的 issue 没解决完,二是即便 bower 挂了,它这个生态系统还会留存一段时间,过渡的空间还是足够的
ember 不好学,虽然社区活跃度很高可是流行程度不如 react angular,作为比较了解 ember 的前端工程师,我不推荐初学者入坑。正如 eric 所说,ember 正在处于阵痛期,有很多设想和目标正在实现中,如果你没有足够的认知度和技术能力,入这个坑很辛苦的……
Angular 和 Ember 都参与到了 Type Script 的设计开发之中,Angular 是想用 Type Script 做其主力开发语言,而其中要用到的一些新特性(es next 范畴中的)比如 decorator 等就请了 yehuda 他们来一起研讨方案,yehuda 是 TC39 的委员会成员,对标准未来的走向把握性比较强。在最近一个视频里 yehuda 谈到了这段经历,表示很愉快。另外还有一段很愉快的经历是和 react 团队的交流合作,主要是为 glimmer 引擎去吸收想法和建议。最近油管上有个新频道:https://www.youtube.com/channel/UCJG0MvLP03kyzzAkD-w98aQ,这里面定期放出一些源码解读的访谈性视频(都很长很细,1 到 2 小时),目前出的前四期几乎全是关于 ember 的,yehuda 一个人就做了三期(有一期是 Rust & Ruby),还有一期是 rwj 担当。他们在视频里以 ember 的源码为切入点全面探讨了很多前端热门技术和架构思想的细节,也有很多前瞻性的思考以及和竞争对手的分析对比。值得看看。
我个人喜欢 ember 的原因其实蛮简单的,就是核心团队对待产品、社区、以及技术演进的态度,也可能是我参与比较多的缘故所以了解更深。
可能就是想太多了。
网络状况 hold 不住……
既然大家都提到了这个 ppt,我就先说几件和这个 ppt 有关的事情。这份 ppt 和最初的演讲已经过去三个月了吧,在这三个月里 Ember 的确践行了吸纳同行菁华的承诺,有一些改变确确实实的发生或正在发生着,当然也有一些原本就不错的地方得以继续保持并进一步改善:
一说 React,必提单向数据流。以至于给人一种错觉:Angular/Ember 等前辈就是只会双向绑定的,是 Evil 的。
这是一个误会。Angular 我不去说它太多,本身它就没有 Data Store 这层设计,Data Model 的主要任务还是在 http requests 这一块,一旦进入了 ViewModel 层,由于每一个 View 都和一个 $scope
耦合着,所以很难轻松的实现单向数据流——除非你不介意把数据统统都丢在 $rootScope
,然而那样的话性能就会很糟糕了。它这种设计就注定了把数据分散至各个独立的 ViewModel,然后分别去双向绑定是最自然的方式,硬要强求单向数据流也是难为人家。
而 Ember 则是另外一回事,Ember Data 提供了天然的优质 Data Store,借由 Router 提供数据向下逐层流动的入口,其整体架构其实一开始就很单向的,这一点我在一开始接触 Ember 的时候几乎完全没有意识到。有熟悉 Angular 的可能会问,像 ui.router 那样善用 resolve
钩子向 ViewModel 提供数据不也是一样的吗——这可是你自己举的例子哦(不久前我刚在这里讲过的例子)!嘿嘿,不一样哦:
// ui.router's resolve:
resolve: {
data(SomeModel) {
return SomeModel.someRequest(withSomeParams) // a promise will be return
}
}
// Ember Route's model hook:
model(params) {
return this.store.query('ModelName', params) // a promise will be return
}
上面的两个例子看起来像极了对吗?然而差别就在于:Angular 的 SomeModel
们是各自独立的,没有一个 single source of truth,如果它返回的数据在应用程序的任意层级被修改然后去 update/delete 等等都是很“正常”的事情——我指的是 Angular 开发者的编写习惯会认为这样做很正常——作为旁观者你永远也无法预知数据会在哪里/何时发生改变,甚至是否有改变?除非,你替 Angular 加一个数据层,帮它集中管理各种 Data Models 以及它们的状态;而 Ember 好就好在它有这么个 this.store
,也就是 Ember Data 了。对于路由来说,它根本不知道有没有/有哪些 Data Models,数据的进入是全权委托给 Ember Data 的,同样数据的状态和各种请求也是全权交给 Ember Data 去负责的。当进入到下层的 ViewModel 之后(以后可以认为没有 VM 了,全是 Components),没有任何“人”可以随意改变这些数据,也不应该随意发起 update/delete 等请求,只需要借由 actions 来通知 Ember Data 代理这些事宜即可。
所以哪怕是看起来极像的代码,你仔细一琢磨却发现其中内涵大有不同。Angular 那种的充满了不可预知性,数据层操作和视图层操作很容易耦合在一起(因为每个 Model 自己管理自己的行为,更改其一并不能影响其他);Ember 这种则是有一个很明显的分离,即使将来有一天你不想用 Ember Data 了(换用 POJOs?)你也无需担心大量的代码改动,顶多去实现一个 this.store
这样的接口就好。
这一切只是要说明一件事情:从一个应用的整体来看,其实单向数据流也是 Ember 的天然特质;只是在 React 明确这一概念之前,大家都没有意识到罢了。
不过在那个 ppt 里也提到,尽管很多人认为“双向绑定是罪恶之源”,但也要看到它是有自己的用武之地的。对于 webapp 来说,任何地方坚持单向数据流都好,唯独表单这个东西还真是需要双向绑定。
以下是一个类似 TODO App 在最新版的 React 里的简单实现(不借助双向绑定):https://gist.github.com/nightire/1f29fe5747eca3f07fe9
然后是一个同样的应用,换成了默认双向绑定的 Ember 简单实现(没写成 Components):https://gist.github.com/nightire/7b1be58719446614b907,并且可以在线预览(也是 Ember 社区提供的工具):https://ember-twiddle.com/7b1be58719446614b907
你可以轻易的比较出哪一个写起来更简单一些,即便是把后者再组件化也增加不了几行代码的。非常多的应用,特别是企业内部办公/管理等应用都高度的依赖表单操作,在那些应用场景下双向绑定并不是什么罪恶,只要你理解它并且善加使用。
Ember 社区也同样意识到,滥用双向绑定会造成很多不良后果,所以未来的 Ember 将会默认使用单向数据绑定(我指的是在模版上绑定数据的写法默认是单向的)。当开发者确实知道自己在做什么的时候,他可以选择开启双向绑定。这是一个相应的例子: https://gist.github.com/nightire/e8febb30359c4c2da2fa 和在线预览:https://ember-twiddle.com/e8febb30359c4c2da2fa
后面的例子演示了在 Ember 中封装组件之后的样子,依然很简洁。同时演示了:1)显式声明的 mutable property binding(双向绑定);2)closure action(闭包动作)
第一点是 Ember 的 Observable Object 天然支持的,只是为了避免滥用双向绑定所以在模板语法上“故意”给开发者设了一道“坎”;第二点则应该是向 React 取来的经了,尽管 React 并没有 closure action 这个名词,但是类似于这样的声明:
<ChannelForm addChannel={ this.addChannel.bind(this) } />
和这样的调用:
this.props.addChannel(this.state.channelName)
几乎就是 closure action 的模仿对象,Ember 借助了 HTMLBars helper,更进一步的省去了 .bind(this)
这个尾巴。再下一步,当 angle-brackets components 正式登场后,仅从写法而言两者之间还有多大区别?更不要说 Ember 是模版与脚本分离写的,React 解释 JSX 的时候反复强调是为了对 designer 友好,可终究不如分离的模版来的痛快不是?
当然 React 也是可以双向绑定的,不过你得启用它的 addon 包,本质上它是一个 mixin,但在 Ember 里则是底层的 Object System 所提供的 Observable 特性——一旦浏览器支持了原生的 Object.observe
,Ember 在其中扮演的“二传手”的角色就可以彻底跑龙套去了(同时会减少代码实现的依赖,变得更轻量)。一个做加法,一个做减法,殊途同归,只是到那时 React 是否会大大方方的接纳双向绑定特性呢?
说到 Observable,Ember 里现在必要的 get()ter/set()ter
本质上就是为了这个,这玩意儿我也烦,只是我知道它们会“死去”的,时间问题。ppt 里也提到了,开发者们似乎更喜欢 React 里显式的 setState()
,其实这就是为了通知受状态变化影响的视图去更新自己(无论是 Virtual DOM 还是 Glimmer 都是如此),原生环境的现状就是如此,大家其实都是半斤八两。Angular 倒是看起来干净了,可一代目里 $scope
的连带问题被吐槽也不见得就少了,忍忍,再忍忍,至少这几年的进步还是让人看得到希望的。Yehuda Katz 是 TC39 的常委之一,这也是让大家信任 Ember 的未来的重要原因。
以上,仅仅是最近一段时间 Ember 的许多进步之中的一小部分而已,更多的东西我自己也还在消化和实践之中。总之组件化的大势对于 UI 编程来说是不可避免的,大家其实都在往这个方向努力着。然而大规模的应用程序开发又不仅仅只是 UI 编程而已,所以不必因为一时一处的井喷式进步就觉得“一药治百病”了,要成为更好的开发者,我们就必须看得更远、更广。
bourbon 是好东西,不过你那个和 bootstrap 比较的例子其实没什么说服力,本来 bootstrap 就不应该这样用的。那些暴露出来的 class helpers 只适合用于创建 rapid prototype,正儿八经去写的时候也应该用语义化的 HTML+CSS Classnames,然后使用 bs 的 mixins 把那些 helpers 封装起来。所以本质上它们并没有太大的不同,只是 bourbon 没有去考虑做 rapid prototype 的场景,所以它只提供 mixins 而不提供 class helpers,于是就逼迫开发者正儿八经的干每一件事情(这当然是好事)。
这一点很难说是 bs 有意为之,还是忽略了对一般开发者的提点,然而我确实看到文档中有这样的描述:
You can use predefined grid classes or Sass mixins for more semantic markup.
而且很多国外的作品和 themes 也都是这样去实践的,可为什么在国内就只知道你例子中所体现的写法呢?
我只能猜测大家都不看文档的,另外就是 bs 不去强调这一点也分半口锅吧。
再过一段时间,我预期大概两三个星期后能得到一段空闲的时间,然后我要谈谈复杂组件的设计(因为我最近做了一个很复杂的组件)和 DDAU 在其中的实践,接着还想谈谈 Data Store。
React 的确很好,这一点无需否认;然而如果某项技术你只能听到一片叫好却听不到有人说它的缺点,那就一定有什么问题了;而且我看到很多盲目跟风的人甚至都不知道 React 只是一个 view engine 而不是 frameworks,这就有点让人无语了。这说明支持和称赞 React 的人群里有很大的水分。
而 Ember 自进入视野开始就不断的备受质疑,从来没有被交口称赞的时候。可正是这种“特质”一直吸引着我,让我觉得它很真实,即使在我的工作环境不允许使用它的过去几年,我也没有放弃对它的兴趣。现在我终于可以用它做点真实的事情了,于是我对它的认识越来越深。就我所看到的(国内的)对 Ember 的绝大多数评价,我认为几乎都在瞎编乱造,要么就是在炒一两年前的冷饭。有些属于 Ember 的缺点早已不复存在却被反复用来吐槽(比如说渲染性能,Glimmer 实际表现比 React 还好那么一丢丢,这有多少人知道?);而那些 Ember 的闪光点却甚少有人知道,比如说 React 带起的 DDAU,大家可知道 DDAU 得以实现或存在最基本的前提是什么吗?是 Data Store:the only source of truth。而目前最好的 Data Store 实现之一就是 Ember Data,它的出现远比 DDAU 这个概念还要来得早,如果你真的能玩儿转 Ember Data,然后再回头看看 Flux/Redux 等解决方案所“提供”的 Data Store,就知道简直是小儿科。(提供打引号,是因为像 Redux 那样的轻量级实现,不会去做一个“超级数据仓库”的,他们只会强调所谓 Data Store 就是一个 Single and maybe huge Plain JavaScript Object。
我不是说 React/Flux/Redux 它们不好,但是它们主攻的方向和目标是与 Ember 这样的框架很不同的,根本就不适合拿来相互比较。我几乎尝试过所有新鲜出炉的框架,那些标榜轻量级/简单易用的框架就的确只适合做轻量级的应用程序,反之亦然——你当然可以拿它们来做复杂的应用,但前提是你有那个能力去拼装组合;比方说 Ember Data 也是可以配合 Redux 来用的,这样由 Redux 控制单向数据流,Ember Data 提供一个高级数据仓库也是很不错的事情(但是 Ember 没必要这样做,因为 Redux 能做到的 Ember 只会做到更多,只需要使用者改变一些思路和理念即可)
我也不是说 Ember 就已经很好了,实际上它现存的问题也不少,不过在你发表评论之前是否应该先对其有个足够的了解呢?比如说 @aidewoode 提到的“最大的问题是还不是 routable component”,的确,routable component 是下一个 big thing for ember,但就目前而言还真算不上最大的问题。因为目前(假设你一直在用最新的 stable 版本)controller 的唯二作用就是:1)接收 model hook 返回的 model 并暴露给路由入口的 template 使用;2)query params 的声明和部分操作;除此二者之外(特别是第一点还是隐式完成的,不需要你写什么)剩下的事情都可以交给 routes 或 normal components 各负其责。我最近的项目里,controller 的代码少得可怜,而 template 唯一的功能就是做各种 components 的容器,这感觉已经和 react 没有什么差别了。
更甚至,如果你现在就把散落在路由入口模版里的各种 components 用一个大的 component 包起来,那你几乎可以说这个就是 routable component 了,底层的差异不必太在意,就应用程序的结构来说,这就是一两个月后的 ember app,所以这真的不算什么最大的问题,只是看你如何理解这种变化的态势了。
什么是真正的问题?很多。有人提到了文档的事情,和最近半年 Ember 的演变相比,文档的滞后的确很严重。幸运的是 Core Team 已经意识到了,并且对未来一到两个小版本的发布目标进行了调整:完善文档是第一要务,我看到了一些对于官网和 guides/api 的新设计,用不了多久就要改头换面了。
还有就是解决复杂组件结构相互通信,共享状态的事情。DDAU 只是一个纲领性的方向,但它并没有解决很多细节的实现问题,何时/怎样向下或者向上?何时/怎样适合单向或者双向?如果你确实碰到需要数据向上/动作向下的场景却依然想要坚持 DDAU 的原则你该如何去做?框架又提供了哪些机制帮助你这样去做?我用 React 做过一些试验,其中一些场景它能做的也并不很令人满意,然而对于 Ember 这些也是一样的问题。
Contextual Components / Closure Actions / Kebab Actions ... 这些才是 Ember 和它的社区正在面对和交付解决方案的问题,这些东西(或许)才是回答上述问题的确切的具体的答案。在这里就不得不提一下 Ember 社区的 RFCs,正是这样的群策性政策让真正的 Ember 用户得以保持信心。RFC 是一个独立的 repo,它所做的就是让用户提交自己在开发过程中遇到的各种痛点(甚至不需要是 Ember 的用户),然后 Ember 的核心团队以及其他经验丰富的架构师会针对这些问题提出 proposal,阐述问题的根源/解决方案的思路/在 Ember 中的具体实现/以及可能存在的 drawbacks 等等。这些 RFCs 会完全公开它的演进过程,每一个人可以参与其中提出自己的观点和想法,大家一起来完善和修正它们,直到它成为 Ember 正式的一部分。
这是我在其他前端框架社区里没有看到的东西,这也是我会对 Ember 保持信心的根源,从实际问题出发,到提供确切解决方案为止,不回避问题,坦承过去的失误,积极探索未来的可能性,不拒绝来自其他社区的 good ideas,把形而上的理念具体化……我认为这是所有终端开发者都希望看到的框架。
每次涉及到这类话题我就会打好多字,这种现象其实我自己也是够了……然而如果每一个人在发表见解之前能对目标有一定的了解和分析,我也就无需写这些了。如果你想要赞美谁,先想想它还有什么缺陷,以免被反对者立靶子;如果你想要批评谁,先想想它有什么优点是别人做不到/没做到的,以免被支持者点名打脸。如果人人都能这样去想和做,整个社区里会少很多“xxx 完爆所有 n 条街”,或者“xxx 弱爆了”这样了无聊论点——不过追根究底,硬要去比较两者的行为才是最愚蠢的,决定你选择的不是他人的主观评价,而是你所选择的东西正在做什么以及将要去做什么。
去了解,不要去比较。一定要比较的话,也是先了解之后再比较。
#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
,前面讲过的。关于 routable component 的细节,可以看一下这个 RFC:https://github.com/ef4/rfcs/blob/routeable-components/active/0000-routeable-components.md
另外我并没有说 route 比 controller 重要,这篇文章也没打算说明这一点。路由与控制器是两个层级的概念,谈不上谁比谁更重要。不过就如你所说目前松散的结构的确可以变得紧凑一些,这种变化其实来自于 component 的完善。最终 component 就等于过去的 controller + view & template,而 routable component 是可以用来转递初始 state 的 component(非常粗略的说),这样一来 states(一般就是 data)通过路由获取之后,不需要 controller 来“保持”它们,而是直接交给 routable component,有此开始逐层渲染下面的 component 最终形成完整的应用。一旦路由变更,自然就有另一个 routable component 接手,逐层渲染……大概是这样子的,毕竟还没有正式 landing 的 feature,所以我说的可能会有不准确的地方。不过归根结底 controller 可以不是必须的。
顺便多说几句(但是因为 routable component 没有 landing,所以仅供闲聊参考):
去掉 controller 这一层会有很多好处,也能让整个 Ember 变得更容易理解,举例来说——
以前,路由自己会调用 render
方法,你也可以改写它。render
做的事情比较多,就像这样:
this.render('post', {
view: 'special-post',
controller: 'other-controller' ,
model: myPost,
into: 'application',
outlet: 'sidebar',
})
所以,一个路由(对应着 app 的一种状态)至少得需要 controller w/ model + view & template w/ outlet(s) 才算是完整的一块。这当然说得通也合乎情理,但是这种分散的结构也带来很多问题,比如说对初学者不友好,比如说相互通信和依赖很复杂等等。
早先出炉的 basic component 把 view & template 取代了(部分 controller 的职责也包括在内),后面准备出炉的 routable component 以及 aync component(专为数据交互设计)等于是要把 route 的一部分 hooks 和 controller 合并掉。还拿 render
为例,以后 route 调用 render
的签名就成了:
this.render('post', attributes)
或者是
this.render('post', attributes, { into: 'application', outlet: 'sidebar' })
'post'
就是一个 routable component,其是就是 PostRoute 和 对应的 component,attributes
是传递给 routable component 的初始“状态”,一般来说就是以前的 model
hook。一旦把 attributes
传给了 component,接下来就是 parent component & children components 们的事儿了,不需要 controller,data persistence and communication 有 store,IOC 有 services,完了。真没 controller 什么事儿了。
into
和 outlet
还有必要存在,但是现在不需要单独的 application-template(它就是最顶层的 routable component 的一部分),outlet(s) 自然也直接写进 component 里面了。
如此一来,以前的 beforeModel
afterModel
setupController
还有 this.controllerFor
等等方法都不再需要,整个 route 变得清爽明确,对初学者会友好的多。这些细节在我前面提到的 RFC 里都有具体的实现思想描述,我这里算是粗略的概述一下。
不错哦,没用过 alioss,下次试试你的 addon。
ember-data 是客户端的 data store,实际上它和你怎么与 server 通信并没有直接的关系,只是它自带的 adapter 实现了对应的接口,底层还是 ajax,该怎么 handle server error 是你的事情。客户端的数据与服务端不一致可以通过 serializer 层来转换。
bower 和 npm 的问题是前端生态圈的历史遗留问题,并非 ember-cli 非要这么选择,其他的 build system 也有类似的情况,比如 yeoman。
组件间的通信看 component 的文档就是了,怎么叫没有体现?包括 DDAU 也是可以的,ember 并不限制你如何通信。
graphql,socket.io 这些东西本质上就是第三方库,ember 的核心团队不会对此有什么特别“看法”,ember 提供的 service 机制可以让你把任何第三方库整合进你的 ember app 里,你也可以选择直接使用它们,并不会因为你用了 ember(或其他什么框架)这些东西就变了。当然,社区会有人做一些 addon 把常用的第三方库封装好以便快速使用,这就好像 gems 一样。你可以看看这里:http://www.emberaddons.com/