从0开始写一个简单的vite hmr 插件
从0开始写一个简单的vite hmr 插件
0. 写在前面
唠叨半天,赶紧开始吧

1. 初始化项目
由于是真从0开始,我们这里不选择vite官方提供的create-vite,而是通过依赖安装的方式一步步搭建起来一个vite-plugin
按照你习惯的方式初始化项目
mkdir vite-plugin-todo
// pnpm
pnpm init
// yarn
yarn init
// npm
npm init
cd vite-plugin-todo
安装vite
// pnpm
pnpm add vite
// yarn
yarn add vite
// npm
npm add vite
初始化项目目录
// 用来作为vite的入口,以及页面展示
touch index.html
// src文件夹以及main入口
mkdir src
touch src/main.ts
// plugins文件夹,存放我们的vite插件
mkdir plugins
// 创建vite配置文件, 以及vite环境配置文件
touch vite.config.ts
touch src/vite-env.d.ts
在index.html 中添加main.ts 入口
// index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>vite-hmr-plugin-test</title>
</head>
<body>
<!--这里添加main.ts 入口-->
<script src="/src/main.ts" type="module"></script>
</body>
</html>
修改package.json 的命令
{
"name": "vite-plugin-todo",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "vite dev"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@types/node": "^18.8.5",
"vite": "^3.1.8"
}
}
为了使得typescript能够解析nodejs模块
pnpm add @types/nodejs
yarn add @types/nodejs
npm install @types/nodejs
尝试一下pnpm dev 没报错的话就OK了
项目的结构如下

2. 初识vite plugin
2.1 vite 插件是什么
2.2 vite 插件的生命周期
在说vite插件生命周期之前,我们还是先完善一下vite.config.ts
import { defineConfig } from "vite";
export default defineConfig({
plugins: [
// Plugins
],
assetsInclude: [
"src/**/*.todo"
]
})
- 定义并导出一个配置
plugins是用来存放vite插件实例的assetsInclude是用来指明需要解析的资源路径的,我们这里以.todo为资源后缀

插件生命分为3个阶段,启动时,模块传入时,服务器关闭时
关于这几个模块的具体说明见vite官方文档。
这里只说我们用到的transform
从名字可以看出是有关变化的函数,它的作用正是在我们执行导入的时候,提供检测的函数。

每当执行写一个import,vite就会把这个信息传递到每一个插件的transform中
插件根据所需转化自己需要的.
transform接收两个参数,src为导入的文本内容,另外一个id则是此模块的绝对路径(可以通过绝对路径进行文件类型判断)
transform(src, id) {
return {
code: "",
// ...
}
}
2.3 vite 插件是怎么提供其他资源导入的功能的?
前面提到的transform函数是一个解析函数,当通过import导入的时候,就会触发,然后经过一定处理之后返回。
所以你应该想到了,其他的资源应该是以某种符合js语法的方法导入了,而这个处理过程transform实现了这个过程,让一个原本不符合js语法的资源,变的合法了。

那么说到底是怎么实现的呢?
答:通过注入的方法。

