手写 mini 版 Webpack

news2024/11/16 12:04:49

目录

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: {} };
    // ⬅️ 第二步 执行 fn
    fn(localRequire, module, module.exports);
    return module.exports;
  }
  // ⬅️ 第一步 执行 require(0)
  require(0);
})({
  // entry.js
  0: [
    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.coloradmin.cn/o/129220.html

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

相关文章

[Verilog]有限状态机设计举例

有限状态机设计举例 摘要&#xff1a;有限状态机&#xff08;FSM&#xff09;是许多数字系统中用来控制系统和数据流路径行为的时序电路。FSM的实例包括控制单元和时序。 本实验介绍了两种类型的FSM&#xff08;Mealy和Moore&#xff09;的概念&#xff0c;以及开发此类状态机的…

Codeforces Round #837 (Div. 2)

A. Hossam and Combinatorics 题目链接&#xff1a;Problem - A - Codeforces 样例输入&#xff1a; 2 5 6 2 3 8 1 6 7 2 8 3 2 10样例输出&#xff1a; 2 4题意&#xff1a;给定一个有n个元素的数组&#xff0c;然后让我们求出有多少对(i,j)满足|a[i]-a[j]|max|a[p]-q[q]…

Hudi学习01 -- Hudi简介及编译安装

文章目录Hudi简介Hudi概述Hudi特性Hudi使用场景Hudi编译安装安装Maven编译hudi修改pom文件修改源码兼容hadoop3解决spark模块依赖的问题hudi编译命令Hudi简介 Hudi概述 Apache Hudi (Hadoop Upserts Delete and Incremental) 是下一代流数据湖平台。Apache Hudi 将核心仓库和…

并发编程中用到的几种常见锁

没有加锁而造成的数据竞争 任务&#xff1a;使用10个线程&#xff0c;同时对一个count加100000&#xff1b;最后我们期望的结果是100000&#xff1b; 实验代码&#xff1a; #include <stdio.h> #include <pthread.h> #include <unistd.h> #include <ti…

git项目 拉项目 提交 上传 保姆级教程

git 项目拉取提交 下载git https://git-scm.com/ 拉取代码 打开需要存代码的位置 右键 git bash打开git客户端 输入命令 git clone [复制的地址]上传代码 修改代码 方法一&#xff1a;命令行 打开对应的文件夹&#xff0c;右键打开git bash 拉取最新代码&#xff08;选…

React学习07-React扩展知识

setState setState更新状态的2种写法: setState(stateChange, [callback])------对象式的setState stateChange为状态改变对象(该对象可以体现出状态的更改)callback是可选的回调函数, 它在状态更新完毕、界面也更新后(render调用后)才被调用 setState(updater, [callback])-…

代码随想录算法训练营第九天(字符串)| 28. 实现 strStr(),459.重复的子字符串

代码随想录算法训练营第九天&#xff08;字符串&#xff09;| 28. 实现 strStr()&#xff0c;459.重复的子字符串 28. 实现 strStr() 因为KMP算法很难&#xff0c;大家别奢求 一次就把kmp全理解了&#xff0c;大家刚学KMP一定会有各种各样的疑问&#xff0c;先留着&#xff0…

Qt编写雷达模拟仿真工具2-自定义QGraphicsItem按钮

一、前言 雷达模拟仿真工具&#xff0c;整体结构采用的QGraphicsView框架&#xff0c;场景需要设计一个可点击的自定义按钮出来&#xff0c;在QGraphicsView中一切基础元素点都是基于QGraphicsItem&#xff0c;在QGraphicsItem类中我们可以看到它不继承自QObject&#xff0c;那…

25万美金奖励章鱼加速器2022冬季获胜团队!

全长 1427 字&#xff0c;预计阅读 6 分钟 作者&#xff1a;MiX 2022年12月21日&#xff0c;章鱼加速器2022年冬季 Web3 创业营圆满落幕&#xff0c;61个入营项目中有5个脱颖而出&#xff0c;获得「章鱼未来之星」称号&#xff0c;排名不分先后&#xff0c;他们分别是&#xf…

第五章. 可视化数据分析图表—Seaborn图表(折线图,直方图,条形图,散点图)

第五章. 可视化数据分析图 5.7 Seaborn图表 Seaborn是一个基于Matplotlib的高级可视化效果库&#xff0c;偏向于统计图表&#xff0c;主要针对的是数据挖掘和机器学习中的变量特征选取&#xff0c;相比Matplotlib&#xff0c;他的语法相对简单&#xff0c;但是具有一定的局限性…

Windows下安装oracle19c

oracle 19c 不支持 Windows 7 和 Windows 8 以及 Windows Server 2008&#xff08;GetOverlappedResultEx function 函数不支持 win7&#xff09;&#xff0c;支持 Windows 8.1 以上及 Windows Server 2012 1.下载地址&#xff1a; https://www.oracle.com/database/technolo…

Gnoppix Linux 22.12 发布

导读基于 Kali Linux 的 Linux 滚动发行版 Gnoppix 22.12 带来了 GNOME 43、Linux 内核 6.0 和新的升级。作为传统的现场 CD 发行版 Knoppix 项目的继承者&#xff0c;Gnoppix Linux 是专门为渗透测试和反向工程而设计的。它为网页应用安全和数字权利保护进行了优化。除了对安全…

DNA甲基化重编程为红梨中光诱导的花青素生物合成提供了见解

期刊&#xff1a;Plant Science 影响因子&#xff1a;5.363 发表时间&#xff1a;2022 样本类型&#xff1a;果皮 客户单位&#xff1a;南京农业大学 凌恩生物客户南京农业大学吴俊团队发表在《Plant Science》上的文章“DNA methylatio…

【Ctfer训练计划】——(七)

作者名&#xff1a;Demo不是emo 主页面链接&#xff1a;主页传送门 创作初心&#xff1a;舞台再大&#xff0c;你不上台&#xff0c;永远是观众&#xff0c;没人会关心你努不努力&#xff0c;摔的痛不痛&#xff0c;他们只会看你最后站在什么位置&#xff0c;然后羡慕或鄙夷座…

【数据结构】LinkedList与链表

作者&#xff1a;✿✿ xxxflower. ✿✿ 博客主页&#xff1a;xxxflower的博客 专栏&#xff1a;【数据结构】篇 语录&#xff1a;⭐每一个不曾起舞的日子&#xff0c;都是对生命的辜负。⭐ 文章目录✿1.ArrayList的缺陷✿2.链表2.1链表的概念及结构2.2链表的模拟实现MySingleLi…

aws eks 集群container runtime升级容器管理工具的切换

参考资料 https://cloud-atlas.readthedocs.io/zh_CN/latest/kubernetes/debug/crictl.htmlhttps://zhuanlan.zhihu.com/p/562014518 container runtime Low-Level和High-Level容器运行时。runc、lxc、lmctfy、Docker&#xff08;容器&#xff09;、rkt、cri-o。每一个都是为…

springboot simple (12) springboot RabbitMQ

这里首先简单的介绍了RabbitMQ &#xff0c;然后实现了springboot集成RabbitMQ &#xff0c;包含两个工程&#xff1a; 1 Producer 生产者 2 Consumer 消费者 1 RabbitMQ 简介 AMQP &#xff1a;Advanced Message Queue&#xff0c;高级消息队列协议。 RabbitMQ 是一个由 Erl…

智能大屏兴起,酷开科技赋能营销战略!

随着科技的发展&#xff0c;智能大屏的功能与技术都在日新月异的快速更迭&#xff0c;年轻消费群体也对大尺寸智能大屏表现出了特别的偏爱&#xff0c;以前说到看视频、网上购物、阅读书籍时&#xff0c;人们第一时间就会想到手机&#xff0c;但随着智能大屏的出现&#xff0c;…

商用设计素材库,设计师必备。

免费、商用设计素材网站。 1、菜鸟图库 https://www.sucai999.com/?vNTYxMjky站内平面海报、UI设计、电商淘宝、免抠、高清图片、样机模板等素材非常齐全。还有在线抠图、CDR版本转换功能&#xff0c;能有效的为设计师节省找素材时间&#xff0c;提高工作效率。网站素材都能免…

LeetCode链表经典题目(二)

1. LeetCode203. 移除链表元素 2. LeetCode707.设计链表 3. LeetCode206. 反转链表 4. LeetCode24. 两两交换链表中的节点​ 5. LeetCode19. 删除链表的倒数第 N 个结点 6. LeetCode面试题 02.07. 链表相交 7. LeetCode142. 环形链表 II​ 1. LeetCode203. 移除链表元素 …