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.rhkb.cn/news/467440.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【python】Flask

文章目录 1、Flask 介绍2、Flask 实现网页版美颜效果3、参考 1、Flask 介绍 Flask 是一个用 Python 编写的轻量级 Web 应用框架。它设计简单且易于扩展,非常适合小型项目到大型应用的开发。 以下是一些 Flask 库中常用的函数和组件: 一、Flask 应用对…

AI大模型如何重塑软件开发流程?

《AI大模型对软件开发流程的重塑:变革、优势、挑战与展望》 一、传统软件开发流程与模式(一)传统软件开发流程(二)传统软件开发模式面临的问题(一)AI在软件开发中的应用场景(二&…

OceanBase 应用实践:如何处理数据空洞,降低存储空间

问题描述 某保险行业客户的核心系统,从Oracle 迁移到OceanBase之后,发现数据存储空间出现膨胀问题,数据空间 datasize9857715.48M,实际存储占用空间17790702.00M。根据 required_mb - data_mb 值判断,数据空洞较为严重…

Zookeeper运维秘籍:四字命令基础、详解及业务应用全解析

文章目录 一、四字命令基础二、四字命令详解三、四字命令的开启与配置四、结合业务解读四字命令confconsenvi命令Stat命令MNTR命令ruok命令dump命令wchswchp ZooKeeper,作为一款分布式协调服务,提供了丰富的四字命令(也称为四字短语&#xff…

MATLAB大数计算工具箱及其用法

1. MATLAB大数工具箱Variable Precision Integer Arithmetic介绍 Variable Precision Integer Arithmetic是John DErrico 开发的大数运算工具箱,可以用完全任意大小的整数进行算术运算。支持vpi定义的数组和向量。 2.MATLAB代码 完整代码见: https://download.cs…

【野生动物识别系统】Python+深度学习+人工智能+卷积神经网络算法+TensorFlow+ResNet+图像识别

一、介绍 动物识别系统,使用Python作为主要开发语言,基于深度学习TensorFlow框架,搭建卷积神经网络算法。并通过对18种动物数据集进行训练,最后得到一个识别精度较高的模型。并基于Django框架,开发网页端操作平台&…

数据库_SQLite3

下载 1、更新软件源: sudo apt-get update 2、下载SQLite3: sudo apt-get install sqlite3 3、验证: sqlite3启动数据库,出现以下界面代表运行正常。输入 .exit 可以退出数据库 4、安装sqlite3的库 sudo apt-get install l…

PyTorch核心概念:从梯度、计算图到连续性的全面解析(三)

文章目录 Contiguous vs Non-Contiguous TensorTensor and ViewStrides非连续数据结构:Transpose( )在 PyTorch 中检查Contiguous and Non-Contiguous将不连续张量(或视图)转换为连续张量view() 和 reshape() 之间的区别总结 参考文献 Contig…

如何解决导入aioredis报错TypeError: duplicate base class TimeoutError的问题(轻松解决,亲测有效)

下面是根据你的要求撰写的文章: 文章目录 📖 介绍 📖🏡 演示环境 🏡📒 aioredis导包报错 📒📝 解决方案📝 小贴士⚓️ 相关链接 ⚓️📖 介绍 📖 最近在使用Python异步redis模块aioredis的时候遇到了一个错误,导包报错提示 TypeError: duplicate base cla…

基于Springboot+Android的智慧社区互助平台 (含源码数据库)

1.开发环境 开发系统:Windows10/11 架构模式:MVC/前后端分离 JDK版本: Java JDK1.8 开发工具:IDEA 数据库版本: mysql5.7或8.0 数据库可视化工具: navicat 服务器: SpringBoot自带 apache tomcat 主要技术: Java,Springboot,mybatis,mysql,vue 2.视频演示地址 3.功能 这个系…

讨论一个mysql事务问题

最近在阅读一篇关于隔离级别的文章,文章中提到了一种场景,我们下面来分析一下。 文章目录 1、实验环境2、两个实验的语句执行顺序3、关于start transaction和start transaction with consistent snapshot4、实验结果解释4.1、实验14.2、实验24.3、调整实…

Kubernetes-编排工具篇-01-Kustomize与Helm对比

Kustomize与Helm对比 0、前言 K8s 是一个开源容器编排平台,可自动执行容器化应用程序的部署、扩展和管理。近年来,K8s 已成为采用云原生架构和容器化技术的组织的标准。 但是由于K8s的复杂性,所以很多公司以及开源组织都在开发相关的工具来…

确定图像的熵和各向异性 Halcon entropy_gray 解析

1、图像的熵 1.1 介绍 图像熵(image entropy)是图像“繁忙”程度的估计值,它表示为图像灰度级集合的比特平均数,单位比特/像素,也描述了图像信源的平均信息量。熵指的是体系的混乱程度,对于图像而言&#…

数字IC后端设计实现之Innovus自动修复Min Step DRC Violation方案

在实际IC后端项目中我们经常会遇到min step的DRC Violation,如下图所示。 在咱们IC后端训练营项目中也会遇到这类DRC Violation。这类DRC Violation的本质是出现Metal的Notch,即metal有凹槽。 如果是pg net的 Min Step问题,我们可以使用下面的…

进程相关内容

进程内容 进程类型守护进程进程的概念查看进程信息父子进程创建子进程进程结束 – exit/_exit进程回收 –wait waitpid 进程类型 交互进程 (Interactive Process) 交互进程是由用户通过终端或图形界面直接启动的进程,例如我们在命令行输入的命令。它通常需要等待用…

石墨舟氮气柜:半导体制造中的关键保护设备

石墨舟是由高纯度石墨材料制成的,主要用于承载硅片或其他基板材料通过高温处理过程,是制造半导体器件和太阳能电池片的关键设备之一。 石墨舟在空气中容易与氧气发生反应,尤其是在高温处理后,表面可能更为敏感;石墨舟具…

rabbitMq双节点高可用集群安装(亲测可用)

查询系统版本 cat /etc/redhat-release CentOS Linux release 7.7.1908 (Core) rabbitmq v3.9.13 (centos7支持比较大的版本了,后面版本貌似都是centos8以上) erlang erlang-23.3.4.11-1.el7.x86_64 (需要和rabbitmq版本匹配&…

简单介绍一下mvvm mvc mvp以及区别、历史

MVC(Model - View - Controller) 因MVC架构的灵活性,架构图形式很多,仅供参考 历史: MVC 是最早出现的软件架构模式之一,其历史可以追溯到 20 世纪 70 年代,最初被用于 Smalltalk - 80 环境。…

Nordic SoftDevice蓝牙主机操作流程

Nordic SoftDevice蓝牙主机操作流程 之前学习nordic的nus client 主机例程时做了些笔记,现在有空重新整理了一下发出来。 NRF_SDH_BLE_OBSERVER 宏介绍 这个宏可以设置多个BLE事件的回调函数,并按设置的优先级依次执行。这么一来,就可以将…

C++生成高斯分布随机数

简单实现 在 C 中&#xff0c;可以使用 头文件中的功能来生成正态分布&#xff08;高斯分布&#xff09;随机数。以下是一个示例&#xff0c;展示如何使用 C11 及以上版本的标准库生成正态分布随机数。 #include <iostream> #include <random> #include <cmat…