普通视图

发现新文章,点击刷新页面。
昨天 — 2025年8月17日首页

👋 一起写一个基于虚拟模块的密钥管理 Rollup 插件吧(一)

作者 xiaohe0601
2025年8月17日 17:26

在现代 Web 应用开发中,密钥的使用几乎是不可避免的,无论是加解密本地敏感数据、调用第三方 SDK 还是网络请求签名等场景都需要用到密钥。

如何相对安全、灵活地管理密钥一直是一个令人头疼的问题,我们既希望在开发环境可以方便地修改、调试和注入密钥,又不希望这些密钥在构建产物中被明文暴露,以免被有心之人轻松获取。

通常情况,我们会先手动将密钥通过特定的算法混淆拆分成多份放入源码中,运行时再通过逆运算将这些片段合并还原得到密钥原文,这样在构建产物中密钥就不会以明文的形式暴露。

例如下面这样:

// 假设 chunk1、chunk2、chunk3 是密钥混淆拆分后的片段
const chunk1 = "abc";
const chunk2 = "123";
const chunk3 = "!@#";

// 运行时通过逆运算合并还原得到密钥原文
const key = combine(chunk1, chunk2, chunk3);

console.log(key); // iamxiaohe

但是如果需要添加或者修改密钥,我们就得针对新的密钥再重复手动混淆拆分这个操作。众所周知,手动操作既低效又容易出错,那么我们能不能编写一个插件来自动完成这个过程呢?

插件设计

在开始设计之前,我们先整理一下需求,思考这个插件需要帮我们完成什么工作,简单梳理如下:

  1. 能够直接使用明文配置密钥
  2. 针对配置的密钥能够自动混淆拆分
  3. 运行时自动合并还原密钥
  4. 密钥支持简单的导入使用

API 设计

需求整理完成,然后需要再设想一下期望的插件使用方式,这有利于技术选型以及后续的插件实现工作。

我们希望这个插件的使用尽可能贴合开发者的直觉,最理想的使用方式是这样的:只需要在构建工具的配置中简单引入插件,传入一份密钥配置表,便可以在业务代码中通过特定的模块路径导入密钥,而无需关心密钥的具体构建逻辑与混淆拆分细节。

Vite 是一个超快的前端构建工具,现在大多数项目都使用 Vite 构建,所以我们的插件也考虑为 Vite 提供优先支持。通过查阅 Vite 的 插件 API 文档,可以知道 Vite 内部由 Rollup 驱动,如果插件不涉及 Vite 独有的 hook(例如开发服务器相关),那么就可以编写一个 Rollup 插件同时支持 Vite 和 Rollup 使用。

至此,我们可以初步构思出插件的 API 设计如下(以 Vite 为例):

// vite.config.(js|ts)

import CryptoKey from "rollup-plugin-crypto-key";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [
    CryptoKey({
      keys: {
        DEMO_KEY1: "iamxiaohe",
        DEMO_KEY2: "ilovexiaohe"
      }
    })
  ]
});
import { DEMO_KEY1, DEMO_KEY2 } from "crypto-key";

console.log(DEMO_KEY1); // iamxiaohe
console.log(DEMO_KEY2); // ilovexiaohe

虚拟模块

我们设想从 crypto-key 中导入插件配置的密钥,但是这个 crypto-key 并不是已安装的模块或者某个真实的文件,而是通过插件动态生成对应的代码并从内存中加载使用,通过查阅文档发现 虚拟模块 可以满足这个需求。

虚拟模块是一种很实用的模式,使你可以对使用 ESM 语法的源文件传入一些编译时信息。

export function myPlugin() {
  const virtualModuleId = "virtual:my-module";
  const resolvedVirtualModuleId = "\0" + virtualModuleId;

  return {
    name: "my-plugin",
    resolveId(id) {
      if (id === virtualModuleId) {
        return resolvedVirtualModuleId;
      }
    },
    load(id) {
      if (id === resolvedVirtualModuleId) {
        return `export const msg = "from virtual module";`;
      }
    }
  };
}

这使得可以在 JavaScript 中引入这些模块:

import { msg } from "virtual:my-module";

console.log(msg);

虚拟模块在 Vite(以及 Rollup)中都以 virtual: 为前缀,作为面向用户路径的一种约定。

