步骤
01 从入口文件开始查找所有的依赖模块
- 读取代码内容
- 读取模块文件相对路径
- 读取模块中子依赖包,首先需要解析当前模块
- 解析结果,是否存在子依赖包dependcies, 解析的源代码sourceCode
- 代码解析:vue---> html css js es6 -> es5
02 模块解析
- 使用ast语法树解析: astexplorer.net
- 将代码中的require修改__webpack_require__
- 将require("./jd") -> require('./jd.js')
- 收集dependencies
03 打包输出
- 使用模版生成bundle.ejs,传入的参数必须是动态的
- 模版express ejs
- 使用fs将生成的文件写入bundle.js
04 执行手写loader
- less sass vue ...
- 作用:转化,less->css vue->js、html、css
- 自定义loader less-loader style-loader
05 执行手写plugin
- 代码加工:压缩、合并、混淆
- 通过tapable--发布订阅处理plugin事件流程
- 在特定的生命钩子函数中执行相应功能
- 需要一个固定的apply方法,会在编译器中调用
打包核心代码
配置文件:my-webapck.config.js
const path =require('path')
const myPlugin = require("./plugin/myplugin")
module.exports = {
entry: '../jd-pack/src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, "dist")
},
module: {
rules: [
{
test: /\.less$/,
use: [
path.resolve(__dirname,'loader', 'style-loader'), // 自定义loader
path.resolve(__dirname, 'loader', 'less-loader')
]
}
],
plugins: [
new myPlugin() // 自定义plugin
]
}
}
bundle模版文件:
(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;
}
__webpack_require__.m = modules;
__webpack_require__.c = installedModules;
return __webpack_require__(__webpack_require__.s = "<%=entryId%>");
})
({
<%for(let key in modules) {%>
"<%-key%>": (function(module, exports, __webpack_require__) {eval(`<%-modules[key]%>`);}),
<%}%>
});
入口文件:
#! /usr/bin/env node
const path= require('path')
const config = require(path.resolve('my-webapck.config.js'))
const Compiler = require('../lib/Compiler')
const compiler = new Compiler(config)
// 编译
compiler.run()
打包:
const path = require('path')
const fs = require('fs')
const ejs = require('ejs')
const types = require('@babel/types')
const babylon = require('babylon')
const traverse = require('@babel/traverse').default
const generator = require('@babel/generator').default
const {SyncHook} = require('tapable') // 发布订阅模式
// babylon @babel/traverse @babel/types @babel/generator
// 编译器
class Compiler {
constructor(config) {
this.config = config
this.entry = config.entry
this.entryId;// 存储入口文件
// 当前目录
this.root = process.cwd()
this.modules = {} // 保存所有模块
// 添加钩子
this.hooks = {
entryOption: new SyncHook() , // 开始
compile: new SyncHook(), // 编译
afterCompile: new SyncHook(), // 编译后
run: new SyncHook(), // 运行
emit: new SyncHook(), // 发射
done: new SyncHook() // 完成
}
// 获取配置文件中的plugins,并执行applay函数
let plugins = this.config.module.plugins
if(Array.isArray(plugins)) {
plugins.forEach(p => {
p.apply(this)
})
}
}
// 模块文件解析 source--文件内容 parentPath--文件目录
parse(source, parentPath) {
const ast = babylon.parse(source, {
// 以严格模式解析并允许模块声明
sourceType: "module",
})
let dependencies = [] // 子模块依赖
traverse(ast, {
CallExpression(p) {
let node = p.node
if(node.callee.name === 'require') {
node.callee.name = "__webpack_require__"
let moduleName = node.arguments[0].value
moduleName = moduleName + (path.extname(moduleName) ? "" : ".js")
moduleName = "./" + path.join(parentPath,moduleName)
dependencies.push(moduleName)
// 将更新后的子模块依赖名写回去
node.arguments = [types.stringLiteral(moduleName)]
}
}
})
let sourceCode = generator(ast).code
console.log(sourceCode,dependencies)
return {sourceCode, dependencies}
}
getSource(modulePath) {
// for loader
let rules = this.config.module.rules
let content = fs.readFileSync(modulePath,'utf-8')
for(let i = 0; i<rules.length; i++) {
let rule = rules[i]
let {test, use} = rule
let len = use.length - 1 // loader的总长度
// 获取配置文件中的loader,传入文件内容并执行loader函数
if(test.test(modulePath)) {
function normalLoader() {
let loader = require(use[len--])
content = loader(content)
if(len >=0) {
normalLoader()
}
}
normalLoader()
}
}
return content
}
// 从root节点找所有的依赖模块
// modulePath模块文件路径
// isEntry--是否是入口
buildModule(modulePath, isEntry) {
let source = this.getSource(modulePath)
let moduleName = "./" + path.relative(this.root, modulePath)
if(isEntry) {
this.entryId = moduleName
}
let {sourceCode,dependencies } = this.parse(source,path.dirname(moduleName))
this.modules[moduleName] = sourceCode
// 递归
dependencies.forEach(dep => {
this.buildModule(path.join(this.root,dep), false)
});
}
//生成打包文件
emitFile() {
let main = path.join(this.config.output.path, this.config.output.filename)
let templateStr = this.getSource(path.join(__dirname,"bundle.ejs")) // 拿到模版内容
let result = ejs.render(templateStr, {entryId:this.entryId, modules:this.modules})
this.assets = {}
this.assets[main] = result // 文件全名-文件内容
fs.writeFileSync(main, this.assets[main])
}
run() {
this.hooks.run.call()
this.hooks.compile.call() // 编译
this.buildModule(path.resolve(this.root,this.entry), true)
this.hooks.afterCompile.call() // 编译完成
this.hooks.emit.call()
this.emitFile()
this.hooks.done.call() // 完成
}
}
module.exports = Compiler
手写loader
style-loader:
function loader(sourceCss) {
let style =`
let style = document.createElement('style')
style.innerHTML = ${JSON.stringify(sourceCss)}
document.head.appendChild(style)
`
return style
}
module.exports = loader
less-loader:
const less = require('less')
function loader(sourceLess) {
let css = ""
less.render(sourceLess, function(err, res) {
css = res.css
})
css = css.replace(/\n/g, '\\n')
return css
}
module.exports = loader
手写plugin
class MyPlugin {
apply(compiler) {
console.log('start')
// 注册订阅
compiler.hooks.emit.tap('emit', function() {
console.log('emit')
})
compiler.hooks.done.tap('done', () => {
console.log('打包结束')
})
}
}
module.exports = MyPlugin