普通视图

发现新文章,点击刷新页面。
昨天以前首页

别再写改名脚本了,一个 Vite 插件搞定压缩、校验、自动哈希命名vite-plugin-pack-orchestrator

2026年4月25日 22:46

📦 Vite 构建压缩插件:vite-plugin-pack-orchestrator

🤔 为什么又造一个轮子?

市面上已经有一些 Vite 打包插件,比如 vite-plugin-zip-packvite-plugin-compress 等,能用,但总差那么点意思 — 大多只支持 ZIP,功能也比较单一。

实际项目里,打包这个环节往往没那么简单:

  1. 多种压缩格式 🗜️ — ZIP 方便分享给同事,TAR.GZ 部署到 Linux 服务器,7Z 追求更高压缩比存档归档,不同场景需要不同格式
  2. 文件校验 🔐 — 打包后需要 MD5/SHA1 校验值来确认版本一致性,尤其是发布给客户的场景
  3. 灵活命名 ✏️ — 版本号、时间戳、哈希值,文件名里能带的信息越多越好
  4. CI/CD 友好 🚀 — 流水线里每次构建产物都应该是唯一可追溯的,压缩后自动带哈希改名,省去人工处理的麻烦(写脚本去改也麻烦一些)

现有插件基本没法同时满足这些,所以写了 vite-plugin-pack-orchestrator

⚡ 和其他插件有什么不同

功能 大多数打包插件 本插件
压缩格式 仅 ZIP ZIP / TAR / TAR.GZ / 7Z (RAR需要商业授权,所以没搞)
校验和 MD5 / SHA1 / SHA256
文件名模板 固定命名 支持 [name] [version] [timestamp] [hash] 占位符
Hook 扩展 onBeforeBuild / onAfterBuild / onError 等钩子
文件过滤 部分支持 include + exclude glob 模式
7Z 支持 需要系统安装 7z 内置,零依赖
输出目录控制 固定位置 archiveOutDir 自定义

📥 安装

npm install vite-plugin-pack-orchestrator

🚀 快速上手

最基本的用法,两行配置搞定:

// vite.config.ts
import { defineConfig } from 'vite';
import orchestrator from 'vite-plugin-pack-orchestrator';

export default defineConfig({
  plugins: [
    orchestrator({
      pack: {
        outDir: 'dist',          // 要打包的目录,默认就是 'dist'
        format: 'zip',           // 压缩格式:zip | tar | tar.gz | 7z
        fileName: 'myapp',       // 压缩包文件名
      },
    }),
  ],
  build: { outDir: 'dist' },
});

执行 vite build 后,会在项目根目录生成 myapp.zip

⚙️ 配置项详解

pack — 打包配置

pack: {
  outDir: 'dist',              // 要打包的源目录(相对于项目根目录),默认 'dist'
  fileName: 'myapp',           // 文件名,支持占位符(见下方说明)
  format: 'zip',               // 压缩格式:'zip' | 'tar' | 'tar.gz' | '7z'
  compressionLevel: 9,         // 压缩级别 0-9,默认 9(最高压缩率)
  archiveOutDir: './releases', // 压缩包输出目录,不写默认项目根目录
  exclude: ['**/*.map'],       // 排除的文件(glob 匹配)
  include: ['**/*.js'],        // 只包含的文件(可选,不设置则包含全部)
}

fileName 占位符

文件名支持以下占位符,打包时自动替换:

占位符 说明 示例值
[name] package.json 中的 name my-awesome-app
[version] package.json 中的 version 1.2.0
[timestamp] 当前时间戳 1714012345678
[hash] 构建内容 MD5 哈希(完整 32 位) a1b2c3d4e5f6...
[hash:8] MD5 哈希前 N 位(自定义长度) a1b2c3d4
// 示例:fileName 设为 'release-[version]-[timestamp]'
// 输出:release-1.2.0-1714012345678.zip

// 示例:fileName 设为 '[name]-v[version]'
// 输出:my-awesome-app-v1.2.0.zip

// 示例:fileName 设为 '[name]-[hash]'
// 输出:my-awesome-app-a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6.zip

// 示例:fileName 设为 '[name]-[hash:8]'
// 输出:my-awesome-app-a1b2c3d4.zip

如果 fileName 不包含扩展名,插件会根据 format 自动追加 .zip.tar.gz 等后缀。

🔗 hooks — 钩子函数

onBeforeBuild — 构建前执行