如果可能的话,插件名应该被用作命名空间,以避免与生态系统中的其他插件发生冲突。举个例子,vite-plugin-posts 可以要求用户导入一个 virtual:posts 或者 virtual:posts/helpers 虚拟模块来获得编译时信息。

在内部,使用了虚拟模块的插件在解析时应该将模块 ID 加上前缀 \0,这一约定来自 Rollup 生态。这避免了其他插件尝试处理这个 ID(比如 node 解析),而例如 sourcemap 这些核心功能可以利用这一信息来区别虚拟模块和正常文件。\0 在导入 URL 中不是一个被允许的字符,因此我们需要在导入分析时替换掉它们。一个虚拟 ID 为 \0{id} 在浏览器中开发时,最终会被编码为 /@id/__x00__{id}。这个 id 会被解码回进入插件处理管线前的样子,因此这对插件钩子的代码是不可见的。

所以,根据约定我们也使用 virtual: 作为导入前缀,修改如下:

import { DEMO_KEY1, DEMO_KEY2 } from "virtual:crypto-key";

模块划分

完成了插件的 API 设计,我们还需要思考插件的模块如何划分。

现在,回想之前梳理出的插件需求,如果直接把所有逻辑都装进一个插件模块里,很快会发现一些问题:

  • 逻辑耦合严重:密钥的混淆拆分与还原算法和构建工具相关的插件逻辑混杂在一起,不利于测试和维护。
  • 可复用性差:如果将来希望适配其他构建工具而不局限于 Rollup / Vite,则无法直接复用。

既然如此,不妨把核心算法与构建工具适配的逻辑分开,让它们互相独立、各司其职,但又能通过清晰的接口相互协作。

所以我们可以将插件拆分为以下两个模块:

  • crypto-splitter

    这是插件的核心,负责密钥的混淆拆分与还原,它不关心上层的构建工具。

  • rollup-plugin-crypto-key

    作为插件的桥梁,它负责在 Rollup 中调用 crypto-splitter 的能力实现虚拟模块相关的逻辑。

插件设计其实是插件开发全流程中最困难的部分,我们已经顺利完成,可以说是轻舟已过万重山。

插件实现

有了清晰的设计思路,现在就可以进入到大家最擅长的编码环节啦!

crypto-splitter

首先需要选择一种密钥拆分还原算法,这是插件最核心的部分,也是最复杂的部分,所以我们 一定要全栈自研、从心出发 让 AI 帮忙写一个。

那么目前主流的密钥拆分还原算法有哪些呢?AI 回答如下:

