编写简易版本webpack
为了加深对webpack打包流程的理解,手写了一版简易的webpack, 当然是跟着教学视频手敲了几遍了哈
编写前知识准备
- ejs: 使用ejs模板来直接套用webpack打包后生成的文件;
- node: path, fs等node核心模块知识储备;
- tapable: tapable 的hook使用;
- npm: npm link命令,将npm 模块链接到对应的运行项目中去,方便地对模块进行调试和测试
- babel: 使用babylon,@babel/traverse,@babel/types,@babel/generator来解析遍历AST,模拟生成webpack构建后的代码(,还能趁机扫盲下babel原理的一些,以后再也不怕面试官问: babel是如何将ES6转换成ES5的呢?) 话不多说 开始上代码吧!
my_webpack目录构建
npm link 和package.json bin说明
在本地开发npm模块的时候,我们可以使用npm link命令,将npm 模块链接到对应的运行项目中去,方便地对模块进行调试和测试
1.创建bin文件夹, 编写对应的模块
2.在package.json中配置bin:{} key:value形式, value为相对于package.json的bin路径
"bin": {
"my_webpack": "./bin/my_webpack.js"
}
3. 在该模块中(bark_webpack)使用npm link 将 编写的模块连接到全局
4. 在需要使用的模块中 npm link webpack_mine 将全局的webpack_mine 映射到项目本地
./bin/my_webpack.js
#! /usr/bin/env node
// 告诉npm执行环境是node
/**
* webpack构建阶段
* 1.初始化阶段: 读取shell等配置文件,合并配置参数,加载Plugin,实例化Compiler,执行run方法开始编译,
* 2.编译阶段: 确定入口, 从入口触发,针对每一个module串行调用loader去翻译内容,再找到该module依赖的的module,递归进行编译内容
* 3.输入阶段: 对编译完成的module组合成Chunk,把Chunk转换成文件,输出到指定目录
*/
/**
* webpack 构建流程:
* 1.初始化参数: 读取配置,合并参数,
* 2.开始编译:根据参数示例话Compiler,执行run方法准备编译,
* 3.确定入口: 确定配置中的entry入口
* 4.编译模块: 从入口触发,调用所有的loader配置,对模块进行翻译,再找出该模块依赖的模块,递归此步骤,直到所有模块翻译完成
* 5.完成编译: 得到每个模块最终的翻译内容和依赖关系
* 6.输出资源: 根据入口和模块之间的依赖关系,组装成一个个包含多个模块的Chunk,把每个Chunk转换成一个单独文件加入到输出列表,
* 7.输入完成: 确定好输出内容后,根据output配置,确定输入路径和文件名,把文件内容写入到文件系统
*
*/
const path = require('path')
// 第一步: 初始化参数
const config = require(path.resolve('webpack.config.js'))
console.log('1.初始化参数')
// 第二步: 实例化Compiler, 执行 run方法
const Compiler = require('../lib/Compiler')
console.log('2.实例化Compiler')
const compiler = new Compiler(config)
compiler.hooks.entryOption.call()
compiler.run()
./lib/Compiler
const path = require('path')
const fs = require('fs')
const ejs = require('ejs')
const { SyncHook } = require('tapable')
//引入babel构建AST依赖
const babylon = require('babylon')
const traverse = require('@babel/traverse').default
const t = require('@babel/types')
const generator = require('@babel/generator').default
class Compiler {
constructor(config) {
this.config = config; // 配置参数
this.root = process.cwd() // 获取到执行环境的绝对路径
// 第三步: 确定入口
this.entry = config.entry
this.entryModuleId;
// 保存所以模块: 缓存
this.modules = {}
// webpack内部实现了三个阶段的一些hook钩子,这里列举一些
this.hooks = {
entryOption: new SyncHook(),
compile: new SyncHook(),
afterCompile: new SyncHook(),
afterPlugins: new SyncHook(),
run: new SyncHook(),
emit: new SyncHook(),
done: new SyncHook()
}
// 初始化Compiler之前要加载所有的plugins
let plugins = this.config.plugins
if (Array.isArray(plugins)) {
plugins.forEach(plugin => {
// webpack规定每个plugin 都要提供apply方法
plugin.apply(this)
})
}
this.hooks.afterPlugins.call()
}
getSource(modulePath) {
let content = fs.readFileSync(modulePath, 'utf8')
// 使用loader翻译内容
let rules = []
if (this.config.module && this.config.module.rules && Array.isArray(this.config.module.rules)) {
rules = this.config.module.rules
}
rules.forEach(rule => {
const { test, use } = rule
// 获取配置use的最后一个索引loader
let len = use.length - 1
if (test.test(modulePath)) {
// 根据配置的loader递归解析
function mormalLoader() {
let loader = require(use[len--])
content = loader(content)
if (len >= 0) {
mormalLoader()
}
}
mormalLoader()
}
})
return content
}
parse(source, parentPath) {
// 这里要解析源码构建ast,生成webpack最终的编译代码
let ast = babylon.parse(source)
// 最终的依赖数组
let dependencies = []
// 遍历AST,修改接点
traverse(ast, {
CallExpression(p) {
// 获取树接点
let node = p.node
// 将require 转换 __webpack_require__
if (node.callee.name === 'require') {
node.callee.name = '__webpack_require__'
// require('./a'), 判断,需要给a添加后缀
let moduleName = node.arguments[0].value
moduleName = moduleName + (path.extname(moduleName) ? '' : '.js') // ./a.js
// 添加父路径
moduleName = './' + path.join(parentPath, moduleName) // ./src/a.js
dependencies.push(moduleName)
// 修改AST的arguments
node.arguments = [t.stringLiteral(moduleName)]
}
}
})
// 根据修改后的AST生成代码
let sourceCode = generator(ast).code
return { sourceCode, dependencies }
}
buildModule(modulePath, isEntry) {
/* 第四步: 4.编译模块: 从入口触发,调用所有的loader配置,对模块进行翻译,
再找出该模块依赖的模块,递归此步骤,直到所有模块翻译完成
*/
// 获取翻译后的文件内容
let source = this.getSource(modulePath)
console.log('4.编译模块')
//现在要构成webpack的依赖关系
/*
{
'./src/index.js': eval(..),
'./src/a/a.js':eval(...)
}
*/
// 模块相对路径: './' + 模块绝对路径 - 工作环境路径
let moduleId = './' + path.relative(this.root, modulePath)
if (isEntry) {
// 保存入口文件moduleId
this.entryModuleId = moduleId
}
// 第五步: 5.完成编译: 得到每个模块最终的翻译内容和依赖关系
const { sourceCode, dependencies } = this.parse(source, path.dirname(moduleId))
// 依赖关系
this.modules[moduleId] = sourceCode;
// 根据模块内的依赖关系递归构建子模块
(dependencies || []).forEach(dep => {
// 已经不是入口了, 标记为false
this.buildModule(path.join(this.root, dep), false)
})
console.log('5.完成编译得到依赖关系', this.modules)
}
emitFile() {
console.log('6.输出资源,组装Chunk')
// 根据配置拿到output.path
const outputPath = path.join(this.config.output.path, this.config.output.filename)
// 使用ejs来模拟
let templateStr = this.getSource(path.resolve(__dirname, 'main.ejs'))
const code = ejs.render(templateStr, { entryId: this.entryModuleId, modules: this.modules })
this.assets = {}
this.assets[outputPath] = code
console.log('6.输出完成,写入文件')
fs.writeFileSync(outputPath, this.assets[outputPath])
}
run() {
this.hooks.run.call()
console.log('2.执行run方法')
// 第三步: 确定入口路径
this.hooks.compile.call()
const modulePath = path.resolve(this.root, this.entry)
console.log('3.确定入口路径', modulePath)
this.buildModule(modulePath, true);
this.hooks.afterCompile.call()
this.hooks.emit.call()
this.emitFile()
this.hooks.done.call()
}
}
module.exports = Compiler
./lib/main.ejs
(function (modules) {
// 缓存模块
var installedModules = {};
// 创建 requie函数,类似于node中的require
function __webpack_require__(moduleId) {
// 执行前检查要导入的模块是否在缓存中,存在则直接从缓存中导出模块
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// 缓存中不存在,创建模块并且添加到缓存中
var module = installedModules[moduleId] = {
i: moduleId, // 模块路径 ./src/a.js
l: false, // 模块是否执行
exports: {} // 模块
};
// 从 modules 中获取 index 为 moduleId 的模块对应的函数
// 再调用这个函数,同时把函数需要的参数传入
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// 修改标志位,标记已执行
module.l = true;
// 返回这个模块的导出值
return module.exports;
}
// Webpack 配置中的 publicPath,用于加载被分割出去的异步代码
__webpack_require__.p = "";
// 从入口加载
return __webpack_require__(__webpack_require__.s = `<%- entryId%>`);
})
({
<%for(let key in modules){%>
"<%-key%>": (function (module, exports, __webpack_require__) {
eval(`<%- modules[key]%>`);
}),
<%}%>
});
./package.json
{
"name": "bark_webpack",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"bin": {
"my_webpack": "./bin/my_webpack.js"
},
"dependencies": {
"@babel/generator": "^7.12.5",
"@babel/traverse": "^7.12.9",
"@babel/types": "^7.12.7",
"babylon": "^6.18.0",
"ejs": "^3.1.5",
"tapable": "^2.1.1"
}
}