前端模块化-手写mini-vite
前言
本文总结了一些关于 Vite 的工作原理,以及一些实现细节。
本节对应的 demo 可以在这里找到。
什么是 Vite
Vite 是一个基于浏览器原生 ES imports 的开发服务器。利用浏览器去解析 imports,在服务器端按需编译返回,完全跳过了打包这个概念,服务器随起随用。
实现步骤
- 项目搭建
- 实现 cli
- 起静态服务器, nodemon 监听文件修改,执行 vite 命令
- 处理 index.html
- 处理 js,处理 node_modules 的引入
- 中间件拆分
- 处理 react 文件
项目结构
├── _example
├── cli
│ └── index.js
├── src
│ └── index.js
_example 通过 npx create-vite-app 创建的 vite 项目, 用于和 mini-vite 对比
npx create-vite _example --template react
实现 cli
新建 cli/index.js
#! /usr/bin/env node
console.log("mini-vite!");
mini-vite/package.json
{"bin": "cli/index.js"
}
通过 yarn link 将 cli 链接到全局
# _demo/mini-vite目录
yarn link
在 _example 中 link
# _demo/mini-vite/_example目录
yarn link mini-vite
在 package.json 中添加命令
{"scripts": {"dev:mini-vite": "mini-vite"}
}
跑下 dev:mini-vite 命令,可以看到控制台已经打印出 mini-vite!
起静态服务器
依赖安装
yarn add koa koa-static
在 src 目录下新建 index.js
// src/index.js
const Koa = require("koa");
const KoaStatic = require("koa-static");const app = new Koa();// 执行命令时的路径
const rootPath = process.cwd();
app.use(KoaStatic(rootPath));app.listen(8000, () => {console.log("mini-vite server启动成功!");
});
同时,在_example 中的 package.json 中添加命令
{"scripts": {"dev:mini-vite": "nodemon -w ../ --exec mini-vite","mini-vite": "mini-vite"}
}
并安装 nodemon
# mini-vite/_example
yarn add nodemon -D
执行,可以看到控制台打印出 mini-vite server 启动成功!同时在浏览器中打开 http://localhost:8000/ 可以看到项目已经跑起来了。(这里的端口号是 8000,是因为 create-vite-app 默认的端口号是 3000,所以这里我们用 8000)
同时修改 index.js 也可以看到 terminal 中打印出修改成功。
处理 jsx
现在我们已经可以返回静态文件了,但是在返回 index.html 中后,浏览器随即发起了 src/main.jsx 的请求
<script type="module" src="/src/main.jsx"></script>
然后就报错了,因为浏览器无法解析 jsx 文件,所以我们需要对.jsx 进行处理,将 src/main.jsx 改为 src/main.js
main.jsx:1 Failed to load module script: Expected a JavaScript module script but the server responded with a MIME type of “text/jsx”. Strict MIME type checking is enforced for module scripts per HTML spec.
首先是 jsx 的转换
在 mini-vite 目录安装依赖:
# mini-vite
yarn add @babel/core @babel/plugin-transform-react-jsx
添加 transformJsx 函数
function transformJsx(jsxCode) {const babel = require("@babel/core");const options = {// presets: ['@babel/preset-env'], // 注意这里不要使用 @babel/preset-env,因为它会将所有的代码都转换成 ES5,包括importplugins: [["@babel/plugin-transform-react-jsx",{pragma: "React.createElement",pragmaFrag: "React.Fragment",},],],};const { code } = babel.transform(jsxCode, options);return code;
}
修改 src/index.js 的代码, 进行了一点重构, 添加中间件的机制
// index.js
const Koa = require("koa");
const KoaStatic = require("koa-static");function createServer() {const app = new Koa();const context = {app,rootPath: process.cwd(),};const resolvePlugins = [moduleRewirePlugin, serverStaticPlugin];resolvePlugins.forEach((plugin) => plugin(context));
}createServer();function serverStaticPlugin({ app, rootPath }) {app.use(KoaStatic(rootPath));app.use(KoaStatic(rootPath, "/public"));app.listen(8000, () => {console.log("mini-vite server启动成功!");});
}function moduleRewirePlugin({ app, context }) {app.use(async (ctx, next) => {await next();if (ctx.body && ctx.response.is("jsx")) {// 初始的 ctx.body 是一个 Readable 流,需要转换成字符串const jsxCode = await readBody(ctx.body);// 通过babel转换jsx代码const transformedCode = transformJsx(jsxCode);ctx.type = "application/javascript";ctx.body = transformedCode;}});
}function transformJsx(jsxCode) {const babel = require("@babel/core");const options = {// presets: ['@babel/preset-env'], // 注意这里不要使用 @babel/preset-env,因为它会将所有的代码都转换成 ES5,包括importplugins: [["@babel/plugin-transform-react-jsx",{pragma: "React.createElement",pragmaFrag: "React.Fragment",},],],};const { code } = babel.transform(jsxCode, options);return code;
}function readBody(stream) {return new Promise((resolve, reject) => {if (!stream.readable) {resolve(stream);} else {let res = "";stream.on("data", (data) => {res += data;});stream.on("end", () => {resolve(res);});stream.on("error", (err) => {reject(err);});}});
}
可以看到此时浏览器已经成功请求到了 main.js, 并且我们的 jsx 语法也被转换成了 React.createElement
但此时浏览器报错了
Uncaught TypeError: Failed to resolve module specifier “react”. Relative references must start with either “/”, “./”, or “…/”.
原因是我们在 main.js 中引入了 react,但是浏览器无法解析 node_modules 中的模块,所以我们需要对 node_modules 中的模块进行处理。
处理 node_modules
添加自定义的 babel 插件
module.exports = function ({ types: t }) {return {visitor: {ImportDeclaration(path, state) {const { node } = path;const id = node.source.value;// 简化场景: 不是以 / . 开头的,都是第三方模块,不考虑alias等其他情况if (/^[^\/\.]/.test(id)) {node.source = t.stringLiteral("/@modules/" + id);}},},};
};
服务端做对应的处理
const customAliasPlugin = require("./babel-plugin-custom-alias");const regex = /^\/@modules\//;
function moduleResolvePlugin({ app, context }) {app.use(async (ctx, next) => {if (!regex.test(ctx.path)) {return next();}const id = ctx.path.replace(regex, "");console.log("id", id);const mapping = {// 从package.json中读取esm读出来的字段,这里只是简化了一下,正常应该从package.json中读取esm导出react: path.resolve(process.cwd(), "node_modules/react/index.js"),"react-dom/client": path.resolve(process.cwd(),"node_modules/react-dom/client.js"),};ctx.type = "application/javascript";const content = fs.readFileSync(mapping[id], "utf-8");ctx.body = content;});
}
上述操作遇到一个问题就是,react 没有提供 esm 的版本!
看了下 React 官方的 package.json 的 export 字段
{"exports": {".": {"react-server": "./react.shared-subset.js","default": "./index.js"},"./package.json": "./package.json","./jsx-runtime": "./jsx-runtime.js","./jsx-dev-runtime": "./jsx-dev-runtime.js"}
}
找到对应的 index.js
"use strict";if (process.env.NODE_ENV === "production") {module.exports = require("./cjs/react.production.min.js");
} else {module.exports = require("./cjs/react.development.js");
}
这里也提到了:https://segmentfault.com/q/1010000043780457
两种方案
-
- 找一个有 esm 的版本,比如 https://github.com/esm-bundle/react
-
- 还是原来的包,但是需要在服务端做一些处理,将 cjs 的包转换成 esm 的包
看看 vite-plugin-react
是如何这个问题的, 还是回到浏览器,查看正常 vite 打包出来的文件
import __vite__cjsImport0_react_jsxDevRuntime from "/node_modules/.vite/deps/react_jsx-dev-runtime.js?v=78b1e259";
const jsxDEV = __vite__cjsImport0_react_jsxDevRuntime["jsxDEV"];
import __vite__cjsImport1_react from "/node_modules/.vite/deps/react.js?v=78b1e259";
const React = __vite__cjsImport1_react.__esModule? __vite__cjsImport1_react.default: __vite__cjsImport1_react;
import __vite__cjsImport2_reactDom_client from "/node_modules/.vite/deps/react-dom_client.js?v=78b1e259";
const ReactDOM = __vite__cjsImport2_reactDom_client.__esModule? __vite__cjsImport2_reactDom_client.default: __vite__cjsImport2_reactDom_client;
import App from "/src/App.jsx";
// ReactDOM.createRoot(document.getElementById("root")).render
看了下 node_modules/.vite/deps/react.js
,确实是把代码 copy 了一份,然后把 cjs 的包转换成了 esm 的包,这个过程在 vite 中称为 optimizeDeps
处理 commonJS
vite 内部用了 esbuild 去处理,这里我们就不用 esbuild 了,直接用 babel 去处理
yarn add @babel/core babel-plugin-transform-commonjs
🚧🚧🚧: 注意,这里有两个包
- @babel/plugin-transform-modules-commonjs: 将 esm 转换成 cjs
- @babel/plugin-transform-commonjs: 将 cjs 转换成 esm
我们在启动服务的时候,添加一个 setupDevDepsAssets 的过程
setupDevDepsAssets(process.cwd());
/*** 依赖预构建,将react, react-dom, scheduler等第三方库转换成ES Module, 写入开发临时文件夹* @param {*} rootPath*/
function setupDevDepsAssets(rootPath) {//查看node_modules/.mini-viteconst tempDevDir = path.resolve(rootPath, "node_modules", ".mini-vite");if (!fs.existsSync(tempDevDir)) {fs.mkdirSync(tempDevDir);}// 将项目中的 react, react-dom, scheduler 等第三方库转换成 ES Module,写入到 node_modules/.mini-vite 目录下// 这里只是简化,实际上要从index.html中开始递归查找依赖,然后再转换const mapping = {react: {sourcePath: path.resolve(rootPath,"node_modules/react/cjs/react.development.js"),targetPath: path.resolve(tempDevDir, "react.js"),},"react-dom/client": {sourcePath: path.resolve(rootPath,"node_modules/react-dom/cjs/react-dom.development.js"),targetPath: path.resolve(tempDevDir, "react-dom.js"),},scheduler: {sourcePath: path.resolve(rootPath,"node_modules/scheduler/cjs/scheduler.development.js"),targetPath: path.resolve(tempDevDir, "scheduler.js"),},};Object.keys(mapping).forEach((key) => {const { sourcePath, targetPath } = mapping[key];transformCjsToEsm(sourcePath, targetPath);});/*** 将 CommonJS 转换成 ES Module,部分三方库没有提供 ES Module 版本,比如React* @param {*} sourcePath* @param {*} targetPath*/function transformCjsToEsm(sourcePath, targetPath) {const content = fs.readFileSync(sourcePath, "utf-8");const babel = require("@babel/core");// 转换CommonJS代码为esmconst transformedCode = babel.transform(content, {plugins: ["transform-commonjs"],}).code;// 路径重写,将 require('react') 转换成 require('/@modules/react')// TODO: 两段代码合并const pathRewritedCode = babel.transform(transformedCode, {plugins: [customAliasPlugin],}).code;fs.writeFileSync(targetPath, pathRewritedCode);}
}
添加之后查看网络请求,可以看到已经成功请求到了 react.js 和 react-dom.js
看到控制台有报错,原因是在 App.js 中没有引入 React
React 自动引入
熟悉 React 的朋友都知道,在 React17 之前, 我们在使用 React 的时候,需要手动引入 React,原因是 JSX 语法会被转换成 React.createElement。
import React from "react";
function App() {return <div>hello world1</div>;
}
但是在 React17 之后,我们不需要手动引入 React 了, 有兴趣可以看看官网介绍, 因为 React 会自动注入到全局中,所以我们需要在 App.js 中添加 React 的引入
安装
yarn add @babel/plugin-transform-react-jsx-development
我们在代码转化中添加自动引入的逻辑
const transformedCode = babel.transform(jsxCode, {plugins: ["@babel/plugin-transform-react-jsx-development", // 引入jsxcustomAliasPlugin,],
}).code;
可以看到代码成功做了转化
接着是 import { jsxDEV as _jsxDEV } from "/@modules/react/jsx-dev-runtime";
的处理
在原来的 mapping 中添加 jsx-dev-runtime 的引入
mapping = {react: {sourcePath: path.resolve(rootPath,"node_modules/react/cjs/react.development.js"),targetPath: path.resolve(tempDevDir, "react.js"),},["react/jsx-dev-runtime"]: {sourcePath: path.resolve(rootPath,"node_modules/react/cjs/react-jsx-dev-runtime.development.js"),targetPath: path.resolve(tempDevDir, "jsx-dev-runtime.js"),},
};
可以看到 hello world1 已经成功渲染到页面上了
参考
- vite-plugin-react
本文首发于个人Github前端开发笔记,由于笔者能力有限,文章难免有疏漏之处,欢迎指正
相关文章:

前端模块化-手写mini-vite
前言 本文总结了一些关于 Vite 的工作原理,以及一些实现细节。 本节对应的 demo 可以在这里找到。 什么是 Vite Vite 是一个基于浏览器原生 ES imports 的开发服务器。利用浏览器去解析 imports,在服务器端按需编译返回,完全跳过了打包这个…...

SpringBoot中fastjson扩展: 自定义序列化和反序列化方法实战
❃博主首页 : 「码到三十五」 ,同名公众号 :「码到三十五」,wx号 : 「liwu0213」 ☠博主专栏 : <mysql高手> <elasticsearch高手> <源码解读> <java核心> <面试攻关> ♝博主的话 :…...

【QT】鼠标按键事件 - QMouseEvent QKeyEvent
qt 事件 事件1. 事件概念2. 事件的处理3. 按键事件(1)单个按键(2)组合按键 4. 鼠标事件(1)鼠标单击事件(2)鼠标释放事件(3)鼠标双击事件(4&#x…...
纯手工在内网部署一个Docker私有仓库
纯手工在内网部署一个Docker私有仓库 下载Docker仓库的镜像上传仓库的镜像导入仓库的镜像启动仓库镜像配置客户端的Docker上传镜像到本地仓库从本地仓库拉取镜像 下载Docker仓库的镜像 这个镜像不太好找,有需要的可以从下面的地址中下载。 通过百度网盘分享的文件…...
农林经济管理学报
《农林经济管理学报》是由江西省教育厅主管、江西农业大学主办、北京大学中国农业政策研究中心和中国人民大学农业与农村发展学院学术支持的农林经管类学术双月刊,以主要刊载农林经济政策与理论,反映农林经济管理前沿动态和研究成果,开展学术…...

【初阶数据结构题目】16.用队列实现栈
用队列实现栈 点击链接答题 思路: 出栈:找不为空的队列,将size-1个数据导入到另一个队列中。 入栈:往不为空队列里面插入数据 取栈顶元素: 例如: 两个队列: Q1:1 2 3Q2:…...
使用 OpenAI Whisper v2 模型进行中英文混合语音识别
https://huggingface.co/openai/whisper-large-v2 使用 OpenAI Whisper 模型进行中英文混合语音识别 在本篇博客中,我们将详细介绍如何使用 OpenAI 的 Whisper 模型进行中英文混合语音识别,并设置 Hugging Face 的缓存路径。 简介 Whisper 是 OpenAI 提供的一个强大的自动…...
代码随想录算法训练营day37|动态规划part05
完全背包问题; 第一题:518. Coin Change II class Solution {public int change(int amount, int[] coins) {//递推表达式int[] dp new int[amount 1];//初始化dp数组,表示金额为0时只有一种情况,也就是什么都不装dp[0] 1;fo…...
Git 如何提交代码
一. 简介 前面几篇文章简单学习了 git常用命令,文章如下: Git使用过程中涉及的几个区域-CSDN博客 Git常用命令的使用-CSDN博客 本文学习一下 如何使用 git命令,将本地代码提交到远程仓库。 二. 使用 git命令将本地代码提交到远程仓库中 …...

SpringBoot-application.properties为对象赋值
简单对象赋值 第一种方式 首先让该Bean交由Spring管理,然后加上ConfigurationProperties(prefix"前缀") <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-configuration-processor</artifactId>&l…...
Head First设计模式学习笔记
Head First设计模式学习笔记 大家好,我是微赚淘客系统3.0的小编,是个冬天不穿秋裤,天冷也要风度的程序猿! 一、策略模式 策略模式定义了算法族,分别封装起来,让它们之间可以互相替换,此模式让…...

240806-RHEL 无法通过 ssh username@ip 远程连接,报错:Connection closed by ip port 22
A. 原因排查 遇到这个错误通常意味着 SSH 服务可能在目标主机上没有正常运行,或有防火墙/网络配置问题。以下是一些排查步骤: 检查 SSH 服务状态: 确认 SSH 服务是否正在目标主机上运行。 sudo systemctl status sshd重启 SSH 服务ÿ…...

C语言:复读机2种写法(输入什么就输出什么)
(1)题目:输入什么内容,输出就是什么内容,遇到"#"为止。输入一个随便的字符 (2)代码: 【1】getchar()和putchar() #include "stdio.h"int main() {char ch;pr…...
PySide6/PyQT学习笔记(很杂)
QGroupBox样式:科技机甲 QGroupBox { border: 2px solid #333; /* 深色边框,类似金属质感 */ border-radius: 8px; /* 轻微的圆角 */ background-color: #222; /* 暗色背景,模拟机甲内部或科技界面 */ color: #fff; /* 字体颜色为白色&a…...

学习笔记-JWT 保持登录状态
目录 一、解析 token 1. 在 JWT 工具类添加解析 token 的方法 2. 在 Controller 添加获取用户数据的方法 二、获取用户信息 1. 发起 axios 请求用户信息 2. 在路由守卫中调用方法 3. 使用 三、token 时效性 1. 设置 token 过期时间 2. 判断 token 是否过期 3. 在拦截…...

React 性能优化
使用 useMemo 缓存数据 (类似 vue 的 computed)使用 useCallback 缓存函数异步组件 ( lazy )路由懒加载( lazy )服务器渲染 SSR用 CSS 模拟 v-show 循环渲染添加 key使用 Fragment (空标签)减少层级 不在JSX 中定义函数࿰…...

后端常见问题及深度解决方案
🐟作者简介:一名大三在校生,喜欢编程🪴 🐡🐙个人主页🥇:Aic山鱼 🐠WeChat:z7010cyy 🦈系列专栏:🏞️ 前端-JS基础专栏✨前…...

C:野指针介绍(定义、危害、规避)以及野指针与空指针的区分
目录 1、野指针 1.1 野指针的成因 1.指针未初始化 2.指针越界访问 3.指针指向的空间释放 1.2 野指针的危害 1.3 如何规避野指针 1. 指针初始化 2. 小心指针越界 3.指针变量不使用就及时赋上NULL 4. 指针使用前检查是否是空指针 5. 避免返回局部变量的地址 1.4 区…...

vue中v-html 后端返回html + script js中click事件不生效
效果图: 需求:点击加号执行后端返回的script中的代码 后端返回的html: <!DOCTYPE html> <html langzh> <head> <title>xxx</title> <style>body{font-size: 14px}p{text-indent: 30px;}textarea{width…...
介绍maven生命周期-水温
Maven生命周期是指一系列的构建阶段,包括项目的清理、编译、测试、打包、部署等。Maven通过定义生命周期来规范项目构建过程,使得开发人员可以方便地执行一系列的构建任务。 Maven的生命周期分为三个阶段: clean生命周期:主要用…...
树莓派超全系列教程文档--(62)使用rpicam-app通过网络流式传输视频
使用rpicam-app通过网络流式传输视频 使用 rpicam-app 通过网络流式传输视频UDPTCPRTSPlibavGStreamerRTPlibcamerasrc GStreamer 元素 文章来源: http://raspberry.dns8844.cn/documentation 原文网址 使用 rpicam-app 通过网络流式传输视频 本节介绍来自 rpica…...

【Oracle APEX开发小技巧12】
有如下需求: 有一个问题反馈页面,要实现在apex页面展示能直观看到反馈时间超过7天未处理的数据,方便管理员及时处理反馈。 我的方法:直接将逻辑写在SQL中,这样可以直接在页面展示 完整代码: SELECTSF.FE…...

边缘计算医疗风险自查APP开发方案
核心目标:在便携设备(智能手表/家用检测仪)部署轻量化疾病预测模型,实现低延迟、隐私安全的实时健康风险评估。 一、技术架构设计 #mermaid-svg-iuNaeeLK2YoFKfao {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg…...

【网络安全产品大调研系列】2. 体验漏洞扫描
前言 2023 年漏洞扫描服务市场规模预计为 3.06(十亿美元)。漏洞扫描服务市场行业预计将从 2024 年的 3.48(十亿美元)增长到 2032 年的 9.54(十亿美元)。预测期内漏洞扫描服务市场 CAGR(增长率&…...

家政维修平台实战20:权限设计
目录 1 获取工人信息2 搭建工人入口3 权限判断总结 目前我们已经搭建好了基础的用户体系,主要是分成几个表,用户表我们是记录用户的基础信息,包括手机、昵称、头像。而工人和员工各有各的表。那么就有一个问题,不同的角色…...

Vue2 第一节_Vue2上手_插值表达式{{}}_访问数据和修改数据_Vue开发者工具
文章目录 1.Vue2上手-如何创建一个Vue实例,进行初始化渲染2. 插值表达式{{}}3. 访问数据和修改数据4. vue响应式5. Vue开发者工具--方便调试 1.Vue2上手-如何创建一个Vue实例,进行初始化渲染 准备容器引包创建Vue实例 new Vue()指定配置项 ->渲染数据 准备一个容器,例如: …...

Linux中《基础IO》详细介绍
目录 理解"文件"狭义理解广义理解文件操作的归类认知系统角度文件类别 回顾C文件接口打开文件写文件读文件稍作修改,实现简单cat命令 输出信息到显示器,你有哪些方法stdin & stdout & stderr打开文件的方式 系统⽂件I/O⼀种传递标志位…...

rknn toolkit2搭建和推理
安装Miniconda Miniconda - Anaconda Miniconda 选择一个 新的 版本 ,不用和RKNN的python版本保持一致 使用 ./xxx.sh进行安装 下面配置一下载源 # 清华大学源(最常用) conda config --add channels https://mirrors.tuna.tsinghua.edu.cn…...

篇章二 论坛系统——系统设计
目录 2.系统设计 2.1 技术选型 2.2 设计数据库结构 2.2.1 数据库实体 1. 数据库设计 1.1 数据库名: forum db 1.2 表的设计 1.3 编写SQL 2.系统设计 2.1 技术选型 2.2 设计数据库结构 2.2.1 数据库实体 通过需求分析获得概念类并结合业务实现过程中的技术需要&#x…...

Linux操作系统共享Windows操作系统的文件
目录 一、共享文件 二、挂载 一、共享文件 点击虚拟机选项-设置 点击选项,设置文件夹共享为总是启用,点击添加,可添加需要共享的文件夹 查询是否共享成功 ls /mnt/hgfs 如果显示Download(这是我共享的文件夹)&…...