Shamir 密钥共享(Shamir's Secret Sharing, SSS)

  • 基于多项式插值(Lagrange 插值)原理。
  • 将密钥 KK 作为一个域上的常数项,构造一个随机多项式 f(x)=K+a1x+a2x2++at1xt1f(x) = K + a_1 x + a_2 x^2 + \dots + a_{t-1} x^{t-1}
  • 每个参与者得到多项式在不同 xx 点的值作为份额。
  • 只要 tt 个份额,就可以通过 Lagrange 插值还原原始多项式,从而得到密钥 KK

Blakley 密钥共享

  • 基于几何原理:在 tt 维空间中,每个份额对应一个超平面。
  • 原始密钥对应空间中的一个点。
  • 至少 tt 个超平面交点才能唯一确定该点。

Asmuth-Bloom 密钥共享

  • 基于中国剩余定理(CRT)。
  • 选择一组互质整数 m1<m2<...<mnm_1 < m_2 < ... < m_n
  • 密钥通过模运算生成份额,每个份额 si=Kmodmis_i = K \bmod m_i
  • 至少 tt 个份额即可用 CRT 还原 KK

XOR 分割

  • 将密钥按比特或字节拆分成若干随机序列。
  • 最后一个份额通过 XOR 得到,使所有份额 XOR 后等于原密钥。
  • 例如:K=S1S2SnK = S_1 \oplus S_2 \oplus \dots \oplus S_n

简单起见,我们选择最容易的 XOR 分割算法来实现我们的插件核心。

既然是密钥拆分和还原,那么接下来就编写对应的两个方法,split 用于混淆拆分密钥,combine 用于合并还原密钥。

/**
 * 拆分配置项
 */
export interface SplitOptions {
  /** 拆分片段数量,默认为 4 */
  segments?: number;
}

/**
 * 将输入的密钥按指定片段数量进行拆分,生成可重组的随机化片段数组。
 *
 * @param key 密钥原文
 * @param options 配置项
 * @returns 随机化片段数组
 */
export function split(key: string, options: SplitOptions = {}): string[] {
  const {
    segments = 4
  } = options;

  // 如果 key 为空,则直接返回空数组
  if (key.length <= 0) {
    return [];
  }

  const chunks: string[] = [];

  // 生成前 segments - 1 个随机化片段
  for (let i = 0; i < segments - 1; i += 1) {
    chunks.push(
      [...key]
        .map((char) => {
          return String.fromCharCode(
            char.charCodeAt(0) ^ Math.floor(Math.random() * 256)
          );
        })
        .join("")
    );
  }

  // 生成最后一个片段,保证能通过逆运算还原出密钥原文
  chunks.push(
    [...key]
      .map((char, index) => {
        return String.fromCharCode(
          char.charCodeAt(0) ^ chunks.reduce((acc, it) => {
            return acc ^ it.charCodeAt(index);
          }, 0)
        );
      })
      .join("")
  );

  return chunks;
}

split 方法将输入的密钥 key 按照指定 segments 数量拆分成若干加密片段。前 segments - 1 个片段通过随机数异或生成,第 segments 个片段通过异或前面的所有片段与 key 生成,这样可以保证用所有片段才能还原出原始的 key

/**
 * 将拆分后的随机化片段数组重新合并还原成原始密钥。
 *
 * @param chunks 随机化片段数组
 * @returns 原始密钥
 */
export function combine(chunks: string[]): string {
  // 如果没有片段,则直接返回空字符串
  if (chunks.length <= 0) {
    return "";
  }

  return [...chunks[0]]
    .map((_, index) => {
      return String.fromCharCode(
        // 按位异或所有片段对应字符
        chunks.reduce((acc, it) => {
          return acc ^ it.charCodeAt(index);
        }, 0)
      );
    })
    .join("");
}

combine 方法接收由 split 方法生成的随机化片段数组,通过逐字符按位异或的方式恢复原始字符串。它要求必须传入完整的片段数组,否则无法保证恢复结果的正确性。

至此,我们完成了 crypto-splitter 模块的开发。

rollup-plugin-crypto-key

现在我们开始编写 Rollup 插件,将 crypto-splitter 的能力接入到 Rollup 中。

import type { Plugin } from "rollup";
import { getCode } from "./code";

export interface Options {
  /**
   * 密钥映射表,例如 { KEY1: "xxxx", KEY2: "yyyy" }
   */
  keys?: Record<string, string>;
}

// 虚拟模块标识符,供用户在代码中导入使用
const VIRTUAL_MODULE_ID = "virtual:crypto-key";
// 内部使用的虚拟模块标识符(带 \0 前缀,避免其他插件尝试处理这个 ID)
const RESOLVED_VIRTUAL_MODULE_ID = `\0${VIRTUAL_MODULE_ID}`;

export default function cryptoKey(options: Options = {}): Plugin {
  const {
    keys = {}
  } = options;

  return {
    name: "crypto-key",
    resolveId(source) {
      // 只关心 virtual:crypto-key,其他模块不处理
      if (source !== VIRTUAL_MODULE_ID) {
        return null;
      }

      // 返回内部标识符用于 load 阶段判断使用
      return RESOLVED_VIRTUAL_MODULE_ID;
    },
    load(id) {
      // 过滤其他模块
      if (id !== RESOLVED_VIRTUAL_MODULE_ID) {
        return null;
      }

      // 返回密钥处理代码以供运行时调用
      return getCode(keys);
    }
  };
}

接下来,我们需要实现 getCode 方法返回密钥处理代码。在开始编码之前,先要知道期望的结果是什么,稍加思索后可以得出如下代码:

import { combine } from "crypto-splitter";

const $1 = ["111", "aaa", "!@#"];
const $2 = ["$%^", "bbb", "222"];

export const KEY1 = combine($1);
export const KEY2 = combine($2);

其中 $1$2 是通过 crypto-splittersplit 方法拆分后的随机化片段数组,然后通过 combine 方法运行时还原为密钥原文并导出 KEY1KEY2

那么 getCode 方法需要做的事情就是遍历 keys 调用 split 方法拆分密钥,然后根据上述模板生成并返回 JavaScript 代码。

// code.ts

import { split } from "crypto-splitter";

export function getCode(keys: Record<string, string>): string {
  const values = Object.entries(keys)
    .map(([key, value]) => {
      return {
        key,
        chunks: split(value)
      };
    });

  return `import { combine } from "crypto-splitter";

${
  values
    .map((item, index) => {
      return `const $${index + 1} = ${JSON.stringify(item.chunks)};`;
    })
    .join("\n")
}

${
  values
    .map((item, index) => {
      return `export const ${item.key} = combine($${index + 1});`;
    })
    .join("\n")
}`;
}

到这里,我们完成了插件的核心实现:用一个独立的 crypto-splitter 模块实现了简单可复用的拆分/合并算法,再通过 rollup-plugin-crypto-key 模块把这套机制以虚拟模块的形式接入到 Rollup / Vite 中,最终用户只需在配置中添加密钥即可在代码中像导入普通模块一样使用它们。

插件使用

既然完成了插件的实现,最后当然是要体验使用一下自己编写的插件啦!

// vite.config.(js|ts)

import CryptoKey from "rollup-plugin-crypto-key";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [
    CryptoKey({
      keys: {
        DEMO_KEY1: "iamxiaohe",
        DEMO_KEY2: "ilovexiaohe"
      }
    })
  ]
});
import { DEMO_KEY1, DEMO_KEY2 } from "virtual:crypto-key";

