手写 mini 版 Webpack

news/2024/12/5 5:35:36/

目录

1. mini 版 Webpack 打包流程

2. 创建 minipack.js

2.1 需要用到的插件库

2.1.1 babylon —— 解析 JavaScript 语法,生产 AST 语法树

2.1.2 babel-traverse —— 对 AST 进行遍历、转换的工具

2.1.3 transformFromAst —— 将 ES6、ES7 等高级的语法,转化为 ES5 的语法

2.2 读取文件内容,并提取它的依赖关系

2.3 递归获取项目的所有依赖(绘制项目依赖图谱)

2.4 自定义实现 require 方法,找到导出变量的引用逻辑

2.5 创建 dist 目录,将打包的内容写入 main.js 中

3. 测试 minipack.js

3.1 添加 minipack.js 同级文件/文件夹

3.2 初始化测试目录 example

3.3 完善 package.json,安装必要依赖

3.4 完善 index.html,填充打包文件

3.5 项目打包、打包效果展示

4. 分析打包生成的文件 dist/main.js

5. 真正的 Webpack 打包流程

6. Webpack 热更新(Hot Module Replacement)

6.1 什么是 Webpack 热更新?

6.2 热更新原理 —— WebSocket

7. 参考文章


Webpack 是前端最常用的构建工具之一,为了解 Webpack 整体打包流程中:需要做的事,需要输出的结果,因此手写 mini 版 Webpack

深入浅出 Webpack · 深入浅出 Webpackhttp://webpack.wuhaolin.cn/

1. mini 版 Webpack 打包流程

  • 从入口文件开始解析
  • 查找入口文件引入了哪些 JavaScript 文件,找到依赖关系
  • 递归遍历引入的其他 JavaScript 文件,生成最终的依赖关系图谱
  • 将 ES6 语法转化成 ES5
  • 最终生成一个可以在浏览器加载执行的 JavaScript 文件
  • mini 版 Webpack 未涉及 loader、plugin 等复杂功能,只是一个非常简化的例子

2. 创建 minipack.js

2.1 需要用到的插件库

2.1.1 babylon —— 解析 JavaScript 语法,生产 AST 语法树

// babylon 解析 JavaScript 语法,生产 AST 语法树(AST 能把 JavaScript 代码,转化为 JSON 数据结构)

const babylon = require('babylon');

2.1.2 babel-traverse —— 对 AST 进行遍历、转换的工具

// babel-traverse 是一个对 AST 进行遍历、转换的工具

const traverse = require('babel-traverse').default;

2.1.3 transformFromAst —— 将 ES6、ES7 等高级的语法,转化为 ES5 的语法

// 将 es6、es7 等高级的语法,转化为 es5 的语法

const { transformFromAst } = require('babel-core');

2.2 读取文件内容,并提取它的依赖关系

// 每一个 JavaScript 文件,对应一个id
let ID = 0;/*** 读取文件内容,并提取它的依赖关系* @param {*} filename 文件路径* @returns 文件id(唯一)、文件路径、文件的依赖关系、文件代码*/
function createAsset(filename) {const content = fs.readFileSync(filename, 'utf-8');// 获取该文件对应的 AST 抽象语法树const ast = babylon.parse(content, {sourceType: 'module',});// dependencies —— 保存 所依赖模块的 相对路径const dependencies = [];// 通过查找 import 节点,找到该文件的依赖关系(也就是文件中 import 的其他文件)traverse(ast, {ImportDeclaration: ({ node }) => {// 查找import节点dependencies.push(node.source.value);},});// 通过递增计数器,为此模块分配唯一标识符,用于缓存已解析过的文件const id = ID++;// 用 '@babel/preset-env' 将 代码 转换为 浏览器可以运行的内容const { code } = transformFromAst(ast, null, {// `presets` 选项是一组规则,告诉 `babel` 如何传输我们的代码presets: ['@babel/preset-env'],});// 返回此模块的相关信息return {id, // 文件id(唯一)filename, // 文件路径dependencies, // 文件的依赖关系code, // 文件代码};
}

2.3 递归获取项目的所有依赖(绘制项目依赖图谱)

