萧暮

关于 Angular 2 那些让我兴奋的东西

萧暮 · 2016-12-19翻译 · 1170阅读 原文链接

在过去的半年里,我花了大量时间玩 Angular 2。创建了一些有趣的项目和与之相关的讨论写了若干文档并和其他小伙伴讨论他们正在做的事。然后我还写了一系列文章。我觉得现在是总结和分享我在这个平台上发现的有趣的东西了。

TypeScript

在 Angular 社区开始使用 TypeScript 前我对它并不感兴趣。了解并看过一些例子,但觉得它对我来说没什么用。

因为长久以来我都只用动态类型语言(JavaScript,Clojure,Ruby)进行开发,我的代码里渐渐没有类型检查,编辑器里没有自动补全。我已经习惯了在工作时不考虑那些东西。我知道 TypeScript 对大部分人的卖点是什么,但那些对我没用。我只是觉得不必给自己找麻烦,比如使用额外的工具包,寻找和学习各种我要用的 JavaScript 库的类型定义。

另一方面,有段时间我对 ElmPureScript 这种纯粹的函数式编程方法产生了兴趣。这不禁让我思考,如果我要用静态类型语言,为什么不选择一个万能型的语言呢?函数式语言中的类型系统要比 TypeScript 能做的要多得多。也许更重要的是,它们能够指出你的代码中那些存在或不存在的副作用。从另一个方面来说,相比仅仅告诉你 foo 有没有属性 bar,这样更清晰,也更安全。

所以 TypeScript 并没能打动我。但是当我开始用它做一些项目的时候,事情开始不同了。表面上看来,我用 TypeScript 进行编程,却完全感受不到它的存在。对于有多年的 JavaScript 开发经验的我来说,并没有发生多大的改变。

我意识到这是领悟 TypeScript 的关键。当我听到别人总说“这不过是个 JavaScript 上的薄层”,我觉得是挺有道理。但我还是得在实现它前体验一番。这只是 JavaScript - 不过加上了安全措施。与其他语言相比,这真的没有任何意义。

尽管我开始在我的 JavaScript 项目中定位问题,心想“如果用了 TypeScript 接口,我们就能更早定位 Bug”,该发生的还是会发生。尤其在来回传递嵌套数据结构的时候。

function convertData(data) {
  // 好吧,'data' 是什么鬼?
  // 它是一个对象数组的对象吗?
  // 还是一个数组对象的数组?
  // 还是用 console.log 打出来看看吧...
}

相比,

interface Datum {
  name:string,
  items:{id: number, description: string}
}

function convertData(data:Datum[]) {

}

现在你会想念那个从 TypeScript 得到的安全措施。看来这个工具实际上对我来说还是有用的。所以我会继续使用它。同时我还会继续用 Elm,但不再把它与 TypeScript 本身相比较了。

毫无疑问,使用工具会有一些代价,比如 TypeScript 本身,还有那些让你晕头转向的奇怪错误消息等。但这似乎是一个不错的权衡,在新的 Angular 2 项目中安装 Angular 命令行工具 ,不用花时间配置,确实轻松了不少。

明智的数据流

在 Angular 1 中还没有“数据流”的概念。而是通过作用域 和在这些作用域之上的绑定和监控网络进行数据和事件的更新。在有些应用中这个网络是灵活和整洁的。但在现实世界的绝大多数应用中都是一团糟。

React 使用了一个更合理的方式来是实现 UI 数据流:组件的层次结构。数据从上层向下传递,变化事件从下层向上传递。90% 都是如此,只有少部分例外。

Angular 2 也采用了这种方式。通过数据流入,操作流出,使得每个组件都有清晰定义的输入和输出。放弃了作用域和层级继承。

变化检测策略也有些不同,可以选择最有效率的方式去检查变化:如果我拿到不可变的数据结构或者输入信息,可以用 OnPush。如果我知道只要组件存在,一些东西就永远不会改变,可以用 CheckOnce。这有点像 React 的 shouldComponentUpdate ,不过一切都很美好、明晰。

这意味着如果我想用 Redux 和 持久化的数据结构 ,框架不会跟我作对。事实上确有这样的项目:绑定 Redux,让它更简单。或者是如果我想完全使用 RxJS 基础的数据流,也可以兼容。其他有趣的工作还有: 将转播集成到 Angular 2 组件中

现在还有一个 Angular 2 缺失的功能是非常想要的,那就是热加载

Redux 有着非常好的架构模式,但如果你听过 Dan Abramov 谈 Redux 的起源,就会发现他的动机并不是有“一个好的架构”,而是能够支持热加载的实践性目标。也就是说,在应用运行期间,能够在代码修改的地方得到反馈循环,而不需要重启应用或者丢失当前组件中的 UI 状态。

当你用过这种像涡轮增压一样反馈循环后,就不想再倒回去了。所以我们在 Angular 2 中也需要它。

幸运地是这没理由不发生。Minko Gechev 早期做了一些实验,我相信他现在还想继续做下去。

静态模板编译

