当前位置: 首页 > news >正文

手写webpack核心原理,支持typescript的编译和循环依赖问题的解决

 

主要知识点

  1. babel读取代码的import语句
  2. 算法:bfs遍历依赖图
  3. 为浏览器定义一个require函数的polyfill
  4. 算法:用记忆化搜索解决require函数的循环依赖问题

Quick Start

GitHub:https://github.com/Hans774882968/mini-webpack

npm install
npm run bundle
修改index.html依赖的js文件路径(bundle_ts.js),复制到dist文件夹,然后点击打开index.html。

依赖

npm i @babel/parser
npm i @babel/traverse
npm i -D @types/babel__traverse
npm i @babel/core @babel/preset-env
npm i -D @babel/preset-typescript
npm i -D @types/babel__core
npm i -D eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser
npm i typescript@4.7.4

注意点:

  1. 配置eslint后记得重启一下vscode,IDE提示才会生效。
  2. 我们的命令在2022-08-27下载了@babel/core7.18.13,对应的ts版本要指定为typescript@4.7.4,否则运行代码会报错。

引言

主要是借鉴参考链接1来实现一个mini-webpack,但在功能上有所超越:

  1. 根据入口文件的拓展名,决定用ts或js来编译。
  2. 借鉴参考链接3,用“记忆化搜索”解决循环依赖问题。

最大的缺憾是不清楚ts-loader怎么实现,因此这里编译ts的做法是直接判定入口文件的扩展名为.ts,然后用babel实现。

因为参考链接1写得很清晰了,本文仅定位为一个额外补充,不会写得很详细。

初始化项目

npm init
tsc --init

tsconfig.json主要需要设置:

"compilerOptions": {"module": "commonjs",
},
"include": ["bundle.ts"
]

样就能用tsc命令编译入口文件了。

接下来给package.json加一个命令:"bundle": "tsc && node bundle.js",以后可以直接npm run bundle模拟打包命令了。

目前除了在nodejs代码里用'@babel/preset-typescript'插件以外,不知道怎么快速方便地编译src文件夹下的ts,只好:先手工修改tsconfig.jsonincludecompilerOptions.module,接着tsc编译,最后还原tsconfig.json

读取单个文件

getModuleInfo函数主要分析文件的依赖和完成代码转换。

  1. 我们需要分析文件的import语句,把依赖的文件(相对路径)转换为相对于项目根目录的路径(下称“绝对路径”)。使用babel相关的库@babel/parser@babel/traverse@babel/core完成。
  2. 代码转换用@babel/coretransformFromAst方法完成。
  3. 我们需要保证生成的js的模块规范是commonjs。对于编译js的情况不需要特别指明,而编译ts的情况需要指明插件:plugins: ['@babel/plugin-transform-modules-commonjs'](参考链接5)。
如何支持typescript的编译

只需要修改@babel/parserparse方法和@babel/coretransformFromAst方法的调用方式。

需要用到@babel/preset-typescript这个插件。

@babel/preset-typescript没有@babel/preset-env方便,

需要指明filename属性和@babel/plugin-transform-modules-commonjs插件。

相关语句:

const ast = parser.parse(body, {sourceType: 'module',plugins: getType() === 'ts' ? ['decorators-legacy', 'typescript'] : []
});babel.transformFromAst(ast, undefined,getType() === 'ts' ?{presets: ['@babel/preset-typescript'],filename: file,plugins: ['@babel/plugin-transform-modules-commonjs']} :{ presets: ['@babel/preset-env'] },(err, result) => {if (err) reject(err);resolve(result as babel.BabelFileResult);}
);

遍历依赖图

parseModule函数。因为循环依赖也只不过是形成递归,所以依赖图不局限于DAG,可以是任意有向图。所以只需要用bfs遍历一下(这里更正参考链接1的一处小错误,遍历算法不是递归而是bfs)。

  1. parseModule函数中的for循环for (const { deps } of a)用到了它会继续遍历新加入的元素的特性,不能替换为forEach,是js实现bfs的最简方案。
  2. parseModule函数中的await Promise.all是循环中使用async/await的解决方案(参考链接4)。

parseModule函数的输出为depGraph哈希表,其一个对象的deps属性应该设计为一个哈希表,而非直接设计为数组,下文会解释原因。

生成单个文件

getBundle函数把上面生成的depGraph哈希表扔进代码模板里,这就是打包结果。

为了在浏览器环境给出一个合法的commonjs的polyfill(这里只需要给出requireexports对象),我们在代码模板中定义了自己的require函数。对于一个代码文件来说,其返回值为这个文件的exports对象,其副作用为把整个文件的代码执行了一遍。

