现在 所有主流现代浏览器都已经支持 https://caniuse.com/#feat=es6-module JavaScript 模块. 本文将介绍如何使用 JS 模块, 如何有效地部署, 以及 Chrome 团队如何使 JS 模块在未来变得更好用.
什么是 JS 模块?
JS 模块 (也称为 "ES 模块" 或 "ECMAScript 模块") 是 ES6 中一项非常重要的语言特性. 在此以前, 你可能使用过用户级别的 JavaScript 模块系统, 比如在 Node.js 中的 CommonJS https://nodejs.org/docs/latest-v10.x/api/modules.html , 或者是 https://github.com/amdjs/amdjs-api/blob/master/AMD.md , 或者其他别的实现. 所有的模块系统都包含一个共同点: 允许导入和导出内容.
现在, JavaScript 拥有标准化的语法来完成这些事. 在一个模块中, 可以使用 export 关键字来导出任何内容, 比如一个 const , 一个 function 或任何其他变量绑定或是声明. 只需在变量语句或声明前面加上 export 即可:
- // lib.mjs
- export const repeat = (string) => `${string} ${string}`;
- export function shout(string) {
- return `${string.toUpperCase()}!`;
- }
然后可以使用 import 关键字从另一个模块导入模块. 在这里, 我们从 lib 模块导入 repeat 和 shout 函数, 并在 main 模块中使用它们 :
- // main.mjs
- import {repeat, shout} from './lib.mjs';
- repeat('hello');
- // 'hello hello'
- shout('Modules in action');
- // 'MODULES IN ACTION!'
还也可以从模块中导出 default 值:
- // lib.mjs
- export default function(string) {
- return `${string.toUpperCase()}!`;
- }
这时候 default 导出可以使用任何名称导入:
- // main.mjs
- import shout from './lib.mjs';
- // ^^^^^
模块与经典脚本有点不太一样:
模块默认开启严格模式 https://developer.mozilla.org/en-US/docs/web/JavaScript/Reference/Strict_mode .
模块中不支持 HTML 注释语法.
- // Don't use HTML-style comment syntax in JavaScript!
- const x = 42; <!-- TODO: Rename x to y.
- // Use a regular single-line comment instead:
- const x = 42; // TODO: Rename x to y.
- 模块拥有一个顶级词法作用域. 也就是说, 如果在一个模块中运行 var foo = 42; 不会创建一个名为 foo, 可以在浏览器通过 window.foo 访问的全局变量.
- 新的静态 import 和 export 语法仅在模块中可用, 不适用于经典脚本.
- 正是由于这些差异, 相同的 JavaScript 代码在模块与经典脚本时可能会在处理上存在差异. 因此, JavaScript runtime 需要区分哪些脚本是模块.
- 在浏览器中使用 JS 模块
- 在 Web 上, 可以将 <script> 元素中的 type 属性设置为 module, 通过这种方式来告诉浏览器将其视为模块.
- <script type="module" src="main.mjs"></script>
- <script nomodule src="fallback.js"></script>
- 能够解析 type="module" 的浏览器将忽略具有 nomodule 属性的脚本. 这意味着, 可以对支持模块的浏览器提供基于模块的代码, 同时对其他不支持浏览器的提供 fallback 脚本. 这个特性在性能上是有很大好处的, 由于只有现代浏览器支持模块, 如果浏览器能够解析模块, 那应该还支持模块之前的语言特性 https://codepen.io/samthor/pen/MmvdOM , 比如箭头函数或是 async- await. 这样就不必在基于模块的包中编译这些语言特性, 就能为现代浏览器提供体积更小并且没有经过编译的代码 https://philipwalton.com/articles/deploying-es2015-code-in-production-today/ . 只有在传统浏览器才会降级使用带有 nomodule 的脚本.
- 模块和经典脚本之间的浏览器特定差异
- 现在已经了解到模块与经典脚本的不同之处, 除了上面列出的平台无关差异之外, 还有一些浏览器特定差异.
- 比如, 模块只会执行一次, 而经典脚本则只要将其添加到 DOM 多少次就会执行多少次.
- <script src="classic.js"></script>
- <script src="classic.js"></script>
- <!-- classic.js executes multiple times. -->
- <script type="module" src="module.mjs"></script>
- <script type="module" src="module.mjs"></script>
- <script type="module">import './module.mjs';</script>
- <!-- module.mjs executes only once. -->
另外, 模块脚本及其依赖关系通过 CORS 获取. 也就是说, 任何跨域模块脚本都必须提供正确的 HTTP 头部信息, 比如
- Access-Control-Allow-Origin: *
- .
另一个区别与 async 属性有关, 它可以让脚本下载而不阻止 HTML 解析器(就像 defer), 但会立即执行脚本, 不保证顺序, 并且不需要等待 HTML 解析完成. async 属性不适用于内联经典脚本, 但仍适用于内联
- <script type="module">
- .
关于文件扩展名的说明
可能已经注意到我们正在使用 .mjs 作为模块的文件扩展名. 在 Web 上, 文件扩展名无关紧要, 只要该文件是 JavaScript MIME 类型 text/javascript https://html.spec.whatwg.org/
multipage/scripting.html#scriptingLanguages:javascript-mime-type . 从 script 元素的 type 属性, 浏览器就能知道它是一个模块.
不过, 我们建议在模块使用 .mjs 扩展名, 原因有两个:
在开发过程中, 它清楚地表明该文件是一个模块, 而不是一个普通的脚本. 如前所述, 模块的处理方式与普通脚本不同, 因此必须通过某种方式表明其差异.
与 Node.js 保持一致, 模块实现的实验版本 https://nodejs.org/api/esm.html 目前只支持带 .mjs 扩展名的文件.
注意: 在 Web 上部署 .mjs , 需要在 Web 服务器将此扩展名配置成
Content-Type: text/javascript
. 另外, 你可能还希望配置编辑器将 .mjs 文件视为 .js 文件来获得语法高亮显示, 事实上大多数现代编辑已经默认这样做了.
模块标识符
在 import 模块时, 指定模块位置的字符串称为 "模块标识符" 或 "导入标识符". 在之前的例子中, 模块标识符是 './lib.mjs':
- import {shout} from './lib.mjs';
- // ^^^^^^^^^^^
在浏览器中使用模块标识符有一些限制. 目前不支持 "纯" 模块标识符, 这个限制可以参考 HTML 规范 https://html.spec.whatwg.org/multipage/webappapis.html#resolve-a-module-specifier , 以便将来浏览器可以允许自定义模块加载器为纯模块标识符赋予特殊含义, 如下所示:
- // Not supported (yet):
- import {shout} from 'jquery';
- import {shout} from 'lib.mjs';
- import {shout} from 'modules/lib.mjs';
另一方面, 下面的例子都是支持的:
- // Supported:
- import {shout} from './lib.mjs';
- import {shout} from '../lib.mjs';
- import {shout} from '/modules/lib.mjs';
- import {shout} from 'https://simple.example/modules/lib.mjs';
现在, 模块标识符必须是完整的 URL, 或是类似 /,./ 或 ../ 这样的相对 URL .
模块默认 defer
经典 <script> 元素默认阻止 HTML 解析器. 我们可以通过添加 defer 属性 https://html.spec.whatwg.org/multipage/scripting.html#attr-script-defer 来保证了脚本下载与 HTML 解析同时进行.
https://developers.google.com/web/fundamentals/primers/imgs/async-defer.svg
而模块脚本默认 defer. 因此, 不需要添加 defer 到
<script type="module">
标签. 不仅主模块的下载与 HTML 解析并行, 所有依赖模块也是如此.
其他模块特性
动态 import()
到目前为止, 我们只使用静态 import. 使用静态 import 时, 整个模块需要在主代码运行之前下载并执行. 但有时, 可能并不希望预先加载模块, 而是按需加载模块, 只有在用到时才加载, 比如时在用户单击链接或按钮时, 以此到达提高初始化性能的需求. 这就是 动态 import() https://developers.google.com/web/updates/2017/11/dynamic-import .
- <script type="module">
- (async () => {
- const moduleSpecifier = './lib.mjs';
- const {repeat, shout} = await import(moduleSpecifier);
- repeat('hello');
- // 'hello hello'
- shout('Dynamic import in action');
- // 'DYNAMIC IMPORT IN ACTION!'
- })();
- </script>
- function loadThumbnail(relativePath) {
- const url = new URL(relativePath, import.meta.url);
- const image = new Image();
- image.src = url;
- return image;
- }
- const thumbnail = loadThumbnail('../img/thumbnail.png');
- container.append(thumbnail);
- export function drop() { /* ... */ }
- export function pluck() { /* ... */ }
- export function zip() { /* ... */ }
- <link rel="modulepreload" href="lib.mjs">
- <link rel="modulepreload" href="main.mjs">
- <script type="module" src="main.mjs"></script>
- <script nomodule src="fallback.js"></script>
- const worker = new SharedWorker('worker.mjs', { type: 'module' });
- const registration = await navigator.serviceWorker.register('worker.mjs', { type: 'module' });
- import moment from 'moment';
- import { pluck } from 'lodash-es';
- <virtual-scroller>
- <!-- Content goes here. -->
- </virtual-scroller>
来源: https://juejin.im/entry/5b38c69b51882574e10e0be8