您的位置:首页 > Web前端 > AngularJS

后Angular时代二三事

2016-04-16 10:56 549 查看
https://github.com/xufei/blog/issues/21

JavaScript框架/库一直就是百花齐放,最近几年更是层出不穷。回顾这几年,有两个最引人注目的东西,一个是Angular,一个是React。其中,Angular最火的时间是2013年中到2014年末,React从2014年中开始升温,然后又由于ReactNative等周边项目,导致关注度很高。

2014年末,Angular官方宣布了一个大新闻,要完全重写Angular 2.0。这个事情让很多想要使用Angular的人止步不前,也给很多人带来了困惑。

随后,Angular 2.0的开发者之一创建了新的框架Aurelia,整体思路上与Angular相似,有一些细节的差异。那么,我们应当如何看待这些框架呢?

为什么Angular 2重写

如果不是有重大原因,没有哪个开发者会做出彻底重写,产生很多不兼容变更的决定。对于Angular来说,它面临这么一些原因:

Web标准的升级,主要是Web Components相关标准和ECMAScript的后续版本
自身存在的一些问题:性能,模块,过于复杂的指令等等

使用转译语言

也正是Angular 2.0那篇大新闻,使大家知道了AtScript这样的语言,它在TypeScript的基础上添加了注解等功能。

有很多语言可以转译成JavaScript,比如CoffeeScript,Dart,TypeScript等,从最近的一些事件来看,TypeScript可以算是JavaScript转译领域的最大赢家。

很多人可能会有这样的疑问:为什么我们要用这些东西,而不是直接编写原生的JavaScript?开发语言的选择,很大程度上反映了我们对JavaScript组件化方案抽象度的需求。

比如说,Angular中,可以使用TypeScript来写业务代码,React中,通过JSX来使用组件,这都是具有较高抽象度的方案,能够让业务代码变得更直观。

ES6

先不看这些转译语言,来看看ES6,它给我们带来了很多编程的便利,每一次这种语言细节的升级,都引入了一些好用的东西,所以我们当然是期望尽早使用它。但问题是,浏览器的支持程度总是落后的,如果用它写了,在很多浏览器上不支持,比如箭头函数:

this.removeTodo = function(todo) {
this.todos = this.todos.filter(item => todo!=item);
};


所幸,我们有Babel这样的转换器,可以把这样的代码翻译成ES5代码,它的生成结果就是

this.removeTodo = function(todo) {
this.todos = this.todos.filter(function(item) {
return todo != item;
});
};


这个例子并不明显,如果你使用class之类的东西,就能体会到更大的改变。虽然说class这些只是语法糖,但用起来还是很爽的,可以复用一些传统的设计模式之类。

对于那些只需支持ES5+的项目而言,现在开始选用ES6语法编写代码是非常合适的,因为我们有Babel这样的东西,我们可以享受ES6新语法带来的愉悦编程体验,而无需承担兼容风险。

ES6新语法有很多,想要在生产过程中更好地使用,可以参见百度ecomfe的这篇使用ES6进行开发的思考

TypeScript

Angular和Aurelia都支持TypeScript,可以直接使用TypeScript编写业务代码。如果选用这样的框架,个人建议直接使用TypeScript。

为什么在类似Angular这样的体系里,我要建议使用TypeScript呢,因为这么几个原因:

长期的兼容性更好

很可能在现在这个阶段,你的项目还需要面对一些不支持ES6的浏览器,所以不能直接写ES6代码,但有可能有一天,浏览器支持了,但你的代码还是老的,它基本上还在使用ES5编写,想要迁移到ES6比较麻烦,以后每次迁移都是痛苦的过程。TypeScript就是以生成JavaScript为目标的,所以如果你用它写,只需选择生成参数,比如生成es5,es6就可以了,就算以后es继续升级,也只要改个参数就完事。

编写体验更好

TypeScript为代码提示作了很多特殊优化,比如:

ele.on("click", function(e) {
// 这里我们是不知道e上面有什么,在编写的时候得不到提示
});


但是如果使用TypeScript编写,因为这个e的类型确定,所以就能有提示。

使用这样的语言也能够更快让非前端方向的人参与项目。

工作流程与管控

