Angular

Angular 8 终于来了,包括 Ivy 的预览、service worker 支持,差异化加载以及一些锦上添花的东西。 Manfred Steyer 解释了最新 Angular 版本中最重要的变化。

Angular 8 刚刚发布!

完全按照计划,没有任何意外:框架和 CLI 的更新可以通过 ng update 完成,其新功能是一个受欢迎的补充,符合“演化而不是革命”的座右铭。

在本文中,我将介绍 Angular 8 和 Angular CLI 8 的最重要的新功能。我在文中的例子可以在 GitHub 上找到。

先瞅一眼 Ivy

Ivy 是 Angular 世界下一个望眼欲穿的大新闻,它是新的 Angular 编译器,也是新的渲染管道。Ivy 有可能产生相当小 bundle,它使渐进式编译更容易,也是 Angular 领域未来创新的基础。

由于 Angular 大量的底层部分已经为此进行了更改,因此 Angular 团队特别注意与以前的 Angular 版本的兼容性:在切换到 Ivy 之后,现有的程序应该能够像以前一样工作。在一切正常的前提下,能够得到明显更小的 bundles 应该就足够了。这并非是他们大发善心,而是因为 Google 有 600 多个以 Angular 为基础的应用程序 —— 尽管是谣传,但实际数字要高得多。

在 Angular 8 中 Ivy 的预览版现在可供测试。此版本的目标是获得早期反馈。因此,Angular 团队建议不要把 Ivy 用于生产环境,而是继续使用经典视图引擎(图1)。

使用与不使用 Ivy 时的 hello world 程序的 Bundle 大小(来源:由Brad Green和Igor Minar撰写的 ngconf 2019 主题演讲)

使用与不使用 Ivy 时的 hello world 程序的 Bundle 大小(来源:由Brad Green和Igor Minar撰写的 ngconf 2019 主题演讲)

感谢差异加载(如下所示),bundle 大小已经可以立即得到优化。

正如 Google Angular 团队背后的技术总监 Brad Green 在 ngconf 2019 中提到的那样,Ivy 将在兼容模式下结合差异加载,显着改善 bundle 的尺寸。寻求刺激的人可以尝试一下未来的 Ivy API。该模式下有非常大的优化潜力。目前这些 API 仍然被标记为私有。你可以通过查看它的类和函数来进行判断:它们以特殊字符 ɵ 开头。

如果你想尝试 Ivy,可以通过 enable-ivy 开关生成一个新项目:

ng new ivy-project --enable-ivy

这样做的结果是 CLI 会在 tsconfig.app.json 中存储以下配置条目:

"angularCompilerOptions": { 
        "enableIvy": true 
}

在更新到 Angular 8 之后,也可以手动添加此条目,以便用 Ivy 测试现有程序。

要在调试模式下运行程序,建议使用 AOT:

ng serve --aot

此外,值得一提的是通过 ng build 创建的程序的大小。等到 Angular 9 发布时 Ivy 最终应该会默认激活。在此之前,Angular 团队计划采取进一步措施以确保与旧版本的兼容性。

Web worker

根据定义,JavaScript 是单线程的。因此,对于数据调用等较大任务异步处理是很常见的。不用说,这对计算密集型没有帮助。特别是那些广泛的 JavaScript 解决方案变得越来越普遍,这就是为什么现在几乎所有的浏览器都支持支持 Web worker。它们是浏览器在自己的线程中运行的脚本。通过发送消息与浏览器选项卡中的线程进行通信。

虽然 Web worker 本身与 Angular 无关,但在构建过程中必须考虑它们。目标是为每个 Web worker 提供一个 bundle 包。此任务由新的 Angular CLI 完成。

为了说明这个新功能,我将通过实现所谓的 “n 皇后问题”的 JavaScript 进行说明。这个想法是在棋盘上每行放一个皇后,而不能相互公鸡。这意味着在同一行、列或对角线中不能有其他皇后。

n皇后问题的一种解决方案

n 皇后问题的一种解决方案

