部署

本章会描述部署和优化Angular应用的工具与技术。

目录

概览

本章描述把Angular应用发布到远端服务器时所需的准备与部署技术。从简单却未优化的版本到充分优化但涉及更多知识的版本。

最简化的部署方式

部署应用最简化的方式是直接把它发布到开发环境之外的Web服务器上。

它已经在本地运行过了。我们基本上只要把它原封不动的复制到别人能访问到的非本地服务器上就可以了。

  1. 把一切文件(或几乎一切文件)从本地项目目录下复制到服务器的目录下。

  2. 如果准备把该应用放在子目录下,就要编辑index.html,并适当设置<base href>。 比如,如果到index.html的URL是www.mysite.com/my/app/,就把基地址设置为<base href="/my/app/">。如果是放在根路径下就不用动它。 详情参见稍后

  3. 把服务器上缺失的文件重定向到index.html,详情参见稍后

  4. 按照稍后的描述启用生产模式(可选)。

这就是最简化的部署方式。

这不是生产级部署。它没有优化过,并且对用户来说也不够快。 但是当你向经理、团队成员或其它利益相关者内部分享你的进度和想法时它是足够的。 一定要读读稍后的为生产环境优化

从Web上加载npm包(SystemJS)

node_modules文件夹包含着在浏览器中运行应用时所需的更多代码。 "快速起步"项目中所需的node_modules通常由20,500+个文件和180+ MB的体积。 运行应用时其实只需要其中很小的一部分。

上传这些不需要的文件需要很长时间,而在库的下载期间,用户得进行不必要的等待。

我们可以转而从网上下载所需的这少量文件。

(1) 复制一份专用于部署的index.html,并把所有的node_module脚本替换成加载网上的版本。代码如下:

<!-- Polyfills -->
<script src="https://unpkg.com/core-js/client/shim.min.js"></script>

<!-- Update these package versions as needed -->
<script src="https://unpkg.com/zone.js@0.8.4?main=browser"></script>
<script src="https://unpkg.com/systemjs@0.19.39/dist/system.src.js"></script>

(2) 把systemjs.config.js脚本改为加载systemjs.config.server.js

<!-- This SystemJS configuration loads umd packages from the web -->
<script src="systemjs.config.server.js"></script>

(3) 把 systemjs.config.server.js(稍后有代码)复制到src/文件夹。 这个版本会从网上加载Angular的UMD版本(和其它第三方包)。

把对systemjs.config.js的修改也随时同步到systemjs.config.server.js文件。

注意paths属性:

paths: {
  'npm:': 'https://unpkg.com/' // path serves as alias
},

在标准的SystemJS配置中,npm路径指向node_modules/。 在服务器端的配置中,它指向https://unpkg.com(一个专门存放npm包的服务器), 并从网上直接加载它们。 还有另一些服务提供商做同样的事。

如果你不想或无法从公网上加载这些包,也可以把systemjs.config.server.js中所指出的这些文件或文件夹复制到服务器上的一个库目录。 然后修改配置中的'npm'路径指向该文件夹。

用一个例子实践一下

下面这个例子展示了所有的修改。

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <!-- Doesn't load from node_modules! -->
  5. <!-- Set the base href -->
  6. <base href="/">
  7. <title>Simple Deployment</title>
  8. <meta charset="UTF-8">
  9. <meta name="viewport" content="width=device-width, initial-scale=1">
  10. <link rel="stylesheet" href="styles.css">
  11. <!-- Polyfills -->
  12. <script src="https://unpkg.com/core-js/client/shim.min.js"></script>
  13. <!-- Update these package versions as needed -->
  14. <script src="https://unpkg.com/zone.js@0.8.4?main=browser"></script>
  15. <script src="https://unpkg.com/systemjs@0.19.39/dist/system.src.js"></script>
  16. <!-- This SystemJS configuration loads umd packages from the web -->
  17. <script src="systemjs.config.server.js"></script>
  18. <script>
  19. System.import('main.js')
  20. .catch(function(err){ console.error(err); });
  21. </script>
  22. </head>
  23. <body>
  24. <my-app>loading...</my-app>
  25. </body>
  26. </html>

在真实应用中尝试这些技术之前,先用这个例子实践一下。

  1. 遵循设置步骤创建一个名叫simple-deployment的新项目。

  2. 添加上述的“简单部署”范例文件。

  3. 像其它项目一样使用npm start来运行它。

  4. 在浏览器的开发者工具中审查网络包。注意,它从网上加载了所有包。 我们可以删除node_modules文件夹,该应用仍然可以正常工作(但没办法再重新编译它或者启动lite-server了)。

  5. 把范例工程部署到服务器上(但node_modules文件夹除外)

