大家好,我是作曲家种太阳
之前对webpack了解的只限于表面,边查边用
虽然能勉强应付的过去,但是对webpack内部的运行机制并谈不上透彻,直到最近开始系统学习,才谈得上有了一些深入的了解
这篇文章是带你手写一个简易的webpack,你会学到:
- 了解webpack工作流程
- 熟悉webpack配置
- 了解webpack内部实现原理
不过要说明的是,这篇是我的学习笔记,简易webpack核心源码并非原创,是加上自己理解的产物 感兴趣的小伙伴们可以一起讨论~
0.了解webpack的核心概念
Entry
入口起点(entry point)指示 webpack 应该使用哪个模块,来作为构建其内部依赖图的开始。
进入入口起点后,webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的。
每个依赖项随即被处理,最后输出到称之为 bundles 的文件中。
Output
output 属性告诉 webpack 在哪里输出它所创建的 bundles,以及如何命名这些文件,默认值为 ./dist。
基本上,整个应用程序结构,都会被编译到你指定的输出路径的文件夹中。
Module
模块,在 Webpack 里一切皆模块,一个模块对应着一个文件。Webpack 会从配置的 Entry 开始递归找出所有依赖的模块。
Chunk
代码块,一个 Chunk 由多个模块组合而成,用于代码合并与分割。
Loader
loader 让 webpack 能够去处理那些非 JavaScript 文件(webpack 自身只理解 JavaScript)。
loader 可以将所有类型的文件转换为 webpack 能够处理的有效模块,然后你就可以利用 webpack 的打包能力,对它们进行处理。
本质上,webpack loader 将所有类型的文件,转换为应用程序的依赖图(和最终的 bundle)可以直接引用的模块。
Plugin
loader 被用于转换某些类型的模块,而插件则可以用于执行范围更广的任务。
插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量。插件接口功能极其强大,可以用来处理各种各样的任务。
参考链接: juejin.cn/post/684490…
1.认识Webpack的工作流程(重点)
- 初始化参数:从配置文件和Shell语句中读取并合并参数,得出最终的配置对象
- 用上一步得到的参数初始化Compiler对象
- 加载所有配置的插件
- 执行对象的run方法开始执行编译
- 根据配置中的entry找出入口文件
- 从入口文件出发,调用所有配置的Loader对模块进行编译
- 再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
- 根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 chunks
- 再把每个chunks转换成一个单独的文件加入到输出列表
- 在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统
不用一开始就理解所有的流程,因为后面代码会按照上面的流程
在脑海有个映像就行,下面我画的图可以有助于你的理解:
2.初始化准备工作
(1).创建一个文件mini-webpack,并初始化npm
npm init
(2).在pageckage.json上加入babel依赖包
"devDependencies": {
"@babel/generator": "^7.14.5",
"@babel/parser": "^7.14.6",
"@babel/traverse": "^7.14.5",
"babel-types": "^6.26.0",
},
(3).加入npm执行命令
"scripts": {
"build": "node debugger.js"
},
注意这个是执行debugger.js命令 (4)新建 mini-webpack/debugger.js文件
2.编写mini-webpack/debugger.js文件
const webpack = require('./webpack')
const webpackOptions = require("./webpack.config")
// compiler 代表整个编译过程,是一个对象
const compiler = webpack(webpackOptions)
// 调用run方法可以启动编译
compiler.run((err, stats) => {
const result = stats.toJson({
files: true, // 产出了那些文件
assets: true, // 生成了哪些资源
chunks: true, // 生成了哪些代码块
modules: true, // 模块信息
entries: true, // 入口信息
})
// 查看编译结果
console.log(JSON.stringify(result, null, 2));
})
这个文件主要是调用webpack实例,并且返回一个编译result结果:
不用细致的看这个编译结果,了解大致的字段的作用就行了
{
hash: '50ee8c2052552e5a4565', // 本次编译`产出的hash值
version: '5.51.1', // webpack版本
time: 65, // 话费的时间
builtAt: 1630300492483, // 构建时间戳
publicPath: 'auto', // 资源的访问路径
outputPath: '/XXX//XXX/XXX/XXX/mini-webpack/dist', // 输出的目录
assetsBychunksName: {main: ['main.js']},
"assets":
[
{
"type": "asset",
"name": "main.js",
"size": 167,
"emitted": true,
"comparedForEmit": false,
"cached": false,
"info": {
"javascriptModule": false,
"size": 167
},
"chunksNames": [
"main"
],
"chunksIdHints": [],
"auxiliarychunksNames": [],
"auxiliarychunksIdHints": [],
"related": {},
"chunks": [
"main"
],
"auxiliarychunks": [],
"isOverSizeLimit": false
}
],
"chunks":
[
{
"names": [
"main"
],
"files": [
"main.js"
],
}
],
"modules":
[
{
"type": "module",
"moduleType": "javascript/auto",
"layer": null,
"size": 1,
"sizes": {
"javascript": 1
},
"built": true,
"codeGenerated": true,
"buildTimeExecuted": false,
"cached": false,
"identifier": "/Users/peiyahui/Desktop/code/xxx/mini-webpack/src/index.js",
"name": "./src/index.js",
"nameForCondition": "/Users/peiyahui/Desktop/code/xxx/mini-webpack/src/index.js",
"index": 0,
"preOrderIndex": 0,
"index2": 0,
"postOrderIndex": 0,
"cacheable": true,
"optional": false,
"orphan": false,
"issuer": null,
"issuerName": null,
"issuerPath": null,
"failed": false,
"errors": 0,
"warnings": 0,
"id": "./src/index.js",
"issuerId": null,
"chunks": [
"main"
],
"assets": [],
"reasons": [
{
"moduleIdentifier": null,
"module": null,
"moduleName": null,
"resolvedModuleIdentifier": null,
"resolvedModule": null,
"type": "entry",
"active": true,
"explanation": "",
"userRequest": "./src/index.js",
"loc": "main",
"moduleId": null,
"resolvedModuleId": null
}
],
"usedExports": null,
"providedExports": null,
"optimizationBailout": [],
"depth": 0
}
],
// 入口点
"entrypoints":
{
"main":
{
"name":
"main",
"chunks":
[
"main"
],
"assets":
[
{
"name": "main.js",
"size": 167
}
],
"filteredAssets":
0,
"assetsSize":
167,
"auxiliaryAssets":
[],
"filteredAuxiliaryAssets":
0,
"auxiliaryAssetsSize":
0,
"children":
{}
,
"childAssets":
{}
,
"isOverSizeLimit":
false
}
}
}
3.编写mini-webpack/webpack.js文件
第一步我们引入了webpack.js文件,它是webpack入口文件,接下来编写:
// webpack
function webpack(options) {
//1.初始化参数:从配置文件和Shell语句中读取并合并参数,得出最终的配置对象
let shellConfig = process.argv.slice(2).reduce((shellConfig, item) => {
let [key, value] = item.split("=");
shellConfig[key.slice(2)] = value
return shellConfig
}, {})
let finalConfig = {...options, ...shellConfig};
// 2. 用上一步得到的参数初始化Compiler对象
let compiler = new Compiler(finalConfig)
// 3.加载所有配置的插件
let {plugins} = finalConfig
//
for (let plugin of plugins) {
plugin.apply(compiler)
}
return compiler
}
module.exports = webpack
process.argv.slice(2) 这个是解析webpack执行命令参数的
比如 执行 webpack --mode=development 的时候
会解析出来--mode=development根据 = 拆分出key(mode)和value(development)
4.编写mini-webpack/webpack.config.js文件
第一步我们引入了webpack.config.js文件,它是webpack配置文件
const path = require('path');
const RunPlugin = require('./plugins/run-plugin');
const DonePlugin = require('./plugins/done-plugin');
const AssetPlugin = require('./plugins/assets-plugin');
module.exports = {
mode:'development', // 生产模式
devtool:false,
context:process.cwd(),//上下文目录, ./src .默认代表根目录 默认值其实就是当前命令执行的时候所在的目录
entry:{ // 入口
entry1:'./src/entry1.js',
entry2:'./src/entry2.js'
},
output:{ // 输出地址
path:path.join(__dirname,'dist'),
filename:'[name].js'
},
resolve:{ // 默认识别文件后缀
extensions:['.js','.jsx','.json']
},
module:{ // 匹配对应的test文件后缀字段正则,加载对应的loader
rules:[
{
test:/.js$/,
use:[
path.resolve(__dirname,'loaders','logger1-loader.js'),
path.resolve(__dirname,'loaders','logger2-loader.js')
]
}
]
},
plugins:[ // webpack插件
new RunPlugin(),
new DonePlugin(),
new AssetPlugin()
]
}
webpack的配置,相信你有所了解,主要是理解上面备注的配置字段即可
5.编写两个loader和两个plugin
编写的loader和plguin都很简单了,主要是配合webpack加载loader和plugin原理的 (1)mini-webpack/loader/logger1-loader.js
function loader(source) {
console.log("loading")
return source + "//1"
}
module.exports = loader
(2)mini-webpack/loader/logger2-loader.js
function loader(source) {
console.log("loading22222222222")
return source + "//2"
}
module.exports = loader
(3)mini-webpack/plugins/done-plugins.js
class RunPlugins {
apply(compiler) {
compiler.hooks.done.tap("DonePlugins", () => {
console.log("编译结束了")
})
}
}
module.exports = RunPlugins
(4)mini-webpack/plugins/run-plugins.js
class RunPlugins {
apply(compiler) {
compiler.hooks.run.tap("RunPlugins",()=>{
console.log("开始编译了")
})
}
}
module.exports = RunPlugins
6.编写编写 Compiler 类(重点)
还记得第一步中debuger.js调用了Compiler的run方法返回一个result么?
还记得第三步骤的时候需要生成一个Compiler类的实例么?
嘿嘿,相信你已经串联起来了.
这个 Compiler 类的主要功能就是串联整个编译过程,并触发钩子函数响应对应的插件
但不不做具体编译的工作,而是交给了 Complication 类,可暂不理会
mini-webpack/Compiler.js
const fs = require("fs");
const path = require("path");
const Complication = require("./compilcation");
let {SyncHook} = require("tapable")
class Compiler {
constructor(options) {
this.options = options
this.hooks = {
run: new SyncHook(), // 开始启动编译 刚刚开始
emit: new SyncHook(['assets']), // 会在将要写入的文件时候触发
done: new SyncHook(), // 将会在完成变的时候出发 全部完成
}
}
//
//4. 执行Compiler对象的run方法开始执行编译
run(callback) {
this.hooks.run.call();//触发run钩子
//5. 根据配置中的entry找出入口文件
this.compile((err, stats) => {
this.hooks.emit.call(stats.assets);
//10. 在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统
for (let filename in stats.assets) {
let filePath = path.join(this.options.output.path, filename);
fs.writeFileSync(filePath, stats.assets[filename], 'utf8');
}
callback(null, {
toJson: () => stats
});
});
//中间是我们编译流程
this.hooks.done.call(); //编译之后触发done钩子
}
//
compile(callback) {
let complication = new Complication(this.options)
complication.build(callback)
}
}
module.exports = Compiler
这里的钩子函数,是用了tapable.js实现的,有兴趣的同学可以了解下tapble.js用法,和node的发布订阅模式很像
7. 编写 Complication 类(难点)
Complication 类主要就是做编译工作
mini-webpack/Complication.js
/**
* @name: compilcation
* @author: peiyahui
* @date: 2021/8/31 12:51 下午
* @description:compilcation
* @update: 2021/8/31 12:51 下午
*/
const path = require('path')
const fs = require('fs')
const types = require('babel-types');
const parser = require('@babel/parser');//源代码转成AST抽象语法树
const traverse = require('@babel/traverse').default;//遍历语法树
const generator = require('@babel/generator').default;//把语法树重新生成代码
const baseDir = toUnitPath(process.cwd());
//
function toUnitPath(filePath) {
// return filePath.replace(/\/g, '/');// 不知道为什么在掘金的代码快中会报错,coding的时候请打开
}
class Complication {
constructor(options) {
this.options = options;
this.entries = [] // 入口信息
this.modules = [] // 模块信息
this.chunks = [] // 生成了哪些代码块
this.files = [] // 产出了那些文件
this.assets = [] // 生成了哪些资源
}
build(callback) {
let entry = {}
// 5.根据配置中的entry找出入口文件
if (typeof this.options.entry === "string") {
entry.main = this.options.entry
} else {
entry = this.options.entry
}
// 处理入口
for (let entryName in entry) {
// 获取entry1的绝对路径,context是上线文(执行的环境),默认是根目录
let entryFilePath = path.join(this.options.context, entry[entryName])
// 6.从入口文件出发,调用所有配置的Loader对模块进行编译
let entryModule = this.buildModule(entryName, entryFilePath) // 返回
//
// this.modules.push(entryModulePath);
// 8.根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 chunks
let chunk = {
name: entryName, entryModule, modules: this.modules.filter(item => {
return item.name === entryName || item.extraNames.includes(entryName);
})
};
this.entries.push(chunk)
this.chunks.push(chunk) // 添加到代码块中
}
// 9. 再把每个Chunk转化成一个单独的文件加入到输出列表
//9. 再把每个Chunk转换成一个单独的文件加入到输出列表
this.chunks.forEach(chunk => {
// 替换文件名
let filename = this.options.output.filename.replace('[name]', chunk.name);
// this.assets就是输出列表 key输出的文件名 值就是输出的内容
this.assets[filename] = getSource(chunk);
});
console.log('this.chunks--------->', this.chunks)
// Compiler.run()里面的函数 (err, stats) => {}
callback(null, {
entries: this.entries,
chunks: this.chunks,
modules: this.modules,
files: this.files,
assets: this.assets
});
}
// name:名称 modulePath模块的绝对路径
buildModule(name, modulePath) {
// 6.从入口文件出发,调用所有配置的Loader对模块进行编译
// 6.1 读取文件内容
let sourceCode = fs.readFileSync(modulePath, 'utf8'); //console.log('entry1');
let rules = this.options.module.rules // loader的路径
let loaders = [] // 寻找匹配的loader
//
for (let i = 0; i < rules.length; i++) {
let {test} = rules[i];
//如果此rule的正则和模块的路径匹配的话
if (modulePath.match(test)) {
loaders = [...loaders, ...rules[i].use];
}
}
// 从后往前加载loader
sourceCode = loaders.reduceRight((sourceCode, loader) => {
// 同步加载
return require(loader)(sourceCode);
}, sourceCode);
// 7.再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
//获得当前模块模块ID ./src/index.js
let moduleId = './' + path.posix.relative(baseDir, modulePath);
let module = {id: moduleId, dependencies: [], name, extraNames: []}; // 生成一个module格式
let ast = parser.parse(sourceCode, {sourceType: 'module'}); // 生成ast语法树
traverse(ast, {
//
CallExpression: ({node}) => {
if (node.callee.name === 'require') {
//依赖的模块的相对路径
let moduleName = node.arguments[0].value;//./title1
//获取当前模块的所有的目录
let dirname = path.posix.dirname(modulePath);// /
// 兼容平台的路径写法(/////)
let depModulePath = path.posix.join(dirname, moduleName);
let extensions = this.options.resolve.extensions; // 扩展名
depModulePath = tryExtensions(depModulePath, extensions);//已经包含了拓展名了
//得到依赖的模块ID C:/aproject/zhufengwebpack202106/4.flow/src/title1.js
//相对于项目根目录 的相对路径 ./src/title1.js
let depModuleId = './' + path.posix.relative(baseDir, depModulePath);
//require('./title1');=>require('./src/title1.js');
node.arguments = [types.stringLiteral(depModuleId)]; // 给节点添加参数
//依赖的模块绝对路径放到当前的模块的依赖数组里
module.dependencies.push({depModuleId, depModulePath});
}
}
})
//
let {code} = generator(ast); // 编译代码
module._source = code;//模块源代码指向语法树转换后的新生成的源代码
//7. 再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
module.dependencies.forEach(({depModuleId, depModulePath}) => {
// 缓存
let depModule = this.modules.find(item => item.id === depModuleId);
if (depModule) {
depModule.extraNames.push(name);
} else {
let dependencyModule = this.buildModule(name, depModulePath); // 递归编译,依赖模块
this.modules.push(dependencyModule);
}
});
return module;
}
}
//
function getSource(chunk) {
return `
(() => {
var modules = ({
${chunk.modules.map(module => `
"${module.id}":(module,exports,require)=>{
${module._source}
}
`).join(',')
}
});
var cache = {};
function require(moduleId) {
var cachedModule = cache[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
var module = cache[moduleId] = {
exports: {}
};
modules[moduleId](module, module.exports, require);
return module.exports;
}
var exports = {};
(() => {
${chunk.entryModule._source}
})();
})()
;
`
}
//加载文件
function tryExtensions(modulePath, extensions) {
extensions.unshift('');
for (let i = 0; i < extensions.length; i++) {
let filePath = modulePath + extensions[i];// ./title.js
// 如果有就返回
if (fs.existsSync(filePath)) {
return filePath;
}
}
throw new Error(`Module not found`);
}
module.exports = Complication
这里比较重要的两个函数就是build和buildModule
所以我们只要了解了这两个函数工作流程,就会明白Complication 类的作用
下面我们来看下这两个函数的流程图
8.build函数工作流程解析
接受参数:callback
9.buildModule函数工作流程解析
相信你看到这里,对webpack执行流程和内部实现原理有了一定了解 欢迎小伙伴们一起讨论~