大致的流程如下
-
初始化参数 从配置文件中 webpack.config.js 读取配置对象 然后和 shell参数进行合并得到最终的 配置对象
-
上一个得到的参数 初始化 Complier 对象
-
加载所有配置的插件
-
执行run 开始编译
-
根据配置中的 entry找到入口文件
-
从入口文件出发 调用所有的配置的loader 对模块进行编译
-
在找出该模块对应的依赖的模块, 再递归本步骤直到所有的入口依赖的文件都经过了本步骤的处理 ./src/entry.js
-
根据入口模块之间的依赖关系,组装成一个包含多个模块的 Chunk
9.再把每个chunk 转化为一个单独的文件到输出列表 this.assets 对象 key是文件名 值是文件的内容
整体的具体的流程如下
-
读取配置对象 读取shell 参数对象 然后合并配置对象和参数对象
-
创建Complier (保存options 创建hooks 初始化一些内部对象entries、mudules、chunks、assets、files)
3.挂载插件 先是run done
-
调用Complier的run方法进行编译 找到entry入口文件 然后遍历entry拿到入口文件的路径buildModule函数(1. 读取模块内容 2. rules里找到对应的loaders 3.依次调用所有loader对文件进行转化 )
-
创建入口模块对象(对象里有1.模块ID相对于根目录的路径 2.模块的名字就是入口的名字entry1 entry2 3.依赖模块的绝对路径数组)
-
loader转换后的代码 转为AST语法树, 然后遍历语法树(找源码里的import和require)
-
找依赖的模块,找到后放在当前模块的依赖模块的数组里 然后递归编译依赖模块
-
把每个入口模块和他的依赖的模块构建成一个chunk代码块
-
根据chunk生成 assets 输出文件列表 key就是文件名 value就是 文件内容
-
根据assets 写入文件系统
-
结束
在以上过程中, webpack会在特点的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用webpack 提供的API(run done emit) 改变webpack的运行的结果
其他问题(可以跳过)
// 考虑下优化就是
// 1. 同步操作改为异步
// 2. 能不能通过插件修改输出的文件, 添加 删除 修改 hook.emit
// 如果有多层依赖代码块是如何进行分割的? 答:代码块的分割点就是 import('...')
// 如果不进行代码分割,有几个入口配置就有几个chunk
// compilation 和 complier? 在webpack编译的时候 complier只有一个 但是每次编译后都有又一个新的compilation
// splitChunks webpack5 已经内置了
// let webpack = require('webpack')
// 1. 初始化参数 从配置文件中 webpack.config.js 读取配置对象 然后和 shell参数进行合并得到最终的 配置对象
// shell参数 比如说是 package.json里的 scripts{
// "build": "webpack --mode=development"
// }
// 2. 上一个得到的参数 初始化 Complier 对象
const options = webpack(options)
// 3. 加载所有配置的插件
// 4. 执行run 开始编译
最主要的两个文件如下
complier.run((err,stats) => {
console.log(err)
console.log(stats.toJson({
entries: true, // 入口信息
modules: true, // 本次打包有哪些模块
chunks: true, // 代码块
assets: true, // 产出的资源
files: true, // 最后生产了哪些文件
}))
})
webpack.js文件
webpack.js文件
let Complier = require('./Complier')
function webpack (options) {
// 初始化参数 从配置文件中 webpack.config.js 读取配置对象 然后和 shell参数进行合并得到最终的 配置对象
let shellOptions = process.argv.slice(2).reduce((config, args) => {
let [key,value] = args.split('=') // --mode-production
config[key.slice(2)] = value
return config
},{})
let finnalOption = {...options,...shellOptions}
let complier = new Complier(options)
// 3. 加载所有的配置的插件
if (finnalOption.plugins && Array.isArray(finnalOption.plugins)) {
for (let plugin of options.plugins) {
plugin.apply(complier)
}
}
return complier
}
module.exports = webpack
complier.js文件
complier.js文件
let {SyncHook} = require('./doc/tapable')
let {toUnixPath} = require('./utils')
let fs = require('fs')
let types = require('bable-types') // 判断某个节点是否时某种类型,生成某个新的节点
let parser = require('@bable/parser') // 语法解析 源码生产 AST语法树
let traverse = require('@bable/traverse').default // 遍历器 遍历语法树
let generator = require('@bable/generator').default // 生成器 根据语法树重新生成代码
let rootPath = toUnixPath(process.cwd())
class Complier {
constructor (options) {
this.options = options
this.hooks = {
run: new SyncHook(), // 开始编译
emit: new SyncHook(), // 写入文件系统 //这个钩子会在我们写入文件系统的时候触发 所以可以在这里改变文件
done: new SyncHook() // 编译全部完成
}
this.entries = new Set() // 所有的入口模块, wepack4 是数组 现在是 Set
this.mudules = new Set() // 所有的模块
this.chunks = new Set() // 所有的代码块
this.assets = {} // 存放本次产出的资源文件
this.files = new Set() // 存放这 本次编译所有的产出的文件名
}
run (callback) {
this.hooks.run.call()
// console.log('开始真正的编译')
// 5. 根据配置中的 entry找到入口文件
let entry = {}
if (typeof this.options.entry === 'string') {
entry.main = this.options.entry
} else {
entry = this.options.entry
}
// 6. 从入口文件出发 调用所有的配置的loader 对模块进行编译
for (let entryName in entry) {
let entryPath = toUnixPath(path.join(rootPath, entry[entryName]))
let entryModule = this.buildModule(entryName,entryPath)
this.entries.add(entryModule)
// this.modules.add(entryModule)
// 8. 根据入口模块之间的依赖关系,组装成一个包含多个模块的 Chunk
let chunk = {name:entryName,entryModule,modules: Array.from(this.modules).filter(module => module.name === entryName)}
this.chunks.add(chunk)
}
// 9.再把每个chunk 转化为一个单独的文件到输出列表 this.assets 对象 key是文件名 值是文件的内容
let output = this.options.output
this.chunks.forEach(chunk => {
// let fileName = path.join(output.path, output.fileName.replace('[name]',chunk.name))
let fileName = output.fileName.replace('[name]',chunk.name) // 这里只是文件名没有路径
this.assets[fileName] = getSource(chunk)
})
this.hooks.emit.call()
this.files = Object.keys(this.assets) // 文件名的一个数组
for(let fileName in this.assets) {
let filePath = path.join(output.path, fileName)
fs.writeFileSync(filePath, this.assets[fileName])
}
// 到这里编译全部接受,就可以触发done钩子的回调了
this.hooks.done.call()
callback(null, {
toJson: ()=> ({
entries: this.entries,
chunks: this.chunks,
modules: this.modules,
files: this.files,
assets: this.assets
})
})
}
buildModule(entryName,modulePath) {
// 1. 读取此模块内容
let originalSourceCode = fs.readFileSync(modulePath, 'utf8')
let targetSourceCode = originalSourceCode
// 2. 调用 所有配置的loader 对模块进行编译
let rules = this.options.module.rules
// 这个loaders 得打哦本文件生效的loader 有哪些
this.loaders = []
for (let i = 0; i < rules.length; i++) {
// if (rules[i].test.test(modulePath))
if (modulePath.match(rules[i].test)) {
loaders = [...this.loaders, ...rules[i].use]
}
}
for(let i = this.loaders.length-1; i >= 0; i--) {
targetSourceCode = require(this.loaders[i])(targetSourceCode)
}
// 7. 在找出该模块对应的依赖的模块, 再递归本步骤直到所有的入口依赖的文件都经过了本步骤的处理 ./src/entry.js
// A B C 模块ID 都是相对与根目录的相对路径 ./
//path.posix 把 所有的 都变成 /
let moduleId = './'+path.posix.relative(rootPath, modulePath)
let module = {id: moduleId, dependencies:[],name:entryName}
// 再找出该模块依赖的模块 把转换后的源码转为抽象语法树
let AST = parser.parser(targetSourceCode,{resourceType:'module'})
traverse(ast, {
CallExpression: (node) => {
if (node.callee.name === 'require') { // 说明有引入模块
// 要引入模块的相对路径
let moduleName = node.arguments[0].value
// 为了获取要加载的模块的绝对路径 第一步获得当前模块的所在目录
let dirName = path.posix.dirname(modulePath)
let depModulePath = path.posix.join(dirName,moduleName)
let extensions = this.options.resolve.extensions
depModulePath = tryExtensions(depModulePath,extensions,moduleName,dirName)
let depModuleId = './'+path.posix.relative(rootPath,depModulePath) // ./src/title.js
node.arguments = [types.stringLiteral(depModuleId)]
// 判断现有的已经编译过的 modules里有没有这个板块,如果有就不要在添加依赖了如没有就添加
let alreadyModuleIds = Array.from(this.modules).map(module => module.id)
if(!alreadyModuleIds.includes(depModuleId)) {
module.dependencies.add(depModulePath)
}
}
}
})
let {code} = generator(ast)
module._source = code // 此模块的 源代码
// 把当前模块编译完成,找到他所有的依赖的模块,进行递归的编译,添加到 this.modules中
module.dependencies.forEach(dependency => {
let depModule = this.buildModule(entryName, dependency)
this.mudules.add(depModule)
})
}
}
// 获取 chunk 对应的 源代码 输出的文件内容
// name 代码块的名字 entryModule入口模块 modules所有的模块
function getSource (chunk) {
`(() => {
var modules = ({
${
chunk.modules.map(module => `
"${module.id}":
((module)=>{ ${module._source}})
`).join(',')
}
})
var cache = {}
function require(moduleId) {
var cachedModule = cache[moduleId]
if (cachedModule !== undefined) {
return cachedModule.exports
}
var module = cache[moduleId] = {
exports: {}
}
modules[moduleId](module, module.exports, require)
return module.exports
}
var exports = {}
(() => {
${chunk.entryModule._source}
})()
})()`
}
/**
*
* @param {*} modulePath 拼出来的模版路径 c:/src/title
* @param {*} extensions ['.js','.jsx','.json']
* @param {*} originModulePath ./title
* @param {*} moduleContext c:src/
*/
function tryExtensions (modulePath,extensions,originModulePath,moduleContext) {
extensions.unshift('') // ['','.js','.jsx','.json']
for (let i = 0; i < extensions.length; i++) {
if (fs.existsSync(moduleP+extensions[i])) {
return modulePath + extensions[i]
}
}
// 如果还是没有找到 代码执行到这里 说明没有一个后缀匹配上 需要报错
throw new Error(`Module not found: Error: Can't resolve ${originModulePath} in ${moduleContext}`)
}
module.exports = Complier