1. 从一次页面渲染异常说起:为什么需要过滤器
先讲个真实场景。某次我在维护一个后台管理系统,订单列表页需要把后端返回的状态码展示成可读文案。后端给的是1、2、3、4,页面上要显示待支付、已支付、已发货、已完成。一开始我直接在模板里写了三四个三元表达式嵌套,代码大概长这样:
<span>{{ order.status === 1 ? '待支付' : order.status === 2 ? '已支付' : order.status === 3 ? '已发货' : '已完成' }}</span>说实话,这种写法在首次上线时勉强能用,但后续需求一变更就崩了。产品经理说要把4改成已完成(可评价),同时新增一个5表示退款中。我翻遍模板找到那行三元表达式,改了五分钟才改对。后来仔细一琢磨,这种状态映射逻辑根本不应该写在模板里,应该用过滤器统一处理。
Vue 过滤器就是用来解决这类问题的。它的本质是一个可以在模板插值或v-bind表达式中使用的函数,接收原始数据,处理后再返回展示结果。比如上面的场景,用过滤器写就是:
<span>{{ order.status | statusText }}</span>filters: { statusText(value) { const map = { 1: '待支付', 2: '已支付', 3: '已发货', 4: '已完成(可评价)', 5: '退款中' }; return map[value] || '未知状态'; } }模板瞬间清爽了,状态和文案的映射关系也集中到了一个地方,后续改文案只动过滤器即可。
很多人觉得过滤器是个无关紧要的小功能,但实际项目里用好了,对模板可读性的提升是肉眼可见的。这篇文章我就把 Vue 过滤器的核心用法、注册方式、注意事项和踩坑经验一次性讲透,适合正在用 Vue 2 做项目、或者从 Vue 2 迁移 Vue 3 时被过滤器问题卡住的开发者参考。内容基于实际项目经验整理,不是官方文档的复读,重点放在“怎么用最合理”和“哪些坑必须避开”上。
2. Vue 过滤器的核心机制与适用场景
2.1 过滤器到底是什么
Vue 官方文档里的定义是:过滤器可以用在两个地方——双花括号插值和v-bind表达式。它由“管道符”|触发,前面的值会作为第一个参数传入过滤器函数。
<!-- 在双花括号插值中使用 --> <p>{{ message | capitalize }}</p> <!-- 在 v-bind 中使用 --> <div v-bind:id="rawId | formatId"></div>需要看穿的一点是,过滤器的执行时机是组件渲染过程中,而不是数据变更时。也就是说,过滤器在每次重新渲染时都会重新执行,这一点影响性能评估。如果一个过滤器内部做了很重的计算(比如大量数据遍历、复杂正则匹配),每次都跑一遍,性能问题就会积累。这一点下面细讲。
过滤器的设计思路和 Unix 管道命令很像。数据像水流一样,从左到右流动,每一级管道做一次处理。多重过滤器也是这么链式调用的:
<p>{{ text | lowercase | capitalize }}</p>text的值先传给lowercase,处理完的结果再传给capitalize。注意顺序是从左到右执行的,内容变换的方向别搞反了。
2.2 过滤器和计算属性的分工
初学者经常纠结一个问题:同样的数据处理,用computed和用filter都能实现,到底该选哪个?我自己的实践经验是:如果处理结果要绑定给多个属性、或者要参与其他计算逻辑,用computed;如果纯粹是模板展示环节的格式转换,用filter。
举个例子,商品价格可能是1299,页面要显示¥1,299.00。如果你在多个地方用到这个格式化结果——列表页、详情页、购物车——那我会倾向于写成过滤器,因为它是纯粹的展示格式化,不参与任何业务计算:
<p>{{ price | formatPrice }}</p>但如果你要根据价格是否超过 1000 来展示不同等级的优惠标签,那建议用computed来派生:
computed: { priceLevel() { if (this.price >= 1000) return 'high'; if (this.price >= 500) return 'mid'; return 'low'; } }核心判断标准就一条:这个数据变了之后,是会影响到多个视图的展示格式,还是会影响一段业务逻辑?格式问题交给过滤器,逻辑问题交给计算属性。
2.3 过滤器的能力边界
过滤器不是万能的。Vue 2 官方明确规定,过滤器只能用在插值和v-bind表达式中,不能用在v-model上,不能用在指令的指令参数里,也不能在 JS 逻辑代码中直接调用。如果你想在组件的methods里复用某个格式化逻辑,正确做法是把核心逻辑单独抽成一个函数,然后过滤器和 methods 都引用它:
// utils/format.js export function formatPrice(value) { return `¥${Number(value).toFixed(2)}`; } // 组件内 import { formatPrice } from '../utils/format'; filters: { formatPrice }, methods: { handleCheckout() { const displayPrice = formatPrice(this.price); } }单纯依赖过滤器做所有格式化,很容易遇到场景无法覆盖的时候。把逻辑抽离出来再复用,才是更健康的代码组织方式。
3. 从入门到实战:过滤器的注册与参数传递
3.1 全局过滤器注册
全局过滤器通过Vue.filter()注册,注册后所有组件实例都能使用,不需要在组件里重复定义。
// main.js Vue.filter('formatDate', (value, format = 'YYYY-MM-DD') => { if (!value) return ''; const date = new Date(value); const pad = n => String(n).padStart(2, '0'); if (format === 'YYYY-MM-DD') { return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`; } return date.toISOString(); });全局过滤器适合高频通用场景:日期格式化、金额千分位、字符串截断、文本清洗。我通常把全局过滤器集中放在一个filters/index.js文件里统一管理:
// filters/index.js import Vue from 'vue'; const filters = { formatDate: ..., formatPrice: ..., trimText: ..., upperFirst: ... }; Object.keys(filters).forEach(key => { Vue.filter(key, filters[key]); });在入口文件直接import './filters'即可,不用在组件里逐个引入,非常省事。
3.2 组件内过滤器注册
组件内过滤器只在当前组件中生效,注册方式是在组件的filters选项中定义。和全局过滤器相比,组件内过滤器的优势是隔离性好,不会污染全局命名空间。如果某个格式化逻辑只会在这一个组件里用到,就别注册全局的,写在组件内部更干净:
export default { data() { ... }, filters: { fileSize(bytes) { if (!bytes) return '0 B'; const units = ['B', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(1024)); return `${(bytes / Math.pow(1024, i)).toFixed(2)} ${units[i]}`; } } }文件大小格式化、性别数字转文字、枚举状态转标签类名这种场景,特别适合组件内过滤器。它们往往和当前业务强相关,复用性不高,全局注册反而增加认知负担。
3.3 参数传递:过滤器的高级用法
过滤器的第一个参数永远是管道符左边的值。如果要传递额外参数,直接写在过滤器名后面,用括号传参即可。
<p>{{ amount | toFixed(2) }}</p> <p>{{ content | sliceText(0, 20, '...') }}</p>对应的过滤器函数定义里,value是管道符号前的数据,后面的参数依次填入:
filters: { toFixed(value, digits) { return Number(value).toFixed(digits); }, sliceText(value, start, end, ellipsis = '...') { if (!value) return ''; if (value.length <= end) return value; return value.slice(start, end) + ellipsis; } }多条参数管道组合使用也很常见,但要注意别过度封装。我见过有人写出一行五六个过滤器串在一起的模板,排查问题简直噩梦:
<p>{{ rawData | filterA | filterB(1, 2) | filterC | filterD }}</p>这种代码第一时间看不出每一级到底做了什么,出错时只能一级一级去查。建议一个模板里的过滤器链控制在两三个以内,如果逻辑确实复杂,老老实实抽到computed里吧。
3.4 过滤器中的this指向陷阱
一个高频踩坑点:过滤器函数内部的this不是当前组件实例。Vue 2 源码中过滤器函数是直接调用的,不绑定组件上下文。所以你在过滤器里写this.someData,拿不到想要的东西。
filters: { formatWithConfig(value) { // 这里 this 不是组件实例! return `${this.prefix}${value}`; } }如果你确实需要在过滤器中依赖组件上下文的数据(比如根据当前用户权限决定展示格式),有几个替代方案:
- 把依赖数据作为参数传入过滤器
- 模板中直接传
this.xxx对应的变量(注意模板中可直接用prefix) - 改用
computed或methods
正确写法:
<p>{{ content | formatWithConfig }}</p>配合:
filters: { formatWithConfig(value, prefix) { return `${prefix}${value}`; } }模板里改成:
<p>{{ content | formatWithConfig(prefix) }}</p>这样数据从外部传入,过滤器保持纯函数特性,测试和维护都更舒服。
4. 过滤器、串联过滤器的链式调用与性能思考
4.1 链式调用的执行顺序
首先把执行顺序彻底说透。对于:
<p>{{ text | filterA | filterB }}</p>执行流程是:
1. text 作为参数调用 filterA,得到 resultA 2. resultA 作为参数调用 filterB,得到 resultB 3. resultB 输出到模板顺序是从左到右。有些人习惯反过来理解,看到管道符就想到后面的数据处理前面的,这是误区。实际验证一下最直接:写两个过滤器,一个加字符串前缀PRE_,一个添加后缀_SUF,顺序调换后结果完全不同。
4.2 过滤器性能开销的真实情况
过滤器在渲染期间执行,意味着组件任何响应式数据变化导致重新渲染,过滤器全部重新跑一遍。哪怕过滤器依赖的数据完全没变,也一样重新执行。这在列表数据很大的场景下会产生明显卡顿。
我在一个近千行的表格页里踩过这个坑。每行五六列都用了全局过滤器做格式化,过滤器中还有正则匹配。当表格数据更新时,整页渲染耗时肉眼可见地增加。后来优化方案是:把格式化逻辑迁移到computed里,在数据进入表格前就处理好:
computed: { formattedRows() { return this.rows.map(row => ({ ...row, orderStatusText: this.statusText(row.orderStatus), priceText: this.formatPrice(row.price), dateText: this.formatDate(row.createdAt) })); } }循环里一次性处理好所有格式化,模板里只做简单绑定,渲染性能和可追踪性都提升不少。过滤器更推荐用在单一、轻量格式化的场景,复杂数据处理交给computed或后端完成。
4.3 数据清洗:过滤器的隐藏用法
除了展示格式化,过滤器还可以承担一部分数据清洗工作。比如接口返回的富文本里经常带着乱七八糟的标签,直接v-html有 XSS 风险,配合一个清洗过滤器会好很多:
<div v-html="content | sanitizeHtml"></div>sanitizeHtml(value) { if (!value) return ''; // 这里建议用成熟库替代简单正则,比如 DOMPurify return value.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, ''); }但必须强调:过滤器不能替代真正的安全防护。过滤器的输出是在客户端做展示处理,真正的 XSS 防护必须由后端在输入阶段做严格校验,前端过滤器只是兜底措施。尤其是涉及v-html的场景,不要只用简单的字符串替换就完事,否则等于把安全防线漏了个大洞。
这种“清洗型过滤器”用好了体验很好。比如用户输入的手机号展示时需要脱敏:
<p>{{ phone | maskPhone }}</p>maskPhone(value) { if (!value || value.length < 7) return value; return value.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2'); }本质上,过滤器就是一个“展示前管道处理器”,做格式化、脱敏、清洗都是它的适合领地。
5. 必须避开的五个大坑与排除实践
5.1 滤器名称冲突的坑
全局过滤器和组件内过滤器允许同名,同名时优先使用组件内过滤器。这个规则本身不复杂,但多人协作时很容易踩坑。比如说全局定义了一个formatDate,某个组件内又定义了同名但逻辑不同的formatDate,排查 bug 时极难发现——你以为是全局逻辑,实际被组件覆盖了。
规范建议:组件内过滤器命名时,前缀加上组件名或业务前缀,例如orderStatusText、userPhoneMask,尽量避免和全局过滤器同名。同时,新增过滤器之前先在filters/index.js里扫一眼,确认不存在同名情况。
5.2 过滤器里改传入参数的坑
JavaScript 中对象和数组是引用传递。如果你在过滤器里对传入的对象参数做了修改,会直接改动原始数据。
filters: { addLabel(item) { item.label = '新标签'; // 这里改了原始对象! return item; } }模板渲染时原始对象被污染,可能导致其他依赖该数据的地方出现难以预料的 bug。过滤器应当是纯函数——相同输入永远是相同输出,且不修改外部状态。需要返回修改后的值,先拷贝再操作:
filters: { addLabel(item) { const copy = { ...item, label: '新标签' }; return copy; } }5.3 日期格式化时区问题
这是国际化项目里常见的坑。后端返回时间戳或 ISO 字符串,前端直接new Date(value)后格式化,会遇到时区偏移的问题。比如某后端返回2024-06-01T16:00:00.000Z(UTC 时间),在 UTC+8 时区下本地显示是2024-06-02 00:00:00。直接按getFullYear()取,取到的是本地时区的日期,可能是第二天。
处理方案:过滤器中明确指定使用哪个时区和格式,或者后端统一返回已经处理好的本地时间字符串。如果必须前端处理,最好借助成熟的日期库(注意:项目需自行评估依赖风险和许可证,适合场景再引入),自己徒手实现 UTC 转本地且处理夏令时之类的问题,代码会变得非常不可控。
5.4 Vue 3 中过滤器被移除的迁移问题
如果你在做 Vue 3 升级,这里有个必须提前知道的点:Vue 3 官方移除了过滤器。原因是从设计上看,过滤器不够“显性”,且已有计算属性和方法可以覆盖它的应用场景。
迁移方案有两个:
方案一:替换为方法(methods)
<p>{{ formatDate(date) }}</p>methods: { formatDate(value) { // ... } }方案二:替换为计算属性(computed)
这种方法适合格式化逻辑依赖响应式数据且会被多处引用的场景:
computed: { formattedDate() { return this.formatDate(this.date); } }如果项目里用了大量过滤器,我建议先写个扫描脚本把| filterName全部找出来,逐个替换。尤其注意:在 Vue 3 中模板里存在|管道符号,不会报错,但会被当作非法表达式直接编译失败。所以升级前做一次全局检索很关键。
还有一个过渡用的思路,有的团队会在 Vue 3 项目中通过自定义指令或混入来模拟过滤器,但我个人不建议这么做——绕了一圈只是维持旧习惯,最终还是要学会用方法或计算属性思维来组织代码。直接一步到位迁移到 methods/computed 更省钱。
5.5 忘记注册直接使用的坑
模板里写了{{ value | myFilter }},但忘了在filters里定义,或者全局注册里没写上。报错信息通常是:
Failed to resolve filter: myFilter排查思路很简单:先在源码里全局搜myFilter,确认是否存在于filters/index.js或组件filters选项里。如果是按需加载的模块化结构,还要确认当前组件是否真的 import 了对应文件。这种问题排查起来不难,但每次出现都会浪费不少时间,最好在代码提交前做一次模板过滤器的完整检索。
6. 一套完整的序化方案:从真实项目中提炼
6.1 场景回顾
我曾经参与维护过一个中等规模的后台管理前端,技术栈是 Vue 2 + Element UI。页面涉及订单、用户、商品、营销等多个模块,格式化需求非常多:状态码转文案、金额千分位、时间戳转日期、手机号脱敏、文本截断、枚举转标签颜色等等。早期代码里每个组件都零散地定义着自己的格式化方法,同一个“创建时间格式化”逻辑在五个组件里复制了五份。后来一次重构,我们把这块系统性地整理了一遍。
6.2 分层设计思路
最终方案分三层:
第一层:全局通用格式化逻辑(工具函数层)
放在utils/format.js,是纯函数集合,不依赖 Vue 任何特性,方便在任意 JS 文件中调用和单元测试:
// utils/format.js export function formatPrice(value, currency = '¥') { if (value === null || value === undefined) return `--`; return `${currency}${Number(value).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; } export function formatDateTime(value) { if (!value) return ''; const d = new Date(value); // ... } export function maskPhone(phone) { // ... }第二层:全局过滤器注册层
把工具函数包装成过滤器,在filters/index.js统一注册。工具函数和过滤器一一对应,函数名保持统一,避免隐藏映射:
// filters/index.js import Vue from 'vue'; import * as formatters from '../utils/format'; const filterMap = { formatPrice: formatters.formatPrice, formatDateTime: formatters.formatDateTime, maskPhone: formatters.maskPhone }; Object.keys(filterMap).forEach(name => { Vue.filter(name, filterMap[name]); });第三层:组件内局部过滤器
业务强绑定的格式化逻辑(比如“订单状态徽标类名、用户类型标签文案”这类只在该模块使用的),写在组件内filters中,同样建议从工具函数引入,而不是直接在过滤器里写复杂逻辑:
// 组件内 import { formatDateTime } from '../utils/format'; export default { filters: { orderTime: formatDateTime, statusBadge(status) { const map = { pending: 'warning', paid: 'success', shipped: 'primary' }; return map[status] || 'info'; } } };6.3 模板中的调用规范
我们对设计规范和命名规范的约束如下:
- 过滤器命名一律采用驼峰式,语义清晰,避免缩写如
fmt、t之类的。 - 串联过滤器超过 2 个,强制抽到
computed。 - 过滤器内部禁止修改传入参数。
- 过滤器内部禁止访问
this,依赖外部状态时改用参数传入。 - 新增加过滤器必须同步补充对应的工具函数和基本注释。
模板效果:
<template> <div> <p>成交金额:{{ order.amount | formatPrice }}</p> <p>下单时间:{{ order.createdAt | formatDateTime }}</p> <p>联系手机:{{ order.phone | maskPhone }}</p> <el-tag :type="order.status | statusBadge"> {{ order.status | statusText }} </el-tag> </div> </template>这样模板核心只表达“展示什么”,不再关心“怎么转换”,维护时只需要在一个地方修改。
6.4 对既有项目的渐进式改造
做完这套整理后,我总结出一个对旧项目友好的迁移节奏:
- 先建立
utils/format.js,把项目中重复的格式化逻辑收敛成纯函数。 - 再建立
filters/index.js,用最常用的几个全局过滤器替换模板中高频出现的格式化调用。 - 最后逐个页面清理组件内重复的格式化代码,能复用全局的就不复制粘贴。
这个过程不需要大版本重构,每走一步都能正常上线,风险逐个隔离。对于已经用了很久的 Vue 2 项目来说,这是最平缓的演进方式。
7. 做一个决策可行的总结:过滤器用不用、怎么用
聊了这么多,最后梳理一下我个人的核心判断,方便你在实际项目里快速做决策。
过滤器适合解决的场景:
- 简单格式转换:日期、金额、文本截断。
- 高频内容脱敏:手机号、身份证、邮箱。
- 枚举状态码转展示文案。
- 模板插值和
v-bind上的小型数据清洗。
不建议用过滤器的场景:
- 需要依赖组件实例数据的逻辑(用
computed)。 - 逻辑复杂、多个步骤处理的流程(该方法不阻塞的情况下,但用函式方法更清晰)。
- 数据需要被多处使用的场景(用
computed缓存)。 - 新写的 Vue 3 项目(直接用
methods或computed)。
如果你还在维护 Vue 2 项目,过滤器作为一种轻量级模板工具,用得恰当能大幅度提升代码可读性。如果你正准备转向 Vue 3,也不用过度遗憾——把原来写在过滤器里的逻辑迁到methods/computed中,代码表达实际上更直白。我自己做了几次替换之后反而觉得,强制不用过滤器,逼着你把格式化逻辑提到组件层,虽然之前需要改动的地方多一些,但整体代码结构确实更加清晰可测了。
最后分享一个小习惯:我现在每写一个过滤器,都会顺手在旁边写两三个测试用例。过滤器是纯函数,测试成本极低,效果却立竿见影。遇到时区问题、边界值为空之类的情况,写完测试的时候已经能提前排掉一半的坑。如果你还没有这个习惯,下次写过滤器时尝试加上,应该能感受到明显的好处。