/*** 递归获取项目的所有依赖(绘制项目依赖图谱)* @description 从入口文件开始,递归读取各个依赖文件* @param {*} entry 项目入口文件* @returns*/
function createGraph(entry) {// 读取 入口文件 内容,并提取它的依赖关系const mainAsset = createAsset(entry);// 入口文件的信息(文件id(唯一)、文件路径、文件的依赖关系、文件代码),作为第一项放到数组里const queue = [mainAsset];for (const asset of queue) {asset.mapping = {};// 获取 这个模块的 所在的目录const dirname = path.dirname(asset.filename);// 遍历 这个模块的 文件依赖关系asset.dependencies.forEach((relativePath) => {/*** 获取 每个依赖文件的 绝对路径* 通过将相对路径与父资源目录的路径连接,将相对路径转变为绝对路径*/const absolutePath = path.join(dirname, relativePath);// 递归解析其中所引入的其他资源const child = createAsset(absolutePath);asset.mapping[relativePath] = child.id;// 将 `child` 推入队列, 通过 递归 实现获取项目所有依赖queue.push(child);});}// queue这就是最终的依赖关系图谱return queue;
}

2.4 自定义实现 require 方法,找到导出变量的引用逻辑

/*** 自定义实现 require 方法,找到导出变量的引用逻辑* @param {*} graph 项目的所有依赖* @returns*/
function bundle(graph) {let modules = '';graph.forEach((mod) => {modules += `${mod.id}: [function (require, module, exports) { ${mod.code} },${JSON.stringify(mod.mapping)},],`;});const result = `(function(modules) {function require(id) {const [fn, mapping] = modules[id];function localRequire(name) {return require(mapping[name]);}const module = { exports : {} };fn(localRequire, module, module.exports); return module.exports;}require(0);})({${modules}})`;return result;
}

2.5 创建 dist 目录,将打包的内容写入 main.js 中

// ❤️ 通过入口文件,递归获取项目的所有依赖
const graph = createGraph('./example/entry.js');
// 自定义实现 require 方法,找到导出变量的引用逻辑
const result = bundle(graph);// 创建 dist 目录,将打包的内容写入 main.js 中
fs.mkdir('dist', (err) => {if (!err) {fs.writeFile('dist/main.js', result, (err1) => {if (!err1) console.log('打包成功');});}
});

3. 测试 minipack.js

3.1 添加 minipack.js 同级文件/文件夹

在 minipack.js 同级的位置,添加:

  • example 文件夹(相当于真实项目文件)
  • package.json(使用 npm init -y 初始化)
  • index.html(引入 minipack 打包后的文件,并展示)

3.2 初始化测试目录 example

name.js

export const name = 'mini Webpack By Lyrelion';

message.js

import { name } from './name.js';export default `hello ${name}!`;

entry.js(项目入口文件)

import message from './message.js';// 创建 <p></p> DOM节点
let p = document.createElement('p');
// 将 message 的内容显示到页面中
p.innerHTML = message;
// 追加 DOM 节点
document.body.appendChild(p);

3.3 完善 package.json,安装必要依赖

根据前面写 minipack.js 时用到的依赖,补充 package.json 文件

{"name": "webpack","version": "1.0.0","description": "","main": "entry.js","scripts": {"test": "echo \"Error: no test specified\" && exit 1"},"keywords": [],"author": "","license": "ISC","dependencies": {"@babel/core": "^7.20.7","@babel/preset-env": "^7.20.2","babel-core": "^7.0.0-beta.41","babel-traverse": "^6.26.0","babylon": "^6.18.0","fs": "^0.0.1-security","path": "^0.12.7"}
}

这里强调一下 babel,安装的时候报了很多错

解决方案:我是根据报错提示,调整 babel-core 版本,再重新安装,如下所示 

3.4 完善 index.html,填充打包文件

在 minipack.js 中,写死了打包后输出的文件位置,因此在 index.html 里写死即可

<!DOCTYPE html>
<html><head><meta charset="utf-8" /><title>mini Webpack</title><meta name="viewport" content="width=device-width, initial-scale=1" /></head><body><!-- 引入打包后的 main.js --><script src="./dist/main.js"></script></body>
</html>

3.5 项目打包、打包效果展示

打包前的目录结构:

执行打包命令:node minipack.js

打包成功后的目录结构:

打包效果:

4. 分析打包生成的文件 dist/main.js

main.js 里有一个立即执行函数,接收一个对象,该对象有三个属性

  • 0 代表entry.js;
  • 1 代表message.js;
  • 2 代表name.js

