-
Notifications
You must be signed in to change notification settings - Fork 81
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Angular1.x + ES6 开发风格指南 #34
Comments
非常棒的思路呢,我们在使用中只是使用了部分ES6的功能来简化代码,连import之类的都没用上。 另外,我们当前也处在比较纠结的一个阶段,暂定目标是先搞一个基于ng2的东西出来,之前做的东西再慢慢迁移。 请问,你们针对angular2怎么看,有打算升级么? |
@hstarorg 我们暂时是没打算升级的,主要考虑的还是一个成本问题,毕竟ng2的新的语法跟typescript不是每个人都能快速掌握的。 ng2带来的一些东西还是很值得研究的,比如rx跟zone,不过对于我们公司这样一个团队(angular上普遍比较熟)直接切ng2成本还是有点高,相反我可能更倾向于vue2。 |
@kuitos 我们也需要升级成本(学习成本,修改成本)的问题,但是又没啥好的办法。现在连使用ng1都还是老旧的语法,这个团队多了也很难处理。 我仅仅了解vue1,比较有特色的双向绑定,暂时对vue2不了解。看样子得抽时间看看了。 抛开基础框架不说,ng2比ng1简单,但是搭框架反而更复杂了。 |
很赞! 个人也是觉得民工叔在定义 directive 的时候采用 class 的方式不如博主所用的对象字面量的方式来的直观 请教一个问题 @kuitos , 虽然我也同意博主所说的尽可能把 model 做的框架无关, 利于迁移. 但是如果很长一段时间框架基本不会迁移, 是不是还是直接采用 DI 的形式好一些. 毕竟博主所呈现出来的代码书写风格已经很不像 angular 了. 不知道这个转变对于团队来说成本有多大? |
关于DI的问题,我文中这样描述的
假设这样一段代码 DI(使用自制的Inject decorator):@Inject('ServiceA', 'ServiceB')
class Controller{
getName(){
return this._ServiceA + this._ServiceB;
}
} module importimport ServiceA from './ServiceA';
import ServiceB from './ServiceB';
class Controller{
getName(){
return this._ServiceA + this._ServiceB;
}
} 哪种维护起来成本更高显而易见吧,DI的方式你几乎不可能知道ServiceA跟ServiceB在哪里定义的。 你说到的迁移,我一开始确实也是从这一点出发的,给他设计了一个可能迁移上层框架的‘伪需求’。但是后来思想有所转变,就是不论框架是否可能迁移,M/VM层做成框架无关都是架构上确实存在的需求。
各层解耦独立是核心诉求,便于架构迁移只是这件事带来的‘副作用’。 另外,这年头还不用ES6写代码,就真的太土了啊!😂 |
好的! 让我先消化消化..... 过段时间我再来向您请教! |
@xiaoyu2er 团队切换代价这个事情没有那么大,总的来说,就是把es5的controller, service之类换成了es6的class形势,其他地方都是一些配置性的变更 |
兄弟劝你一句,别升级瞎整了.angular2 和 an1是断层发展的, angular1 谷歌是不是维护都难说. 你还整合 es6 . 给自己找事. 想用 es6 试着切到 react 或者 vue. react 最佳. |
@hcforbaidu 不是每个项目都可以从零开始的 国内外用 ng1 的公司还是很多的 |
@xufei !! 恩 自己还在摸索一套规范 好让同事可以比较不那么纠结的过渡过来, 大体上是 参考了博主和民工叔的风格. 谢谢! |
@xiaoyu2er 我的个人看法. 因为之前确实搭建了一套基于 angular1的二次开发方案,用于快速开发. an1确实有他方便的地方. 但是后来遇到太多太多坑. an1的思想和做法目前来看值得商榷的地方太多太多. 谷歌也明白这个问题,所以an2和1断层发展. 所以对 ng1的项目而言. 我们的思路是维护可用就好.对 an1的项目空费心思划不来.仅是个人看法. |
@xiaoyu2er 前阵子看到了一个post:https://github.com/toddmotto/angular-styleguide 基本想法是一样的,不过这个作者写的要细节多了😂 虽然有些地方我不是很同意(DI/Service/Filter等几块),不过还是推荐你看看 |
@kuitos 好的! 稍微看了一下 看完交流! |
@kuitos 先问个问题, template 你是放在 bundle 里还是? |
@xiaoyu2er 组件模板 import 过来变成字符串模板一起打包,业务模板(通常是路由,可以理解成container) import 过来是文件路径,跟 templateUrl 关联。基于 webpack loader |
大神,ng-include 用ES6 该如何实现呢? |
import includingTpl from './include.html';
class Controller {
includingTpl = includingTpl
} <ng-include src="$ctrl.includingTpl"></ng-include> @Seven4X 是这个意思么? |
@kuitos 可以,可以 这操作 666 不愧是大神 |
还有一个疑惑请教大神,在使用ui-router的时候,如何不通过$scope访问父节点的数据呢? |
不赞同部分思想,选择了框架就要用好框架,而不是在框架的基础上再大量的去框架,如DI部分。 赞同部分思想,如组件中 index.js 作为框架语法包装器;业务逻辑采用原生ES;数据层、业务模型能脱离 View 独立测试。 |
Angular1.x + ES6 开发风格指南
阅读本文之前,请确保自己已经读过民工叔的这篇blog
Angular 1.x和ES6的结合
大概年初开始在我的忽悠下我厂启动了Angular1.x + ES6的切换准备工作,第一个试点项目是公司内部的组件库(另有seed项目)。目前已经实施了三个多月,期间也包括一些其它新开产品的试点。中间也经历的一些痛苦及反复(组件库代码经历过几次调整,现在还在重构ing),总结了一些经验分享给大家。(实际上民工叔的文章中提到了大部分实践指南,我这里尝试作一定整理及补充,包括一些自己的思考及解决方案)
开始之前务必再次明确一件事情,就是我们使用ES6来开发Angular1.x的目的。总结一下大概三点:
其中第1点是技术投资需要,第2、3点是架构需要。
我们先来看看要达到这些要求,具体要如何一步步实现。
Module
在ES6 module的帮助下,ng的模块机制就变成了纯粹的迎合框架的语法了。
实践准则就是:
example:
通过这种方式,无论被依赖的模块的模块名怎么改变都不会对其他模块造成影响。
Controller
ng1.2版本开始提供了一个controllerAs语法,自此Controller终于能变成一个纯净的ViewModel(视图模型)了,而不是像之前一样混入过多的$scope痕迹(供angular框架使用)。
example
这种方式写controller等同于ES5中这样去写:
不过ES6的class语法糖会让整个过程更自然,再加上ES6 Module提供的模块化机制,业务逻辑会变得更清晰独立。
Component(Directive)
以datepicker组件为例
注意,这里我们先写的controller而不是指令的link/compile方法,原因在于一个数据驱动的组件体系下,我们应该尽量减少DOM操作,因此理想状态下,组件是不需要link或compile方法的,而且controller在语义上更贴合mvvm架构。
**注意,这里跟民工叔的做法有点不一样。**叔叔的做法是把指令做成class然后在index.js中import并初始化,like this:
但是我的意见是,整个系统设计中index.js作为angular的包装器使得代码变成框架可识别的,换句话说就是只有index.js中是可以出现框架的影子的,其他地方都应该是框架无关的使用原生代码编写的业务模型。
1.5之后提供了一个新的语法
moduleInstance.component
,它是moduleInstance.directive
的高级封装版,提供了更语义更简洁的语法,同时也是为了顺应基于组件的应用架构的趋势(之前也能做只是语法稍啰嗦且官方没有给出best practice导向)。比如上面的例子用component语法重写的话:component语义更简洁明了,比如
bindToController
->bindings
的变化,而且默认controllerAs = '$ctrl'
。还有一个重要的差异点就是,component语法只能定义自定义标签,不能定义增强属性,而且component定义的组件都是isolated scope。另外angular1.5版本有一个大招就是,它给组件定义了相对完整的生命周期钩子(虽然之前我们能用其他的一些手段来模拟init到destroy的钩子,但是实现的方式框架痕迹太重,后面会详细讲到)!而且提供了单向数据流实现方式!
example
component相关详细看这里:angular component guide
从angular的这些api变化来看,ng的开发团队正在越来越多的吸取了一些其他社区的思路,这也从侧面上印证了前端框架正在趋于同质化的事实(至少在同类型问题领域,方案趋于同质)。顺带帮vue打个广告,不论是进化速度还是方案落地速度,vue都已经赶超angular了。推荐大家都去关注下vue。
Service、Filter
自定义服务 provider、service、factory、constant、value
angular1.x中有五种不同类型的服务定义方式,但是如果我们以功能归类,大概可以归出两种类型:
angular原本设计service的目的是提供一个应用级别的共享单元,单例且私有,也就是只能在框架内部使用(通过依赖注入)。在ES5的无模块化系统下,这是一个很好的设计,但是它的问题也同样明显:
很显然,ES6 Module并不会出现这些问题。举例说明,我们之前使用一个服务是这样的:
index.js
Service.js
Controller.js 这里使用了工具库angular-es-utils来简化ES6中使用依赖注入的方式。
假如哪天在调用controller.getUserName()时报错了,而且错误出在service.getName方法,那么查错的方式是?我是只能全局搜了不知道你们有没有更好的办法。。。
如果我们使用依赖注入,直接基于ES6 Module来做,改造一下会变成这样:
Service.js
Controller.js
这样定位问题是不是容易很多!!
从这个案例上来看,我们能完美模拟基础的 Service、Factory 了,那么还有Provider、Constant、Value呢?
Provider跟Service、Factory差异在于Provider在ng启动阶段可配置,脱离ng使用ES6 Module的方式,服务之间其实没什么区别。。。:
Provider.js
应用入口时配置:
app.js
Contant跟Value呢?其实如果我们忘掉angular,它们倆完全没区别:
Constant.js
使用ng内置服务
上面我们提到我们所有的服务其实都可以脱离angular来写以消除依赖注入,但是有一种状况比较难搞,就是假如我们自定义的工具方法中需要使用到angular的built-in服务怎么办?要获取ng内置服务我们就绕不开依赖注入。但是好在angular有一个核心服务
$injector
,通过它我们可以获取任何应用内的服务(Service、Factory、Value、Constant)。但是$injector
也是ng内置的服务啊,我们如何避开依赖注入获取它?我封装了个小工具可以做这个事:这样做确实可以但总觉得不够优雅,不过好在大部分场景下我们需要用到built-in service的场景比较少,而且对于
$http
这类基础服务,调用者不应该直接去用,而是提供一个更高级的封装出去,对调用着而言内部使用的技术是透明,可以是$http
也可以是fetch
或者whatever。通过这些手段,对于业务代码而言基本上是看不到依赖注入的影子的。
Filter
angular中filter做的事情有两类:过滤和格式化。归结起来它做的就是一种数据变换的工作。filter的问题不仅仅在于DI的弊端,还有更多其他的问题。vue2中甚至取消了filter的设计,参见[Suggestion]Vue 2.0 - Bring back filters please。其中有一点我特别认可:过度使用filter会让你的代码在不自知的情况下走向混乱的状态。我们可以自己去写一系列的transformer(或者使用underscore之类的工具)来做数据处理,并在vm中显式的调用它。
一步步淡化框架概念
如果想将业务模型彻底从框架中抽离出来,下面这几件事情是必须解决的。
依赖注入
前面提到过,通过一系列手段我们可以最大程度消除依赖注入。但是总有那些edge case,比如我们要用$stateParams或者服务来自路由配置中注入的local service。我写了一个工具可以帮助我们更舒服的应对这类边缘案例 Link to Controller
依赖属性计算
对于需要监控属性变化的场景,之前我们都是用
$scope.$watch
,但是这又跟框架耦合了。民工叔的文章里提供了一个基于accessor的写法:template
这样当firstName/lastName发生变化时,fullName也会相应的改变。基于的原理是
Object.defineProperty
。但是民工叔也指出了一个由于某种不知名的原因导致绑定失效,不得不用$watch的场景。这个时候$onChanges
就派上用场了。但是$onChanges
回调有个限制就是,它的变更检测时基于reference的而不是值的内容的,也就是说绑定primitive没问题,但是绑定引用类型(Object/Array等)那么内容的变化并不会被捕获到,例如:template
点击user-list组件时,userCount值并不会变化,因为
$onChanges
并没有被触发。对于这种情况呢,你可能需要引入immutable方案了。。。怎么感觉事情越来越复杂了。。。组件生命周期
组件新增的四个生命周期对于我而言可以说是最重大的变化了。虽然之前我们也能通过一些手段来模拟生命周期:比如用compile模拟init,postLink模拟didMounted,
$scope.$on('$destroy')
模拟unmounted。但是它们最大的问题就是身上携带了太多框架的气息,并不能服务文明剥离框架的初衷。具体做法不赘述了,看上面组件部分的介绍Link To Component.
事件通知
以前我们在ng中使用事件模型有
$broadcast
、$emit
、$on
这几个api用,现在没了它们我们要怎么玩?我的建议是,我们只在必要的场景使用事件机制,因为事件滥用和不及时的卸载很容易造成事件爆炸的情况发生。必要的场景就是,当我们需要在兄弟节点、或依赖关系不大的组件间触发式通信时,我们可以使用自制的 事件总线/中介者 来帮我们完成(可以使用我的这个工具库angular-es-utils/EventBus)。在非必要的场景下,我们应该尽量使用
inline-event
的方式来达成通信目标:useage
总结
理想状态下,对于一个业务系统而言,会用到angular语法只有
angular.controller
、angular.component angular.directive
、angular.config
这几种。其他地方我们都可以实现成框架无关的。对于web app架构而言,angular/vue/react 等组件框架/库 提供的只是 模板语法&胶水语法(其中胶水语法指的是框架/库 定义组件/控制器 的语法),剥离这两个外壳,我们的业务模型及数据模型应该是可以脱离框架运作的。古者有云,衡量一个完美的MV*架构的标准就是,在V随意变化的情况下,你的M*是可以不改一行代码的情况下就完成迁移的。
在MV_架构中,V层是最薄且最易变的,但是M_理应是 稳定且纯净的。虽然要做到一行代码不改实现框架的迁移是不可能的(视图层&胶水语法的修改不可避免),但是我们可以尽量将最重的 M* 做成框架无关,这样做上层的迁移时剩下的就是一些语法替换的工作了,而且对V层的改变也是代价最小的。
事实上我认为一个真正可伸缩的系统架构都应该是这样一个思路:勿论是 MV* 还是 Flux/Redux or whatever,确保下层 业务模型/数据模型 的纯净都是有必要的,这样才能提供上层随意变化的可能,任何模式下的应用开发,都应该具备这样的一个能力。
The text was updated successfully, but these errors were encountered: