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 :id="rawId | formatId"></div> <!-- 一个更实际的例子:根据状态码过滤出对应的CSS类名 --> <div :class="statusCode | 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 为 null,toFixed 会报错 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 :class="`status-${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 :class="`status-${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:2000="totalSales"></h2> <!-- 2秒内动画到目标值 --> </template> <script setup> import { animatedNumber } from '@/directives/animatedNumber'; const totalSales = 1234567; </script>5.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 :title="fullDate">{{ 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 :timestamp="post.createdAt" format="YYYY年MM月DD日" /> </template>这种方式将格式化逻辑完全封装在了组件内部,父组件只需关心“要显示这个日期”,而无需关心“如何显示”。这符合组件化的设计原则,也是 Vue 应用更高级的架构模式。
回过头看,过滤器的历史使命已经完成。它曾在 Vue 2 时代为我们提供了便捷的数据展示方案。今天,我们拥有了更强大的 Composition API、更完善的 TypeScript 生态和更清晰的代码组织理念。告别过滤器,拥抱函数和计算属性,意味着我们的代码正在走向更规范、更健壮、更面向未来的方向。在下次面对数据格式化需求时,不妨先想一想:“这是一个纯工具函数,还是一个计算属性,抑或是一个展示组件?” 想清楚这个问题,你的代码设计就已经成功了一大半。