计算棋盘上所有可能的解决方案的算法被认为是计算密集型的。虽然对有 8 行和 8 列的常规棋盘的计算相当快,但是普通计算机从 12×12 格开始就达到了其极限。当前最高记录是解决具有 27 x 27 格的解决方案。俄罗斯的超级计算机完成了此任务。

为了将类似这样的计算甩给后台,我们必须首先用 Angular CLI 创建 一个Web worker:

ng generate worker n-queens

此语句不仅为 worker 创建文件,还为构建过程和现有文件中的条目创建配置文件。如果同一文件夹包含具有公共文件扩展名 .component.ts 的同名组件,则 CLI 甚至会使用与 Web worker 通信的代码对其进行丰富。

worker本身只包含 message 事件的事件监听器:

import nQueens from './n-queens';

addEventListener('message', ({ data }) => {
  const result = nQueens(data.count);
  postMessage(result, undefined);
});

当主线程向 worker 发送消息时会执行该事件。该参数包含从主线程发来的信息。在当前的情况下,它仅限于属性 count ,它声明了棋盘大小。在计算函数 nQueens 之后,事件监听器通过 postMessage 将结果发送回主线程。*因此,浏览器在那里触发 message 事件。

Worker 类被应用于 using 组件来与此 worker 脚本交互:

const count = parseInt(this.count, 10);

const worker = new Worker('../logic/n-queens.worker', {
    type: 'module' // Worker uses EcmaScript modules
});

worker.postMessage({count});

worker.addEventListener('message', (event) => {
  // tslint:disable-next-line: no-console
  console.debug('worker result', event.data);

  // Update chessboard
  this.processResult(event.data);
});

组件通过 postMessage 向 worker 发送带有所需棋盘大小的消息,从而触发计算。它通过消息事件接收结果。

最后 CLI 负责将工作脚本正确的转换和捆绑。由此启动的 TypeScript 编译器会通过它们的后缀 .worker.ts 来识别它们,它们在由 ng generate worker 生成的 tsconfig.worker.json 中注册。为了确保 CLI 在翻译和捆绑主程序时不再考虑这些文件,ng generate worker 将相同的文件模式放在 tsconfig.app.jsonexclude 部分中。

完整的实现包含在作者的样本集[1]中。为了便于说明,可以在主线程和 Web worker 中解决可用的 n 皇后问题。例如,当你为 12 x 12 棋盘请求解决方案时,你将看到 UI 在第一种情况下会被冻结,而 worker 的后台计算不会降低 UI 的可操作性。

差异加载

目前将程序编译成旧 ECMAScript 5 代码仍然是很常见的,因为“古老的 JavaScript ”在今天仍然在到处运行。这意味着 IE 11 和 Google 搜索引擎后面的网络爬虫都可以执行这些代码。

但是,新的 ECMAScript 2015 及其后续版本更加高效:这些版本允许更紧凑的 bundle 包,浏览器也可以更有效地解释它们。

从版本 8 开始,CLI 包含一个名为差异加载的功能。其背后的想法是提供两组 bundle:一组基于 ECMAScript 5 并且针对较旧的浏览器,另一组基于较新的 ECMAScript 版本,例如 ECMAScript 2015,以此为现代浏览器提供上述优势。

要激活差异加载,你不用做太多的事情:只需要为 ECMAScript 版本设置一个上限和下限。在 tsconfig.json 中输入版本上限,如下所示:

"target": "es2015"

另一方面,下限由浏览器列表来定义。根据市场份额等特定标准,它是一个用来标识许多支持的浏览器的文件。它们可以存储在例如 browserslist 文件中,CLI 在生成新项目时同时会在 projectroot 中创建:

> 0.5%
last 2 versions
Firefox ESR
not dead
IE 9-11

如下图所示,browserslist 指向 ECMAScript 5 浏览器,条目为 IE 9-11。因此,CLI 将下限确定为此版本。如果 CLI 收到构建( ng build)指令,则将对两个版本进行编译和 bundling 过程:

构建差异加载

构建差异加载

这个过程的缺点显而易见:构建过程所需的时间加倍。

