Angular的变革
xufei opened this issue · 11 comments
作为Web前端,你幸福吗?
每隔18个月,前端都要难一倍。
每一年,前端都会冒出更多的概念,更多的框架/库,更多的实践。
2015年,前端有哪些东西转入流行?
- ES2015,Babel
- 以React和Vue为代表的前端组件化框架
但是,在这关键的一年里,Angular社区相对来说,有些沉寂了,主要原因是Angular自身处于一个剧烈变革期,1.x已经趋向成熟,难有本质提升,而2.0尚未有正式发布的消息。
2015年被黑得最多的主流前端组件化框架是什么?Angular。
黑得最漂亮的一句:
其实是 java 程序员把他们习惯的那一套『仪式感』带入了前端框架导致的。
——尤雨溪
Angular最近在搞的2.0版本,变更可谓剧烈,几乎完全是个新框架。但如果我们关注过1.x版本的演进,就会发现,它也曾经历过不少变革。我们所能看到的变化,其实都是有伏笔的,而Angular官方也在努力做一些事情,让这两代之间能尽量衔接起来。
每个框架在发展过程中,都经历过一次一次的自我革新,那么,Angular经历了什么?
- controllerAs (1.2)
- One-time bindings (1.3)
- new router (1.4)
- decorator (1.4)
- components (1.5)
- …… (2.0)
这些变更,分别都有怎样的含义呢?
业务模型的纯化
在1.2版本之前,我们这样写一个控制器:
angular.module("app").controller("DemoCtrl", ["$scope", function($scope) {
$scope.arr = [0, 1, 2, 3];
$scope.addItem = function() {
$scope.arr.push($scope.arr.length);
};
}]);
然后这样使用它:
<div ng-controller="DemoCtrl">
<button ng-click="addItem()">add item</button>
<ul>
<li ng-repeat="item in arr">{{item}}</li>
</ul>
</div>
在1.2版本之后,我们有了controllerAs,可以不必注入$scope了:
angular.module("app").controller("DemoCtrl", [function() {
this.arr = [0, 1, 2, 3];
this.addItem = function() {
this.arr.push(this.arr.length);
};
}])
然后这样使用它:
<div ng-controller="DemoCtrl as demo">
<button ng-click="demo.addItem()">add item</button>
<ul>
<li ng-repeat="item in demo.arr">{{item}}</li>
</ul>
</div>
更进一步,还可以把上面的逻辑代码改造成这样:
angular.module("app").controller("DemoCtrl", [Demo]);
function Demo() {
this.arr = [0, 1, 2, 3];
}
Demo.prototype.addItem = function() {
this.arr.push(this.arr.length);
};
经过这样的转变,我们可以发现,原先的“controller”很清晰了,变成了很纯净的视图模型。这给我们带来的好处是,这一层的东西更容易测试和迁移。
使用controllerAs语法还有一个好处,可以做到逻辑代码的跨版本通用性,甚至是跨框架通用性。
性能优化
在Angular 1.x的整个发展过程中,一直有人在质疑它的性能,为此,开发组也进行了大量的优化。
因为1.x采用的是脏检查的方式来判断数据变更,所以,如何提升变动项的查找会是一件比较重要的事。从1.0到1.4,几乎每个版本都在这个方面作了一些提升,尽可能压榨出更高的性能来。
Angular也添加了诸如单次绑定之类的特性,以减少对初次加载,但不再变更的变量的追踪。
业界有不少类似的框架使用的是存取器做数据变更观测,Angular2使用zone.js来观测数据变更。
脏检查的原理是:我们所有的对数据的赋值,都是在某些特性场景下触发的,比如:
- UI事件
- 网络事件
- 定时器
如果在每次操作之后,对数据保留一份复制,然后下一次再有事件发生的时候,把新老数据进行比对,就可以判定哪些数据产生了变更,从而可以更新关联的界面。
而zone.js更像是一种“多线程”的技术。它把数据的变更过程利用worker切换出去,等执行完了再更新回来,这样就不会阻塞主线程,这是一个非常有创意的做法,因此,Angular2的渲染性能是比较好的。
这种理念在Web开发中前所未有,但是其实在其他一些客户端领域早有实践。
组件化的开发理念
在Angular 1.4之前版本中,并未刻意强调组件化的理念,业务开发人员拥有较高的自由度,比如说,可以选择使用directive,用自定义元素、自定义属性的方式来实现一定程度的组件化,也可以直接使用ng-include和路由,以比较松散的方式完成业务功能。
但是在1.5版本中,新的组件注册语法诞生了,这就是components。
angular.module("app", []).component('counter', {
bindings: {
count: '='
},
controller: function () {
function increment() {
this.count++;
}
function decrement() {
this.count--;
}
this.increment = increment;
this.decrement = decrement;
},
template: [
'<div class="todo">',
'<input type="text" ng-model="counter.count">',
'<button type="button" ng-click="counter.decrement();">-</button>',
'<button type="button" ng-click="counter.increment();">+</button>',
'</div>'
].join('')
});
这样使用:
<div ng-controller="CountCtrl as vm">
<counter count="vm.count"></counter>
</div>
在Angular 2中,组件化更是变成了一种强制的理念。一个组件包含以下部分:
- 模板
- 控制器
- 可选的路由
注意到在这里,我们不再有controller,service,directive这些概念,因为都已经转化为纯粹的ES模块。其中,组件可大致对等于以前的directive,只是配置方式更加友好了。
@Component({
selector: 'basic-routing',
directives: [ ROUTER_DIRECTIVES],
template: `<a [router-link]="['/Home']">Home</a>
<a [router-link]="['/ProductDetail']">Product Details</a>
<router-outlet></router-outlet>`
})
@RouteConfig([
{path: '/', component: HomeComponent, as: 'Home'},
{path: '/product/', component: ProductDetailComponent, as: 'ProductDetail' }
])
class RootComponent{}
尽管粗略看上去,这段代码会比较奇特,但你可以这么想:主体逻辑都是放在普通的class里,剩下的组件相关的配置放在注解中。这样一想,就没有那么别扭了。
更灵活的路由
Angular 1.x早期自带的路由ngRoute比较简单,可以满足最基本的业务开发需求。
但是,在很多较复杂业务中,子路由成为了比较迫切的需求,我们可能会需要路由的嵌套,或者平级存在多个路由,因此,很多开发者选用了第三方的路由库uiRouter。
这两种路由配置方式都是典型的集中式配置,集中式路由在跟踪、定位等方面有优势,但绝大部分情形下,不灵活。
路由的实质是什么?是组件关系的一种映射,既然是这样,集中化的配置会导致,每当组件包含关系有变化,就可能需要修改全局配置,这是不太好的。
如果构建一个全组件化的系统,我们每个组件实际上只关注自身和所包含的子组件的url映射,基于这个理念,就有了Angular2的路由系统。
Angular2的路由是组件式路由,分散定义在每个组件上,并且,管理了所在组件的一些生命周期。
比如上一节的例子中,我们可以看到:
@RouteConfig([
{path: '/', component: HomeComponent, as: 'Home'},
{path: '/product/', component: ProductDetailComponent, as: 'ProductDetail' }
])
这段代码是个路由配置,指明了本组件下属两个组件,分别有不同的url,它们会被加在到模板中的router-outlet部分中:
template: `<a [router-link]="['/Home']">Home</a>
<a [router-link]="['/ProductDetail']">Product Details</a>
<router-outlet></router-outlet>`
在1.4版本中,Angular引入了ngNewRouter,实际上这个就是Angular2的兼容版本,理念完全一致。
基于这套路由机制,我们可以通过canDeactivate,deactivate,canActivate,activate等方法来更好地控制组件的生命周期。
开发语言的升级
在使用Angular 1.x的时候,我们可以使用ES3进行开发,也可以使用ES5,尤其是后者,目前绝大部分主流浏览器都支持,所以可以直接使用,使用ES5编写纯逻辑代码会是一件比较舒服的事情。
但我们也可以用ES6,CoffeeScript,TypeScript之类的语言去编写Angular 1.x应用,只是需要进行一些转化,因为它们不是Angular 1.x的默认开发语言。
到2.0的时代,官方推荐用来开发Angular应用的语言就变成了TypeScript和ES6了,严格的语法检查和各种增强特性,使得开发过程变得更加准确高效。
到现在这个时间点,因为Babel之类转译工具的极大发展,前端又普遍对构建过程逐渐习惯,使用ES6和TypeScript的好处已经远远大于坏处了,所以可以从现在开始就立刻切换到这些语言,无论是在用Angular 1.x还是将来要使用2.0。
编程模型的改变
如果用过Angular2的HTTP模块,会发现跟1.x版本的已经很大不同了。
假如我们要实现一个mapData,从远程请求到一个数值数组,把里面每个元素乘以2之后,再传递给下一个方法。
在1.x里,我们是这样写的:
function mapData() {
return $http.get(url)
.then(result => result.data)
.then(data => data.map(item => item * 2));
//下面的写法不必要,因为内部非异步,感谢@imcotton提醒
/*
var defer = $q.defer();
$http.get(url).then(function(result) {
var newData = result.map(function(item) {
return item * 2;
});
defer.resolve(newData);
});
return defer.promise;
*/
}
但是在2里面,是这样写:
function mapData() {
return Http.get(url).map(item =>item * 2);
}
可以看到,这两者有不少差别。1.x的$http.get方法,返回结果是个Promise,所以,如果要持续传递下去,我们也要新建一个Promise并且返回。但是在2里面,Http.get的返回类型是RX Observable,它对很多东西的处理方式会不太一样,所以业务代码的写法也会有所不同,从代码上看,会有很明显的简化。
粗略一看,可能觉得这个RX Observable的例子没什么奇特的,即使你返回一个普通的数组,它也可以map啊,可以reduce,可以filter之类,仍然能传递下去,但RX的这个还可以subscribe之类,像这样:
Http.get(url).map(item => item * 2)
.subscribe(result => {
this.todoList.push(result);
});
这类特性能很大程度上减少我们实现业务功能所需的代码量。
参阅:RxJS
小结
综合以上,我们发现Angular从1.x到2.0的发展过程中,出现了这样一些变革:
- 视图模型的纯化
- 性能的优化
- 组件化的开发理念
- 更灵活的路由
- 开发语言的升级
- 编程模型的改变
这些变革体现了Angular在往一个强大而灵活,复杂而高效的前端组件化框架方向努力。
对自我的彻底革新,并不代表过去“错了”,而是代表过去曾经辉煌过的一些东西,随着时代的发展,渐渐走向过时。如果一个东西不随着时代的发展而修正自己,很快就会被历史的车轮无情碾过。(上面一句请勿联想,不主动不拒绝不承认不负责)
2015年11月15日,南京GDG,幻灯片:http://xufei.github.io/slides/2015/revolution-of-angular.html#0
感谢前辈分享,请问ES6大规模开始使用大约会在什么时候?现在学Angular1.X是不是已经没用了?还有想学好Angular2.0,一定要会ES6或者TypeScript么?
纠正,1.x 里 $http
返回的 Promise
可以直接使用,并不需要 wrapper
angular.module('App', ['ngMockE2E']).run(function ($httpBackend, $http) {
$httpBackend.whenGET('/list').respond([1, 2, 3]);
function mapData(url) {
return $http.get(url)
.then(result => result.data)
.then(data => data.map(item => item * 2))
;
}
mapData('/list').then(alert); // [2, 4, 6]
});
@sinoon 目前如果要用的话,1.x还是可以用,但写的时候要注意淡化框架相关的一些东西,这个后面我再写一篇来详细说。
虽然2.0可以用ES5来开发,但强烈建议你学ES6,未来几年内,这是必备技能。至于TypeScript,用它会有好处,也有负担,看你团队意愿了。
之前在CFF看到民工老师你用ES6 class写directive,感觉一下亲切不少,因此相比之下1.5新推出的component syntax 似乎也没那么具有吸引力了。
最近看了几个ng-connect的vedio,让我感觉ng和react斗艳也挺好的,ng2也从react里借鉴了一些设计,功能模块设计的更加灵活,之前就看到有人这样评价:
Angular v2 doesn't seem like a "framework", but more like a library that sits on top of the web standards.
再加上了引入了ES6 & TS,进一步减少了JS本身的语法噪音,感觉代码一下子清爽、规整了很多。
至于最后一句...这算是黑么...(您要是不加注释,说不定我就不联想了....)
你给出的1.2版本之后的那个controller As ,你的module函数的第二个参数没加,这个地方会报错;改正之后会出现repeat指令的一个错误,https://docs.angularjs.org/error/ngRepeat/dupes?p0=item%20in%20attr&p1=number:5&p2=5 ;解决方法是ng-repeat="item in demo.arr" 改为 ng-repeat="item in demo.arr track by $index"
@zeroone001 module后面的那个引用数组吗?我是默认这个模块已定义了。。。如果单独跑的话,要写
angular.module("app", [])
repeat错误这个,是因为数组序号不对,加了个0,或者用track by $index
@xufei 我刚尝试了一下,确实是在前面某个地方定义之后,在后面不需要加[],是可以的。第二点你说的数组序号不对,指的是数组第一个元素不能写成0吗?好像去掉0也不行
@zeroone001 现在加上0了应该是对的啊,刚才你说的那个错误,是因为数组中会出现重复元素,索引失效。