给Java程序员的Angular快速指南

太长不读版:

Spring + Angular 的全栈式开发,生产力高、入门难度低(此处省略一万字),是 Java 程序员扩展技术栈的上佳选择。

如果你动心了,接下来就是那省略的一万字……

痛点 – 团队分工与协作

在前后端分离的开发方式中,拆故事卡是个难题。

如果前后端同时工作于一张卡上,但配合不够默契或节奏不同步,就会出现一方空转的现象。如果前后端各一张卡,又不容易实现端到端验收,可能导致先做完的一方在另一个结束后还要再次返工的现象。而且,两个人都要深入理解这张卡所描述的业务细节,而这往往是不必要的。

更重要的是,BUG 最容易出现在边界处。

业务卡不像技术卡那样能跟其它卡片划出明确的边界,前后端之间必然具有千丝万缕的联系。这种联系越紧密,出 BUG 的机会也就越大。

技术架构上的挑战,也会反映到人员架构上。我们人类不是星灵,无法做到心灵相通。因此前后端开发者需要对合作方所拥有的知识进行很多主观假设。

如果这些假设中存在错误,又没能及时沟通来消除它(甚至可能都意识不到这些假设的存在),那么 BUGs 就要登场了。而像业务卡这种级别的密切协作中可能隐含的假设实在太多了,除非经过长时间的磨合,否则很难消除,但大多数项目上可没有那么多磨合时间。

解决方案 —— 全栈式开发

人员架构

该如何解决呢?克服上述问题的办法就是全栈式开发。也就是说,调整人员架构去适应技术架构。

简单来说:每个人都同时写前端和后端。他不必是前端专家也不必是后端专家,但是两边都要会写。他的关注点不是技术知识,而是业务知识。他的工作目标是贯穿前后端的价值流,对单个故事进行端到端交付。

但是,要如何克服实现中遇到的技术难题以及保障代码质量呢?那就要靠团队中的技术专家了。

总体来说,全栈式团队的人员架构就是大量全栈业务工程师 + 少量技术专家。当然,技术专家不一定要安排单独的人担任,只要技术满足要求,也可以由某位全栈工程师兼任,只是他做计划时要留出做技术支持的时间。

通过 Code Review、Pair 等敏捷实践,技术专家可以起到团队放大器的作用,让整个团队的生产力翻倍。

个人工作流

作为全栈工程师,你首先要对一个业务故事进行建模,包括业务模型、视图模型、领域模型、存储模型等,建模的过程也就是你理解业务的过程。这时候要注意多和 BA、UX、DBA 等沟通,以确保你的理解不存在方向性错误,不要太沉迷细节,防止见木不见林。

单源建模的优点是这些模型之间很容易保持一致,这无论是对前期开发还是对后期维护都是有帮助的。

建模完毕之后,就要开始设计前后端之间的接口了。接口是前后端分离式架构中最容易开裂的地方,也是对未来的演化影响最大的地方之一。它很重要,但也不必小心翼翼的 —— 全栈工程师对接口变化的适应能力要强大得多。因为接口的提供方和消费方都是你,信息非常透明,不存在任何额外的假设。对不完美的接口,你可以在后续开发过程中迭代好几个版本来把它打磨到最理想的形态,改接口将不再沉重和危险。

接口设计完之后,有两种路径,取决于界面和后台逻辑的特点。

如果对业务理解还不是很有信心,那就先用 Mock 的方式把前端写出来,然后把这个 Mock 版当做可执行的原型去跟 BA、QA,甚至客户进行实际操作演示,用可操作原型来验证你对业务的理解。对一般粒度的故事卡,线框图级的可操作原型通常能在半天内完成。通过原型尽早发现错误,可以避免以后沉重的返工。而且,这是一个可演化原型,不是一次性原型,不会浪费掉。

如果后端很容易实现(但先不必做优化工作),那么就可以不必 Mock,先初步完成后端开发,并让前端直接对接真实的后端。先拿这个比 Mock 版原型更逼真一点的原型串起流程,然后再进行优化和打磨工作。

在整个过程中,你可以根据不同的需要,来与不同的技术专家进行 Pair,并且你最终的代码也会在例行 Code Review 中得到前端专家、后端专家、DBA、DevOps 专家等人的点评和改进,不必担心自己在单项技术上的短板影响交付。

全栈的挑战

全栈固然美好,但也要迎接很多挑战,而 Angular 会帮你分担这些痛苦。

首先遇到的挑战是语言切换

前后端 JavaScript 全栈固然在特定场景下有效,但是在很多企业应用中是远远不够的。至少到目前为止,企业应用还主要是 Java 的天下。本文所讨论的也都是 Java + JavaScript 的全栈。

我们都知道,Java 和 JavaScript 之间的差异就像雷锋和雷峰塔之间的差异。Java 程序员通常很难适应 JavaScript,不过现在有了更像 Java 的 TypeScript。而 Angular 就是原生基于 TypeScript 的框架,稍后我会做一个摘要讲解,你会发现自己很熟悉它的味道。

