您现在的位置是:首页 >技术教程 >使用Vite虚拟模块功能重写多语言和多皮肤插件网站首页技术教程
使用Vite虚拟模块功能重写多语言和多皮肤插件
背景
为了处理在Vite和Vue3场景下,打包部署后实现多语言和皮肤的更换和修改的功能,我开发了两个vite插件。插件在构建结束前,把资源文件转换和复制到dist中,再使用HTTP请求读取资源,解决了多语言包和多皮肤包扩展的问题。
但是,当时的实现形式有点“稚嫩”,仅仅是做了文件的转换和复制功能。而开源生态中的插件,例如vite-plugin-pages
,@intlify/vite-plugin-vue-i18n
等,提供了非常方便的资源引入功能,只需要简单引入插件,就能实现页面路由,多语言文件等资源的快速导入。我的插件也希望拥有这样的功能。
旧插件的问题
首先,我们需要了解旧插件有什么问题,那些地方需要优化。
- 插件直接写在业务工程代码中,没有作为一个独立npm包
- 插件配置功能不完善,使用不便
- 如果用户修改了Vite中构建成果输出路径和静态资源路径,插件默认distPath将会报错
- 插件未提供开发模式和生产模式下多语言资源引入的功能,需要用户在工程中编写较多引入资源的代码
- 插件未提供切换皮肤的功能,同样需要用户在工程中自己编写
- 插件使用Axios获取生产模式多语言资源,需要额外引入依赖(使用fetch不需要引入)
- 多语言功能引入了解析json5、yaml等格式的依赖包,在生产模式中也被包含,使构建包体积变大
- 部分场景要求多语言后缀,例如
.msg .name
,如果直接在Object的key中包含.
,vue-i18n并不支持
下面我们一个一个来解决这些问题。虽然标题的重点是“Vite虚拟模块功能”,但是为了方便理解,实际上文章会按照顺序介绍整个插件的重写过程。
工程结构
新的多语言和多皮肤插件有了自己的独立工程,成为了独立的npm包。这两个插件都可以在工程中独立安装使用,甚至在非vue框架下使用。
多语言插件结构
|-- vite-plugin-i18n-xxx
|-- .eslintignore # eslint忽略文件
|-- .eslintrc.js # eslint配置
|-- .gitignore # git提交忽略文件
|-- .npmrc # npm配置
|-- .prettierignore # prettier忽略文件
|-- .prettierrc.js # prettier配置
|-- build.config.ts # unbuild构建配置
|-- client.d.ts # 虚拟模块声明文件
|-- package.json # nodejs项目配置
|-- tsconfig.json # typescript配置
|-- dist # 构建成果
| |-- index.cjs # 插件入口文件
| |-- index.d.ts # 插件声明文件
| |-- index.mjs # 插件esmodule入口文件
|-- src # 插件源码
| |-- buildI18n.ts # 构建多语言包逻辑
| |-- getDistI18n.ts # 获取构建包多语言逻辑
| |-- index.ts # 源码入口文件
| |-- types.d.ts # 源码声明
| |-- virtualModule.ts # 虚拟模块相关逻辑
|-- virtualCode # 虚拟模块使用的代码
|-- emptyI18n.js # 输出空函数
|-- fetchI18n.js # 获取构建包多语言
多皮肤插件结构
|-- vite-plugin-skin-xxx
|-- .eslintignore # eslint忽略文件
|-- .eslintrc.js # eslint配置
|-- .gitignore # git提交忽略文件
|-- .npmrc # npm配置
|-- .prettierignore # prettier忽略文件
|-- .prettierrc.js # prettier配置
|-- build.config.ts # unbuild构建配置
|-- client.d.ts # 虚拟模块声明文件
|-- package.json # nodejs项目配置
|-- tsconfig.json # typescript配置
|-- dist # 构建成果
| |-- index.cjs # 插件入口文件
| |-- index.d.ts # 插件声明文件
| |-- index.mjs # 插件esmodule入口文件
|-- src # 插件源码
| |-- buildSkin.ts # 构建多皮肤包逻辑
| |-- changeSkin.ts # 获取和变更皮肤逻辑
| |-- index.ts # 源码入口文件
| |-- types.d.ts # 源码声明
| |-- virtualModule.ts # 虚拟模块相关逻辑
|-- virtualCode # 虚拟模块使用的代码
|-- getBuildSkin.js # 开发模式获取皮肤
|-- getDevSkin.js # 生产模式获取皮肤
这两个插件的工程结构基本一致,实际开发的方法和使用也基本一致。
构建和npm包
两个插件都从业务工程中独立出来,成为了独立的npm包。原来的插件代码放到src/index.ts
中,内容未改变,依然使用ts编写。
unbuild构建
和脚手架工程一致,这里使用了unbuild作为构建工具,入口即是原来的插件代码。
// build.config.ts
import { defineBuildConfig } from 'unbuild'
export default defineBuildConfig({
// 入口文件
entries: ['src/index'],
clean: true,
// 生成ts声明文件
declaration: true,
// 警告是否会引发报错
failOnWarn: false,
// rollup配置
rollup: {
// 生成cjs
emitCJS: true,
esbuild: {
// 压缩代码
minify: false,
},
},
})
- 这里未压缩代码,因为压缩后代码很难被人类阅读,排查问题困难。作为一个在工程开发模式使用的工具,本身代码量也不大,因此决定不压缩了。
- 最后构建后的成果在dist目录中,入口为
index.cjs
。 - 同时还这里还生成了ts声明文件
index.d.ts
,提供给用户使用。
独立npm包
这里以多语言插件的package.json
为例说明,多皮肤插件的配置基本一致。
{
"name": "vite-plugin-i18n-xxx",
"version": "0.0.5",
"author": "jiazhen",
"main": "dist/index.cjs",
"types": "dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
},
"./client": {
"types": "./client.d.ts"
}
},
"scripts": {
"build": "unbuild",
"lint": "eslint src --fix",
"pretty": "prettier --write ."
},
"devDependencies": {
"@types/js-yaml": "^4.0.5",
"@types/node": "^18.15.11",
"@typescript-eslint/eslint-plugin": "^5.47.1",
"eslint": "^8.30.0",
"eslint-config-prettier": "^8.5.0",
"eslint-define-config": "^1.12.0",
"eslint-plugin-prettier": "^4.2.1",
"js-yaml": "^4.1.0",
"json5": "^2.2.1",
"prettier": "^2.8.1",
"unbuild": "^1.1.2",
"@typescript-eslint/parser": "^5.47.1",
"typescript": "^4.9.3"
},
"files": [
"dist",
"client.d.ts",
"virtualCode"
]
}
主要改动:
- exports npm包的导出配置
- 默认导出的是整个插件的入口文件。
- client 中导出的是虚拟模块的类型声明文件,在后面会描述
- types 默认导出的类型声明文件,这里导出插件整体的类型文件
- files 设置npm包上传的文件,仅仅只上传需要的文件即可,源码和工程配置不必上传
- devDependencies 所有的依赖都是开发依赖,打包之后该插件的使用不再需要依赖支持。
js-yaml
和json5
是功能需要使用的依赖,打包时直接被打包进dist中。unbuild
是打包使用的工具,dist中不需要包含。- 其他的声明文件,
eslint
,prettier
,typescript
等配置都是仅开发时才使用的,也不需要也不会打包进dist。
其实多语言和多皮肤插件本身在用户业务工程中使用时,除了虚拟模块中导出的内容会进入到构建包中,其它插件代码在打包后是不会进入构建包的。
这样,我们就解决了问题1,作为一个独立npm包。问题7解决了一半,原有构建部分使用依赖包被包含到独立npm包中了,但是业务工程中引入代码中多语言时依然需要这两个依赖。
使用Vite虚拟模块,引入多语言资源
虚拟模块简介
虚拟模块实际上是Vite背后的打包器Rollup的功能。在Rollup和Vite官方文档中都有关于虚拟模块的示例:
简单来说,我们在工程代码中普通引入一个模块,这个模块是需要真实存在的。但是使用虚拟模块,我们引入的模块可以不用真实存在,而是一个在插件中生成的新内容。通过虚拟模块,我们可以传入一些编译时信息。目前开源的很多vite生态插件都使用了该功能。
引入多语言资源
该功能的思路和实现基本是仿照了@intlify/vite-plugin-vue-i18n
插件。代码中简化了部分该章节不涉及的内容,且在后面的章节还会有改动。
// src/index.ts
import { getVirtualId, getJsonCode } from './virtualModule'
// 虚拟文件的的key
const MessageVirtualId = 'vite-plugin-i18n-xxx:messages'
export default function i18nBuildPlugin(
// 源码中多语言目录位置
srcPath = path.join('src', 'i18n'),
// 构建包中多语言位置
distPath = path.join('dist', 'assets', 'i18n')
) {
srcPath = path.join(process.cwd(), srcPath)
distPath = path.join(process.cwd(), distPath)
let isProduction = false
return {
name: 'vite-plugin-i18n-xxx',
resolveId(id: string) {
// 虚拟模块插件前缀
if (id === MessageVirtualId) {
return getVirtualId(MessageVirtualId)
}
},
load(id: string) {
if (id === getVirtualId(MessageVirtualId)) {
const messages: I18nFlat = getDevLangs(srcPath)
return getJsonCode(messages)
}
},
}
}
// src/virtualModule.ts
// 获取虚拟模块的id
export function getVirtualId(id: string) {
return '