稀土掘金技术社区 03月02日
写了5个vite插件后,发现其实vite插件并不难
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

本文总结了编写Vite插件的入门技巧和思维,通过多个例子介绍了代码增强、文件操作、文件路由等方面的应用,还分享了一些经验和相关插件。

🎯Vite插件用于增强代码,可解决纯前端无法实现的问题

💻通过动态注入代码,在浏览器控制台打印项目信息

📁利用Vite环境操作文件,提升开发效率,如管理国际化资源文件

🚀实现文件系统路由,增强项目维护性,为前端提供强大运行时能力

原创 Minko 2025-02-28 08:30 重庆

点击关注公众号,“技术干货”及时达!

点击关注公众号,“技术干货” 及时达!

前言

如果你是一名纯前端开发,很少或者根本没有写过 nodejs,那么从编写 vite 插件入门 nodejs,会带给你很丝滑的体验。

这篇文章将从我个人的经验总结出编写 vite 插件的入门技巧和思维,希望对大家有所帮助。

如何入门 vite 插件?

先有想法,再去学习。如果你遇到了一些问题,无法通过纯前端代码实现,或许这时候就可以想想,能不能用 vite 插件实现。有了想法之后,再去 vite/rollup 文档上看应该使用插件的哪些钩子。我认为这样是比较好的实践方式。

vite 插件的本质

既然我们想写一个 vite 插件,那么应该对其有个基本的认知。

我认为:「vite 插件用于增强代码。」

代码增强

代码增强也就是通过 vite 的能力,对项目代码做魔改,以实现功能。

以下我通过 3 个例子来说明什么是代码增强。

动态注入代码

举个例,假设我项目启动后,在浏览器控制台中,打印出项目的版本号、构建时间、构建环境。

构建时间、构建环境在前端中都无法获取,只能在 vite 环境中获取到,所以就可以写 vite 插件来实现。

想法有了,那现在需要知道,在什么钩子中能获取到我们需要的数据,查阅 vite 官方文档,有个 configResolved钩子能获取到构建环境,那么插件已经有个雏形了

import path from 'path'
import { defineConfig, PluginOption, ResolvedConfig } from 'vite'
import { createRequire } from 'module'
import dayjs from 'dayjs'
import tz from 'dayjs/plugin/timezone'
import utc from 'dayjs/plugin/utc'
dayjs.extend(tz)
dayjs.extend(utc)
function viteLogTime() {
let config: ResolvedConfig
let version: string
const currentTime = dayjs().tz("Asia/Shanghai").format("YYYY-MM-DD HH:mm:ss")

return {
name: 'vite-log-time',
configResolved(_config) {
config = _config
const require = createRequire(import.meta.url)
version = require(path.resolve(config.root, 'package.json')).version
}
}
}

到这一步,就拿到了我们所需要的信息,下一步,就是思考如何把信息打印到浏览器控制台上

让我们站在纯前端的视角来思考这个问题,要把信息打印到控制台,很简单吧?在代码中加 console.log呗!

没错,就是这么简单,切换到 vite 插件的视角来解决这个问题,同样也是给代码中加 console,但是怎么加,加在哪里?

查阅 vite 文档后,发现有两个钩子,可以对项目代码进行魔改,一个叫 transform,一个叫 transformIndexHtml。那我们就用这两个钩子来试试

如果使用 transform,那需要确定要魔改的文件,在这里我们就找入口文件,确保了唯一性。

import path from 'path'
import { defineConfig, PluginOption, ResolvedConfig } from 'vite'
import { createRequire } from 'module'
import dayjs from 'dayjs'
import tz from 'dayjs/plugin/timezone'
import utc from 'dayjs/plugin/utc'
dayjs.extend(tz)
dayjs.extend(utc)
function viteLogTime() {
let config: ResolvedConfig
let version: string
const currentTime = dayjs().tz("Asia/Shanghai").format("YYYY-MM-DD HH:mm:ss")

return {
name: 'vite-log-time',
configResolved(_config) {
config = _config
const require = createRequire(import.meta.url)
version = require(path.resolve(config.root, 'package.json')).version
},
transform(code, id) {
if(id.endsWith('src/main.tsx')) {
const info = {
mode: config.mode,
currentTime,
version,
}
return {
code: `
console.log('构建信息:', '${JSON.stringify(info)}')
${code}
`,
map: null
}
}
},
}
}

启动项目后,就可以看到控制台打印了信息!

还有个 transformIndexHtml钩子,咱们也试试

import path from 'path'
import { defineConfig, PluginOption, ResolvedConfig } from 'vite'
import { createRequire } from 'module'
import dayjs from 'dayjs'
import tz from 'dayjs/plugin/timezone'
import utc from 'dayjs/plugin/utc'
dayjs.extend(tz)
dayjs.extend(utc)
function viteLogTime() {
let config: ResolvedConfig
let version: string
const currentTime = dayjs().tz("Asia/Shanghai").format("YYYY-MM-DD HH:mm:ss")

return {
name: 'vite-log-time',
configResolved(_config) {
config = _config
const require = createRequire(import.meta.url)
version = require(path.resolve(config.root, 'package.json')).version
},
transformIndexHtml() {
const info = {
mode: config.mode,
currentTime,
version,
}
return [
{
tag: 'script',
attrs: {
type: 'module',
},
children: `console.log('构建信息:', '${JSON.stringify(info)}')`,
},
]
}
}
}

这样也能打印构建信息。

这是一个非常简单的例子,但也能从中看出,vite 插件可以通过魔改注入代码,来解决纯前端无法实现的点。

文件操作

在 node 环境中,我们可以大展身手,开发最离不开的就是一个个代码文件,接下来我举个例,通过 vite 环境操作文件来提升开发效率

