这篇翻译不完整。请帮忙从英语翻译这篇文章

这篇指南会给你入门 Javascript 模块的全部信息。

模块化的背景

Javascript 程序们本来很小——在早期,它们大多被用来执行独立的脚本任务,提供在需要的地方与你的 web 页面交互的能力,所以一般不需要多大的脚本。过了几年,我们现在有了运行大量 Javascript 脚本的复杂程序,还有一些被用在其他环境(例如 Node.js)。

因此,近年来,有必要开始考虑提供一种将 JavaScript 程序拆分为可按需导入的单独模块的机制。Node.js 已经提供这个能力很长时间了,还有很多的 Javascript 库和框架 已经开始了模块的使用(for example other CommonJS and AMD-based module systems like RequireJS, and more recently Webpack and Babel.

好消息是,最新的浏览器开始原生支持模块功能了,and this is what this article is all about. 这会是一个好事情 — 浏览器能够最优化加载模块,使它比使用库更有效率,做额外的客户端进程和round trips。

浏览器支持

使用JavaScript 模块依赖于importexport,浏览器兼容性如下:

import

Update compatibility data on GitHub
DesktopMobileServer
ChromeEdgeFirefoxInternet ExplorerOperaSafariAndroid webviewChrome for AndroidFirefox for AndroidOpera for AndroidSafari on iOSSamsung InternetNode.js
importChrome Full support 61Edge Full support 16
Full support 16
Full support 15
Disabled
Disabled From version 15: this feature is behind the Experimental JavaScript Features preference.
Firefox Full support 60
Full support 60
No support 54 — 60
Disabled
Disabled From version 54 until version 60 (exclusive): this feature is behind the dom.moduleScripts.enabled preference. To change preferences in Firefox, visit about:config.
IE No support NoOpera Full support 47Safari Full support 10.1WebView Android Full support 61Chrome Android Full support 61Firefox Android Full support 60
Full support 60
No support 54 — 60
Disabled
Disabled From version 54 until version 60 (exclusive): this feature is behind the dom.moduleScripts.enabled preference. To change preferences in Firefox, visit about:config.
Opera Android Full support 44Safari iOS Full support 10.1Samsung Internet Android No support Nonodejs Full support 8.5.0
Notes Disabled
Full support 8.5.0
Notes Disabled
Notes files must have suffix .mjs, not .js
Disabled From version 8.5.0: this feature is behind the --experimental-modules runtime flag.
Dynamic importChrome Full support 63Edge No support No
Notes
No support No
Notes
Notes See development status.
Firefox Full support 67
Full support 67
No support 66 — 67
Disabled
Disabled From version 66 until version 67 (exclusive): this feature is behind the javascript.options.dynamicImport preference (needs to be set to true). To change preferences in Firefox, visit about:config.
IE No support NoOpera Full support 50Safari Full support 11.1WebView Android Full support 63Chrome Android Full support 63Firefox Android Full support 67
Full support 67
No support 66 — 67
Disabled
Disabled From version 66 until version 67 (exclusive): this feature is behind the javascript.options.dynamicImport preference (needs to be set to true). To change preferences in Firefox, visit about:config.
Opera Android Full support 46Safari iOS Full support 11.1Samsung Internet Android No support Nonodejs ?

Legend

Full support  
Full support
No support  
No support
Compatibility unknown  
Compatibility unknown
See implementation notes.
See implementation notes.
User must explicitly enable this feature.
User must explicitly enable this feature.

export

Update compatibility data on GitHub
DesktopMobileServer
ChromeEdgeFirefoxInternet ExplorerOperaSafariAndroid webviewChrome for AndroidFirefox for AndroidOpera for AndroidSafari on iOSSamsung InternetNode.js
exportChrome Full support 61Edge Full support 16
Full support 16
Full support 15
Disabled
Disabled From version 15: this feature is behind the Experimental JavaScript Features preference.
Firefox Full support 60
Full support 60
No support 54 — 60
Disabled
Disabled From version 54 until version 60 (exclusive): this feature is behind the dom.moduleScripts.enabled preference. To change preferences in Firefox, visit about:config.
IE No support NoOpera Full support 47Safari Full support 10.1WebView Android No support NoChrome Android Full support 61Firefox Android Full support 60
Full support 60
No support 54 — 60
Disabled
Disabled From version 54 until version 60 (exclusive): this feature is behind the dom.moduleScripts.enabled preference. To change preferences in Firefox, visit about:config.
Opera Android Full support 44Safari iOS Full support 10.1Samsung Internet Android No support Nonodejs ?

Legend

Full support  
Full support
No support  
No support
Compatibility unknown  
Compatibility unknown
User must explicitly enable this feature.
User must explicitly enable this feature.

介绍一个例子

为了演示模块的使用,我们创建了一个 simple set of examples ,你可你在Github上找到。这个例子演示了一个简单的模块的集合用来在web页面上创建了一个<canvas> 标签,在canvas上绘制 (and report information about) 不同形状。

这的确有点简单,但是保持足够简单能够清晰地演示模块。

Note: 如果你想去下载这个例子在本地运行,你需要通过本地web 服务器去运行。然后在一个本地web 服务器上去运行 (或者你自己开发版的服务器), 这个例子将不会工作,除非你修改 import 语句的模块的路径. As you'll see below, module imports use paths that are relative to the root of the site, in the same way that service workers do.   (修订版 1889482)

基本的示例文件的结构

在我们的第一个例子 (see basic-modules) 文件结构如下:

index.html
main.js
modules/
    canvas.js
    square.js

Note: 在这个指南的全部示例项目的文件结构是基本相同的; 需要熟悉上面的内容

modules 目录下的两个模块的描述如下:

  • canvas.js — contains functions related to setting up the canvas:
    • create() — creates a canvas with a specified width and height inside a wrapper <div> with a specified ID, which is itself appended inside a specified parent element. Returns an object containing the canvas's 2D context and the wrapper's ID.
    • createReportList() — creates an unordered list appended inside a specified wrapper element, which can be used to output report data into. Returns the list's ID.
  • square.js — contains:
    • name — a constant containing the string 'square'.
    • draw() — draws a square on a specified canvas, with a specified size, position, and color. Returns an object containing the square's size, position, and color.
    • reportArea() — writes a square's area to a specific report list, given its length.
    • reportPerimeter() — writes a square's perimeter to a specific report list, given its length.

导出模块的功能

为了获得模块的功能要做的第一件事是把它们导出来。使用 export 语句来完成。

最简单的方法是把它(指上面的export语句)放到你想要导出的项前面,比如:

export const name = 'square';

export function draw(ctx, length, x, y, color) {
  ctx.fillStyle = color;
  ctx.fillRect(x, y, length, length);

  return {
    length: length,
    x: x,
    y: y,
    color: color
  };
}

你能够导出函数, var, let, const, 和— 我们等会会看到 — 类. They need to be top-level items; 比如你不能够在函数中导出。

一个更方便的方法到处所有你想要到处的模块的方法是在模块文件的末尾使用一个export 语句, followed by a comma-separated list of the features you want to export wrapped in curly braces. 比如:

export { name, draw, reportArea, reportPerimeter };

导入功能到你的脚本

你想在模块外面使用一些功能,那你就需要导入他们才能使用。最简单的就像下面这样的:

import { name, draw, reportArea, reportPerimeter } from '/js-examples/modules/basic-modules/modules/square.js';

使用 import 语句,然后你被花括号包围的用逗号分隔的你想导入的功能列表 , 然后是关键字from, 然后是模块文件的路径。--相对于站点根目录的相对路径,对于我们的basic-modules 应该是 /js-examples/modules/basic-modules

当然,我们写的路径有一点不同---我们使用点语法意味 “当前路径”,跟随着包含我们想要找的文件的路径。这比每次都要写下整个相对路径要好得多,因为它更短,使得URL 可移植 ---如果在站点层中你把它移动到不同的路径下面仍然能够工作。(修订版 1889482)

所以看例子吧:

/js/examples/modules/basic-modules/modules/square.js

变成了

.modules/square.js

You can see such lines in action in main.js.

Note:在一些模块系统中你可以忽略文件扩展名(比如'/model/squre' .这在原生JavaScript 模块系统中不工作。此外,记住你需要包含最前面的正斜杠。   (修订版 1889482)

因为你导入了这些功能到你的脚本文件,你可以想定义在相同的文件中的一样去使用它。下面展示的是在 main.js 中的import 语句下面的内容。

let myCanvas = create('myCanvas', document.body, 480, 320);
let reportList = createReportList(myCanvas.id);

let square1 = draw(myCanvas.ctx, 50, 50, 100, 'blue');
reportArea(square1.length, reportList);
reportPerimeter(square1.length, reportList);

应用模块到你的HTML

Now we just need to apply the main.js module to our HTML page. This is very similar to how we apply a regular script to a page, with a few notable differences.

首先,你需要把 type="module" 放到 <script> 标签中, 来声明这个脚本是一个模块:

<script type="module" src="main.js"></script>

你导入模块功能的脚本基本是作为顶级模块。 If you omit it, Firefox for example gives you an error of "SyntaxError: import declarations may only appear at top level of a module".

你只能在模块内部使用 importexport 语句 ;不是普通脚本文件。

Note: You can also import modules into internal scripts, as long as you include type="module", for example <script type="module"> //include script here </script>.

其他模块与标准脚本的不同

  • 你需要注意本地测试 —  如果你通过本地加载Html 文件 (比如一个 file:// 路径的文件), 你将会遇到 CORS 错误,因为Javascript 模块安全性需要。你需要通过一个服务器来测试。
  • Also, note that you might get different behavior from sections of script defined inside modules as opposed to in standard scripts. This is because modules use strict mode automatically.
  • 加载一个模块脚本时不需要使用 defer 属性 (see <script> attributes) 模块会自动延迟加载。
  • 最好一个但不是不重要,你需要明白模块功能导入到单独的脚本文件的范围 — 他们无法在全局获得. 因此,你只能在导入这些功能的脚本文件中使用他们,你也无法通过Javascript console 中获取到他们, 比如,在DevTools 中你仍然能够获取到语法错误,但是你可能无法像你想的那样使用一些debug 技术 

默认导出 versus 命名导出

到目前为止我们导出的功能都是由named exports 组成— each item (be it a function, const, etc.) has been referred to by its name upon export, and that name has been used to refer to it on import as well.

还有一种导出类型叫做 default export — this is designed to make it easy to have a default function provided by a module, and also helps JavaScript modules to interoperate with existing CommonJS and AMD module systems (as explained nicely in ES6 In Depth: Modules by Jason Orendorff; search for "Default exports").

看个例子来解释它如何工作。In our basic-modules square.js you can find a function called randomSquare() that creates a square with a random color, size, and position.我们想导出作为默认的,所以在文件的底部我们这样写 :

export default randomSquare;

注意,不要大括号。

我们可以把 export default 放到函数前面,定义它为一个匿名函数,像这样:

export default function(ctx) {
  ...
}

Over in our main.js file, we import the default function using this line:

import randomSquare from '/js-examples/modules/basic-modules/modules/square.js';

在一次,没有大括号,因为每个模块只允许有一个默认导出, 我们知道 randomSquare 就是需要的那个. 上面的那一行相当于下面的缩写:

import {default as randomSquare} from '/js-examples/modules/basic-modules/modules/square.js';

Note: The as syntax for renaming exported items is explained below in the Renaming imports and exports section.

避免命名冲突

到目前为止,我们的canvas 图形绘制模块看起来工作的很好。但是如果我们添加一个绘制其他形状的比如圆形或者矩形的模块会发生什么?这些形状可能会有相关的函数比如 draw()reportArea(),等等;如果我们用相同的名字导入不同的函数到顶级模块文件中,我们会收到冲突和错误。

幸运的是,有很多方法来避免。我们将会在下一个节看到。

重命名导出与导入

在你的 import 和 export 语句的大括号中,可以使用 as 关键字跟一个新的名字,来改变你在顶级模块中将要使用的功能的标识名字。So for example both of the following would do the same job, albeit in a slightly different way:

// inside module.js
export {
  function1 as newFunctionName,
  function2 as anotherNewFunctionName
};

// inside main.js
import { newFunctionName, anotherNewFunctionName } from '/modules/module.js';
// inside module.js
export { function1, function2 };

// inside main.js
import { function1 as newFunctionName,
         function2 as anotherNewFunctionName } from '/modules/module.js';

Let's look at a real example. In our renaming directory you'll see the same module system as in the previous example, except that we've added circle.js and triangle.js modules to draw and report on circles and triangles.

Inside each of these modules, we've got features with the same names being exported, and therefore each has the same export statement at the bottom:

export { name, draw, reportArea, reportPerimeter };

When importing these into main.js, if we tried to use

import { name, draw, reportArea, reportPerimeter } from '/js-examples/modules/renaming/modules/square.js';
import { name, draw, reportArea, reportPerimeter } from '/js-examples/modules/renaming/modules/circle.js';
import { name, draw, reportArea, reportPerimeter } from '/js-examples/modules/renaming/modules/triangle.js';

The browser would throw an error such as "SyntaxError: redeclaration of import name" (Firefox).

Instead we need to rename the imports so that they are unique:

import { name as squareName,
         draw as drawSquare,
         reportArea as reportSquareArea,
         reportPerimeter as reportSquarePerimeter } from '/js-examples/modules/renaming/modules/square.js';

import { name as circleName,
         draw as drawCircle,
         reportArea as reportCircleArea,
         reportPerimeter as reportCirclePerimeter } from '/js-examples/modules/renaming/modules/circle.js';

import { name as triangleName,
        draw as drawTriangle,
        reportArea as reportTriangleArea,
        reportPerimeter as reportTrianglePerimeter } from '/js-examples/modules/renaming/modules/triangle.js';

Note that you could solve the problem in the module files instead, e.g.

// in square.js
export { name as squareName,
         draw as drawSquare,
         reportArea as reportSquareArea,
         reportPerimeter as reportSquarePerimeter };
// in main.js
import { squareName, drawSquare, reportSquareArea, reportSquarePerimeter } from '/js-examples/modules/renaming/modules/square.js';

And it would work just the same. What style you use is up to you, however it arguably makes more sense to leave your module code alone, and make the changes in the imports. This especially makes sense when you are importing from third party modules that you don't have any control over.

创建模块对象

上面的方法工作的挺好,但是有一点点混乱、亢长。一个更好的解决方是,导入每一个模块功能到一个模块功能对象上。 The following syntax form does that:

import * as Module from '/modules/module.js';

This grabs all the exports available inside module.js, and makes them available as members of an object Module, effectively giving it its own namespace. So for example:

Module.function1()
Module.function2()
etc.

Again, let's look at a real example. If you go to our module-objects directory, you'll see the same example again, but rewritten to take advantage of this new syntax. In the modules, the exports are all in the following simple form:

export { name, draw, reportArea, reportPerimeter };

The imports on the other hand look like this:

import * as Canvas from '/js-examples/modules/module-objects/modules/canvas.js';

import * as Square from '/js-examples/modules/module-objects/modules/square.js';
import * as Circle from '/js-examples/modules/module-objects/modules/circle.js';
import * as Triangle from '/js-examples/modules/module-objects/modules/triangle.js';

In each case, you can now access the module's imports underneath the specified object name, for example

let square1 = Square.draw(myCanvas.ctx, 50, 50, 100, 'blue');
Square.reportArea(square1.length, reportList);
Square.reportPerimeter(square1.length, reportList);

So you can now write the code just the same as before (as long as you include the object names where needed), and the imports are much neater.

模块与类(class)

As we hinted at earlier, you can also export and import classes; this is another option for avoiding conflicts in your code, and is especially useful if you've already got your module code written in an object-oriented style.

You can see an example of our shape drawing module rewritten with ES classes in our classes directory. As an example, the square.js file now contains all its functionality in a single class:

class Square {
  constructor(ctx, listId, length, x, y, color) {
    ...
  }

  draw() {
    ...
  }

  ...
}

which we then export:

export { Square };

Over in main.js, we import it like this:

import { Square } from '/js-examples/modules/classes/modules/square.js';

And then use the class to draw our square:

let square1 = new Square(myCanvas.ctx, myCanvas.listId, 50, 50, 100, 'blue');
square1.draw();
square1.reportArea();
square1.reportPerimeter();

合并模块

There will be times where you'll want to aggregate modules together. You might have multiple levels of dependencies, where you want to simplify things, combining several submodules into one parent module. This is possible using export syntax of the following forms in the parent module:

export * from 'x.js'
export { name } from 'x.js'

Note: This is actually shorthand for a import followed by an export, i.e. "I am importing module x.js, then re-exporting some or all of its exports".

For an example, see our module-aggregation directory. In this example (based on our earlier classes example) we've got an extra module called shapes.js, which aggregates all the functionality from circle.js, square.js, and triangle.js together. We've also moved our submodules inside a subdirectory inside the modules directory called shapes. So the module structure is now

modules/
  canvas.js
  shapes.js
  shapes/
    circle.js
    square.js
    triangle.js

In each of the submodules, the export is of the same form, e.g.

export { Square };

Next up comes the aggregation part. Inside shapes.js, we include the following lines:

export { Square } from '/js-examples/modules/module-aggregation/modules/shapes/square.js';
export { Triangle } from '/js-examples/modules/module-aggregation/modules/shapes/triangle.js';
export { Circle } from '/js-examples/modules/module-aggregation/modules/shapes/circle.js';

These grab the exports from the individual submodules and effectively make them available from the shapes.js module.

Note: Even though the shapes.js file is inside the modules directory, we still need to write these URLs relative to the module root, so /modules/ is needed. This is a common cause of confusion when working with JavaScript modules.

Note: The exports referenced in shapes.js basically get redirected through the file and don't really exist there, so you won't be able to write any useful related code inside the same file.

So now in the main.js file, we can get access to all three module classes by replacing

import { Square } from '/js-examples/modules/classes/modules/square.js';
import { Circle } from '/js-examples/modules/classes/modules/circle.js';
import { Triangle } from '/js-examples/modules/classes/modules/triangle.js';

with the following single line:

import { Square, Circle, Triangle } from '/js-examples/modules/module-aggregation/modules/shapes.js';

动态加载模块

The newest part of the JavaScript modules functionality to be available in browsers is dynamic module loading. This allows you to dynamically load modules only when they are needed, rather than having to load everything up front. This has some obvious performance advantages; let's read on and see how it works.

This new functionality allows you to call import() as a function, passing it the path to the module as a parameter. It returns a promise, which fulfills with a module object (see Creating a module object) giving you access to that object's exports, e.g.

import('/modules/myModule.js')
  .then((module) => {
    // Do something with the module.
  });

Let's look at an example. In the dynamic-module-imports directory we've got another example based on our classes example. This time however we are not drawing anything on the canvas when the example loads. Instead, we include three buttons — "Circle", "Square", and "Triangle" — that, when pressed, dynamically load the required module and then use it to draw the associated shape.

In this example we've only made changes to our index.html and main.js files — the module exports remain the same as before.

Over in main.js we've grabbed a reference to each button using a document.querySelector() call, for example:

let squareBtn = document.querySelector('.square');

We then attach an event listener to each button so that when pressed, the relevant module is dynamically loaded and used to draw the shape:

squareBtn.addEventListener('click', () => {
  import('/js-examples/modules/dynamic-module-imports/modules/square.js').then((Module) => {
    let square1 = new Module.Square(myCanvas.ctx, myCanvas.listId, 50, 50, 100, 'blue');
    square1.draw();
    square1.reportArea();
    square1.reportPerimeter();
  })
});

Note that, because the promise fulfillment returns a module object, the class is then made a subfeature of the object, hence we now need to access the constructor with Module. prepended to it, e.g. Module.Square( ... ).

参见

文档标签和贡献者

此页面的贡献者: RainSlide, StorytellerF, hotbaby
最后编辑者: RainSlide,