前面我们已经完成了一个简单的演示项目,大致了解了 Angular 的开发流程。现在,我们要回过头来,看看 Angular 程序是如何从代码显示到浏览器中的。
引导就是把 Angular 程序初始化并且加载到浏览器显示的过程。
之前我们直接使用ng serve
命令运行 Angular 程序,看不到编译后的结果。为了探究 Angular 的引导过程,我们需要使用ng build
命令,把 Angular 项目编译打包。默认情况下,项目根目录会生成一个 dist 目录,即最终发布用的 Angular 程序。在接下来的内容中,我们都会使用 dist 目录生成的编译之后的文件。
总体来说,Angular 程序的引导过程大致分为一下几个步骤:
- HTML 入口点被加载,也就是 index.html 被加载
- JavaScript 包被加载,通常就是 Angular 本身、第三方依赖以及程序本身
- 程序自己的包被执行
- main.ts 作为入口点,需要加载 Angular,并且触发根模块的引导
- Angular 加载根模块,渲染整个应用程序
下面我们一步一步看,整个引导过程是怎样的。
首先,index.html 被浏览器下载到本地并且开始解析。经过编译的 index.html 内容一般如下:
<!DOCTYPE html><html lang="en"><head> <meta charset="utf-8"> <title>TodomvcApp</title> <base href="/"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="icon" type="image/x-icon" href="favicon.ico"> <style>body,html{margin:0;padding:0}body{-moz-osx-font-smoothing:grayscale}body{font:14px Helvetica Neue,Helvetica,Arial,sans-serif;line-height:1.4em;background:#f5f5f5;color:#111;min-width:230px;max-width:550px;margin:0 auto;-webkit-font-smoothing:antialiased;font-weight:300}</style><link rel="stylesheet" href="styles.4624dd4d7b2e8e969209.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" href="styles.4624dd4d7b2e8e969209.css"></noscript></head> <body> <app-root></app-root> <script src="runtime.d5b1b50f788b857b859e.js" defer></script><script src="polyfills.4e2bd010f12e9f62f018.js" defer></script><script src="main.34c6a9a09f2b4bf274ee.js" defer></script> </body></html>
注意这个文件和 src 目录下的 index.html 已经有所不同:Angular 编译之后会把生成的 js、css 等文件插入到 index.html 相应的位置。
HTML 文件的<body>
标签必须包含一个根组件,作为组件的入口点;在这个例子中,就是<app-root>
。只要 Angular 被正确加载并且运行,就可以识别到这个组件,然后使用组件对应的模板更新 DOM。
至此,Angular 应用程序就已经启动完毕了。下面,我们更深入一些。
回顾一下,当我们直接打开 src 目录下的 index.html 时,并不会看到任何 JS 文件,而前面所说的,直接打开编译之后的 index.html,则可以看到一堆 JS 文件的加载。这些文件都是在构建过程中被自动插入的。当我们使用 Angular CLI 构建时,会首先读取 angular.json 中的项目配置信息,然后基于此构建整个项目。
Angular CLI 底层使用 Webpack。这是一个模块打包器。简单来说,Webpack 在自己的大量插件的帮助下,把源代码文件和各种资源文件转换成浏览器可以执行的 JavaScript 代码包。这些代码包包含了项目执行所需的所有代码、各种依赖。当我们运行ng build
命令之后,打开 dist 文件夹可以看到类似下面的文件:

其中,文件名中间的随机字符串部分是由 Webpack 自动生成的。
前面我们已经讨论过 index.html,接下来看看其它几个文件:
- main.js:项目本身代码以及所有
import
的代码 - vendor.js:第三方依赖库代码,如果没有第三方依赖,则没有这个文件
- polyfills.js:允许新特性在旧平台运行的兼容性代码
- runtime.js:Webpack 在运行时加载代码的工具类
这些文件都是由 Webpack 构建生成,并且自动插入到 index.html 中,正如前面我们所看到的那样。
另外,如果有懒加载模块,这里同样会生成对应的文件。这点我们会在后面的章节详细介绍。
这些文件都会被插入到 index.html,因此浏览器会把这些文件全部下载下来。但是,仅仅下载文件,并不能把项目运行起来。真正让项目运行起来,还需要执行这些文件所包含的代码。接下来,我们要了解,这部分代码是如何被执行的。
我们所关心的是 main.js,因为按照上面的说明,这里实际包含了项目本身的代码。但 Angular 究竟执行 main.js 的哪部分代码呢?这实际是在 angular.json 中配置的:
{ "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "cli": { "analytics": false }, "version": 1, "newProjectRoot": "projects", "projects": { "todomvc-app": { "projectType": "application", "schematics": { "@schematics/angular:application": { "strict": true } }, "root": "", "sourceRoot": "src", "prefix": "app", "architect": { "build": { "builder": "@angular-devkit/build-angular:browser", "options": { "outputPath": "dist/todomvc-app", "index": "src/index.html", "main": "src/main.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.app.json", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "src/styles.css" ], "scripts": [] }, "configurations": { "production": { "budgets": [ { "type": "initial", "maximumWarning": "500kb", "maximumError": "1mb" }, { "type": "anyComponentStyle", "maximumWarning": "2kb", "maximumError": "4kb" } ], "fileReplacements": [ { "replace": "src/environments/environment.ts", "with": "src/environments/environment.prod.ts" } ], "outputHashing": "all" }, "development": { "buildOptimizer": false, "optimization": false, "vendorChunk": true, "extractLicenses": false, "sourceMap": true, "namedChunks": true } }, "defaultConfiguration": "production" }, "serve": { "builder": "@angular-devkit/build-angular:dev-server", "configurations": { "production": { "browserTarget": "todomvc-app:build:production" }, "development": { "browserTarget": "todomvc-app:build:development" } }, "defaultConfiguration": "development" }, "extract-i18n": { "builder": "@angular-devkit/build-angular:extract-i18n", "options": { "browserTarget": "todomvc-app:build" } }, "test": { "builder": "@angular-devkit/build-angular:karma", "options": { "main": "src/test.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.spec.json", "karmaConfig": "karma.conf.js", "assets": [ "src/favicon.ico", "src/assets" ], "styles": [ "src/styles.css" ], "scripts": [] } } } } }, "defaultProject": "todomvc-app" }
注意这里的第 25 行,指定了 main.js 被加载之后,应该执行的代码文件,默认就是 main.ts。正是根据这一配置,Angular CLI 保证在代码包文件加载之后,Webpack 会立即加载并执行对应的模块。
现在,我们已经知道,main.ts 就是 Angular 项目的入口点。那么,接下来,我们来看看 main.ts 究竟干了些什么。
如果没有做任何修改,main.ts 应该是这样的:
import { enableProdMode } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './app/app.module'; import { environment } from './environments/environment'; if (environment.production) { enableProdMode(); } platformBrowserDynamic().bootstrapModule(AppModule) .catch(err => console.error(err));
首先,我们看到,这里有一些import
语句。这部分语句在把 TypeScript 编译成 JavaScript 之后依然会保留。运行时,这部分import
语句就会被 Webpack 加载并执行。只有这些导入语句执行完毕之后,剩下的代码才会被执行。
接下来,如果是生产环境,也就是environment.production
被设置为true
,会开启 Angular 的生产模式。这一步是为了提高性能。
最后,也是最重要的一步,
platformBrowserDynamic().bootstrapModule(AppModule) .catch(err => console.error(err));
虽然只有一行代码,但这一行干了很多事情。platformBrowserDynamic()
是一个模块,用于在 Web 浏览器环境中加载 Angular 上下文。这个模块会加载框架的必须元素和配置。通过不同的模块,Angular 可以在不同环境中运行。
因此,platformBrowserDynamic()
用于加载 Angular。一旦完成,第二部分,bootstrapModule()
通知 Angular 去引导指定的模块。默认情况下,所有的 Angular 项目都包含至少一个名为AppModule
的模块。到这一步,Angular 会接管整个项目,加载组件、服务、指令、管道等等,所有可以在AppModule
中导入、引用、声明的一切。如果一切正常的话,Angular 会找能够匹配app-root
选择器的组件。通常这个组件就是AppComponent
。一旦 Angular 找到了这个组件,就会把它渲染出来,同时更新 DOM。
至此,Angular 程序就在浏览器运行起来,整个引导过程结束。