Angular的整体方案,由于分层很清晰,在JavaScript代码中基本就是纯逻辑,这样的代码如果使用TypeScript编写,会更加精炼,更加清晰。

这几年,大家逐渐接受了一个现实,那就是:前端也是需要构建的,所以我们有grunt,gulp这样的构建工具。之前我们不愿意写转译语言,是因为其他环节不需要构建,为了一些语法糖而引入整个构建环节代价太大。现在,既然发布之前的构建环节不可缺少,使用转译语言也不过就是加一段配置而已,这个使用代价已经小很多了。

Angular这样的解决方案,所面向的多数都是重量级产品,这些产品本身就会有构建环节,也基本上会使用IDE,所以,使用TypeScript的代价不大。

当项目变大的时候,我们会面临很大的管理成本,比如对代码的分析,结构调整,模块依赖关系梳理等,在TypeScript上面做,会比在JavaScript上面做更有优势。

最近几年前端领域“工程化”这个词被说得太多,但其实绝大部分说的都只是“工具化”。早在Visual Studio 2005中,就存在很多Factory插件,举例来说,一个普通项目的工作流程可能是这样:

使用ER图设计模型结构
一键生成数据库表结构和存取过程
一键生成数据库访问层和实体定义代码
一键生成Web Service接口
根据WSDL,一键生成客户端的调用接口
剩下的就是做界面,调用这些接口了

比如说我们做到一半,需要变更模型,也只是需要在ER图那边修改,然后依次一键变更过来。很多时候我们也会有代码的目录调整,批量更名,如果使用约束较强的语言,这部分可靠性会更高。

组件化与路由

如果用过angular 1.x,会对它的路由机制印象深刻。有复杂业务需求的人一般都不会使用内置的ng-route,而是会使用第三方的ui-router,这两者的核心差别是子路由的定义。

比如:

A界面有两个选项卡,分别B,C,如果我们想要:

app.html#a/b
app.html#a/c

这样的多级路由,在ng-route中想要定义,就比较麻烦,而在ui-router中,允许使用嵌套的ui-view指令,可以比较方便地支持这一功能。

在这两种方式下,路由都是全局配置的,但我们考虑在全组件化的场景下,组件的嵌套会受到这种路由配置的制约。比如,本来我们只是期望把某个组件嵌入到另外一个组件中,就能完成功能,但为了路由,不得不额外在全局路由配置的地方,加一个配置,而且每当组件层级发生变更的时候,这个配置都需要改,这就大幅拖累了我们组件体系的灵活度。

为此,我们可能会期望把路由配置放在每个组件中,比如说,组件A定义自己的路由为a,组件B的路由为b,组件C的路由为c,无需额外的配置,当B和C放在A中作为选项卡的时候,上面那两条路由会自动生效。

在Angular的新路由机制中,就是这样处理的,这也是Angular 2.0和Aurelia的共同路由机制。在这种机制下,如果有一天我们在另外一个更高层的组件D中,引入了组件A,那路由就会自己变成类似:

app.html#d/a/b
app.html#d/a/c

这个是非常灵活的,这对于我们构建一个全组件化的系统很有利,另外,这实际上实现了路由的动态配置。

当然,对这个问题,也是有争议的,因为路由不再集中配置,很难有一个地方能查看所有的路由状况了。

此外,由于在Angular 2和Aurelia中都凸显了组件的概念,组件的生命周期被引入了,比如说,组件的四个状态:

创建前
创建
销毁前
销毁

这些跟路由进行配合,可以把我们的加载过程,前置、后置条件过程都整理得很清楚。

指令与Web Components

最近,越来越多的人开始关注Web相关标准的推进,在HTML这个方面,最重要的标准就是Web Components,它主要是提供扩展HTML元素的能力(Custom Elements)。

HTML is great for declaring static documents, but it falters when we try to use it for declaring dynamic views in web-applications. AngularJS lets you extend HTML vocabulary for your application. The resulting environment is
extraordinarily expressive, readable, and quick to develop.

这一段来自Angular的官方介绍。扩展HTML的词汇,是Angular的一种愿景,在这个里面,除了包含对元素的扩展,还有属性(Attribute)。

