React Aria 多语言构建瘦身实战:@react-aria/optimize-locales-plugin 从原理到配置
【免费下载链接】react-spectrumA collection of libraries and tools that help you build adaptive, accessible, and robust user experiences.项目地址: https://gitcode.com/GitHub_Trending/re/react-spectrum
本文围绕 React Spectrum 仓库中的国际化构建优化插件 optimize-locales-plugin 展开。React Aria / React Spectrum 默认会将 20 个左右的语言包(en-US、fr-FR、zh-CN……)全部打进产物,而该插件通过拦截模块解析,只保留你应用实际支持的 locale,从而显著缩小 bundle 体积。读完后你将理解它的拦截原理、locale 匹配规则,并掌握 webpack / Next.js / Vite / Rollup / esbuild 五种构建工具下的完整配置方式。
插件解决的问题:语言包体积膨胀
React Spectrum 各包在构建产物中以内嵌intl/目录的方式携带翻译字符串。以 packages/@adobe/react-spectrum/intl/inlinealert/index.js 为例,该文件一次性import了 20 个 locale 的 JSON 文件并合并为按语言代码索引的对象:
import csCZ from './cs-CZ.json'; import daDK from './da-DK.json'; // ... import zhTW from './zh-TW.json'; export default { 'cs-CZ': csCZ, // ... 'zh-TW': zhTW };这意味着即使应用只服务中文用户,产物里依然带着捷克语、丹麦语等全部语言字符串。@react-aria/optimize-locales-plugin的目标就是:把配置中未列出的 locale 对应的字符串模块从 bundle 中剔除。
插件基于 unplugin),因此一套核心逻辑即可同时覆盖 Vite、Rollup、Webpack 与 esbuild。Parcel 不在 unplugin 支持范围内,官方为它提供了专门的替代方案@react-aria/parcel-resolver-optimize-locales(见下文 Parcel 用户)。
核心实现:在模块解析阶段"劫持"语言包导入
插件的全部逻辑只有 LocalesPlugin.js 一个文件,其本质是向宿主构建工具注册一个resolveId钩子,在构建器解析导入路径时做拦截。关键源码如下:
// packages/dev/optimize-locales-plugin/LocalesPlugin.js const localeSpecifierRegex = /[a-z]{2}-[A-Z]{2}/; const sourcePathRegex = //\\[/\\]/; module.exports = createUnplugin(({locales}) => { locales = locales.map(l => new Intl.Locale(l)); return { name: 'locales-plugin', vite: { enforce: 'pre' }, resolveId: { filter: { id: localeSpecifierRegex }, handler(specifier, sourcePath, options) { if (!sourcePathRegex.test(sourcePath) || options?.ssr) { return; } let match = specifier.match(localeSpecifierRegex); if (match) { let locale = new Intl.Locale(match[0]); if (!locales.some(l => localeMatches(locale, l))) { return path.join(__dirname, 'empty.js'); } } return null; } } }; });从源码结构看,插件的工作流程分为四步:
- 正则过滤(filter):
resolveId注册时附带filter.id = /[a-z]{2}-[A-Z]{2}/。只有导入路径中包含"小写两位-大写两位"形态 locale 标识(如./fr-FR.json)的模块才会进入 handler,其他导入(如./helpers)完全不受影响,几乎零性能开销。 - 导入方白名单(sourcePathRegex):只有当发起导入的文件位于
@react-stately/*、@react-aria/*、@react-spectrum/*、@adobe/react-spectrum、react-stately、react-aria或react-aria-components这些包路径下时,才会执行裁剪逻辑(正则同时兼容/与\,支持 Windows 路径)。这保证了插件只作用于 React Spectrum / React Aria 生态内部的语言包导入,你项目里其他恰好形似 locale 的文件不会被误伤。 - locale 匹配判定:handler 用正则从 specifier 中提取出 locale 标识,转成
Intl.Locale对象后与用户配置的 locales 逐一比较;若都不匹配,则把该模块重定向到包内的 empty.js——其内容仅有一行export default undefined;。这样被剔除语言包的模块仍然可被正常解析和 tree-shaking,产物中对应语言字符串彻底消失。 - SSR 排除:
options?.ssr为真时直接return,不做任何重定向。也就是说服务端构建保留全部语言包,裁剪只发生在客户端 bundle 中——这是合理的,因为 SSR 场景下语言切换/回退逻辑仍可能引用完整语言表。
另外注意vite: { enforce: 'pre' }这一配置:插件被标记为"预置"执行,确保它的resolveId跑在大多数插件之前,语言包重定向不会被其他解析逻辑抢先处理掉。
locale 匹配规则:裸语言代码可覆盖所有地区变体
配置中的 locale 字符串先被转成Intl.Locale实例(locales.map(l => new Intl.Locale(l))),随后由文件末尾的localeMatches函数做匹配判定:
function localeMatches(localeToMatch, includedLocale) { return ( localeToMatch.language === includedLocale.language && (!includedLocale.region || localeToMatch.region === includedLocale.region) ); }该函数表达了两条规则:
- 语言代码必须相等:
fr-FR的导入只有在配置了fr系 locale 时才会被保留。 - 配置中的 region 是"可选门槛":若配置的 locale 不含 region(如裸写
fr),则所有fr-*地区变体(fr-CA、fr-BE……)全部保留;若配置了具体 region(如fr-FR),则只保留精确匹配。
测试文件 对这套规则做了完整验证,值得重点关注的几个用例:
| 测试用例 | 断言结果 | 验证的规则 |
|---|---|---|
en-US配置下导入./fr-FR.json(来自@react-aria/button等 7 个生态包) | 全部重定向到empty.js | 未列出的 locale 被剔除,且各包均在白名单内 |
导入方是some-other-pkg时导入./fr-FR.json | 返回undefined(不处理) | 白名单外模块不受影响 |
配置['en-US', 'fr-FR']时导入./fr-FR.json | 返回null(正常解析) | 列出的 locale 原样保留 |
配置['en-US', 'fr']时导入./fr-CA.json | 返回null(正常解析) | 裸语言代码保留所有地区变体 |
传入{ssr: true} | 返回undefined(不处理) | SSR 构建跳过裁剪 |
Windows 风格路径C:\repo\node_modules\@adobe\react-spectrum\... | 重定向到empty.js | 反斜杠路径同样可识别 |
测试中还验证了 filter 本身的过滤行为:./fr-FR.json能命中filter.id,而./helpers不会——与源码中的正则过滤一致。
构建工具配置
locales是唯一配置项(类型定义为readonly string[],见 LocalesPlugin.d.ts 中的UnpluginInstance<Options, false>)。配置中未列出的 locale 字符串会从 bundle 中移除。下面是原文档给出的各构建工具配置,均可直接复制使用。
webpack
// webpack.config.js const optimizeLocales = require('@react-aria/optimize-locales-plugin'); module.exports = { // ... plugins: [ optimizeLocales.webpack({ locales: ['en-US', 'fr-FR'] }) ] };Next.js
Next.js 底层即 webpack,通过next.config.js的webpack钩子注入插件即可:
// next.config.js const optimizeLocales = require('@react-aria/optimize-locales-plugin'); module.exports = { webpack(config) { config.plugins.push( optimizeLocales.webpack({ locales: ['en-US', 'fr-FR'] }) ); return config; } };Vite
// vite.config.js import optimizeLocales from '@react-aria/optimize-locales-plugin'; export default { plugins: [ optimizeLocales.vite({ locales: ['en-US', 'fr-FR'] }) ] };如前所述,Vite 场景下插件以enforce: 'pre'预置模式生效,可放心放在plugins数组中,无需刻意调整顺序。
Rollup
// rollup.config.js import optimizeLocales from '@react-aria/optimize-locales-plugin'; export default { plugins: [ optimizeLocales.rollup({ locales: ['en-US', 'fr-FR'] }) ] };esbuild
import {build} from 'esbuild'; import optimizeLocales from '@react-aria/optimize-locales-plugin'; build({ plugins: [ optimizeLocales.esbuild({ locales: ['en-US', 'fr-FR'] }) ] });说明:原文档此处以 esbuild 原生 API 演示参数形态。由于 unplugin 的 esbuild 支持通常由 unplugin 生态中的
esbuild-plugin适配层完成,实际项目若报错可检查 esbuild 插件挂载方式是否正确。
Parcel 用户请改用 @react-aria/parcel-resolver-optimize-locales
unplugin 不直接覆盖 Parcel。README 明确提示:Parcel 用户应使用@react-aria/parcel-resolver-optimize-locales。该 resolver 就位于本仓库 packages/dev/parcel-resolver-optimize-locales,同样服务于"只保留应用支持的语言包"这一目标。如果你的项目基于 Parcel(仓库内多个examples/示例如examples/s2-parcel-example即采用 Parcel),请查阅对应包的文档获取配置方式,而不是尝试在这里挂载 unplugin 实例。
使用前提与注意事项
结合源码与测试,使用该插件时需要注意以下边界:
- locale 书写格式:匹配依赖
/[a-z]{2}-[A-Z]{2}/形态的标识,导入路径中的 locale 必须是标准xx-XX形态(语言小写、地区大写)才能被识别和裁剪;配置项建议同样使用Intl.Locale可解析的标准格式。 - 作用范围仅限 React Spectrum / React Aria 生态包:
sourcePathRegex白名单意味着插件只裁剪上述 7 类包路径下的导入,第三方库自带语言包不在处理范围内。 - SSR 构建不受影响:
ssr选项为真时插件静默退出,服务端产物保留全部语言包;体积优化只体现在客户端 bundle 上。 - 被剔除的模块以空模块兜底:重定向目标 empty.js 导出
undefined,因此若运行时代码在语言回退链中显式访问某个被剔除 locale 的翻译,取到的将是undefined而非抛错。生产环境请确保locales配置覆盖了用户可能实际使用的语言。 - 插件不改变运行时行为,只改变构建产物:它不修改任何源码,也不影响开发调试(dev 下同样生效,但 SSR 除外),可以放心长期挂入构建链。
小结
@react-aria/optimize-locales-plugin用不到 60 行代码解决了一个多语言项目的通用痛点:通过 unplugin 的resolveId钩子,在模块解析阶段把未列出的 locale JSON 重定向为空模块,实现"按应用支持的语言裁剪 bundle"。其设计上有几个值得借鉴的细节——正则预过滤降低钩子执行成本、导入方白名单避免误伤、裸语言代码兼容所有地区变体、SSR 构建豁免、Windows 路径兼容。对于同时使用 React Spectrum / React Aria 且服务多语言的团队,这是一个低成本、低风险、且已被仓库内单元测试充分验证的构建瘦身手段。
【免费下载链接】react-spectrumA collection of libraries and tools that help you build adaptive, accessible, and robust user experiences.项目地址: https://gitcode.com/GitHub_Trending/re/react-spectrum
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考