从0开始写一个简单的vite hmr 插件

从0开始写一个简单的vite hmr 插件

0. 写在前面


唠叨半天,赶紧开始吧

从0开始写一个简单的vite hmr 插件插图

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

    
    
    
    vite-hmr-plugin-test

    

修改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了

项目的结构如下

从0开始写一个简单的vite hmr 插件插图1

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 为资源后缀

从0开始写一个简单的vite hmr 插件插图2

插件生命分为3个阶段,启动时,模块传入时,服务器关闭时

关于这几个模块的具体说明见vite官方文档。

这里只说我们用到的transform

从名字可以看出是有关变化的函数,它的作用正是在我们执行导入的时候,提供检测的函数。

从0开始写一个简单的vite hmr 插件插图3

每当执行写一个import,vite就会把这个信息传递到每一个插件的transform中

插件根据所需转化自己需要的.

transform接收两个参数,src为导入的文本内容,另外一个id则是此模块的绝对路径(可以通过绝对路径进行文件类型判断)

transform(src, id) {

        return {
                code: "",
                // ...
        }
}

2.3 vite 插件是怎么提供其他资源导入的功能的?

前面提到的transform函数是一个解析函数,当通过import导入的时候,就会触发,然后经过一定处理之后返回。

所以你应该想到了,其他的资源应该是以某种符合js语法的方法导入了,而这个处理过程transform实现了这个过程,让一个原本不符合js语法的资源,变的合法了。

从0开始写一个简单的vite hmr 插件插图4

那么说到底是怎么实现的呢?

答:通过注入的方法。

从0开始写一个简单的vite hmr 插件插图5

在浏览器加载之前,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>TODO Parser');
        },

        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 = `
  • ` console.log("通过",html); } else if (readyRegex.test(line)) { html = `
  • ` 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;
    

    从0开始写一个简单的vite hmr 插件插图6

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

    4. HMR 实现

    从0开始写一个简单的vite hmr 插件插图7

    注意,vite中server和client是可以相互通信!这里只需要server向client发送消息

    4.1 server 发送

    vite服务器实例的获取有很多种方法:

    1. 直接通过vite钩子 configureServer(server) {}获取

      一般用来给vite服务器添加中间件

    2. 通过处理更新的钩子获取 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>TODO Parser');
            },
    
            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 [];
            }
        }
    }
    

    从0开始写一个简单的vite hmr 插件插图8

    • 如此简单的HMR就实现了,画面不会重新加载了。

    5. 写在最后

    HMR最好还是精确到元素,所以最好给parser提供一个能够精确定位到元素的id,以便模块更新的时候,能够精确定位到对于的元素以更新,而不是把所有的资源重新加载一遍。

    6. 拓展阅读

    强烈建议去阅读vite官方文档,写的真的很详细。

    另外,vite的模块解析,有一部分是通过rollup来实现的,所以可以去学学rollup的解析,加深理解。

    7. 代码

    Mushrr/vite-hmr-plugin-test (github.com)

    文章来源于互联网:从0开始写一个简单的vite hmr 插件

    THE END
    分享
    二维码