《Webpack实战 入门、进阶与调优(第2版)》- 第二章 模块打包

65 阅读3分钟

第二章 模块打包

带着问题:

  • 不同模块标准(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.exportsexports放在模块的结尾。

导入

使用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. 动态与静态 => 决定是否有明确的依赖关系

  1. nJs模块的导入函数require(), 路径可以动态指定,可以是一个表达式,甚至可以在一个if语句中判断是否导入模块。所以,在代码执行前,我们没有办法确定明确的依赖关系。也称之为动态。

  2. Module模块的导入导出必须是声明式的,路径不支持将表达式,并且导入导出必须存在于顶级作用域。所以,不需要执行代码,模块引用关系就非常明确。也称之为静态。参见import


代码

index.js中CommonJs

if (true) {
  // 动态路径
  const commonjsModulePath = './calculator-commonjs.js'
  
  const calculatorCommonJs = require(commonjsModulePath)
  
  calculatorCommonJs.sayMyname()
}

这段代码可以直接用node index.js执行,不会有报错。 image-6.png

但是,在webpack之中会报错。 image-7.png

参见官方文档解决 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()
}

报错信息: image-5.png

以上,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'

输出 image-8.png

分析 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'

输出 image-9.png

说明

这本书上没有报错,但是在webpack5下会报错。没有深究。 image-10.png

解决

问题在于模块之间循环引用了,解决即可。 webpack生态提供出的插件 circular-dependency-plugin

效果如下图 image-11.png