掌握这些之后,就可以在你的真实项目中试用这些过程了。

为生产环境优化

虽然可以直接从开发环境下部署,但是它还远远没有优化。

客户端发起了很多小的请求来取得一个个单独的应用代码和模板文件,从浏览器开发工具的Network标签中就可以确认这一点。 每个小文件都会花费很多时间在与服务器建立通讯而不是传输内容上。

开发环境下的文件有很多注释和空格,以便于阅读和调试。 浏览器会下载整个库,而不只是应用需要的那部分。 从服务器传到客户端的代码(即有效载荷)的数量会显著大于应用运行时真正需要的那部分。

大量请求和载荷意味着应用相对于优化过的版本会花更多时间进行启动。 当用户看到什么或做什么有用的事情之前,就已经过去了(浪费了)很多秒。

这重要吗?取决于很多业务和技术方面的因素,我们必须自己评估它们。

如果重要,那么有很多工具和技术可以减少请求数和体积。

每个工具做的事情都不一样,但它们结合起来会相辅相成。

我们也可以使用任何喜欢的构建系统。 无论选择的是什么,都务必把它自动化,以便可以一步构建出产品。

预编译(AOT)

Angular的预编译器会在构建过程中预先编译应用的组件及其模板。

预编译过的应用启动更快,原因如下:

要了解AOT编译器的更多知识,参见烹饪宝典中的AOT一章, 它描述了如何在命令行中执行AOT编译器,并使用rollup进行构建、最小化、混淆和摇树优化。

Webpack(与AOT)

Webpack 2是另一个选项,它可以内联模板、样式表、打包、最小化和混淆应用。 "Webpack简介"一章中将会教你如何配合Angular使用Webpack。

考虑使用官方的 Angular预编译插件来配置Webpack。 这个插件会转译TypeScript代码、独立打包延迟加载的NgModules,而且不用对源码做任何修改就能执行AOT编译。

使用rollup消除死代码

任何永远不会调到的代码就是死代码。 通过移除应用和第三方库中的死代码,可以实质性减小应用的总大小。

摇树优化是一种消除死代码的技术,它会从JavaScript模块中移除导出。 如果一个库导出了一些东西,但是应用代码没有导入过它,摇树工具就会从代码中移除它。

常用的摇树优化工具是Rollup,一个带有查件的生态系统,可以完成打包、最小化和混淆。 要了解关于摇树优化和消除死代码技术的更多知识,参见这个帖子,它的作者就是rollup之父Rich Harris。

修剪库

不要指望自动移除所有死代码。

手动移除不用的库,特别是index.html中不用的脚本。 为实际使用的那些库则努力选择更小的代用库。

有些库可以构建出只带所需特性的、自定义的、带皮肤的版本。另一些库则可以让你按需导入它的特性。 RxJS就是一个很好的例子,我们会单独导入Observable的操作符(operator),而不是导入整个库。

首先,度量性能

如果我们能对“是什么导致了应用变慢”的问题有一个清晰、准确的理解,那就可以对优化什么、如何优化做出更好地决策了。 真正的原因可能并不是你所想的那样。 我们可能花费大量的时间和金钱去优化一些东西,但它却无法产生可感知的效果甚至让应用变得更慢。 我们应该在那些最重要的环境中实际运行,来度量应用的实际行为。

Chrome开发工具的网络性能页是开始学习度量性能的好地方。

WebPageTest工具是另一个不错的选择,它能帮你验证你的部署是否成功了。

Angular配置

修改Angular配置可以显示出快速启动应用和完全不加载之间的差异。

base标签

HTML中的<base href="..."/>用于指定一个解析相对路径的基地址,如图片、脚本和样式表。 比如,指定<base href="/my/app/">时,浏览器就会把some/place/foo.jpg这样的URL解析成到my/app/some/place/foo.jpg的服务端请求。 在浏览期间,Angular路由器会使用base href作为组件、模板和模块文件的基地址。

参见另一种备选方案APP_BASE_HREF

在开发期间,我们通常会在index.html所在的目录中启动服务器。这个目录就是根目录,因为/就是本应用的根,所以我们要在index.html的顶部添加<base href="/">

但是在共享服务器或生产服务器上,我们可能得从子目录下启动服务器。 比如,当加载本应用的URL是http://www.mysite.com/my/app/时,子目录就是my/app/,而我们就要在服务器版的index.html中添加<base href="/my/app/">

base标签没有正确配置时,该应用会加载失败,并且浏览器的控制台会对这些缺失的文件显示404 - Not Found错误。 看看它在尝试从哪里查找那些文件,并据此调整base标签。

