当前位置: 首页 > 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;后端接口也没有问题 但后端收…...

Prompt Tuning、P-Tuning、Prefix Tuning的区别

一、Prompt Tuning、P-Tuning、Prefix Tuning的区别 1. Prompt Tuning(提示调优) 核心思想:固定预训练模型参数,仅学习额外的连续提示向量(通常是嵌入层的一部分)。实现方式:在输入文本前添加可训练的连续向量(软提示),模型只更新这些提示参数。优势:参数量少(仅提…...

linux arm系统烧录

1、打开瑞芯微程序 2、按住linux arm 的 recover按键 插入电源 3、当瑞芯微检测到有设备 4、松开recover按键 5、选择升级固件 6、点击固件选择本地刷机的linux arm 镜像 7、点击升级 &#xff08;忘了有没有这步了 估计有&#xff09; 刷机程序 和 镜像 就不提供了。要刷的时…...

linux 下常用变更-8

1、删除普通用户 查询用户初始UID和GIDls -l /home/ ###家目录中查看UID cat /etc/group ###此文件查看GID删除用户1.编辑文件 /etc/passwd 找到对应的行&#xff0c;YW343:x:0:0::/home/YW343:/bin/bash 2.将标红的位置修改为用户对应初始UID和GID&#xff1a; YW3…...

解决本地部署 SmolVLM2 大语言模型运行 flash-attn 报错

出现的问题 安装 flash-attn 会一直卡在 build 那一步或者运行报错 解决办法 是因为你安装的 flash-attn 版本没有对应上&#xff0c;所以报错&#xff0c;到 https://github.com/Dao-AILab/flash-attention/releases 下载对应版本&#xff0c;cu、torch、cp 的版本一定要对…...

rnn判断string中第一次出现a的下标

# coding:utf8 import torch import torch.nn as nn import numpy as np import random import json""" 基于pytorch的网络编写 实现一个RNN网络完成多分类任务 判断字符 a 第一次出现在字符串中的位置 """class TorchModel(nn.Module):def __in…...

AI+无人机如何守护濒危物种?YOLOv8实现95%精准识别

【导读】 野生动物监测在理解和保护生态系统中发挥着至关重要的作用。然而&#xff0c;传统的野生动物观察方法往往耗时耗力、成本高昂且范围有限。无人机的出现为野生动物监测提供了有前景的替代方案&#xff0c;能够实现大范围覆盖并远程采集数据。尽管具备这些优势&#xf…...

PostgreSQL——环境搭建

一、Linux # 安装 PostgreSQL 15 仓库 sudo dnf install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-$(rpm -E %{rhel})-x86_64/pgdg-redhat-repo-latest.noarch.rpm# 安装之前先确认是否已经存在PostgreSQL rpm -qa | grep postgres# 如果存在&#xff0…...

前端高频面试题2:浏览器/计算机网络

本专栏相关链接 前端高频面试题1&#xff1a;HTML/CSS 前端高频面试题2&#xff1a;浏览器/计算机网络 前端高频面试题3&#xff1a;JavaScript 1.什么是强缓存、协商缓存&#xff1f; 强缓存&#xff1a; 当浏览器请求资源时&#xff0c;首先检查本地缓存是否命中。如果命…...

联邦学习带宽资源分配

带宽资源分配是指在网络中如何合理分配有限的带宽资源&#xff0c;以满足各个通信任务和用户的需求&#xff0c;尤其是在多用户共享带宽的情况下&#xff0c;如何确保各个设备或用户的通信需求得到高效且公平的满足。带宽是网络中的一个重要资源&#xff0c;通常指的是单位时间…...

软件工程教学评价

王海林老师您好。 您的《软件工程》课程成功地将宏观的理论与具体的实践相结合。上半学期的理论教学中&#xff0c;您通过丰富的实例&#xff0c;将“高内聚低耦合”、SOLID原则等抽象概念解释得十分透彻&#xff0c;让这些理论不再是停留在纸面的名词&#xff0c;而是可以指导…...