webpack是如何进行依赖图谱收集的?
我自己学习webpack
已有很长时间了,但是经常会遇到这样的问题: 可以熟练配置webpack
的一些常用配置,但是对一些不常见的api
或者概念总是云里雾里。因此,对着网上资料手写了一个简易版的webpack
,现在对其中的依赖图谱收集部分进行梳理,搞清楚webpack
是如何进行打包的。
打包流程
获取入口文件
根据 webpack.config.js
配置文件 的 entry
属性获取入口文件的绝对路径
getEntry() {
let entry = Object.create(null)
const { entry: optionsEntry } = this.options
if (typeof optionsEntry === 'string') {
entry['main'] = optionsEntry
} else {
entry = optionsEntry
}
// 将entry变为绝对路径
Object.keys(entry).forEach(key => {
const value = entry[key]
if (!path.isAbsolute(value)) {
// 转化为绝对路径的同时统一路径分隔符为 /
entry[key] = toUnixPath(path.join(this.rootPath, value))
}
})
return entry
}
编译入口文件
编译入口文件主要是要分析入口文件依赖了哪些模块,然后继续递归编译依赖的模块。
buildEntryModule(entry) {
Object.keys(entry).forEach(entryName => {
const entryPath = entry[entryName]
// 编译入口文件
const entryObj = this.buildModule(entryName, entryPath)
// 每个文件都是一个模块对象
this.entries.add(entryObj)
})
}
buildModule(moduleName, modulePath) {
// 1. 读取文件原始代码
const originSourceCode = (this.originSourceCode = fs.readFileSync(
modulePath,
'utf-8'
))
// moduleCode为修改后的代码,现在赋初始值
this.moduleCode = originSourceCode
// 2. 调用loader进行处理
this.handleLoader(modulePath)
// 3. 调用webpack进行模块编译,获得最终的module对象
const module = this.handleWebpackCompiler(moduleName, modulePath)
// 4. 返回module
return module
}
编译的工作主要在handleWebpackCompiler
函数中:
handleWebpackCompiler(moduleName, modulePath) {
// 将当前模块路径相对于项目启动根目录计算出相对路径 作为模块ID
const moduleId = './' + path.posix.relative(this.rootPath, modulePath)
// 创建模块对象,初次进来就是入口模块
const module = {
id: moduleId,
dependencies: new Set(), // 该模块所依赖模块相对于根路径的路径地址
name: [moduleName] // 该模块所属的入口文件
}
// 调用babel分析我们的代码
const ast = parser.parse(this.moduleCode, {
sourceType: 'module'
})
// 深度优先,遍历语法AST
traverse(ast, {
// 当遇到require语句时
CallExpression: nodePath => {
const node = nodePath.node
if (node.callee.name === 'require') {
// 获得源代码中引入模块相对路径
const requirePath = node.arguments[0].value
// 寻找模块绝对路径
// modulePath是当前文件的绝对路径,这一步是为获得当前文件的目录路径
const moduleDirName = path.posix.dirname(modulePath)
// 根据extensions获取依赖的绝对路径
const absolutePath = tryExtensions(
path.posix.join(moduleDirName, requirePath),
this.options.resolve.extensions,
requirePath,
moduleDirName
)
// 生成moduleId - 相对于根路径的模块ID 添加进入新的依赖模块路径
const moduleId =
'./' + path.posix.relative(this.rootPath, absolutePath)
// 通过babel修改源代码中的require变成__webpack_require__语句
node.callee = t.identifier('__webpack_require__')
// 修改源代码中require语句引入的模块 全部修改变为相对于根路径的相对路径来处理
node.arguments = [t.stringLiteral(moduleId)]
// 为当前模块添加require语句造成的依赖(内容为相对于根路径的模块ID)
module.dependencies.add(moduleId)
}
}
})
// 遍历结束根据AST生成新的代码
const { code } = generator(ast)
// 为当前模块挂载新的生成的代码
module._source = code
// 递归依赖深度遍历 存在依赖模块则加入modules中
module.dependencies.forEach(dependency => {
const depModule = this.buildModule(moduleName, dependency)
// 将编译后的任何依赖模块对象加入到modules对象中去
this.modules.add(depModule)
})
// 返回当前模块对象
return module
}
创建module
对象
每一个文件都是一个模块对象:
const module = {
id: moduleId, // 该模块的相对路径
dependencies: new Set(), // 该模块依赖了那些模块
name: [] // 该模块属于哪个入口文件
}
编译模块
-
利用
babel
把入口文件代码转化为AST
抽象语法树,找出模块中的require
语句,并把require
替换为__webpack_require__
,因为我们自己会实现一个__webpack_require__
方法,所以在开发过程中就可以使用require
而不会报错。 -
找到依赖的模块的相对路径,通过
module.dependencies.add(moduleId)
加入到父模块的dependencies
, 这样就构成了一个依赖关系。 -
对依赖模块递归编译,找出依赖的依赖,这样就构成了依赖图谱的收集。
-
this.entries
用来存储多入口文件,比如:
entry: {
main: path.resolve(__dirname, './src/entry1.js'),
second: path.resolve(__dirname, './src/entry2.js')
}
this.modules
用来收集整个项目的依赖模块,除了入口文件模块,每个模块里面都有一个name
属性,它的值用来表示那个入口文件引用了该模块。比如:
现在有两个入口文件都引用了一个模块:
// 入口文件1
const depModule = require('./module')
// 入口文件2
const depModule = require('./module')
// 模块文件
const name = '19Qingfeng'
module.exports = {
name
}
this.modules
打印如下:
{
id: './example/src/module.js',
dependencies: Set(1) { './example/src/module1.js' },
name: [ 'main', 'second' ],
_source: '
"const name = '19Qingfeng';\n" +
'module.exports = {\n' +
' name,\n' +
'};\n' +
"const loader2 = '19Qingfeng';\n" +
"const loader1 = 'https://github.com/19Qingfeng';"
}
可以发现这个模块的name
属性包含了['main', 'second']
,即两个入口文件名。
生成chunks
buildEntryModule(entry) {
Object.keys(entry).forEach(entryName => {
const entryPath = entry[entryName]
const entryObj = this.buildModule(entryName, entryPath)
this.entries.add(entryObj)
// 根据当前入口文件和模块的相互依赖关系,组装成为一个个包含当前入口所有依赖模块的chunk
this.buildUpChunk(entryName, entryObj)
})
}
buildUpChunk(entryName, entryObj) {
const chunk = {
// 每一个入口文件作为一个chunk
name: entryName,
// entry编译后的对象
entryModule: entryObj,
// 寻找与当前entry有关的所有module
modules: Array.from(this.modules).filter(i => i.name.includes(entryName))
}
// 将chunk添加到this.chunks中去
this.chunks.add(chunk)
}
buildUpChunk
的主要作用是根据入口文件名称,从this.modules
把依赖于入口文件的依赖全部挑选出来,组成一个chunks
。
所以,我们知道了什么是chunks
,即一个入口文件对应一个chunks
。后面会将代码分割,分割出来的也是chunks
。
生成最终代码assets
this.chunks.forEach(chunk => {
// 把webpack.config.js配置中的占位符替换为定义的文件名
const parseFileName = output.filename.replace('[name]', chunk.name)
// assets中 { 'main.js': '生成的字符串代码...' }
this.assets[parseFileName] = getSourceCode(chunk)
})
/**
* 把入口文件和起所依赖的文件拼接到一起
* @param {*} chunk
* name 属性入口文件名称
* entryModule 入口文件module对象
* modules 依赖模块对象
*/
function getSourceCode(chunk) {
const { name, entryModule, modules } = chunk
return `
(() => {
var __webpack_modules__ = {
${modules
.map(module => {
return `
'${module.id}': (module) => {
${module._source}
}
`
})
.join(',')}
};
// The module cache
var __webpack_module_cache__ = {};
function __webpack_require__(moduleId) {
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
var module = (__webpack_module_cache__[moduleId] = {
exports: {},
});
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
return module.exports;
}
var __webpack_exports__ = {};
(() => {
${entryModule._source}
})();
})();
`
}
可以发现,我们定义了一个__webpack_require__
方法,它用来替换我们在代码中写的require
或者 import
。同时,定义了一个__webpack_modules__
变量用来存放该入口文件依赖的所有模块。
输出打包文件
Object.keys(this.assets).forEach(fileName => {
const filePath = path.join(output.path, fileName)
fs.writeFileSync(filePath, this.assets[fileName])
})
通过fs.writeFileSync
把最终的代码输出到具体文件中。这样就完成了整个打包过程。
总结
强烈推荐风佬的这篇文章Webapck5核心打包原理全流程解析,本文就是对风佬文章的简写。
转载自:https://juejin.cn/post/7173967302755352612