本文也来自拉勾教育的课程学习笔记,旨在了解webpack打包流程
1、在终端输入npx webpack 或者 yarn webpack执行打包,这时候会找到并执行node_modules/.bin/webpack.cmd
2、执行webpack.cmd其实就是执行webpack/bin/webpack.js
3、在webpack/bin/webpack.js中会
require(webpack-cli/bin/cli.js)
复制代码
也就是执行cli.js这个文件,这个文件里面是个自调用函数,也就是开始webpack打包流程
4、在cli.js里面会先处理options和命令行里面的参数,也就是把用户设置的options和默认options以及命令行参数合并然后通过
const webpack = require("webpack");
复制代码
引入webpack/lib/webpack的导出webpack函数,然后执行这个函数
5、执行webpack(options)返回compiler,其中webpack函数步骤
-
实例化compiler对象
- 把options上的context挂到compiler实例上
- 初始化hooks对象,也就是初始化相应的hook(生命周期钩子),其中核心生命周期钩子包括:
entryOption -> beforeRun -> run -> beforCompile -> compile -> thisCompilation -> compilation -> make -> afterCompile -> emit
-
传入的options挂载到compiler上
-
初始化 NodeEnvironmentPlugin(让compiler具体文件读写能力)
new NodeEnvironmentPlugin().apply(compiler)
复制代码
-
将options上的piugin插件挂载到compiler上,也就是遍历执行plugin.apply方法
if (options.plugins && Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
plugin.apply(compiler)
}
}
复制代码
-
挂载webpack内置插件,执行entryOption钩子,将compilation.addEntry()方法执行挂载到make钩子上
// webpack.js
new WebpackOptionsApply().process(options, compiler);
复制代码
// WebpackOptionsApply.js
class WebpackOptionsApply {
// process的目的就是将compilation.addEntry()方法执行挂载到make钩子上
process(options, compiler) {
new EntryOptionPlugin().apply(compiler)
compiler.hooks.entryOption.call(options.context, options.entry)
}
}
复制代码
6、执行compiler.run方法
-
将参数callback赋值给变量finalCallback
-
定义变量onCompiled,在这里会执行emitAssets,也就是将处理好的 chunk 写入到指定的文件然后输出至dist
-
依次执行beforeRun、run生命周期钩子,并在run生命周期的done回调里执行compiler.compile方法
this.hooks.beforeRun.callAsync(this, (err) => {
this.hooks.run.callAsync(this, (err) => {
this.compile(onCompiled)
})
})
复制代码
7、执行compiler.compile方法
-
调用compiler.newCompilationParams初始化变量params
newCompilationParams() {
// 让params具备创建模块的能力
const params = {
normalModuleFactory: new NormalModuleFactory()
}
return params
}
复制代码
-
依次执行beforeCompile、compile生命周期钩子
-
执行完compile生命周期钩子后,调用compiler.newCompilation(params)初始化compilation,在newCompilation方法中也会执行thisCompilation,compilation生命周期钩子
- 在Compilation的构造函数中会给compilation挂载compiler….很多属性
// Compiler.js
newCompilation(params) {
const compilation = this.createCompilation()
this.hooks.thisCompilation.call(compilation, params)
this.hooks.compilation.call(compilation, params)
return compilation
}
createCompilation() {
return new Compilation(this)
}
// Compilation.js
class Compilation extends Tapable {
constructor(compiler) {
super()
this.compiler = compiler
this.context = compiler.context
this.options = compiler.options
// 让 compilation 具备文件的读写能力
this.inputFileSystem = compiler.inputFileSystem
this.outputFileSystem = compiler.outputFileSystem
this.entries = [] // 存入所有入口模块的数组
this.modules = [] // 存放所有模块的数据
this.chunks = [] // 存放当前次打包过程中所产出的 chunk
this.assets = []
this.files = []
this.hooks = {
succeedModule: new SyncHook(['module']),
seal: new SyncHook(),
beforeChunks: new SyncHook(),
afterChunks: new SyncHook()
}
}
复制代码
-
生成compilation后执行make生命周期钩子
- 就是在此处执行了entryOption钩子执行时往make钩子上挂载的compilation.addEntry方法,该方法接受了4个参数context, entry, name, callback
- addEntry里调用了compilation._addModuleChain方法
- _addModuleChain里调用了compilation.createModule方法,createModule执行顺序如下:
-
- 通过normalModuleFactory创建一个module,也就是用来加载入口文件的模块
-
- 初始化afterBuild方法,在这里会执行当前创建的模块的依赖模块加载
-
- 调用了compilation.buildModule(module, afterBuild),这里会调用module.build()方法
- 01 从文件中读取到将来需要被加载的 module 内容,这个
- 02 如果当前不是 js 模块则需要 Loader 进行处理,最终返回 js 模块
- 03 上述的操作完成之后就可以将 js 代码转为 ast 语法树
- 04 当前 js 模块内部可能又引用了很多其它的模块,因此我们需要递归完成
- 05 前面的完成之后,我们只需要重复执行即可
- 调用了compilation.buildModule(module, afterBuild),这里会调用module.build()方法
-
- 执行doAddEntry方法,就是把当前创建的入口文件模块推进compilation.entries中
-
- 把当前的入口文件模块推进compilation.modules中
-
- 执行依赖模块的加载,也就是上述步骤3-5
-
执行完make生命周期钩子后,执行make钩子的done回调,这里会执行compilation.seal()方法,它的具体谈执行如下
- 执行compilation里的seal和beforeChunks钩子
- 遍历compilation.entries,并将其中的每一个入口模块和它的依赖模块合并成一个chunk,并推入遍历compilation.chunks数组中
- 调用compilation.createChunkAssets生成最终代码
-
调用compilation.emitAssets也就是执行了compiler.run方法中的onCompiled
// Compilation.js
/**
* 完成模块编译操作
* @param {*} context 当前项目的根
* @param {*} entry 当前的入口的相对路径
* @param {*} name chunkName main
* @param {*} callback 回调
*/
addEntry(context, entry, name, callback) {
this._addModuleChain(context, entry, name, (err, module) => {
callback(err, module)
})
}
_addModuleChain(context, entry, name, callback) {
this.createModule({
parser,
name: name,
context: context,
rawRequest: entry,
resource: path.posix.join(context, entry),
moduleId: './' + path.posix.relative(context, path.posix.join(context, entry))
}, (entryModule) => {
this.entries.push(entryModule)
}, callback)
}
/**
* 定义一个创建模块的方法,达到复用的目的
* @param {*} data 创建模块时所需要的一些属性值
* @param {*} doAddEntry 可选参数,在加载入口模块的时候,将入口模块的id 写入 this.entries
* @param {*} callback
*/
createModule(data, doAddEntry, callback) {
let module = normalModuleFactory.create(data)
const afterBuild = (err, module) => {
// 在 afterBuild 当中我们就需要判断一下,当前次module 加载完成之后是否需要处理依赖加载
if (module.dependencies.length > 0) {
// 当前逻辑就表示module 有需要依赖加载的模块,因此我们可以再单独定义一个方法来实现
this.processDependencies(module, (err) => {
callback(err, module)
})
} else {
callback(err, module)
}
}
this.buildModule(module, afterBuild)
// 当我们完成了本次的 build 操作之后将 module 进行保存
doAddEntry && doAddEntry(module)
this.modules.push(module)
}
/**
* 完成具体的 build 行为
* @param {*} module 当前需要被编译的模块
* @param {*} callback
*/
buildModule(module, callback) {
module.build(this, (err) => {
// 如果代码走到这里就意味着当前 Module 的编译完成了
this.hooks.succeedModule.call(module)
callback(err, module)
})
}
processDependencies(module, callback) {
// 01 当前的函数核心功能就是实现一个被依赖模块的递归加载
// 02 加载模块的思想都是创建一个模块,然后想办法将被加载模块的内容拿进来?
// 03 当前我们不知道 module 需要依赖几个模块, 此时我们需要想办法让所有的被依赖的模块都加载完成之后再执行 callback?【 neo-async 】
let dependencies = module.dependencies
async.forEach(dependencies, (dependency, done) => {
this.createModule({
parser,
name: dependency.name,
context: dependency.context,
rawRequest: dependency.rawRequest,
moduleId: dependency.moduleId,
resource: dependency.resource
}, null, done)
}, callback)
}
seal(callback) {
this.hooks.seal.call()
this.hooks.beforeChunks.call()
// 01 当前所有的入口模块都被存放在了 compilation 对象的 entries 数组里
// 02 所谓封装 chunk 指的就是依据某个入口,然后找到它的所有依赖,将它们的源代码放在一起,之后再做合并
for (const entryModule of this.entries) {
// 核心: 创建模块加载已有模块的内容,同时记录模块信息
const chunk = new Chunk(entryModule)
// 保存 chunk 信息
this.chunks.push(chunk)
// 给 chunk 属性赋值
chunk.modules = this.modules.filter(module => module.name === chunk.name)
}
// chunk 流程梳理之后就进入到 chunk 代码处理环节(模板文件 + 模块中的源代码==》chunk.js)
this.hooks.afterChunks.call(this.chunks)
// 生成代码内容
this.createChunkAssets()
callback()
}
createChunkAssets() {
for (let i = 0; i < this.chunks.length; i++) {
const chunk = this.chunks[i]
const fileName = chunk.name + '.js'
chunk.files.push(fileName)
// 01 获取模板文件的路径
let tempPath = path.posix.join(__dirname, 'temp/main.ejs')
// 02 读取模块文件中的内容
let tempCode = this.inputFileSystem.readFileSync(tempPath, 'utf8')
// 03 获取渲染函数
let tempRender = ejs.compile(tempCode)
// 04 按ejs的语法渲染数据
let source = tempRender({
entryModuleId: chunk.entryModule.moduleId,
modules: chunk.modules
})
// 输出文件
this.emitAssets(fileName, source)
}
}
emitAssets(fileName, source) {
this.assets[fileName] = source
this.files.push(fileName)
}
复制代码
// NormalModule.js
class NormalModule {
constructor(data) {
this.context = data.context
this.name = data.name
this.moduleId = data.moduleId
this.rawRequest = data.rawRequest
this.parser = data.parser // TODO: 等待完成
this.resource = data.resource
this._source // 存放某个模块的源代码
this._ast // 存放某个模板源代码对应的 ast
this.dependencies = [] // 定义一个空数组用于保存被依赖加载的模块信息
}
build(compilation, callback) {
/**
* 01 从文件中读取到将来需要被加载的 module 内容,这个
* 02 如果当前不是 js 模块则需要 Loader 进行处理,最终返回 js 模块
* 03 上述的操作完成之后就可以将 js 代码转为 ast 语法树
* 04 当前 js 模块内部可能又引用了很多其它的模块,因此我们需要递归完成
* 05 前面的完成之后,我们只需要重复执行即可
*/
this.doBuild(compilation, (err) => {
this._ast = this.parser.parse(this._source)
// 这里的 _ast 就是当前 module 的语法树,我们可以对它进行修改,最后再将 ast 转回成 code 代码
traverse(this._ast, {
CallExpression: (nodePath) => {
let node = nodePath.node
// 定位 require 所在的节点
if (node.callee.name === 'require') {
// 获取原始请求路径
let modulePath = node.arguments[0].value // './title'
// 取出当前被加载的模块名称
let moduleName = modulePath.split(path.posix.sep).pop() // title
// [当前我们的打包器只处理 js ]
let extName = moduleName.indexOf('.') == -1 ? '.js' : ''
moduleName += extName // title.js
// 【最终我们想要读取当前js里的内容】 所以我们需要个绝对路径
let depResource = path.posix.join(path.posix.dirname(this.resource), moduleName)
// 【将当前模块的 id 定义OK】
let depModuleId = './' + path.posix.relative(this.context, depResource) // ./src/title.js
// 记录当前被依赖模块的信息,方便后面递归加载
this.dependencies.push({
name: this.name, // TODO: 将来需要修改
context: this.context,
rawRequest: moduleName,
moduleId: depModuleId,
resource: depResource
})
// 替换内容
node.callee.name = '__webpack_require__'
node.arguments = [types.stringLiteral(depModuleId)]
}
}
})
// 上述的操作是利用ast 按要求做了代码修改,下面的内容就是利用 .... 将修改后的 ast 转回成 code
let { code } = generator(this._ast)
this._source = code
callback(err)
})
}
doBuild(compilation, callback) {
this.getSource(compilation, (err, source) => {
this._source = source
callback()
})
}
getSource(compilation, callback) {
compilation.inputFileSystem.readFile(this.resource, 'utf8', callback)
}
}
复制代码
7、执行compiler.emitAssets()方法以及compiler.run()执行时传入的回调
- compiler.emitAssets中回调用compiler的emit生命周期钩子这时候一次构建基本结束了
emitAssets(compilation, callback) {
// 当前需要做的核心: 01 创建dist 02 在目录创建完成之后执行文件的写操作
// 01 定义一个工具方法用于执行文件的生成操作
const emitFlies = (err) => {
const assets = compilation.assets
let outputPath = this.options.output.path
for (let file in assets) {
let source = assets[file]
let targetPath = path.posix.join(outputPath, file)
this.outputFileSystem.writeFileSync(targetPath, source, 'utf8')
}
callback(err)
}
// 创建目录之后启动文件写入
this.hooks.emit.callAsync(compilation, (err) => {
mkdirp.sync(this.options.output.path)
emitFlies()
})
}
复制代码