Omnipresent 是一家面向全球雇主的一站式 HR 服务商,帮助企业雇佣和管理分布在不同国家的远程员工。这类全球 HR SaaS 平台的前端复杂度远超普通管理系统:合同签署流程要适配几十个国家的劳动法模板,薪酬页面要处理多币种与多时区,租户之间还可能存在品牌定制需求。要在 Vue 3 中稳定支撑这些能力,单靠组件化是不够的,必须把工程化体系完整地搭建起来。本文从架构分层、国际化、权限、性能与构建五个维度,详细拆解 Vue 3 在全球 HR 服务场景下的工程化实践。
一、整体架构:用 pnpm monorepo 拆分多租户前端
全球 HR 服务的业务模块天然可拆:雇员管理、合同管理、薪酬发放、合规审批、报表分析。如果全部塞进一个 Vue 应用,构建时间会线性膨胀,团队协作也会互相阻塞。推荐使用 pnpm workspace 搭建 monorepo,把代码拆成 packages 与 apps 两层。
packages 目录存放可复用的基础能力,比如 UI 组件库、国际化文案包、API 客户端、工具函数库;apps 目录存放具体的应用入口,例如面向租户的管理后台、面向雇员的 Self-Service 门户、面向内部的运营平台。三个应用共享同一套 packages,既保证了体验一致,又避免了复制粘贴代码导致的维护灾难。
hr-frontend/
├── pnpm-workspace.yaml
├── apps/
│ ├── admin/ # 租户管理后台
│ ├── employee/ # 雇员自助门户
│ └── ops/ # 内部运营平台
└── packages/
├── ui/ # 基于 Vue 3 的组件库
├── i18n/ # 多语言文案
├── api-client/ # 统一请求层
└── utils/ # 工具函数
pnpm-workspace.yaml 的配置很简单,声明 packages 和 apps 两个目录即可:
packages: - 'apps/*' - 'packages/*'
TypeScript 是这套架构的粘合剂。建议在根目录维护一份 tsconfig.base.json,统一 strict 模式、路径别名与编译目标,各子包通过 extends 继承。薪酬计算、税务规则这类对数据结构精度要求极高的模块,有了类型约束后,重构成本会显著降低。此外,契约优先的开发方式也值得推行:后端通过 OpenAPI 生成接口类型定义,前端直接引用,接口字段变更时编译期就能报错,而不是等到联调时才暴露问题。
二、国际化与时区:全球服务的第一道门槛
HR 服务涉及的文案不只是界面翻译,还包括法务合同术语、薪酬单据字段、国家名称等。Vue 3 官方推荐的方案是 vue-i18n@9,它对组合式 API 有原生支持。核心实践是把文案按业务域拆分文件,避免单个语言包膨胀到数千行难以维护。
import { createI18n } from 'vue-i18n';
import en from './locales/en';
import de from './locales/de';
import zh from './locales/zh';
export const i18n = createI18n({
legacy: false, // 启用组合式 API 模式
locale: 'en',
fallbackLocale: 'en',
messages: { en, de, zh }
});
组件内使用 useI18n 获取翻译函数,配合 t 函数处理带变量的文案。要注意数字与货币的格式化永远不要手写拼接,而是交给 Intl.NumberFormat,它能正确处理各国的小数分隔符与货币符号:
const formatSalary = (amount, currency, locale) => {
return new Intl.NumberFormat(locale, {
style: 'currency',
currency: currency,
maximumFractionDigits: 0
}).format(amount);
};
// formatSalary(65000, 'EUR', 'de-DE') 输出 65.000 €
// formatSalary(65000, 'USD', 'en-US') 输出 $65,000
时区是比翻译更容易踩坑的部分。薪酬发放日、合同生效日必须以租户或雇员所在时区展示,而不是浏览器本地时区。推荐后端统一返回 ISO 8601 带时区偏移的字符串,前端用 dayjs 配合 utc 与 timezone 插件做展示层转换,存储层永远保持 UTC。日期选择器组件也要传入明确的 timezone 参数,否则用户在东京选择的一月一号,到服务端可能就变成了十二月三十一号的深夜。
三、权限体系:从动态路由到按钮级控制
全球 HR 平台的角色非常细:租户管理员只能看自己公司的数据,财务角色只能访问薪酬模块,雇员只能查看自己的合同。这套权限要在前端落地为两层:路由级权限决定页面能否访问,操作级权限决定按钮是否显示。
路由级权限的实现思路是先注册完整路由表,再根据接口返回的权限列表过滤。用组合式函数封装这个逻辑,比写在路由守卫里更清晰:
import { ref } from 'vue';
import { useRouter } from 'vue-router';
export function usePermission() {
const permissions = ref(new Set());
const router = useRouter();
const loadPermissions = async () => {
const res = await fetch('/api/me/permissions');
const data = await res.json();
permissions.value = new Set(data.codes);
// 过滤路由:meta.requiresPermission 未通过则移除
filterRoutes(router.getRoutes());
};
const has = (code) => permissions.value.has(code);
return { permissions, loadPermissions, has };
}
操作级权限建议封装一个 v-permission 自定义指令,代码侵入性最小:
// main.js 中注册指令
app.directive('permission', {
mounted(el, binding) {
const { has } = usePermissionStore();
if (!has(binding.value)) {
el.parentNode?.removeChild(el);
}
}
});
// 模板中使用
// <button v-permission="'payroll:approve'">审批薪酬</button>
必须强调一点:前端权限只是体验层的过滤,真正的安全边界在后端。前端隐藏按钮的目的是避免用户误操作,而不是防止越权访问,所有敏感接口都必须在服务端二次校验权限。这一点在薪酬这种涉及敏感数据的模块尤其重要,任何依赖前端判断的安全设计都是不可接受的。
四、性能优化:大表单与长列表的实战方案
HR 系统的薪酬录入页往往有上百个输入字段,雇员列表可能达到数万条。如果直接渲染,页面交互会明显卡顿。针对大表单,核心手段是把表单拆成子表单组件,每个子表单独立维护响应式状态,避免一个巨大的 reactive 对象导致依赖追踪开销失控。对于不需要深度响应式的展示型数据,用 shallowRef 或 markRaw 跳过代理:
import { shallowRef, markRaw } from 'vue';
// 大列表数据只需整体响应,逐项无感知
const employeeList = shallowRef([]);
const loadList = async () => {
const res = await fetch('/api/employees?page=1&size=50');
employeeList.value = markRaw(await res.json());
};
针对长列表,推荐使用虚拟滚动组件(如 vue-virtual-scroller),只渲染视口内的行,内存占用与滚动性能都能保持稳定。表格的排序与筛选逻辑尽量放到服务端,前端只负责渲染,这对多租户系统还有额外好处:减轻浏览器负担的同时,数据一致性也更容易保证。
路由组件全部采用懒加载是另一个基本功,配合 Vite 的分包策略可以进一步控制首屏体积:
const routes = [
{
path: '/payroll',
component: () => import(
/* webpackChunkName: "payroll" */
'../views/payroll/index.vue'
)
}
];
构建层面,Vite 的按需引入配合 unplugin-vue-components 可以让组件库只打包实际用到的部分。对于 Element Plus 这类组件库,按需引入相比全量引入通常能减少一半以上的首屏体积。同时建议开启 gzip 或 brotli 预压缩,全球用户的网络条件差异很大,压缩收益非常直接。
五、质量保障与团队协作规范
工程化的最后一环是让多人协作可控。基础配置包括:ESLint 加 eslint-plugin-vue 统一代码风格,Prettier 格式化,husky 与 lint-staged 在提交前自动检查,CI 流水线跑单元测试与构建验证。组件测试推荐 Vitest 加 @vue/test-utils,重点覆盖权限指令、国际化格式化函数这类全局逻辑。
import { mount } from '@vue/test-utils';
import { describe, it, expect } from 'vitest';
import SalaryCard from '../SalaryCard.vue';
describe('SalaryCard', () => {
it('根据 locale 正确格式化金额', () => {
const wrapper = mount(SalaryCard, {
props: { amount: 65000, currency: 'EUR', locale: 'de-DE' }
});
expect(wrapper.text()).toContain('65.000');
});
});
目录结构上,业务模块按 feature 组织而不是按文件类型组织。一个 payroll 模块下放自己的组件、store、api 与测试,改动时不需要在六七个顶级目录之间跳转,删除或迁移模块也只需移动一个文件夹。这种结构对多团队并行开发的 SaaS 项目非常友好。
总结来看,Vue 3 工程化构建全球 HR 服务的关键在于:用 monorepo 承载多应用复用,用类型系统守住数据契约,用国际化与时区方案覆盖全球用户,用分层权限匹配组织结构,用性能与构建优化保障体验。把这些环节串起来,Vue 3 就不再只是一个视图层框架,而是一套能支撑业务长期演进的前端基础设施。