在 Vite 开始打包之前执行,适合做一些前置清理工作:

hooks: {
  onBeforeBuild: async () => {
    // 构建前的一些处理
  },
}
onBundleGenerated — bundle 生成后执行

Vite bundle 生成后、压缩包创建前执行,可以拿到构建产物信息:

hooks: {
  onBundleGenerated: (bundle) => {
    console.log('生成的文件:', Object.keys(bundle));
  },
}
onAfterBuild — 压缩完成后执行(核心)

这是本插件最强大的功能。 压缩包创建完成后,插件会自动计算 MD5 / SHA1 / SHA256 三种校验和,然后传给 onAfterBuild。你可以利用这些校验和来重命名压缩包

返回一个新路径(和原路径不同),插件会自动重命名文件:

hooks: {
  onAfterBuild: (path, format, checksums) => {
    // path      — 当前压缩包的完整路径
    // format    — 压缩格式('zip' | 'tar' | 'tar.gz' | '7z')
    // checksums — 校验和对象:{ md5: string, sha1: string, sha256: string }
    return path; // 返回原路径则不重命名
  },
}

实际案例:

// 案例 1:在扩展名前插入 SHA1 短哈希(最常用)
// myapp.zip → myapp-3a7b2c1d.zip
onAfterBuild: (path, format, checksums) =>
  path.replace(/(\.(?:zip|tar\.gz|tar|7z))$/, `-${checksums.sha1.slice(0, 8)}$1`);

// 案例 2:用 MD5 全量替换文件名
// myapp.zip → a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6.zip
onAfterBuild: (path, format, checksums) =>
  path.replace(/^.+(?=\.\w+$)/, checksums.md5);

// 案例 3:追加格式和哈希到原始文件名
// myapp.zip → myapp-zip-a1b2c3d4.zip
onAfterBuild: (path, format, checksums) =>
  path.replace(/(\.\w+)$/, `-${format}-${checksums.sha256.slice(0, 8)}$1`);

// 案例 4:完全自定义文件名,用 format 参数自动适配后缀
// myapp.zip → release-a1b2c3d4e5f6.zip
onAfterBuild: (path, format, checksums) =>
  `release-${checksums.md5.slice(0, 12)}.${format}`;

// 案例 5:不重命名,只是拿校验和做点其他事(比如写入文件)
onAfterBuild: async (path, format, checksums) => {
  fs.writeFileSync('checksums.json', JSON.stringify(checksums));
  // 不 return 或 return 原路径 = 不重命名
}
onError — 出错时执行

打包失败时回调,适合接入告警通知:

hooks: {
  onError: async (error) => {
    console.error('打包出错了:', error.message);
    // 可以在这里接入钉钉/企业微信告警
  },
}

🔄 为什么说压缩后自动改名对 CI/CD 很重要?

在持续集成/持续部署的流水线中,每次构建的产物都需要是唯一可追溯的。如果压缩包文件名固定叫 dist.zip,你怎么知道这次构建和上次有什么区别?回滚的时候该拿哪个版本?

本插件通过 onAfterBuild 钩子拿到校验和后,可以自动在文件名中插入哈希值:

hooks: {
  onAfterBuild: (path, format, checksums) =>
    path.replace(/(\.zip)$/, `-${checksums.sha1.slice(0, 8)}$1`);
}

构建后输出:

myapp-1.0.2-3a7b2c1d.zip
myapp-1.0.2-7f9e4b2a.zip

文件名本身就是指纹 🔑,一眼就能区分不同构建,部署脚本直接按文件名定位版本,不需要额外维护版本映射表。回滚也简单 — 找到上一个哈希文件名部署即可。配合 [version] [timestamp] 占位符,追溯性更强。

🎯 完整示例

把前面的配置合在一起,就是一个完整的生产级配置:

// vite.config.ts
import { defineConfig } from 'vite';
import orchestrator from 'vite-plugin-pack-orchestrator';

export default defineConfig({
  plugins: [
    orchestrator({
      pack: {
        outDir: 'dist',                    // 打包 dist 目录
        fileName: 'myapp-[version]',       // 文件名带版本号
        format: 'zip',                     // ZIP 格式
        archiveOutDir: './releases',       // 输出到 releases 目录
        exclude: ['**/*.map'],             // 排除 sourcemap
      },
      hooks: {
        // 压缩完成后自动加上 SHA1 哈希
        onAfterBuild: (path, format, checksums) =>
          path.replace(/(\.(?:zip|tar\.gz|tar|7z))$/, `-${checksums.sha1.slice(0, 8)}$1`),
        // 出错时打印日志
        onError: (error) => console.error('打包失败:', error.message),
      },
    }),
  ],
  build: { outDir: 'dist' },
});

