在 Vue 3 的工程化体系中,数据隐私保护已经从“临时打码”演变为一套完整的数据处理规范。脱敏侧重于隐藏敏感字段的部分内容,例如手机号中间四位替换为星号;匿名化则强调移除能够定位到自然人的直接标识符,使数据在统计分析时不可回溯。两者都需要前置到数据流转的关键节点,而不是散落在各个组件的模板中。
为什么不能在模板里直接脱敏
很多团队初期会在模板中用表达式处理敏感数据,比如 {{ phone.replace(/(d{3})d{4}(d{4})/, '$1****$2') }}。这种做法短期看省事,但会带来三个问题:第一,脱敏逻辑分散在多个视图,修改规则要全局搜索;第二,组件内拿到的原始数据仍然完整,一旦被其他函数误用就会泄露;第三,无法对脱敏前后的数据做统一审计和测试。
从工程化角度,敏感数据应该在“进入响应式状态之前”就被处理。Vue 3 的响应式系统并不关心数据内容是否合规,它只负责追踪变化。因此我们要在数据源侧建立清洗层,让组件拿到的本身就是脱敏或匿名化之后的对象。
用组合式函数封装脱敏规则
组合式函数(composable)是 Vue 3 复用有状态逻辑的最佳载体。我们可以把常见的脱敏规则抽离成独立模块,通过明确的函数签名约束输入输出。
// usePrivacy.js
export function maskPhone(phone) {
if (!phone || phone.length !== 11) return phone;
return phone.replace(/(d{3})d{4}(d{4})/, '$1****$2');
}
export function maskEmail(email) {
if (!email || !email.includes('@')) return email;
const [name, domain] = email.split('@');
const show = name.length <= 2 ? name[0] : name.slice(0, 2);
return show + '***@' + domain;
}
export function usePrivacy() {
return { maskPhone, maskEmail };
}
上面的代码把手机号和邮箱的脱敏逻辑集中管理。在组件里只需引入 usePrivacy 即可调用,避免重复编写正则。如果未来业务要求脱敏位数调整,只需改这一处。
需要注意的是,组合式函数本身不自动拦截数据,它只是工具。真正的工程化落地,还要配合请求层和状态层使用。
在请求拦截器中统一脱敏
axios 的响应拦截器是介入数据流的天然位置。我们可以约定接口返回的敏感字段使用特定后缀,例如 _raw,拦截器检测到后自动转换。
// privacyInterceptor.js
import axios from 'axios';
import { maskPhone, maskEmail } from './usePrivacy';
function deepMask(obj) {
if (Array.isArray(obj)) {
return obj.map(deepMask);
}
if (obj && typeof obj === 'object') {
const result = {};
for (const key in obj) {
if (key.endsWith('_raw')) {
const newKey = key.replace('_raw', '');
if (key.startsWith('phone')) result[newKey] = maskPhone(obj[key]);
else if (key.startsWith('email')) result[newKey] = maskEmail(obj[key]);
else result[newKey] = obj[key];
} else {
result[key] = deepMask(obj[key]);
}
}
return result;
}
return obj;
}
const instance = axios.create();
instance.interceptors.response.use(response => {
response.data = deepMask(response.data);
return response;
});
这段拦截器会递归遍历响应体,把带有 _raw 后缀的字段脱敏后存为干净字段。这样做之后,Pinia 或组件拿到的数据已经安全,即使模板直接渲染也不会暴露原文。
这种方案的优点是规则集中、对业务组件无侵入;缺点是如果后端字段命名不规范,需要维护映射表。建议在前端约定文档里明确敏感字段传输约定。
匿名化处理的实践思路
匿名化比脱敏更彻底。例如在做用户行为分析时,我们不应该带着用户 ID 上报,而是生成不可逆的访客标识。可以借助组合式函数结合 localStorage 实现。
// useAnonymousId.js
import { ref } from 'vue';
export function useAnonymousId() {
const anonId = ref(localStorage.getItem('anon_id'));
if (!anonId.value) {
anonId.value = 'v-' + Math.random().toString(36).slice(2, 10);
localStorage.setItem('anon_id', anonId.value);
}
return anonId;
}
该函数在首次访问时生成随机访客 ID,后续复用。上报埋点数据时只带 anonId,服务端无法据此反推真实账号。与脱敏不同,匿名化通常作用于标识类字段,且最好在数据采集层就完成,不要等进入 Vue 组件后再删。
在工程化架构中,建议把匿名化 ID 的生成放在独立的 SDK 模块,Vue 组件仅负责触发事件,不直接拼装上报体,降低耦合。
自定义指令做展示层兜底
尽管数据应在源头处理,但展示层仍可作为最后一道防线。我们可以写一个 v-mask 指令,在绑定文本时自动脱敏。
// directives/mask.js
export const vMask = {
mounted(el, binding) {
const type = binding.arg || 'phone';
const val = binding.value;
if (type === 'phone' && val) {
el.textContent = val.replace(/(d{3})d{4}(d{4})/, '$1****$2');
} else {
el.textContent = val;
}
},
updated(el, binding) {
const type = binding.arg || 'phone';
const val = binding.value;
if (type === 'phone' && val) {
el.textContent = val.replace(/(d{3})d{4}(d{4})/, '$1****$2');
} else {
el.textContent = val;
}
}
};
使用方式为 <span v-mask:phone="user.phone"></span>。注意这里的 <span> 是 HTML 标签,在正文描述中已转义。指令适合做兜底,不应作为主方案,否则会出现前文说的逻辑分散问题。
综合来看,Vue 3 工程化的数据隐私方案应该是:拦截器统一脱敏、composable 提供规则、匿名 ID 独立生成、指令仅兜底。这样才能在可维护性和合规性之间取得平衡。
小结与落地建议
脱敏与匿名化不是功能点,而是数据流规范。团队在搭建 Vue 3 项目时,应在脚手架阶段就植入隐私处理层,并把相关函数写入公共包。测试环节要补充敏感数据断言,确保不会出现原文落地前端的情况。
当业务扩张到多端复用,这套逻辑还能抽成纯 TS 模块,供小程序或 Node 服务端复用,进一步降低合规成本。