在浏览器加载之前,vite先帮你把各种import模块全部转换好,转换为如上的形式,那你说这都定义成变量了,浏览器肯定认啊,对吧!
那你可能会问我还是不明白,到底怎么转换的,其实就是通过transform的返回值来解析转换的。
transform(src, id) {
// 解析这个文件,是不是你要的type
// 执行转换
// 把转换的结果可以通过 `` 插值到code里面
return {
code: "", // 转换后的代码
// ...
}
}
2.4 vite插件长什么样?
export default function todoParser() {
// 插件创建之初的代码,可以在这里配置插件所需的资源
return {
name: "todo-parser", // 插件名
// 生命周期函数
transform(src, id) {
// 解析这个文件,是不是你要的type
// 执行转换
// 把转换的结果可以通过 `` 插值到code里面
return {
code: "", // 转换后的代码
// ...
}
}
}
}
2.5 如何让typescript支持导入这个模块?
回到之前,我们不是说vite插件中transform能够丰富资源的导入,
但是这不代表typescript就认可,不认可依然不能提供完备的补全和检查,
所以为了让typescript彻底服气,就需要在vite-env.d.ts中写一段模块解析的配置
// vite-env.d.ts
declare module '*.todo' {
export const data: string;
export function parser(content: string);
}
这里定义了一个模块,并导出了两个成员
一个叫data, 是string类型的资源
一个叫parser,是个解析函数(稍后会介绍)
这样写了之后,typescript就会默认我们能够导入.todo 后缀的文件,并且这里面有两个成员,一个是data,一个是parser。
3. todo插件编写
O 吃饭
X 喝水
O 跑步五公里
这样一种文本,O表示未完成,X表示完成,后面表示当前todo的信息
3.1 todo插件
在plugins中创建一个todoParser.ts
export default function todoParser(): Plugin {
let todoFileRegex = /\.(todo)$/;
// 解析.todo 的正则
return {
name: "todo-parser",
transformIndexHtml(html) {
return html.replace(/<title>(.*?)<\/title>/, '<title>TODO Parser</title>');
},
async transform(src, id) {
// module inject
console.log(id);
// 看看当前文件是否通过了正则,如果通过了,就执行
if (todoFileRegex.test(id)) {
return {
// 这里的parser是解析器,稍后会说
code: `
export let data = "${parser(src)}"
export ${parser}
`
};
}
}
}
}
相信阅读了前面有关vite插件的介绍应该不难理解
3.2 parser
为了能够解析.todo文件,并且输出我们希望的内容,
还需要提供解析一个解析器来解析。
// todoParser.ts
function parser(src: string) {
// 解析
const lines = src.split('\n');
let todoList = "";
let finishRegex = /^X/;
let readyRegex = /^O/;
let content = /\s(.*)$/
let randomId: string;
for (let line of lines) {
randomId = Math.random().toString(32).slice(2);
let html: string;
if (finishRegex.test(line)) {
console.log(line);
html = `<li><input type='checkbox' checked id='${randomId}'/><label for='${randomId}'>${line.trim().match(content)![1]}</li>`
console.log("通过",html);
} else if (readyRegex.test(line)) {
html = `<li><input type='checkbox' id='${randomId}'/><label for='${randomId}'>${line.trim().match(content)![1]}</li>`
console.log("拒绝",html);
}
todoList += html!;
}
return todoList;
}
我们这里通过正则获取了每一行数据中表示状态的 OX, 以及其内容,并且封装为一组checkbox
这些文本信息可以直接插入html以显示其内容
3.3 插件的装载
import { defineConfig } from "vite";
import todoParser from './plugins/todoParser';
export default defineConfig({
plugins: [
todoParser()
],
assetsInclude: [
"src/**/*.todo"
]
})
回到vite.config.ts中,在plugins数组内部直接执行todoParser(),实现插件的装载
在main.ts 中接收这个导入的资源,并且赋值到document中
import { data } from './assets/journey.todo'
import './style.css'; // 样式,这里消除了li的一般样式 list-style: none
console.log(data);
document.body.innerHTML = data;

- 上面的预览图可以发现我们实现了功能,但是每次一写完,整个页面就会全部刷新,这可不太好,所以还需要HMR
4. HMR 实现

