webpack打包的流程大致可以归纳成:
- options:通过config文件传递进来的配置参数
- webpack:生成Compiler实例及其他webpack初始化逻辑
- compiler:编译的核心桥梁,根据配置文件,通过执行webpack(options),生成的并返回一个Compiler实例compiler(自我理解为一个编译工厂)
2.1 在生成的Compiler实例上将options配置参数挂载到compiler.options,以方便将来获取配置参数
2.2 通过new NodeEnvironmentPlugin()让compiler得到读写文件的能力
2.3 遍历options配置中的plugins,如果有plugins,则通过tapable机制注册一个的监听事件,以便将来某个时刻触发监听来执行插件
2.4 通过new WebpackOptionsApply()的process方法来注册所有内置插件的监听事件(第4大点) - 通过new WebpackOptionsApply().process(options, compiler)注册内置插件
4.1 新建一个EntryOptionPlugin实例并注册entryOption监听事件
4.2 执行entryOption事件来注册make监听事件 - 执行compiler的run方法来打包生成代码文件,以及返回最终的打包对象数据
5.1 生成一个Compilation实例compilation(负责具体的代码编译)
5.2 执行make事件来读取文件源代码转换成ast语法树,随后替换其中的关键字为webpack关键字,再转换成编译后的源代码
5.3 遍历已被编译的源代码是否有依赖模块,如果有再重复执行5.2步骤来递归编译
5.4 通过compilation实例方法将编译后的代码写入模版文件
5.5 通过compiler实例将代码写入到指定文件夹下的指定文件,并返回打包后的数据信息
话不多说,下面我们直接从webpack的入口文件开始来一步步拆解模拟一个webpack的简易打包器
1. 启动打包程序
start.js
const webpack = require('./lib/myWebpack/webpack')
const options = require('./webpack.config')
// 生成compiler编译工厂
const compiler = webpack(options)
// 执行打包程序
compiler.run((err, stats) => {
console.log(err)
console.log(stats)
})
2. 生成Compiler实例及其他webpack初始化逻辑
webpack.js
const Compiler = require('./Compiler')
const NodeEnvironmentPlugin = require('./NodeEnvironmentPlugin')
const WebpackOptionsApply = require('./WebpackOptionsApply')
const webpack = options => {
// 实例化一个compiler对象(编译工厂)
const compiler = new Compiler(options.context)
// 将options配置参数挂载到compiler.options,以方便将来获取配置参数
compiler.options = options
// 让compiler获得读写文件的能力
new NodeEnvironmentPlugin().apply(compiler)
// 遍历options配置中的plugins注册监听事件,以便将来某个时刻触发监听来执行插件
if (options.plugins && Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
plugin.apply(compiler)
}
}
// 注册内置插件
new WebpackOptionsApply().process(options, compiler)
return compiler
}
module.exports = webpack
3. 生成Compiler实例
// 实例化一个compiler对象(编译工厂)
const compiler = new Compiler(options.context)
通过上文的分析,Compiler必须要具备一个run方法来开始编译,并且需具备一些钩子来让用户注册自定义插件和挂载内置插件。由于Compiler只是一个“工厂”,只负责流程管理,所以我们还需要创造一个工人compilation来负责代码的编译工作,当compilation把代码编译完成后,通知Compiler来写入文件。
const {
Tapable,
SyncHook,
SyncBailHook,
AsyncSeriesHook,
AsyncParallelHook
} = require('tapable')
class Compiler extends Tapable {
constructor (context) {
super()
this.context = context
// 生成一系列钩子
this.hooks = {
done: new AsyncSeriesHook(['stats']),
entryOption: new SyncBailHook(['context', 'entry']),
beforeRun: new AsyncSeriesHook(['compiler']),
run: new AsyncSeriesHook(['compiler']),
thisCompilation: new SyncHook(['compilation', 'params']),
compilation: new SyncHook(['compilation', 'params']),
beforeCompile: new AsyncSeriesHook(['params']),
compile: new SyncHook(['params']),
make: new AsyncParallelHook(['compilation']),
afterCompile: new AsyncSeriesHook(['compilation']),
emit: new AsyncSeriesHook(['compilation'])
}
}
emitAssets(compilation, callback) {
// TODO: 写入打包文件
}
run (callback) {
this.hooks.beforeRun.callAsync(this, err => {
this.hooks.run.callAsync(this, err => {
// TODO:this.compile(callback) 执行编译逻辑,callback为编译完成后的回调
})
})
}
}
module.exports = Compiler
接下来将编译逻辑compile方法补全,按源码的逻辑,此时会生成一个compilation工人,同时会调用一些钩子事件,最重要的就是make钩子的调用进入make流程,在make流程完成后执行回调处理onCompiled编译完成之后的流程
compile (callback) {
// 生成beforeCompile,compile钩子用到的参数
const params = this.newCompilationParams()
this.hooks.beforeCompile.callAsync(params, err => {
this.hooks.compile.call(params);
// 生成compilation
const compilation = this.newCompilation(params)
// 执行make钩子
this.hooks.make.callAsync(compilation, err => {
// TODO:make钩子执行完成的回调
})
})
}
newCompilationParams () {
const params = {
normalModuleFactory: new NormalModuleFactory()
}
return params
}
newCompilation (params) {
const compilation = new Compilation(this)
this.hooks.thisCompilation.call(compilation, params)
this.hooks.compilation.call(compilation, params)
return compilation
}
此时的Compile.js代码
const {
Tapable,
SyncHook,
SyncBailHook,
AsyncSeriesHook,
AsyncParallelHook
} = require('tapable')
const NormalModuleFactory = require('./NormalModuleFactory')
const Compilation = require('./Compilation')
class Compiler extends Tapable {
constructor (context) {
super()
this.context = context
// 生成一系列钩子
this.hooks = {
done: new AsyncSeriesHook(['stats']),
entryOption: new SyncBailHook(['context', 'entry']),
beforeRun: new AsyncSeriesHook(['compiler']),
run: new AsyncSeriesHook(['compiler']),
thisCompilation: new SyncHook(['compilation', 'params']),
compilation: new SyncHook(['compilation', 'params']),
beforeCompile: new AsyncSeriesHook(['params']),
compile: new SyncHook(['params']),
make: new AsyncParallelHook(['compilation']),
afterCompile: new AsyncSeriesHook(['compilation']),
emit: new AsyncSeriesHook(['compilation'])
}
}
run (callback) {
const onCompiled = () => {}
this.hooks.beforeRun.callAsync(this, err => {
this.hooks.run.callAsync(this, err => {
this.compile(onCompiled)
})
})
}
compile (callback) {
// 生成beforeCompile,compile钩子用到的参数
const params = this.newCompilationParams()
this.hooks.beforeCompile.callAsync(params, err => {
this.hooks.compile.call(params);
// 生成compilation
const compilation = this.newCompilation(params)
// 执行make钩子
this.hooks.make.callAsync(compilation, err => {
// TODO:make钩子执行完成的回调, 开始处理chunk
})
})
}
newCompilationParams () {
const params = {
normalModuleFactory: new NormalModuleFactory()
}
return params
}
newCompilation (params) {
const compilation = new Compilation(this)
this.hooks.thisCompilation.call(compilation, params)
this.hooks.compilation.call(compilation, params)
return compilation
}
}
module.exports = Compiler
4. NodeEnvironmentPlugin,赋予compiler实例读写能力
// 让compiler获得读写文件的能力
new NodeEnvironmentPlugin().apply(compiler)
我们让NodeEnvironmentPlugin类继承fs模块的能力,由于属于插件,根据webpack的思想,需要有一个apply方法来调用实现
NodeEnvironmentPlugin.js
const fs = require('fs')
class NodeEnvironmentPlugin {
constructor(options) {
this.options = options || {}
}
apply(compiler) {
complier.inputFileSystem = fs
complier.outputFileSystem = fs
}
}
module.exports = NodeEnvironmentPlugin
5. 注册自定义插件apply方法,注册钩子事件
// 遍历options配置中的plugins注册监听事件,以便将来某个时刻触发监听来执行插件
if (options.plugins && Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
plugin.apply(compiler)
}
}
6. 注册webpack内置插件
// 注册内置插件
new WebpackOptionsApply().process(options, compiler)
通过注册内置插件来注册compiler实例上的entryOption,make钩子监听事件。
WebpackOptionsApply.js
const EntryOptionPlugin = require('./EntryOptionPlugin')
class WebpackOptionsApply {
process (options, compiler) {
// 入口插件注册
new EntryOptionPlugin().apply(compiler)
compiler.hooks.entryOption.call(options.context, options.entry)
}
}
module.exports = WebpackOptionsApply
EntryOptionPlugin.js
const SingleEntryPlugin = require("./SingleEntryPlugin")
const itemToPlugin = function (context, item, name) {
return new SingleEntryPlugin(context, item, name)
}
class EntryOptionPlugin {
apply (compiler) {
compiler.hooks.entryOption.tap('EntryOptionPlugin', (context, entry) => {
// 注册make钩子
itemToPlugin(context, entry, "main").apply(compiler)
})
}
}
module.exports = EntryOptionPlugin
SingleEntryPlugin.js
class SingleEntryPlugin {
constructor(context, entry, name) {
this.context = context
this.entry = entry
this.name = name
}
apply (compiler) {
compiler.hooks.make.tapAsync('SingleEntryPlugin', (compilation, callback) => {
const { context, entry, name } = this
compilation.addEntry(context, entry, name, callback)
})
}
}
module.exports = SingleEntryPlugin
7. compilation编译工实现
首先实现一个ast解析器,将来用来把代码转换成ast语法树
Parser.js
const babylon = require('babylon')
const { Tapable } = require('tapable')
class Parser extends Tapable {
parse (source) {
return babylon.parse(source, {
sourceType: 'module',
plugins: ['dynamicImport'] // 当前插件可以支持 import() 动态导入的语法
})
}
}
module.exports = Parser
通过make钩子,我们得知compilation实例上需要有一个addEntry方法作为入口
const { Tapable, SyncHook } = require('tapable')
const NormalModuleFactory = require('./NormalModuleFactory')
const Parser = require('./Parser') // 将代码解析成ast语法树
const parser = new Parser()
class Compilation extends Tapable {
constructor (compiler) {
super()
this.compiler = compiler
this.context = compiler.context
this.options = compiler.options
this.inputFileSystem = compiler.inputFileSystem // 让 compilation 具备文件的读写能力
this.outputFileSystem = compiler.outputFileSystem // 让 compilation 具备文件的读写能力
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()
}
}
/**
* 完成模块编译操作
* @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,
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) {
// 代码工厂,解析ast语法树,编译最终代码形态
const module = normalModuleFactory.create(data)
const afterBuild = () => {
// TODO
}
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)
})
}
}
module.exports = Compilation
compilation createModule需要实现一个模块代码工厂类,将Parser后的ast代码替换关键字后,编译成源代码
NormalModuleFactory.js
const NormalModule = require("./NormalModule")
class NormalModuleFactory {
create (data) {
return new NormalModule(data)
}
}
module.exports = NormalModuleFactory
NormalModule.js
const path = require('path')
const types = require('@babel/types')
const generator = require('@babel/generator').default
const traverse = require('@babel/traverse').default
class NormalModule {
constructor (data) {
this.context = data.context
this.name = data.name
this.moduleId = data.moduleId
this.rawRequest = data.rawRequest
this.parser = data.parser
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 => {
// 将源代码转成ast语法树
this._ast = this.parser.parse(this._source)
// 对语法树进行修改,最后再将 ast 转回成 code 代码
traverse(this._ast, {
CallExpression: nodePath => {
let node = nodePath.node
// 定位 require 所在的节点
if (node.callee.name === 'require') {
// 获取原始请求路径
let modulePath = node.arguments[0].value
// 取出当前被加载的模块名称
let moduleName = modulePath.split(path.posix.sep).pop()
// 当前我们的打包器只处理 js
let extName = moduleName.indexOf('.') == -1 ? '.js' : ''
moduleName += extName
// 最终我们想要读取当前js里的内容,所以我们需要个绝对路径
let depResource = path.posix.join(path.posix.dirname(this.resource), moduleName)
// 将当前模块的id定义为./src/**形式
let depModuleId = './' + path.posix.relative(this.context, depResource)
// 记录当前被依赖模块的信息,方便后面递归加载
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 转回成 code
const { 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)
}
}
module.exports = NormalModule
接下来回到Compilation中完成afterBuild之后的工作,判读module中是否有依赖,如果有继续递归执行createModule,直到没有依赖为止,最后执行addEntry的回调函数(即make钩子执行完成的回调)
const { Tapable, SyncHook } = require('tapable')
const NormalModuleFactory = require('./NormalModuleFactory')
const Parser = require('./Parser') // 将代码解析成ast语法树
const parser = new Parser()
class Compilation extends Tapable {
constructor (compiler) {
super()
this.compiler = compiler
this.context = compiler.context
this.options = compiler.options
this.inputFileSystem = compiler.inputFileSystem // 让 compilation 具备文件的读写能力
this.outputFileSystem = compiler.outputFileSystem // 让 compilation 具备文件的读写能力
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()
}
}
/**
* 完成模块编译操作
* @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,
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) {
// 代码工厂,解析ast语法树,编译最终代码形态
const module = normalModuleFactory.create(data)
const afterBuild = (err, module) => {
// 判断当前次module加载完成之后是否需要处理依赖加载
if (module.dependencies.length > 0) {
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)
})
}
}
module.exports = Compilation
8. make钩子执行完成后的操作
在Compilation的afterBuild执行完成后,会触发make钩子的回调函数,通过Compilation开始处理chunk,将刚才被编译过的code写入模板文件中,最后执行写入目录操作,到此Compiler的任务就完成了。现在先回到Compiler里面,补全Compiler代码
Compiler.js
const {
Tapable,
SyncHook,
SyncBailHook,
AsyncSeriesHook,
AsyncParallelHook
} = require('tapable')
const path = require('path')
const mkdirp = require('mkdirp')
const Stats = require('./Stats')
const NormalModuleFactory = require('./NormalModuleFactory')
const Compilation = require('./Compilation')
class Compiler extends Tapable {
constructor (context) {
super()
this.context = context
// 生成一系列钩子
this.hooks = {
done: new AsyncSeriesHook(['stats']),
entryOption: new SyncBailHook(['context', 'entry']),
beforeRun: new AsyncSeriesHook(['compiler']),
run: new AsyncSeriesHook(['compiler']),
thisCompilation: new SyncHook(['compilation', 'params']),
compilation: new SyncHook(['compilation', 'params']),
beforeCompile: new AsyncSeriesHook(['params']),
compile: new SyncHook(['params']),
make: new AsyncParallelHook(['compilation']),
afterCompile: new AsyncSeriesHook(['compilation']),
emit: new AsyncSeriesHook(['compilation'])
}
}
// 创建打包目录,在目录创建完成之后执行文件的写操作
emitAssets(compilation, callback) {
// 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()
})
}
run (callback) {
// 输出stats打包后的数据信息
const finalCallback = function (err, stats) {
callback(err, stats)
}
const onCompiled = (err, compilation) => {
// 调用emitAssets将处理好的 chunk 写入到指定的文件然后输出至打包目录
this.emitAssets(compilation, (err) => {
let stats = new Stats(compilation)
finalCallback(err, stats)
})
}
this.hooks.beforeRun.callAsync(this, err => {
this.hooks.run.callAsync(this, err => {
this.compile(onCompiled)
})
})
}
compile (callback) {
// 生成beforeCompile,compile钩子用到的参数
const params = this.newCompilationParams()
this.hooks.beforeCompile.callAsync(params, err => {
this.hooks.compile.call(params);
// 生成compilation
const compilation = this.newCompilation(params)
// 执行make钩子
this.hooks.make.callAsync(compilation, err => {
// 开始处理 chunk
compilation.seal((err) => {
this.hooks.afterCompile.callAsync(compilation, (err) => {
callback(err, compilation)
})
})
})
})
}
newCompilationParams () {
const params = {
normalModuleFactory: new NormalModuleFactory()
}
return params
}
newCompilation (params) {
const compilation = new Compilation(this)
this.hooks.thisCompilation.call(compilation, params)
this.hooks.compilation.call(compilation, params)
return compilation
}
}
module.exports = Compiler
Stats.js
class Stats {
constructor (compilation) {
this.entries = compilation.entries
this.modules = compilation.modules
this.chunks = compilation.chunks
this.files = compilation.files
}
toJson() {
return this
}
}
module.exports = Stats
9. 补全Compilation中处理chunk的逻辑
处理chunk指的就是依据某个入口,然后找到它的所有依赖,将它们的源代码放在一起,之后再做合并
Chunk.js
class Chunk {
constructor (entryModule) {
this.entryModule = entryModule
this.name = entryModule.name
this.files = [] // 记录每个 chunk的文件信息
this.modules = [] // 记录每个 chunk 里的所包含的 module
}
}
module.exports = Chunk
Compilation.js
const { Tapable, SyncHook } = require('tapable')
const NormalModuleFactory = require('./NormalModuleFactory')
const Parser = require('./Parser') // 将代码解析成ast语法树
const ejs = require('ejs')
const Chunk = require('./Chunk')
const path = require('path')
const async = require('neo-async')
const parser = new Parser()
const normalModuleFactory = new NormalModuleFactory()
class Compilation extends Tapable {
constructor (compiler) {
super()
this.compiler = compiler
this.context = compiler.context
this.options = compiler.options
this.inputFileSystem = compiler.inputFileSystem // 让 compilation 具备文件的读写能力
this.outputFileSystem = compiler.outputFileSystem // 让 compilation 具备文件的读写能力
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()
}
}
/**
* 完成模块编译操作
* @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,
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) {
// 代码工厂,解析ast语法树,编译最终代码形态
const module = normalModuleFactory.create(data)
const afterBuild = (err, module) => {
// 判断当前次module加载完成之后是否需要处理依赖加载
if (module.dependencies.length > 0) {
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 】
const 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)
}
// 处理Chunk
seal (callback) {
this.hooks.seal.call()
this.hooks.beforeChunks.call()
// 当前所有的入口模块都被存放在了 compilation 对象的 entries 数组里
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)
}
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)
// 获取模板文件的路径
const tempPath = path.posix.join(__dirname, 'temp/main.ejs')
// 读取模块文件中的内容
const tempCode = this.inputFileSystem.readFileSync(tempPath, 'utf8')
// 获取渲染函数
const tempRender = ejs.compile(tempCode)
// 按ejs的语法渲染数据
const source = tempRender({
entryModuleId: chunk.entryModule.moduleId,
modules: chunk.modules
})
// 输出文件
this.emitAssets(fileName, source)
}
}
emitAssets (fileName, source) {
this.assets[fileName] = source
this.files.push(fileName)
}
}
module.exports = Compilation
至此webpack的简要打包流程及一个简易的webpack打包器就完成了,一句话总结起来就是:
根据配置文件,递归拿到文件的源代码,替换源代码中的关键字,然后合并代码,最后根据模板输出到指定目录
附文结构
- Chunk.js
- Compilation.js
- Compiler.js
- EntryOptionPlugin.js
- NodeEnvironmentPlugin.js
- NormalModule.js
- NormalModuleFactory.js
- Parser.js
- SingleEntryPlugin.js
- Stats.js
- webpack.js
- WebpackOptionsApply.js