xufei / blog

my personal blog
6.67k stars 762 forks source link

Angular的变革 #25

Open xufei opened 9 years ago

xufei commented 9 years ago

作为Web前端,你幸福吗?

每隔18个月,前端都要难一倍。

每一年,前端都会冒出更多的概念,更多的框架/库,更多的实践。

2015年,前端有哪些东西转入流行?

但是,在这关键的一年里,Angular社区相对来说,有些沉寂了,主要原因是Angular自身处于一个剧烈变革期,1.x已经趋向成熟,难有本质提升,而2.0尚未有正式发布的消息。

2015年被黑得最多的主流前端组件化框架是什么?Angular。

黑得最漂亮的一句:

其实是 java 程序员把他们习惯的那一套『仪式感』带入了前端框架导致的。 ——尤雨溪

Angular最近在搞的2.0版本,变更可谓剧烈,几乎完全是个新框架。但如果我们关注过1.x版本的演进,就会发现,它也曾经历过不少变革。我们所能看到的变化,其实都是有伏笔的,而Angular官方也在努力做一些事情,让这两代之间能尽量衔接起来。

每个框架在发展过程中,都经历过一次一次的自我革新,那么,Angular经历了什么?

这些变更,分别都有怎样的含义呢?

业务模型的纯化

在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来观测数据变更。

脏检查的原理是:我们所有的对数据的赋值,都是在某些特性场景下触发的,比如:

如果在每次操作之后,对数据保留一份复制,然后下一次再有事件发生的时候,把新老数据进行比对,就可以判定哪些数据产生了变更,从而可以更新关联的界面。

而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在往一个强大而灵活,复杂而高效的前端组件化框架方向努力。

对自我的彻底革新,并不代表过去“错了”,而是代表过去曾经辉煌过的一些东西,随着时代的发展,渐渐走向过时。如果一个东西不随着时代的发展而修正自己,很快就会被历史的车轮无情碾过。(上面一句请勿联想,不主动不拒绝不承认不负责)

xufei commented 9 years ago

2015年11月15日,南京GDG,幻灯片:http://xufei.github.io/slides/2015/revolution-of-angular.html#0

sinoon commented 9 years ago

感谢前辈分享,请问ES6大规模开始使用大约会在什么时候?现在学Angular1.X是不是已经没用了?还有想学好Angular2.0,一定要会ES6或者TypeScript么?

kunl commented 9 years ago

@sinoon 没必要必须会 es6 或者 TypeScript ,es5 也可以开发, 官方示例提供的就包括 TypeScript 和 es5 两种写法。

imcotton commented 9 years ago

纠正,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]

});
xufei commented 9 years ago

@sinoon 目前如果要用的话,1.x还是可以用,但写的时候要注意淡化框架相关的一些东西,这个后面我再写一篇来详细说。

虽然2.0可以用ES5来开发,但强烈建议你学ES6,未来几年内,这是必备技能。至于TypeScript,用它会有好处,也有负担,看你团队意愿了。

xufei commented 9 years ago

@imcotton 感谢提醒,忘了这里内部不是异步了。。。

Saviio commented 9 years ago

之前在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本身的语法噪音,感觉代码一下子清爽、规整了很多。

至于最后一句...这算是黑么...(您要是不加注释,说不定我就不联想了....)

zeroone001 commented 9 years ago

你给出的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"

xufei commented 9 years ago

@zeroone001 module后面的那个引用数组吗?我是默认这个模块已定义了。。。如果单独跑的话,要写

angular.module("app", [])

repeat错误这个,是因为数组序号不对,加了个0,或者用track by $index

zeroone001 commented 9 years ago

@xufei 我刚尝试了一下,确实是在前面某个地方定义之后,在后面不需要加[],是可以的。第二点你说的数组序号不对,指的是数组第一个元素不能写成0吗?好像去掉0也不行

xufei commented 9 years ago

@zeroone001 现在加上0了应该是对的啊,刚才你说的那个错误,是因为数组中会出现重复元素,索引失效。