☰
Vue 2过滤器迁移到Vue 3:从管道符到函数调用的完整指南
2026/10/16 1:10:31 网站建设 项目流程

从Vue 2时代走过来的前端,应该都写过类似{{ price | formatMoney }}这样的模板表达式。过滤器在Vue 2里确实是格式化文本的一把好手,日期、金额、截断、脱敏都能通过管道符“串”在模板里,代码干净得不像话。但前提是你得知道它的边界在哪:哪些场景该用、哪些场景不该用,以及为什么到了Vue 3它会被彻底移除,换成函数调用。这篇文章不打算念文档,就把我平时用过滤器踩过的坑、总结的规则和可以直接抄的写法一次说清楚,给正在Vue 2项目里维护老代码,或者刚从Vue 2迁移到Vue 3的朋友做个参考。

1. 过滤器到底是什么?先把它在模板里的位置搞清楚

1.1 过滤器不是函数,是模板语法糖

很多人刚接触过滤器时会误以为它就是“模板里的一个方法”,其实更准确的说法是:过滤器定义的函数本体,真正起作用的是模板编译器对管道符|的解析。你在组件里写filters: { formatMoney() {} },如果从来不在模板里用管道符,那它就是个躺着不动的普通对象属性;一旦在模板里写了{{ value | formatMoney }},Vue 的模板编译器就会把这一段翻译成对formatMoney(value)的函数调用。

也就是说,过滤器真正的价值体现在模板层,它让“数据的展示前转换”这件事留在模板里完成,而不是在数据源或者事件回调里提前处理。这个定位决定了它的适用面:适合单一值的轻量格式化,不适合复杂的业务判断。

1.2 Vue 2 和 Vue 3 对过滤器的态度完全相反

这里必须先把版本差异讲清楚。如果你在用 Vue 2,过滤器是可用的,注册方式分局部和全局两种;如果你已经切到 Vue 3,那很遗憾,过滤器这个特性在 Vue 3.0 正式版发布时就被移除了,官方给出的理由也很直接:过滤器本身只是函数的语法糖,直接用函数或者计算属性替代就够了,没必要为模板单独保留一套语法。

所以一个很现实的局面是:老项目里布满了{{ date | formatTime }},新代码却只能老老实实写{{ formatTime(date) }}。如果你正在维护 Vue 2 项目,过滤器照用没问题,但心里要有数,它不是 Vue 核心语法里长盛不衰的东西。理解这一点,后面做技术选型或迁移时才会少走弯路。

2. 过滤器的注册姿势:局部注册、全局注册和命名冲突

2.1 组件内局部注册:适合一次性格式化

局部注册是最常见的用法,直接在组件配置项的filters字段里声明即可。我平时的习惯是,只在某一个组件里用到的格式化逻辑就用局部注册,比如某个详情页的金额展示、某个列表的状态文案转换。

export default { data() { return { price: 12800 } }, filters: { formatMoney(value, symbol = '¥', decimals = 2) { const num = Number(value) if (Number.isNaN(num)) return value return symbol + num.toFixed(decimals).replace(/\B(?=(\d{3})+(?!\d))/g, ',') } } }

模板里直接写:

<p>{{ price | formatMoney('$', 2) }}</p>

重点说明一下参数顺序:管道符左边传入的值永远是过滤器的第一个参数,后面的参数才是你在模板表达式里额外传的。这不是约定,是规则。formatMoney的形参value接收到的是12800,symbol接收'$',decimals接收2。顺序颠倒是新手容易犯的错误。

2.2 全局注册:适合跨页面复用的兜底方案

如果一个格式化逻辑在十几个组件里都要用,逐个局部注册就是纯粹的重复劳动。这个时候用Vue.filter做全局注册才是正经路子:

// main.js 在 Vue 2 项目入口文件 import Vue from 'vue' Vue.filter('formatDate', (value, pattern = 'YYYY-MM-DD') => { if (!value) return '' const date = new Date(value) if (Number.isNaN(date.getTime())) return value const pad = (n) => String(n).padStart(2, '0') return pattern .replace('YYYY', date.getFullYear()) .replace('MM', pad(date.getMonth() + 1)) .replace('DD', pad(date.getDate())) })

全局注册的过滤器会在所有组件实例的模板环境中生效,不需要每个组件单独声明,直接{{ createdTime | formatDate('YYYY/MM/DD') }}就能用。不管组件里有没有写filters选项,模板都能识别全局过滤器。

不过全局注册有一个副作用:所有组件模板在编译时都有可能用到这个过滤器,哪怕实际只有五个页面用了,它会成为整个应用模板环境的一部分。项目里维护这些“看不见的全局API”需要格外注意命名,稍不留神就会和局部过滤器的名字撞车。

2.3 命名冲突与this指向:两个最容易被忽略的细节

命名冲突是一个真实存在且很坑的调度问题。当一个局部过滤器和全局过滤器同名时,局部过滤器会覆盖全局过滤器。这看起来像是“就近原则”很合理,但当你改了一个组件里的局部过滤器,却发现另一个页面里同名的全局过滤器行为完全不受影响时,排查起来就非常折磨。我的做法是统一加前缀,比如appGlobalMoney、appDate,在团队内形成一种开发约定,减少无意中的覆盖。

另一个更隐蔽的问题是this指向。Vue 文档明确指出,过滤器函数内部的this不指向组件实例。这意味着你不能在过滤器里直接this.someData获取组件数据。我见过有人试图在过滤器里根据组件的某个状态决定格式化格式,结果拿到undefined,又不知道问题在哪。如果确实需要根据组件状态来格式化,把它做成一个计算属性,或者直接在模板里调用组件方法,都比过滤器更合适。

3. 过滤器的真实应用场景拆解:从日期到金额到脱敏

3.1 格式化日期:最典型的复用场景

日期格式化是过滤器出现频率最高的使用场景,没有之一。后端接口老是返回2025-03-15T08:30:00.000Z这种 ISO 格式,页面上想显示成2025年03月15日 08:30,如果每次都手写new Date(...)再拼接,模板立马变成一坨难维护的逻辑。

我的常规写法是封装一个支持多占位符的日期过滤器:

filters: { formatDateTime(value, pattern = 'YYYY-MM-DD HH:mm') { if (!value) return '-' const date = new Date(value) if (Number.isNaN(date.getTime())) return '-' const pad = (n) => String(n).padStart(2, '0') const map = { YYYY: date.getFullYear(), MM: pad(date.getMonth() + 1), DD: pad(date.getDate()), HH: pad(date.getHours()), mm: pad(date.getMinutes()) } let result = pattern Object.keys(map).forEach((key) => { result = result.replace(new RegExp(key, 'g'), map[key]) }) return result } }

这个过滤器几乎覆盖了工作中的常见需求:默认显示2025-03-15 08:30,也可以传YYYY/MM/DD换成斜杠格式,传HH:mm单独显示时间。最关键的一点,过滤器内部捕获了所有非法日期并返回'-',避免了Invalid Date这种鬼东西直接渲染到页面上。

3.2 文本截断、大小写和脱敏处理

列表页常见的长标题截断、用户手机号脱敏、邮箱脱敏这类需求,用过滤器写起来非常有视觉上的优势。模板里一行搞定,完全不用在数据层多写一个方法。

filters: { truncate(value, length = 30, tail = '...') { if (!value) return '' if (String(value).length <= length) return value return String(value).slice(0, length) + tail }, maskPhone(value) { if (!value || value.length !== 11) return value return value.replace(/^(\d{3})\d{4}(\d{4})$/, '$1****$2') }, capitalize(value) { if (!value) return '' return String(value).charAt(0).toUpperCase() + String(value).slice(1) } }

配套使用效果很直观:

<p>{{ articleTitle | truncate(20) }}</p> <p>{{ userPhone | maskPhone }}</p> <p>{{ userName | capitalize }}</p>

注意脱敏逻辑必须放在过滤器中做“展示层脱敏”,而不是真的把用户手机号数据改掉。这个思路很重要:过滤器只影响展示,不污染原始数据。假如某天后端的手机号字段变成了脱敏格式,你只需要撤回模板里的过滤器就行,不用查数据到底是在哪一步被改掉的。