`;(function (graph) {var exportsInfo = {};function require (file) {if (exportsInfo[file]) return exportsInfo[file];function absRequire (relPath) {return require(graph[file].deps[relPath]);}var exports = {};exportsInfo[file] = exports;(function (require, exports, code) {eval(code);})(absRequire, exports, graph[file].code);return exports;}require('${entryPath}');
})(${depGraph});`

值得注意的是,这个require函数实际上是一个递归函数。在eval(code)时可能产生递归。

depGraph哈希表的一个对象的deps属性为什么设计为一个哈希表,而非直接设计为数组?因为待执行的代码中所有的路径都是相对路径,我们需要用graph[file].deps[relPath]这样的方式把它转换为绝对路径。为了完成这个转换,我们还需要设计absRequire函数,它只不过起到一个拦截器的作用。

如何解决循环依赖

此时我们如果打包一个含有循环依赖的入口文件,运行时会栈溢出。以最简单的情况为例:a模块的a函数引用b模块的b函数,b模块的b函数引用a模块的a函数。

怎么解决呢?根据参考链接3,我们可以用“记忆化搜索”的思路,开一个全局变量var exportsInfo = {};。并在exports对象生成以后,立即exportsInfo[file] = exports;。上文案例中,b模块获得的a模块的exports对象的值是空的,但因为对象的浅拷贝特性,对象地址是正确的,在require函数解析a模块完毕后,b模块也就能获得a模块的exports对象的正确值了。

相信对acmer来说这个算法很经典,没有相关背景的话可以尝试在浏览器打断点帮助理解。

入口

main函数在最开始读取配置(模仿webpack.config.js),在最后把getBundle生成的单个文件写入文件系统。

参考链接

  1. 手写webpack核心原理,再也不怕面试官问我webpack原理:手写webpack核心原理,再也不怕面试官问我webpack原理 - 掘金
  2. @babel/core官方文档:@babel/core
  3. webpack如何解决循环依赖:webpack如何解决循环依赖 - 知乎
  4. 循环中使用async/await的解决方案:五种在循环中使用 async/await 的方法 - 知乎
  5. @babel/plugin-transform-modules-commonjs · Babel

相关文章:

手写webpack核心原理,支持typescript的编译和循环依赖问题的解决

主要知识点 babel读取代码的import语句算法:bfs遍历依赖图为浏览器定义一个require函数的polyfill算法:用记忆化搜索解决require函数的循环依赖问题 Quick Start GitHub:https://github.com/Hans774882968/mini-webpack npm install npm…...

开箱即用之MyBatisPlus XML 自定义分页

调用方法 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;public Page<User> queryListByPage(User user) { Page<User> page new Page<>(1, 12); return userMapper.queryListByPage(page, user); } mapper接口 import co…...

GPT应用开发:运行你的第一个聊天程序

本系列文章介绍基于OpenAI GPT API开发应用的方法&#xff0c;适合从零开始&#xff0c;也适合查缺补漏。 本文首先介绍基于聊天API编程的方法。 环境搭建 很多机器学习框架和类库都是使用Python编写的&#xff0c;OpenAI提供的很多例子也是Python编写的&#xff0c;所以为了…...

力扣刷MySQL-第一弹(详细解析)

&#x1f389;欢迎您来到我的MySQL基础复习专栏 ☆* o(≧▽≦)o *☆哈喽~我是小小恶斯法克&#x1f379; ✨博客主页&#xff1a;小小恶斯法克的博客 &#x1f388;该系列文章专栏&#xff1a;力扣刷题讲解-MySQL &#x1f379;文章作者技术和水平很有限&#xff0c;如果文中出…...

Xcode 15 for Mac:超越开发的全新起点

作为一名开发人员&#xff0c;你是否正在寻找一款强大而高效的开发工具&#xff0c;来帮助你在Mac上构建出卓越的应用程序&#xff1f;那么&#xff0c;Xcode 15就是你一直在寻找的答案。 Xcode 15是苹果公司最新推出的一款集成开发环境&#xff08;IDE&#xff09;&#xff0…...

2021腾讯、华为前端面试题集(基础篇)

Vue 面试题 生命周期函数面试题 1.什么是 vue 生命周期2.vue 生命周期的作用是什么 3.第一次页面加载会触发哪几个钩子 4.简述每个周期具体适合哪些场景 5.created 和 mounted 的区别 6.vue 获取数据在哪个周期函数 7.请详细说下你对 vue 生命周期的理解&#xff1f; **vue 路由…...

怎么修改或移除WordPress后台仪表盘概览底部的版权信息和主题信息?

