编译产物剖析
(() => {
// 模块依赖
var __webpack_modules__ = ({
"./src/index.js":
((module, __unused_webpack_exports, __webpack_require__) => {
// 执行模块代码其中 同时执行__webpack_require__ 援用代码
eval(`const str = __webpack_require__("./src/a.js");
console.log(str);`);
}),
"./src/a.js":
((module, __unused_webpack_exports, __webpack_require__) => {eval(`const b = __webpack_require__("./src/base/b.js");
module.exports = 'a' + b;`);
}),
"./src/base/b.js":
((module, __unused_webpack_exports, __webpack_require__) => {eval(`module.exports = 'b';`);
}),
});
var __webpack_module_cache__ = {};
function __webpack_require__(moduleId) {
// 获取_webpack_module_cache__ 是否有 exports 值
var cachedModule = __webpack_module_cache__[moduleId];
// 如果曾经有了,不必再执行模块代码
if (cachedModule !== undefined) {return cachedModule.exports;}
var module = __webpack_module_cache__[moduleId] = {exports: {}
};
// 依据 moduleId 模块文件门路,找到模块代码并执行传入 module, module.exports, __webpack_require__
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
return module.exports;
}
// 执行入口文件代码
var __webpack_exports__ = __webpack_require__("./src/index.js");
})()
以上代码是通过精简过的,能够看到以下工具函数
- __webpack_modules__: 是一个对象,它的值是所有模块的代码,key 值对应是模块文件门路
- __webpack_module_cache__: 缓存 exports 的值
- __webpack_require__: 加载模块代码,依据模块文件门路
- __webpack_exports__: 模块对外裸露办法
通过以上工具办法,就能够在浏览器 run 起来;从源代码 es6、es7 新个性新写法,都须要转成浏览器可辨认的代码;
如:
// es6
import
// es5
__webpack_require__
webpack 通过自定义__webpack_require__、__webpack_exports__ …, 实现多个模块代码打包。
接下来将依照上述逻辑,来构建简易版的 webpack,通过以下几个阶段
- 配置信息
- 依赖构建
- 生成模版代码
- 生成文件
配置信息
class Compiler {constructor(config) {
// 获取配置信息
this.config = config;
// 保留入口门路
this.entryId;
// 模块依赖关系
this.modules = {};
// 入口门路
this.entry = config.entry;
// 工作门路
this.root = process.cwd();}
构建依赖
getSource(modulePath) {
const rules = this.config.module.rules;
let content = fs.readFileSync(modulePath, 'utf8');
return content;
}
buildModule(modulePath, isEntry) {
// 拿到模块内容
const source = this.getSource(modulePath);
// 模块 id
const moduleName = './' + path.relative(this.root, modulePath);
if (isEntry) {this.entryId = moduleName;}
// 解析源码须要把 source 源码进行革新,返回一个依赖列表
const {sourceCode, dependencies} = this.parse(source, path.dirname(moduleName)); // ./src
// 把相对路径和模块中的内容,对应起来
this.modules[moduleName] = sourceCode;
dependencies.forEach((dep) => { // 递归加载模块
this.buildModule(path.join(this.root, dep), false)
})
}
通过 buildModule
解析源码,造成模块依赖this.modules[moduleName
;
- 找到模块源码
this.getSource(modulePath);
- 解析源码,转换 ast,返回源码和模块依赖门路
this.parse(source, path.dirname(moduleName))
- 生成门路与模块代码对象:
this.modules[moduleName] = sourceCode
- 对模块中有依赖的文件,造成迭代调用
this.buildModule(path.join(this.root, dep), false)
从新执行以上办法
解析源码
parse(source, parentPatch) { // AST 解析语法树
const ast = babylon.parse(source);
let dependencies = []; // 依赖数组
traverse(ast, {CallExpression(p) {
const node = p.node;
if (node.callee.name == 'require') {
node.callee.name = '__webpack_require__';
let moduleName = node.arguments[0].value; // 模块名字
moduleName = moduleName + (path.extname(moduleName) ? '':'.js'); // ./a.js
moduleName = './' + path.join(parentPatch, moduleName); // src/a.js
dependencies.push(moduleName);
node.arguments = [t.stringLiteral(moduleName)];
}
}
});
const sourceCode = generator(ast).code;
return {sourceCode, dependencies}
}
解析模块源代码,替换 require 办法成__webpack_require__
,同时把文件门路也转换掉
代码生成模版
// ejs 模版代码
(() => {
var __webpack_modules__ = ({<%for(let key in modules){%>
"<%-key%>":
((module, __unused_webpack_exports, __webpack_require__) => {eval(`<%-modules[key]%>`);
}),
<%}%>
});
var __webpack_module_cache__ = {};
function __webpack_require__(moduleId) {var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {return cachedModule.exports;}
var module = __webpack_module_cache__[moduleId] = {exports: {}
};
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
return module.exports;
}
var __webpack_exports__ = __webpack_require__("<%-entryId%>");
})()
;
将把 this.modules
、this.entryId
数据,传入此模版中,生成可执行代码
生成文件
emitFile() {const {output} = this.config;
const main = path.join(output.path, output.filename);
// 模块字符串
let templateStr = this.getSource(path.join(__dirname, 'main.ejs'));
// 生成代码
const code = ejs.render(templateStr, {entryId: this.entryId, modules: this.modules});
this.assets = {};
this.assets[main] = code;
// 将代码写入 output 文件夹 / 文件
fs.writeFileSync(main, this.assets[main])
}
loader
将援用资源,转换成模块
getSource(modulePath) {
const rules = this.config.module.rules;
let content = fs.readFileSync(modulePath, 'utf8');
for (let i = 0; i < rules.length; i++) {const rule = rules[i];
const {test,use} = rule;
let len = use.length -1
if(test.test(modulePath)) {function normalLoader() {const loader = require(use[len--]);
content = loader(content);
if(len >= 0) {normalLoader();
}
}
normalLoader();}
}
return content;
}
依据门路获取源码,判断以后门路是否能匹配上 loader 文件test.test(modulePath)
,
如果能够匹配,将模块源码传入,loader 办法中,再做其余转换 content = loader(content);
并且造成递归调用;
// 自定义 loader
// less-loader
const {render} = require('less')
function loader(source) {
let css = '';
render(source,(err,c) => {css = c;})
css = css.replace(/\n/g,'\\n')
return css;
}
module.exports = loader;
// style-loader
function loader(source) {
let style = `
let style = document.createElement('style')
style.innerHTML = ${JSON.stringify(source)}
document.head.appendChild(style);
`;
return style;
}
module.exports = loader;
配置文件
const path = require('path');
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
filename: 'bundle2.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.less$/,
use:[path.resolve(__dirname,'loader','style-loader'), // 后执行
path.resolve(__dirname,'loader','less-loader') // 先执行
]
}
]
},
}
plugin
从状态上看,插件通常是一个带有 apply 函数的类:
class SomePlugin {apply(compiler) {}}
apply 函数运行时会失去参数 compiler,以此为终点能够调用 hook 对象注册各种钩子回调,
例如:compiler.hooks.make.tapAsync,这外面 make 是钩子名称,tapAsync 定义了钩子的调用形式,
webpack 的插件架构基于这种模式构建而成,插件开发者能够应用这种模式在钩子回调中,插入特定代码
配置文件
const path = require('path');
class P {constructor() { }
apply(compiler) {
// 获取 compiler 上办法,注册各个阶段回调
compiler.hooks.emit.tap('emit',function () {console.log('emit')
})
}
}
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
filename: 'bundle2.js',
path: path.resolve(__dirname, 'dist')
},
plugins: [new P()
]
}
compiler.js
const {SyncHook} = require('tapable');
class Compiler {constructor(config) {
this.config = config;
// 保留入口门路
this.entryId;
// 模块依赖关系
this.modules = {};
// 入口门路
this.entry = config.entry;
// 工作门路
this.root = process.cwd();
// 开始注册同步公布订阅
this.hooks = {entryOption:new SyncHook(),
compile:new SyncHook(),
afterCompile:new SyncHook(),
afterPlugins:new SyncHook(),
run:new SyncHook(),
emit:new SyncHook(),
done:new SyncHook()};
const plugins = this.config.plugins;
// 拿到配置项里的 plugin
if(Array.isArray(plugins)) {plugins.forEach((plugin) => {
// 调用 plugin 中实例办法 apply,并传入整个 Compiler 类
plugin.apply(this);
})
}
this.hooks.afterPlugins.call();}
plugin 外围就是 tapable
采纳公布 / 订阅的模式,先收集 / 订阅插件中所须要回调,在 webpack 生命周期中去执行,这样插件就能够在应用的机会,获取想要的上下文,从而进行干涉以及其余操作。
以上就是各个阶段要害外围代码局部
残缺代码
const path = require('path');
const fs = require('fs');
const babylon = require('babylon');
const traverse = require('@babel/traverse').default;
const t = require('@babel/types');
const generator = require('@babel/generator').default;
const ejs = require('ejs');
const {SyncHook} = require('tapable');
// babylon 解析 js 转换 ast
// https://www.astexplorer.net/
// @babel/travers
// @babel/types
// @babel/generator
class Compiler {constructor(config) {
this.config = config;
// 保留入口门路
this.entryId;
// 模块依赖关系
this.modules = {};
// 入口门路
this.entry = config.entry;
// 工作门路
this.root = process.cwd();
this.hooks = {entryOption:new SyncHook(),
compile:new SyncHook(),
afterCompile:new SyncHook(),
afterPlugins:new SyncHook(),
run:new SyncHook(),
emit:new SyncHook(),
done:new SyncHook()};
const plugins = this.config.plugins;
if(Array.isArray(plugins)) {plugins.forEach((plugin) => {plugin.apply(this);
})
}
this.hooks.afterPlugins.call();}
getSource(modulePath) {
const rules = this.config.module.rules;
let content = fs.readFileSync(modulePath, 'utf8');
for (let i = 0; i < rules.length; i++) {const rule = rules[i];
const {test,use} = rule;
let len = use.length -1
if(test.test(modulePath)) {function normalLoader() {const loader = require(use[len--]);
content = loader(content);
if(len >= 0) {normalLoader();
}
}
normalLoader();}
}
return content;
}
parse(source, parentPatch) { // AST 解析语法树
const ast = babylon.parse(source);
let dependencies = []; // 依赖数组
traverse(ast, {CallExpression(p) {
const node = p.node;
if (node.callee.name == 'require') {
node.callee.name = '__webpack_require__';
let moduleName = node.arguments[0].value; // 模块名字
moduleName = moduleName + (path.extname(moduleName) ? '':'.js'); // ./a.js
moduleName = './' + path.join(parentPatch, moduleName); // src/a.js
dependencies.push(moduleName);
node.arguments = [t.stringLiteral(moduleName)];
}
}
});
const sourceCode = generator(ast).code;
return {sourceCode, dependencies}
}
buildModule(modulePath, isEntry) {
// 拿到模块内容
const source = this.getSource(modulePath);
// 模块 id
const moduleName = './' + path.relative(this.root, modulePath);
if (isEntry) {this.entryId = moduleName;}
// 解析源码须要把 source 源码进行革新,返回一个依赖列表
const {sourceCode, dependencies} = this.parse(source, path.dirname(moduleName)); // ./src
// 把相对路径和模块中的内容,对应起来
this.modules[moduleName] = sourceCode;
dependencies.forEach((dep) => { // 递归加载模块
this.buildModule(path.join(this.root, dep), false)
})
}
emitFile() {const {output} = this.config;
const main = path.join(output.path, output.filename);
let templateStr = this.getSource(path.join(__dirname, 'main.ejs'));
const code = ejs.render(templateStr, {entryId: this.entryId, modules: this.modules});
this.assets = {};
this.assets[main] = code;
fs.writeFileSync(main, this.assets[main])
}
run() {this.hooks.run.call();
this.hooks.compile.call();
// 执行,并且创立模块的依赖关系
this.buildModule(path.resolve(this.root, this.entry), true);
this.hooks.afterCompile.call();
// 发射一个文件,打包后的文件
this.emitFile();
this.hooks.emit.call();
this.hooks.done.call();}
}
module.exports = Compiler;
github 链接:
https://github.com/NoahsDante…
如果对你有帮忙,点个 start