当你运行一个 Angular 1 应用时,有大量与你的应用逻辑无关的代码参与:HTML 模板被获取,解析,附加到 DOM。指令元素、属性和插值公式被解析的 DOM 搜索。所有表达式被解析和编译成 JavaScript,并为它们创建监听器

没有不得不延迟到运行时的情况。在应用源码树中,所有模板和 JavaScript 代码都已存在,所以我们有必要在构建时处理这些模板的全部信息。平台架构只需提供支持。

Angular 1 并不支持静态模板编译,但 Angular 2 支持。Angular 2 有一个静态编译模式,这时所有的模板都会在构建时被处理。获取 HTML 并从中生成 TypeScript 代码。对需要应用到页面上的每个变化,以精确而小的 DOM 操作,给模板中所有的表达式生成 monomorphic 变化检测器代码。

这里生成的代码是那些如果你想要最好的性能,就必须要全部手写的代码。它没有任何运行时框架的性能开销。这是非常快的,而且不需要 virtual DOM。如果热加载的话,JavaScript 虚拟机执行和内联的部分也需要编码。

Miško 关于 ng-conf 的演讲:

Angular 2 的另一个优化是代码大小。当你用像 Angular 这样的框架时,不一定会用到它的每行代码。安装包里上百个函数在你的应用里可能永远用不上。把这些代码发给用户没有任何意义,但是通常很难避免这种情况,因为 JavaScript 天生的动态特性,很难知道 HTML 模板具体会调用什么。

Angular 的静态编译模式也有所不同。Angular 的编译器知道所有被引用的 TypeScript 和模板代码,因此可以主动消除无用代码路径。tree-shaking minification 可以使应用最后构建时的代码更小。

像这样整体的程序优化方案并不是新思路,已经在其他技术中存在很长时间了。比如 Google Web Toolkit 已经做了近十年的编译器优化。在 ClojureScript 中一直如此,有赖于 Google Closure Compiler,其高级优化模式也做了类似的工作。但是据我所知,目前还没有任何一个主流 JavaScript 框架有可用的产品。

Web Worker 支持

现在我们使用的大多数设备都有多核 CPU,可以同时处理多个任务。如果你在写一个原生应用,就可以在代码里使用多线程或者类似并发特性的能力。

这事在 Web 中就没这么简单了。JavaScript 没有多线程。但是有了 Web Workers,就能在浏览器中运行多个后台进程。

但就算使用 Web Workers 也没这么容易。Web Workers 都是独立进程,不会和 UI 共享任何状态,或其他东西,甚至没有访问 DOM 的能力。你只能通过传递异步的 JSON 消息来和 Web Workers 通信。这确实是个完美可用的并行模型,但这意味着你不能随便把代码扔给 Web Worker 然后期望它能正常工作。

也就是说,除非你用的是平台内置的 Web Workers,比如 Angular 2,就可以配置它以便让你的大部分应用代码可以在 Web Worker 中工作,包括诸如数据流和变化检测等。剩下唯一的事就是 UI “线程”了:DOM 访问。Angular 会维护 worker 和 UI 之间的消息。

只要你愿意留在 Angular 提供的抽象层里,就可以方便地使用,主要包括限制 DOM 本身,只能通过合法的 API 来访问。

不是所有应用都能从此获益。如果你做的全部是表单的数据入口,那么不需要在后台做任何事。这也是为什么 Web Worker 不是默认开启,而需要你主动选择。

估计我写的大部分应用都用不到它,但是我很高兴当我想做一些数据处理相关的事时,可以把它们放到后台执行,而不用牺牲 UI 的响应能力或者把我的代码搞复杂。

服务端渲染

Angular 2 实现了一个完全的通用渲染方案,初始应用视图可以在服务端渲染然后再发送 HTML。这意味着:

  • 用户很快得到想要的内容
  • 被搜索引擎收录(无论他们如何支持JavaScript)
  • Facebook 用户分享你的应用时,可以获得预览内容

现在我大部分都从事 B2B 的相关工作,比如 a)登录障碍后的解决,b)在一个上下文中,初始加载时间的确定。由于以上原因,通用渲染不是我的第一选择,我不想用这些去增加应用架构的复杂度。但是就像我对 Web Worker 的态度一样,当我需要它时能有的用。

有趣的是,Brad Green 在一个主题演讲中提到 ng-conf,这个通用渲染器也可以运行在除 Node.js 的其他平台。这对使用纯 Java 或 .Net 技术栈的大企业来说是非常有吸引力的,他们的技术人员可不热衷于部署 Node 服务器。如果 Angular Universal 可以在 JVM 中的 Nashorn 引擎) 中正确工作,自然而然地适应这些组织机构已经存在的服务器环境。而且还更容易集成到已有的 Java 服务端代码中,因为你能在同一个进程中运行它们。

懒加载

当应用大到一定程度,会积累大量的 JavaScript 代码,CSS 和其他资源,这会导致应用加载变慢。所有代码和资源要通过网络传输并被浏览器处理。

