AMD CMD
AMD、CMD 相对命比较短,到 2014 年基本上就摇摇欲坠了。
CommonJs
require/exports 相关的规范由于野生性质,在 2010 年前后出生。
CommonJS 作为 Node.js 的规范,一直沿用至今。由于 npm 上 CommonJS 的类库众多,以及 CommonJS 和 ES6 之间的差异,Node.js 无法直接兼容 ES6。所以现阶段 require/exports 任然是必要且实必须的。
require/exports 的用法只有以下三种简单的写法:
const fs = require('fs')
exports.fs = fs
module.exports = fs
- 说白了,导出一个对象,导入一个对象,完成。
- exports是module.exports的引用,修改exports就相当于修改module.exports。
- 但是注意,直接exports = xx , 这种只是修改了引用,并没有修改导出对象,如果直接赋值导出对象的话,需要module.exports = fs
- 这里导出是 exports , es6是export,注意区别。
ES6
出自 ES6 的 import/export 相对就晚了许多。被大家所熟知和使用也是 2015 年之后的事了。 这其实要感谢 babel(原来项目名叫做 6to5,后更名为 babel) 这个神一般的项目。由于有了 babel 将还未被宿主环境(各浏览器、Node.js)直接支持的 ES6 Module 编译为 ES5 的 CommonJS —— 也就是 require/exports 这种写法 —— Webpack 插上 babel-loader 这个翅膀才开始高飞,大家也才可以称 " 我在使用 ES6! "
import/export 的写法就多种多样:
import fs from 'fs'
import {default as fs} from 'fs'
import * as fs from 'fs'
import {readFile} from 'fs'
import {readFile as read} from 'fs'
import fs, {readFile} from 'fs'
export default fs
export const fs
export function readFile
export {readFile, read}
export * from 'fs'
- default 是 ES6 Module 所独有的关键字,export default fs 输出默认的接口对象,import fs from 'fs' 可直接导入这个对象;
- ES6 Module 中导入模块的属性或者方法是强绑定的,包括基础类型;而 CommonJS 则是普通的值传递或者引用传递。
看个例子:
// counter.js
exports.count = 0
setTimeout(function () {
console.log('increase count to', ++exports.count, 'in counter.js after 500ms')
}, 500)
// commonjs.js
const {count} = require('./counter')
setTimeout(function () {
console.log('read count after 1000ms in commonjs is', count)
}, 1000)
//es6.js
import {count} from './counter'
setTimeout(function () {
console.log('read count after 1000ms in es6 is', count)
}, 1000)
运行结果:
➜ test node commonjs.js
increase count to 1 in counter.js after 500ms
read count after 1000ms in commonjs is 0
➜ test babel-node es6.js
increase count to 1 in counter.js after 500ms
read count after 1000ms in es6 is 1
可以看出,ES6的引入可以读取到最新的值的变化,是强绑定,而CommonJS 是传递的引用(基础类型是值传递)。
package.json中的type
- type字段的产生用于定义package.json文件和该文件所在目录根目录中.js文件和无拓展名文件的处理方式。
- 值为'module'则当作es模块处理;
- 值为'commonjs'则被当作commonJs模块处理
- 目前node默认的是如果pacakage.json没有定义type字段,则按照commonJs规范处理
- node官方建议包的开发者明确指定package.json中type字段的值
- 无论package.json中的type字段为何值,.mjs的文件都按照es模块来处理,.cjs的文件都按照commonJs模块来处理
错误处理
- 如果package.json中没有设置type,那么默认是按CommonJs处理的。当代码中出现import的话就会报错, 解决办法是引入babel转义成CommonJs的语法。
import { createRequire } from 'module';
^^^^^^
SyntaxError: Cannot use import statement outside a module
<node_internals>/internal/modules/cjs/loader.js:1054
Process exited with code 1
- 反之亦然,设置type=module,那么无法使用require,会得到如下报错
Uncaught ReferenceError: require is not defined
src/modulet/CommonJsimport.js:4
Process exited with code 1
如果一定要使用,那么可以引入这个函数。
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
使用办法
之前说了那么多,那么实际使用的过程中,可能会出现混乱的情况,下面记录一下。
老式的CommonJS(CJS)和新型的ESM(又名MJS)。
- CJS使用require()、module.exports;
- ESM使用import、export。
情况一、 cjs引入cjs
正常引用就行
情况二、 esm引入esm
正常引用就行
情况三、esm引入cjs
- ESM可以使用 import CJS脚本,但是只能使用“默认导入”语法:import _ from 'lodash',而不是“命名导入”语法:import {shuffle} from 'lodash',如果CJS使用命名导出,则很麻烦。命名导出:module.exports.sum = (x, y) => x + y; 默认导出: module.exports = 'baz'
- 或者像之前说的,把require找回来。
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const {foo} = require('./foo.cjs');
这个方法的问题在于它没能帮多大忙。实际上也就比做一个默认导入然后解构多了几行代码。
import cjsModule from './foo.cjs';
const {foo} = cjsModule;
另外,像 Webpack 和 Rollup 这样的打包工具并不知道如何处理 createRequire 这样的模式,所以意义何在呢?
情况四、cjs引入esm
- CJS 无法 require() ESM,因为有顶层的 await 限制
- 目前为止,如果你在写 CJS,你想 import 一段 ESM 代码,你得使用异步动态的 import()。
(async () => {
const {foo} = await import('./foo.mjs');
})();
指导原则
- 为你的库包提供CJS版本
- 为您的CJS提供一个薄的ESM包装器
import cjsModule from '../index.js';
export const foo = cjsModule.foo;
- 将exports映射添加到您的package.json
"exports": {
"require": "./index.js",
"import": "./esm/wrapper.js"
}
ESM和CJS是完全不同的
- 在CommonJS中,require()是同步的;它不返回承诺或调用回调。require()从磁盘(或什至从网络)读取,然后立即运行脚本,脚本本身可能会产生I / O或其他副作用,然后返回在module.exports上设置的任何值。
- 在ESM中,模块加载器以异步阶段运行。在第一阶段,它解析脚本以检测对import和export的调用,无需运行导入的脚本。在解析阶段,ESM加载程序可以立即检测到命名导入中的错字并引发异常,而无需实际运行依赖项代码。
- 然后,ESM模块加载器异步下载并解析您导入的所有脚本,然后异步下载您导入的脚本,从而构建依赖关系的“模块图”,直到最终找到一个不导入任何内容的脚本。最后,允许该脚本执行,然后允许运行依赖该脚本的脚本,依此类推。
- ES模块图中的所有“兄弟”脚本都是并行下载的,但是它们会按顺序执行,并由加载程序规范保证。
- ESM更改了JavaScript中的许多内容。ESM脚本默认使用严格模式(use strict),它们this不引用全局对象,作用域的工作方式不同,等等。
- 这就是为什么即使在浏览器中<script>标签也默认为非ESM的原因;您必须添加一个type="module"属性以选择进入ESM模式。
TS中的import
JavaScript 中有多种 export 的方式,而 TypeScript 中针对这种情况做了多种 import 语法。
// commonjs 模块
import * as xx from 'xx'
// es6 模块
import xx from 'xx'
// commonjs 模块,类型声明为 export = xx
import xx = require('xx')
// 没有类型声明,默认导入 any 类型
const xx = require('xx')
- import * as xx from 'xx' 的语法来一般都是用来导入使用 module.exports 导出的模块。
import * as path from 'path'
import xx from 'xx' 默认情况下,import xx from 'xx' 的语法只适用于 ECMAScript 6 的 export default 导出
import xx = require('xx') import xx = require('xx') 是用来导入 commonjs 模块的库,特殊的地方在于这个库的类型声明是 export = xx 这种方式导出的
const xx = require('xx') 当一个模块没有类型声明文件的时候,可以使用 commonjs 原始的 require() 方式来导入模块,这样会默认该模块为 any。