前言
webpack为了解决前端的模块化开发,有了 webpack,我们可以不用再担心浏览器的兼容性问题,直接就能编写基于 CommonJS 或是 ES6 module 的代码,这些代码经过 webpack 构建后,就可以直接在浏览器中运行了。
默认情况下,无论对于 CommonJS(require) 还是 ES6 module(import),webpack 都是采用 CommonJS 方式来实现模块的同步加载。
Webpack如何实现CommonJS的加载
先写个工具函数JS
// math.js
module.exports.minus = function(a, b) {
return a - b;
};
exports.add = function(a, b) {
return a + b;
};
然后定义一个入口模块:index.js,index.js 通过 require 导入了 math.js,然后调用了里面的 add 和 minus 方法。
const math = require('./math');
console.log(math.add(2, 1));
console.log(math.minus(2, 1));
webpack配置将模式设为 development 关闭代码压缩,开启 source-map 支持源代码调试,生成产物最终放在 dist 目录下,名字为 main.js。
const path = require("path");
module.exports = {
mode: "development",
devtool: "source-map",
entry: path.join(__dirname, 'index.js'),
output: {
filename: "main.js",
path: path.join(__dirname, 'dist')
}
};
在根目录下执行 webpack --config webpack.config.js,就可以在 dist 目录下看到最终的生成产物 main.js 了。
上图显示的就是一个简化版的 main.js。从main.js 代码片段可以看出,整个文件其实只含一个立即执行函数(IIFE),通常称之为 webpackBootstrap。它接受一个模块对象,key键是模块路径,value是模块内容。
webpackBootstrap函数:
- 定义了一个模块缓存对象 installedModules,用来缓存已经加载过的模块
- 定义了一个模块加载函数 __webpack_require__,用来同步加载模块
- 使用 __webpack_require__ 同步加载入口模块 index.js
看看模块对象里的内容:
- 依赖模块 math 函数:math 模块函数中的内容和 math.js 文件中的内容是一样的,没有做任何转换,module 和exports 都是通过模块函数参数传入的
- 入口模块 index 函数:在 index.js 文件中,我们使用 require 来加载 math.js,而在 index 模块函数中,require 被转换成了 __webpack_require__,module、exports 和 __webpack_require__ 也都是通过模块函数参数传入的
在 webpackBootstrap 函数内部,还是入口模块 index 函数内部,都用到了__webpack_require__ 模块加载函数。
从上图可以看到,__webpack_require__ 的执行流程如下:
- 通过 InstallledModuled 对象判断模块是否已缓存,如果模块已缓存的话,就直接返回缓存模块的 exports 属性。
- 如果模块没有缓存的话,就会初始化一个新的模块对象,然后将它加入缓存。模块对象里面有三个字段:i 表示模块 id;l 表示模块加载状态,false 表示未加载;exports 表示模块的导出对象。
- 执行模块函数,执行模块函数时会将 module,module.exports,以及 __webpack_require 函数作为参数传入。这里有两个值得注意的地方:一个就是这里同时传入了 module 和 module.exports ,module.exports 对应模块函数参数中的 exports,这也印证了我们之前说的在 CommonJS 中,exports 是 module.exports 的引用;第二个就是这里还通过 call 的方式做了一个动态绑定,将模块函数的调用对象,绑定为 module.exports,这主要是为了保证在模块函数中使用 this 时,this 能指向当前模块,而不是指向全局对象。
- 执行完模块函数,就会将模块标记为已加载,最后返回 module.exports。
从 webpack_require 加载函数的执行逻辑,我们可以看到,相同的模块只有在第一次执行时才会执行模块函数,其他情况下都只会返回缓存的 exports 对象。
Webpack如何实现ES6 module的加载
写法变更:
// math.js
export function add(a, b) {
return a + b;
}
export function minus(a, b) {
return a - b;
}
// index.js
import { add, minus } from "./math";
console.log(add(2, 1));
console.log(minus(2, 1));
webpack 配置文件不用做任何修改,最后执行 webpack --config webpack.config.js,看看新生成的 main.js。
区别主要有两点: webpackBootstrap 中新增了一些函数定义;模块函数改动了一些内容。
webpackBootstrap的不同之处:
它主要新增了三个函数:
__webpack_require__.o:Object.hasOwnProperty 的包裹函数。
__webpack_require__.d:为传入的 exports 对象定义一个新的属性,这个新的属性是可枚举的,并且使用 getter 方法来获取属性值。
__webpack_require__.r:为传入的 exports 对象定义一个 __esModule 属性,值为true,标识这是一个 ES6 module。
最后是入口模块 index,它主要的变化包括:
- 入参名称由 exports 改成了 __webpack_exports__
- 自动加上了 ”use strict”,使用严格模式
- 调用 __webpack_require__.r,标识这是一个 ES6 module
- 将 import 转换成了 __webpack_require__,__webpack_require__ 会返回一个 exports对象,然后执行挂载在 exports 对象上的方法
从上面的分析我们可以看出,ES6 module 在底层实际上被 webpack 转换成了类似于 CommonJS 的语法。
同步的博文内容改摘于webpack模块化原理-同步加载模块
webpack实现异步加载
使用没有同步那么广泛,这里只写使用方法。
// math.js
export function add(a, b) {
return a + b;
}
export function minus(a, b) {
return a - b;
}
// index.js
import("./math").then(math => {
console.log(math.add(2, 1));
console.log(math.minus(2, 1));
});
webpack.config.js,内容和 webpack 同步加载模块中一样。
请注意,异步加载的模块内如果有异步加载的模块或发生了循环依赖的模块,则无法保证异步的返回值是正确的。
举例:
// 项目深处某个util.js
import DEFAULTOPT_AREA from './defaultChartOptions/areaConfig';
import('@/form/controls/enum').then(ControlTypeEnum => {
const a = {
[ControlTypeEnum.Area]: DEFAULTOPT_AREA(),
};
console.log(a);
});
// enum.js
// 大量的入口文件引入,形成了依赖循环
import AreaControl from './Charts/Area';
import MapControl from './Charts/AreaMap';
....
export const ControlTypeEnum = {
/** @name 热力地图 */
HeatMap: HeatMapControl.type,
....
}
返参的key键是undefined,而value却是正常的(因为在项目中value只在util里import,其他地方没有import,不存在循环依赖,而enum这个文件在项目浅层就多次被引用,极有可能已经形成了循环依赖)
什么是循环依赖,循环依赖的具体原因请往下看。
webpack加载js/ts的顺序(同步加载)
根目录
- src
- util
- a.js
- b.js
- c.js
- d.js
- index.js
- index.html
- webpack.config.js
- package.json
- util
index.js 项目入口文件
// index.js
import * as a from './a'
// a.js
import * as b from './b'
import * as d from './d'
console.log('file: a.js')
// b.js
import * as c from './c'
console.log('file: b.js')
// c.js
console.log('file: c.js')
// d.js
console.log('file: d.js')
index.html
...
<head>
...
<script src="./dist/index.js"></script>
</head>
...
webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'index.js',
path: path.resolve(__dirname, 'dist')
}
}
打包编译后
该项目具体执行顺序
- 首先读取index.js,发现有import a.js
- 进入a.js ,发现有import ,导入第一个文件 b.js
- 进入b.js,发现有import,进入 c.js
- 在c.js里没有import,则执行c.js里面的代码,此时打印 console.log('file: c.js')
- 执行完c.js后,回退到上个js,即b.js
- 执行b.js代码,此时打印 console.log('file: b.js')
- 执行完b.js,回退到上个js,即a.js
- 在a.js,导入第二个文件 d.js
- 进入d.js,没有导入的js,则执行当前js代码,此时打印 console.log('file: d.js')
- 执行完d.js,回退到a.js,继续执行a.js代码,此时打印 console.log('file: a.js')
- 执行完a.js,回退到index.js,结束!
webpack会从 webpack.config.js 配置entry的入口js文件开始读起,从上到下按顺序执行。webpack读取js会先看有没有import 。
如果有import,则按import的顺序依次读取导入的js。
如果没有import,则继续执行当前js代码。
执行完当前js代码,会回退到上个js继续执行,直到回退到入口文件index.js
如果已经import过的js,则不再重复导入
举例:
// index.js
import * as a1 from './a1';
import * as a2 from './a2';
console.log('index')
// a1.js
import * as a2 from './a2';
console.log('a1')
// a2.js
console.log('a2')
结果:
a1
a2(只有一次,是在a1里面引用的,index后引入的a2不再)
index
总得来说,同步加载流程如下:
循环依赖
简单来说循环依赖 指的是 A 模块依赖于 B,B 模块 又依赖于 A。
详细点来说便是在有2个或2个以上的文件之间的相互依赖关系构成闭环的时候,有时会出现Can't read Property 'xxx' of undefined或者(0,xxx) is not a function这类的错误,比如:
比如src/index.js引用src/a.js,而src/a.js中也引用了src/index.js
来看复杂一点的例子:在实际上的业务场景里存在这样的调用关系
可以看见枚举Enum.js构成了闭环的引用结构
那么:
// enum.js
// 大量的入口文件引入,形成了依赖循环
import AreaControl from './Charts/Area';
import MapControl from './Charts/AreaMap';
....
export const ControlTypeEnum = {
/** @name 热力地图 */
HeatMap: HeatMapControl.type,
....
}
// 图表组件内的util.js
import DEFAULTOPT_AREA from './defaultChartOptions/areaConfig';
import { ControlTypeEnum } from '@/form/controls/enum';
export const a = {
[ControlTypeEnum.Area]: DEFAULTOPT_AREA()
};
// page.vue
import {a} from'xx/xx/util'
console.log(a)
报错了,注意,报错并非在页面文件里,而是在util.js里面。因为enum造成了依赖循环,所以a对象的key键是undefined,value因为只在组件util里引用所以能正常取值。
那循环依赖的原因是什么?
当模块还处于第一次执行中的状态时,如果碰到依赖循环的情况的话,webpack可能会认为一个没有完全加载完成的模块已经加载完了。
这么说可能很复杂,我们换个简单例子,当A引用B,B引用A,index引用A的时候,会发生什么?
- index入口文件import了A文件
- 在webpack中installModules不存在A,所以执行__webpack_require__方法
- 把A存入installModules后,执行call方法(可以理解为A绑定exports的方法)过程中,发现A引用了B,那么继续去import B文件。注意此时A已加入了instalModules中,所以执行模块函数已经有了A的路径key而没有value(‘./src/index/js’: undefined)
- 往B里面走,发现Bimport了A,且调用了A(会在引用时立即执行)的。此时webpack往installModules里去找,发现A的key已经存在,随即去调用A,但是A的value是undefiend(因为还在等待B的import结束),所以报错。
如何解决循环依赖问题
在实际项目过程中,由于项目引用复杂,不易缕清。循环依赖的问题屡见不鲜。既然我们不能避免循环依赖的问题,则需要避免因为循环依赖而出现调用失败的问题。
解决思路:把引用里调用转换成运行时调用,不在import文件里直接调用,而转为函数形式供给外部调用。
上面的例子改写:
import DEFAULTOPT_AREA from './defaultChartOptions/areaConfig';
import { ControlTypeEnum } from '@/form/controls/enum';
const getControlsDefaultOpt = () => ({
[ControlTypeEnum.Area]: DEFAULTOPT_AREA(),
});
// page.vue
import {getControlsDefaultOpt} form './util.js'
....
created() {
this.chartOpt = getControlsDefaultOpt()[this.field.type]
}
这样子就可以实现啦