Angular 学习之路 05 - 引导

本章将介绍 Angular 是如何引导的:我们将知道 Angular 内部是如何工作的。前面我们已经利用 Angular CLI 创建了一个完整的 Angular 项目。这个项目有模板式的代码以及一对配置文件。当我们使用ng serve命令运行时,我们可以看到一个还算不错的页面。那么,从开始启动到这个页面显示到浏览器,Angular 究竟做了什么呢?

引导是初始化或者加载 Angular 应用的技术。我们以之前创建的 Angular 项目为例,看看那个AppComponent组件是如何加载,并且最终将页面显示出来的。事实上,Angular 为了加载第一个视图,经历了下面几个步骤:

  • 加载 index.html
  • 加载 Angular、第三方库以及应用本身的代码
  • 将 main.ts 作为应用的入口点
  • 根模块
  • 根组件
  • 模板

加载 index.html

Web 应用需要一个入口点,index.html 通常就是第一个加载的文件。下面我们打开这个文件,看看里面有什么内容:

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>demo</title>
  <base href="/">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
  <app-root></app-root>
</body>
</html>

注意,这个文件中没有加载任何 JavaScript,也没有加载 CSS。<body>里面只有<app-root>一个标签。那么,Angular 是如何加载的呢?为了说明这一点,首先我们要构建整个项目。

构建应用

使用 Angular CLI 构建项目非常简单,只要一行命令:

ng build

我们知道,浏览器只能解释 JavaScript;而 Angular 项目使用的是 TypeScript。也就是说,Angular 项目必须经过构建才能运行。之前我们使用的是ng serve这个命令。ng serve会首先构建整个 Angular 项目,将构建结果保存在内存中,然后启动一个开发测试用的服务器,通过这个服务器(默认监听 4200 端口,也就是前面我们要访问时提供的端口号)访问 Angular 项目。ng build与此类似,只不过ng build会将构建的结果默认保存在 dist 文件夹下,并且不会启动内置的开发服务器。

注意,ng build构建的是开发版本,如果要构建发布版,则需要使用ng build --prod。发布版构建时间更长,但会进行代码优化、代码压缩、代码混淆等步骤。

执行过ng build之后,打开 dist 中的 index.html 文件。index.html 看起来如下:

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Demo</title>
  <base href="/">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
  <app-root></app-root>
<script src="runtime.js" defer></script><script src="polyfills.js" defer></script><script src="styles.js" defer></script><script src="vendor.js" defer></script><script src="main.js" defer></script></body>
</html>

经过编译之后的 index.html 已经与原始的不一样了。我们看到 index.html 的最后多出来五个 JavaScript 文件:runtime.js、polyfills.js、styles.js、vendor.js 以及 main.js:

  • runtime.js - Webpack 运行时文件
  • polyfills.js - Polyfill 脚本,用于提供浏览器兼容性
  • styles.js - 以 JavaScript 文件保存的全局样式
  • vendor.js - Angular 核心库以及其它第三方库
  • main.js - 应用程序的代码,也就是 src 中的代码

这些文件是由 Webpack 的模块加载器添加到 index.html 中的。

什么是 Webpack?

Webpack 是一个打包器,它会扫描应用中的 JavaScript 文件,将许多 JavaScript 文件合并为一个或多个大的文件。Webpack 能够打包 JavaScript、CSS、SASS、LESS、图像、HTML、字体等文件。

Angular CLI 使用 Webpack 作为模块打包器。Webpack 需要一大堆配置才能正确工作,而这一切 Angular CLI 都已经帮我们做好了。

Webpack 会扫描项目中的文件,然后将可以合并的文件进行合并。在这个例子中,Webpack 最终合并得到五个文件。

加载应用

当 index.html 加载完成之后,Angular 会继续加载自己的核心库和第三方依赖库。现在,Angular 需要定位到程序的入口点。

正如 Java、C++ 等使用main()函数作为程序入口点一样,Angular 使用 main.ts 作为自己的入口点。这个文件就在 src 文件夹中。

angular.json

main.ts 文件作为入口点并不是写死在 Angular 中的,而是在 angular.json 文件中配置的。

{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "version": 1,
  "newProjectRoot": "projects",
  "projects": {
    "demo": {
      "projectType": "application",
      "schematics": {},
      "root": "",
      "sourceRoot": "src",
      "prefix": "app",
      "architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:browser",
          "options": {
            "outputPath": "dist/demo",
            "index": "src/index.html",
            "main": "src/main.ts",
            "polyfills": "src/polyfills.ts",
            "tsConfig": "tsconfig.app.json",
            "aot": true,
            "assets": [
              "src/favicon.ico",
              "src/assets"
            ],
            "styles": [
              "src/styles.css"
            ],
            "scripts": []
          },
          "configurations": {
            "production": {
              "fileReplacements": [
                {
                  "replace": "src/environments/environment.ts",
                  "with": "src/environments/environment.prod.ts"
                }
              ],
              "optimization": true,
              "outputHashing": "all",
              "sourceMap": false,
              "extractCss": true,
              "namedChunks": false,
              "extractLicenses": true,
              "vendorChunk": false,
              "buildOptimizer": true,
              "budgets": [
                {
                  "type": "initial",
                  "maximumWarning": "2mb",
                  "maximumError": "5mb"
                },
                {
                  "type": "anyComponentStyle",
                  "maximumWarning": "6kb",
                  "maximumError": "10kb"
                }
              ]
            }
          }
        },
        ...
      }
    }},
  "defaultProject": "demo"
}

注意,main.ts 的设置在 projects - demo - architect - build - options 中。这个文件就是整个应用程序的入口点。

程序入口点 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));

我们在 main.ts 前面几行可以看到

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

这意味着,我们从 @angular/platform-browser-dynamic 导入了platformBrowserDynamic

什么是platformBrowserDynamic

platformBrowserDynamic是一个模块,用于在桌面浏览器加载 Angular 应用。

Angular 应用可以在多个平台使用多种方式引导。例如,我们可以在桌面浏览器加载 Angular 应用,也可以在移动设备使用 Ionic 或 NativeScript 加载。如果我们使用的是 NativeScript,那么就要使用nativescript-angular/platform库的platformNativeScriptDynamic,然后调用platformNativeScriptDynamic().bootstrapModule(AppModule)。详细代码见 Angular NativeScript 的引导文档

import { AppModule } from './app/app.module';

上面语句引入了AppModuleAppModule是应用的根模块。前面我们介绍过,Angular 使用模块组织项目:每一个 Angular 项目至少有一个模块。这个模块会第一个加载,被称为根模块。

platformBrowserDynamic().bootstrapModule(AppModule)
  .catch(err => console.error(err));

platformBrowserDynamic()通过调用bootstrapModule()函数加载根模块,这个函数的参数是一个模块的引用,比如这里的AppModule

根模块至少包含一个组件。当根模块加载的时候,这个组件也就被加载。AppModule中使用bootstrap指定这个根组件。Angular 会读取bootstrap元数据,然后加载其中列出的AppComponent组件。

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

One Response

  1. langyo 2020年9月1日

Leave a Reply