vite build 一次搞定,不需要额外的打包脚本。


插件很轻量,代码开源,欢迎试用和提建议 🎉

ba30da3a555101d1dd06ef37b59c22d8.jpg

Svelte/SvelteKit 多语言配置指南

2026年4月19日 14:36

方案对比

方案 适用场景 复杂度 依赖大小
自定义 Store SvelteKit 全栈 0
svelte-i18n 纯 Svelte 应用 ~3KB
typesafe-i18n 类型安全优先 ~5KB
paraglide-js 编译时优化 ~2KB

方案一:自定义 Store(推荐 SvelteKit)

最轻量的方案,无需额外依赖,代码完全可控。

GitHub: 无(纯手写)

1. 目录结构

src/lib/i18n/
├── translations.ts      # 翻译数据聚合
├── index.ts             # 导出接口
└── locales/
    ├── zh.ts            # 中文
    └── en.ts            # 英文

2. 翻译文件

// src/lib/i18n/locales/zh.ts
export const zh = {
    nav: {
        home: '首页',
        about: '关于'
    },
    welcome: '欢迎'
};

// src/lib/i18n/locales/en.ts
export const en = {
    nav: {
        home: 'Home',
        about: 'About'
    },
    welcome: 'Welcome'
};

3. 核心实现

// src/lib/i18n/translations.ts
import { zh } from './locales/zh';
import { en } from './locales/en';

export const translations = { zh, en };
export type Language = keyof typeof translations;
export type TranslationType = typeof zh;

// 检测浏览器语言
export function detectLang(): Language {
    if (typeof navigator === 'undefined') return 'zh';
    const lang = navigator.language.toLowerCase();
    return lang.startsWith('zh') ? 'zh' : 'en';
}

// 从 localStorage 读取
export function getStoredLang(): Language | null {
    if (typeof localStorage === 'undefined') return null;
    const stored = localStorage.getItem('lang');
    return stored === 'zh' || stored === 'en' ? stored : null;
}
// src/lib/i18n/index.ts
import { writable, derived, get } from 'svelte/store';
import { translations, detectLang, getStoredLang, type Language } from './translations';

// 优先从 localStorage 读取,否则检测浏览器语言
const initialLang = getStoredLang() || detectLang();

// 当前语言 Store
export const currentLang = writable<Language>(initialLang);

// 翻译函数 Store
export const t = derived(currentLang, ($lang) => {
    return (key: string): string => {
        const keys = key.split('.');
        let value: any = translations[$lang];
        for (const k of keys) {
            value = value?.[k];
        }
        // 回退到 key 本身
        return typeof value === 'string' ? value : key;
    };
});

// 切换语言
export function setLang(lang: Language) {
    currentLang.set(lang);
    if (typeof localStorage !== 'undefined') {
        localStorage.setItem('lang', lang);
    }
}

// 获取当前语言(非响应式,用于脚本)
export function getLang(): Language {
    return get(currentLang);
}

4. 组件中使用

$ 前缀的作用:Svelte 中 $storeNamestoreName.subscribe() 的语法糖,表示自动订阅该 Store,值变化时组件自动更新。

<!-- +layout.svelte -->
<script lang="ts">
    import { currentLang, t, setLang } from '$lib/i18n';
    
    // 不带 $:获取 Store 对象本身
    console.log(currentLang);  // Store 对象 { subscribe, set, update }
    
    // 带 $:获取 Store 的当前值(自动订阅)
    console.log($currentLang); // 'zh' 或 'en'
</script>

<nav>
    <!-- 使用 $t() 获取翻译,$currentLang 获取当前语言 -->
    <a href="/">{$t('nav.home')}</a>
    <a href="/about">{$t('nav.about')}</a>
    
    <button on:click={() => setLang($currentLang === 'zh' ? 'en' : 'zh')}>
        {$currentLang === 'zh' ? 'EN' : '中文'}
    </button>
</nav>

对比

写法 含义 使用场景
currentLang Store 对象 传递给函数、调用方法
$currentLang Store 的值 模板中显示、读取当前值

