ESM 基本语法
导出模块的功能
为了获得模块的功能要做的第一件事是把它们导出来。使用 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 ,和等会会看到的类。
export 要放在最外层;比如你不能够在函数内使用 export 。
一个更方便的方法导出所有你想要导出的模块的方法是在模块文件的末尾使用一个 export 语句,语句是用花括号括起来的用逗号分割的列表。比如:
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 可移植 -- 如果在站点层中你把它移动到不同的路径下面仍然能够工作。
那么看看例子吧:
/js/examples/modules/basic-modules/modules/square.js
变成了
./modules/square.js
你可以在 main.js 中看到这些。
备注: 在一些模块系统中你可以忽略文件扩展名(比如 '/model/squre')。这在原生 JavaScript 模块系统中不工作。
因为你导入了这些功能到你的脚本文件,你可以像定义在相同的文件中的一样去使用它。下面展示的是在 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);
默认导出 versus 命名导出
到目前为止我们导出的功能都是由 named exports 组成 —- 每个项目(无论是函数,常量等)在导出时都由其名称引用,并且该名称也用于在导入时引用它。
还有一种导出类型叫做 default export —- 这样可以很容易地使模块提供默认功能,并且还可以帮助 JavaScript 模块与现有的 CommonJS 和 AMD 模块系统进行互操作(正如 ES6 In Depth: Modules by Jason Orendorff 的模块中所解释的那样;搜索“默认导出”)。
看个例子来解释它如何工作。在我们的基本模块 square.js 中,您可以找到一个名为 randomSquare() 的函数,它创建一个具有随机颜色,大小和位置的正方形。我们想作为默认导出,所以在文件的底部我们这样写:
export default randomSquare;
注意,不要大括号。
我们可以把 export default 放到函数前面,定义它为一个匿名函数,像这样:
export default function(ctx) {
...
}
在我们的 main.js 文件中,我们使用以下行导入默认函数:
import randomSquare from './modules/square.js';
同样,没有大括号,因为每个模块只允许有一个默认导出,我们知道 randomSquare 就是需要的那个。上面的那一行相当于下面的缩写:
import {default as randomSquare} from './modules/square.js';
重命名导出与导入
在你的 import 和 export 语句的大括号中,可以使用 as 关键字跟一个新的名字,来改变你在顶级模块中将要使用的功能的标识名字。因此,例如,以下两者都会做同样的工作,尽管方式略有不同:
// 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';
让我们看一个真实的例子。在我们的 renaming 目录中,你将看到与上一个示例中相同的模块系统,除了我们添加了 circle.js 和 triangle.js 模块以绘制和报告圆和三角形。
在每个模块中,我们都有 export 相同名称的功能,因此每个模块底部都有相同的导出语句:
export { name, draw, reportArea, reportPerimeter };
将它们导入 main.js 时,如果我们尝试使用
import { name, draw, reportArea, reportPerimeter } from './modules/square.js';
import { name, draw, reportArea, reportPerimeter } from './modules/circle.js';
import { name, draw, reportArea, reportPerimeter } from './modules/triangle.js';
浏览器会抛出一个错误,例如“SyntaxError: redeclaration of import name”(Firefox)。
相反,我们需要重命名导入,使它们是唯一的:
import { name as squareName,
draw as drawSquare,
reportArea as reportSquareArea,
reportPerimeter as reportSquarePerimeter } from './modules/square.js';
import { name as circleName,
draw as drawCircle,
reportArea as reportCircleArea,
reportPerimeter as reportCirclePerimeter } from './modules/circle.js';
import { name as triangleName,
draw as drawTriangle,
reportArea as reportTriangleArea,
reportPerimeter as reportTrianglePerimeter } from './modules/triangle.js';
请注意,您可以在模块文件中解决问题,例如
// 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';
它也会起作用。你使用什么样的风格取决于你,但是单独保留模块代码并在导入中进行更改可能更有意义。当您从没有任何控制权的第三方模块导入时,这尤其有意义。
创建模块对象
上面的方法工作的挺好,但是有一点点混乱、亢长。一个更好的解决方是,导入每一个模块功能到一个模块功能对象上。可以使用以下语法形式:
import * as Module from '/modules/module.js';
这将获取 module.js 中所有可用的导出,并使它们可以作为对象模块的成员使用,从而有效地为其提供自己的命名空间。例如:
Module.function1();
Module.function2();
再次,让我们看一个真实的例子。如果你转到我们的 module-objects 目录,将再次看到相同的示例,但利用上述的新语法进行重写。在模块中,导出都是以下简单形式:
export { name, draw, reportArea, reportPerimeter };
另一方面,导入看起来像这样:
import * as Canvas from './modules/canvas.js';
import * as Square from '/./modules/square.js';
import * as Circle from './modules/circle.js';
import * as Triangle from './modules/triangle.js';
在每种情况下,您现在可以访问指定对象名称下面的模块导入。
let square1 = Square.draw(myCanvas.ctx, 50, 50, 100, 'blue');
Square.reportArea(square1.length, reportList);
Square.reportPerimeter(square1.length, reportList);
因此,您现在可以像以前一样编写代码(只要您在需要时包含对象名称),并且导入更加整洁。
模块与类(class)
正如我们之前提到的那样,您还可以导出和导入类;这是避免代码冲突的另一种选择,如果您已经以面向对象的方式编写了模块代码,那么它尤其有用。
你可以在我们的 classes 目录中看到使用 ES 类重写的形状绘制模块的示例。例如,square.js 文件现在包含单个类中的所有功能:
class Square {
constructor(ctx, listId, length, x, y, color) {
// …
}
draw() {
// …
}
// …
}
然后我们导出:
export { Square };
在 main.js 中,我们像这样导入它:
import { Square } from './modules/square.js';
然后使用该类绘制我们的方块:
let square1 = new Square(myCanvas.ctx, myCanvas.listId, 50, 50, 100, 'blue');
square1.draw();
square1.reportArea();
square1.reportPerimeter();
合并模块
有时你会想要将模块聚合在一起。您可能有多个级别的依赖项,您希望简化事物,将多个子模块组合到一个父模块中。这可以使用父模块中以下表单的导出语法:
export * from 'x.js'
export { name } from 'x.js'
备注: 这实际上是导入后跟导出的简写,即“我导入模块 x.js,然后重新导出部分或全部导出”。
有关示例,请参阅我们的 module-aggregation 。在这个例子中(基于我们之前的类示例),我们有一个名为 shapes.js 的额外模块,它将 circle.js,square.js 和 riangle.mjs 中的所有功能聚合在一起。我们还将子模块移动到名为 shapes 的 modules 目录中的子目录中。所以模块结构现在是这样的:
modules/
canvas.js
shapes.js
shapes/
circle.js
square.js
triangle.js
在每个子模块中,输出具有相同的形式,例如,
export { Square };
接下来是聚合部分。在 shapes.js 里面,我们包括以下几行:
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';
它们从各个子模块中获取导出,并有效地从 shapes.js 模块中获取它们。
备注: 即使 shapes.js 文件位于 modules 目录中,我们仍然需要相对于模块根目录编写这些 URL ,因此需要 /modules/ 。这是使用 JavaScript 模块时混淆的常见原因。
备注: shapes.js 中引用的导出基本上通过文件重定向,并且实际上并不存在,因此您将无法在同一文件中编写任何有用的相关代码。
所以现在在 main.js 文件中,我们可以通过替换来访问所有三个模块类
import { Square } from './modules/square.js';
import { Circle } from './modules/circle.js';
import { Triangle } from './modules/triangle.js';
使用以下单行:
import { Square, Circle, Triangle } from './modules/shapes.js';
动态加载模块
浏览器中可用的 JavaScript 模块功能的最新部分是动态模块加载。这允许您仅在需要时动态加载模块,而不必预先加载所有模块。这有一些明显的性能优势;让我们继续阅读,看看它是如何工作的。
这个新功能允许您将 import() 作为函数调用,将其作为参数传递给模块的路径。它返回一个 promise ,它用一个模块对象来实现(参见创建模块对象),让你可以访问该对象的导出,例如
import('/modules/mymodule.js')
.then((module) => {
// Do something with the module.
});
我们来看一个例子。在 dynamic-module-imports 目录中,我们有另一个基于类示例的示例。但是这次我们在示例加载时没有在画布上绘制任何东西。相反,我们包括三个按钮 -- “圆形”,“方形”和“三角形” -- 按下时,动态加载所需的模块,然后使用它来绘制相关的形状。
在这个例子中,我们只对 index.html 和 main.js 文件进行了更改 -- 模块导出保持与以前相同。
在 main.js 中,我们使用 document.querySelector() 调用获取了对每个按钮的引用,例如:
let squareBtn = document.querySelector('.square');
然后,我们为每个按钮附加一个事件监听器,以便在按下时,相关模块被动态加载并用于绘制形状:
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();
})
});
请注意,由于 promise 履行会返回一个模块对象,因此该类成为对象的子特征,因此我们现在需要使用 Module 访问构造函数。在它之前,例如 Module.Square( ... )。