webpack 执行流程 — 实现 myWebpack

前言

实现 myWebpack 主要是为了更好的理解,webpack 中的工作流程,一切都是最简单的实现,不包含细节内容和边界处理,涉及到 ast 抽象语法树和编译代码部分,最好可以打印出来观察一下,方便后续的理解。

react 项目中的 webapck

为了更好的了解 Webpack 的执行流程,我们可以先通过观察 react 项目结构中,有关 webpack 的一些内容。当然我们得先拥有一个新的项目,可以通过下面的步骤得到:

  • 使用 【create-react-app 项目名称】命令创建项目
  • 进入对应的项目目录,运行 npm run eject 命令拉取,react 项目中和 webapck 相关的配置
    相比于正常的 react 项目,会多出来两个文件目录分别是 configscripts

image.png

config 目录下主要存放的是 webpack 相关的配置内容:

image.png

scripts 目录下主要存放的是在 pakage.json 中存放的 3 个默认 script 脚本相关的内容:

image.png
image.png

主要看 scripts 目录下的 build.js 文件,这里面引入了 webpack 并且调用 webpack(config) 得到了 compiler 对象,最后使用了 compiler.run(callback) 的方式开始进行打包,代码中具体位置如下:

在这里插入图片描述

image.png

Webpack 执行流程

通过 react 项目中的目录结构结合以及 webpack 中的 Node 接口相关内容,可得到以下几个阶段:

  • 解析配置参数 —— 合并 shell 传入和 webpack.config.js 文件配置参数
  • 初始化 Compiler —— 通过 webpack(config) 得到 Compiler 对象,并注册所有配置插件,插件监听 webpack 构建生命周期的事件节点,做出相应处理
  • 开始编译 —— 调用 Compiler 对象 run() 方法开始执行编译
  • 确定入口 —— 根据配置的 entry 找出所有入口文件,开始解析文件,并构建 AST 语法树,找出依赖模块,进行递归处理
  • 编译模块 —— 递归中根据文件类型和 loader 配置,调用所有配置的 loader 对文件进行转换,再找出该模块依赖的模块,再递归本步骤,直到所有入口依赖的文件都经过了本步骤的处理
  • 完成模块编译 —— 模块编译结束后,得到每个文件结果,包含每个模块以及他们之间的依赖关系
  • 输出资源 —— 根据 entry 或分包配置生成代码块 chunk,再把每个 chunk 转换成一个单独的文件加入到输出列表

    PS:输出资源 这一步是修改输出内容的最后机会

  • 输出完成 —— 根据配置确定输出路径和命名,输出所有 chunk 到文件系统

核心流程图示:

实现 myWebpack

准备工作

根据 react 项目中的目录结构,可以得到一个简单 my-webpac 的项目结构:

  • config 目录 —— 存放的是 webpack.config.js 相关配置
  • script 目录 —— 存放的是 script 脚本需要执行的 js 文件
  • lib 目录 —— 存放的就是 myWebpack 库(需要自己实现)
  • src 目录 —— 就是 webpack.config.js 中默认的入口文件目录,其中的 index.js 为入口文件,其他的 js 文件均属于要测试打包的 js 模块

在后面的内容中为了更好的实现模块化,目录结构可能会稍微进行修改,最初的文件结构如下:

my-webpack
├─ config
│  └─ webpack.config.js
├─ lib
│  └─ myWebpack
│     └─ index.js
├─ package-lock.json
├─ package.json
├─ script
│  └─ build.js
└─ src├─ add.js├─ desc.js└─ index.js

开始实现

简单配置 config 目录下的 webpack.config.js

// config/webpack.config.jsmodule.exports = {entry: "./src/index.js",output: {path: 'dist',filename: 'index.js'}
};

实现 script 目录下的 build.js

这里只需要引入 myWebpack.jswebapck.config.js 文件,通过把配置内容 config 传入 myWebpack() 方法,并执行得到 compiler 对象,最终通过 compiler.run() 开始执行打包的处理程序