console.log(DEMO_KEY1); // iamxiaohe
console.log(DEMO_KEY2); // ilovexiaohe

还是以 Vite 为例,我们可以看到插件的使用非常直观:只需在 Vite 配置中传入明文密钥,业务代码中即可像导入普通模块一样获取密钥值,而不必关心拆分、混淆或运行时还原的具体实现。这样既保证了开发时的便捷性,又避免了在构建产物中明文暴露密钥,极大地降低了开发与安全管理的复杂度。同时,这也验证了虚拟模块的设计思路:插件自动生成模块内容,用户只需关注导入和使用,而不需要额外手动操作密钥。

🚨 注意

由于浏览器环境的特殊性,任何客户端的保护措施都是 “防君子不防小人”,只能增加破解难度,并不能保证绝对的安全!如果需要提高安全性,应该与其他防护措施相结合。

源码

插件的完整代码可以在 virtual-crypto-key 仓库中查看。赠人玫瑰,手留余香,如果对你有帮助可以给我一个 ⭐️ 鼓励,这将是我继续前进的动力,谢谢大家 🙏!

下一步

现在插件已经可以顺利使用并且符合预期效果,我们达成了第一个里程碑!

但是细心的同学会发现,我们的插件在 TypeScript 中使用时会报如下错误信息:

Cannot find module virtual:crypto-key or its corresponding type declarations.

这是因为我们没有为 virtual:crypto-key 提供类型定义,所以 TypeScript 的编译器并不认识这个模块。这将会导致类型检查不通过,并且 IDE 也不知道应该如何提示代码,让用户的开发体验大大降低。

作为一个现代的插件,我们当然要将用户的开发体验放在第一位,所以下一章我们将会一起实现对 TypeScript 的支持!

昨天以前首页

从零搭建 Vite 7 + React 19 + Tailwind CSS v4 企业级应用

作者 乐潇游
2025年8月11日 15:05

本文记录从零搭建 Vite + React + Tailwind CSS 项目,省去一个个官网去查阅文档,旨在方便快速丝滑的创建项目。

项目中涉及到 pages 页面自动生成路由svg 自动转组件等插件,以及 Vite 企业级应用常见配置,可按需选择添加。

初始化项目

这里选择 pnpm 管理项目:

$ pnpm create vite

执行命令后按照提示操作即可。如需支持 TS 还是非 TS,又或者 TS + SWC 等等,根据自己的需求选择。

基于文件的路由(Pages 目录自动生成路由)

很多框架或脚手架(比如 next.jsnuxt.jsumi.js等)都是约定一个目录自动生成路由,如果不想用那些框架的情况下,又希望根据 pages 目录自动生成路由,可以安装这个插件:

pnpm i -D vite-plugin-pages

安装完成后进行如下配置:

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
import Pages from 'vite-plugin-pages';

