乐趣区

webpack机制

简介
以下仅为个人粗略总结和代码,看不懂的稍加理解,本文主要用做个人记录。
先大致总结一下

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

退出移动版