注意,vite中server和client是可以相互通信!这里只需要server向client发送消息
4.1 server 发送
vite服务器实例的获取有很多种方法:
直接通过vite钩子 configureServer(server) {}获取
一般用来给vite服务器添加中间件
通过处理更新的钩子获取 handleHotUpdate({file, server, modules}){}
这里我们要实现的是热更新,所以采用handleHotUpdate就可以了,在模块更新的时候,就会触发这个函数,通过server向client发送更新的消息,以及更新的数据,然后让浏览器在未刷新的情况下直接更新
async handleHotUpdate({ server, file, modules }) {
let fileData = await fs.readFile(modules[0].id as string);
server.ws.send({
type: 'custom',
event: 'special-update', // 事件名
data: {
msg: "Update from server",
updateVal: fileData.toString()
}
})
console.log(`${file} should be updated`);
return [];
}
通过node的fs模块读取到了文本的数据
随后通过server.ws.send()向client发送的数据,其中更新之后的数据存放在data.updateVal中
4.2 client 获取
在vite中,模块热更新以事件的形式抛出,具体来说是
import.meta.hot.on('xxx事件', () => {} /*事件回调*/)
我们这里编写如下代码
if (import.meta.hot) {
import.meta.hot.on('special-update', (data) => {
data = parser(data.updateVal);
document.body.innerHTML = data;
})
}
如果更新了,那么就执行parser,解析数据,最后把数据赋值到document.body.innerHtml上。
那这个代码应该写在哪儿呢?
答应该写在,模块导入的未知,也就是transform函数的返回值中
这样才能保证每一个.todo模块都能够热更新!
// 完整的parser
export default function todoParser(): Plugin {
let todoFileRegex = /\.(todo)$/;
// local variable
function log(msg) {
console.log(msg);
}
return {
name: "todo-parser",
transformIndexHtml(html) {
return html.replace(/<title>(.*?)<\/title>/, '<title>TODO Parser</title>');
},
transform(src, id) {
// module inject
console.log(id);
if (todoFileRegex.test(id)) {
return {
code: `
export let data = "${parser(src)}"
export ${parser}
if (import.meta.hot) {
import.meta.hot.on('special-update', (data) => {
data = parser(data.updateVal);
document.body.innerHTML = data;
})
}
`,
};
}
},
async handleHotUpdate({ server, file, modules }) {
let fileData = await fs.readFile(modules[0].id as string);
server.ws.send({
type: 'custom',
event: 'special-update',
data: {
msg: "Update from server",
updateVal: fileData.toString()
}
})
console.log(`${file} should be updated`);
return [];
}
}
}