3.3 金额格式化和千分位输出

金额处理有个容易忽略的坑:后端返回的小数有可能是字符串,也有可能是null或undefined,前端过滤时必须对原始值做防御。只写value.toFixed(2)的过滤器,遇到null直接控制台报错。所以我在金额过滤器里第一件事就是类型检查。

filters: { formatMoney(value, symbol = '¥', decimals = 2) { if ([null, undefined, ''].includes(value)) return '--' const num = Number(value) if (Number.isNaN(num)) return '--' const negative = num < 0 const absNum = Math.abs(num) const formatted = absNum.toFixed(decimals).replace(/\B(?=(\d{3})+(?!\d))/g, ',') return (negative ? '-' : '') + symbol + formatted } }

这里的正则千分位写法是我长期在用的:

.replace(/\B(?=(\d{3})+(?!\d))/g, ',')

它会在非开头的每组 3 位数字前加逗号。处理负数时要先把绝对值格式化完再拼负号,否则负数小数点的千分位会错位。这个细节不亲手写一遍很难发现。

配合上一条规则,模板里写:

<p>{{ orderAmount | formatMoney('CNY ', 2) }}</p>

3.4 过滤器链式调用与多参数传递

过滤器之所以叫“管道”,就是因为它天然支持链式写法:

{{ userInputValue | trim | truncate(10) | capitalize }}

执行顺序是capitalize(truncate(trim(userInputValue), 10)),从左到右,前一个过滤器的输出会成为后一个过滤器的输入。链条上的每一个过滤器都应该保持职责单一,一个负责去空格,一个负责截断,一个负责首字母大写,解耦得清清楚楚。

这里要提醒一下性能问题:链式过长会导致每渲染一次模板就执行一整条函数链。如果数据量不大,两三个过滤器链完全没问题;但如果是在一个几百行的表格里每条数据都要跑五六个过滤器,性能开销就会明显上来了。这种场景就应该考虑把过滤链的逻辑合并成一个函数,或者直接用计算属性提前算好。

4. 过滤器的坑位大赏:排查记录与解决方案

4.1 过滤器里用了this,为什么拿不到数据

这个问题出现的频率几乎每周都能遇到。比如:

filters: { formatWithRate(value) { // this.rate 永远是 undefined return value * this.rate } }

要明确一点:Vue 在调用过滤器时并没有把组件实例绑定为函数的this。普通方法可以通过this访问data、computed、methods,但过滤器不行。原因是模板编译阶段生成的渲染函数里,过滤器是被当作独立函数引用的,而不是组件实例的方法。

如果确实需要根据组件状态来格式化,通常有三个替代方案:

方案写法适合场景
计算属性在组件内写computed把格式化逻辑放到计算属性上,返回已格式化结果数据依赖组件状态、需要缓存
模板方法调用{{ format(value, rate) }},format声明在methods里一次性的复杂转换
把状态作为参数传入过滤器`{{ valueformatWithRate(rate) }}`

我自己的习惯是:过滤器永远保持“纯函数”姿态,输入值加可选参数,输出展示值,绝不依赖外部状态。

4.2 过滤器 vs 计算属性 vs 方法,到底谁干谁的活

很多人把这三个方案混着用,结果模板里一会儿是管道符,一会儿是方法调用,一会儿又是computed,代码风格很飘。我自己做了个决策表:

维度过滤器计算属性方法
适用场景单一值、可复用、展示层格式化依赖多个响应式数据、结果需要被多处引用逻辑要访问组件上下文、需要事件触发
是否可以传参可以不行(只能定义时定死依赖)可以
是否有缓存无有,依赖不变就不重新计算无
this可用性不可用可用可用
Vue 3 是否支持不支持支持支持

这里的结论不是“过滤器一定比方法强”,而是职责不同。如果你需要的是标准化、可复用的展示转换,比如时间、金额、脱敏,优先考虑过滤器;如果转换要依赖组件里的其他状态,而且同一个结果在模板里被用了好几处,那计算属性是更好的选择,它天然带缓存,不会重复计算。方法则适合逻辑不纯、需要访问组件数据的场景。

4.3 Vue 3 项目迁移:过滤器被删了怎么办

这是一个躲不开的话题。Vue 3 移除过滤器后,老代码里所有管道符写法都会直接编译报错。迁移的常规做法是把过滤函数单独抽成一个工具模块或一个全局函数对象,在组件里显式引入或者解构使用。

例如把原有的一组过滤器集中定义在一个文件里:

// utils/filters.js export const formatDate = (value, pattern = 'YYYY-MM-DD') => { /* ... */ } export const formatMoney = (value, symbol = '¥', decimals = 2) => { /* ... */ } export const truncate = (value, length = 30, tail = '...') => { /* ... */ }

在 Vue 3 组件里用script setup的写法最轻量:

<script setup> import { formatDate, formatMoney } from '@/utils/filters' </script> <template> <div>{{ formatDate(createdAt) }}</div> <div>{{ formatMoney(price, '€', 2) }}</div> </template>

如果项目里同样逻辑在很多组件里用,还可以在app.config.globalProperties上挂载一个函数,模板里直接调用:

// main.js import { formatDate } from '@/utils/filters' app.config.globalProperties.$formatDate = formatDate

模板:

<p>{{ $formatDate(createdAt) }}</p>

这个方法本质上是把过滤器变成了全局属性方法,虽然失去了管道符的优雅,但胜在兼容性好,迁移成本低。

4.4 其他容易忽略的边界问题

第一个边界问题是v-model绑定值不能用过滤器。有人天真地以为可以写v-model="price | formatMoney",这在模板编译阶段就会直接报错。v-model期望绑定的是一个可写变量,而过滤器返回的是被处理后的展示值,写不回去。这种情况正确的做法是把原始数据存放在data里,显示的时候用过滤器,输入事件里再手动处理。

第二个边界问题是过滤器不应该直接修改传入的原始值。比如在过滤器里做了value.push(...)或者给对象加了属性,这会污染源数据,而且因为渲染是响应式追踪的,很容易触发意料之外的重新渲染循环。一律采用新建变量、返回新值的写法。

第三个边界问题是性能。过滤器是无缓存的,每次组件重新渲染,模板里所有过滤器都会重新执行一次。对于数据量小、渲染频率低的页面影响不大,但如果放在高频更新的表格列里,几百行、每行五六个过滤器,整体耗时会有明显变化。优化思路是合并链条、减少不必要的重复格式化,或者把整个列表的格式化逻辑改到计算属性里,利用缓存只算一次。

5. 实用的小总结:把过滤器用出长期价值

5.1 维护老项目时,我该怎么对待过滤器

如果你在维护一个 Vue 2 的老项目,我个人建议不要全面禁用过滤器,也不要强行把所有管道符改造成函数调用。理由很现实:你手头可能有上百个模板都用了过滤器,全量改造投入大、回归测试范围广、收益却不明显。更稳妥的方式是“新写代码不用过滤器,存量代码逐步迁移”。新做的功能如果只是在模板里做个简单的展示转换,直接写成方法或者计算属性;遇到需要改动的老模板,顺手把管道符改写一下。这样下来,在一到两个迭代周期里,过滤器的数量自然就降下来了。

5.2 最后分享一个我常用的过滤函数设计习惯

我写过滤器时习惯在同一个文件中把相关的格式化函数集中放好,但注册时只暴露真正必要的那几个。例如日期格式化和金额格式化写在一个format.js里,组件里局部注册时用...展开或者按需声明。全局注册只放跨页面复用频率极高的,避免每个组件模板都能看到一堆用不上的全局过滤器。

另外一个很实用的小技巧是:过滤器中给不合法值兜底。不管是null、undefined、空字符串还是非法日期,都统一返回一个安全的展示占位符(比如'-'或'--')。很多线上问题都不是格式化逻辑写错了,而是某个接口某个字段没根据预期返回,导致模板直接渲染出异常内容。过滤器这一层做好兜底,相当于给页面展示加了一道保险。这种防御性写法投入很小,但排障体验会好非常多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询