【Webpack4打包机制原理解析】
webpack
是一个打包模块化JavaScript
的工具,在webpack
里一切文件皆模块,通过Loader
转换文件,通过Plugin
注入钩子,最后输出由多个模块组合成的文件。webpack
专注于构建模块化项目。
# 简单版打包模型步骤
我们先从简单的入手看,当 webpack 的配置只有一个出口时,不考虑分包的情况,其实我们只得到了一个bundle.js的文件,这个文件里包含了我们所有用到的js模块,可以直接被加载执行。那么,我可以分析一下它的打包思路,大概有以下4步:
- 利用
babel
完成代码转换及解析,并生成单个文件的依赖模块Map
- 从入口开始递归分析,并生成整个项目的依赖图谱
- 将各个引用模块打包为一个立即执行函数
- 将最终的
bundle
文件写入bundle.js
中
# 单个文件的依赖模块Map
- 我们会可以使用这几个包:
@babel/parser
:负责将代码解析为抽象语法树@babel/traverse
:遍历抽象语法树的工具,我们可以在语法树中解析特定的节点,然后做一些操作,如ImportDeclaratio
n获取通过import
引入的模块,FunctionDeclaration
获取函数@babel/core
:代码转换,如ES6的代码转为ES5的模式
由这几个模块的作用,其实已经可以推断出应该怎样获取单个文件的依赖模块了,转为
Ast
->遍历Ast
->调用ImportDeclaration
。代码如下
// exportDependencies.js const fs = require('fs') const path = require('path') const parser = require('@babel/parser') const traverse = require('@babel/traverse').default const babel = require('@babel/core')const exportDependencies = (filename)=>{const content = fs.readFileSync(filename,'utf-8')// 转为Astconst ast = parser.parse(content, {sourceType : 'module'//babel官方规定必须加这个参数,不然无法识别ES Module})const dependencies = {}//遍历AST抽象语法树traverse(ast, {//调用ImportDeclaration获取通过import引入的模块ImportDeclaration({node}){const dirname = path.dirname(filename)const newFile = './' + path.join(dirname, node.source.value)//保存所依赖的模块dependencies[node.source.value] = newFile}})//通过@babel/core和@babel/preset-env进行代码的转换const {code} = babel.transformFromAst(ast, null, {presets: ["@babel/preset-env"]})return{filename,//该文件名dependencies,//该文件所依赖的模块集合(键值对存储)code//转换后的代码} } module.exports = exportDependencies
可以跑一个例子:
//info.js const a = 1 export a // index.js import info from'./info.js' console.log(info) //testExport.js const exportDependencies = require('./exportDependencies') console.log(exportDependencies('./src/index.js'))
单个文件的依赖模块Map
有了获取单个文件依赖的基础,我们就可以在这基础上,进一步得出整个项目的模块依赖图谱了。首先,从入口开始计算,得到
entryMap
,然后遍历entryMap.dependencies
,取出其value
(即依赖的模块的路径),然后再获取这个依赖模块的依赖图谱,以此类推递归下去即可,代码如下:
const exportDependencies = require('./exportDependencies') //entry为入口文件路径 const exportGraph = (entry)=>{const entryModule = exportDependencies(entry)const graphArray = [entryModule]for(let i = 0; i < graphArray.length; i++){const item = graphArray[i];//拿到文件所依赖的模块集合,dependencies的值参考exportDependenciesconst { dependencies } = item;for(let j in dependencies){graphArray.push(exportDependencies(dependencies[j]))//关键代码,目的是将入口模块及其所有相关的模块放入数组}}//接下来生成图谱const graph = {}graphArray.forEach(item => {graph[item.filename] = {dependencies: item.dependencies,code: item.code}})//可以看出,graph其实是 文件路径名:文件内容 的集合return graph } module.exports = exportGraph
# 输出立即执行函数
首先,我们的代码被加载到页面中的时候,是需要立即执行的。所以输出的
bundle.js
实质上要是一个立即执行函数。我们主要注意以下几点:
- 我们写模块的时候,用的是
import/export
.经转换后,变成了require/exports
- 我们要让
require/exports
能正常运行,那么我们得定义这两个东西,并加到bundle.js
里 - 在依赖图谱里,代码都成了字符串。要执行,可以使用
eval
因此,我们要做这些工作:
- 定义一个
require
函数,require
函数的本质是执行一个模块的代码,然后将相应变量挂载到exports
对象上 - 获取整个项目的依赖图谱,从入口开始,调用
require
方法。完整代码如下:
const exportGraph = require('./exportGraph') // 写入文件,可以用fs.writeFileSync等方法,写入到output.path中 const exportBundle = require('./exportBundle') const exportCode = (entry)=>{//要先把对象转换为字符串,不然在下面的模板字符串中会默认调取对象的toString方法,参数变成[Object object]const graph = JSON.stringify(exportGraph(entry))exportBundle(`(function(graph) {//require函数的本质是执行一个模块的代码,然后将相应变量挂载到exports对象上function require(module) {//localRequire的本质是拿到依赖包的exports变量function localRequire(relativePath) {return require(graph[module].dependencies[relativePath]);}var exports = {};(function(require, exports, code) {eval(code);})(localRequire, exports, graph[module].code);return exports;//函数返回指向局部变量,形成闭包,exports变量在函数执行后不会被摧毁}require('${entry}')})(${graph})`) } module.exports = exportCode
至此,简单打包完成。贴一下我跑的demo的结果。
bundle.js
的文件内容为:
(function(graph) {//require函数的本质是执行一个模块的代码,然后将相应变量挂载到exports对象上function require(module) {//localRequire的本质是拿到依赖包的exports变量function localRequire(relativePath) {returnrequire(graph[module].dependencies[relativePath]);}var exports = {};(function(require, exports, code) {eval(code);})(localRequire, exports, graph[module].code);return exports;//函数返回指向局部变量,形成闭包,exports变量在函数执行后不会被摧毁}require('./src/index.js') })({"./src/index.js":{"dependencies":{"./info.js":"./src/info.js"},"code":"\"use strict\";\n\nvar _info = _interopRequireDefault(require(\"./info.js\"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\n\nconsole.log(_info[\"default\"]);"},"./src/info.js":{"dependencies":{"./name.js":"./src/name.js"},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports[\"default\"] = void 0;\n\nvar _name = require(\"./name.js\");\n\nvar info = \"\".concat(_name.name, \" is beautiful\");\nvar _default = info;\nexports[\"default\"] = _default;"},"./src/name.js":{"dependencies":{},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.name = void 0;\nvar name = 'winty';\nexports.name = name;"}})
# webpack打包流程概括
webpack
的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:
- 初始化参数
- 开始编译 用上一步得到的参数初始Compiler对象,加载所有配置的插件,通 过执行对象的run方法开始执行编译
- 确定入口 根据配置中的 Entry 找出所有入口文件
- 编译模块 从入口文件出发,调用所有配置的 Loader 对模块进行编译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
- 完成模块编译 在经过第4步使用 Loader 翻译完所有模块后, 得到了每个模块被编译后的最终内容及它们之间的依赖关系
- 输出资源 根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再将每个 Chunk 转换成一个单独的文件加入输出列表中,这是可以修改输出内容的最后机会
- 输出完成 在确定好输出内容后,根据配置确定输出的路径和文件名,将文件的内容写入文件系统中。
在以上过程中,
Webpack
会在特定的时间点广播特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,井且插件可以调用Webpack
提供的API
改变Webpack
的运行结果。其实以上7个步骤,可以简单归纳为初始化、编译、输出,三个过程,而这个过程其实就是前面说的基本模型的扩展。
相关文章:
【Webpack4打包机制原理解析】
webpack是一个打包模块化 JavaScript 的工具,在 webpack里一切文件皆模块,通过 Loader 转换文件,通过 Plugin 注入钩子,最后输出由多个模块组合成的文件。webpack专注于构建模块化项目。 # 简单版打包模型步骤 我们先从简单的入手…...
如何提高接口响应速度
在非大数据(几万以上记录)的情况下,影响接口响应速度的因素中最大的是查询数据库的次数,其次才是数组遍历和简单数据处理(如根据已有字段增加新的属性,或计算值)。 一般一次数据库查询需要50毫秒…...

项目敏感配置信息加固
概述 引入jasypt做密码等敏感配置信息的加固 项目集成依赖 pom.xml引入jasypt-spring-boot-starter依赖 <dependency><groupId>com.github.ulisesbocchio</groupId><artifactId>jasypt-spring-boot-starter</artifactId><version>3.0.…...
HCIA-AI课程大纲
该阶段详细介绍各个机器学习范式方法,涵盖有监督、无监督、半监督、强化学习,以及深度学习算法基础,共计 72 课时。 第一节:华为云 ModelArts 云服务开发环境搭建 - (2 课时) - 华为云 ModelArts 云服务简…...

keil program algorithm 出错
前段时间 在 调试下载算法时,遇到一个奇怪的问题 就是 加载下载算法后, 下载算法的RAM空间 大小不能修改为 单片机的最大RAM,只能改到最大4KB的空间大小, 再大就报错 刚开始报错 一直不知道原因,走了很多弯路, 到最…...

SITNE24V2BNQ-3/TR一种瞬态电压抑制器,对标PESD1CAN
SITNE24V2BNQ是一种瞬态电压抑制器,设计用于保护两个汽车控制器区域 网络(CAN)母线不受ESD等瞬变造成的损坏。 SITNE24V2BNQ采用SOT-23封装。标准产品不含铅和卤素。 产品参数 方向:双向通道数:2VRWM(V)(Max):24IPP8/20μS(A)(M…...

Vue3【四】使用Vue2的写法写一个新的组件子组件和根组件
Vue3【四】使用Vue2的写法写一个新的组件 Vue3【四】使用Vue2的写法写一个新的组件 Vue3是向下兼容的,所有可以使用Vue的选项式写法 运行截图 目录结构 文件源码 App.vue <template><div class"app"><h1>你好世界! 我是App根组件<…...

指标体系建设10大坑
在企业经营和运营管理中,指标体系的建设至关重要,它在一定程度上是反映业务的问题状况,影响决策者的决策。但是,在指标体系的建设过程中,常常会存在一些不容忽视的“坑”,今天做个总结,以下为个…...
ubuntu 20.04上docker 使用gpu
要在Docker容器中使用GPU,你需要确保系统上已经安装了正确的NVIDIA驱动程序,并且安装了NVIDIA Container Toolkit。以下是详细的步骤: 1. 安装NVIDIA驱动程序 确保你的系统上已经安装了适当版本的NVIDIA驱动程序。你可以通过运行以下命令来检查驱动程序是否正确安装: nv…...

短剧系统投流版开发,为运营公司投流业务赋能
短剧系统投流版开发是一项复杂的任务,旨在为运营公司的投流业务提供强大的技术支持和赋能。以下是一些关键步骤和考虑因素,以确保短剧系统投流版的成功开发: 一、明确业务需求与目标 首先,需要深入了解运营公司的业务需求、目标…...
入坑必看的几个嵌入式方向热点问题
我们为何要学嵌入式?---需求、薪资、长期发展 嵌入式是成为下一个JAVA吗? 互联网开发和嵌入式开发怎么选? 高薪热门就业方向有哪些? 刚入门,刚毕业,学完没有“工作经验”,能有人要吗&#x…...

电能表如何与智能家居进行有效的融合
随着智能家居技术的不断发展,越来越多的家庭开始使用智能家电、智能照明、智能安防等智能设备,以实现更加便捷、舒适、安全的居住环境。而电能表作为电力系统中不可或缺的一环,不仅承担着计量电能的重要职责,还可以为智能家居系统…...

jmeter多用户登录并退出教程
有时候为了模拟更真实的场景,在项目中需要多用户登录并退出操作,大致参考如下 多用户登录前面已经实现:参考博文 多用户登录并退出jmx文件:百度网盘 提取码:0000 一、多用户退出操作 添加一个setUp线程组࿰…...

阿里云ECS实例镜像本地取证
更新时间:2024年03月21日10:09:37 1. 说明 很多非法案件中,服务器是直接搭建在阿里云上的,比如我们在拿到OSSKey之后(技术方法、其它方法等),可以将涉案服务器镜像导出,在本地进行取证分析。 …...

不要硬来!班组管理有“巧思”
班组管理,听起来似乎是一个充满“硬气”的词汇,让人联想到严肃、刻板的制度和规矩。然而,在实际操作中,我们却需要运用一些“巧思”,以柔克刚,让班组管理既有力度又不失温度。 在班组管理中,我们…...
[原创][Delphi多线程]使用TMonitor和TQueue配合实现TThreadedQueue的经典使用案例.
[简介] 常用网名: 猪头三 出生日期: 1981.XX.XX QQ: 643439947 个人网站: 80x86汇编小站 https://www.x86asm.org 编程生涯: 2001年~至今[共22年] 职业生涯: 20年 开发语言: C/C、80x86ASM、PHP、Perl、Objective-C、Object Pascal、C#、Python 开发工具: Visual Studio、Delph…...

vue3 基于el-tree增加、删除节点(非TypeScript 写法)
话不多说,直接贴代码 <template><div class"custom-tree-container"><!-- <p>Using render-content</p><el-tree style"max-width: 600px" :data"dataSource" show-checkbox node-key"id" …...
小抄 20240607
1 一定要多接触幸运的人,好运的人更有可能继续好运。 这不是迷信,好运的背后是见识、性格、逻辑的加持,一定有过人之处,才能经常好运。 反过来,那些经常走霉运的人,一定是底层逻辑出了问题,陷…...

【GIS教程】土地利用转移矩阵
随着科技社会的不断进步,人类活动对地理环境的影响与塑造日益明显,土地不断的侵蚀与改变也导致一系列的环境问题日益突出。土地利用/覆盖(LUCC)作为全球环境变化研究的重点问题为越来越多的国际研究机构所重视,研究它的…...

API接口测试工具:jmeter的安装、汉化、Jmeter桌面快捷图标和基本使用
文章目录 测试工具:JmeterJmeter安装和配置Jmeter汉化设置中文语言:永久方式设置中文语言:临时方式 设置Jmeter桌面快捷图标jmeter基本用法Jmeter无法保存测试问题解决 测试工具:Jmeter Jmeter依赖于JDK,所以必须确保…...

CTF show Web 红包题第六弹
提示 1.不是SQL注入 2.需要找关键源码 思路 进入页面发现是一个登录框,很难让人不联想到SQL注入,但提示都说了不是SQL注入,所以就不往这方面想了 先查看一下网页源码,发现一段JavaScript代码,有一个关键类ctfs…...

Docker 运行 Kafka 带 SASL 认证教程
Docker 运行 Kafka 带 SASL 认证教程 Docker 运行 Kafka 带 SASL 认证教程一、说明二、环境准备三、编写 Docker Compose 和 jaas文件docker-compose.yml代码说明:server_jaas.conf 四、启动服务五、验证服务六、连接kafka服务七、总结 Docker 运行 Kafka 带 SASL 认…...
工程地质软件市场:发展现状、趋势与策略建议
一、引言 在工程建设领域,准确把握地质条件是确保项目顺利推进和安全运营的关键。工程地质软件作为处理、分析、模拟和展示工程地质数据的重要工具,正发挥着日益重要的作用。它凭借强大的数据处理能力、三维建模功能、空间分析工具和可视化展示手段&…...

HarmonyOS运动开发:如何用mpchart绘制运动配速图表
##鸿蒙核心技术##运动开发##Sensor Service Kit(传感器服务)# 前言 在运动类应用中,运动数据的可视化是提升用户体验的重要环节。通过直观的图表展示运动过程中的关键数据,如配速、距离、卡路里消耗等,用户可以更清晰…...
A2A JS SDK 完整教程:快速入门指南
目录 什么是 A2A JS SDK?A2A JS 安装与设置A2A JS 核心概念创建你的第一个 A2A JS 代理A2A JS 服务端开发A2A JS 客户端使用A2A JS 高级特性A2A JS 最佳实践A2A JS 故障排除 什么是 A2A JS SDK? A2A JS SDK 是一个专为 JavaScript/TypeScript 开发者设计的强大库ÿ…...

Golang——9、反射和文件操作
反射和文件操作 1、反射1.1、reflect.TypeOf()获取任意值的类型对象1.2、reflect.ValueOf()1.3、结构体反射 2、文件操作2.1、os.Open()打开文件2.2、方式一:使用Read()读取文件2.3、方式二:bufio读取文件2.4、方式三:os.ReadFile读取2.5、写…...
MySQL 主从同步异常处理
阅读原文:https://www.xiaozaoshu.top/articles/mysql-m-s-update-pk MySQL 做双主,遇到的这个错误: Could not execute Update_rows event on table ... Error_code: 1032是 MySQL 主从复制时的经典错误之一,通常表示ÿ…...
云原生周刊:k0s 成为 CNCF 沙箱项目
开源项目推荐 HAMi HAMi(原名 k8s‑vGPU‑scheduler)是一款 CNCF Sandbox 级别的开源 K8s 中间件,通过虚拟化 GPU/NPU 等异构设备并支持内存、计算核心时间片隔离及共享调度,为容器提供统一接口,实现细粒度资源配额…...
6个月Python学习计划 Day 16 - 面向对象编程(OOP)基础
第三周 Day 3 🎯 今日目标 理解类(class)和对象(object)的关系学会定义类的属性、方法和构造函数(init)掌握对象的创建与使用初识封装、继承和多态的基本概念(预告) &a…...

Spring AOP代理对象生成原理
代理对象生成的关键类是【AnnotationAwareAspectJAutoProxyCreator】,这个类继承了【BeanPostProcessor】是一个后置处理器 在bean对象生命周期中初始化时执行【org.springframework.beans.factory.config.BeanPostProcessor#postProcessAfterInitialization】方法时…...