前提摘要
在学习webpack打包的过程中,必然会面临模块的概念。这里就深入模块,帮助我们了解Webpack如何对模块进行打包合并
使用方法比较
导出
CommonJs
正确使用:
-
module.exports = {}
-
exports.add = function XX() {}
错误使用:
exports = {}
CommonJS模块内部会有一个module对象,用于存放当前模块的信息。CommonJS的每个模块头部都默认添加了以下代码:
var module = {
exports: {}
}
var exports = module.exports;
-
exports.add = function XXX() {}相当于在module.exports对象上添加了属性。 -
不可以直接给exports赋值
exports = {}会使exports指向一个新的对象 -
混用时常常使用不恰当的地方
exports.add = function() { console.info("add") };
module.exports = { name: "han meimei" }
如上的方法,moudle.exports的赋值,会导致第一次的exports.add方法被覆盖
- 导出不一定在模块末尾,导出后面的代码依旧会执行
module.export = { name: "li lei" }
console.info("你能看到我吗?"); // 这里是可以正常执行打印的
- module对象中有一个loaded属性用来记录该模块是否被加载过。默认是false,表示没有加载过
ES6 Module
- 命名导出
// 写法1
export const name = "calculator";
// 写法2
const name = "calculator";
export { name };
- 默认导出(一个模块只能有一个)
export default {
name: "calculator"
}
我们可以将export default理解为对外输出了一个名为defalut的变量,因此不需要进行变量声明
export default "string"
export default class {}
export default function() {}
导入
CommonJs
- 使用require方法进行模块导入
- require的模块是第一次被加载,这时会首先执行该模块,然后导出内容
- require的模块被加载过。这时该模块的代码不会再次执行,而是直接导出上次执行后得到的结果
// calculator.js
console.info("running calculator.js"); // 只会执行一次
module.exports = {
name: "calculator",
add: function(a, b) { return a + b }
}
// index.js
const add = require("./calculator.js").add;
const sum = add(2,3);
console.info("sum:", sum);
const moduleName = require("./calculator.js").name;
console.info("end");
// 输出结果
running calculator.js
sum: 5
end
- 有时不需要获取导出的内容,只想要执行导入的文件。直接使用require就行
require("./task.js");
- require接受模块动态加载
const moduleName = ["foo.js", "bar.js"];
moduleName.forEach(name => {
require('./' + name);
})
ES6 Module
- import语法导入
// calculator.js
const name = "calculator";
export { name };
// index.js
import { name } from "./calculator.js";
导入变量的效果相当于在当前作用域下声明了这些变量(name),并且不可以更改。就是所有导入的变量是只读的。
- 使用import * as <myModule>可以把所有导入的变量作为属性值添加到<myModule>对象中
import * as calculator from "./calculator.js";
cosnole.info(calculator.name);
- 复合写法
export { name } from "./calculator.js";
只支持通过命名导出的方法暴露出来的变量
区别
动态与静态
- 动态:模块依赖关系是建立发生在代码运行阶段
- 静态:模块依赖关系是建立在代码编译阶段
| CommonJS | ES6 Module |
|---|---|
| 动态(运行阶段) | 静态(编译阶段) |
| 函数式(require) | 声明式(import * from "") |
| 任意位置 | 顶层作用域(文件顶层) |
值拷贝与动态映射
| CommonJS | ES6 Module |
|---|---|
| 值拷贝 | 动态映射 |
| 引入的值可修改 | 引入的值不可修改 |
CommonJS
// calculator.js
var count = 0;
module.exports = {
count: count,
add: function (a, b) {
count++;
return a + b;
}
}
// index.js
const count = require("calculator.js").count; // 这里的count是index文件内部自己的count
const add = require("calculator.js").add;
console.info(count);
add(1, 4);
console.info(count); // 0:calcultor.js中变量的改变不会对这里的“拷贝值”有影响,所以还是0
count++;
console.info(count);
// 输出结果
0
0
1
ES6 Module
// calculator.js
var count = 0;
export {
count: count,
add: function(a, b) {
count++;
return a + b;
}
}
// index.js
import { count, add } from "calculator.js"; // 这里的count是映射值,理解为calculator.js中count的地址引用
console.info(count);
add(2, 3);
console.info(count); // 执行add方法后,calculator.js中的count变成了1,这里是映射值,也会是1
// 输出结果
0
1
映射关系就像一面镜子,从镜子里我们可以实时观察到原有的事物
循环依赖
循环依赖就是指模块A依赖于模块B,同时模块B依赖于模块A 先看下两种模式的结果
- CommonJS
// foo.js
const bar = require("./bar.js");
console.info("val of bar", bar);
module.exports = "this is foo.js";
// bar.js
const foo = require("./foo.js");
console.info("val of foo", foo);
module.exports = "this is bar.js";
// index.js
require("./foo.js");
// 打印结果
val of foo {}
val of bar this is bar.js
执行顺序分析:
- 执行index.js,引入foo.js文件,开始执行foo.js文件(执行权交给foo.js)
- 在foo.js文件中,引入bar.js文件,开始执行bar.js文件(执行权交给bar.js)
- 在bar.js文件中,引入foo.js文件,这里产生了依赖,但是这里的
执行权不会交给foo.js,这里foo.js文件还没有执行完成(或者说没有初始化完成),这里bar.js引进的是foo.js的默认导出,也就是(module.exports = {}),这里foo的值也就是{};所以打印的是 val of foo {} - bar.js文件执行完毕,并且导出module.exports = "this is bar.js";执行权重新回到foo.js
- foo.js此时拿到的bar.js文件导出,打印 val of bar this is bar.js
我们再从Webpack实现角度来看,从上面例子打包后,bundle中这样一段代码非常重要:
// The require function
function __webpack_require__(moduleId) {
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// Create a new module (and put it into the cache)
var module = installedModules[moduleId] = {
i: moduleId, // id
l: false, // loaded 是否引入过
exports: {}, // 默认export
}
}
当index.js引入了foo.js后,相当于执行了__webpack_require__函数,初始化了一个module对象并放入installedModules中。当bar.js再次引用foo.js时,又执行了__webpack_require__函数。但这次是直接从installedModules中取值,此时它的module.exports是{}。可以帮助理解和解释上面第三步出现的现象
- ES6 Module
// foo.js
import bar from "bar.js";
console.info("val of bar", bar);
export default "this is foo.js";
// bar.js
import foo from "foo.js";
console.info("val of foo", foo);
export default "this is bar";
// index.js
import foo from "./foo.js"
// 输出顺序
val of foo undefined
val of bar this is bar
这里和CommonJS一样,没有输出正确的值。只不过和CommonJS默认导出{}不同,这里获取到的是undefined
但是在前面我们谈论过,CommonJS是值拷贝,ES6 Module的值是动态映射关系。我们就可以利用映射关系支持循环依赖
// foo.js
import bar from "./bar.js";
function foo(invoker) {
console.info(invoker + "invokes foo.js");
bar("foo.js");
}
export default foo;
// bar.js
import foo from "foo.js";
function bar(invoker){
console.info(invoker + "invokes bar.js");
foo("bar.js");
}
// index.js
import foo from "foo.js";
foo("index.js");
// 输出顺序
index.js invokes foo.js
foo.js invokes bar.js
bar.js invokes foo.js
这样的结果看着都赏心悦目
- 首先index.js文件开始引入foo.js文件,进入foo.js文件执行(执行权交给foo.js)
- foo.js文件引入bar.js文件(执行权交给bar.js)
- bar.js文件引入了foo.js文件,但是执行权不会交给foo.js,一直执行到结束,完成了bar函数的定义和导出,注意这里头部引入的foo.js文件还没有执行完成,
foo是undefined。 - 执行权回到foo.js,继续执行到结束,完成foo函数定义。那么此时因为ES6 Module的动态映射特性,foo已经不再是undefined,
而是foo函数了。 - foo.js执行完成后,执行权回到index.js文件,调用foo函数,此时依次执行foo -> bar -> foo
其他
作用域
| scripts | CommonJS | ES6 Module |
|---|---|---|
| 全局作用域,容易污染全局环境 | 模块本身的作用域(所有变量和函数只能自身访问) | 模块自身作用域 |