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

变量 varablie 声明- Rust 变量 let mut 声明与 C/C++ 变量声明对比分析

一、变量声明设计&#xff1a;let 与 mut 的哲学解析 Rust 采用 let 声明变量并通过 mut 显式标记可变性&#xff0c;这种设计体现了语言的核心哲学。以下是深度解析&#xff1a; 1.1 设计理念剖析 安全优先原则&#xff1a;默认不可变强制开发者明确声明意图 let x 5; …...

【JavaEE】-- HTTP

1. HTTP是什么&#xff1f; HTTP&#xff08;全称为"超文本传输协议"&#xff09;是一种应用非常广泛的应用层协议&#xff0c;HTTP是基于TCP协议的一种应用层协议。 应用层协议&#xff1a;是计算机网络协议栈中最高层的协议&#xff0c;它定义了运行在不同主机上…...

通过Wrangler CLI在worker中创建数据库和表

官方使用文档&#xff1a;Getting started Cloudflare D1 docs 创建数据库 在命令行中执行完成之后&#xff0c;会在本地和远程创建数据库&#xff1a; npx wranglerlatest d1 create prod-d1-tutorial 在cf中就可以看到数据库&#xff1a; 现在&#xff0c;您的Cloudfla…...

C++ 基础特性深度解析

目录 引言 一、命名空间&#xff08;namespace&#xff09; C 中的命名空间​ 与 C 语言的对比​ 二、缺省参数​ C 中的缺省参数​ 与 C 语言的对比​ 三、引用&#xff08;reference&#xff09;​ C 中的引用​ 与 C 语言的对比​ 四、inline&#xff08;内联函数…...

AI书签管理工具开发全记录(十九):嵌入资源处理

1.前言 &#x1f4dd; 在上一篇文章中&#xff0c;我们完成了书签的导入导出功能。本篇文章我们研究如何处理嵌入资源&#xff0c;方便后续将资源打包到一个可执行文件中。 2.embed介绍 &#x1f3af; Go 1.16 引入了革命性的 embed 包&#xff0c;彻底改变了静态资源管理的…...

【7色560页】职场可视化逻辑图高级数据分析PPT模版

7种色调职场工作汇报PPT&#xff0c;橙蓝、黑红、红蓝、蓝橙灰、浅蓝、浅绿、深蓝七种色调模版 【7色560页】职场可视化逻辑图高级数据分析PPT模版&#xff1a;职场可视化逻辑图分析PPT模版https://pan.quark.cn/s/78aeabbd92d1...

C#中的CLR属性、依赖属性与附加属性

CLR属性的主要特征 封装性&#xff1a; 隐藏字段的实现细节 提供对字段的受控访问 访问控制&#xff1a; 可单独设置get/set访问器的可见性 可创建只读或只写属性 计算属性&#xff1a; 可以在getter中执行计算逻辑 不需要直接对应一个字段 验证逻辑&#xff1a; 可以…...

MFC 抛体运动模拟:常见问题解决与界面美化

在 MFC 中开发抛体运动模拟程序时,我们常遇到 轨迹残留、无效刷新、视觉单调、物理逻辑瑕疵 等问题。本文将针对这些痛点,详细解析原因并提供解决方案,同时兼顾界面美化,让模拟效果更专业、更高效。 问题一:历史轨迹与小球残影残留 现象 小球运动后,历史位置的 “残影”…...

Python Einops库:深度学习中的张量操作革命

Einops&#xff08;爱因斯坦操作库&#xff09;就像给张量操作戴上了一副"语义眼镜"——让你用人类能理解的方式告诉计算机如何操作多维数组。这个基于爱因斯坦求和约定的库&#xff0c;用类似自然语言的表达式替代了晦涩的API调用&#xff0c;彻底改变了深度学习工程…...

省略号和可变参数模板

本文主要介绍如何展开可变参数的参数包 1.C语言的va_list展开可变参数 #include <iostream> #include <cstdarg>void printNumbers(int count, ...) {// 声明va_list类型的变量va_list args;// 使用va_start将可变参数写入变量argsva_start(args, count);for (in…...