很多时候,仅仅有元素的扩展,是不足以满足需求的。举例说,让某个按钮闪烁,我们有两种方式实现:

创建一种可以闪烁的按钮
创建一种可以闪烁的行为

其中,前者是特定的解决方案,创建一个自定义元素<blink-button></blink-button>可以达到目的,但闪烁这个动作可以是一种通用行为,我们可能需要让图片闪烁,让链接闪烁,让各种元素都能闪烁,把这种行为扩展到不同的元素上。

如果用jQuery,我们可能会写:

$.fn.blink = function(options) {
// 这里对DOM进行处理,添加闪烁功能
};


然后在使用的时候:

$('.some-element').blink();


如果说有自定义属性,可能我们就只要写:

<span blink>aaa</span>
<a blink>aaa</a>
<button blink>aaa</button>


借助数据绑定,还可以把blink绑定到一个变量上,由这个变量动态控制是否闪烁。

<div blink="hasNewMessage">aaa</div>


在Angular 1.x中,使用指令(directive)来实现自定义元素和自定义属性,这个东西设计得很复杂,所以不太容易上手,在2.0中,这一块改了。

在Angular 2和Aurelia中,使用很简单的标记来表明某个东西是自定义元素还是属性。

@customAttribute('blink')
@inject(Element)
export class Blink {
element:any;

constructor(element) {
this.element = element;
}
}


@customElement('my-calendar')
export class Calendar {
}


自定义属性的理念,在早期IE中实现的HTML Components中有很好的体现,它允许使用JavaScript编写DOM元素相关的代码,然后在css中作为行为附加到选择器上。

组件化与MVVM

对于大型Web应用来说,组件化是必须的,但是如何实现组件化,每个人都有自己的看法,所以组件化这个词就像民主,法制一样,容易谈,难做。

我们所期望的组件化往往是这样:





但实际上,很可能是这样:





实际在用组件,尤其UI组件的时候,会出现很多尴尬的地方,比如说同一个组件在不同场景下形态不一致,所以我们需要多个层次的组件复用级别。

在Angular 1.x中,组件化并不是一个很明确的概念,它的整体思路还是:逻辑层+模板层这样的概念,此外,有一些指令(directive),用于表达对HTML标签、属性的增强。

在2.0版本中,组件成为了一个很清晰的东西。一个常见的组件,包含界面模板片段和逻辑类两个部分。

如果我们经历过Angular 1.2之前的版本,可能会感受到controller的一些变化。比如说,之前我们写一个controller,可能是:

function TestCtrl($scope) {
$scope.counter = 0;

$scope.inc = function() {
$scope.counter++;
};
}


然后这样用:

<div ng-controller="TestCtrl">
{{counter}}
<button ng-click="inc()">+1</button>
</div>


在1.2之后,我们会这样写:

function TestCtrl() {
this.counter = 0;

this.inc = function() {
this.counter++;
};
}


然后这样用:

<div ng-controller="TestCtrl as test">
{{test.counter}}
<button ng-click="test.inc()">+1</button>
</div>


注意TestCtrl的实现,里面没有$scope了,这意味着什么呢?意味着这个“controller”已经不再是controller了,而是view model,这个部分的代码变得更加纯净,每有一个对应的界面片,就实例化一个出来与之对应绑定。

在Angular 2和Aurelia里面,HTML模板与视图模型被视为一体,当做一个组件,而Aurelia的灵活度更高,因为它尽可能地把额外的配置放在HTML模板中,所以视图模型变得更单纯,也存在复用价值了。

Aurelia跟Angular 2有不少细节差异,写法上大致的对比可以从这里看出:Porting an Angular 2.0 App to Aurelia

Angular支持使用pojo作为数据模型,这可以算是它的优点之一,这样,它对模型层的定义就比BackBone和Knockout简洁很多。

但是在2.0时代,我个人是倾向于预定义模型类型的,因为在MVVM这三层中,不宜过于淡化VM和M的分界,分清哪些东西是从属于模型的,哪些东西是从属于视图模型,在很多情况下都会很重要。这会影响我们另外一些工程策略,比如测试环节的处理方式。

在大型应用中,model应当与store视为一体,在比如数据的共享,缓存,防冲突,防脏等方面综合考虑,而view model可以不要考虑得这么复杂。

