# 实现一个mini-webpack
本来记录自己实现一个mini-webpack
时候遇到的坑和总结webpack
的大致流程
# 初始化
我们需要写一个webpack
的包,首先需要初始化一下项目
npm init -y
然后我们配置一下package.json
的bin
选项
// package.json
"bin": {
"cy-pack": "./bin/cy-pack.js"
},
然后我们需要新建一个bin
文件夹和新起一个cy-pack.js
文件,我们看下文件的内容
#! /usr/bin/env node
// 拿到当前执行名的路径 拿到webpack.config.js
const path = require('path')
const config = require(path.resolve(process.cwd(), 'webpack.config.js'))
const Compiler = require('./lib/Compiler.js')
const compiler = new Compiler(config)
compiler.run()
#! /usr/bin/env node
表示当前运行环境是node环境,下面就是对应的代码,这里不做解释了
最后通过命令npm link
将这个包映射到全局npm上去,在另外一个文件夹使用我们的包的时候只需要npm link cy-pack
就好啦
# 手写一个最简单的打包工具
// Compiler.js
const path = require('path')
const fs = require('fs')
const babylon = require('babylon') //将源码转化为AST
const traverse = require('@babel/traverse').default
const t = require('@babel/types')
const generator = require('@babel/generator').default
const ejs = require('ejs')
class Compiler {
constructor (config) {
this.config = config
this.entryId //入口文件的路径
this.modules = {} // 所有依赖的模块
this.entry = config.entry // 入口路径
this.root = process.cwd() //当前工作路径
}
getSource (modulePath) {
return fs.readFileSync(modulePath, 'utf8')
}
parse (source, parentPath) { // 靠AST解析语法树解析源码
const ast = babylon.parse(source)
const dependencies = [] //依赖的数组
traverse(ast, {
CallExpression(p) {
let node = p.node
if (node.callee.name === 'require') { //找到require
node.callee.name = '__webpack_require__'
let moduleName = node.arguments[0].value //取到require引入的名字
moduleName = moduleName + (path.extname(moduleName) ? '' : '.js') //如果没有后缀就补充.js
moduleName = './' + path.join(parentPath, moduleName)
dependencies.push(moduleName)
node.arguments = [t.stringLiteral(moduleName)]
}
}
})
let sourceCode = generator(ast).code
return { sourceCode, dependencies }
}
buildModule (modulePath, isEntry) { // 构建模块
const source = this.getSource(modulePath) //拿到模块的内容
const moduleName = './' + path.relative(this.root, modulePath)
if (isEntry) { //如果是根模块
this.entryId = moduleName
}
const { sourceCode, dependencies } = this.parse(source, path.dirname(moduleName))
dependencies.forEach(dep => { //递归加载
this.buildModule(path.join(this.root, dep), false)
})
this.modules[moduleName] = sourceCode
}
emitFile () { //发射打包后的文件
// 用数据渲染我们的模板 然后输出到对应的文件去
const main = path.join(this.config.output.path, this.config.output.filename)
const templateStr = this.getSource(path.join(__dirname, 'main.ejs'))
const code = ejs.render(templateStr, { //将数据传入ejs 然后得到代码块
entryId: this.entryId,
modules: this.modules
})
this.assets = {}
this.assets[main] = code
fs.writeFileSync(main, this.assets[main])
}
run () {
// 执行并创建模块的依赖关系
this.buildModule(path.resolve(this.root, this.entry), true)
this.emitFile()
}
}
module.exports = Compiler
我们来总结一下基本的流程
通过在使用我们包的目录使用
npx cy-pack
运行我们的打包工具,然后我们可以拿到当前执行这个命令的路径根据这个路径我们可以拿到
webpack.config.js
,并将对应的数据进行存储然后调用
buildModule
创建我们的模块关系,主要思路是通过配置中的入口路径读取到入口文件的内容,然后调用bodylon.parse
将读到的内容转化为AST, 然后通过traverse
找到代码中的require
,将他替换成我们自己实现的__webpack_require__
,然后替换对应require
中的路径,最后生成依赖表单就好了(依赖表单是处理过的路径)然后通过遍历依赖表单调用
buildModule
方法递归处理完全部依赖的文件最后可以通过ejs模板等方式将数据写入模板中,最后输出到配置的
output
对应的路径和文件就好了
我们来看下最基本的模板
// main.ejs
(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;
}
return __webpack_require__(__webpack_require__.s = "<%-entryId%>");
})
({
<%for(let key in modules){%>
"<%-key%>":
(function (module, exports, __webpack_require__) {
eval(`<%-modules[key]%>`);
}),
<%}%>
});