第二章 模块打包
带着问题:
- 不同模块标准(CommonJs、ESModules、AMD、CMD)
- 模块打包原理
自己的思考:
理解不同模块标准产生的背景,使用方式
CommonJS
背景
2009年社区提出的标准,Node.js实现采用了其部分,并且在其基础上进行了调整,有区别,但是大家淡化了这个区别。
模块
规定每个文件都是一个模块。与<script>引入的区别是:不会污染全局环境,所有变量仅自己可用,对外不可见。
例子:
calculator-commonjs.js
var name = 'calculator.js'
index.js
var name = 'index.js'
const calculatorCommonJs = require('./calculator-commonjs')
console.log(name)
使用node index.js执行,
输出
index.js
导出
使用module.exports导出模块, 默认值{}, 注 (切不可直接赋值module,js的语言特性允许你这么操作)
导出语句不代表模块的末尾,为了提高可读性,应该将module.exports或exports放在模块的结尾。
导入
使用require导入模块,a. 重复的导入CommonJs模块只会执行一次,从第二次开始使用缓存。b. 支持动态导入,不一定在文件顶级,可以存在于任何位置。
导入、导出 例子:
calculator-commonjs.js
var name = 'calculator-commonjs.js'
function sayMyname() {
console.log('my name is', name)
}
module.exports = {
name,
sayMyname,
}
module.exports.name = name;
exports.name = name;
index.js
var name = 'index.js'
const calculatorCommonJs = require('./calculator-commonjs')
calculatorCommonJs.sayMyname()
ES Module
背景
2015.06正式发布ES6, js语言真正有模块这一标准。
模块
同上。ES6默认采用严格模式,不管有没有use strict字段。
ESM规则改写,例子:
var name = 'calculator.js'
function sayMyname() {
console.log('my name is', name)
}
export default {
name,
sayMyname,
}
导出
a. 默认导出export default
b. 命名导出export
导入
import ... from ...
导入、导出
例子:
calculator-esmodule.js
var name = 'calculator.js'
function sayMyname() {
console.log('my name is', name)
}
export default {
name,
sayMyname,
}
index.js
var name = 'index.js'
import calculatorEsmodule from "./calculator-esmodule.js"
calculatorEsmodule.sayMyname()
复合写法
命名写法才有,用于项目中的入口文件等。
CommonJs VS. ES Module
1. 动态与静态 => 决定是否有明确的依赖关系
-
nJs模块的导入函数
require(), 路径可以动态指定,可以是一个表达式,甚至可以在一个if语句中判断是否导入模块。所以,在代码执行前,我们没有办法确定明确的依赖关系。也称之为动态。 -
Module模块的导入导出必须是声明式的,路径不支持将表达式,并且导入导出必须存在于顶级作用域。所以,不需要执行代码,模块引用关系就非常明确。也称之为静态。参见
import
代码
index.js中CommonJs
if (true) {
// 动态路径
const commonjsModulePath = './calculator-commonjs.js'
const calculatorCommonJs = require(commonjsModulePath)
calculatorCommonJs.sayMyname()
}
这段代码可以直接用node index.js执行,不会有报错。
但是,在webpack之中会报错。
参见官方文档解决 require.context()
index.js
if (true) {
// 动态路径
// const commonjsModulePath = './calculator-commonjs.js'
const contextObj = require.context('./', false, /calculator-commonjs.js/)
const calculatorCommonJs = contextObj('./calculator-commonjs.js')
calculatorCommonJs.sayMyname()
}
注 正则匹配返回的是内容对象,从build后的main.js可以看出来。
index.js中,引入ES Modules的方式改一下。
if (true) {
import calculatorEsmodule from './calculator-esmodule.js'
console.log(calculatorEsmodule)
calculatorEsmodule.sayMyname()
}
报错信息:
以上,CommonJs动态、ES Modules静态之区别。
2. 值复制与动态映射 (值的拷贝 值的引用)
cjs是导出值的副本,esm是值的动态映射(前提是单独导出模块,才会有以上的区别;esm的动态映射和静态导入让它可以做tree-shaking)
3. 循环依赖
3.1 cjs
代码
入口文件 index.cjs.03.js
require('./03/a-cjs')
模块 a.cjs.js
const b = require('./b-cjs')
console.log('value of b', b)
module.exports = 'this is a-cjs.js'
模块 b.cjs.js
const a = require('./a-cjs')
console.log('value of a', a)
module.exports = 'this is b-cjs.js'
输出
分析
index.js执行,a.js被记录到缓存模块中;
a.js执行,b.js被记录到缓存模块cachedModule中,但是此时a模块没有跑完,module.exports = {};
b.js执行,从cachedModule中找到了a模块对应的key,就返回了,只不过是一个空对象{}而已;
再继续a.js中未完成的部分,输出b模块。(结果如上图)
3.2 esm
代码
入口文件 index.esm.03.js
import a from './03/a.esm.js'
模块 a.esm.js
import b from './b.esm.js'
console.log('value of b', b)
export default 'this is a-cjs.js'
模块 b.esm.js
import a from './a.esm.js'
console.log('value of a', a)
export default 'this is b-cjs.js'
输出
说明
这本书上没有报错,但是在webpack5下会报错。没有深究。
解决
问题在于模块之间循环引用了,解决即可。 webpack生态提供出的插件 circular-dependency-plugin。
效果如下图