简介
以下仅为个人粗略总结和代码,看不懂的稍加理解,本文主要用做个人记录。
先大致总结一下
- 1.从哪里开始:webpack根据入口模块开始。
- 2.如何进行:递归读取每个文件,会形成一个依赖列表,依赖列表的,依赖列表是一个以文件相对路径为key,文件内容为value的对象。
- 3.如何处理:对于每个文件会通过AST解析语法树,返回源码。
- 4.loader在哪:loader是何时何地进行处理?它是在读取文件的时候开始起作用,通过正则匹配文件是否需要处理。然后读取配置文件的loader配置,然后通过递归的方式处理文件。
- 5. Plugins呢:plugins是在合适的时机开始工作,那么这个合适时机如何控制呢,是通过tapable事件流机制,实现发布订阅模式。
- 6.最后:最后返回的是一个匿名自执行函数,定义了一个webpack__require方法,解析传入的依赖列表,递归执行。
然后看下代码
核心代码如下:
//入口文件webpack.js
#! /usr/bin/env node
//第一步:找到当前执行命令的路径,拿到webpack.config.js
let path = require("path")
let config = require(path.resolve('webpack.config.js'))
console.log(path.resolve(),'resolve--------------->')
let Compiler = require('../lib/Compiler')
let compiler = new Compiler(config)
//标识运行编译
compiler.run()
//Compiler.js
const fs = require('fs')
const path = require('path')
const babylon = require('babylon')
const travere = require('@babel/traverse').default
const t = require('@babel/types')
const generator = require('@babel/generator').default
const ejs = require('ejs')
const {SyncHook} = require('tapable')
//babylon把源码转为AST
//@babel/traverse
//@babel/generator
//@babel/types
class Compiler {
constructor(config) {
this.config = config //保存入口文件路径
this.entryID = '' //主模块入口路径
this.modules = {} //存放模块依赖关系
this.entry = config.entry //入口路径
this.root = process.cwd() //当前工作目录
this.hooks = {
entryOption: new SyncHook(),
compile: new SyncHook(),
afterCompile: new SyncHook(),
afterPlugins: new SyncHook(),
run: new SyncHook(),
emit: new SyncHook(),
done: new SyncHook(),
}
//如果传递了plugins参数
let plugins = this.config.plugins
if(Array.isArray(plugins)) {
plugins.forEach(plugin => {
plugin.apply(this)
})
}
}
run() {
//执行,并创建模块的依赖关系
this.buildModule(path.resolve(this.root, this.entry), true)
//发射一个文件,就是打包后的文件
this.emitFile()
}
//构建模块
buildModule(modulePath, isEntry) {
//首先读取入口文件
let source = this.getSource(modulePath)
//模块的ID = this.root - modulePath
let moduleName = './' + path.relative(this.root, modulePath)
if(isEntry) {
this.entryID = moduleName //保存入口名字
}
//解析,需要把source源码进行改造,返回一个依赖列表
let {sourceCde, dependencies } = this.parse(source, path.dirname(moduleName))
this.modules[moduleName] = sourceCde
dependencies.forEach(dep => { //附模块的递归加载
this.buildModule(path.join(this.root, dep), false)
})
}
//解析源码, AST解析语法树
parse(source, parentPath) {
// console.log(source, parentPath)
let ast = babylon(source)
let dependencies = [] //依赖数组
travere(ast, {
CallExpression() {
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) //'src/a.js'
dependencies.push(moduleName)
node.arguments = [t.stringLiteral(moduleName)]
}
}
})
let sourceCode = generator(ast).code
return {sourceCode, dependencies}
}
//公用读文件的方法
getSource(modulePath) {
let content = fs.readFileSync(modulePath, 'utf8')
let rules = this.config.module.rules //拿到规则
//拿到每个规则来处理
for(let i=0; i<rules.length; i++) {
let rule = rules[i]
let { test, use } = rule
let len = use.length - 1
if(test.test(modulePath)) { //这个模块需要通过loader转换
function normalLoader() {
let loader = require(use[len]) //获取对应loader函数
content = loader(content)
//递归调用loader
if(len >= 0) {
normalLoader()
}
}
normalLoader()
}
}
return content
}
//发射文件
emitFile() {
//用数据 渲染我们的
//拿到输出到哪个目录下
let main = path.join(this.config.output.path, this.config.output.filename)
//模板路径
let templateStr = this.getSource(path.join(__dirname, 'main.ejs'))
let code = ejs.render(templateStr, {entryId: this.entryID, modules: this.modules})
this.assets = {
//资源中路径对应的代码
}
this.assets[main] = code
fs.writeFileSync(main, this.assets[main])
}
}
module.exports = Compiler