5. SSR 服务端渲染支持

SvelteKit 原生支持 SSR,语言从 URL/Cookie 检测,服务端预加载翻译。

服务端与客户端的差异

环境 可用 不可用
服务端 URL、Cookie、Header localStorage、navigator
客户端 全部

Cookie 工具函数

// src/lib/i18n/cookies.ts
import type { Cookies } from '@sveltejs/kit';

export function getLangFromCookies(cookies: Cookies): 'zh' | 'en' {
    const stored = cookies.get('lang');
    return stored === 'zh' || stored === 'en' ? stored : 'zh';
}

export function setLangCookie(cookies: Cookies, lang: 'zh' | 'en') {
    cookies.set('lang', lang, {
        path: '/',
        maxAge: 60 * 60 * 24 * 365  // 1年
    });
}

Layout Load(服务端预加载):

// src/routes/+layout.ts
import type { LayoutLoad } from './$types';
import { translations } from '$lib/i18n/translations';
import { getLangFromCookies, setLangCookie } from '$lib/i18n/cookies';

export const load: LayoutLoad = ({ cookies, url }) => {
    // 服务端:从 Cookie 或 URL 参数获取语言
    const langParam = url.searchParams.get('lang');
    const lang = (langParam === 'en' ? 'en' : 'zh');

    // 同步 Cookie
    setLangCookie(cookies, lang);

    // 预加载翻译数据
    const t = translations[lang];

    return { lang, t };
};

Layout(接管切换):

<!-- src/routes/+layout.svelte -->
<script lang="ts">
    import { onMount } from 'svelte';
    import { setLang } from '$lib/i18n';

    let { data, children } = $props();
    
    // 初始化语言
    setLang(data.lang);
    
    // 语言切换
    function switchLang() {
        const newLang = $currentLang === 'zh' ? 'en' : 'zh';
        setLang(newLang);
        // 跳转刷新
        window.location.href = `/?lang=${newLang}`;
    }
</script>

<nav>
    <a href="/?lang=zh">中文</a>
    <a href="/?lang=en">EN</a>
    <button on:click={switchLang}>
        当前: {$currentLang}
    </button>
</nav>

{@render children()}

工作原理

  1. 用户访问 /?lang=en
  2. 服务端从 URL 读取参数,同步到 Cookie,返回预加载了英文的 HTML
  3. 客户端 setLang(data.lang) 同步 Store,页面已有翻译
  4. 用户切换语言 → 跳转 /?lang=zh → 服务端返回中文 HTML

SEO 友好

  • 搜索引擎爬虫访问 /?lang=zh 抓中文内容
  • 访问 /?lang=en 抓英文内容
  • 每个语言都有独立 URL

5. SSR 注意事项

<!-- 安全访问 localStorage -->
<script lang="ts">
    import { onMount } from 'svelte';
    import { setLang } from '$lib/i18n';
    
    onMount(() => {
        // 客户端才执行
        const saved = localStorage.getItem('lang');
        if (saved) setLang(saved as 'zh' | 'en');
    });
</script>

方案二:svelte-i18n (纯svelte推荐)

社区最流行的方案,API 设计简洁。

npm install svelte-i18n

初始化文件

// src/lib/i18n.ts
import { register, init, getLocaleFromNavigator, locale } from 'svelte-i18n';

// 注册语言文件(懒加载)
register('zh', () => import('./locales/zh.json'));
register('en', () => import('./locales/en.json'));

// 初始化配置
init({
    fallbackLocale: 'zh',
    initialLocale: getLocaleFromNavigator()
});

// 导出切换函数
export { locale };
export const setLocale = (lang: string) => locale.set(lang);

应用入口引入

// src/main.ts (纯 Svelte)
import './lib/i18n';  // ← 必须先导入初始化
import App from './App.svelte';

const app = new App({ target: document.body });
export default app;
// src/routes/+layout.ts (SvelteKit)
import { browser } from '$app/environment';
import { locale, waitLocale } from 'svelte-i18n';
import '$lib/i18n';  // 导入执行初始化
import type { LayoutLoad } from './$types';

export const load: LayoutLoad = async () => {
    if (browser) {
        const saved = localStorage.getItem('lang');
        if (saved) locale.set(saved);
    }
    await waitLocale();  // 等待翻译加载完成
    return {};
};

组件使用

<script>
    import { _, locale } from 'svelte-i18n';
    import { setLocale } from '$lib/i18n';