// script/build.jsconst myWebpack  = require('../lib/myWebpack'); // 这里相当于 require('../lib/myWebpack/index.js')
const config  = require('../config/webpack.config.js');//  获得 compiler 对象
const compiler = myWebpack(config);// 开始打包
compiler.run();

实现 lib 目录下的 myWebpack 的具体内容(即其目录下的 index.js

根据 build.js 中对 myWebpack 使用方式,可以知道在 myWebpack 必然是一个 function 且,其返回值必须是 Compiler 类的实例对象,毕竟被称为 compiler 对象。在这就出现了一个 Compiler 类的相关内容,为了更好的模块化,我们在 lib/myWebpack 目录下新建 compiler.js 文件,里面专门实现 Compiler 类的相关逻辑。

所以,在 lib/myWebpack/index.js 中的处理就是实现 myWebpack 函数,引入 Compiler 类并把 new Compiler(config) 的结果进行返回即可:

// lib/myWebpack/index.jsconst Compiler = require('./Compiler.js')function myWebpack(config) {return new Compiler(config)
}module.exports = myWebpack

实现 lib 目录下 myWebpack 中的 compiler.js 内容

根据 build.js 中对 compiler 对象的使用方式,compiler.js 中必然会存在 Compiler 类 ,并且肯定存在 run() 方法,而且 run() 方法中需要处理的几件事可以归纳为:

  • 根据 entry 配置中的路径,将文件解析成 ast 抽象语法树
  • 根据 ast 收集依赖存放自定义 deps 对象上
  • 根据 ast 编译成可以在浏览器上正常运行的 code 内容
  • 以及把编译好的 code 通过 output 配置中的 pathfilename 写入到文件系统
    其中,前三步属于编译解析的内容,因此,具体逻辑我们可以抽离到 lib/myWebpack/parser.js 中实现并向外暴露对应内容即可,并且放在 Compiler 类里面的 build()方法统一处理,最后一步输出资源可以抽离到 Compiler 类里面的 generate()方法中。