(图片来自:http://t.cn/RobG5nA

其次是基础设施

基于 JRE 的构建体系和基于 NodeJS 的构建体系看似差异很大,实际上却有很大程度的相似性。但前端两年一换代的疯狂迭代,以及层出不穷的新名词、新工具,仍然难免会让后端心生恐惧。不过不用担心,Angular 替你封装了一切,你只需要装上 NodeJS 环境和 Angular CLI 就可以了。你不需要关心它封装了哪些第三方工具,至于今后的工具链怎么疯狂迭代,那都是 Angular 开发组需要操心的事。

最后是最佳实践

前后端从表面上看差异很大 —— 前端轻灵,后端稳重。

但在我看来它们很少存在本质性的差异,更像是不同的社区文化导致的结果。而在更高的层次上看,两边的技术具有很大的相似性。无论是函数式编程还是工程化开发,都不是某一方所特有的,而是 IT 领域的共同资产。况且,它们还一直在相互影响,相互渗透 —— 这两年后端变得越来越轻灵,而前端变得越来越工程化。长远来看,文化合流是必然的趋势。

事实上,前后端很多优秀设计和最佳实践都是殊途同归的。像 Spring 和 Angular,它们都采用了久经考验的面向对象范式;都使用依赖注入技术进行解耦;都拥抱函数式编程;都提供了丰富的 AOP 支持等。虽然细节上各有千秋,但仅从代码上就能感受到它们之间的相似性。

我该怎么办?

听完这些,你是否已经蠢蠢欲动?接下来,就跟我开始 Angular 之旅吧。

语言 – TypeScript

Angular 使用 TypeScript 作为主要开发语言。如果你还不熟悉 TypeScript,那可以把它看做 Java 和 JavaScript 的混合体。TypeScript 是 ES6 的超集,这就意味着,任何有效的 ES6 语法都同样是有效的 TypeScript 语法。

事实上,从 Java 出发学 TypeScript,可能比从 ES5/6 学 TypeScript 还要简单一些。不过,对于 Javaer 来说,学习 TypeScript 时有一些重要的不同点要特别注意。

TypeScript 的类型只存在于编译期

TypeScript 的一个首要设计约束就是要兼容 ES5/6,因此不能随意增加基础设施,而像 Java 这种级别的类型支持在原生 JavaScript 中是根本不存在的。

你可以把 TypeScript 的类型看做仅仅给编译器和 IDE 用的。因此,在运行期间没有任何额外的类型信息(只有 ES5 固有的那一小部分),像 Java 那样完善的反射机制是很难实现的(可以用装饰器/注解实现,但比较繁琐)。

TypeScript 的装饰器 vs. Java 的注解

TypeScript 的装饰器和 Java 的注解在语法上很相似,但其实在语法含义上有着本质的区别。TypeScript 的装饰器是个函数,而 Java 的注解是个数据。语法上,装饰器名字后面必须带括号,不能像注解那样省略。

不过,在 Angular 中,TypeScript 装饰器的实际用途就是为类或属性添加注解而已。因此,有些文章中,包括早期的官方文档中,用的都是注解的说法。当然,以后写新文章还是都用装饰器吧。

类与接口

TypeScript 中的类和 ES6 中的类几乎是一样的,和 Java 中的类也很相似。

接口则不同,我们前面说过,TypeScript 中的类型信息只存在于编译期,而接口作为“纯粹的”类型信息,也同样只存在于编译期。也就是说,在运行期间你无法判断某个对象的类是否实现了某个接口。在 Angular 中,实际上使用的是暴力探测法来判断的:查找这个接口中规定的方法(只匹配名称),如果存在,则认为实现了这个接口。

这也意味着,你就算不显式 implements 接口,但只要声明了其中的方法,Angular 也会正确的识别它。但这不是一个好习惯,你应该始终显式 implements 接口,删除时也要同时删除接口声明和对应的方法。不过也不用担心,Angular 自带的 lint 工具会帮你检查是否有忘了显式 implements 接口,多注意提示就可以了。

接口是给编译器和 IDE 看的,这很有用。比如,我们可以在 IntelliJ/WebStorm 中声明某个类实现了一个接口,然后在这个类名上按 alt-enter ,就会出现 “Implement interface XXX” 菜单 —— 就像 Java 中一样。事实上,一些 IDE 对 TypeScript 的支持程度已经接近 Java 了:代码提示、重构、类型检查、简短写法提醒等,应有尽有。

值得注意的是:你也可以 implement 一个类,而不仅是 extends 它,也就是说类可以在很多场景下代替接口!Angular 风格指南提出,“考虑在服务和可声明对象(组件、指令和管道)中用类代替接口”。因为运行期间接口不存在,所以在 Angular 中不能把接口用作依赖注入的 Token,也就不能像 Java 中那样要求注入一个接口,并期待框架帮你找出实现了这个接口的可注入对象,但类存在,因此,上述场景下要尽量用抽象类来代替接口。

鸭子类型

为了支持 JavaScript 的动态性和遗留代码,TypeScript 的类型匹配要比 Java 宽松不少。比如,如果两个类(或接口)的属性和方法(名称、类型)都完全一致,那么即使它们没有继承关系,也可以相互替代(但如果类有私有属性,则不能,就算两者完全一样也不行)。表面上看这可能过于宽松了,但在实际开发中还是很有用的,使用中要注意突破 Java 固有思维的限制。

在 TypeScript 中还支持可选属性(name?: Type),也就是说如果两个类的差别仅仅在可选属性上,那么它们也是可以相互替代的。

字面量与匿名类型

TypeScript 在某些方面可能更符合你对 Java “应该是什么样子”的期待,至少在我看来是这样。要声明一个匿名对象、匿名数组型变量?直接写出来就好了const user = {name: 'tom', age: 20}。除此之外,它还能声明匿名类型 let user: {name: string, age: number} = ...

当然,也不能滥用它们。对于一次性使用或暂时一次性使用的变量或类型,用字面量和匿名类型很方便,可读性也好,但是如果它要使用两次以上,那就该重构成正式的类型了。

any

TypeScript 中的 any 大致相当于 Java 中的 Object,如果你看到通篇 Object 的 Java 代码你会不会想骂街?any 也一样。不必完全禁止 any,但如果你要使用 any,请务必先想清楚自己要做什么。

void

如果你在 Java 中经常使用 void,那就遵循同样的原则用在 TypeScript 中。在 TypeScript 中,当你不声明函数的返回类型时,它会返回自动推断的类型(没有明确的 return value 语句时会推断为 undefined 类型),如果你不想返回任何值,那么请把返回类型指定为 void 来防止别人误用。

this

JavaScript 中的 this 是个奇葩。虽然这是函数式语言中的标配,但从语言设计上真是让人忍不住吐槽。要是能像 Groovy 那样分出 this / owner / delegate 就好了。

吐槽归吐槽,对于 Java 程序员,该怎么避免自己踩坑呢?很简单:对普通函数,任何涉及到 this 的地方都用箭头函数 ()=>,而不要用普通的 function foo(),因为前者是替你绑定好了符合直觉的 this 的;对方法,不要把任何涉及到 this 的方法当作函数指针传给别人,但可以在模板中自由使用。在 Angular 中,这两条原则可以帮你回避掉绝大部分 this 错误。更多的细节可以先不管,随着使用经验的增加,你会逐渐弄明白这些规则的。

其它

以上这些是开发中常遇到的注意事项,其它的特性我就不一一列举了,请自行参考 TypeScript 的官方文档。

范式与模型

MVVM

Angular 的基本编程模型是 MVVM,你可以把它看做 MVC 的一个变种。事实上,这是一个很符合直觉的模型:你看到一个页面,先在大脑中抽取出它的信息架构(属性)和操作(方法),定义好它们之间的逻辑关系和处理流程,这就是视图模型(VM)。你把它们落实到代码,变成内存对象,然后 Angular 就会帮你把它和页面(View)关联起来。你不懂怎么操作 DOM?没关系,你只要会操作内存对象就可以了,这应该是你非常擅长的吧?剩下的那些脏活儿 Angular 都会帮你搞定。

不过,Angular 关心的只是“要有” VM,至于你如何生成这个 VM,它并不会做任何假设和限制。

自由混搭与切换

你想怎么生成 VM?

  • 像后端控制器那样直接写在组件中?没问题!
  • 像后端那样委托给服务?没问题!
  • 像 Redux 那样委托给单一 Store?没问题!
  • 像 Java 8 Stream 那样用流水线生成?没问题!
  • 自己几乎不处理,完全委托给后端 API?没问题!

这么多方式各有不同的适用场景,但也不必过早担心如何选型。只要你的组件设计合理(职责分明、接口明确等),那么在这些方式之间切换,或者混用它们,都不会很难。

作为起点,可以先直接写在组件中,然后按需重构成服务,服务中可以直接写代码,也可以实现 Redux 风格的单一 Store,或者用 RxJS 写流水线。

RxJS

在 Angular 开发人员的成长过程中,有一个很重要的坎就是 RxJS,它的背后是 FRP(函数响应式编程)范式。不过对于 Javaer 来说,它的门槛并不高。如果你会用 RxJava / RxGroovy 等 ReactiveX 族的任何一个库,那么你几乎可以不用专门再学,它们都是同一个大家族,编程范式甚至部分操作符的名称都一样,稍微对比一下差异就可以了。如果不会,请继续往下读(以下的讨论也适用于 RxJava 等,不过我文中只用 RxJS 举例)。

RxJS 是一种 FRP(函数响应式编程)库,它同时具有函数式编程和响应式编程的优点。

如果你会用 Java 8 Stream,那么也有很多知识可以复用到这里。相对于 Java 8 Stream,RxJS 的限制稍微宽松一些,但我建议你仍然按照 Java 那种严格的方式使用它(比如不要对流外的变量赋值)。

所谓响应式编程,我们可以把它想象成一条流水线,流水线上不断传送待加工的材料(原料、半成品、成品等),流水线上每个工序的工人负责对传送到眼前的材料进行一定的处理(做出响应),然后放回流水线,接着它就会被传送到下一个工序。

设计上,每个工序的职责都应该是明确而单一的,这样才能达到最高的效率和流水线的可定制性。

把这些概念映射到 RxJS,流水线就是 Observable(可观察对象),工序就是 operator(操作符),材料就是传给每个 operator 的参数。

是不是感到很熟悉?没错,它跟 MessageQueue 是一样的模型,只是应用在不同的层次而已。在编程领域,这种现象随处可见,善于发现并掌握这种现象,是你作为资深程序员能实现快速跨领域学习的根本保障。

相对于 Java 8 Stream,RxJS 比较特别的一点是它完全屏蔽了同步和异步之间的差异。也就是说,其中的 operator 不知道也不需要关心这个数据是同步传过来的还是异步传过来的。只要你遵循一些显而易见的原则,你就可以一直用同步方式给数据,之后即使要突然改成异步,原有的代码也不会被破坏。

事实上,我在 Angular 开发中经常利用这种特性来加速开发。比如假设我最终需要从后端 API 获取某些信息,在这个 API 开发好之前,我可以先在前端模拟出响应结果,进行后续开发。这时候,如果我用 Observable 的方式声明数据源,那么虽然我目前用同步的方式提供数据,但是将来我可以直接切换成 HTTP 数据源,而不用担心破坏现有代码。

细部原理

宏观上的要点已经讲完了,接下来我们快速过一遍微观的。我只讲要点,要想深入细节请参阅文中给出的参考资料。

Angular 模块

Angular 模块不同于 JavaScript 模块,它是一个架构级的基础设施,用来对应用进行宏观拆分,硬化边界,防止意外耦合。

模块的划分主要基于业务领域的边界,而在开发组织形式上,也要和模块划分方式相互对齐,尽量让每个模块都有明确的负责人。

参见 https://angular.cn/guide/ngmodules

路由

传统的路由功能完全是由后端提供的,但是在单页面应用中,在页面中点击 URL 时,将会首先被前端程序拦截,如果前端程序能处理这个 URL,那就会直接在前端处理,而不会向后端发送这个请求。

前端可以根据这个 URL 修改视图,给用户与后端路由一样的结果,但省去了网络交互的过程,因此会显得非常快捷。

路由是业务功能的天然边界,善用路由对于改善代码结构和可维护性是很有帮助的。

在 Angular 中,路由还同时提供了惰性加载等特性,因此,早期对路由进行合理规划非常重要。不过也不用过于担心,Angular 中重新划分路由的代价并不高。

参见 https://angular.cn/guide/router#appendix-emlocationstrategyem-and-browser-url-styles

模板与视图

你可以把模板看做 JSP,主要区别是 JSP 是后端渲染的,每次生成都需要一次网络交互,而模板是前端渲染的,在浏览器中执行模板编译成的 JS 来改变外观和响应事件。

模板语法

虽然看起来奇怪,但 [prop](click)*ngFor 等模板语法中的特殊符号都是完全合法的 HTML 属性名,实际上,属性名中只禁用各类空白字符、单双引号等少数几个显而易见的无效字符(正则:[^\t\n\f \/>"'=])。

参见 https://www.w3.org/TR/2011/WD-html5-20110525/syntax.html#syntax-attribute-name

属性与……属性

由于历史原因,英文的 Attribute 和 Property 都被译为属性,然而两者是截然不同的。Angular 中的常规绑定语法针对的都是 Property,只有 [attr.xxx] 绑定针对的是 Attribute。

参见 https://angular.cn/guide/template-syntax#html-attribute-vs-dom-property

组件与指令

你可以把组件看做后端模板中的 taglib,区别是它们运行在浏览器中而不是服务端。组件与指令在用途上的区别是,组件充当搭建界面的砖块,它的地位和 HTML 元素并无区别;而指令用于为 HTML 元素(包括组件)添加能力或改变行为。

所以,组件中不应该操纵 DOM,只应该关注视图模型,而指令负责在模型和 DOM 之间建立联系。指令应该是单一职责的,如果需要完成多个职责,请拆成多个指令附加到同一个元素上。

服务与依赖注入

Angular 的服务与依赖注入和 Spring 中的很像,主要的区别是 Angular 是个树状的多级注入体系,注入器树是和组件树一一对应的,当组件要查找特定的服务时,会从该组件逐级向上查找,直到根部。

这实际上是职责链模式。当前组件找不到某个服务时,就会委托给其父节点来查找。和策略模式结合使用,组件就可以通过自己提供一个服务来替换父组件提供的服务,实现一种支持默认处理的逻辑。

参见 https://angular.cn/guide/hierarchical-dependency-injection

表单与验证

在前端程序中,验证主要是为了用户友好性,而不是安全。安全是后端的工作,不能因为前端做了验证而放松。

Angular 对表单提供了非常强力的支持。如果你的应用中存在大量表单、大型表单、可复用表单或交互比较复杂的表单,那么 Angular 的表单功能可以为你提供强大的助力。

Angular 的表单提供了不同层级的抽象,让你可以在开发中轻松分离开显示、校验、报错等不同的关注点。也让你可以先用文本框快速搭出一个表单,将来再逐个把这些文本框替换成自定义编辑框,而不会破坏客户代码。

参见 https://angular.cn/guide/user-input

测试

Angular 对测试的支持非常全面,可以实现各个不同层次的测试。

但是不要因为拿到把这么好用的锤子就满世界敲。要根据不同的价值需求去决定测什么不测什么。

别忘了每个 Angular 的类,无论服务、组件、指令还是管道等,都是 POJO,你可以用测 POJO 的方式测试它们,得到毫秒级反馈,而且这往往会更高效。

参见 https://angular.cn/guide/testing。但要记住:虽然 Angular 支持这么多种方式,但你不一定要用到这么多种方式。

安全

在 Angular 中,你不会无意间造成安全隐患。只要注意一点就够了:DomSanitizer.bypassSecurityTrust* 要慎用,务必确保传给它的东西不可能被攻击者定制,必要时请找安全专家推演。参见 https://angular.cn/guide/security#sanitization-and-security-contexts。

如果你在发起 POST 等请求时收到了 403 错误,那可能是因为后端开启了 CSRF 防护。Angular 内置了一个约定 —— 如果服务端 csrf token 的cookie名是 XSRF-TOKEN,并且能识别一个名叫 X-XSRF-TOKEN 的请求头,那么它就会自动帮你完成 CSRF 验证。当然,你也可以自定义这些名称来适配后端,参见 https://angular.cn/guide/http#configuring-custom-cookieheader-names

跨域与反向代理

本地开发时,前端有自己的服务器,显然无法与后端 API 服务器运行在同一个端口上,这样就导致了跨域问题。要解决跨域问题,主要有 CORS 和反向代理这两种方式。CORS 是标准化的,但是受浏览器兼容性的影响较大;而反向代理则通过把 API “拉”到前端的同一个域下,从根本上消除了跨域访问。

开发时,Angular CLI 内置了对反向代理的支持;部署时,各个主流 Web 服务器都能很好地支持反向代理。

一般项目中建议还是优先使用反向代理的方式。

(图片来自:http://t.cn/RgsWKEJ

杂谈

你不必写 CSS

很多后端初学前端时会被卡在 CSS 上,在心里喊一句 WTF。但实际上,在团队开发中,你可能根本不必写 CSS。

现在已经有了很多现成的 CSS 库,比如已经熟透的 Bootstrap,还有后起之秀 Material Design、Ant Design 等等。你只要能根据其表达的视觉含义,正确套用它们定义的 CSS 类就够了。尽量不要自己手写 CSS,否则可能反倒会给将来的页面美化工作带来困扰。

选好了基础框架,并且和 UX 对齐之后,团队中只需要一个 CSS 高手就能实现所有的全局性设计规则。对于全栈工程师来说,充其量只有对当前页面中的少量元素进行定制时才需要写 CSS,况且还可以通过找人 pair 来解决偶尔碰到的难题。

全栈,让设计更简单

前后端技术各有所长,有些事情用前端实现更简单,有些用后端实现更简单。综合考量前端技术和后端技术,往往可以产生更简单、更优秀的设计。广度在业务开发中往往比深度有用,这也是全栈工程师的优势所在。而团队中的技术专家主要负责深度。

分工是动态的

技术专家或全栈工程师,并不是什么荣誉头衔,只是分工不同而已。

同一个项目上你可以同时担任全栈工程师和技术专家;这个项目你是全栈工程师,下一个项目上也可能专门担任技术专家。团队的协作方式永远是动态的、随需应变的。

不用担心全栈会限制你的技术深度,实际上,全栈对提高你的技术深度是有帮助的,因为很多技术的“根”都是互通的。

相信你的直觉

资深后端首先是一个资深程序员,你对于“应该如何”的期待,很可能是对的。如果你觉得 Angular 应该有某项功能或某种设计,它很可能确实有。去 Stackoverflow 搜一下,找找你的答案,这是你成为高级 Angular 程序员的捷径。

万法归一

形容某人聪明时经常说“万法皆通”,实际上“万法皆通”不如“一法通而万法通”。很多技术之间的相似程度超出你的想象,这些相似的部分其实就是技术的核心。用万法归一的思路去学习总结,会给你带来真正的提高。

资料 & 学习指南

学习 Angular 的最佳资料是它的官方文档,它无论是从准确、全面,还是及时性等方面都是最佳的。

它的英文文档站是 https://angular.io,中文文档站是 https://angular.cn,这是由我和另外两位社区志愿者共同翻译的,期间还得到了很多社区志愿者的支持。中文文档和英文文档至少在每个大版本都会进行一次同步翻译。虽然时间有限导致语言上还有粗糙之处,不过你可以相信它的技术准确度是没问题的。

阅读时,请先阅读架构概览 https://angular.cn/guide/architecture,然后阅读教程 https://angular.cn/tutorial(有经验的程序员不需要跟着敲代码,如果时间紧也可跳过),最后阅读风格指南 https://angular.cn/guide/styleguide。风格指南很重要,不用记住,但务必通读一遍,有点印象供将来查阅即可。

文档站中还提供了 API 参考手册,它提供了简单快速的站内搜索功能,需要了解哪些细节时到里面查就可以了。

另外,ng-zorro 组件库的一位开发者还整理了一份不完全指南,包括中英文资料:https://zhuanlan.zhihu.com/p/36385830


更多精彩洞见,请关注微信公众号:思特沃克

Share

它比微信小程序早出现半年,却不曾引爆技术圈

作为一个程序员,这两天的朋友圈被微信小应用刷屏了吧?想试了吧?没拿到邀请吧?没关系,我来帮你!

当然,我没法帮你拿到邀请码 —— 因为我也没有。不过,别失望,我今天要跟大家谈一项比微信小程序更宏观的概念 —— Google提出的PWA。

PWA是什么?

PWA全称是“Progressive Web Apps”,译成中文就是渐进式应用。这是Google在2015年6月15日提出的概念,参见Alex Russell写的《Infrequently Noted》

那么,什么是渐进式应用呢?

Alex写道:

在昨天的晚餐上,我和Frances列举出了新型应用的几个特征:

  • 响应式:适应任何形态因素
  • 不依赖网络:在Service Worker(H5中的新技术,appcache的升级版)的支持下能够离线使用
  • 像本地应用一样交互:采用“壳+内容”模式来实现类似本地应用的浏览和交互体验
  • 保持最新:始终透明的升级到最新版,这要感谢Service Worker提供的升级机制
  • 安全:通过TLS(这是Service Worker所要求的)来防止通讯被窥探
  • 可发现的:具有像“应用”一样的唯一标识,这要感谢W3C的Manifest文件格式,它在“Service Worker”中注册的scope能允许搜索引擎发现这些应用。
  • 可“再接触”的:可以访问操作系统的“可再接触”界面,比如推送通知
  • 可安装的:通过浏览器提供的提示,可以把它加入主屏,允许用户把觉得有用的应用“保留”下来,而不用到应用商店去安装,那样太麻烦了。
  • 可链接的:这意味着它们是零阻力、零安装,并且易于共享的。URL更能发挥社交传播的能量。

PWA怎么用?

一个PWA的使用过程大概是这样的:

这货开始就是一个普通网页:

01_pwa_start

然后,这是什么鬼?

02_pwa_prompt

这网站看着还不错,那就接受吧!

03_pwa_accept

它出现在手机的首屏了,以后随叫随到!

04_pwa_homescreen

点了就能像一个本地应用那样启动它:

05_pwa_launched

看,启动完也跟一个本地应用没有区别:

06_pwa_launcher

PWA的好处

Web应用(网页)和本地应用(App)各有优势,比如:

  • Web应用不需要安装,因此减少了用户流失(想想你要安装一个应用时有多么犹豫吧)
  • Web应用方便传播,你只要在微信或者QQ里发一个网址就有很多人会去看(比如本文 ^_^)
  • Web应用的收藏、前进、后退这些功能非常有用
  • 本地应用的留存度比较高。你看过一个网址后再回来的概率大约是本地应用的1/3。
  • 本地应用可以推送通知,而Web应用只有在浏览器打开状态下才行
  • 本地应用可以离线使用,只要做适当的设计就可以不受网络环境的影响
  • 本地应用可以访问更加丰富的功能,比如更大的本地存储空间

那么,它们能否合二为一呢?这就是PWA所做的事!

通过前面提到的这些步骤,PWA可以让用户同时获得这两种好处。这无论对于用户本身还是对于应用的开发者都非常有意义。

如何实现PWA?

其实从Alex的描述中就可以看到,在技术上我们离实现PWA并不远:

Web应用开发技术

已经具备。

特别是得益于Angular等SPA应用的普及,SPA不但响应更加快速,更加接近原生应用的操作体验,而且让我们可以完全不依赖服务器工作。

离线存储技术

可能有人还记得一项叫AppCache的技术,它已经废弃了,现在被Service Worker所取代。这是一个更强大的AppCache,它不仅能存储HTML/JS等静态文件,而且还让你能够在客户设备上运行起一个仿真的超轻量级Web服务器,你在里面已经几乎可以写Node程序了!

这是新的H5标准的一部分,而不是PWA特有的。

推送通知

H5标准中已经有了用来推送通知的API(用过Web Gmail的话应该见过),现在只要把它和操作系统的API对接就行了。当然,这最好由操作系统提供支持,这已经实现了,那就是新推出的Android 7。

App壳程序

这个壳程序其实自己来实现也没问题,技术上没啥难度,不过不用重复造轮子了,因为官方已经出了一个,参见https://github.com/GoogleChrome/application-shell。如果用Angular 2就更幸福了,因为Angular 2还制作了一个Angular 2专用的壳:https://github.com/angular/mobile-toolkit。不过,它现在还处于未发布状态,早鸟可以开始提前尝试了。

微信小程序与PWA

虽然”微信小程序”概念的提出比PWA晚了半年,不过相信张小龙这个级别的产品经理不会是简单的抄袭PWA概念,而是会发挥微信生态圈的优势,提供更广阔的应用场景。或许我们应该把微信小程序看做PWA的一种优质实现。

假如你没有拿到微信小程序的邀请码,不妨像我一样,先从研究PWA开始吧。

Share

弯道超车!后端程序员的Angular快速指南

开篇寄语 —— 弯道超车,为时未晚

前端领域如火如荼,工资水平也水涨船高。作为后端程序员的你,羡慕吗?但羡慕是没用的,更别提嫉妒恨了。古人曰:与其临渊羡鱼,不如退而结网。

接下来,我不但要教你结网,还要教你后端程序员弯道超车的秘诀。我将对前端领域的概念进行简要说明,并尽量用后端领域的概念来作类比,受到笔者个人背景的限制,可能会更多使用Java世界的概念来进行类比,不过.net等世界也大同小异。

笔者就是从后端程序员转型而来,深知阻挠后端程序员转型的那些拦路虎。比如:陌生的工具链、陌生的语言、庞杂的W3C标准、不一样的设计风格、不一样的编码风格等等,不过最大的拦路虎是……框架太多!每套框架都号称自己有一系列优点,让人眼花缭乱,无从抉择。

事实上,对于前端程序员来说,在这些框架之间进行选择确实很困难,因为有时候前端程序员会过于沉迷细节。比如,他/她可能在50毫秒和100毫秒的响应时间之间举棋不定,可能会为了实现细节上的优点,而影响项目管理和可维护性。最严重的问题可能还在于:误判了前端需求的增长速度和工程化开发的重要性,而这正是后端程序员们弯道超车的好时机。

细说从头

1-origin

以2004年Gmail的推出为起点,前端领域已经呈现出爆炸性增长的趋势,经过十多年的成长,它是否即将进入停滞期?我只能说“Too young, too naive!”。在Web初兴的时候,同样有编写传统桌面应用的程序员觉得它会很快成长到停滞期,不过结果大家都已经看到了。在我看来,并不是什么“前端编程技术”横空出世了,而是它的需求一直都存在,却由于技术条件不足而被压抑着 —— 直到Web技术成熟到能把它释放出来,这才导致了前端横空出世并急速膨胀的假象。

回忆一下,在“前端”的概念诞生之前,我们是怎样实现一个Web应用的?我们会先在服务器上合成一段HTML,把它发回给浏览器;之后,几乎任何操作都会向服务器发送一个请求,服务器再渲染一个完整的新页面发回来。

跳出习惯性思维,反思一下:这是自然的吗?我们为什么会需要如此复杂的过程?这其实是被迫做出的妥协:在从Netscape诞生开始的很长时间内,浏览器中的JS都是一个“玩具语言(这是JS之父说的)”:语法繁杂、坑多、解释器性能低下、无模块化机制、无成熟的工具链,无成熟的第三方库等等,JS程序员(如果当时有的话)也毫无悬念的占据了鄙视链的底端。

面对JS这样一位“猪队友”,程序员们还能怎么办?只能求助于万能的服务端语言了:它几乎不会受到浏览器的制约,可以自由使用所需的一切编程资源。幸运的是,Web技术的标准化工作在这个过程中得以蹒跚前行,而JS的标准化工作也在三大浏览器巨头的博弈中艰难的前进着。

Chrome,特别是V8引擎的诞生,终于结束了JS解释器的性能问题,更重要的是,基于V8引擎,诞生了伟大的NodeJS。NodeJS就是前端世界的JRE或.net CLR。它主要有三大贡献:

  1. 让JS语言“入侵”了后端世界和桌面世界。

    这在前端开发的襁褓期有效扩大了JS语言的适用范围,积累了大量第三方库,很多第三方库只要在合适的工具支持下也能在前端领域正常使用。

  2. 为前端开发提供了工具链。

    如今的前端世界,其工具链的复杂度和完善度已经逐渐逼近后端世界,比如:类似于gradle的构建工具gulp、grunt,类似于maven的包管理工具npm,类似于junit的测试框架karma等等,它们无一例外,都是基于NodeJS的。

  3. 提供了标准化的模块化方案CommonJS。

    在NodeJS诞生之前,模块化一直是JS世界的短板,虽然也有不少相互竞争的JS模块化方案,却都没能一统江湖,这主要是因为当时的很多前端应用都过于简单,对模块化并没有迫切需求。而随着NodeJS入侵到后端世界和桌面世界,模块化成了不得不做的事情,于是NodeJS内置的CommonJS就成为了事实性的标准。JS的最新版本ES6中内置的模块化机制就是类似于CommonJS的。

与此同时,在另一条战线上,还有一些技术在平行推进,那就是前端DOM库与前端框架。在2006年,一个名叫jQuery的DOM库横空出世,它封装了复杂的、特定于浏览器的DOM操纵类API,让程序员可以不必处理一些繁琐的细节差异,从而简化了浏览器中的DOM编程。

在那个时代,虽然尚未正式提出“前端”的概念,不过已经出现了不少事实上的前端程序。但这些前端程序相对于如今包罗万象的前端还是过于原始了,很多前端代码都只是嵌入在后端页面中的龙套。

不过,这些程序就像最早爬到岸上的鱼一样,带人们发现了一个新世界,对前端程序的需求也随之井喷。什么是“前端程序”呢?在我看来主要有如下几个特征:

  1. 客户端渲染

    与传统上借助后端生成新页面的方式不同,前端程序借助浏览器的API来呈现内容(也就是“渲染”)并处理用户动作,在这个过程中,并不需要借助服务端的运算能力,也不需要网络。

  2. 单页面

    客户端渲染技术衍生出的一个主要特征是单页面应用。因为不需要再由服务器发回新页面,所以前端程序在理论上就具备了独自渲染内容并全权处理用户交互的能力,只在必要时,才会通过Web API寻求服务器的帮助。

  3. 实时反馈

    客户端渲染技术衍生出的另一个主要特征是实时反馈。在传统的应用中,除非内嵌JS代码,否则任何反馈都需要由服务端代码生成并发回,而且编程相对复杂。这导致很少有程序能够给出实时反馈,即使做到了实时反馈的,也会因为网络延迟等问题而损害用户体验,而专业的前端程序则可以借助客户端运算轻松实现实时反馈。

随着技术的进步,前端终于具备了摆脱“石器时代”的条件,于是,前端的时代终于要开始了。

前端时代!

2-competition

以jQuery为代表的DOM库在使用中逐渐暴露出了很多缺点,特别是混杂逻辑代码和操纵DOM的代码导致难以维护。于是一大批新的前端MV*框架悄然出现了。

框架不同于库:库是一组被动式的代码,如果你不调用它,它就什么都不做;而框架不同,框架提供了启动、事件处理等各种通用性代码,你按照框架规约写自己的代码,并把它“告诉”框架,框架会在合适的时机用合适的方式调用它们。

确实,这没什么新鲜的,你早就用过Spring或asp.net了,不是吗?从这一点上来说,前端框架与后端框架大同小异。不过,前端框架还是有自己的鲜明特色的:

  1. 它们是……用JS写的。

    毫无疑问,JS尽管并不完善,但它目前是并且仍将是前端世界的霸主,我认为其中最重要的原因是:它是各大浏览器厂商利益的最大公约数。Google曾孵化了一个在浏览器和后端共用的语言Dart,不过现在连自己的浏览器都不打算直接支持它了。从技术上讲,Dart无疑是相当先进的,但现实却更加残酷。

  2. 它们是弱类型的。

    受限于JS的能力,前端框架无法访问运行时类型(就像Java或.net中的反射机制),也就无法像后端框架那样大量借助接口来定义扩展方式。因此,框架只能借助一些复杂的技巧来达成目标。当然,后续的技术发展在一定程度上改变了这一点,那就是微软的新语言TypeScript的诞生,我们稍后再展开这个话题。

  3. 它们是灵活的。

    得益于JS的动态特性和弱类型特性,前端框架也非常灵活,比如你可以把任意对象传给调用者,只要这个对象有调用者所需的属性或方法即可,而不用像Java那样明确定义接口。灵活,是优点,也是缺点 —— 在小规模、需求稳定的程序中,它可以极大的提高开发速度,用过Ruby或者Python的程序员大概深有体会;但在大规模、需求频繁变化的程序中,它将是BUG之源,用过Ruby或Python的程序员大概深有体会。

那些年,前端MV*库的竞争,其激烈程度几乎不下于各种语言的竞争。2009年,一个注定要名声大噪的框架加入了这场前端MV*大战,它叫Angular。

Hello, Angular!

3-angular-1

Angular的英文原意是“角”,也就是“锐角、直角”的“角”。它的主要开发者是Adobe Flex的开发者Misko以及很多来自Google的后端程序员,因此它有很多理念和概念来自于Flex和后端编程领域,如声明式界面(Declarative UI)、服务(Service)、依赖注入(Dependency Injection)等,并为单元测试提供了优秀的支持。可以说,它天生就有后端基因,其设计目标也是处理像传统后端一样复杂的需求。幸运或者不幸的是,它仍然是一个前端框架。它具有高度的灵活性 —— 既可以写得很规整,也可以写得很烂。当然,在某种意义上,这不应该算作Angular的问题,而是JS的“原罪”。

这种情况意味着,如果有成熟的最佳实践和优秀的开发规范,Angular程序可以写得很漂亮:简洁明了、模块清晰、分层明确、关注点分离。但在开发组意识到社区需要一份来自官方的开发规范之前,Angular 0.x和1.x版本的烂代码和坏习惯已经泛滥成灾了。

幸运的是,Angular有一个繁荣、强大的社区,社区在行动。无论是英文社区还是中文社区,都出现了一些优秀的Angular工程师,他们总结出了一些经验和教训,并给出了自己的解决之道,全凭自己的力量与热情在社区中传播。如果你是一个后端程序员,会发现这些最佳实践和开发规范似曾相识。没错,很多优秀的Angular工程师本来就是后端工程师出身。这并不奇怪,前端岁月尚浅,优秀的前端工程师当然会有很多是从优秀的后端工程师转型而来的。

但这还不是根本原因。在有些人的思维中,前端和后端好像是两个截然不同的世界。并非如此!编程之道本来就是互通的,并不存在前端的编程之道和后端的编程之道。主导这两个开发领域的设计原则不外乎就是SOLID等少数几个,无论是前端的编程规范还是后端的编程规范,都是对这些原则的实例化。

社区的努力,在一定程度上弥补了Angular早期版本的缺憾,但,这还不够。我们需要一份官方的开发规范,甚至,一个更好的Angular。后者才是重点!

Hello, Angular 2!

优秀的框架特性、繁荣的社区、广泛的应用,但都被ES5(JS的早期版本)这个猪队友给拖累了,另一个猪队友则是老版本浏览器 —— 特别是IE8及更低的版本。于是,就在Angular 1.x如日中天的时候,Angular开发组高调开始了新版本的开发工作,它就是Angular 2!这里还有很多小插曲按下不表,等我有时间开杂谈时再慢慢说。

Angular 2本身不再是用ES5写成的,而是TypeScript,简称TS。TS是微软开发的一个新语言,它是ES6的超集,这意味着,凡是有效的ES6代码都同样是有效的TS代码;另一方面,ES6是ES5的超集,所以凡是有效的ES5代码也同样是有效的TS代码。但是在ES6的基础上,TS增加了可选的类型系统以及在未来ES8中才会出现的装饰器等特性。

你想知道TS为什么这么牛?很简单,因为他爸是 —— 不,不,不是李刚,他爸是Anders Hejlsberg,如果Java程序员没听说过他还情有可原,如果是.net程序员,那就自己去面壁思过吧 —— 他是微软.net的总架构师,C#语言之父,更资深的程序员可能还用过Delphi,那是Anders的“长子”。一个人设计了三个流行的工业级语言,也真是够了。

虽然TS已经诞生了很久,但却一直没有流行起来,这主要是因为它还缺少一个“杀手级应用”。现在,Angular 2来了!

在摆脱了一个猪队友之后,Angular 2终于可以随心所欲的展示自己的风采了,比如:基于类型的依赖注入、强类型的库文件、更加便捷的语法、标准化的模块化机制等等,无法一一列举。

但还有另一个猪队友在拖后腿,那就是老式浏览器,对,说的就是你 —— IE 8!Angular从1.3开始就彻底抛弃了它,2.x就更不用说了。有一阵子,曾经传言Angular 2不支持IE 11以下所有版本的IE,不过幸好,Angular开发组终于对现实做出了妥协,否则这又会是一个重大的公关危机了。能与IE 8说再见,真好。不过,这也意味着,当你准备开始用Angular 2做项目的时候,务必先跟客户或产品经理敲定不需要支持IE 8,否则还是老老实实用Angular 1.2吧。

Angular 2,后端之友

Introduction-to-website-building-using-Angular-2-740X296

在Angular 1中就从后端借鉴过很多概念,到Angular 2自然就更进一步了。这些概念对没有做过后端开发的新前端来说会有一定的难度,不过对后端程序员来说这不过是小菜一碟。接下来我们就逐个讲讲。

服务与依赖注入

没错,它们跟后端的服务与依赖注入是同一个概念,只是在实现细节上略有不同:

后端的服务是一个单例,在Angular 2中同样如此;

后端的服务是使用类型来注入的,在Angular 2中同样如此,不过由于TS的限制,Angular 2中通常会根据类进行注入,而不是像传统的后端程序那样优先使用接口;

后端的依赖注入器是由框架提供的,Angular 2中同样如此;

后端的依赖可以进行配置,Angular 2中同样如此,不过它的配置方式更加灵活,它不需要单独的配置文件(该死的XML),而是直接用程序代码,这赋予了它额外的灵活性,却几乎没有损失(这让我想起了Grails)。

不过Angular 2的依赖注入体系比传统的后端更加灵活,它是一棵由多个注入器组成的树,这棵树跟组件树平行存在。你可以把局部使用的服务放在中下层节点上,来限制它的作用范围,减小耦合度;你可以预留一些占位(Placeholder)服务,等待调用方实现它,以达到“用组合代替继承”的效果(要了解详情,请自行分析LocationStrategy的设计);可以在不同的层级上配置同一个类的不同依赖实例,这样它就可以覆盖掉上层的配置,在必要时临时建立一个“独立王国”。

可选强类型

强类型是很多Java程序员信心的保障,但同时也因为过于繁琐而饱受抨击。

现在,它随着TS又来到了前端世界。不过不用害怕Java世界中的悲剧重演,因为TS中的强类型是“可选”强类型。这意味着你可以完全不定义变量、属性、参数等的数据类型,TS编译器也会照样放行。当你需要快速建立一个原型时,这种特性会非常有用,因为你不用现在就做很多决策。但当有一天你的原型经历了从产品经理到CEO的重重考验,终于修成正果的时候,你会发现它“太烂”了。

这是好事,这说明你在开发过程中没有浪费精力。但如果你想继续像这样把它发展成一个产品级应用,那就要悲剧了。因为代码中有太多只有你自己知道的约定和隐式接口,但新过来和你进行合作开发的人是无法和你心灵相通的。用不了多久,本来就是一团面条的代码就变成了一坨浆糊,然后你就开始了无止境的加班岁月。没错,“福兮祸之所依”,现实就是这么残酷。

为了走得更远,你先得为代码中的变量、属性、参数等标上数据类型、抽象出接口,并且基于它们建立相应的开发规范(最好能用持续集成(CI)工具进行保障)。有了这些,即使是两个负情商的大老爷们儿也能轻松做到“心灵相通”了。

加完类型之后,你仿佛回到了自己所熟悉的后端领域。现在,你的地盘儿,你做主!

测试驱动开发

如果测试驱动开发还不是你的基本功,那可能说明你在后端开发方面还有短板。即使你不是想做全栈,而是想完全转型成前端,也应该补习一下测试驱动开发的技能。因为未来的前端开发,即使在纯逻辑类代码的复杂度上都可能会赶上后端。

在1.x的时代,Angular就以其优秀的“可测试性”而著称了,Angular 2当然不会放弃这个传统优势。Angular 2的单元测试更加简单,我还是直说吧:Angular 2中单元测试的方式更像后端。在Angular 1.x的时代,单元测试中不得不使用诸如$controller(如果你不懂,请忽略它)等框架内部API,而Angular 2测试框架的设计中完全封装了它们,当你测试一个组件时,大部分时候几乎就是在测试一个普通的类。

传统的前端程序员可能不太容易理解测试驱动开发的思维方式,特别是对于没有什么后端经验的资深前端。这也同样是后端程序员实现弯道超车的好机会。随着前端职责的加重,在前端代码中,会出现越来越多的复杂逻辑,这些复杂逻辑如果没有测试驱动开发的保障,将被迫用“写代码、切换到浏览器、界面上点点看看、切换回IDE”的低效循环进行开发。

更重要的是,它很容易诞生高度耦合、恰好能用的烂代码。但在测试驱动开发的保障下,可以先从最简单的规约开始,逐步补充更多规约。在开发过程中,你只要不时瞥一眼IDE的测试控制台就可以了。这样不但开发起来更快,而且可以收获良好的代码结构,因为容易测试的代码通常也都是松耦合的。

分工,1+1 > 2

后端程序员学习前端技术时,往往会为HTML/CSS等头疼不已,这些都是相对陌生甚至完全陌生的领域,如果急于为团队贡献生产力,那么请把这些“后背”交给你的队友。

延续Angular的一贯传统,Angular 2对团队分工提供了卓越的支持,它通常会把一个界面分成模板(*.html*.jade)、样式(*.css*.scss*.less*.styl)、组件(*.js*.ts)和组件单元测试(*.spec.ts*.spec.js)等几个基本名(base name)相同的文件,它们被放在独立文件中但能很好的相互协作。

当你的前端技能还在蹒跚学步的时候,请放心的写下一些粗糙的HTML/CSS代码,比如用div搭建出丑陋但能与你所写的组件顺畅协作的html文件。然后,提交它,等你的队友帮你把它修成漂亮的产品级界面。同样的,如果你的前端队友还不太清楚该如何干净漂亮的从组件中抽取出服务,那么你就可以放心的帮他/她修改组件代码,而不用担心无意间破坏了模板和样式。

一个团队中,如果能有谦逊的super star当然好,但这种团队是可遇而不可求的,更现实的期望是一个能相互信任、各尽所能的团队。而Angular在设计时就充分考虑了团队分工的需求,要想建设这样一个团队,毫无疑问,Angular将是你的首选平台!

结束语 —— Angular 2,全栈养成计划

5-team

好吧,我承认我可耻的做了一次标题党。本文并非在煽动后端程序员去革前端程序员的命,而是希望无论是前端程序员还是后端程序员,都能成长为优秀的全栈程序员(是的,前端程序员如果理解了Angular 2中的这些概念也会更容易向后端发展)。全栈程序员由于能有效节省沟通成本(比如不用频繁协商API)而被很多开发组织寄予厚望,但真正培养起来可没那么容易。

有一阵子,培养全栈程序员的期望被放在了Fullstack JavaScript上 —— 它既能写前端程序又能写后端程序还能写桌面程序。不过事实证明,这种期望落空了。即使经过了大爆发,NodeJS在企业应用开发、大数据等领域的资源积累也远远不及Java、C#、Python,甚至将来还有被新崛起的Scala和Go超越的危险。

或许我们应该换一种思路了:全栈一定要用同一种语言写前端和后端吗?

并非如此。事实上,我们更应该看重的是编程模型、思维方式和协作模式等方面的复用,而语言层面只是细枝末节而已。所以,Java或C#,加上TS与Angular 2,给了培养全栈的新曙光。相似的概念模型、相似的思维方式、相似的协作模式,这才是全栈程序员真正的核心技能,与语言无关。

这些,才是Angular 2给专业开发团队带来的,最珍贵的礼物!

Share

我为什么选择Angular 2?

没有选择是痛苦的,有太多的选择却更加痛苦。而后者正是目前前端领域的真实写照。新的框架层出不穷: 它难吗?它写得快吗?可维护性怎样?运行性能如何?社区如何?前景怎样?好就业吗?好招人吗?组建团队容易吗?

每一个框架都得评估数不清的问题,直到耗光你的精力。这种困境,被称为“布利丹的驴子” —— 一只 驴子站在两堆看似完全相同的干草堆中间,不知道如何选择,最终饿死了。

 

当然,那只是一个哲学寓言。现实中,大多数人采用了很简单的策略来破解它:跟风,选择目前最流行的那个。这是一个低成本高收益的策略, 不过也有风险:成为现实版的《猴子下山》。最理想的方案还是要看出这两堆“干草”之间的差异,选择更适合自己的那个。

本文就将带你了解Angular 2这个“干草堆”的各种细节。

ALL-IN-ONE

不管是1还是2,Angular最显著的特征就是其整合性。它是由单一项目组常年开发维护的一体化框架, 涵盖了M、V、C/VM等各个层面,不需要组合、评估其它技术就能完成大部分前端开发任务。 这样可以有效降低决策成本,提高决策速度,对需要快速起步的团队是非常有帮助的。

让我们换一种问法吧:你想用乐高搭一个客厅,还是买宜家套装?

Angular 2就是前端开发领域的“宜家套装”,它经过精心的前期设计,涵盖了开发的各个层面,层与层之间都经过了精心调适, 是一个“开箱即用”的框架。

当然,你可能还记着Angular 1带给你的那些快乐以及……痛苦。这是有历史原因的。由于它是从一个用来做原型的框架演化而来的, 加上诞生时间很早(2009年,作为对比,jQuery诞生于2006年),生态不完善,连模块化机制都得自己实现 (这就是angular.module的由来,也是Angular 2中抛弃它的理由)。在长达七年的演化过程中,各种进化的遗迹非常明显,留下了不少“阑尾”。

但Angular 2就不同了,它的起点更高,整合了现代前端的各种先进理念,在框架、文档、工具等各个层面提供了全方位的支持。 比如它的“组件样式”能让你在无需了解CSS Module的前提下获得CSS Module的好处,它的Starter工程能让你在无需了解Webpack的 前提下获得Hot Module Replacement等先进特性,它能让你从Web Worker(你知道这是什么吗?)中获得显著的性能提升。

你只管在前台秀肌肉吧!剩下的,让Angular在幕后帮你搞定!

逃离“版本地狱”

如果是一两个人开发一个预期寿命不到一年的系统,那么任何框架都可以胜任。但,现实中我们都把这种系统称之为“坑”。 作为一个高度专业、高度工程化的开发组,我们不能把“坑”留给友军。这些坑中,最明显的就是“版本地狱”。

想象一下,你仅仅升级了库的小版本号,原来的软件就不能用了,损坏了N处单元测试以及M个E2E场景。 这种情况对于开发组来说简直是一个噩梦 —— 毕竟,谁也不想做无用功,更何况是一而再、再而三的。 或者,它每次小的改动都会修改大版本号 —— 对,我就是不向后兼容,你能咋地?你所能做的就是每一次决定升级都如临大敌, 不但要小心翼翼的升级这个库本身还要升级与它协作的几乎所有库。

可见,乱标版本号可能会让使用者付出巨大的代价。这不但体现在工作量上,还会体现在研发团队的招募与培养上,想象一下, 对小版本之间都不兼容的框架,研发团队都需要记住多少东西?那简直是噩梦!

但是,版本号的问题在业内早就有了事实性标准,那就是SemVer语义化版本标准。 唯一的问题是,作为框架/库的作者,遵循SemVer标准需要付出的努力是巨大的。是否愿意付出这些代价, 是一个框架(及其开发组)是否成熟的重要标志。

Angular就是这样一个框架,其开发组曾作出的努力是有目共睹的。如果你是从Angular 1.2开始使用的,那么你为1.2所写的代码 都可以毫无障碍的迁移到最新的1.5版,新的版本只是引入了新特性,而没有破坏老特性。 这是因为Angular开发组写了大量单元测试和E2E测试,借助CI来保障这种平滑过渡。只有在从1.0到1.2的迁移过程中(1.1一直是beta,忽略不计), 出现了一系列破坏性变更,这些变更被明确的记录在文档中,并且解释了做出这些变更的理由 —— 大多数是因为提升前端代码安全性。 即使你恰好遇到了这些破坏性变更,它也会给出明确的错误提示。

这些必要而无聊的工作,正是帮助我们逃离“版本地狱”的关键。是否愿意做这些无聊而琐碎的工作,是一个框架的开发组是否成熟、专业的关键特征。

模块化的技术

虽然Angular是一个ALL-IN-ONE的框架,但这并不妨碍它同时是一个灵活的框架。还记得OCP(开闭原则)吗? 一个好的设计,对扩展应该是开放的,对修改应该是关闭的。

Angular 2很好的践行了OCP原则以及SoC(关注点分离)原则。

它非常有效的分离了渲染与交互逻辑,这就让它可以很好的跟 包括React在内的渲染引擎搭配使用,除此之外,它还可以使用内存渲染引擎,以实现服务端渲染;还可以使用Native渲染引擎, 以编译出真正的原生程序(NativeScript)。

它还分离了数据供应与变更检测逻辑,从而让它可以自由使用包括RxJS以及ImmutableJS在内的第三方数据框架/工具。

不仅如此。

在Angular 1和Angular 2中最具特色的应该算是依赖注入(DI)系统了,它把后端开发中用来解决复杂问题、实现高弹 性设计的DI技术引入了前端。Angular 2中更是通过引入TypeScript赋予它更高的灵活性和便利性。

不过,Angular 2并没有敝帚自珍,把它跟框架本身紧紧黏结在一起,而是把它设计成了一个独立可用的模块。 这就意味着,无论你正在使用什么前端框架,甚至NodeJS后端框架,都可以自由使用它,并从中获益。

开放的心态

当年的浏览器大战,让人记忆犹新,Chrome的出品商Google和IE的出品商微软正是浏览器大战的两大主角。

俗话说:没有永远的朋友,也没有永远的敌人,只有永远的…… 精益求精。

正是在这样的“俗话”指导下,Google与微软相逢一笑泯恩仇,撤销了很多针对彼此的诉讼,甚至在一些领域开始合作。 而实际上,在他们公开和解之前,就已经开始公开合作了,其契机就是Angular 2。

这就要扯出一点小八卦了。

在Angular 2开发的早期阶段,由于传统JS(ES5)以及当时的ES6草案无法满足项目组的开发需求,项目组有点烦。 后来有人灵机一动开发了一种叫做AtScript的新语言,它是什么呢?一个带类型支持和Annotation支持的升级版JS。 其实在类型支持方面,TypeScript早就已经做了,而且那时候已经比较成熟,唯一的遗憾是不支持Annotation, 也就是像Java中那样通过@符号定义元数据的方式。

Angular开发组就这样孤独的走过了一小段儿时间,后来也不知道怎么这两个欢喜冤家就公然牵手了。 总之,最后的结果是:TypeScript中加入了Annotation特性,而Angular放弃了AtScript改用TypeScript。

这次结合无论对两大厂商还是对各自的粉丝,都是一个皆大欢喜的结局,称其为世纪联手也不为过。 此后,Google与微软不再止于暗送秋波,而是开始了一系列秀恩爱的动作。无论如何,对于开发者来说,这都是一个好消息。

软粉们可能还记得在6.1的微软开发者大会上,微软曾公开提及Angular 2。事实上,TypeScript目前虽然已经相当完备,但应用度仍然不高。 就个人感觉来说,Angular 2将是TypeScript的一个杀手级应用。TypeScript如果当选TIOBE年度语言,Angular 2的推出功不可没。

为什么我要特意插播这段儿故事呢?那是因为我认为一个产品的未来最终取决于开发组的未来,而不是相反。 软件的本质是人!一个心态开放、讲究合作、勇于打破陈规陋习的开发组,才是框架给人信心的根本保障。

全生命周期支持

除非你打算写一个一次性的代码,否则软件的生命周期会很长。宏观来看,真正的挑战来自多个方面,而且在不断变化。 开始的阶段,我们需要非常快速的建立原型,让它跑起来,引入最终用户来试用,这个时候,挑战来自开发速度以及可复用资产。

之后,会建立一个逐渐扩大的项目组,来完善这个原型的功能。主要的挑战是让这个原型通过重构不断进行演化, 特别是在演化的后半个阶段,产品需要保持随时可以上线的状态。但技术上的挑战那都不是事儿!关键是人。

如何让新人快速融入项目组,贡献生产力?这可没那么简单。“你先去学xxx 0.5、yyy 0.9、zzz 5.3… 还要了解它们前后版本之间的差异,我再给你讲代码”,这种话,我可说不出口。 一个优秀的框架需要对分工提供良好的支持,每个人都可以先从一些简单任务开始,逐步的从修改一个文件扩大到修改一个目录再到独立实现一个特性。

有了这种分工,每个团队成员就可以从一个成功走向一个更大的成功。这就需要框架在设计上具有良好的局部性: 你可以放心大胆的修改一部分,而不用担心影响另一部分。你可以只修改CSS,可以只修改HTML,可以只修改TS/JS, 而不用担心自己的修改破坏了其他部分。而Angular 2通过声明式界面、组件样式以及独立模板文件等对这种安全感提供了有力的保障。

再然后,如果你的软件顺利的进入了线上维护阶段,那么恭喜你,你终于迎来真正的大Boss了!这个大Boss就是可维护性。 Angular开发组有很多程序员来自Google,如果要问谁的软件维护经验最丰富,Google和微软必然位列其中。 微软通过TypeScript的强类型机制体现了自己的经验,而Google则通过将OCP、SoC、SRP等一系列软件设计原则融入Angular体现了自己的经验。 具体技术上则体现为:DI、生命周期钩子、组件等等。

最后,如果你的软件完成了历史使命需要逐步退出,是不是就能松口气了?不行,你还得继续留人维护它。 如果你选择的是一个闭源的或某个社区很羸弱的开源技术,你就把以前的主力架构师、资深程序员留下来继续维护它吧。 或者你就得鼓起勇气跟用户说:你们玩儿,我先走了。而Angular是一个大型开源项目,并得到了Google的鼎力支持。 即使经历过Angular 2项目组初期的公关失败,它仍然有着其它竞品无法企及的繁荣社区 —— 无论在全球还是在中国。 显然,无论是对传统社区资源的继承,还是对新社区资源的开拓,我们都有理由相信,Angular社区仍将一如既往地繁荣。 至少,如果你不想让自己的软件建立在一大堆由个人维护的核心库上,那么Angular毫无疑问是最好的选择。

遇见未来的标准

编程领域,创新无处不在。但对一个工程团队来说,最难得的是标准。标准意味着:

  • 招人容易。因为标准的东西会的人最多,而且人们愿意学它 —— 因为知道学了就不会浪费。
  • 养人容易。因为网上有很多教程可用,即使不得已自己做教程,性价比也是最高的。
  • 换人容易。标准就意味着不是私有技术,一个人离开了,就能用另一个人补上,而不会出现长期空缺,影响业务。

但是,标准永远会比创新慢一拍或N拍。这就意味着如果你追新,那么在获得技术上的收益的同时,也要付出工程上的代价。 固然,两者不可兼得,但是还是有一些策略能够调和两者的。最简单的策略就是:遇见未来的标准。

所谓未来的标准,就是某个标准的草案,它很有希望成为未来的标准,这代表了行业对某项技术的认可,于是,即使它还不成熟,人们也愿意为之投资。 比如虽然Google曾经提出过N种自有技术,包括SPDY、Gears、OGG等,但却并没有把它们变成私有技术,而是对社区开放以及并入W3C的标准草案。

Angular 2采用的就是这种策略。它所参照的标准草案比较多,一个是WebWorker,它借助WebWorker来把繁重的计算工作移入辅助线程, 让界面线程不受影响;另一个是WebComponents,Angular 2的组件化体系就是对WebComponents的一种实现;最后是CSS scoping, 现在虽然市面上有了很多CSS模块化技术,但事实上最终还是会被统一到CSS Scoping标准上, 届时,如果:local等关键字无法进入标准,就会成为私有技术。而Angular 2选择的方式是直接实现CSS scoping标准草案, 比如:host伪类、:host-context伪类等。 显然,采用这种策略,“遇见未来的标准”的成功率会高得多。

可以看到,Angular 2的设计哲学中贯穿着对标准化的热烈拥抱,这是我判断它将赢得未来的另一个信心来源。

速度与激情

Angular 2终于摆脱了旧的技术框架束缚,开始了对速度的极致追求。在Angular 1中,虽然绝大多数场景下性能都不是问题, 不过还是因为其代码中存在的一个用来实现脏检查的三重循环而饱受抨击 —— 似乎真有人能感受到30毫秒和100毫秒的差异似的。

不过,有软肋总是不太好。于是,在Angular 2中,通过重新设计和引入新技术,从原理上对速度进行了提升, 据官方以前提供的一个数据说是把“变更检测”的效率提升了500%。

它在“变更检测”算法上做了两项主要的改进:

  1. 在设计上,把以前的“多轮检查、直到稳定”策略改成了“一轮检查、直接稳定”策略。 当然,这会对自己的代码产生一定的限制,但实际上只在有限的少数几个场景下才会遇到这个限制,而且官方文档中也给出了明确的提示。
  2. 在实现上,把“变更检测”算法移入了由WebWorker创建的辅助线程中执行。 现代的家用电脑基本都已经是多核超线程的,但是浏览网页时实际上无法充分利用全部线程,而WebWorker提供了一个全新的机制, 让一些相对繁重的计算工作运行在辅助线程中,等执行完了再通知主线程。即使你不明白WebWorker的工作原理, 至少也能知道Ajax请求是不会阻塞界面响应的,WebWorker也一样。

除此之外,Angular还对数据源进行了抽象,你完全可以用ImmutableJS来作为Angular的数据源,获得其在提升“变更检测”速度方面的所有优势。

除了“变更检测”外,Angular以及所有前端SPA程序,都有一个通病:首次加载太慢,要下载完所有js和css才能渲染出完整的界面来。 React通过虚拟DOM解决了此问题,而Angular 2则通过独立的服务端渲染引擎解决了这个问题。 我们前面说过,Angular 2把渲染引擎独立了出来,因此可以在NodeJS中实现服务端的内存渲染。 所谓内存渲染就是在内存中把DOM结构渲染出来并发回给浏览器,这样,客户端不需要等JS代码下载完就可以显示出完整的界面了。 这种分离还带来了另一个好处,那就是SEO。SEO同样是传统SPA的一个难点,它和服务端渲染是同一个问题的两个方面, 因此随着服务端渲染的完成,SEO问题也被顺便解决了。

让你无缝升级

Angular 2开发组在早期阶段曾犯下一个严重的公关错误:过早宣布不兼容Angular 1,也不提供从Angular 1到2的升级方案。

这让Angular 2开发组付出了沉重的代价,可谓伤透了粉丝的心。作为技术人员,这种失误固然是可以理解的,但却需要付出更多的努力来弥补它。 而Angular 2确实这么做了。

在Angular 2中,开发组提供了一个UpgradeAdapter类,这个升级适配器让Angular 1.x的所有部件都能和Angular 2.x中的所有部件协同工作。

而最牛的地方在于,它让你可以一个文件一个文件的逐步升级到Angular 2,而在整个升级过程中,应用可以继续在线上跑!看着神奇吗? 其实也算不了啥,Google做这种事早就是轻车熟路了。这只不过是对Angular开发组出色的工程化开发能力的一点小小证明而已。

不过,既然如此,当初又何必急匆匆宣布不兼容呢?可见, 再出色的工程团队,如果缺少了和社区沟通的产品运营技巧,也会付出巨大代价。希望Angular开发组以后能谨言慎行,多用行动来平息质疑。

后面我还会有专门的文章以亲历者的视角讲述这个故事。

Share