前言
本文结合vite后端集成的需求和大家探讨如何开发一款vite插件。
背景
公司的业务是一个低代码PAAS平台,名叫kintone,同时这个低代码平台有非常高的可定制性。它接受用户上传自定义js。
然后你懂得,这样我们就有了非常大的操作空间。可以直接上react,或者vue创建一个前端spa项目后挂载在指定的dom上, 这样平台的界面就完全被我们自定义了。同时它还有rest api。我们能通过rest api获取后端数据,这样业务信息也可以扩展到我们想要展示的页面。
但是它也有一个问题,它引用你上传的js文件的方式是固定的,只能使用同步,不能使用esmodule的方式(也就是type="module")来引入。这样我们就无法使用vite来进行开发构建啦。 所以我就想到了用vite插件来改造它。
这里也用它来作为一个例子,相信也有很多小伙伴有遇到过同样的问题,其实这个问题就是泛化为后端html渲染已经固定,你如何通过vite来服务其他资产。
翻看vite文档。其实有专门的一章来讲解后端集成。
vite后端集成
分析文档
我们今天就来参考这篇vite后端集成文档,一步一步分析该如何解决这些问题。
1. 启用和创建manifest
:
首先在你使用vite配置文件中
js
// vite.config.js
export default defineConfig({
build: {
manifest: true,
}
})
2. 禁用module preload 的 polyfill
参考文档,也就是build.modulePreload.polyfill设置为false
js
// vite.config.js
export default defineConfig({
build: {
modulePreload: { polyfill: false },
}
})
这两件事都是build阶段要做的,所以后面我们要在插件的生命周期的build的阶段加上去。
3. 注入文件
在开发环境中,在服务器的 HTML 模板中注入以下内容
js
<!-- 如果是在开发环境中 -->
<script type="module" src="http://localhost:5173/@vite/client"></script>
<script type="module" src="http://localhost:5173/main.js"></script>
这部分,注入是关键。但是这个本地环境地址,包括端口,其实是随用户设置可变的,我们在插件中需要获取到用户的自定义配置,然后动态生成,并且注入。
4. 得到正确的资源路径
为了正确地提供资源,你有两种选项:
- 确保服务器被配置过,将会拦截代理资源请求给到 Vite 服务器
- 设置
server.origin
以求生成的资源链接将以服务器 URL 形式被解析而非一个相对路径
这对于图片等资源的正确加载是必需的。
这部分其实挺关键,我们可以通过第二种选项来实现。就是在serve阶段,设置server.config.server.origin
为我们本地地址即可。
5. react框架的Inject
如果你正使用 @vitejs/plugin-react
配合 React,你还需要在上述脚本前添加下面这个,因为插件不能修改你正在服务的 HTML:
js
<script type="module">
import RefreshRuntime from 'http://localhost:5173/@react-refresh'
RefreshRuntime.injectIntoGlobalHook(window)
window.$RefreshReg$ = () => {}
window.$RefreshSig$ = () => (type) => type
window.__vite_plugin_react_preamble_installed__ = true
</script>
同样我们需要获取用户实际的本地开发地址,端口。并且进行注入。
插件开发
接下来,我们就结合刚才分析的文档,一步步实现这个插件。
vite生命周期
我们先找到Vite 独有钩子这块。
config
钩子
这个生命周期是用来获取用户的设置,并且可以通过这个钩子加入你想给用户额外添加的设置。此时回顾到上文1和2部分,就是需要我们在build的配置。 那我们就把这个配置在config这个生命周期中重写。
js
config(config, env) {
envConfig = env;
const entry = getEntry(config);
config.build = {
modulePreload: { polyfill: false },
manifest: true,
cssCodeSplit: false,
rollupOptions: {
input: entry,
output: {
format: "iife",
},
},
};
...
}
github代码
configResolved
钩子
这个钩子和config的那个钩子有什么不同呢?这个钩子已经不再支持插件对用户配置信息的修改了。通过ts也可以观察到,他接受的参数也是Readonly了。
那这个钩子能做什么?比如说我需要写一些配置到.env中,此时我们就能在这个钩子里进行操作,因为这个钩子中读到的是vite的最终配置,它不会被改了,此时读到的.env所在的文件夹等等都是最终的。然后我们就能通过inquirer等库,建立和用户的交互,写入一些用户的自定义设置到.env文件中。
js
const envDir = viteConfig.envDir
? normalizePath(path.resolve(resolvedRoot, viteConfig.envDir))
: resolvedRoot;
let envUrl =
envConfig.mode === "development"
? path.resolve(envDir, ".env.development")
: path.resolve(envDir, ".env.production");
const envContent = { ...existingEnv, ...env };
// 将环境变量写入到.env文件
const envContentStr = Object.entries(envContent)
.map(([key, value]) => `${key}=${value}`)
.join("\n");
fs.writeFileSync(envUrl, envContentStr);
github代码
configureServer
钩子
最后是configureServer这个钩子,因为它能获取到服务器启动的配置,所以之前提到3,4,5这些部分,需要知道本地配置,端口的这些代码,就需要写在这个钩子中。
获取本地开发的服务器信息:
js
import { type ViteDevServer } from "vite";
function isIpv6(address: any) {
return address.family === "IPv6" || address.family === 6;
}
export default function getServerInfo(server: ViteDevServer) {
const address = server.httpServer?.address();
if (!address || typeof address === "string") {
console.error("Unexpected dev server address", address);
process.exit(1);
}
const protocol = server.config.server.https ? "https" : "http";
const host = isIpv6(address) ? `[${address.address}]` : address.address;
const port = address.port;
const devServerUrl = `${protocol}://${host}:${port}`;
return devServerUrl;
}
设置server.config.server.origin
js
const devServerUrl = getServerInfo(server);
if (!server.config.server.origin) {
server.config.server.origin = devServerUrl;
}
inject js代码
js
import type { ScriptList } from "kintone-types";
function reactInject(devServerUrl: string) {
return `const scriptElement = document.createElement("script");
scriptElement.type = "module";
scriptElement.textContent = \`import RefreshRuntime from '${devServerUrl}/@react-refresh';
RefreshRuntime.injectIntoGlobalHook(window);
window.$RefreshReg$ = () => {};
window.$RefreshSig$ = () => (type) => type;
window.__vite_plugin_react_preamble_installed__ = true;\`;
document.body.appendChild(scriptElement);`;
}
export default function kintoneModuleInject(
devServerUrl: string,
scriptList: ScriptList,
react?: boolean
): string {
return `(function () {
const viteClientInject = document.createElement("script");
viteClientInject.type = "module";
viteClientInject.src = "${devServerUrl}"+'/@vite/client';
document.body.appendChild(viteClientInject);
${react ? reactInject(devServerUrl) : ""}
const scriptList = ${JSON.stringify(scriptList)};
function loadScript(src,type) {
const script = document.createElement("script");
script.type = type;
script.src = "${devServerUrl}"+src;
document.body.appendChild(script);
}
for (const script of scriptList){
const {src,type}=script
loadScript(src,type)
}
})();
`;
}
github代码
开发(serve)和构建(build)模式的区分
因为1,2部分是build阶段的。其它部分,只需要在serve阶段执行就行了,那我们可以用 apply: "serve" 或者 apply: "build"。这个按需调用参数来指定它在哪种模式下执行。
插件开发中遇到的问题以及一些工具
命令交互工具
推荐使用@inquirer/prompts 而不是 inquirer。 @inquirer/prompts是重构后的新版本。inquirer是老版本。老版本不仅9.x只支持esm,而且在进行构建时还有bug(8.x)。
esm打包
很多工具都已经只发布esm版本,会导致导入打包后出现ESM的引用错误,这个问题比较复杂,可以参考其他小伙伴总结的一些经验 typescript 项目中的 esm 模块依赖问题,简单的解决办法就是改用一些老版本来尝试。
插件打包工具
插件打包工具从一开始最基本的tsc,到tsup,最后到unbuild,配置一步步简化。如果你想得到更多有帮助的预设,减少配置,就直接使用unbuild吧。
开源发包库
使用bumpp
同时建立一个npm script: "release": "npm run build && bumpp"
可以让你发布大版本,小版本,建立tag。非常方便。
changelog
写changelog,可以使用这个:changelogen
发布到npm
一条命令:npm publish
统计图标
类似于这些图标都是来源于shields.io这个网站来提供服务。