// lib/myWebpack/compiler.jsconst { getAst, getDeps, getCode } = require('./parser.js')
const fs = require('fs')
const path = require('path')class Compiler {constructor(options = {}) {// webpack 配置对象this.options = options// 所有依赖的容器this.modules = []}// 启动打包run() {// 获取 options 中的路径const filePath = this.options.entry// 首次构建,获取入口文件信息const fileInfo = this.build(filePath)// 保存文件信息this.modules.push(fileInfo)// 遍历所有依赖this.modules.forEach((fileInfo) => {// 获取当前文件所有依赖: { relativePath: absolutePath }const deps = fileInfo.depsfor (const relativePath in deps) {// 获取对应绝对路径const absolutePath = deps[relativePath]// 对依赖文件进行打包处理const fileInfo = this.build(absolutePath)// 将打包后的结果保存到 modules 中,方便后面进行处理this.modules.push(fileInfo)}})// 将 modules 数组整理成更好的依赖关系图/*{'index.js': {'code': 'xxx','deps': {[relativePath]: [absolutePath]} }}*/const depsGraph = this.modules.reduce(function (graph, module) {return {...graph,[module.filePath]: {code: module.code,deps: module.deps,},}}, {})// 根据依赖关系图构建输出内容this.generate(depsGraph)}// 开始构建build(filePath) {// 将文件解析成 ast 抽象语法树const ast = getAst(filePath)// 根据 ast 收集依赖:{ relativePath: absolutePath }const deps = getDeps(ast, filePath)// 根据 ast 编译成 codeconst code = getCode(ast)return {filePath, // 当前文件路径deps, // 当前文件的所有依赖code, // 当前文件解析过的代码}}// 生成输出资源generate(depsGraph) {const bundle = `(function(depsGraph){// require 加载入口文件function require(module){// 定义暴露对象var exports = {};// require 内部在定义 localRequire 是为了让 require 递归function localRequire(relativePath){// 找到引入模块的绝对路径,通过 require 进行加载return require(depsGraph[module].deps[relativePath]);}(function(require, exports, code){eval(code);})(localRequire, exports, depsGraph[module].code);// 作为 require 的返回值 —— 让后面的 require 函数能得到被暴露的内容return exports;}require('${this.options.entry}');})(${JSON.stringify(depsGraph)});`const { output } = this.optionsconst dirPath = path.resolve(output.path)const filePath = path.join(dirPath, output.filename)// 如果指定目录不存在就创建目录if (!fs.existsSync(dirPath)) {fs.mkdirSync(dirPath)}// 写入文件fs.writeFileSync(filePath, bundle.trim(), 'utf-8')}
}module.exports = Compiler
generate() 方法中 bundle 变量内容的解释
  • 外部包裹一个立即执行的匿名函数,主要就是为了生成独立作用域,实现 js 的模块化
  • 其中的 require() 方法,就是通过 eval 函数去执行,被编译后的 code,因为被编译后的 code 是字符串形式的 js 代码
  • require() 方法中的 localRequire () 方法实际上执行的还是 require() 方法本身,但对当前模块路径做了一定处理,这里其实就是递归
  • require() 方法中还有一个立即执行的匿名函数,接收三个参数:require, exports, code,其中 code 参数容易理解,但是为什么我们需要传递 require, exports 参数呢?
    • 这一点我们可以通过看被编译之后的 code 的内容就知道了,例如入口文件 index.js 和它里面引入 add.js 的编译结果 code 如下:
// index.js 内容编译结果 => 这里需要使用到 require 方法,因此外部必须传入
"'use strict';
var _add = _interopRequireDefault(require('./add.js'));
var _desc = _interopRequireDefault(require('./desc.js'));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
console.log('add = ', (0, _add['default'])(1, 2));
console.log('desc = ', (0, _desc['default'])(3, 1));"// add.js 内容编译结果 => 这里需要使用到 exports 对象,因此外部必须传入
"'use strict';
Object.defineProperty(exports, '__esModule', {  value: true});
exports['default'] = void 0;
function add(x, y) {  return x + y;}
var _default = add;
exports['default'] = _default;"

实现 lib 目录下 myWebpack 中的 parser.js 内容

这里需要做的就是下面的三件事:

  • 根据 entry 配置中的路径,将文件解析成 ast 抽象语法树,需要借助 @babel/parser 中的 parse 方法
  • 根据 ast 收集依赖存放自定义 deps 对象上,借助 @babel/traverse 遍历 ast 中的 program.body,方便在特定时机收集依赖
  • 根据 ast 编译成可以在浏览器上正常运行的 code 内容,需要借助 @babel/core 中的 transformFromAst 方法
// lib/myWebpack/parser.jsconst babelParser = require('@babel/parser')
const { transformFromAst } = require('@babel/core')
const babelTraverse = require('@babel/traverse').default
const path = require('path')
const fs = require('fs')const parser = {getAst(filePath) {// 通过 options.entry 读入口文件const file = fs.readFileSync(filePath, 'utf-8')// 将入口文件内容解析成 ast —— 抽象语法树const ast = babelParser.parse(file, {sourceType: 'module', // 处理被解析文件中的 ES module})return ast},getDeps(ast, filePath) {// 获取到文件所在文件夹的路径const dirname = path.dirname(filePath)// 存储依赖的容器const deps = {}// 根据 ast 收集依赖babelTraverse(ast, {// 内部会遍历 ast 中的 program.body,根据对应的语句类型进行执行// ImportDeclaration(code) 方法会在 type === "ImportDeclaration" 时触发ImportDeclaration({ node }) {// 获取当前文件的相对路径const relativePath = node.source.value// 添加依赖:{ relativePath: absolutePath }deps[relativePath] = path.resolve(dirname, relativePath)},})return deps},getCode(ast) {// 编译代码: 将浏览器中不能被识别的语法进行编译const { code } = transformFromAst(ast, null, {presets: ['@babel/preset-env'],})return code},
}module.exports = parser

最终的目录结构

my-webpack
├─ config
│  └─ webpack.config.js
├─ lib
│  └─ myWebpack
│     ├─ compiler.js
│     ├─ index.js
│     └─ parser.js
├─ package-lock.json
├─ package.json
├─ README.md
├─ script
│  └─ build.js
└─ src├─ add.js├─ desc.js└─ index.js

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

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

相关文章

【WRF模拟】全过程总结:WPS预处理及WRF运行

【WRF模拟】全过程总结:WPS预处理及WRF运行 1 数据准备1.1 嵌套域设置(Customize domain)-基于QGis中gis4wrf插件1.2 静态地理数据1.2.1 叶面积指数LAI和植被覆盖度Fpar(月尺度)1.2.2 地面反照率(月尺度)1.2.3 土地利用类型+不透水面积1.2.4 数据处理:geotiff→tiff(W…

详解Gemini API的使用:在国内实现大模型对话与目标检测教程

摘要:本博客介绍了如何利用Gemini API实现多轮对话和图像目标检测识别功能,在Python中快速搭建自己的大模型完成实际任务。通过详细的步骤解析,介绍了如何申请Gemini API密钥,调用API、对话实现的代码,给出了上传图片识…

「QT」几何数据类 之 QPoint 整型点类

✨博客主页何曾参静谧的博客📌文章专栏「QT」QT5程序设计📚全部专栏「VS」Visual Studio「C/C」C/C程序设计「UG/NX」BlockUI集合「Win」Windows程序设计「DSA」数据结构与算法「UG/NX」NX二次开发「QT」QT5程序设计「File」数据文件格式「PK」Parasolid…

vue页签

效果: 快来学习: Vue 3 Composition API 和 script setup 语法 Composition API:Vue 3 引入的 Composition API 相比 Vue 2 的 Options API 提供了更灵活的代码组织方式。使用 setup 函数,可以将组件的所有功能和逻辑集中在一起&a…

参数高效微调

参数高效微调 参数高效微调简介 对于预训练数据涉及较少的垂直领域,大语言模型需要对这些领域及相应的下游任务进行适配。上下文学习和指令微调是进行下游任务适配的有效途径,但它们在效果或效率上存在缺陷。为弥补这些不足,参数高效微调&am…

第3篇 滑动开关控制LED__ARM汇编语言工程<一>

Q:如何设计实现滑动开关控制LED的ARM汇编程序呢?与Nios II汇编语言有何不同呢? A:基本原理:该应用程序用到DE1-SoC开发板上的10个红色LED、10个滑动开关SW和4个按钮开关。DE1-SoC_Computer system的qsys系统中IP的硬件…

Windows配置hosts文件域名本地解析IP地址,网页打开

在Windows系统中,配置hosts文件可以实现对域名的本地解析,即将特定的域名映射到指定的IP地址。以下是在Windows系统中配置hosts文件的详细步骤: 一、找到hosts文件位置 “C:\Windows\System32\drivers\etc” 二、备份hosts文件并打开 建议…

【主机游戏】艾尔登法环游戏攻略

艾尔登法环,作为一款备受好评但优化问题频发的游戏,就连马斯克都夸过 今天介绍一下这款游戏 https://pan.quark.cn/s/24760186ac0b 角色升级 在《艾尔登法环》中,角色升级需要找到梅琳娜。你可以在关卡前废墟的营地附近,风暴关…

网络原理(应用层)->HTTP

前言 大家好我是小帅,今天我们来了解应用层协议HTTP 文章目录 1. HTTP 请求响应格式(重点)1.1 HTTP 协议的⼯作过程1.2 HTTP请求格式1. 3HTTP响应格式 2. HTTP 请求 (Request)2.1 使⽤ ping 命令查看域名对应的 IP 地址2.2 URL encode2.3 认识…

JavaScript中执行上下文和执行栈是什么?

一、执行上下文 简单的来说,执行上下文是一种对Javascript代码执行环境的抽象概念,也就是说只要有Javascript代码运行,那么它就一定是运行在执行上下文中 执行上下文的类型分为三种: 全局执行上下文:只有一个&#…

2023上半年下午1,2

问题1不要看图1-1父图,直接看图1-2子图去找 用户就是农户和租户 按数据流输入的词语后面加表字即D的名称,流向D的 信息有包含,子图加了,父图就不平衡了 添加图一般不加实体,加联系(菱形)&#x…

Linux基础(2)

学习地点(泷羽sec的个人空间-泷羽sec个人主页-哔哩哔哩视频 (bilibili.com)) LInux目录介绍 Linux常见目录及作用 /:操作系统的根路径 /bin:存储二进制可执行目录,普通用户和管理员都可以执行的命令 /etc:…

算法简介:动态规划

动态规划 1. 动态规划2. 案例2.1 旅游行程最优化2.2 最长公共子串 1. 动态规划 背包问题:背包可以容纳的重量是4磅,吉他为1磅,价值1500元;音响为4磅,价值3000元;笔记本电脑为3磅,价值为2000元。…

解释区块链技术的应用场景和优势。

区块链技术的应用场景包括但不限于以下几个方面: 1. 金融领域:区块链技术可以用于跨境支付、智能合约、数字货币和资产管理等方面,提供更安全、快速和可追溯的交易体验。 2. 物联网领域:区块链技术可以为物联网设备提供身份验证…

【EMNLP2024】基于多轮课程学习的大语言模型蒸馏算法 TAPIR

近日,阿里云人工智能平台PAI与复旦大学王鹏教授团队合作,在自然语言处理顶级会议EMNLP 2024 上发表论文《Distilling Instruction-following Abilities of Large Language Models with Task-aware Curriculum Planning》。文章提出了一个名为 TAPIR 的知…

棱镜七彩参加“融易行”产融对接南京站项目路演活动 展示供应链安全创新成果

近日,江苏省软件强链“融易行”产融对接南京站活动圆满举行,棱镜七彩作为江苏省重点软件企业受邀参加活动,并展示了公司在供应链安全与开源治理方面的创新成就。 本次活动由江苏省工业和信息化厅、南京市工业和信息化局主办,关键软…

5_api_intro_imagerecognition_html2word

HTML 转 Word API 接口 支持网页转 Word,高效转换为 Word,提供永久链接。 1. 产品功能 超高性能转换效率;支持将传递的 HTML 转换为 Word,支持 HTML 中的 CSS 格式在 Word 文档中的呈现;支持传递网站的 URL&#xff0c…

软件工程 软考

开发大型软件系统适用螺旋模型或者RUP模型 螺旋模型强调了风险分析,特别适用于庞大而复杂的、高风险的管理信息系统的开发。喷泉模型是一种以用户需求为动力,以对象为为驱动的模型,主要用于描述面向对象的软件开发过程。该模型的各个阶段没有…

蓝桥杯 懒洋洋字符串--字符串读入

题目 代码 #include <iostream>using namespace std;int main(){int n;cin>>n;char s[210][4];int ans0;for(int i0;i<n;i){scanf("%s",s[i]);}for(int i0;i<n;i){char as[i][0];char bs[i][1];char cs[i][2];// cout<<a<< <<b…

GS-Blur数据集:首个基于3D场景合成的156,209对多样化真实感模糊图像数据集。

2024-10-31&#xff0c;由韩国首尔国立大学的研究团队创建的GS-Blur数据集&#xff0c;通过3D场景重建和相机视角移动合成了多样化的真实感模糊图像&#xff0c;为图像去模糊领域提供了一个大规模、高覆盖度的新工具&#xff0c;显著提升了去模糊算法在真实世界场景中的泛化能力…