- 如此简单的HMR就实现了,画面不会重新加载了。
5. 写在最后
HMR最好还是精确到元素,所以最好给parser提供一个能够精确定位到元素的id,以便模块更新的时候,能够精确定位到对于的元素以更新,而不是把所有的资源重新加载一遍。
6. 拓展阅读
强烈建议去阅读vite官方文档,写的真的很详细。
另外,vite的模块解析,有一部分是通过rollup来实现的,所以可以去学学rollup的解析,加深理解。
7. 代码
Mushrr/vite-hmr-plugin-test (github.com)
从0开始写一个简单的vite hmr 插件的更多相关文章
- 用Python写一个简单的Web框架
一.概述 二.从demo_app开始 三.WSGI中的application 四.区分URL 五.重构 1.正则匹配URL 2.DRY 3.抽象出框架 六.参考 一.概述 在Python中,WSGI( ...
- 如何写一个简单的http服务器
最近几天用C++写了一个简单的HTTP服务器,作为学习网络编程和Linux环境编程的练手项目,这篇文章记录我在写一个HTTP服务器过程中遇到的问题和学习到的知识. 服务器的源代码放在Github. H ...
- 如何写一个简单的shell
如何写一个简单的shell 看完<UNIX环境高级编程>后我就一直想写一个简单的shell来作为练习,因为有事断断续续的写了好几个月,如今写了差不多来总结一下. 源代码放在了Github: ...
- 分享:计算机图形学期末作业!!利用WebGL的第三方库three.js写一个简单的网页版“我的世界小游戏”
这几天一直在忙着期末考试,所以一直没有更新我的博客,今天刚把我的期末作业完成了,心情澎湃,所以晚上不管怎么样,我也要写一篇博客纪念一下我上课都没有听,还是通过强大的度娘完成了我的作业的经历.(当然作业 ...
- 一步一步写一个简单通用的makefile(三)
上一篇一步一步写一个简单通用的makefile(二) 里面的makefile 实现对通用的代码进行编译,这一章我将会对上一次的makefile 进行进一步的优化. 优化后的makefile: #Hel ...
- Java写一个简单学生管理系统
其实作为一名Java的程序猿,无论你是初学也好,大神也罢,学生管理系统一直都是一个非常好的例子,初学者主要是用数组.List等等来写出一个简易的学生管理系统,二.牛逼一点的大神则用数据库+swing来 ...
- (2)自己写一个简单的servle容器
自己写一个简单的servlet,能够跑一个简单的servlet,说明一下逻辑. 首先是写一个简单的servlet,这就关联到javax.servlet和javax.servlet.http这两个包的类 ...
- [闲的蛋疼系列]从零开始用TypeScript写React的UI组件(0)-先写一个Button??
0.咸鱼要说的 一入前端深似海,咸鱼入海更加咸. 最近闲的蛋疼,手上年前的事也完成了7788了,借助[PG1]的话来说,我们要keep real. 咸鱼肯定不real 了,因为我们都活在梦里,所以咱们 ...
- express 写一个简单的web app
之前写过一个简单的web app, 能够完成注册登录,展示列表,CURD 但是版本好像旧了,今天想写一个简单的API 供移动端调用 1.下载最新的node https://nodejs.org/zh- ...
随机推荐
- [eJOI2019]异或橙子 题解
简要题面 维护一个数据结构,支持单点修改,询问区间所有子区间的异或和的异或和 . 做法 首先,题目要求所有子区间的异或和的异或和,发现每个元素异或两次就变成 \(0\),所以考虑统计每个元素出现的次数 ...
- PLC转OPC UA的协议转换网关需要多少钱呢?
嵌入式OPC UA网关BL102简化了OPC UA程序的开发与IIOT工业物联网应用 在制造业数字化升级过程中,我们碰到最多的工作便是针对每一款PLC去开发一套OPC UA程序,然后通过这套程序去读取 ...
- 使用python3.7和opencv4.1来实现人脸识别和人脸特征比对以及模型训练
原文转载自「刘悦的技术博客」https://v3u.cn/a_id_126 OpenCV4.1已经发布将近一年了,其人脸识别速度和性能有了一定的提高,这里我们使用opencv来做一个实时活体面部识别的 ...
- 使用Typora+EasyBlogImageForTypora写博客,无图床快速上传图片
如今,使用markdown攥写博客已成为主流,而Typora作为markdown的主流工具,广受大众好评,本文讲述从Typora的安装到快速将Typora写好的博文上传到博客园 Typora下载 Ty ...
- GitHub 主页美化设置教程
GitHub profile设置教程 早些时候逛GitHub,就发现别人的主页特别的精美,当时没有空研究,前几天得空给安排了一下 先看一下成品 贴一个github上一个男人的主页 下面这个是我的 果然 ...
- java日常开发必备:list的四种遍历
在平时的开发过程中使用List的场景很多,你知道List的遍历有多少种方式?今天一起来梳理下List的几种遍历方式.这里以java.util.ArrayList为例来演示. 这里有一个最简单的 ...
- bash脚本里的-h是什么意思?
问题描述 我在看脚本的时候,看到了下面代码 其中的-h "$PRG"我一时没明白是在判断什么东西.然后翻阅了一下菜鸟教程和其他教程,都没有说. 问题解决 -h其实是在判断这个文件是 ...
- [题解]Balance
1.题目 POJ-1837 2.题目大意 一个天平上有一些钩子,现在有一些砝码.给出每个钩子到原点(姑且这么叫吧)的距离(-15 ~ 15,负数代表在左边,正数相反)以及砝码的重量(1 ~ 20),求 ...
- 创新能力加速产业发展,SphereEx 荣获“中关村银行杯”『大数据与云计算』领域 TOP1
8 月 9 日下午,2022 中关村国际前沿科技创新大赛"中关村银行杯"大数据与云计算领域决赛在北京市门头沟区中关村(京西)人工智能科技园·智能文创园落下了帷幕.SphereEx ...
- navicat创建连接 2002-can‘t connect to server on ....
环境: 系统:centos7 生产环境:docker 中部署MySQL 报错提示符:"2002-Can't connect to server on '192.168.200.22'(100 ...