最近在通过对webapck的学习,自己手写实现一个mini-webpack,特地在此进行记录。
我们首先需要准备一份待打包的文件进行测试。
文件夹目录如下
loader
| less-loader.js
| style-loader.js
plugins
| p1.js
src
| base
| b.js
| a.js
| index.js
| index.less
webpack.config.js
入口文件index.js
require('./index.less')
let a = require('./a.js') // module.exports = 'a'
let b = require('./base/b.js') // module.exports = 'ab'
console.log(a+b) // abb
loader文件
less-loader.js:
const less = require('less')
function loader (content){
let css = ''
less.render(content, function(e,code){
css = code.css
})
//这里由于我们在写css时会换行,需要对换行进行转义。
css = css.replace(/\n/g,'\\n')
return css
}
module.exports = loader
-----------------------------------
style-loader.js:
function loader(content){
let style = `
let style = document.createElement('style')
style.innerHTML = ${JSON.stringify(content)}
document.head.appendChild(style)
`
return style
}
module.export = loader
webpack.config.js:
const path = require('path')
const P = require('./plugins/P1')
module.exports = {
entry:'./src/index.js',
output:{
path: path.resolve(__dirname,'dist'),
filename: 'main.js'
},
mode:'development',
module:{
rules:[
{
test:'/\.less$/',
use:[
path.resolve(__dirname,'loader','style-loader.js'),
path.resolve(__dirname,'loader','less-loader.js')
]
}
]
}
}
新建文件夹hd-pack写我们自己的mini-webpack工具。
1、npm init -y
2、bin
| hd-pack.js
lib
| Compiler.js
| main.ejs
3、连接:当前目录下npm link
4、在待打包的文件目录下,npm link hd-pack
这个时候,待后面hd-pack文件中的代码写完整,直接可以利用npx hd-pack打包文件了。
hd-pack.js
#! /usr/bin/env node
// 使用node环境
const fs = require('fs')
const path = require('path')
const Compiler = require('../lib/Compiler.js')
let config = require(path.resolve('webpack.config.js'))
// webpack的配置文件
// webpack会先创建一个compiler实例并调用run方法。
let compiler = new Compiler(config)
compiler.run()
webpack打包实现的原理都在Compiler.js中。
const fs = require('fs')
const path = require('path')
// 文件解析成AST
const babylon = require('babylon')
const t = require('@babel/types')
// AST中收集依赖
const traverse = require('@babel/traverse').default;
const generator = require('@babel/generator').default;
const ejs = require('ejs')
//首先Compiler导出的是一个类
class Compiler {
constructor(config){
// 配置信息
this.config = config
// 相对路径
this.entryId = ''
// 入口文件
this.entry = config.entry
// 依赖收集
this.modules = {}
// 当前的工作路径
this.root = process.cwd()
}
run(){
// 收集依赖
this.buildModules(path.resolve(this.root,this.entry),true)
// 生成打包文件
this.emitFile()
}
// 在读取文件时,由于webpack只能处理js文件,所以此时需要用loader对非js文件进行处理。
getCode(filePath){
let content = fs.readFileSync(filePath,'utf8')
// 获取配置中的loaders
let rules = this.config.modules.rules
rules.forEach((e,i)=>{
let {test,use} = e
if(test.test(filePath)){
// 如果正则检验成功,则进行loader处理
// 注意一点:loader的执行顺序是从上到下,所以需要倒序处理
use = use.reverse()
use.forEach((e,i)=>{
let loader = require(e)
content = loader(content )
})
}
})
return content
}
// 参数:绝对路径、是否是入口文件
buildModules(pathAbs,isEntry){
// 读取文件内容
let content = this.getCode(pathAbs)
// 得到一个相对路径--可以用正常的webpack打包看看,里面都是用相对路径作为key。
let moduleName = './' + this.relative(this.root,pathAbs)
if(isEntry){
this.entryId = isEntry
}
let res = this.parse(content,path.dirname(moduleName))
this.modules[moduleName] = res.code
// 递归解析所有依赖
res.deps.forEach((e,i)=>{
this.buildModules(path.resolve(this.root, e),false)
})
}
parse(code,parentPath){
const AST = babylon.parser(code)
// 存储被引用的相对路径
let dep = []
traverse(AST,{
CallExpression({node}){
// callee:对应于函数的名字。caller:函数的参数
if(node.callee.name==='require'){
node.callee.name='__webpack_require__'
// 得到模块的路径--即为key值
let moduleName = node.arguments[0].value
// 判断是否有扩展名
moduleName += path.extname(moduleName) ? '' : '.js'
// 改为相对路径
moduleName = './' + path.join(parentPath, moduleName)
deps.push(moduleName)
node.arguments = [t.stringLiteral(moduleName)]
}
}
})
let code = generator(AST).code
return {code,deps}
}
emitFile(){
// 目标文件
let main = path.join(this.config.output.path, this.config.output.filename)
// 模板文件:由webpack打包出来并作一定修改。
let temp = fs.readFileSync(path.join(__dirname, 'main.ejs'), 'utf8')
// 模块渲染,传参
let tempCode = ejs.render(temp, { entryId: this.entryId, modules: this.modules })
this.assets = {}
this.assets[main] = tempCode
// console.log(tempCode);
fs.writeFileSync(main, this.assets[main])
}
}
module.exports = Compiler
注意事项:
1、在mini-webpack的package.json中需要加上"bin": {"hd-pack": "./bin/hd-pack.js"}字段;
2、webpack中loader的职责应遵守单一原则,即一个loader只完成一个职责,这里less-loader实际上还对css进行了处理;
3、plugin问题:本质上一个类,其就是利用tapable发布订阅原理,在打包的各个阶段进行发布事件。
ps:
简单实现了一个mini-webpack,但是还有很多webpack知识点未曾涉及到,如果有疑问,欢迎留言讨论。