这是一篇非常基础的打包原理
开始前准备
- 先来看个目录结构

还是图片好,目录树可真丑
- 写一个webpack的基础配置,只需要主入口和输出配置
/* webpack.config.js */
module.exports = {
'entry': './src/index.js',
'output': {
'filename': 'index.js',
'path': path.resolve(__dirname, 'bundle')
}
}
- 主入口是index.js,引入了a.js和b.js两个文件
/* src/index.js */
const name = require('./a.js')
const age = require('./b/b.js')
console.log('index.js')
console.log(name, age)
/* src/a.js */
module.exports = 'Rskmin'
/* src/b/b.js */
module.exports = 19
开始
从打包好的文件入手,我去除了这次用不到的部分。只留下了最重要的__webpack_require__方法
(function (modules) {
// 模块
var installedModules = {};
function __webpack_require__(moduleId) {
// 1.判断是否有该模块的缓存
if (installedModules[moduleId]) {// 如果有就直接返回缓存内容
return installedModules[moduleId].exports;
}
// 2.如果没有就创建这个模块的缓存
var module = installedModules[moduleId] = {
i: moduleId,
// 缓存状态
l: false,
// 缓存内容
exports: {}
};
// 3.执行模块内部表达式,获取计算结果并存入缓存。同时传入`__webpack_require__`方法,用于本模块引入其他模块
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// 成功后将缓存状态置`true`
module.l = true;
// 返回模块返回值
return module.exports;
}
return __webpack_require__("./src/index.js");
})
({
"./src/index.js":
(function (module, exports, __webpack_require__) {
const name = __webpack_require__("./src/a.js");
const age = __webpack_require__("./src/b/b.js");
console.log('index.js');
console.log(name, age);
}),
"./src/a.js":
(function (module, exports, __webpack_require__) {
module.exports = 'Rskmin';
}),
"./src/b/b.js":
(function (module, exports, __webpack_require__) {
module.exports = 19;
}),
})
-
可以看到webpack生成了一个立即执行的表达式,并把打包好的一个个文件模块装入对象,作为参数传入表达式
-
在表达式的最底部调用了
__webpack_require__方法,执行了主模块的内容,主模块中对其他模块的引入也是调用了这个方法
稍微做个笔记:模块内容即一段程序,程序的目的就是计算并给出结果,缓存就是缓存这个结果
关注点来到打包好的模块,打包的目的就是生成这些模块
- 每个模块都生成了一个键值对
key是文件的相对路径(相对于配置文件)value是将模块的内容装入了一个表达式- 模块内对其他模块的引入从
require()变成了__webpack_require__,路径也变成了相对于配置文件的路径(这样就可以和key对应)
分析结束,开始处理模块
- 把打包功能封装成一个功能对象,该对象用来打包各个模块
/* lib/Complier.js */
class Complier {
constructor(config) {
// 想要打包必须知道入口和出口,这个就是webpack的配置文件
this.config = config
}
}
/* bin/pack */
const path = require('path')
const Complier = require('../lib/Complier')
// 引入配置文件,创建打包对象传入配置文件
const configPath = path.resolve(process.cwd(), 'webpack.config.js')
const config = require(configPath)
const cp = new Complier(config)
// 调用打包方法开始打包
cp.run()
process.cwd() 传送门
run方法,首先处理模块内容
/* lib/Complier.js */
class Complier {
constructor(config) {
this.config = config
// 保存模块的依赖
this.modules = {}
}
run() {
// 从入口开始构建模块
this.buidModule(this.config.entry)
}
buidModule(modulePath) {
// 拿到主模块代码
let code = this.getSource(modulePath)
// ***处理当前模块的代码***
let { resultCode, dependencies } = this.parseModule(code) // <--进入
// 将主模块的路径和代码保存到modules中(每个模块都生成了一个键值对)
this.modules[modulePath] = resultCode
// 处理依赖模块,递归构建
dependencies.forEach(depPath => {
this.buidModule(depPath)
})
}
getSource(modulePath) {
return fs.readFileSync(modulePath, 'utf8')
}
parseModule(code) {}
}
拎出parseModule,读取了入口文件后要做一件事
把模块内的require替换成__webpack_require__,路径替换成相对于配置文件的路径
用正则替换很大概率影响到模块内其他内容,所以用抽象语法树解析,然后替换内容(这里使用babel转化抽象语法树)
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const generate = require('@babel/generator').default
const t = require('@babel/types')
parseModule(code) {
// 将当前模块的代码转换成抽象语法树
let ast = parser.parse(code)
// 定义变量保存主模块地址
let rootPath = path.dirname(this.config.entry)
// 定义数组保存当前模块所有的依赖,用于继续递归解析模块
let dependencies = []
// 遍历抽象语法树修改抽象语法树中的内容
traverse(ast, {
CallExpression(nodePath) {
let node = nodePath.node
if (node.callee.name === 'require') {
// 将require修改为__webpack_require__
node.callee.name = '__webpack_require__'
// 修改require导入的路径
let modulePath = node.arguments[0].value
modulePath = '.\\' + path.join(rootPath, modulePath)
modulePath = modulePath.replace(/\\/g, '/')
dependencies.push(modulePath)
// 创建新的stringLiteral结点替换掉旧的
node.arguments = [t.stringLiteral(modulePath)]
}
}
})
// 将修改之后的抽象语法树转换成代码
let resultCode = generate(ast).code
// 返回结果
return { resultCode, dependencies }
}
总之就干了替换这件事
解析结束后Complier.modules中就保存的你要的key和value
把正确的东西置于正确的位置(用模板引擎生成最后的代码)
模板
/* lib/main.ejs */
(function (modules) {
var installedModules = {};
function __webpack_require__(moduleId) {
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
module.l = true;
return module.exports;
}
return __webpack_require__("<%-entryId%>");
})
({
<% for(let key in modules) {%>
"<%-key%>":
(function (module, exports, __webpack_require__) {
<%-modules[key]%>
}),
<% } %>
})
最终的Complier类
const fs = require('fs')
const path = require('path')
const ejs = require('ejs')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const generate = require('@babel/generator').default
const t = require('@babel/types')
class Complier {
constructor(config) {
this.config = config
this.modules = {}
}
run() {
this.buidModule(this.config.entry)
// 利用模板引擎生成最终代码
this.emitFile()
}
buidModule(modulePath) {
let code = this.getSource(modulePath)
let { resultCode, dependencies } = this.parseModule(code)
this.modules[modulePath] = resultCode
dependencies.forEach(depPath => {
this.buidModule(depPath)
})
}
parseModule(code) {
let ast = parser.parse(code)
let rootPath = path.dirname(this.config.entry)
let dependencies = []
traverse(ast, {
CallExpression(nodePath) {
let node = nodePath.node
if (node.callee.name === 'require') {
node.callee.name = '__webpack_require__'
let modulePath = node.arguments[0].value
modulePath = '.\\' + path.join(rootPath, modulePath)
modulePath = modulePath.replace(/\\/g, '/')
dependencies.push(modulePath)
node.arguments = [t.stringLiteral(modulePath)]
}
}
})
let resultCode = generate(ast).code
return { resultCode, dependencies }
}
getSource(modulePath) {
return fs.readFileSync(modulePath, 'utf8')
}
emitFile() {
// 读取EJS模板
let templatePath = path.resolve(__dirname, 'main.ejs')
let template = fs.readFileSync(templatePath, 'utf8')
// 利用变量替换模板中的内容
let resultCode = ejs.render(template, { 'entryId': this.config.entry, 'modules': this.modules })
// 将最终的内容写入到文件中
let outputDir = this.config.output.path
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir)
}
const outputPath = path.resolve(outputDir, this.config.output.filename)
fs.writeFileSync(outputPath, resultCode)
}
}
module.exports = Complie
到这里就愉快的结束了
