资讯动态

Vue过滤器:从数据格式化到现代前端数据流处理

发布时间:2026/8/24 2:25:13 来源:尧图企业网站定制
1. 从“数据格式化”说起为什么我们需要过滤器在任何一个前端项目里我们都会遇到一个高频且琐碎的需求数据展示前的“化妆”。比如后端接口返回了一个时间戳1640995200000你需要在页面上显示为“2021-12-31”或者用户输入了一串手机号13800138000你需要展示为138-0013-8000又或者一个商品价格99.9你需要格式化为¥99.90。这些操作本质上都是对原始数据的格式化Formatting或转换Transforming。在早期的开发中我们可能会在每个需要展示的地方写一个工具函数来处理// 一个简单的日期格式化函数 function formatDate(timestamp) { const date new Date(timestamp); return ${date.getFullYear()}-${(date.getMonth()1).toString().padStart(2, 0)}-${date.getDate().toString().padStart(2, 0)}; } // 在模板中使用 const html div订单创建时间${formatDate(order.createTime)}/div;这种方式当然可行但问题也很明显逻辑与模板耦合且无法复用。同一个格式化逻辑如果在十个地方用到你就得写十遍或者调用十遍同一个函数。代码显得冗长且当格式化逻辑需要修改时你得找到所有调用的地方维护成本很高。Vue.js 的设计哲学之一就是“关注点分离”和“声明式渲染”。它希望开发者能更专注于数据和业务逻辑而将如何渲染交给框架。对于这种纯粹为了展示而进行的数据转换Vue 2.x 提供了一个非常优雅的解决方案过滤器Filter。过滤器的核心思想是在模板语法的层面提供一种管道式的数据转换能力。你可以把它想象成自来水厂的处理流程水源原始数据经过一道道过滤过滤器函数最终变成可以饮用的自来水渲染到页面的数据。在 Vue 模板中它通过一个竖线|来使用语法直观!-- 将 message 的值传递给名为 capitalize 的过滤器处理 -- p{{ message | capitalize }}/p !-- 链式调用先过滤A再把结果过滤B -- p{{ price | currency | suffix }}/p !-- 可以传递参数 -- p{{ date | formatDate(YYYY-MM-DD) }}/p这种声明式的写法让模板本身就能清晰地表达“这里的数据需要经过某种处理再显示”使得模板更易读逻辑更集中也极大地提升了代码的复用性。一个定义好的过滤器可以在当前组件、甚至全局的所有模板中使用。然而随着 Vue 3 的发布官方决定从核心库中移除了过滤器功能。这个决定在社区引发了不少讨论。很多人疑惑这么好用的功能为什么要移除我们接下来要深入探讨的不仅仅是过滤器怎么用更是要理解其设计背后的权衡以及在现代 Vue 开发中我们有哪些更优的替代方案。这对于无论是维护老项目还是开启新项目都至关重要。2. 过滤器的基础语法与核心玩法虽然 Vue 3 中移除了过滤器但鉴于市面上仍有海量的 Vue 2 项目在运行且其设计思想值得学习我们有必要彻底掌握它的语法和核心用法。这能帮助你在维护旧项目时游刃有余也能让你更好地理解其演进的原因。2.1 定义过滤器的两种方式局部与全局过滤器的定义非常灵活主要分为局部过滤器和全局过滤器。局部过滤器在组件的filters选项中定义仅在该组件及其子组件内可用。这是最常用的方式适用于特定业务场景的格式化需求。// 在一个Vue组件选项中 export default { data() { return { price: 99.9, article: hello vue filters }; }, filters: { // 定义一个名为 currency 的过滤器 // 它接收一个参数 value即管道符 | 前面的表达式结果 currency(value) { if (isNaN(value)) return value; // 非数字直接返回 return ¥ Number(value).toFixed(2); // 格式化为两位小数的货币形式 }, // 定义一个首字母大写的过滤器 capitalize(value) { if (!value) return ; value value.toString(); return value.charAt(0).toUpperCase() value.slice(1); } } };在模板中你可以这样使用template div p价格{{ price | currency }}/p !-- 输出价格¥99.90 -- p标题{{ article | capitalize }}/p !-- 输出标题Hello vue filters -- /div /template全局过滤器通过Vue.filter()方法定义可以在任何 Vue 实例的模板中使用。通常用于定义一些通用的、跨项目的格式化规则如日期、货币等。// 在入口文件 main.js 或一个独立的 filters.js 文件中 import Vue from vue; // 定义一个全局的日期格式化过滤器 Vue.filter(formatDate, function(value, formatStr YYYY-MM-DD) { if (!value) return ; const date new Date(value); const year date.getFullYear(); const month (date.getMonth() 1).toString().padStart(2, 0); const day date.getDate().toString().padStart(2, 0); // 简单的格式替换实际项目建议使用 dayjs 或 date-fns return formatStr.replace(YYYY, year).replace(MM, month).replace(DD, day); }); // 定义一个全局的“隐藏手机号中间四位”的过滤器 Vue.filter(hidePhone, function(value) { if (!value || typeof value ! string) return value; return value.replace(/(\d{3})\d{4}(\d{4})/, $1****$2); });定义好后在任何组件的模板中都可以直接使用template div p注册时间{{ user.createTime | formatDate(YYYY/MM/DD) }}/p p手机号{{ user.phone | hidePhone }}/p /div /template注意当全局过滤器和局部过滤器同名时会优先采用局部过滤器。这为你在特定组件中覆盖全局行为提供了可能。2.2 过滤器的参数与链式调用过滤器函数除了接收管道符前的值作为第一个参数还可以接收额外的参数。filters: { // 一个可以指定连接符的拼接过滤器 joinWith(value, delimiter , ) { if (Array.isArray(value)) { return value.join(delimiter); } return value; } }template p标签{{ tags | joinWith( · ) }}/p !-- 输出标签Vue · JavaScript · 前端 -- /template更强大的是链式调用。你可以将多个过滤器串联起来前一个过滤器的输出会作为后一个过滤器的输入。filters: { truncate(value, length 20) { if (!value) return ; if (value.length length) return value; return value.slice(0, length) ...; }, capitalize(value) { /* ... */ } }template !-- 先截断长文本再将结果的首字母大写 -- p{{ longDescription | truncate(50) | capitalize }}/p /template这种管道式的链式调用让复杂的数据转换流程在模板中一目了然逻辑清晰。2.3 在v-bind指令中使用过滤器过滤器不仅可以在 Mustache 双花括号插值中使用还可以在v-bind指令中使用Vue 2.1.0 支持这大大扩展了其应用场景。template !-- 动态绑定一个经过过滤的ID到属性上 -- div :idrawId | formatId/div !-- 一个更实际的例子根据状态码过滤出对应的CSS类名 -- div :classstatusCode | statusClass状态/div /template script export default { data() { return { rawId: user_123, statusCode: 200 }; }, filters: { formatId(value) { return id-${value.replace(_, -)}; }, statusClass(value) { const map { 200: success, 404: warning, 500: error }; return status-${map[value] || default}; } } }; /script这个特性使得过滤器不仅能用于文本展示还能参与到视图的其他动态部分功能更加全面。3. 实战构建一个健壮的企业级过滤器库了解了基础语法后我们来点实际的。在一个稍具规模的项目中零散地定义过滤器会难以维护。最佳实践是建立一个统一的、健壮的过滤器库。这不仅关乎功能更关乎代码的可维护性、可测试性和团队协作规范。3.1 过滤器库的设计与组织我建议在项目中创建一个src/filters目录并按功能模块组织过滤器。src/ ├── filters/ │ ├── index.js # 入口文件注册所有全局过滤器 │ ├── date.js # 日期时间相关过滤器 │ ├── currency.js # 货币金额相关过滤器 │ ├── text.js # 文本处理相关过滤器 │ ├── business.js # 特定业务相关过滤器 │ └── utils.js # 过滤器通用工具函数 └── main.js每个过滤器文件独立导出函数。以date.js为例我们通常会引入专业的日期库如dayjs来保证功能的强大和准确而不是自己重复造轮子。// src/filters/date.js import dayjs from dayjs; import relativeTime from dayjs/plugin/relativeTime; import dayjs/locale/zh-cn; dayjs.extend(relativeTime); dayjs.locale(zh-cn); // 设置中文 /** * 日期格式化过滤器 * param {Date|String|Number} value - 日期值 * param {String} format - 格式字符串默认 YYYY-MM-DD HH:mm:ss * returns {String} 格式化后的日期字符串 */ export function dateFormat(value, format YYYY-MM-DD HH:mm:ss) { if (!value) return --; const date dayjs(value); if (!date.isValid()) return 无效日期; return date.format(format); } /** * 相对时间过滤器如“3分钟前” * param {Date|String|Number} value - 日期值 * returns {String} 相对时间字符串 */ export function fromNow(value) { if (!value) return --; const date dayjs(value); if (!date.isValid()) return 无效日期; return date.fromNow(); }在入口文件index.js中我们统一注册这些过滤器为全局过滤器。// src/filters/index.js import * as dateFilters from ./date; import * as currencyFilters from ./currency; import * as textFilters from ./text; import * as businessFilters from ./business; // 将所有过滤器函数合并到一个对象中 const filters { ...dateFilters, ...currencyFilters, ...textFilters, ...businessFilters, }; // 安装函数在 main.js 中调用 const install (Vue) { if (install.installed) return; install.installed true; Object.keys(filters).forEach(key { Vue.filter(key, filters[key]); }); }; // 同时支持全局注册和按需导入 export default install; export * from ./date; export * from ./currency; // ... 导出其他模块最后在项目入口main.js中安装这个过滤器库。// src/main.js import Vue from vue; import App from ./App.vue; import filters from ./filters; Vue.use(filters); // 一键注册所有全局过滤器 new Vue({ render: h h(App), }).$mount(#app);3.2 编写具有防御性的过滤器代码过滤器直接用在模板中输入值可能千奇百怪。一个健壮的过滤器必须做好防御性编程避免因为意外输入导致整个组件渲染失败或页面白屏。1. 处理空值和无效值这是最常见的坑。后端数据可能为null、undefined或空字符串。// 不好的例子如果 value 为 nulltoFixed 会报错 filters: { currencyBad(value) { return ¥ value.toFixed(2); } } // 好的例子增加类型检查和默认值 filters: { currencyGood(value, symbol ¥, decimals 2) { // 检查是否为有效数字 if (value null || value || isNaN(value)) { return --; // 或返回 symbol 0.00根据业务定 } const num Number(value); // 处理极端情况如 Infinity if (!isFinite(num)) return --; return symbol num.toFixed(decimals); } }2. 处理边界情况和异常格式比如日期过滤器传入的可能是非法的时间戳、错误的日期字符串甚至是对象。export function dateFormat(value, format YYYY-MM-DD) { // 1. 空值处理 if (value null || value ) { return --; // 统一的占位符 } // 2. 尝试创建 dayjs 对象 const date dayjs(value); // 3. 验证日期有效性 if (!date.isValid()) { // 可以记录日志便于排查 console.warn(Invalid date value for filter: ${value}); return 无效日期; } // 4. 执行格式化 return date.format(format); }3. 保持过滤器“纯”过滤器应该是纯函数即相同的输入永远得到相同的输出且不产生副作用不修改外部状态、不发起网络请求、不操作DOM。这符合其“数据转换”的定位也便于测试和调试。3.3 过滤器的单元测试可测试性是健壮代码的基石。由于过滤器是纯函数为其编写单元测试非常简单。以currency过滤器为例// tests/unit/filters/currency.spec.js import { currency } from /filters/currency; describe(Currency Filter, () { it(should format positive number correctly, () { expect(currency(99.9)).toBe(¥99.90); expect(currency(100)).toBe(¥100.00); expect(currency(1234.567, $, 3)).toBe($1234.567); }); it(should format negative number correctly, () { expect(currency(-50.5)).toBe(¥-50.50); }); it(should handle non-numeric input gracefully, () { expect(currency(null)).toBe(--); expect(currency(undefined)).toBe(--); expect(currency(abc)).toBe(--); expect(currency()).toBe(--); }); it(should handle edge cases like Infinity, () { expect(currency(Infinity)).toBe(--); expect(currency(-Infinity)).toBe(--); }); });通过编写全面的测试用例你可以确保过滤器在各种边界情况下都能稳定工作这在团队协作和项目重构时能给你巨大的信心。4. 过滤器的局限性与 Vue 3 的移除之辩尽管过滤器在 Vue 2 中广受欢迎但它并非完美无缺。Vue 团队决定在 Vue 3 的核心库中移除它是经过深思熟虑的。理解这些局限性能帮助我们在新旧技术栈间做出更明智的选择。4.1 过滤器在复杂逻辑面前的乏力过滤器最擅长处理简单的、纯文本的格式化。一旦逻辑变得稍微复杂它的短板就暴露无遗。场景一需要根据数据动态决定显示内容。假设有一个订单状态码status你需要根据不同的状态码显示不同的文本和颜色。用过滤器实现会非常别扭template !-- 笨拙的写法需要多个过滤器且逻辑割裂 -- span :classstatus-${status | statusType} {{ status | statusText }} /span /template script filters: { statusType(value) { const map {1: pending, 2: processing, 3: shipped, 4: completed}; return map[value] || unknown; }, statusText(value) { const map {1: 待处理, 2: 处理中, 3: 已发货, 4: 已完成}; return map[value] || 未知状态; } } /script这里一个简单的状态展示却需要拆分成两个过滤器并且它们共享同一个映射逻辑造成了代码重复。更优雅的方式是使用计算属性template span :classstatus-${statusInfo.type} {{ statusInfo.text }} /span /template script export default { computed: { statusInfo() { const map { 1: { type: pending, text: 待处理 }, 2: { type: processing, text: 处理中 }, 3: { type: shipped, text: 已发货 }, 4: { type: completed, text: 已完成 }, }; return map[this.status] || { type: unknown, text: 未知状态 }; } } }; /script计算属性将相关逻辑封装在一起更内聚也更易于测试和维护。场景二过滤器的“黑盒”特性不利于调试。当你在模板中看到{{ data | filterA | filterB }}时如果最终显示结果不对你很难直观地知道是filterA出了问题还是filterB出了问题或者是data本身有问题。你不得不依次检查每个过滤器的内部逻辑。而使用计算属性或方法你可以在开发工具中轻松地观察到中间状态。4.2 与 Composition API 和 TypeScript 的格格不入Vue 3 推出的 Composition API 旨在提供更灵活的逻辑组织方式并更好地支持 TypeScript。过滤器在这两方面都存在硬伤。1. 逻辑复用与组合困难Composition API 的核心优势是使用纯 JavaScript/TypeScript 函数来组织和复用逻辑。过滤器却是一个与模板语法强耦合的、Vue 特有的概念。你无法在一个.js或.ts文件中直接导出一个过滤器函数然后在setup()中使用它。它必须通过app.config.globalProperties或组件选项来注册这与 Composition API 的函数式思维背道而驰。2. TypeScript 支持差过滤器在模板中使用而 TypeScript 对模板内的类型推断支持有限。你无法为过滤器的参数和返回值提供清晰的类型定义。当你在一个大型 TypeScript 项目中这种类型安全的缺失会带来隐患。相比之下一个普通的工具函数可以轻松地获得完整的 TypeScript 类型检查和智能提示。// 一个类型安全的工具函数 export function formatCurrency(value: number, symbol: string ¥): string { return ${symbol}${value.toFixed(2)}; } // 在组件中使用有完美的类型提示 import { formatCurrency } from /utils/formatter; const formattedPrice formatCurrency(price); // price 必须是 number4.3 官方解释与社区共识Vue 官方在 RFC征求意见稿中给出了移除过滤器的核心理由功能重叠过滤器能做的计算属性和方法都能做而且通常做得更好、更清晰。增加概念复杂度过滤器增加了用户需要学习的概念而它的收益并不足以证明其存在的必要性。阻碍 IDE 工具链过滤器的自定义语法使得 IDE 和静态分析工具难以提供支持如自动补全、类型检查。与 JavaScript 生态脱节在 JavaScript 中复用逻辑的标准方式是使用函数。过滤器创造了一个“方言”增加了从 Vue 迁移到其他框架或与非 Vue 代码共享逻辑的成本。社区的讨论也逐渐形成了共识对于简单的文本格式化使用一个全局的工具函数库如lodash、date-fns、自定义的formatter.js是更通用、更符合 JavaScript 习惯的做法。对于复杂的逻辑则毫无争议地使用计算属性或 Composition API 中的computed。5. 迁移与替代Vue 3 及现代项目中的最佳实践既然 Vue 3 中没有了过滤器我们该如何应对对于新项目我们直接采用新的最佳实践。对于老项目迁移我们也需要一套平滑的过渡方案。5.1 方案一使用计算属性Computed Properties这是最直接、最Vue风格的替代方案。计算属性本身就是响应式的且具有缓存机制性能优异。将过滤器重写为计算属性!-- Vue 2 过滤器写法 -- template p{{ price | currency($) }}/p p{{ fullName | capitalize }}/p /template script export default { filters: { currency(value, symbol) { /* ... */ }, capitalize(value) { /* ... */ } } } /script!-- Vue 3 计算属性写法 -- template p{{ formattedPrice }}/p p{{ capitalizedFullName }}/p /template script import { computed } from vue; import { formatCurrency, capitalizeText } from /utils/formatters; // 从工具函数导入 export default { setup(props) { const formattedPrice computed(() formatCurrency(props.price, $)); const capitalizedFullName computed(() capitalizeText(props.fullName)); return { formattedPrice, capitalizedFullName }; } } /script优点逻辑清晰与组件状态紧密绑定。完美的 TypeScript 支持。易于调试在 Vue Devtools 中可以直观看到计算属性的值。可以利用计算属性的缓存特性优化性能。缺点如果同一个格式化逻辑在多个组件中使用需要在每个组件中都定义计算属性或者重复导入工具函数略显繁琐。5.2 方案二使用全局方法或工具函数这是最接近原始过滤器“全局复用”理念的方案。我们创建一个独立的工具模块导出纯函数。// src/utils/formatters.js import dayjs from dayjs; export function currency(value, symbol ¥) { // ... 防御性实现 } export function dateFormat(value, format YYYY-MM-DD) { // ... 防御性实现 } export function truncate(text, length 100) { // ... 实现 }在组件中你可以直接导入并使用它们template p{{ formatCurrency(price) }}/p p{{ formatDate(createTime) }}/p /template script import { currency as formatCurrency, dateFormat } from /utils/formatters; export default { setup() { // 你也可以在setup中调用返回给模板 const price ref(99.9); const formattedPrice computed(() formatCurrency(price.value)); return { formatCurrency, dateFormat, formattedPrice }; } }; /script为了在模板中更方便地使用你甚至可以将其挂载到全局属性上类似于 Vue 2 的全局过滤器但这通常不被推荐因为它会污染全局作用域且失去类型提示。在 Vue 3 中如果确实需要可以这样做// main.js import { createApp } from vue; import App from ./App.vue; import * as formatters from ./utils/formatters; const app createApp(App); // 挂载到全局属性模板中通过 $filters 访问 app.config.globalProperties.$filters formatters; app.mount(#app);template p{{ $filters.currency(price) }}/p /template注意使用globalProperties会失去 TypeScript 支持且让模板逻辑变得不那么透明应谨慎使用。5.3 方案三使用自定义指令Directive处理特定场景对于某些不仅需要转换数据还需要操作 DOM的特定格式化场景自定义指令是更好的选择。例如一个将数字动态格式化为带有动画计数效果的元素。过滤器无法操作 DOM但自定义指令可以// src/directives/animatedNumber.js export const animatedNumber { mounted(el, binding) { const target Number(binding.value); const duration binding.arg || 1000; // 通过指令参数传入时长 const step 16; // 约60帧每秒 const totalSteps duration / step; const increment target / totalSteps; let current 0; const timer setInterval(() { current increment; el.textContent Math.round(current).toLocaleString(); if (current target) { el.textContent target.toLocaleString(); clearInterval(timer); } }, step); }, updated(el, binding) { // 当绑定的值更新时重新运行动画 // ... 更新逻辑需要清理旧计时器 } };template h2 v-animated-number:2000totalSales/h2 !-- 2秒内动画到目标值 -- /template script setup import { animatedNumber } from /directives/animatedNumber; const totalSales 1234567; /script5.4 老项目迁移策略如果你正在维护一个大型的 Vue 2 项目并计划升级到 Vue 3对于过滤器的迁移可以采取渐进式策略评估与分类首先统计项目中所有的过滤器将其分为两类简单工具型如日期格式化、货币格式化、文本截断等。这些可以直接替换为从工具函数库导入的函数。复杂业务型包含复杂业务逻辑的过滤器。这些通常是计算属性的候选。创建兼容层在升级初期可以在 Vue 3 中实现一个插件模拟 Vue 2 的过滤器行为为迁移争取时间。// vue3-filter-compat.js import * as formatters from ./utils/formatters; export default { install(app) { app.config.globalProperties.$filters formatters; // 或者如果坚持使用 | 语法需要更复杂的编译器 hack不推荐 } };逐步替换不要试图一次性替换所有过滤器。可以按组件或按页面逐个迁移。每次替换后充分测试相关功能。将“使用过滤器”纳入代码审查清单确保新代码不再使用。最终清理当所有过滤器都被替换后移除兼容层插件和相关的老旧代码。迁移的过程实际上也是一个代码重构和优化的过程。你会更清晰地梳理出哪些是纯粹的显示逻辑哪些是混杂的业务逻辑从而写出更清晰、更易维护的代码。6. 举一反三从过滤器到现代前端数据流处理过滤器的兴衰折射出前端开发思想的一个演变从“模板中心化”到“逻辑中心化”。早期我们希望模板能表达更多逻辑如v-for、v-if、过滤器现在我们更倾向于将逻辑移出模板用 JavaScript/TypeScript 来清晰地表达。这引导我们思考一个更根本的问题在组件中数据应该如何被处理和流转一个健康的组件数据流应该是原始数据来自 props/state - 转换/衍生数据计算属性、工具函数 - 渲染输出模板。计算属性处理依赖于组件响应式状态的衍生数据。例如一个基于列表过滤后的新列表、一个基于多个状态计算出的总和。它有缓存效率高。工具函数处理纯数据转换不依赖组件状态。例如格式化日期、金额、拼接URL。它纯函数的特点使其易于测试和复用。方法处理事件响应或复杂的、非响应式的一次性计算。例如提交表单前的数据校验、一个复杂的排序算法。过滤器试图在模板中扮演“工具函数”的角色但它模糊了关注点。在现代 Vue 开发中我的建议是建立项目级的工具函数库如/utils/formatters、/utils/validators。这是存放所有纯数据转换逻辑的家。在组件中优先使用计算属性来处理与组件状态相关的数据转换。在模板中直接调用工具函数或计算属性。这虽然少了管道符|的语法糖但带来了类型安全、更好的工具链支持和更清晰的逻辑归属。对于极其复杂的显示逻辑考虑将其封装成一个独立的、无状态的展示组件。例如一个FormattedDate组件它接收timestamp和format两个 props内部处理好格式化并渲染。这比任何过滤器或工具函数调用都更具声明性和可复用性。!-- FormattedDate.vue -- template span :titlefullDate{{ relativeTime }}/span /template script setup import { computed } from vue; import { formatDate, fromNow } from /utils/dateUtils; const props defineProps({ timestamp: { type: [Number, String, Date], required: true }, format: { type: String, default: YYYY-MM-DD } }); const fullDate computed(() formatDate(props.timestamp, props.format)); const relativeTime computed(() fromNow(props.timestamp)); /script!-- 使用 -- template FormattedDate :timestamppost.createdAt formatYYYY年MM月DD日 / /template这种方式将格式化逻辑完全封装在了组件内部父组件只需关心“要显示这个日期”而无需关心“如何显示”。这符合组件化的设计原则也是 Vue 应用更高级的架构模式。回过头看过滤器的历史使命已经完成。它曾在 Vue 2 时代为我们提供了便捷的数据展示方案。今天我们拥有了更强大的 Composition API、更完善的 TypeScript 生态和更清晰的代码组织理念。告别过滤器拥抱函数和计算属性意味着我们的代码正在走向更规范、更健壮、更面向未来的方向。在下次面对数据格式化需求时不妨先想一想“这是一个纯工具函数还是一个计算属性抑或是一个展示组件” 想清楚这个问题你的代码设计就已经成功了一大半。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价