webpack的打包核心流程
- 读取
webpack.config.js配置 options, 必须包含entry和ouput配置项 - 实例化一个
Compiler类, 执行run方法开始编译打包工作 - 根据入口文件内容递归解析模块依赖关系,通过正则改写
require关键字为__wepack_require__ - 根据模块依赖数据,发射文件并实例
Compilation类 - 每个过程都可以响应
tapable注册的hooks的回调
compiler 和 compilation 的概念
- Compiler:
compiler实例是负责编译(compile)打包(emitFile)的一个对象,它包含了配置文件的信息,原型上有执行编译run方法,模块解析buildModule方法(这个方法会执行loader),compiler会调用tapable注册的hooks的回调, 来响应插件的注册的钩子 - Compilation:
compilation代表的是这次编译过程的资源对象,提供了对打包生成的资源(chunks, modules)增删改查的方法,大部分的插件都是通过操作compilation来完成优化
beggar-webapck 目录结构
.
|____Compilation.js // 资源对象
|____Compiler.js // 编译对象
|____index.js // toy-webpack的主入口
|____template.ejs // 渲染模板
用来测试 beggar-webapck 的项目结构
|____console-loader.js // 用于测试loader
|____html-plugin.js // 用于测试plugin
|____index.html // html 模板
|____src
| |____index.js // 入口文件
| |____utils.js // 入口文件的依赖文件
|____webpack.config.js // webpack的配置文件
入口文件index.js
const add = require('./utils.js');
console.log(add(1,2))
utils.js
module.exports = function add (...args) {
return args.reduce((a, b) => a + b, 0)
}
console-loader.js
// loader就是一个函数,当前的上下文this的指向compiler实例, consoleLoader.call(compiler, content, sourcemap, ...);
// 第一个参数文件的字符串(string或者buffer)
// loader同时也必须要返回字符串(string或者buffer)
// 我们这个loader十分简单,只是把js文件中的所有的console.log改成console.error
module.exports = function consoleLoader(content) {
// 把console.log 改成console.error
return `${content.replace(/console\.log\(/g, 'console.error(')}`;
}
html-plugin
// 这个插件很简单,在发射bundle之前把bundle的文件名,通过script标签插入到body尾部,这样就不用我们每次新建一个html文件手动插入
const BODY_RE = /<\/body>/gi;
const fs = require('fs');
const path = require('path')
let htmlTemplate = fs.readFileSync(path.resolve('./index.html')).toString();
module.exports = class HtmlPlugin {
apply(compiler) {
compiler.hooks.emit.tapAsync('HtmlPlugin', compilation => {
const { bundleName, assets } = compilation;
htmlTemplate = htmlTemplate.replace(BODY_RE, _ => `<script src=${bundleName}></script>\n` + _);
assets['index.html'] = {
source: () => htmlTemplate,
size: () => htmlTemplate.length,
}
console.log('script has inserted!')
});
compiler.hooks.done.tap('Done', () => {
// 打印成功日志
console.log('Build Success!')
})
}
}
webpack.config.js
// 必须提供entry,output
const path = require('path');
const HtmlPlugin = require('./html-plugin')
module.exports = {
entry: './src/index.js',
output: {
filename: 'app.js',
publicPath: './',
path: path.resolve('dist')
},
module: {
rules: [
{
test: /\.js$/,
use: [
path.resolve('./banner-loader.js')
]
}
]
},
plugins: [
new HtmlPlugin()
]
}
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
</body>
</html>
开始编写toy-webpack
下面的beggar-webpack实现会尽量每行都添加注释
主入口文件index.js
- 这个文件可以理解为第三方库中bin目录下的xxx.js
- 也是
package.json中的main字段的值
const Compiler = require("./Compiler");
const path = require("path");
// 获取webpack.config.js配置
const webpackConfig = require(path.resolve("webpack.config.js"));
const webpack = (options) => {
// 把配置传给我们的compiler
const compiler = new Compiler(options);
// 执行所以插件的apply方法, 插件必须要提供一个apply的方法,这跟vue的插件很相似
if (options.plugins && Array.isArray(options.plugins)) {
options.plugins.forEach(plugin => plugin.apply(compiler));
}
// 开始编译
compiler.run();
};
webpack(webpackConfig);
module.exports = webpack;
Compiler.js
compiler 是我们乞丐版webpack的最关键实现,loader执行时机,递归获取模块依赖关系,发射打包的文件都在这里
const { SyncHook, AsyncSeriesHook } = require('tapable')
const Compilation = require('./Compilation');
const path = require('path');
const ejs = require('ejs')
const fs = require('fs');
// 匹配commonjs中require关键字
const COMMONJS_REQUIRE_RE = /require\(['"]([^'"]+)['"]\)/g;
// 这里只考虑入口工程是都是commonjs写法,如果是es module写法需要用到babel相关的工具,我们先不考虑es module的场景
// @babel/core, @babel/preset-env, @babel/traverse, @babel/genertor 转为commonjs module
// 删除文件中的多行和单行注释,处理在注释写require的场景
const MULTI_LINE_COMMENT_RE = /(?:^|\n|\r)\s*\/\*[\s\S]*?\*\/\s*(?:\r|\n|$)/g;
const SINGLE_LINE_COMMENT_RE = /(?:^|\n|\r)\s*\/\/.*(?:\r|\n|$)/g;
const removeComnent = c => c.replace(MULTI_LINE_COMMENT_RE, '').replace(SINGLE_LINE_COMMENT_RE, '');
class Compiler {
constructor(options) {
// 获取传进来的webpack配置
this.options = options;
// Node.js 进程的当前工作目录,一般来说你的package.json所在的位置等于process.cwd()
this.root = process.cwd();
// 入口文件的绝对路径
this.entryModuleAbsPath = path.join(this.root, options.entry);
// 入口文件的mouleId
this.entryID = '';
// 输出打包文件的目录
this.outputPath = this.options.output.path;
// 入口文件打包后的文件名
this.bundleName = this.options.output.filename;
// 这里只提供几个hook作为说明
this.hooks = {
run: new AsyncSeriesHook(["compiler"]), // 异步串行钩子
done: new SyncHook(), // 同步钩子的回调不能用异步代码,不然无法保证执行顺序
fail: new SyncHook(),
emit: new AsyncSeriesHook(["compilation"])
}
// 获取loader的配置信息
this.rules = this.options.module.rules;
// 获取跟入口文件有关的依赖,a依赖b,b依赖c,则b、c都是a的依赖,这个属性用于渲染我们的template模板
this.modules = [];
}
run() {
// 执行run钩子,如果plugin有注册该钩子的事件会被执行
// tapable中的钩子的注册和调用关系
// 异步钩子
// 注册:tapAsync 调用:callAsync 或者 注册:tapPrmise(回调必须返回一个prmosise) 调用:callPromise
// 同步钩子
// 注册:tap 调用:call
this.hooks.run.callAsync(this, err => {
if (err) {
return this.hooks.fail.call(err);
}
// 获取模块依赖关系,收集modules,执行loader
this.buildModule(this.entryModuleAbsPath, true);
// 发射文件到ouput
this.emitAssets();
})
}
buildModule(modulePath, isEntry) {
// getSource传入一个绝对路径
const content = this.getSource(modulePath);
// 得到转换后的内容,和该文件中的依赖项, 则require的值
const { source, subs } = this.parse(content, modulePath);
// 构造一个格式都是相对于src 比如'./src/index.js' , './src/utils/js'
const moduleId = this.getRelativePath(this.root, modulePath);
if (isEntry) {
// 如果是入口文件, 这个entryID用于渲染template
this.entryID = moduleId;
}
// 加入到modules
this.modules.push({
moduleId,
source: source,
dependencies: subs,
})
// 递归解析所依赖的子模块
subs.forEach(subAbsPath => {
this.buildModule(subAbsPath, false)
})
}
getSource(absPath) {
// 获取文件后缀名 'xxx.vue' ==> '.vue'
const ext = path.extname(absPath);
let content = fs.readFileSync(absPath).toString();
// 筛选符合的loader
const filterRules = this.rules && this.rules.find(rule => rule.test.test(ext));
if (filterRules && Array.isArray(filterRules.use)) {
// 这里假设ues的配置都是字符串,不是对象的形式 --> use: ['style-loader', 'css-loader']
content = this.runLoader(content, filterRules.use.reverse())
}
return content;
}
getRelativePath(rootContext, absPath) {
return './' + path.relative(rootContext, absPath).replace(/\\/, '/')
}
parse(content, modulePath) {
// 子依赖
const subs = [];
// 删除注释
content = removeComnent(content);
content = content.replace(COMMONJS_REQUIRE_RE, (_, subModuleRelativePath) => {
// 依赖的子模块require('./util.js') 那么uitl.js是他依赖项
const subModuleAbsPath = path.join(modulePath, '..', subModuleRelativePath);
subs.push(subModuleAbsPath);
const moduleId = this.getRelativePath(this.root, subModuleAbsPath);
// 把require改成模板定义的__webpack_require__函数, 这个函数用于加载和缓存模块
return `__webpack_require__('${moduleId}')`
})
return {
source: content,
subs,
}
}
emitAssets() {
// 渲染模板
const webpackTemplate = fs.readFileSync(path.join(__dirname, './template.ejs')).toString();
const content = ejs.render(webpackTemplate, {
entryModuleId: this.entryID,
modules: this.modules
})
// 实例化我们的资源对象,它目前的功能只有一个存放生成的文件信息
const compilation = new Compilation({
bundleName: this.bundleName
});
const assets = compilation.assets;
assets[this.bundleName] = {
source: () => content,
size: () => content.size
}
// 调用插件注册的emit回调
this.hooks.emit.callAsync(compilation);
// 通过fs.writeFileSync 把compilation.assets的资源文件写入到output
Object.keys(assets).forEach(filename => {
if (!fs.existsSync(this.outputPath)) fs.mkdirSync(this.outputPath);
fs.writeFileSync(path.join(this.outputPath, filename), assets[filename].source(), 'utf-8')
})
// 调用插件注册的done回调
this.hooks.done.call();
}
// 执行我们的loader
runLoader(content, uses) {
// 因为loader的执行顺序是倒序,从后面开始执行,传送门https://github.com/webpack/loader-runner(不考虑pitch)
return uses.reduce((retContent, loader) => {
const loaderFn = require(loader);
// 绑定我们的compiler实例作为上下文
return loaderFn.call(this, retContent);
}, content)
}
}
module.exports = Compiler
Compilation.js
我们这个类功能十分单一,仅存放资源信息
module.exports = class Compilation {
constructor(options) {
this.assets = [];
this.bundleName = options.bundleName;
}
};
template.ejs
渲染我们的模板, 这个是webpack的web模式的MainTemplate中的模板之一,只保留加载模块的方法 __webpack_require__,
看他的注释就可以知道什么意思,__webpack_require__模拟的是commonjs的require方法,里面暴露的对象module.exports、exports跟commonjs的一致,所以我们才只需要处理require这个关键字,这个方法帮我们减少不少工作。
/******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId]) {
/******/ return installedModules[moduleId].exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = installedModules[moduleId] = {
/******/ i: moduleId,
/******/ l: false,
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Flag the module as loaded
/******/ module.l = true;
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/ // Load entry module and return exports
/******/ return __webpack_require__( "<%-entryModuleId%>"); // 我们入口模块的ID, './src/index.js'
/******/ })
/************************************************************************/
({
<%for (var module of modules){%>
"<%-module.moduleId%>":
(function (module, exports, __webpack_require__) {
<%-module.source%>
}),
<%}%>
});
到这里我们的beggar-webpack已经实现好了,我们来用上面的demo测试下
完整的测试项目结构
新建一个bagger-webpack目录,把我相关的webpack文件都丢进去
.
|____banner-loader.js
|____beggar-webpack
| |____Compilation.js
| |____Compiler.js
| |____index.js
| |____template.ejs
|____dist
| |____app.js
| |____index.html
|____html-plugin.js
|____index.html
|____package.json
|____src
| |____index.js
| |____utils.js
|____webpack.config.js
// 在package.json的script加上
"scripts": {
"build": "node ./beggar-webpack/index.js"
}
执行npm run build, 工程多了dist目录, 里面是我们的打包后的文件,多出的index.html说明我的插件执行ok
[bigham@DESKTOP-MKMH2OT /d/Work_Space/test-mini-webpack/dist]$ npm run build
> test-mini-webpack@1.0.0 build D:\Work_Space\test-mini-webpack
> node ./beggar-webpack/index.js
script has inserted!
Build Success!
//插件emit、done的回调也在日志输出了
dist
.
|____app.js
|____index.html
app.js 的内容
/******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId]) {
/******/ return installedModules[moduleId].exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = installedModules[moduleId] = {
/******/ i: moduleId,
/******/ l: false,
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Flag the module as loaded
/******/ module.l = true;
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/ // Load entry module and return exports
/******/ return __webpack_require__(__webpack_require__.s = "./src/index.js");
/******/ })
/************************************************************************/
({
"./src/index.js":
(function (module, exports, __webpack_require__) {
const add = __webpack_require__('./src/utils.js');
console.error(add(1,2))
}),
"./src/utils.js":
(function (module, exports, __webpack_require__) {
module.exports = function add (...args) {
return args.reduce((a, b) => a + b, 0)
}
}),
});
app.js嗯有webpack那味道了,用浏览器打开index.html,康康~
正确的输出了3,loader也生效了
至此我们的140行乞丐版webpack就完成了,还包含了loader和插件机制。
chunk 和 module 的区别
chunk: 指的是打包出来的文件,app.js就是一个chunk,我们日常工程使用懒加载打包的文件也是一个chunkmodule:chunk和module是包含关系,一个chunk文件里面可能包含了很多module比如app.js这个chunk中就包含了两个module
{
"./src/index.js": function (module, exports, __webpack_require__) { // 这是一个module
const add = __webpack_require__("./src/utils.js");
console.error(add(1, 2));
},
"./src/utils.js": function (module, exports, __webpack_require__) { // 这是一个module
module.exports = function add(...args) {
return args.reduce((a, b) => a + b, 0);
};
},
}