启用生产模式

Angular应用默认运行在开发模式下,正如在浏览器控制台中看到的如下信息:

Angular is running in the development mode. Call enableProdMode() to enable the production mode.
(Angular正运行在开发模式下。调用enableProdMode()来启用生产模式)

切换到生产模式可以通过禁用开发环境下特有的检查(比如双重变更检测周期)来让应用运行得更快。

要在远程运行时启用生产模式,请把下列代码添加到main.ts中。

src/main.ts (enableProdMode)

import { enableProdMode } from '@angular/core';

// Enable production mode unless running locally
if (!/localhost/.test(document.location.host)) {
  enableProdMode();
}

惰性加载

通过只加载应用启动时必须展示的那些应用模块,我们可以显著缩减启动时间。

配置Angular路由器可以延迟加载所有其它模块(以及与它们相关的代码),无论是等应用启动, 还是在需要时才惰性加载

不要立即导入惰性加载模块中的任何东西

这是一种常犯的错误。 我们本打算惰性加载一个模块,但可能无意中在根模块AppModule文件中使用一个JavaScript的import语句导入了它。 这样一来,该模块就被立即加载了。

关于打包(bundle)方式的配置必须考虑到惰性加载问题。 因为惰性加载模块不能在JavaScript中导入(就像刚才说明的),打包器应该默认排除它们。 打包器不知道路由器的配置,并且不会为延迟加载模块创建单独的包。 我们不得不手动创建这些包。

Angular预编译插件会自动识别惰性加载的NgModules,并为它们创建单独的包。

服务端配置

这一节涵盖了我们对服务器或准备部署到服务器的文件要做的那些修改。

带路有的应用必须以index.html作为后备页面

Angular应用很适合用简单的静态HTML服务器提供服务。 我们不需要服务端引擎来动态合成应用页面,因为Angular会在客户端完成这件事。

如果该应用使用Angular路由器,我们就必须配置服务器,让它对不存在的文件返回应用的宿主页(index.html)。

带路由的应用应该支持“深链接”。 所谓深链接就是指一个URL,它用于指定到应用内某个组件的路径。 比如,http://www.mysite.com/heroes/42就是一个到英雄详情页面的深链接,用于显示id: 42的英雄。

当用户从运行中的客户端应用导航到这个URL时,这没问题。 Angular路由器会拦截这个URL,并且把它路由到正确的页面。

但是,当从邮件中点击链接或在浏览器地址栏中输入它或仅仅在英雄详情页刷新下浏览器时,所有这些操作都是由浏览器本身处理的,在应用的控制范围之外。 浏览器会直接向服务器请求那个URL,路由器没机会插手。

静态服务器会在收到对http://www.mysite.com/的请求时返回index.html,但是会拒绝对http://www.mysite.com/heroes/42的请求, 并返回一个404 - Not Found错误,除非,我们把它配置成转而返回index.html

后备页面配置范例

没有一种配置可以适用于所有服务器。 后面这些部分会描述对常见服务器的配置方式。 这个列表虽然不够详尽,但可以为你提供一个良好的起点。

开发服务器

historyApiFallback: {
  disableDotRule: true,
  htmlAcceptHeaders: ['text/html', 'application/xhtml+xml']
}

生产服务器

RewriteEngine On
# If an existing asset or directory is requested go to it as it is
RewriteCond %{DOCUMENT_ROOT}%{REQUEST_URI} -f [OR]
RewriteCond %{DOCUMENT_ROOT}%{REQUEST_URI} -d
RewriteRule ^ - [L]

# If the requested resource doesn't exist, use index.html
RewriteRule ^ /index.html
try_files $uri $uri/ /index.html;
<system.webServer>
  <rewrite>
    <rules>
      <rule name="Angular Routes" stopProcessing="true">
        <match url=".*" />
        <conditions logicalGrouping="MatchAll">
          <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
          <add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
        </conditions>
        <action type="Rewrite" url="/src/" />
      </rule>
    </rules>
  </rewrite>
</system.webServer>
"rewrites": [ {
  "source": "**",
  "destination": "/index.html"
} ]

请求来自另一个服务器的服务(CORS)

Angular开发者在向与该应用的宿主服务器不同域的服务器发起请求时,可能会遇到一种跨域资源共享(CORS)错误。 浏览器会阻止该请求,除非得到那台服务器的明确许可。

客户端应用对这种错误无能为力。 服务器必须配置成可以接受来自该应用的请求。 要了解如何对特定的服务器开启CORS,参见enable-cors.org

下一步

如果我们准备超越简单复制部署的方式,请参阅烹饪宝典中的AOT部分