文件执行过程:

  • 从 require(0) 开始执行,调用内置的自定义 require 函数
  • 执行 fn 函数
  • 执行 require('./message.js'),执行 require(mapping['./message.js']),转化为 require(1),获取 modules[1],也就是执行 message.js 的内容
  • 执行 require('./name.js'),转化为 require(2),执行 name.js 的内容
  • 通过递归调用,将代码中导出的属性,放到 exports 对象中,一层层导出到最外层
  • 最终通过 _message["default"] 获取导出的值,页面显示 hello mini Webpack By Lyrelion!

// 文件里是一个立即执行函数
(function (modules) {function require(id) {const [fn, mapping] = modules[id];// ⬅️ 第四步 跳转到这里 此时 mapping[name] = 1,继续执行 require(1)// ⬅️ 第六步 又跳转到这里 此时 mapping[name] = 2,继续执行 require(2)function localRequire(name) {return require(mapping[name]);}const module = { exports: {} };// ⬅️ 第二步 执行 fnfn(localRequire, module, module.exports);return module.exports;}// ⬅️ 第一步 执行 require(0)require(0);
})({// entry.js0: [function (require, module, exports) {"use strict";// ⬅️ 第三步 跳转到这里 继续执行 require('./message.js')var _message = _interopRequireDefault(require("./message.js"));function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }// 创建 <p></p> DOM节点var p = document.createElement('p');// ⬅️ 最后一步 将内容写到 p 标签中// 将 message 的内容显示到页面中p.innerHTML = _message["default"];// 追加 DOM 节点document.body.appendChild(p);},{ "./message.js": 1 },// message.js], 1: [function (require, module, exports) {"use strict";Object.defineProperty(exports, "__esModule", {value: true});exports["default"] = void 0;// ⬅️ 第五步 跳转到这里 继续执行 require('./name.js')var _name = require("./name.js");var _default = "hello ".concat(_name.name, "!");// ⬅️ 第八步 跳到这里 此时 _name 为 {name: 'mini Webpack By Lyrelion'}, 在 exports 对象上设置 default 属性,值为 'hello mini Webpack By Lyrelion!'exports["default"] = _default;},{ "./name.js": 2 },// name.js], 2: [function (require, module, exports) {"use strict";Object.defineProperty(exports, "__esModule", {value: true});exports.name = void 0;var name = 'mini Webpack By Lyrelion';// ⬅️ 第七步 跳到这里 在传入的 exports 对象上添加 name 属性,值为'mini Webpack By Lyrelion'exports.name = name;},{},],
})

5. 真正的 Webpack 打包流程

Webpack 从项目的 entry 入口文件开始递归分析,调用 Loader 对不同文件进行编译;因为 Webpack 默认只能识别 JavaScript 代码,所以 .css 文件、.vue 文件等,必须要通过 Loader 解析成 JavaScript 代码,才能被 Webpack 识别

利用 babel(babylon)将 JavaScript 代码转化为 AST 抽象语法树;

通过 babel-traverse 对 AST 抽象语法树 进行遍历,找到文件的 import 引用节点(依赖关系);因为 依赖文件 都是通过 import 的方式引入,所以找到 import 节点,就找到了文件的依赖关系

给 每个模块 生成唯一标识 ID,并将解析过的模块缓存起来;如果其他地方也引入该模块,就无需重新解析

根据依赖关系,生成依赖图谱,递归遍历所有依赖图谱的模块,组装成一个个包含多个模块的 Chunk(块);

最后,将生成的文件,输出到 output 目录中

6. Webpack 热更新(Hot Module Replacement)

刷新一般分为两种:

  • 一种是页面刷新,不保留页面状态,直接 window.location.reload()
  • 一种是基于 WDS(webpack-dev-server)的模块热替换,局部刷新页面上发生变化的模块,同时保留当前页面的状态,比如复选框的选中状态、输入框的输入等

6.1 什么是 Webpack 热更新?

开发过程中,代码改变后,Webpack 会重新编译,编译后浏览器替换修改的模块,无需刷新整个页面,就能进行局部更新,提升开发体验

HMR 作为 Webpack 内置的功能,可以通过 HotModuleReplacementPlugin 或 --hot 开启

6.2 热更新原理 —— WebSocket

基本原理:

  • 通过 WebSocket 实现,建立 本地服务 和 浏览器 的双向通信;
  • 当代码发生变化,并重新编译后,通知浏览器 重新请求 需要更新的模块,替换 原有的模块;

通过 webpack-dev-server 开启 server 服务,本地 server 启动之后,再去启动 WebSocket 服务,建立 本地服务 和 浏览器 的双向通信

Webpack 每次编译后,会生成一个 Hash 值,Hash 代表每一次编译的唯一标识。本次输出的 Hash 值会编译新生成的文件标识,被作为下次热更新的标识

Webpack 监听文件变化(通过 文件的生成时间 判断是否有变化),当文件变化后,重新编译

编译结束后,通知浏览器请求变化的资源,同时将新生成的 Hash 值传给浏览器,用于下次热更新使用

浏览器请求到最新的模块后,用新模块 替换 旧模块,从而实现 局部刷新

轻松理解webpack热更新原理 - 掘金一种是页面刷新,不保留页面状态,就是简单粗暴,直接window.location.reload()。 另一种是基于WDS (Webpack-dev-server)的模块热替换,只需要局部刷新页面上发生变化的模块,同时可以保留当前的页面状态,比如复选框的选中状态、输入框的输入等。…https://juejin.cn/post/6844904008432222215

7. 参考文章

带你深度解锁Webpack系列(基础篇) - 掘金

带你深度解锁Webpack系列(进阶篇) - 掘金

带你深度解锁Webpack系列(优化篇) - 掘金


http://www.ppmy.cn/news/7956.html

相关文章

EIZO船舶触摸屏维修T1502-B

EIZO船舶触摸屏使用注意事项&#xff1a; 1 由于显示器电子零件的性能需要约30分钟才能稳定,因此在电源开启之后,应调整显示器30分钟以上。 2为了降低因长期使用而出现的发光度变化以及保持稳定的发光度,建议您以较低亮度使用显示器。 3 当显示器长期显示一个图像的情况下再…

Eth05- Eth以太网发送函数代码解析

1 以太网帧的格式 了解发送函数之前先了解一下以太网帧的格式,以太网帧的格式如下所示: PREAMBLE–以太网帧以 7 字节前导码开头,指示帧的开始,并允许发送方和接收方建立位同步。最初,引入PRE(前导码)是为了允许由于信号延迟而损失几个位。但今天的高速以太网不需要前…

hevc 预测单元语法

预测单元PU规定了编码单元的所有预测模式&#xff0c;一切与预测有关的信息都定义在预测单元部分&#xff0c;比如&#xff0c;帧内预测的方向&#xff0c;帧间预测的分割方式&#xff0c;运动矢量预测。以及帧间预测参考图像索引号都属于预测单元的范畴。一个2Nx2N 的编码单元…

DaoCloud 结合 Karmada 打造新一代企业级多云平台

上周 Cloud Native Days China 南京站 Meetup 顺利举行&#xff0c;「DaoCloud 道客」大容器团队技术负责人-张潇在会上以《DaoCloud 结合 Karmada 打造新一代企业级多云平台》为主题&#xff0c;与 Karmada 社区及其合作伙伴一起&#xff0c;共同交流云原生多云多集群生产实践…

T-SQL程序练习04

目录 一、写一个存储过程 &#x1d439;&#x1d456;&#x1d44f;&#x1d45c;&#x1d45b;&#x1d44e;&#x1d450;&#x1d450; 1. 具体要求 2. T-SQL程序代码 3. 结果显示 二、建立存储过程 &#x1d446;&#x1d44e;&#x1d45b;&#x1d43a;&#x1d462;…

Maven是怎么样构建Spring Boot项目的?

准备好项目运行所需的环境后&#xff0c;就可以使用IDEA开发工具搭建一个Spring Boot入门程序了。我们既可以使用Maven方式构建项目&#xff0c;也可以使用Spring Initializr快捷方式构建项目。这里先介绍如何使用Maven方式构建Spring Boot项目&#xff0c;具体步骤如下。 1.初…

Linux模块代码、编译、加载、卸载一条龙

最近要写一个Linux的内核模块&#xff0c;记录一下内核模块的代码编写、编译、加载和卸载的基本流程&#xff0c;以作备忘&#xff0c;也希望能帮到有需要的同学。 模块代码 //代码来自https://yangkuncn.cn/kernel_INIT_WORK.html //init_works.c #include <linux/kernel…

Java死锁

一.死锁是什么&#xff1f; 死锁指两个或者两个以上的线程在执行过程中&#xff0c;去争夺同样一个共享资源&#xff0c;造成的相互等待的现象&#xff0c;如果没有外部干预&#xff0c;线程会一直阻塞&#xff0c;无法往下执行&#xff0c;这样一直处于相互等待资源的线程叫做…