Open kuitos opened 8 years ago
非常棒的思路呢,我们在使用中只是使用了部分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的问题,我文中这样描述的
- 随着系统代码量的增长,出现服务重名的几率会越来越大。
- 查找一个服务的定义代码比较困难,尤其是一个多人开发的集成系统(当然你也可以把原因归咎于 编辑器/IDE 不够强大)。
假设这样一段代码
@Inject('ServiceA', 'ServiceB')
class Controller{
getName(){
return this._ServiceA + this._ServiceB;
}
}
import ServiceA from './ServiceA';
import ServiceB from './ServiceB';
class Controller{
getName(){
return this._ServiceA + this._ServiceB;
}
}
哪种维护起来成本更高显而易见吧,DI的方式你几乎不可能知道ServiceA跟ServiceB在哪里定义的。
你说到的迁移,我一开始确实也是从这一点出发的,给他设计了一个可能迁移上层框架的‘伪需求’。但是后来思想有所转变,就是不论框架是否可能迁移,M/VM层做成框架无关都是架构上确实存在的需求。
古者有云,衡量一个完美的MV*架构的标准就是,在V随意变化的情况下,你的M*是可以不改一行代码的情况下就完成迁移的。
各层解耦独立是核心诉求,便于架构迁移只是这件事带来的‘副作用’。 成本的话主要还是在要理解MVVM架构的思路以及需要造一些用于抹平ng特性的基础上层组件(如Inject decorator),但这些都是一次性的。相反的它带来的收益却都是持续的。
另外,这年头还不用ES6写代码,就真的太土了啊!😂
好的! 让我先消化消化..... 过段时间我再来向您请教!
@xiaoyu2er 团队切换代价这个事情没有那么大,总的来说,就是把es5的controller, service之类换成了es6的class形势,其他地方都是一些配置性的变更
兄弟劝你一句,别升级瞎整了.angular2 和 an1是断层发展的, angular1 谷歌是不是维护都难说. 你还整合 es6 . 给自己找事. 想用 es6 试着切到 react 或者 vue. react 最佳.
@hcforbaidu 不是每个项目都可以从零开始的 国内外用 ng1 的公司还是很多的 用 ES6 webpack 等只不过是希望提高开发和维护效率
@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
在使用压缩配置如下: mangle: { except: ['$super', '$', 'exports', 'require', 'angular'], keep_fnames: true//it works } 仍旧出现一段错误: angular.js:13920 Error: [$injector:unpr] Unknown provider: eProvider <- e http://errors.angularjs.org/1.5.8/$injector/unpr?p0=eProvider%20%3C-%20e at http://localhost:9797/assets/scripts/angular/angular.js:68:12 at http://localhost:9797/assets/scripts/angular/angular.js:4511:19 at Object.getService [as get] (http://localhost:9797/assets/scripts/angular/angular.js:4664:39) at http://localhost:9797/assets/scripts/angular/angular.js:4516:45 at getService (http://localhost:9797/assets/scripts/angular/angular.js:4664:39) at injectionArgs (http://localhost:9797/assets/scripts/angular/angular.js:4688:58) at Object.instantiate (http://localhost:9797/assets/scripts/angular/angular.js:4730:18) at $controller (http://localhost:9797/assets/scripts/angular/angular.js:10369:28) at Object.
(http://localhost:9797/assets/scripts/ui-router/angular-ui-router.js:4250:30) at http://localhost:9797/assets/scripts/angular/angular.js:1247:18
大神,ng-include 用ES6 该如何实现呢?
import includingTpl from './include.html';
class Controller {
includingTpl = includingTpl
}
<ng-include src="$ctrl.includingTpl"></ng-include>
@Seven4X 是这个意思么?
@kuitos 可以,可以 这操作 666 不愧是大神
这样会把 $ctrl.includingTpl
当做URL的一部分发个请求 src的值不能是HTML代码 得换个思路了
还有一个疑惑请教大神,在使用ui-router的时候,如何不通过$scope访问父节点的数据呢?
现在访问父节点数据是这样写的
$scope.$parent.data
不赞同部分思想,选择了框架就要用好框架,而不是在框架的基础上再大量的去框架,如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:
通过这种方式,无论被依赖的模块的模块名怎么改变都不会对其他模块造成影响。
ng1.2版本开始提供了一个controllerAs语法,自此Controller终于能变成一个纯净的ViewModel(视图模型)了,而不是像之前一样混入过多的$scope痕迹(供angular框架使用)。
example
这种方式写controller等同于ES5中这样去写:
不过ES6的class语法糖会让整个过程更自然,再加上ES6 Module提供的模块化机制,业务逻辑会变得更清晰独立。
以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。
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,确保下层 业务模型/数据模型 的纯净都是有必要的,这样才能提供上层随意变化的可能,任何模式下的应用开发,都应该具备这样的一个能力。