基于MVVM,我们可以在不同层级复用组件,可以把模板和视图模型当做一个整体复用,也可以只复用视图模型,使用不同的模板。在这一点上,Angular 2显然比Aurelia欠考虑。

代码的迁移

Angular的这次升级,最令人不满的是它的不兼容变更。这些变更很多方面来说,是无奈之举,因为前后的差距确实有那么大,想要短期平滑,就得在未来背负更重的历史负担。

但事实上,我们在很多场景下,比如企业应用领域,并没有比它更好的解决方案,所以这时候需要来看看如果想要作一个版本迁移,需要做哪些事情。

如果我们要做从Angular 1.x到2.0的代码迁移,相对最容易,也最值得做迁移的部分是数据模型,但这个问题说难也难,说简单也简单。

很多对分层理解不深的人,很可能把这个代码迁移想得过于复杂。但其实,一个规划良好的Angular 1.x工程,它的代码结构应该是非常有序的,什么东西放在模板里,什么东西放在controller,service,都是非常清楚的,而且,绝大多数controller和service中,是不应有DOM相关的代码的。

比如,service中是什么?主要是数据模型的存取,与服务端的交互,本地缓存,公共方法等,这些东西要迁移到2.0中,是很容易的,只是写法会稍有差别。

接下来往上看看,看这个所谓的controller。在2.0中,不再有controller,service这些东西的区分,一切都是普通的ES类,但是理念还是有的。比如一个含有视图的组件,它的逻辑部分就会是一个ES类,这个也就是视图模型,基本上也就对等于1.x中的controller。

比如最简单的todo:

function TodosCtrl() {
this.todos = [];
this.newTodo = {};

this.addTodo = function() {
this.todos.push(this.newTodo);
this.newTodo = {};
};

this.removeTodo = function(todo) {
this.todos = this.todos.filter(function(item) {
return item != todo;
});
};

this.remainingCount = function() {
return this.todos.filter(function(item) {
return item.finished;
}).length;
};
}


这代码很简单,就是给一个列表添加移除东西,假设我们要把这个代码移植到2.0,可以说基本没有代价,因为在2.0里你要实现这样的功能,也得这么写。

(注意,下面这段是Aurelia代码,并且不是使用ES6,而是使用TypeScript编写)

export class Todos {
public todos: Array<Object> = [];
public newTodo: Object = {};

addTodo(): void {
this.todos.push(this.newTodo);
this.newTodo = { content: "" };
}

removeTodo(todo): void {
this.todos = this.todos.filter(item => todo != item);
}

get remainingCount() {
return this.todos.filter(item => item["finished"]).length;
}
}


这么一看,好像也很容易迁移过去,多数情况下是这样,但这里面有坑。坑在什么地方呢?主要是手动添加变更检测的部分。变更检测是个复杂的话题,在本文中先不讲,后面专门写一篇来讲。

现在我们把逻辑层摆平了,来看界面层,这里主要有三个东西,一个是原先的指令,一个是普通的模板,还有一个是过滤器。

指令的问题好办,我们刚才提到的自定义元素,自定义属性,其实对使用者是没什么差别的,也就是实现的人要把代码迁移一下。

我个人并不赞同在一个业务型的项目中封装太多自定义元素,仅仅那种被称为“控件”的东西才有这个必要,其他东西可以直接采用模板加视图模型的方式,具体理由在前一篇的组件化之路中提到过。如果是按照这种理念去实现的业务项目,指令这块迁移成本也不算高。

过滤器也很好办,2.0 有同样类似的机制实现。

普通模板这边,绝大部分都是固定的工作量,比如ng-repeat,ng-click换个写法而已,里面有一些影响,但基本上是可以用批量转换去搞定的。

所以我们发现,迁移的成本并没有想象的那么大,为了更好地拥抱Web标准和更好的性能,这样的事情是比较值得去做的。

Angular与React

这两种东西代表着现代Web前端的两种方法论,前者是以分层和绑定为核心的大一统框架,后者提供了渲染模型多样化,带生命周期的多层组件机制。由于实现理念的不同,用它们分别开发同样的Web应用也会有很大差异。好比我们造一个仿生机器人,用Angular是先造完骨架,把基本运动功能调试完,然后加装肌肉等部件,最后贴皮肤,眉毛,头发,指甲;用React是先造出各种器官,肢体,然后再拼装。

