Webpack 核心原理
CommonJS 与 AMD¶
CommonJS¶
- js 原生没有模块化规范
- nodejs 推出了由
require
和module.exports
实现的模块化规范,即 CommonJS。
AMD¶
- Web 环境为满足模块化需求,受 CommonJS 启发,推出 AMD,Asynchronous Module Definition
- RequireJS 提供
define
定义模块,require
引入依赖的模块 - RequireJS 利用 JsonP 来对模块文件进行异步加载
UMD¶
- 可运行与 nodejs 环境和 Web 环境
- 没有自己专有的规范,是集结了 CommonJS、AMD 的规范于一身
- UMD 的实现如下:
((root, factory) => {
if (typeof define === 'function' && define.amd) {
// AMD
define(['jquery'], factory);
} else if (typeof exports === 'object') {
// CommonJS
var $ = require('jquery');
module.exports = factory($);
} else {
root.testModule = factory(root.jQuery);
}
})(this, ($) => {
// TO DO
});
ESModule¶
- ES6 之后,在语言层面,通过关键词
import
和export
对模块导入导出。 - JS 解释器(JS Core) 在不同宿主环境是一样的;NodeJS(global、process),浏览器(document、window),小程序(wx、swan)在解释器的基础之上,封装一些环境相关的 API。
- ESModule 属于 JS Core 层的规范,而 CommonJS,AMD 是运行环境层面的规范。
webpack 打包流程¶
- 1) 读取入口文件内容
- 2) 分析入口文件,递归读取模块所依赖的文件内容,生产 AST 语法树
- 通过
@babel/parser
转成 AST - 通过
@babel/traverser
遍历 AST - 通过
@babel/core
和@babel/preset-env
,ES6 转 ES5
- 通过
- 3) 根据 AST 语法树,生成浏览器可运行代码
简单实现模块解析¶
- 以下代码是 webpack 主要流程的简单实现
- 入口文件
./src/index.js
node bundle.js
const fs = require('fs');
const path = require('path');
const parser = require('@babel/parser'); // 转成 AST
const traverse = require('@babel/traverse').default; // 遍历 AST
const babel = require('@babel/core');
// 1. 解析模块
function getModuleInfo(file) {
let body = fs.readFileSync(file, 'utf-8');
// 转成 AST
let ast = parser.parse(body, {
sourceType: 'module' // 解析 es6
});
// console.dir(ast, ast.program.body);
// 遍历 AST,收集依赖
let deps = {};
traverse(ast, {
ImportDeclaration: (nodePath) => {
// console.log(nodePath)
let val = nodePath.node.source.value;
let dirname = path.dirname(file);
deps[val] = './' + path.join(dirname, val);
}
});
// 将 ast 转换成 es5
let { code } = babel.transformFromAst(ast, null, {
presets: ['@babel/preset-env']
});
return { file, code, deps };
}
// 2. 收集所有依赖
function parseModules(file) {
const depsGraph = {};
// 广度优先遍历
let queue = [getModuleInfo(file)];
for(let i = 0; info = queue[i++];) {
depsGraph[info.file] = {
code: info.code,
deps: info.deps
};
for(let k in info.deps) {
let file = info.deps[k];
if (!depsGraph[file]) { // 没有访问过
queue.push(getModuleInfo(file));
}
}
}
return depsGraph;
}
// 3. 生成打包代码
function bundle(file) {
let graph = JSON.stringify(parseModules(file));
let codeStr = `
(function(depsGraph) {
var installedModules = {}; // 模块缓存对象,避免循环引用
function _require(file) {
if (installedModules[file]) {
return installedModules[file].exports;
}
var exports = {};
installedModules[file] = {
i: file,
exports
}
function absRequire(path) {
let absPath = depsGraph[file].deps[path];
return _require(absPath);
}
(function(require, exports, code){ // 递归执行 require
eval(code);
})(absRequire, exports, depsGraph[file].code)
return exports;
}
console.log(installedModules);
_require('${file}'); // 入口文件
})(${graph});
`;
return codeStr;
}
let content = bundle('./src/index.js');
// console.log(content);
// 4. 写入文件
if (!fs.existsSync('./dist')) {
fs.mkdirSync('./dist');
}
fs.writeFileSync('./dist/bundle.js', content);
同步模块之 require¶
- 读取所有的模块,存入 modeuleList
- 收集依赖,保存在 modeuleDepMapList
- 从入口文件开始递归 require(0)
(function() {
// const modeulePathIdMap = {};
// STEP 1. 同步模块
const modeuleList = [
// index.js
function(require, module, exports) {
const moduleA = require('./moduleA.js'); // this is a parentModule
console.log('this is index.js', moduleA);
},
// moduleA.js
function(require, module, exports) {
console.log('this is moduleA.js');
module.exports = 123;
}
];
const modeuleDepMapList = [
// index.js 模块的依赖
{
'./moduleA.js': 1
},
// moduleA.js 模块的依赖
{}
];
function require(id, parentModuleId) {
var currentModuleId = parentModuleId !== undefined ? modeuleDepMapList[parentModuleId][id] : id;
var moduleFunc = modeuleList[currentModuleId];
console.log(id, moduleFunc);
var module = {
exports: {}
};
var _require = id => require(id, currentModuleId); // 递归先执行依赖的模块
_require.ensure = require.ensure;
moduleFunc(_require, module, module.exports);
return module.exports;
}
require(0); // 测试同步模块入口
})()
异步模块之 require.ensure¶
- 异步模块的代码打包会成单独的 chunk
- 定义一个
window.__JSONP
函数,入参为chunkId
和 回调函数moduleFunc
require.ensure
加载 chunk 的异步代码,同时创建一个promise
,缓存在全局cache
中- chunk 加载完后,立即执行
__JSONP()
函数,并传入定义好的回调函数 __JSONP()
函数内部,会获取缓存起来的promise
,执行回调函数moduleFunc
,然后resolve
掉。
异步 chunk 文件¶
// 文件名: 1.chunk.js
__JSONP(1, function(require, module, exports) {
console.log('this is in 1.chunk.js');
module.exports = 456;
});
require.ensure¶
(function() {
// const modeulePathIdMap = {};
// STEP 1. 同步模块
const modeuleList = [
// async-index.js
function(require, module, exports) {
require.ensure('1').then(content => {
console.log(content);
});
}
];
const modeuleDepMapList = [{}];
function require(id, parentModuleId) {
var currentModuleId = parentModuleId !== undefined ? modeuleDepMapList[parentModuleId][id] : id;
var moduleFunc = modeuleList[currentModuleId];
console.log(id, moduleFunc);
var module = {
exports: {}
};
var _require = id => require(id, currentModuleId); // 递归先执行依赖的模块
_require.ensure = require.ensure;
moduleFunc(_require, module, module.exports);
return module.exports;
}
// STEP 2. 异步模块
const cache = {}; // 全局缓存模块状态
// const chunkModeulePathIdMap = {};
// const chuckModeuleList = [];
window.__JSONP = function(chunkId, moduleFunc) {
var currentChunkPromise = cache[chunkId];
var resolve = currentChunkPromise[0];
var module = {
exports: {}
};
moduleFunc(require, module, module.exports);
resolve(module.exports);
}
require.ensure = function(chunkId, parentModuleId) {
var currentModuleId = parentModuleId !== undefined ? modeuleDepMapList[parentModuleId][chunkId] : chunkId;
var chunkPromise = cache[currentModuleId];
// 没缓存
if (chunkPromise === undefined) {
var $script = document.createElement('script');
$script.src = currentModuleId + '.chunk.js';
document.head.append($script);
var promise = new Promise(function(resolve, reject) {
cache[currentModuleId] = [resolve]; // 缓存 resolve 和 promise
});
cache[currentModuleId].push(promise);
return promise;
}
// 有缓存
if (chunkPromise) {
return chunkPromise[1];
}
}
require(0); // 测试异步模块入口
})()
习题¶
说一下模块打包运行原理?
打包流程¶
-
1、读取
webpack
的配置参数; -
2、启动
webpack
,创建Compiler
对象并开始解析项目; -
3、从入口文件(
entry
)开始解析,并且找到其导入的依赖模块,递归遍历分析,形成依赖关系树; -
4、对不同文件类型的依赖模块文件使用对应的
Loader
进行编译,最终转为 Javascript 文件; -
5、整个过程中 webpack 会通过发布订阅模式,向外抛出一些
hooks
,而 webpack 的插件即可通过监听这些关键的事件节点,执行插件任务进而达到干预输出结果的目的。
核心模块¶
文件的解析与构建, 主要依赖于两个核心对象: compiler
和 compilation
。
compiler
对象是一个全局单例,他负责把控整个 webpack 打包的构建流程。compilation
对象是每一次构建的上下文对象,它包含了当次构建所需要的所有信息。每次热更新和重新构建,compiler 都会重新生成一个新的 compilation 对象,负责此次更新的构建过程。- 而每个模块间的依赖关系,则依赖于 AST 语法树。 每个模块文件在通过Loader解析完成之后,会通过
acorn
库 生成模块代码的 AST 语法树,通过语法树就可以分析这个模块是否还有依赖的模块,进而继续循环执行下一个模块的编译解析。
打包结果¶
整个 立即执行函数 里边只有三个变量和一个函数方法:
__webpack_modules__
存放了编译后的各个文件模块的JS内容;__webpack_module_cache__
用来做模块缓存;__webpack_require__
是Webpack内部实现的一套依赖引入函数。- 最后一句则是代码运行的起点,从入口文件开始,启动整个项目。
// webpack 5 打包的bundle文件内容
(() => { // webpackBootstrap
var __webpack_modules__ = ({
'file-A-path': ((modules) => { // ... })
'index-file-path': ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => { // ... })
})
// The module cache
var __webpack_module_cache__ = {};
// The require function
function __webpack_require__(moduleId) {
// Check if module is in cache
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
// Create a new module (and put it into the cache)
var module = __webpack_module_cache__[moduleId] = {
// no module.id needed
// no module.loaded needed
exports: {}
};
// Execute the module function
__webpack_modules__[moduleId](module, module.exports, __webpack_require__ "moduleId");
// Return the exports of the module
return module.exports;
}
// startup
// Load entry module and return exports
// This entry module can't be inlined because the eval devtool is used.
var __webpack_exports__ = __webpack_require__("./src/index.js");
})