webpack是如何进行依赖图谱收集的?

news2024/11/24 3:21:19

我自己学习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个技巧》。内容比较详实,对各个知识点的讲解也十分到位。



有需要的小伙伴,可以点击下方卡片领取,无偿分享

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/185372.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Numpy(7)—字节交换、NumPy 副本和视图、深浅拷贝、矩阵库、NumPy 线性代数、NumPy IO(读写)、NumPy Matplotlib

1.字节交换 import numpy as npA np.array([1, 256, 8755], dtypenp.int16) print(A) print(list(map(hex, A))) print(A.byteswap(inplaceTrue)) print(list(map(hex, A)))2.NumPy 副本和视图 副本是一个数据的完整的拷贝,如果我们对副本进行修改,它不…

【MyBatis 持久层框架】Mapper代理开发详细解读

文章目录1. 前言2. Mapper 代理开发3. 过程剖析4. 总结1. 前言 前面在 MyBatis 快速入门篇中,我们使用了 MyBatis 原生的开发方式操作数据库,解决了 JDBC 操作数据库时的硬编码和操作繁琐的问题。实际上,在 Java 项目中,我们更常…

python3——函数

目录 一、函数定义 二、函数调用 1.打印Hello World 2.判断最大值 3.计算矩形面积 4.help说明文档 三、参数传递 (一)位置参数 (二)关键字参数 (三)默认参数(缺省参数) (四)可变参数(收集参数) 1.位置可变参数(接收所有的位置参数,返回一个元组) 2.关键…

高通开发系列 - MSM8909 lk aboot阶段点灯操作

By: fulinux E-mail: fulinux@sina.com Blog: https://blog.csdn.net/fulinus 喜欢的盆友欢迎点赞和订阅! 你的喜欢就是我写作的动力! 目录 第一种LK提供的接口实现第二种直接操作寄存器这篇文章之前请参考下:高通开发系列 - MSM8909指示灯操作 在LK中点灯有两种方式,一种…

JAVA-定位排查bug

在开发过程中难免会遇到bug,理解bug的含义,定位bug的位置,对于解决bug至关重要!掌握高效的排错技巧,对于程序员来说必不可少。 目录 一、错误异常的分类 二、常见报错信息及原因(持续更新中)…

域内权限维持:AdminSDHolder

01、简介 AdminSDHolder是一个特殊的AD容器,通常作为某些特权组成员的对象的安全模板。Active Directory将采用AdminSDHolder对象的ACL并定期将其应用于所有受保护的AD账户和组,以防止意外和无意的修改并确保对这些对象的访问是安全的。如果攻击者能完全…

Flex布局和主要属性用法详解

目录 前言 一个小例子 基本概念: 设置在主轴上的排列方式 设置在侧轴上的排列方式 更换主轴和侧轴方向 换行 align-content属性 元素(子容器)的相关属性 flex-basis flex-grow flex-shrink属性 flex属性 前言 flex布局是继标准…

JDBC-Statement

1.Statement执行静态sql语句(“字符串”) 返回结果 2.!实际工作一般用PreparedStatement来进行sql语句的执行,因为sql注入的风险 3and4.SQl注入就是Statement没有检查我们输入sql语句,一些别有用心的可能写一些危害数据…

智能手表主控芯片盘点,智能手表GUI,智能手表市场

聚焦:无线连接芯片,市场,技术 祝大家新年快乐,开工大吉!趁寒假简单梳理了下智能手表应用,做个分享,不对的地方欢迎交流指正; 01 市场容量,分类及拓扑 2个数据供参考 一个…

C++ dll、lib 的定义以及引用,

最近在研究socket,发现socket程序要依赖ws2_32.dll,涉及到动态链接库,有点懵,上网恶补了一下链接库的知识,最后总结出这么一篇文章 链接库分为两种:动态链接库(dll) 和静态链接库(lib) 动态链接库 : 动态链…

【C++】C++11语法解析

🌈欢迎来到C专栏~~C11 (꒪ꇴ꒪(꒪ꇴ꒪ )🐣,我是Scort目前状态:大三非科班啃C中🌍博客主页:张小姐的猫~江湖背景快上车🚘,握好方向盘跟我有一起打天下嘞!送给自己的一句鸡汤&#x1f…

大数据分析案例-基于多元线性回归算法构建用户信用评分模型

🤵‍♂️ 个人主页:艾派森的个人主页 ✍🏻作者简介:Python学习者 🐋 希望大家多多支持,我们一起进步!😄 如果文章对你有帮助的话, 欢迎评论 💬点赞&#x1f4…

ue4c++日记9(指定区域生成角色)

目录 创建C类 头文件 代码文件 结果 创建C类 头文件 // Fill out your copyright notice in the Description page of Project Settings.#pragma once#include "CoreMinimal.h" #include "GameFramework/Actor.h" #include "ASPawnVolum.generated…

vSphere with Tanzu概念介绍

vSphere with Tanzu是在vSphere7.0及之后出现的新功能,它可以在虚拟化层创建一个Kubernetes 控制平面,并将vSphere 集群资源转化为Kubernetes集群资源,这样可以直接在ESXI主机上运行Kubernetes工作负载,创建Kubernetes集群并部署容…

测试环境频繁Full GC问题的解决思路

背景 上游调用方,反馈当前welink-front服务不可用; 临时解决办法 手动重启welink-front服务,重启之后观测到业务日志正常刷,说明该问题暂时得到了解决; 但没过多久,上游调用方的同学又找来了&#xff0…

C++ 继承

一. 概念 继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对程序设计的层次结构,体现了由…

IDEA开发 常用代码规范插件 常用辅助类插件

规范类 阿里巴巴规范 Alibaba Java Coding Guidelines 插件地址 阿里官方出的开源的代码规范插件 GitHub地址 配合阿里出的规范手册《码出高效 Java开发手册》食用效果更佳 不用购买哈!GitHub上开源的 码出高效:Java开发手册下载 静态代码检查 Sonar…

数学建模学习笔记(14)聚类模型

聚类模型K均值聚类算法和K均值聚类算法系统聚类算法(层次聚类)DBSCAN聚类算法聚类问题概述:把样本划分为由相似的对象组成的多个类的过程。 K均值聚类算法和K均值聚类算法 K均值聚类算法流程: 指定需要划分的簇的个数K。随机选…

【Redis | 黑马点评】商户查询缓存

文章目录什么是缓存?添加商户缓存缓存更新策略主动更新策略实现商铺查询的缓存与数据库双写一致缓存穿透问题的解决思路编码解决商品查询的缓存穿透问题缓存雪崩问题及解决思路缓存击穿问题及解决思路基于互斥锁的方式解决缓存击穿问题基于逻辑过期的方式解决缓存击…

初始网络编程

专栏简介: JavaEE从入门到进阶 题目来源: leetcode,牛客,剑指offer. 创作目标: 记录学习JavaEE学习历程 希望在提升自己的同时,帮助他人,,与大家一起共同进步,互相成长. 学历代表过去,能力代表现在,学习能力代表未来! 目录 1.网络发展史 1.1 独立模式 1.2 网络互联 1.3 局…