﻿# Angular CLI 7.3 使用 ES2015 的 nomodule 屬性載入 Polyfills 函式庫

最近推出的 [Angular CLI 7.3](https://github.com/angular/angular-cli/releases/tag/v7.3.0) 版本，新增了一個相當亮眼的特性。當你的 Angular 應用程式同時要符合 IE 或舊版瀏覽器時，以往都會用 [Polyfill](https://en.wikipedia.org/wiki/Polyfill_\(programming\)) 來填充缺少的 HTML5/JS APIs，但這件事到了 Angular CLI 7.3 又變得更貼心了。不但如此，這個新特性一樣可以用在各種 SPA 框架中，像是 Vue, React 或其他函式庫也都可以套用相同技巧。欲知詳情請繼續看下去。

![](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/130f990e91433a572bb6-52174591-2988f380-27d1-11e9-8aaf-231cfb25bb35.png)

### 關於 polyfills.ts 檔案

在 Angular CLI 所建立的專案下，都有個 `src/polyfills.ts` 檔案，裡面可以讓你設定需要載入哪些必要的 Polyfills 函式庫，好讓你的網站也可以支援舊版瀏覽器執行。

目前 Angular 支援的 Polyfills APIs 最低可支援到 IE9 瀏覽器！

Angular CLI 內建的 Polyfills 定義一直在進化，幾乎每一個大版都會有些更貼心的設計出現，我們來看看最近幾版的變化與差異。

### 關於 Angular CLI 6 的 polyfills.ts 檔案

如果你用 Angular CLI 6 建立的 Angular 專案，其預設的 `src/polyfills.ts` 檔案內容如下。註解中詳細說明了使用的時機點，想支援哪個瀏覽器就要匯入必要的 Polyfill 模組，有些模組需要額外透過 npm 進行安裝，記得要安裝之後才能使用。

```javascript
/**
 * This file includes polyfills needed by Angular and is loaded before the app.
 * You can add your own extra polyfills to this file.
 *
 * This file is divided into 2 sections:
 *   1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
 *   2. Application imports. Files imported after ZoneJS that should be loaded before your main
 *      file.
 *
 * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
 * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
 * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
 *
 * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html
 */

/***************************************************************************************************
 * BROWSER POLYFILLS
 */

/** IE9, IE10 and IE11 requires all of the following polyfills. **/
// import 'core-js/es6/symbol';
// import 'core-js/es6/object';
// import 'core-js/es6/function';
// import 'core-js/es6/parse-int';
// import 'core-js/es6/parse-float';
// import 'core-js/es6/number';
// import 'core-js/es6/math';
// import 'core-js/es6/string';
// import 'core-js/es6/date';
// import 'core-js/es6/array';
// import 'core-js/es6/regexp';
// import 'core-js/es6/map';
// import 'core-js/es6/weak-map';
// import 'core-js/es6/set';

/** IE10 and IE11 requires the following for NgClass support on SVG elements */
// import 'classlist.js';  // Run `npm install --save classlist.js`.

/** IE10 and IE11 requires the following for the Reflect API. */
// import 'core-js/es6/reflect';

/** Evergreen browsers require these. **/
// Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove.
import 'core-js/es7/reflect';

/**
 * Web Animations `@angular/platform-browser/animations`
 * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
 * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
 **/
// import 'web-animations-js';  // Run `npm install --save web-animations-js`.

/**
 * By default, zone.js will patch all possible macroTask and DomEvents
 * user can disable parts of macroTask/DomEvents patch by setting following flags
 */

// (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
// (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
// (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames

/*
 * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
 * with the following flag, it will bypass `zone.js` patch for IE/Edge
 */
// (window as any).__Zone_enable_cross_context_check = true;

/***************************************************************************************************
 * Zone JS is required by default for Angular itself.
 */
import 'zone.js/dist/zone'; // Included with Angular CLI.

/***************************************************************************************************
 * APPLICATION IMPORTS
 */
```

### 關於 Angular CLI 7 的 polyfills.ts 檔案

我在 [Angular Taiwan 2018](https://2018.angular.tw/) 技術大會的【[Angular 7 全新功能探索](https://youtu.be/7Ryiva24lNE?t=237)】演講中曾經分享過關於 Polyfills 的改進。但我們先來看一下這個版本預設的 `src/polyfills.ts` 檔案內容：

```javascript
/**
 * This file includes polyfills needed by Angular and is loaded before the app.
 * You can add your own extra polyfills to this file.
 *
 * This file is divided into 2 sections:
 *   1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
 *   2. Application imports. Files imported after ZoneJS that should be loaded before your main
 *      file.
 *
 * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
 * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
 * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
 *
 * Learn more in https://angular.io/guide/browser-support
 */

/***************************************************************************************************
 * BROWSER POLYFILLS
 */

/** IE9, IE10, IE11, and Chrome <55 requires all of the following polyfills.
 *  This also includes Android Emulators with older versions of Chrome and Google Search/Googlebot
 */

// import 'core-js/es6/symbol';
// import 'core-js/es6/object';
// import 'core-js/es6/function';
// import 'core-js/es6/parse-int';
// import 'core-js/es6/parse-float';
// import 'core-js/es6/number';
// import 'core-js/es6/math';
// import 'core-js/es6/string';
// import 'core-js/es6/date';
// import 'core-js/es6/array';
// import 'core-js/es6/regexp';
// import 'core-js/es6/map';
// import 'core-js/es6/weak-map';
// import 'core-js/es6/set';

/** IE10 and IE11 requires the following for NgClass support on SVG elements */
// import 'classlist.js';  // Run `npm install --save classlist.js`.

/** IE10 and IE11 requires the following for the Reflect API. */
// import 'core-js/es6/reflect';

/**
 * Web Animations `@angular/platform-browser/animations`
 * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
 * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
 */
// import 'web-animations-js';  // Run `npm install --save web-animations-js`.

/**
 * By default, zone.js will patch all possible macroTask and DomEvents
 * user can disable parts of macroTask/DomEvents patch by setting following flags
 * because those flags need to be set before `zone.js` being loaded, and webpack
 * will put import in the top of bundle, so user need to create a separate file
 * in this directory (for example: zone-flags.ts), and put the following flags
 * into that file, and then add the following code before importing zone.js.
 * import './zone-flags.ts';
 *
 * The flags allowed in zone-flags.ts are listed here.
 *
 * The following flags will work for all browsers.
 *
 * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
 * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
 * (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
 *
 *  in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
 *  with the following flag, it will bypass `zone.js` patch for IE/Edge
 *
 *  (window as any).__Zone_enable_cross_context_check = true;
 *
 */

/***************************************************************************************************
 * Zone JS is required by default for Angular itself.
 */
import 'zone.js/dist/zone'; // Included with Angular CLI.

/***************************************************************************************************
 * APPLICATION IMPORTS
 */
```

大部分內容都是一樣的，除了有些註解說明有調整外，主要是有一行不見了！

```javascript
/** Evergreen browsers require these. **/
// Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove.
import 'core-js/es7/reflect';
```

原本在 Angular CLI 6 之前有一個 [reflect-metadata](https://github.com/zloirock/core-js#ecmascript-reflect) 套件預設被載入，但此套件只有執行在 JIT 模式的時候才需要，一般來說我們都會用 AOT 模式建置專案，因此這個檔案經常需要「手動」註解掉，但很多人不知道這個細節，導致這個檔案一直被額外載入，浪費了 `20.7KB` 左右的下載流量。

從 Angular CLI 7.0 開始，這項設定已經被拿掉，改由 `ng build` 的過程透過 [webpack](https://webpack.js.org/) 全自動判斷是否加入，相當貼心的設計！

### 關於 Angular CLI 7.3 的 polyfills.ts 檔案

從 Angular CLI 7.3 開始，其預設的 `src/polyfills.ts` 檔案內容如下：

```javascript
/**
 * This file includes polyfills needed by Angular and is loaded before the app.
 * You can add your own extra polyfills to this file.
 *
 * This file is divided into 2 sections:
 *   1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
 *   2. Application imports. Files imported after ZoneJS that should be loaded before your main
 *      file.
 *
 * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
 * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
 * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
 *
 * Learn more in https://angular.io/guide/browser-support
 */

/***************************************************************************************************
 * BROWSER POLYFILLS
 */

/** IE10 and IE11 requires the following for NgClass support on SVG elements */
// import 'classlist.js';  // Run `npm install --save classlist.js`.

/**
 * Web Animations `@angular/platform-browser/animations`
 * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
 * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
 */
// import 'web-animations-js';  // Run `npm install --save web-animations-js`.

/**
 * By default, zone.js will patch all possible macroTask and DomEvents
 * user can disable parts of macroTask/DomEvents patch by setting following flags
 * because those flags need to be set before `zone.js` being loaded, and webpack
 * will put import in the top of bundle, so user need to create a separate file
 * in this directory (for example: zone-flags.ts), and put the following flags
 * into that file, and then add the following code before importing zone.js.
 * import './zone-flags.ts';
 *
 * The flags allowed in zone-flags.ts are listed here.
 *
 * The following flags will work for all browsers.
 *
 * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
 * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
 * (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
 *
 *  in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
 *  with the following flag, it will bypass `zone.js` patch for IE/Edge
 *
 *  (window as any).__Zone_enable_cross_context_check = true;
 *
 */

/***************************************************************************************************
 * Zone JS is required by default for Angular itself.
 */
import 'zone.js/dist/zone'; // Included with Angular CLI.

/***************************************************************************************************
 * APPLICATION IMPORTS
 */
```

這個版本就更激進了，直接拿掉以下內容：

```javascript
/** IE9, IE10, IE11, and Chrome <55 requires all of the following polyfills.
 *  This also includes Android Emulators with older versions of Chrome and Google Search/Googlebot
 */

// import 'core-js/es6/symbol';
// import 'core-js/es6/object';
// import 'core-js/es6/function';
// import 'core-js/es6/parse-int';
// import 'core-js/es6/parse-float';
// import 'core-js/es6/number';
// import 'core-js/es6/math';
// import 'core-js/es6/string';
// import 'core-js/es6/date';
// import 'core-js/es6/array';
// import 'core-js/es6/regexp';
// import 'core-js/es6/map';
// import 'core-js/es6/weak-map';
// import 'core-js/es6/set';

/** IE10 and IE11 requires the following for the Reflect API. */
// import 'core-js/es6/reflect';
```

不過各位不用擔心，並不是說從此不支援 IE9 ~ IE11，而是有新的設計出現，叫做 [Conditional ES5 Browser Polyfill Loading](https://github.com/angular/angular-cli/pull/13403/files)！

### 條件式 ES5 瀏覽器相容套件載入機制

全新的 Angular CLI 7.3 在 `angular.json` 設定檔中增加了一條選項設定 `"es5BrowserSupport": true`：

```json
"build": {
  "builder": "@angular-devkit/build-angular:browser",
  "options": {
    "outputPath": "dist/demo1",
    "index": "src/index.html",
    "main": "src/main.ts",
    "polyfills": "src/polyfills.ts",
    "tsConfig": "src/tsconfig.app.json",
    "assets": [
      "src/favicon.ico",
      "src/assets"
    ],
    "styles": [
      "src/styles.css"
    ],
    "scripts": [],
    "es5BrowserSupport": true
  },
  "configurations": {
    "production": {
      "fileReplacements": [
        {
          "replace": "src/environments/environment.ts",
          "with": "src/environments/environment.prod.ts"
        }
      ],
      "optimization": true,
      "outputHashing": "all",
      "sourceMap": false,
      "extractCss": true,
      "namedChunks": false,
      "aot": true,
      "extractLicenses": true,
      "vendorChunk": false,
      "buildOptimizer": true,
      "budgets": [
        {
          "type": "initial",
          "maximumWarning": "2mb",
          "maximumError": "5mb"
        }
      ]
    }
  }
},
```

> 如果從舊版升級上來，記得要手動調整 `src/polyfills.ts` 與 `angular.json` 的選項設定！([schema.json](https://github.com/clydin/angular-cli/blob/dd4c381498344cf9de8ca4d2fc95237931934d0e/packages/angular/cli/lib/config/schema.json#L867-L871))

這個 `"es5BrowserSupport"` 選項如果設定為 `true` 的話，當你使用 `ng build` 或 `ng build --prod` 的時候就會自動自動幫你打包上述這幾個 Polyfills 到一個獨立的 JS 檔案中，檔名為 `es2015-polyfills.*****.js`

但真正的重點在於這個檔案的載入方式，請看一下 `ng build --prod` 完後後建置出來的 `index.html` 檔案內容：

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Demo1</title>
    <base href="/" />

    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link rel="icon" type="image/x-icon" href="favicon.ico" />
    <link rel="stylesheet" href="styles.3ff695c00d717f2d2a11.css" />
  </head>

  <body>
    <app-root></app-root>

    <script
      type="text/javascript"
      src="runtime.b57bf819d5bdce77f1c7.js"
    ></script>
    <script
      type="text/javascript"
      src="es2015-polyfills.41976a8133a2445ac0d9.js"
      nomodule
    ></script>
    <script
      type="text/javascript"
      src="polyfills.fb4ac03bdf7e23477d5b.js"
    ></script>
    <script type="text/javascript" src="main.e42ec0deb336589a6946.js"></script>
  </body>
</html>
```

看到亮點了嗎？

這裡的 `es2015-polyfills.*****.js` 檔案被載入時，被加上了 `nomodule` 屬性。這個屬性只有在支援 ES2015 的瀏覽器才認得，這些現代化的瀏覽器只要看見 `<script>` 標籤使用了 `nomodule` 屬性，就會自動忽略這個 JS 檔案載入，並不是載入後不執行，而是連載入動作都不會發生，大幅節省頻寬！(約 `56K` 左右)

但是 IE 或其他舊版瀏覽器，並不認得 `nomodule` 屬性，所以檔案就還是會載入並執行。如此一來，就徹底解決了跨瀏覽器相容的問題，是不是相當漂亮！

### Safari 10.1 不支援 nomodule 的問題

目前已知[支援 ES2015 模組化技術](https://caniuse.com/#feat=es6-module)的瀏覽器版本如下：

-   Safari 10.1+
-   Chrome 61+
-   Firefox 60+
-   Edge 16+

不過 Safari 10.1 卻不支援 `nomodule` 屬性，解決方法可以參考 [Safari 10.1 nomodule support](https://gist.github.com/samthor/64b114e4a4f539915a95b91ffd340acc) 這個 Polyfills 來解決。這份 Polyfills 有詳細註解說明方式，請看仔細再加入到你的現有專案中！

### 相關連結

-   [What’s new in Angular CLI 7.3? | Ninja Squad](https://blog.ninja-squad.com/2019/01/31/angular-cli-7.3/)
-   [Intelligently load polyfills · Issue #6577 · angular/angular-cli](https://github.com/angular/angular-cli/issues/6577)
-   [Conditional ES5 Browser Polyfill Loading by clydin · Pull Request #13403 · angular/angular-cli](https://github.com/angular/angular-cli/pull/13403/files)
-   [Can I use... JavaScript modules via script tag](https://caniuse.com/#feat=es6-module)
-   [Polyfill.io](https://polyfill.io/)
-   [HTML5 Cross Browser Polyfills · Modernizr/Modernizr Wiki](https://github.com/Modernizr/Modernizr/wiki/HTML5-Cross-Browser-Polyfills)
