js模块

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
/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不引用全局对象,作用域的工作方式不同,等等。
  • 这就是为什么即使在浏览器中

你可能感兴趣的:(js模块)