前面跟大家分享『WordPress怎么把后台左上角的logo和评论图标移除&#xff1f;』和『WordPress后台底部版权信息“感谢使用 WordPress 进行创作”和版本号怎么修改或删除&#xff1f;』&#xff0c;其实在WordPress后台仪表盘的“概览”底部还有一个WordPress版权信息和所使用的…...

计算机三级(网络技术)——应用题

第一题 61.输出端口S0 &#xff08;直接连接&#xff09; RG的输出端口S0与RE的S1接口直接相连构成一个互联网段 对172.0.147.194和172.0.147.193 进行聚合 前三段相同&#xff0c;将第四段分别转换成二进制 11000001 11000010 前6位相同&#xff0c;加上前面三段 共30…...

Node.js基础知识点(四)

本节介绍一下最简单的http服务 一.http 可以使用Node 非常轻松的构建一个web服务器&#xff0c;在 Node 中专门提供了一个核心模块&#xff1a;http http 这个模块的就可以帮你创建编写服务器。 1. 加载 http 核心模块 var http require(http) 2. 使用 http.createServe…...

持久双向通信网络协议-WebSocket-入门案例实现demo

1 介绍 WebSocket 是基于 TCP 的一种新的网络协议。它实现了浏览器与服务器全双工通信——浏览器和服务器只需要完成一次握手&#xff0c;两者之间就可以创建持久性的连接&#xff0c; 并进行双向数据传输。 HTTP协议和WebSocket协议对比&#xff1a; HTTP是短连接&#xff0…...

LV.13 D10 Linux内核移植 学习笔记

具体实验步骤在lv13day10 实验十 一、Linux内核概述 1.1 内核与操作系统 内核 内核是一个操作系统的核心&#xff0c;提供了操作系统最基本的功能&#xff0c;是操作系统工作的基础&#xff0c;决定着整个系统的性能和稳定性 操作系统 操作系统是在内核的基础上添…...

STM32面试体验和题目

目录 一、说一下你之前的工作主要干了什么&#xff1f; 二、stm32有关的知识点 1.stm32的外设有哪一些 2.你的毕业论文的项目里面是怎么设计的 三&#xff0c;C语言的考察 1.写一个结构体&#xff08;结构体的内容自由发挥&#xff09; 2.写一个指针型的变量 3.结构体是…...

微软.NET、.NET Framework和.NET Core联系和区别

我是荔园微风&#xff0c;作为一名在IT界整整25年的老兵&#xff0c;看到不少初学者在学习编程语言的过程中如此的痛苦&#xff0c;我决定做点什么&#xff0c;我小时候喜欢看小人书&#xff08;连环画&#xff09;&#xff0c;在那个没有电视、没有手机的年代&#xff0c;这是…...

Shell脚本同时调用#!/bin/bash和#!/usr/bin/expect

如果你想在一个脚本中同时使用bash和expect&#xff0c;你可以将expect部分嵌入到bash脚本中。以下是一个示例&#xff1a; #!/bin/bash# 设置MySQL服务器地址、端口、用户名和密码 MYSQL_HOST"localhost" MYSQL_PORT"3306" MYSQL_USER"your_usernam…...

C++ Webserver从零开始:基础知识(一)——Linux网络编程基础API

目录 前言 一.socket地址API 1.主机字节序和网络字节序 2.通用socket地址 3.专用socket地址 二.创建socket 三.绑定socket&#xff08;命名socket) 四.监听socket 五.接受连接&#xff08;服务端&#xff09; 六.发起连接&#xff08;客户端&#xff09; 七.关闭连接…...

cookie和session的工作过程和作用:弥补http无状态的不足

cookie是客户端浏览器保存服务端数据的一种机制。当通过浏览器去访问服务端时&#xff0c;服务端可以把状态数据以key-value的形式写入到cookie中&#xff0c;存储到浏览器。浏览器下次去服务服务端时&#xff0c;就可以把这些状态数据携带给服务器端&#xff0c;服务器端可以根…...

【蓝桥杯选拔赛真题30】C++字母转换 第十三届蓝桥杯青少年创意编程大赛C++编程选拔赛真题解析

目录 C/C++字母转换 一、题目要求 1、编程实现 2、输入输出...

资产负债表#通俗易懂

资产负债表&#xff08;the Balance Sheet&#xff09;亦称财务状况表&#xff0c;表示企业在一定日期&#xff08;通常为各会计期末&#xff09;的财务状况&#xff08;即资产、负债和业主权益的状况&#xff09;的主要会计报表。 (99 封私信 / 11 条消息) 能通俗易懂的给小白…...

PCF8563转STM32 RTC避坑指南

