我自己学习webpack
已有很长时间了,但是经常会遇到这样的问题: 可以熟练配置webpack
的一些常用配置,但是对一些不常见的api
或者概念总是云里雾里。因此,对着网上资料手写了一个简易版的webpack
,现在对其中的依赖图谱收集部分进行梳理,搞清楚webpack
是如何进行打包的。
打包流程
获取入口文件
根据 webpack.config.js
配置文件 的 entry
属性获取入口文件的绝对路径
getEntry() {let entry = Object.create(null)const { entry: optionsEntry } = this.optionsif (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. 返回modulereturn module
}
编译的工作主要在handleWebpackCompiler
函数中:
handleWebpackCompiler(moduleName, modulePath) {// 将当前模块路径相对于项目启动根目录计算出相对路径 作为模块IDconst 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'})// 深度优先,遍历语法ASTtraverse(ast, {// 当遇到require语句时CallExpression: nodePath => {const node = nodePath.nodeif (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: [] // 该模块属于哪个入口文件
}
编译模块
1.利用babel
把入口文件代码转化为AST
抽象语法树,找出模块中的require
语句,并把require
替换为__webpack_require__
,因为我们自己会实现一个__webpack_require__
方法,所以在开发过程中就可以使用require
而不会报错。2.找到依赖的模块的相对路径,通过module.dependencies.add(moduleId)
加入到父模块的dependencies
, 这样就构成了一个依赖关系。3.对依赖模块递归编译,找出依赖的依赖,这样就构成了依赖图谱的收集。4.this.entries
用来存储多入口文件,比如:
entry: {main: path.resolve(__dirname, './src/entry1.js'),second: path.resolve(__dirname, './src/entry2.js')
}
5.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)// 根据当前入口文件和模块的相互依赖关系,组装成为一个个包含当前入口所有依赖模块的chunkthis.buildUpChunk(entryName, entryObj)})
}
buildUpChunk(entryName, entryObj) {const chunk = {// 每一个入口文件作为一个chunkname: entryName,// entry编译后的对象entryModule: entryObj,// 寻找与当前entry有关的所有modulemodules: 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 } = chunkreturn `(() => {var __webpack_modules__ = {${modules.map(module => {return `'${module.id}': (module) => {${module._source}}`}).join(',')}};// The module cachevar __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
把最终的代码输出到具体文件中。这样就完成了整个打包过程。
最后
最近找到一个VUE的文档,它将VUE的各个知识点进行了总结,整理成了《Vue 开发必须知道的36个技巧》。内容比较详实,对各个知识点的讲解也十分到位。
有需要的小伙伴,可以点击下方卡片领取,无偿分享