commonjs

概述

每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。在服务器端,模块的加载是运行时同步加载的;在浏览器端,模块需要提前编译打包处理。

由上可知,(在服务器端)模块的依赖关系是在运行时确定的。

特点

  • 所有代码都运行在模块作用域,不会污染全局作用域。
  • 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
  • 模块加载的顺序,按照其在代码中出现的顺序。

暴露

每个模块内部,module 变量代表当前模块。这个变量是一个对象,它的 exports 属性是对外的接口。加载某个模块,其实是加载该模块的 module.exports 属性

require 用于加载模块文件。基本功能是读入并执行一个 JavaScript 文件,然后返回该模块的 exports 对象。如果没有发现指定模块,会报错

加载机制

输入的是被输出的值的拷贝,一旦输出一个值,模块内部的变化就影响不到这个值。比如模块 A 输出了一个 number 和增加该 number 的方法,在模块 B 中引入它们,并调用该方法,发现 number 的值并没有增加。

es6

概述

ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。

两者的区别:

  1. CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。因此上边 number 的值会增加。
  2. CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。因为 CommonJS 加载的是一个对象(module.exports),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成
  3. require 是同步加载,import 是异步加载,更准确地说,ES6 模块有一个独立的静态解析阶段,依赖关系的分析在该阶段完成,最底层的模块第一个执行。require 不支持 ES6 的一个原因便是,它是同步加载,而 ES6 内部可以使用顶层 await 命令,导致无法被同步加载。

导入导出方式

  • 命名导出:导出的数据带 name,统一引入需要 { }。

  • 别名引入:import { cym as Danmo } from ‘…’。

  • 命名空间引入:import * as Danmo from ‘…’,比如原文件下定义了 learn 、play 等方法,我们指定命名空间叫 Danmo,引入后通过 Danmo.learn、Danmo.play 来调用它们,避免使用大量别名引入。

  • 默认导入导出,export default 并且引入无需 { },引入的是 import { default as Danmo} from '...' 的缩写。

    1
    2
    3
    4
    5
    // 这样导出的话,import Danmo from '...',方法的使用和命名空间引入一致
    export default {
    learn () {},
    play () {}
    }

more about ES6 module

Feature

You need to pay attention to local testing — if you try to load the HTML file locally (i.e. with a file:// URL), you’ll run into CORS errors due to JavaScript module security requirements. You need to do your testing through a server.

如果使用 <script type="module"> 将默认启用跨域访问,不能在本地测试,需要通过 server,否则 CORS 报错。

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.

模块自动开启严格模式。

There is no need to use the defer attribute (see `` attributes) when loading a module script; modules are deferred automatically.

模块自动开启 defer。

You will only be able to access imported features in the script they are imported into, and you won’t be able to access them from the JavaScript console.

引入的模块不能在控制台中访问。

Aggregation

把多个子模块的导出汇集到一个文件中,可以 export <sth.> form <path>,比如:

1
2
3
4
5
6
7
modules/
canvas.js
shapes.js
shapes/
circle.js
square.js
triangle.js

每个子模块的导出方式如:export { Square }

在 /modules 下新建 shapes.js:

1
2
3
export { Square } from './shapes/square.js';
export { Triangle } from './shapes/triangle.js';
export { Circle } from './shapes/circle.js';
1
2
// 从 shapes.js 导入:
import { Square, Circle, Triangle } from './modules/shapes.js';

Dynamic module

A recent addition to JavaScript modules functionality is dynamic module loading. This allows you to dynamically load modules only when they are needed, rather than having to load everything up front.

1
2
3
4
5
6
7
8
9
squareBtn.addEventListener('click', () => {
import('./modules/square.js').then((Module) => {
// 可以获取 Module Object,从而得到其导出成员
const square1 = new Module.Square(myCanvas.ctx, myCanvas.listId, 50, 50, 100, 'blue');
square1.draw();
square1.reportArea();
square1.reportPerimeter();
})
});

Top level await

1
2
3
4
5
6
// getColors.js
const colors = fetch('../data/colors.json')
.then((response) => response.json());

// top level await
export default await colors;

We’re using the keyword await before specifying the constant colors to export. This means any other modules which include this one will wait until colors has been downloaded and parsed before using it.

However it won’t block other modules being loaded. For instance our canvas.js continue to load while colors is being fetched.

其他模块如果引入了 getColors.js,需要等待 fetch 请求得到 colors。对于没有引入 getColors.js 的模块,不受影响。

参考

前端模块化详解

JavaScript modules —— MDN

Node.js 如何处理 ES6 模块:讲解了 CommonJS 和 ES6 模块的加载问题,推荐阅读。

ES6 import 后缀名问题:讲解了什么时候 import 需要拓展名,什么时候不需要。