// <https://vite.dev/config/>
export default defineConfig({
  plugins: [
    react(),
    Pages(),
  ],
})

配置路径别名

在引入一些文件的时候,你肯定不希望像这样:

// 你肯定不希望像这样
import utils from '../../../../../utils';

// 一般希望这样
import utils from '@/utils';

如果想要支持类似 @/ 的路径引用别名,增加以下配置即可:

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'

// <https://vite.dev/config/>
export default defineConfig({
  plugins: [
    react(),
  ],
  resolve: {
  // 路径别名(简化导入)
    alias: {
      '@': path.resolve(__dirname, './src')
    },
    // 自动补全扩展名
    extensions: ['.js''.ts''.jsx''.tsx']
  },
})

SVG 图标文件自动转为组件

如果希望在导入 svg 图标的时候,自动转为 React 组件,虽然默认支持,但不想每次都 as 一下,比如这样:

import { ReactComponent as IconUser } from '@/assets/user.svg';

可以增加以下配置:

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
import svgr from 'vite-plugin-svgr';

// <https://vite.dev/config/>
export default defineConfig({
  plugins: [
    react(),
    svgr({
      include: "**/*.svg",
      svgrOptions: {
        plugins: [
        // 一般两者一起使用
        "@svgr/plugin-svgo", // 启用 SVGO 压缩优化
        "@svgr/plugin-jsx" // JSX 转换
       ],
        svgoConfig: {
          floatPrecision: 2, // 设置压缩精度
        },
      },
    }),
  ],
})

这样在使用的时候就可以直接导入了,如下:

import IconUser from '@/assets/user.svg';

增加支持 tailwindcss

安装依赖包

首先安装 Tailwind CSS 及其相关依赖:

pnpm add -D tailwindcss @tailwindcss/vite

配置 Vite 插件

@tailwindcss/vite 加入 vite.config.ts,如下:

import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
  plugins: [
    tailwindcss(),
  ],
})

导入 Tailwind CSS

在 CSS 主入口文件(如 global.css / index.css) 导入 tailwindcss:

@import "tailwindcss";

开始使用

配置好之后就可以在项目中使用了,如:

<div className="flex items-center justify-center">
Hello World!
</div>

关于 Tailwind CSS v4

本项目使用的 Tailwind CSS v4 版本,默认使用 Lightning CSS 引擎,可直接解析 CSS 文件,无需通过 PostCSS 处理。这意味着

  • 无需配置 PostCSS 插件:如 autoprefixer或 postcss-import

  • 配置移至 CSS 文件:主题、变量等直接在 CSS 中通过 @theme指令定义,例如:

    @theme {
      --color-primary: oklch(0.84 0.18 117.33);
      --color-mint-500: oklch(0.72 0.11 178);
      --font-sans: "Inter", sans-serif;
    }
    
    <div class="bg-mint-500">
      <!-- ... -->
    </div>
    

    也可以:

    <div style="background-color: var(--color-mint-500)">
      <!-- ... -->
    </div>
    
  • 零配置内容检测:自动扫描项目文件,无需在 tailwind.config.js中手动配置 content路径。

Vite 企业级应用配置

常见基础配置

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
import tailwindcss from '@tailwindcss/vite'
import Pages from 'vite-plugin-pages';
import svgr from 'vite-plugin-svgr';
import path from 'path';

// <https://vite.dev/config/>
export default defineConfig({
  // base: '/app/', // 部署基础路径(子目录部署时需要)
  plugins: [
    react(),
    Pages(),
    tailwindcss(),
    svgr({
      include: "**/*.svg",
      svgrOptions: {
        plugins: ["@svgr/plugin-svgo", "@svgr/plugin-jsx"], // 启用 SVGO 压缩和 JSX 转换(两者需一起使用)
        svgoConfig: {
          floatPrecision: 2, // 设置压缩精度
        },
      },
    }),
  ],
  resolve: {
    // 配置路径别名(简化导入)
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
    // 自动补全扩展名
    extensions: ['.js', '.ts', '.jsx', '.tsx'],
  },
  // 开发服务器配置
  server: {
    port: 3000, // 端口号
    open: true, // 自动打开浏览器
    proxy: {
      '/api': {
        target: '<http://localhost:8080>', // 后端地址
        changeOrigin: true, // 修改请求源
        rewrite: (path) => path.replace(/^\/api/, ''), // 路径重写
        // secure: false  // 如果出现代理配置无效可将其打开,尝试关闭HTTPS验证
      },
    },
  },
  build: {
    outDir: 'dist', // 打包输出目录
    sourcemap: true, // 生成 sourcemap 便于调试
  },
})