问题一&#xff0c;时间读取错误 原因&#xff0c;读写时间必须Time在前&#xff0c;Date在后 HAL_RTC_GetTime(&hrtc, &time, RTC_FORMAT_BCD); HAL_RTC_GetDate(&hrtc, &date, RTC_FORMAT_BCD); HAL_RTC_SetTime(&hrtc, &time, RTC_FORMAT_BCD); …...

前端重置密码报错记录

昨天晚上&#xff0c;我写了重置密码的前端&#xff0c;测试的时候报错 今天上午&#xff0c;我继续试图解决这个问题&#xff0c;我仔细检查了一遍&#xff0c;前端没有问题 可以正常接收输入的数据并且提交 但是后端接收到的数据为空&#xff0c;后端接口也没有问题 但后端收…...

【网络】每天掌握一个Linux命令 - iftop

在Linux系统中&#xff0c;iftop是网络管理的得力助手&#xff0c;能实时监控网络流量、连接情况等&#xff0c;帮助排查网络异常。接下来从多方面详细介绍它。 目录 【网络】每天掌握一个Linux命令 - iftop工具概述安装方式核心功能基础用法进阶操作实战案例面试题场景生产场景…...

RocketMQ延迟消息机制

两种延迟消息 RocketMQ中提供了两种延迟消息机制 指定固定的延迟级别 通过在Message中设定一个MessageDelayLevel参数&#xff0c;对应18个预设的延迟级别指定时间点的延迟级别 通过在Message中设定一个DeliverTimeMS指定一个Long类型表示的具体时间点。到了时间点后&#xf…...

对WWDC 2025 Keynote 内容的预测

借助我们以往对苹果公司发展路径的深入研究经验&#xff0c;以及大语言模型的分析能力&#xff0c;我们系统梳理了多年来苹果 WWDC 主题演讲的规律。在 WWDC 2025 即将揭幕之际&#xff0c;我们让 ChatGPT 对今年的 Keynote 内容进行了一个初步预测&#xff0c;聊作存档。等到明…...

OkHttp 中实现断点续传 demo

在 OkHttp 中实现断点续传主要通过以下步骤完成&#xff0c;核心是利用 HTTP 协议的 Range 请求头指定下载范围&#xff1a; 实现原理 Range 请求头&#xff1a;向服务器请求文件的特定字节范围&#xff08;如 Range: bytes1024-&#xff09; 本地文件记录&#xff1a;保存已…...

【单片机期末】单片机系统设计

主要内容&#xff1a;系统状态机&#xff0c;系统时基&#xff0c;系统需求分析&#xff0c;系统构建&#xff0c;系统状态流图 一、题目要求 二、绘制系统状态流图 题目&#xff1a;根据上述描述绘制系统状态流图&#xff0c;注明状态转移条件及方向。 三、利用定时器产生时…...

C++中string流知识详解和示例

一、概览与类体系 C 提供三种基于内存字符串的流&#xff0c;定义在 <sstream> 中&#xff1a; std::istringstream&#xff1a;输入流&#xff0c;从已有字符串中读取并解析。std::ostringstream&#xff1a;输出流&#xff0c;向内部缓冲区写入内容&#xff0c;最终取…...

BCS 2025|百度副总裁陈洋:智能体在安全领域的应用实践

6月5日&#xff0c;2025全球数字经济大会数字安全主论坛暨北京网络安全大会在国家会议中心隆重开幕。百度副总裁陈洋受邀出席&#xff0c;并作《智能体在安全领域的应用实践》主题演讲&#xff0c;分享了在智能体在安全领域的突破性实践。他指出&#xff0c;百度通过将安全能力…...

Spring AI与Spring Modulith核心技术解析

Spring AI核心架构解析 Spring AI&#xff08;https://spring.io/projects/spring-ai&#xff09;作为Spring生态中的AI集成框架&#xff0c;其核心设计理念是通过模块化架构降低AI应用的开发复杂度。与Python生态中的LangChain/LlamaIndex等工具类似&#xff0c;但特别为多语…...

C++.OpenGL (14/64)多光源(Multiple Lights)

多光源(Multiple Lights) 多光源渲染技术概览 #mermaid-svg-3L5e5gGn76TNh7Lq {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-3L5e5gGn76TNh7Lq .error-icon{fill:#552222;}#mermaid-svg-3L5e5gGn76TNh7Lq .erro…...

基于TurtleBot3在Gazebo地图实现机器人远程控制

1. TurtleBot3环境配置 # 下载TurtleBot3核心包 mkdir -p ~/catkin_ws/src cd ~/catkin_ws/src git clone -b noetic-devel https://github.com/ROBOTIS-GIT/turtlebot3.git git clone -b noetic https://github.com/ROBOTIS-GIT/turtlebot3_msgs.git git clone -b noetic-dev…...