TikTok 被 ban 的时候,小红书融入了大量的歪果仁,那时候小红书迫切的需求就是国际化。随着业务扩展,国际化越来越流行,我目前也主要是负责的国际化项目。

国际化,无非就是两个字:翻译。说白了,也就是把本土语言,翻译成其他国家语言,放在语言文件中。

import zhCommon from "./zh/common.json"
import zhHome from "./zh/home.json"
import enCommon from "./en/common.json"
import enHome from "./en/home.json"
const languages = ["en", "zh"] as const
const resources = {
en: {
common: enCommon,
home: enHome
},
zh: {
common: zhCommon,
home: zhHome
},
}

国际化资源一般是统一管理,所以也避免不了不断地导入语言 json 文件,每次新增翻译文件时,会做大量的重复工作。于是我想能不能做个 vite 插件来管理所有的国际化资源文件,让开发者从翻译文件中解脱!

核心思路就是 收集指定目录中的语言文件,然后通过虚拟文件的方式让前端可导入。

这里就简单写点伪代码,完整代码在这里:github[1]

export function i18nAlly(options?: I18nAllyOptions): PluginOption {
// 一个可以收集用户指定目录下的所有翻译文件的探测器实例
const localeDetector = new LocaleDetector(options)
let server: ViteDevServer
return {
name: 'vite:plugin-i18n-ally',
enforce: 'pre',
async config() {
// 初始化翻译文件探测器
await localeDetector.init()
},
// 此hook可用于虚拟文件,参考vite官方文档
async resolveId(id: string, importer: string) {
const { virtualModules, resolvedIds } = localeDetector.localeModules
if (id in virtualModules) {
return VirtualModule.resolve(id) // 例如:\0/@i18n-ally/virtual:i18n-ally-en
}
return null
},
async load(id) {
const { virtualModules, resolvedIds, modules, modulesWithNamespace } = localeDetector.localeModules
if (id.startsWith(VirtualModule.resolvedPrefix)) {
const idNoPrefix = id.slice(VirtualModule.resolvedPrefix.length)
const resolvedId = idNoPrefix in virtualModules ? idNoPrefix : resolvedIds.get(idNoPrefix)
// e.g. \0/@i18n-ally/virtual:i18n-ally-en
// 如果是翻译虚拟文件,则返回探测到的文件内容
if (resolvedId) {
const module = virtualModules[resolvedId]
return typeof module === 'string' ? module : `export default ${JSON.stringify(module)}`
}
}
return null
},
} as PluginOption
}

然后在前端,通过导入虚拟文件,就可以获取到翻译资源了。

这个例子相对比较复杂一点,但核心就是告诉新手朋友们,你们也在遇到类似的大量重复工作时,也可以尝试通过 vite 插件来提升开发效率、减少维护负担

文件路由

通常是在一个上层框架才会集成文件路由,比如 nextjs、remix、nuxt 这些。文件系统路由相比配置式路由来说,也是减少了大量重复开发工作,而且使项目结构更加清晰,很大程度上增强了项目的维护性,如果你们既使用 nextjs/remix 这种 ssr 框架,又有普通的 vite 单页面项目,那么统一的文件系统路由,也能对其项目之间的开发习惯。

我是从 react-router 6.4 引入 data api 之后,开始尝试在单页面项目中引入文件系统路由。为什么呢?如果你熟悉 react-router 的话,会发现想要利用好 data-api,最好的实践就是像 remix 那样,在路由文件中导出 data api。如果你不了解的话,也无所谓,接下来也是单纯对文件系统路由做分析思考以及提出解决方案。

文件路由,实际上就是在 node 层,收集到所有的路由文件,然后组装成前端路由库所需要的数据格式,交由前端路由。

插件相对也是比较复杂,完整代码在此处:github[2]

贴一下伪代码:

import type * as Vite from 'vite'
function remixFlatRoutes(options: Options = {}): Vite.PluginOption {
return [
{
name: 'vite-plugin-remix-flat-routes',
// 前端通过虚拟文件导入组装好的路由结构
async resolveId(id) {
if (id === 'virtual:route') {
return '\0virtual:route'
}
return null
},
async load(id) {
switch (id) {
case '\0virtual:route': {
// 遍历项目文件,找到路由文件,并进行组装
const routes = findRoutes()
const { routesString, componentsString } = await routeUtil.stringifyRoutes(routes)
return {
code: `import React from 'react';
${componentsString}
export const routes = ${routesString};
`,
map: null,
}
}
default:
break
}
return null
},
},
]
}

这个例子的核心也是通过 nodejs 解析到前端所需要的数据,然后通过虚拟文件的形式暴露给前端,让前端拥有了更强大的运行时能力。

总结

上文讲到的,其实都是业务相关的插件,当然 vite 也有很多构建时插件,比如代码 zip 压缩、代码分析等,这些我认为不适合在入门时学习,因为大部分前端开发其实都是在跟业务打交道。

在我写了不少的 vite 插件后,我总结了以下经验

    先知道自己想做什么插件,然后再去实践

    如果你刚接触 vite 插件,或许你最头疼的是什么时候用什么钩子,其实当你知道你需要解决什么问题的时候,再去翻文档,或者查 AI,很快就能得到答案

    在 vite 插件中,前端代码不过是 “字符串”,随便你怎么添加删除修改都可以,不要害怕代码改了就会出问题

    多看入门级插件代码,比如 vite-plugin-html, vite-plugin-legacy,这些库或许可以让你明白什么钩子在什么场景下使用

以下是我写的一些 vite 插件,可供学习参考:


点击关注公众号,“技术干货” 及时达!

阅读原文

跳转微信打开

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

Vite插件 代码增强 文件操作 文件路由
相关文章