字段说明:

  • base - 部署基础路径,当项目部署在非根路径时使用(如GitHub Pages);
  • resolve.alias - 让import语句更简洁;
  • server.proxy:解决开发环境 API 请求跨域问题。

性能优化配置

import { defineConfig } from 'vite'

// <https://vite.dev/config/>
export default defineConfig({
  // 生产环境专属配置
  build: {
    assetsDir: 'static', // 静态资源存放目录
    minify: 'terser', // 使用terser进行代码压缩
    chunkSizeWarningLimit: 1500, // 分块大小警告阈值(KB)
    rollupOptions: {
      output: { // 代码分割策略
        manualChunks: {
          react: ['react', 'react-dom'], // 单独打包 React 相关库
          utils: ['lodash', 'axios'] // 工具库单独打包
        }
      }
    }
  }
})

环境变量管理

  1. 创建环境文件:
.env.development  # 开发环境
.env.production   # 生产环境
  1. 变量定义规则:
# 必须以VITE_开头才能被客户端访问
VITE_API_URL=https://api.example.com
VITE_DEBUG=true

安全建议:

敏感变量应放在 .env.local(已默认在 .gitignore

区分环境按需加载配置

针对开发或生产环境各自单独加载配置:

import { defineConfig } from 'vite'

// <https://vite.dev/config/>
export default defineConfig(({ command, mode }) => {
  if (command === 'serve') {
    return {
      /* 开发配置 */
      server: {},
    }
  } else {
    return {
      // 生产环境专属配置
      build: {}
    }
  }
})

多页面应用支持

需要支持多页面入口的时候可以参考以下配置:

import { defineConfig } from 'vite';
import path from 'path';

// <https://vite.dev/config/>
export default defineConfig({
  build: {
    rollupOptions: {
      // 多页面入口配置
      input: {
        main: path.resolve(__dirname, 'index.html'),
        about: path.resolve(__dirname, 'about.html')
      }
    }
  }
})

SSR支持

需要支持服务端渲染时可以添加以下配置:

import { defineConfig } from 'vite';

// <https://vite.dev/config/>
export default defineConfig({
  ssr: {
    target: 'node', // SSR 运行环境
    noExternal: ['react-icons'] // 需要包含的打包依赖
  }
})

第三方库推荐

项目搭建好了之后,就可以去继续完善项目中常用的第三方库了,这里推荐一些比较不错的第三方库,根据自己的需求按需“食用”。

无头 UI 库

既然用上了 tailwindcss,自然少不了一些比较方便自定义 UI 的无头组件库,比如: @headlessui/reactshadcn/ui

  • @headlessui/react:tailwindcss 团队开发的无 UI 组件库,提供纯交互功能,完全无样式。
  • shadcn/ui:设计精美的组件,开放源码,可以按需复制并粘贴到您的应用程序中,无障碍,可定制。它的核心理念是提供可修改的组件代码,而不是一个传统的、黑盒式的组件库。

状态管理库

  • TanStack Query 和 ahooks

    目前比较火热的服务数据状态管理库比如 react-query(现在叫:TanStack Query)、ahooks 等等都是非常优秀的。

  • zustand

    一个轻量且直观的状态管理方案,基于 Hooks、**无需Context Provider、**简单易上手。

国际化

提到国际化,自然就会想到耳熟能详的 i18next。针对各个框架生态下有react-i18nextnext-i18nextvue-i18n 等等。

如果项目中需要用到国际化,建议搭配 i18next-browser-languagedetector 一起“食用”更佳,这个库可以自动检测用户的浏览器语言设置,并将其提供给i18next 来设置页面语言。

具体的搭建步骤就不再详细阐述了,可以自己搜索相关配置文章。

结语

好了,以上就是整个项目的搭建过程以及 Vite 常见的企业级应用配置,掌握这些配置技巧后,可以让我们的项目获得最佳的项目实践,以及最优化的打包输出,如有错误之处,欢迎指正~

❌
❌