国际化:打包 & 构建优化
问题:多余的 Element Plus 语言包被打包
使用 import.meta.glob('element-plus/dist/locale/*.mjs') 导入语言文件时,会匹配到 Element Plus 所有语言包(数十种),导致构建产物中包含大量不需要的翻译文件。
构建产物分析
dist/assets/
├── zh-CN-[hash].js ← 需要的
├── en-[hash].js ← 需要的
├── ar-[hash].js ← 不需要(阿拉伯语)
├── de-[hash].js ← 不需要(德语)
├── es-[hash].js ← 不需要(西班牙语)
├── fr-[hash].js ← 不需要(法语)
├── ja-[hash].js ← 不需要(日语)
├── ko-[hash].js ← 不需要(韩语)
├── pt-[hash].js ← 不需要(葡萄牙语)
├── ru-[hash].js ← 不需要(俄语)
└── ... 更多 ← 不需要
text
两种优化思路
思路二:动态构建匹配字符串(不可行)
尝试使用 availableLocales 动态生成 glob 匹配字符串:
// 不可行!PWA 插件不支持动态的 import 输入
const elementPlusLocalesMap = Object.fromEntries(
Object.entries(
import.meta.glob(
availableLocales.map(l => `element-plus/dist/locale/${l.toLowerCase()}.mjs`),
{ eager: true }
)
).map(...)
)
typescript
也尝试过手动构建对象:
// 同样不可行,打包时仍会包含所有文件
const elementPlusLocalesMap: Record<string, any> = {}
for (const locale of availableLocales) {
elementPlusLocalesMap[locale] = import(
`element-plus/dist/locale/${locale.toLowerCase()}.mjs`
)
}
typescript
结论:动态字符串无法被 Vite 在构建时静态分析,rollup 的 PWA 插件也无法识别动态的 input 资源。
思路一:构建层面 External(可行)
通过 Rollup 的 external 配置,在构建时过滤掉不需要的 Element Plus 语言文件。
实现方案:Rollup External 配置
完整配置代码
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import VueI18nPlugin from '@intlify/unplugin-vue-i18n'
import { resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
import fs from 'node:fs'
import path from 'node:path'
// 读取 locales 目录下已有的文件名
const localesDir = path.resolve(dirname(fileURLToPath(import.meta.url)), 'locales')
const localesFiles = fs.readdirSync(localesDir)
.map((file: string) => {
const match = file.match(/([^/]+)\.(?:json|js|ts)$/)
return match ? match[1] : ''
})
.filter(Boolean)
/**
* 过滤 Element Plus 的 .mjs 语言文件
* 不打包不需要的 locales
*/
function externalElementPlusLocales(id: string): boolean {
// 只处理 element-plus 的 locale 文件
if (!id.includes('element-plus/dist/locale')) {
return false
}
// 获取文件名(不含路径)
const baseName = path.basename(id)
// 判断该文件是否在我们需要的语言列表中
const isNeeded = localesFiles.some(
(locale: string) => locale.toLowerCase() === baseName.replace('.mjs', '')
)
// 不需要的文件 → return false(不 external,即不打包)
// 需要的文件 → return false(也不 external,正常打包)
// 但我们的逻辑是:
// return !isNeeded → 不需要的返回 true(external 掉),需要的返回 false(保留)
return !isNeeded
}
export default defineConfig({
plugins: [
vue(),
VueI18nPlugin({
include: resolve(dirname(fileURLToPath(import.meta.url)), './locales/**'),
}),
],
build: {
rollupOptions: {
external: (id: string) => {
return externalElementPlusLocales(id)
},
},
},
})
typescript
逻辑解读
Rollup external 函数的返回值:
return true → 该文件被 external(不打包到 bundle 中)
return false → 该文件正常处理(打包到 bundle 中)
externalElementPlusLocales 逻辑:
1. 不是 element-plus/dist/locale 的文件 → false(不处理)
2. 是 element-plus/dist/locale 的文件 → 检查文件名
- 文件名在 localesFiles 中 → false(保留,正常打包)
- 文件名不在 localesFiles 中 → true(external 掉,不打包)
text
为什么从 locales 目录读取文件列表
因为 locales 目录下的文件名就是我们项目需要的语言标识(zh-CN、en),以此作为过滤条件最准确:
locales/
├── zh-CN.json → 需要保留 zh-CN 的 Element Plus 语言包
└── en.json → 需要保留 en 的 Element Plus 语言包
element-plus/dist/locale/
├── zh-cn.mjs → 保留(匹配 zh-CN → zh-cn)
├── en.mjs → 保留(匹配 en)
├── ar.mjs → external 掉
├── de.mjs → external 掉
└── ... → external 掉
text
验证优化效果
构建命令
# 完整构建
pnpm build
# 仅构建(跳过类型检查,更快)
pnpm build --mode production
bash
优化前
dist/assets/
├── zh-CN-[hash].js
├── en-[hash].js
├── ar-[hash].js ← 多余
├── de-[hash].js ← 多余
├── es-[hash].js ← 多余
├── fr-[hash].js ← 多余
├── ja-[hash].js ← 多余
├── ko-[hash].js ← 多余
├── pt-[hash].js ← 多余
├── ru-[hash].js ← 多余
└── ... 20+ 文件
text
优化后
dist/assets/
├── zh-CN-[hash].js ← 保留
├── en-[hash].js ← 保留
└── [其他项目资源]
text
构建产物体积显著减少,只包含项目实际使用的语言包。
大小写匹配注意
Element Plus 的语言文件使用小写(zh-cn.mjs),而我们项目使用驼峰(zh-CN.json):
// 统一转为小写比较
const isNeeded = localesFiles.some(
(locale: string) => locale.toLowerCase() === baseName.replace('.mjs', '')
)
typescript
其他构建优化建议
1. 分包策略
// vite.config.ts
build: {
rollupOptions: {
output: {
manualChunks: {
'element-plus': ['element-plus'],
'vue-i18n': ['vue-i18n'],
},
},
},
}
typescript
2. JSON 文件的 Tree-shaking
JSON 格式的翻译文件天然支持 Tree-shaking,未使用的翻译键值在构建时会被移除。这也是推荐使用 .json 格式的原因之一。
3. 懒加载语言包
已在前面的 loadLocaleMessages 中实现:首次切换语言时加载,后续使用缓存。
// 已加载的语言不会重新请求
const loadedLanguages: string[] = []
export async function loadLocaleMessages(locale: string) {
if (loadedLanguages.includes(locale)) {
setI18nLanguage(locale)
return // 直接返回,不重新加载
}
// ... 加载逻辑
}
typescript
总结
import.meta.glob匹配node_modules时会导入所有匹配文件,需在构建层面过滤- 动态构建 glob 匹配字符串不可行(PWA 插件不支持)
- 通过 Rollup 的
external配置,根据locales目录文件名过滤不需要的 Element Plus 语言包 external函数返回true表示不打包,false表示正常处理- 注意大小写匹配:Element Plus 用小写(
zh-cn),项目用驼峰(zh-CN) - 优化后构建产物只包含实际使用的语言包,体积显著减少
↑