所以,最好能只加载部分应用 - 只有当用户真的需要时才加载这部分 UI。比如说,我现在有一个应用,只授权给部分用户在“管理员视图”中管理任务。这背后有大量的代码。但是所有用户都加载了这些代码,其中大部分人根本看不到管理员界面。如果能在真正需要时加载是最好的。

在 Angular 1 中实现这个不太可能。尽管有相应的,但都不得不做些额外的工作来处理所有情况。这不是 Angular 1 架构本身可以做的,比如,在应用启动后,需要向注入器添加新的服务。

在 Angular 2 中一切不同了。通过一个路由器来懒加载那些第一次访问路由时的代码。通过分级依赖注入系统,这些依赖仅和一个确定的路由及其 UI 组件一起加载。

Angular 2 中的懒加载并不是一种 hack,但是一些平台会去支持以使它更容易使用。

组件 CSS

这是一个小而令人惊艳的特性:在 Angular 2 中我们把所有 UI 相关的代码放到组件里,不仅包括 TypeScript 类和 HTML 模板定义的组件,还包括 CSS 定义的样式组件。

我们在为 UI 组件添加样式时,大量时间都花在了为单一组件写 CSS 上。因此,把 CSS 和其他组件代码打包是很有意义的。这也正是 Angular 2 所做的:为每个组件定义样式

这种支持不是简单的共同定位文件:用一种叫视图封装的特性,使得 Angular 处理组件 CSS 代码可以将作用域限定在组件模板内部而不会泄漏。

也就是说,要是我想为一个叫 .selected 的类选择器添加样式,可以不用担心会被应用其他地方引用,或者被重复定义。我不需要去想一个全局唯一的命名方案,或是做一些像 BEM 一样不必要的事。

这有点像 CSS Modules。Angular 组件 CSS 和 CSS Modules 都提供了类似的局部作用域。Angular 的 CSS 没有像 CSS modules 那样的依赖系统,尽管很有可能在组件间共享样式。另一方面,Angular 是基于 Shadow DOM standard 实现的,所以只用专有语法迟早被浏览器原生支持所替代。

动画

浏览器原生支持两种主要的动画实现。它们的优缺点如下:

  1. 通过 CSS Transitions 我们可以方便地声明元素的样式如何随着时间变化。但是编程访问的限制颇多。一切都需要预定义的 CSS,并且不能影响正在运行的过渡动画。如果仅仅在运行时动画属性的值,就倒霉了。
  2. 通过 Web Animations API 我们可以完全控制动画,因为它是通过 JavaScript 代码触发的,可以事后控制。但是这种情况下我们不能使用 CSS 样式。我们得用 JavaScript 来定义动画样式,这很难得到你想要的效果。

Angular 2 动画引擎带给我们两者的完美融合。它构建于 Web 动画 API 之上,但又与 Angular 的组件样式紧密集成,以便可以更好的定义动画样式。

这些动画可以很好地集成到我们的部分应用代码。即使我们以 CSS 和组件元数据的方式声明动画,通常还是会丢失动态运行时的钩子:

  • 直到运行时我们才能拿到 Angular 自动计算的属性,绕过 CSS 过渡的常见问题是不能进行 CSS 硬编码。几周前,我最后一次尝试在 Angular 1 中解决元素动态高度的动画问题。结果令人沮丧。
  • 我们从由 TypeScript 代码驱动的实际组件中触发动画,无需定义 CSS 类去联系动画。
  • 我们自己也可以动态提供一些动画属性,类似一种鼠标单击事件绑定触发动画。想想材料的影响 - 这是我们容易做的。

原生应用支持

我是一个 web 开发者,虽然完成了一些关于 iOS,Android 和 Windows Phone 原生应用的分享,但我永远支持 web。我很高兴看到 web 在更广阔的领域里成为一个不错的选择,最近所有渐进式 web 应用的推进,正是 Angular 2 所倡导的。

也就是说,在某些情况下,我发现原生应用更合适。比如我现在在应用中要频繁使用摄像头设备和一些图像处理代码。目前 web 平台还不适合这类工作。

这就是为什么做原生应用的工具很棒,更多地面向 web 开发者,比如 React NativeNativeScript。在跨平台应用中可以共享代码模块。

更妙的是这些平台完全支持 Angular 2。它本来就是一个多平台工具,能够在浏览器和原生系统里发挥同样的性能。这意味着我可以用它原有的功能写一个实际的原生应用,包括 Angular 的变化检测,依赖注入,指令,组件,管道,服务,动画和可测试性。太棒了。

总结

到目前为止,讨论了很多关于 Angular 2 的东西,比如它与 Angular 1 和类似 React 的 API 有何不同。大家关于 HTML 模板与 JSX,依赖注入和非依赖注入,TypeScript 和 Babel 优劣的争论已久。

有很多有趣的争论,但都没有这些应用平台的多。平台最重要的是选择采用或放弃某些特性,以及如何整合这些特性。Angular 2 实现了一个不错的方案,我今年打算用它开发一些项目。