为了使不同的浏览器可以决定要加载哪个版本的 bundle 包,他们在 index.html 添加中接受 script 的引用:指向 ECMAScript 5 包的那些引用会添加 nomodule。这可以使支持 ECMAScript 2015 及更新版本的浏览器忽略这些引用。另一方面,ECMAScript 2015+ bundle 包由 CLI 通过 type ="module" 实现。因此旧版浏览器将忽略这些脚本标记:

<script src="main-es2015.js" type="module"></script>

<script src="main-es5.js" nomodule></script>

ng build 相比,其他所有 CLI 命令仅使用上限。在上图中所示的这种情况下,是 ECMAScript 2015。出于效率原因,会发生这种情况:特别是在调试和测试期间,开发人员希望尽快看到结果,而不需要等待第二次构建。

延迟加载

自 Angular 出现的第一天起,路由就支持延迟加载。到目前为止,这是通过识别加载模块的魔术值来完成的:

{
    path: 'lazy',
    loadChildren: () => './lazy/lazy.module#LayzModule'
}

“#”号之前的值表示通向模块实现的文件的路径;之后的值代表其中包含的类。这种写作风格也适用于 Angular 8,但是已经被弃用了,现在支持动态 ECMAScript 导入:

{
    path: 'lazy',
    loadChildren: () => import('./lazy/lazy.module').then(m => m.LazyModule)
}

新的书写风格中仍然包含文件名作为魔术值。但是由于许多IDE支持导入,因此无效值将立即返回错误。

ViewChild 和 ContentChild 中的重大变化

ViewChildContentChild 的使用方式发生了重大变化,但遗憾的是,过去并不总是表现出一致的行为。虽然它们在早期版本中被用于组件请求不在结构指令内的元素,如 ngIf 或 *ngFor*,但查询结果已在 ngOnInit 中可用。否则,程序代码或过早的可以在 *ngAfterViewInit*(或 ngAfterContentInit for ContentChild )中访问它。对于以后因数据绑定而仅加载到 DOM 中的元素,程序代码必须分别插入 ngAfterViewChecked 或 *ngAfterContentChecked*。

由于这种行为十分令人困惑,所以现在组件必须指定何时应该进行解决:

@ViewChild('info', { static: false })
  paragraph: ElementRef;

如果 static 的值为 true,则 Angular 会在初始化组件时尝试查找该元素。这只在不在结构指令中时才有效。使用 static:false 时,在启动或刷新视图后进行解析。

ng update 命令 会自动尝试在此处输入正确的值。如果无法做到这一点,则会在其位置添加带有 TODO 的注释。

与相关装饰器 ViewChildrenContentChildren 的查询不受此更改的影响。他们总是表现出 static:false 意义上的动态行为。

ngUpgrade的新功能

到目前为止,AngularJS 1.x 和 Angular 与 ngUpgrade 的混合操作中存在的一个问题是:两个框架的路由有时一直在争夺 URL。这导致了难以理解的副作用。为了避免这种情况,可以使用相同的 Location 服务去访问两个版本框架中的 URL 。

为实现这一目标,Angular 团队扩展了Angular Location 服务的可能性,从而为 AngularJS 中的 $location 提供了替代。

出于这个原因,在 Location 服务中添加了用于监视URL更改的新方法 onUrlChange 以及其他修改:

export class AppComponent {
  constructor(loc: Location, pLoc: PlatformLocation) {
    loc.onUrlChange((url) => console.debug('url change', url));
    console.debug('hostname: ', pLoc.hostname);
  }
}

PlatformLocation 服务提供对 URL 各个部分的附加访问。有关如何使用 $location 替换的详细描述(用于更好地交织两个框架)可以在这里找到。此外,你现在可以找到延迟加载 AngularJS 的想法,它基于前面提到的动态 ECMAScript 导入。

结论

Angular团队再次表达了自己的观点:迁移到新的 Angular 版本很容易,并且不需要进行大的更改。使得使用 Google 的 SPA 框架更加舒适。如果旧版浏览器不受支持或不支持单独的 bundle 包,则差异加载会为进一步优化 bundles 包。 Web worker 支持表明越来越多的计算密集型任务开始进入浏览器。现在可以尝试用 Ivy 迈出第一步。