</script>

<h1>{$_('welcome')}</h1>
<p>{$_('footer.copyright')}</p>

<button on:click={() => setLocale($locale === 'zh' ? 'en' : 'zh')}>
    切换
</button>

带参数的翻译

{
    "hello": "Hello {name}!",
    "items": "You have {count} item | You have {count} items"
}
<p>{$_('hello', { values: { name: 'World' } })}</p>
<p>{$_('items', { values: { count: 5 } })}</p>

方案三:typesafe-i18n

类型安全的国际化方案,IDE 自动补全翻译 key。

npm install typesafe-i18n
npx typesafe-i18n --setup  # 生成配置文件

自动生成类型

// src/i18n/i18n-types.ts(自动生成)
export type Translation = {
    nav: {
        home: string;
        about: string;
    };
    welcome: string;
};

使用

<script lang="ts">
    import { LL } from '$lib/i18n/i18n-svelte';
    import { setLocale } from '$lib/i18n/i18n-util';
</script>

<h1>{$LL.welcome()}</h1>
<a href="/about">{$LL.nav.about()}</a>

方案四:paraglide-js

编译时优化的国际化方案,零运行时开销。

npm install @inlang/paraglide-js

特点

  • 编译时将翻译内联到代码中
  • 只打包用到的翻译
  • 支持 Tree Shaking
// 编译后直接使用
import * as m from '$lib/paraglide/messages.js';

console.log(m.hello_world()); // "Hello World!"

方案五:URL 路由级多语言(SvelteKit)

SEO 友好的方案,语言体现在 URL 中。

/zh/about    → 中文关于页
/en/about    → 英文关于页
/about       → 默认语言

路由配置

// src/params/lang.ts
import type { ParamMatcher } from '@sveltejs/kit';

export const match: ParamMatcher = (param) => {
    return ['zh', 'en'].includes(param);
};
src/routes/
├── [[lang=lang]]/         # 可选语言前缀
│   ├── +page.svelte
│   └── about/
│       └── +page.svelte
└── +layout.ts

加载翻译

// src/routes/[[lang=lang]]/+layout.ts
import type { LayoutLoad } from './$types';
import { translations } from '$lib/i18n/translations';

export const load: LayoutLoad = ({ params }) => {
    const lang = (params.lang as 'zh' | 'en') || 'zh';
    return {
        lang,
        t: translations[lang]
    };
};

关键决策点

场景 推荐方案 理由
快速上线 svelte-i18n 生态成熟,文档丰富
类型安全 typesafe-i18n 编译时检查,IDE 提示
SEO 优先 URL 路由级 语言在 URL,搜索引擎友好
极简依赖 自定义 Store 零依赖,完全可控
大型应用 paraglide-js 编译优化,性能最好
SSR + SEO 自定义 Store + Cookie 服务端预加载,客户端接管切换

最佳实践

1. 延迟加载翻译

// 不要:import zh from './locales/zh';  // 打包进主 bundle
// 要:
register('zh', () => import('./locales/zh.json'));  // 按需加载

2. SSR 安全访问浏览器 API

<script>
    import { browser } from '$app/environment';
    import { onMount } from 'svelte';
    
    // 方式一:onMount
    onMount(() => {
        localStorage.getItem('lang');  // 安全
    });
    
    // 方式二:browser 判断
    if (browser) {
        localStorage.getItem('lang');  // 安全
    }
</script>

3. 回退机制

// 找不到翻译时回退到 key
export function t(key: string): string {
    const value = getNestedValue(translations[lang], key);
    return value || key;  // 回退到 key
}

4. 类型安全(自定义 Store 版)

// 生成翻译 key 的类型
type DotPrefix<T extends string> = T extends '' ? '' : `.${T}`;

type DotPath<T> = (
    T extends object ?
        { [K in keyof T]:
            `${Exclude<K, symbol>}${DotPrefix<DotPath<T[K]>>}`
        }[keyof T] :
        ''
) extends infer D ? Extract<D, string> : never;

export type TranslationKey = DotPath<typeof zh>;

// 使用
export const t = (key: TranslationKey) => ...
// IDE 提示: 'nav.home' | 'nav.about' | 'welcome' ...

5. 语言切换动画

{#key $currentLang}
    <div in:fade={{ duration: 150 }}>
        <h1>{$t('welcome')}</h1>
    </div>
{/key}

参考资源

❌
❌