方法论的事情那个很难说对错,只有看场景。比如亚洲农民跟美洲农民种地,理念肯定是不同的,因为他们面临的场景不同,比如亚洲种地普遍很精细化,美国种地很粗放。这也有些像React和Angular的差别。

我个人不赞同在框架的问题上有太多争论,因为天下武功,到底什么厉害,完全是看人的,一阳指在段正淳手里,只能算二流,到了南帝段智兴手里,可与降龙十八掌齐名。聚贤庄一战,乔帮主用最普通的太祖长拳,打得天下英雄落花流水。如果深刻理解了一个技术的优点和缺点所在,扬长避短,则无往而不利。

近年来,各框架是在互相学习的过程,但是每个东西到底有什么不同,最好还是列出需求,分别用代码体现。现在已经有todomvc这么一个库,用各种框架实现todo,但在我看来,这个需求还太小,不足以表达各自的优势。

我倡议,每个框架的熟练使用者能够选出一些典型场景,然后写一些demo,供更多的人学习对比之用。

Angular与未来

到目前为止,我们在浏览器中看到系统从规模来说都是中小型的,与传统桌面的大型软件们相比,还很幼小。比如Office的开发团队,千人以上的规模,无论是代码的架构,还是人员的分工协作,都可以算是伟大的工程。

在大型系统中,组件化可以说是立足的基础,但怎样去实践组件化的思想,是一个见仁见智的话题。

还是以Office为例,它除了提供图形化的操作界面,还提供了一套API,可以被VBA这样的嵌入语言调用。

比如说,我们可以在界面上选中一个工作表,然后在某行某列填入数据,也可以在VBA中使用这样的语句去达到同样的目的

这就意味着,对于同一种操作,存在多样化的外围接口。继续分析下去,我们会发现,存在一种叫做Office Object Model的东西,这也就是一个核心数据模型,我们所有的操作其实都是体现在这个模型上的,GUI和VBA分别是这个模型的两个外围表现。

所以可以想象,如果Office的测试团队想要测试功能是否正确,他是有两条路要走:

通过VBA这么一个相对简单直接的方式,去调用OOM上的方法和属性,然后再次通过VBA去验证结果
通过GUI上类似录屏的操作,去模拟人的一些操作,然后,通过VBA或者是界面选取的方式验证结果

从这里可以大致感受到,当系统越复杂的时候,独立的模型层越重要,因为必须保持这一层的绝对清晰,才能确保整个系统是正确而稳定的。层层叠加,单向依赖,这使得软件正确性的验证过程变得更加可控。

在业务系统中,又存在另外一些问题。以我曾经从事过的电信行业软件系统为例,整个运营与业务支撑系统由若干个子系统构成,比如:

资源管理,管理卡、号、线等资源
营业系统,负责对外营业
计费与结算
运维与调度,负责人员权限考核调度等
相关的内部管理系统

这些系统基本都已经Web化,如果我们要探讨它们的组件化方式,必须作相当深远的考虑,因为,还可能出现终极杀手——比如呼叫中心系统。

大家打客服电话的时候,有没有注意到,客服人员可以操作的东西,是超过了前台营业员的,这也就说明他实际上能够操作以上某几个系统。可是我们也没有发现他在切换多种功能的时候,花太多时间,说明其实他有一个高度集成的界面入口。

这就来了问题了,如果这里的多数功能是集成其他系统的组件所致,那都该是一些什么样的组件啊?

小结

篇幅所限,不在本文中讨论这些问题。抛出这样的问题来,是为了让大家察觉,在很多不为人知的地方,存在很值得思考的东西。一些新的Web标准是为了解决Web系统的大型化,应用化,但仅仅以这些标准本身而言,还是存在一定的不足,需要更深刻的改变。

我们期望Angular2和Aurelia为代表的新型框架能够给这些领域带来一些灵感,互相碰撞,解放更多人的生产力。

总而言之:

“I think we agree, the past is over.” – George W. Bush
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: