资讯动态

Vue过滤器详解:模板格式化、边界与Vue3迁移替代方案

发布时间:2026/10/16 8:39:44 来源:尧图企业网站定制
做了几年Vue项目从Vue 2用到Vue 3要说模板里最容易被忽略又偶尔会被怀念的小功能过滤器绝对算一个。很多人写模板格式化的时候第一反应就是用一个管道符加函数名比如{{ price | formatMoney }}确实很方便。但方便归方便它有很多边界和限制尤其到了Vue 3之后直接就被移除了这中间有不少值得掰扯清楚的东西。这篇文章想聊的就是过滤器的完整使用规则、它和计算属性/方法之间的边界以及我在实际项目里踩过的几个坑。内容以Vue 2.x为基准展开后面单独讲Vue 3的迁移替代方案。不管你是刚接触过滤器的新手还是正在从Vue 2往Vue 3迁移的老开发应该都能从这里找到一些能直接落地的经验和思路。1. 为什么需要过滤器以及它和计算属性、方法的边界在哪1.1 从实际场景看过滤器的定位先说一个最常见的业务画面后端接口返回的订单金额是分前端要显示成“¥1,299.00”返回的时间戳要显示成“2024-06-18 14:30”用户的手机号要脱敏成“138****6666”。这些都属于典型的数据展示格式化。如果不用过滤器你可能会这样写template div{{ formatMoney(order.amount) }}/div /template script export default { methods: { formatMoney(value) { return ¥ (value / 100).toFixed(2); } } } /script这样写没有问题模板里的formatMoney(order.amount)可读性也还可以。但一旦要嵌套转换比如“先脱敏再改成大写”模板里的方法调用就会变成formatUpper(maskPhone(user.phone))读起来就吃力了。过滤器的写法是{{ user.phone | maskPhone | toUpper }}数据从左往右流动每一步做什么看管道符就行。所以过滤器的定位就是一句话它只负责模板里数据的展示层转换不负责业务逻辑更不修改源数据。适合的场景包括日期格式化、金额处理、文本截断、大小写转换、枚举值映射、敏感信息脱敏等。1.2 边界能用计算属性解决就别用过滤器过滤器虽然好用但它不是万能的。我见过不少同学在过滤器里写复杂逻辑最后维护起来非常痛苦。这里我给一个自己一直在用的判断标准如果格式化结果只在一处模板里使用而且逻辑简单两三行用过滤器完全没问题。如果格式化结果依赖组件里的多个响应式数据或者需要缓存复用优先用计算属性。如果格式化逻辑要在多个组件之间共享优先抽成独立工具函数组件内部再封装。如果格式化过程需要访问组件实例的data、computed、methods直接放弃过滤器换方法或计算属性。计算属性最大的优势是有依赖缓存只有依赖的响应式数据变化时才会重新计算。过滤器则每次渲染都会重新执行哪怕输入值没变。这一点在大列表渲染下会有明显性能差异后面会专门说到。2. 过滤器注册与作用域全局 Vue.filter 和组件内 filters 该如何选择2.1 全局注册的用法、挂载时机和常见误区全局过滤器通过Vue.filter(id, factory)注册一般放在main.js里Vue.filter(formatMoney, function(value, symbol ¥) { if (value null || value undefined) return --; return symbol (value / 100).toFixed(2); });注册之后任意组件的模板里都能直接用{{ amount | formatMoney }}。这个API在Vue 2里还有个不常见的小用法如果只传第一个参数Vue.filter(formatMoney)返回的是已注册的过滤器函数可以用来在非模板环境里手动调用比如在路由守卫或业务模块里复用格式化逻辑。但这个用法我并不推荐在业务代码里频繁使用原因有两个。第一全局过滤器的注册时机有讲究必须在new Vue()之前完成否则组件渲染时找不到过滤器会直接报错第二全局过滤器本质上是挂在一个全局命名空间上的项目越做越大命名冲突的概率就越高。2.2 局部注册组件内 filters 的优先级规则局部过滤器写在组件的filters选项里template div{{ status | statusText }}/div /template script export default { filters: { statusText(value) { const map { 1: 待支付, 2: 已支付, 3: 已退款 }; return map[value] || 未知状态; } } } /script这里有一个特别容易忽略的优先级规则如果组件内定义了和全局同名的过滤器局部过滤器会覆盖全局过滤器。模板解析时先找组件自身filters找不到再找全局Vue.filter注册表。正是因为这条规则我在实际项目里吃过一次亏。当时在某个页面里为了调试临时定义了一个叫formatDate的局部过滤器覆盖了全局的formatDate结果页面上所有日期都变成了调试输出格式排查了很久才发现是覆盖问题。所以这里建议组件内局部过滤器尽量不要和全局过滤器重名如果非要重名一定要明确知道覆盖行为避免其他同事被误导。2.3 我的选型标准可复用性 vs 局部耦合全局过滤器适合那种横跨多个模块、格式化规则完全统一的场景比如金额、日期、脱敏。局部过滤器适合那种只在当前组件内有效的展示转换比如把某个状态码映射成中文文案。从维护角度说我现在的习惯是优先写一个utils/format.js模块把格式化函数全部以纯函数形式导出export function formatMoney(value, symbol ¥) { if (value null || value undefined) return --; return symbol (value / 100).toFixed(2); } export function formatDate(value, fmt YYYY-MM-DD) { // 日期格式化实现 } export function maskPhone(value) { return String(value).replace(/^(\d{3})\d{4}(\d{4})$/, $1****$2); }然后在需要的组件里按需引入本地包一层过滤器或者直接模板里调用。这样做的好处是工具函数是纯粹的JavaScript单元测试好写Vue 3迁移时也不用改业务逻辑。3. 模板语法里的真实规则管道符、参数传递、链式调用3.1 管道符只能在插值表达式和 v-bind 里用别再乱试过滤器的使用位置在Vue官方文档里有明确说明只能用于双大括号插值和v-bind表达式从Vue 2.1.0开始支持v-bind。例如!-- 插值表达式 -- p{{ message | capitalize }}/p !-- v-bind -- div :idrawId | formatId/div注意不能在v-model、v-for、事件绑定等指令里用过滤器。v-model需要的是双向绑定的值过滤器是只读转换语义上就冲突。v-for的迭代语法解析也不支持管道符表达式。可能有人会问“指令内部不能写过滤器那我在v-for循环里想格式化每一项怎么办”办法很简单有两种一种是在v-for作用域内直接用一个方法调用span{{ item.amount | formatMoney }}/span这种其实可以因为它本质还是插值表达式另一种是把格式化逻辑放到提交渲染数据之前处理也就是在computed里先map一遍。3.2 参数传递的“源数据永远在第一位”规则过滤器函数接收的第一个参数永远是管道符左边的值也就是源数据。后面的参数从模板传入。举个例子{{ price | formatMoney($, 2) }}对应的过滤器函数function formatMoney(value, symbol, decimals) { // value 来自管道符左边的 price // symbol 是 $ // decimals 是 2 }这是一个容易搞反的点。很多人第一次用的时候以为第一个参数是模板里传进来的’$’实际上是源数据排在最前面。如果源数据不是管道符左边第一个表达式而是整个表达式的结果呢比如{{ 合计: total | formatMoney }}此时过滤器接收的value是合计: total这个字符串拼接后的结果而不是total本身。所以千万不要把复杂表达式和过滤器混合使用除非你很清楚整个表达式会被当成一个整体作为源值传入。3.3 链式调用的执行顺序与性能代价过滤器支持链式调用比如{{ message | filterA | filterB }}。执行顺序是先执行filterA(message)得到结果再把结果作为filterB的输入继续执行。Vue.filter(filterA, function(value) { return value A; }); Vue.filter(filterB, function(value) { return value B; }); // message 为 hello最终得到 hello A B链式调用确实能让模板读起来很顺畅但代价是每次渲染都会完整执行整条链。比如一个列表有100行每行模板里有3个过滤器链一次渲染就会有300次函数调用。虽然大多数过滤器的逻辑很简单不至于造成性能瓶颈但在数据量大的表格、长列表里这个开销值得警惕。我自己的做法是链路超过两个过滤器的时候优先封装成一个独立方法或计算属性避免模板里写太长、太复杂的转换链。4. 过滤器实际使用中那些真正需要避开的坑包含排查思路4.1 过滤器里的 this 无法拿到组件实例现场还原与排查这是过滤器最大的一个限制。在Vue 2.x里过滤器函数在执行时不会被绑定到组件实例上它的this不是vm而是undefined严格模式下。如果你在过滤器里尝试访问this.userName、this.someData控制台会直接报错或者拿到undefined。我印象很深的一次排查经历有个页面里的表格列需要根据当前组件里的一个开关状态来决定格式化方式当时同事在过滤器里写了filters: { formatValue(value) { if (this.someFlag) { return value.toUpperCase(); } return value; } }结果页面一加载所有列都变成了小写完全不理会someFlag的值。查了半天才发现this.someFlag压根拿不到this就是undefined走了else分支。后来改成在模板里传入参数td{{ row.value | formatValue(someFlag) }}/tdfilters: { formatValue(value, flag) { return flag ? value.toUpperCase() : value; } }这个坑的排查思路其实很简单先确认过滤器函数里是否存在对this的访问。如果存在直接改成参数传递或者干脆换计算属性。千万不要试图在过滤器里通过闭包去拿组件实例那会让代码的依赖关系变得非常隐晦别人根本看不懂这个过滤器依赖了什么外部状态。4.2 别在过滤器内部改动原数据纯函数约束过滤器应该是纯函数同样的输入永远得到同样的输出并且不修改传入的参数、不产生副作用。我在实际项目里见过有人在过滤器里直接改对象的属性filters: { addTag(item) { item.tag 已处理; return item; } }看起来好像“顺手”把数据改了但这样做有几个问题。第一如果item是来自Vuex或组件的响应式数据直接修改属性会触发响应式更新可能导致渲染循环或意外的状态变化。第二过滤器被调用的次数不一定和渲染次数一一对应依赖过滤器去修改数据会让整个数据流的可预测性变得很差。正确的做法是返回一个新值。比如上面这个例子改成filters: { withTag(item) { return Object.assign({}, item, { tag: 已处理 }); } }这样源数据不会被污染渲染出来的还是你想要的展示结果。4.3 过滤器不会按需缓存列表渲染时的多次执行风险计算属性有缓存机制过滤器没有。每次组件的渲染函数执行时模板里的过滤器都会被重新调用一遍。换句话说即使过滤器的输入值没有变化只要组件发生重新渲染过滤器就会重新计算。这在大列表场景下特别明显。举个例子一个组件里有searchText响应式数据用户每次输入搜索关键词整个列表区域都会重新渲染列表里每一行的日期、金额过滤器全部会重新执行一遍。如果每个过滤器内部还做了正则匹配、日期解析之类的操作几百行列表就能明显感觉到卡顿。针对这个问题建议这样处理列表类数据在computed里先把整个列表的格式化结果算好模板里只取格式化后的字段。或者用子组件隔离列表项每个子组件只渲染自己那一行减少无谓的全量渲染。这两种方案都比依赖过滤器更可控也是我在项目里实际验证过有效的做法。4.4 全局过滤器污染隐患与命名覆盖问题前面提到全局过滤器可能会被局部过滤器覆盖这里再补充一个更隐蔽的问题多人协作时如果大家都在main.js里不断追加全局过滤器很容易出现“我在组件里定义的statusText和你定义的全局statusText完全不是一个含义”的情况但因为局部覆盖全局的规则存在不同组件里同一个过滤器的行为可能完全不一致。我曾经在一个中大型项目里见过一个全局过滤器叫status在A页面显示的是中文状态在B页面由于局部覆盖变成了带颜色的标签HTML在C页面又变成了英文缩写。排查起来非常费劲因为模板里都是{{ status | status }}这种写法代码里根本看不出它到底做什么。所以我的建议是全局过滤器的数量要克制命名要带业务前缀比如formatMoney、formatDate这种通用语义的可以全局保留状态映射类的尽量局部化或直接抽成工具函数。5. Vue 3 已经移除过滤器我正在用的替代方案5.1 事实与迁移判断从 Vue 2 过渡时怎么评估存量代码Vue 3的官方发布说明里明确写了一个破坏性变更过滤器功能被移除了。这意味着你在Vue 2项目里写的{{ message | filterName }}直接迁移到Vue 3的模板里会解析出错因为管道符在JavaScript表达式里是位或运算符Vue 3不再对模板中的|做特殊处理。如果你的项目正在从Vue 2升级到Vue 3存量过滤器代码的评估建议分三步走先全局搜一下Vue.filter(和filters: {把使用范围摸清楚。按用途分类纯格式化、状态映射、复杂逻辑。纯格式化和状态映射的直接替换成工具函数或计算属性复杂逻辑本来就不该用过滤器正好借机重构。5.2 可复用格式化函数模块 模板调用最接近原写法的迁移路径最接近原写法的迁移方式是把过滤器函数原样保存成普通函数模板里用方法调用的写法替代管道符。比如原来{{ amount | formatMoney }}迁移后{{ formatMoney(amount) }}对应的函数定义import { formatMoney } from /utils/format; export default { setup() { return { formatMoney }; } }这种写法改动量最小逻辑保持不动只是把管道符换成了函数调用。如果格式化函数不需要访问组件状态这是一个非常稳妥的过渡方案。需要特别提醒的是函数调用和过滤器的直觉刚好相反过滤器的写法是数据在左边、函数在右边函数调用是函数在左边、数据在括号里。迁移时不要盲目全局替换最好先在几个典型页面确认渲染结果没有差异。5.3 使用 computed 保留响应式的格式化结果如果格式化结果依赖多个响应式数据或者希望缓存结果用computed是最合适的。以Composition API为例template div{{ displayPrice }}/div /template script import { computed } from vue; import { formatMoney } from /utils/format; export default { setup(props) { const displayPrice computed(() { return formatMoney(props.order.amount); }); return { displayPrice }; } } /script这样一来只有当props.order.amount变化时才会重新格式化更重要的是格式化逻辑不再藏在模板里测试和调试都方便得多。对于表格里的批量格式化直接在computed里对整个列表做map渲染时直接读取格式化后的字段性能表现也会更好。5.4 借助 globalProperties 实现“伪过滤器”的注意事项Vue 3里可以用app.config.globalProperties注册全局方法让所有组件的模板都能直接访问// main.js import { createApp } from vue; import App from ./App.vue; const app createApp(App); app.config.globalProperties.$formatMoney (value) { if (value null || value undefined) return --; return ¥ (value / 100).toFixed(2); }; app.mount(#app);然后在模板里使用template div{{ $formatMoney(amount) }}/div /template这个方案在模板里写起来很接近原来的全局过滤器体验但我必须提醒几个问题globalProperties上的方法在模板里能直接访问但在setup里不能直接用this.$formatMoney需要先const instance getCurrentInstance()再取值或者干脆在setup里导入工具函数。globalProperties的命名空间是全局的换一个项目或换一个人接手可能根本不知道这个方法来自哪里。所以建议只在业务统一格式化规则时使用而且要写在明确的模块文件里不要散落在main.js中。使用这种方式时如果将来再做服务端渲染或单元测试需要额外处理全局对象注入问题维护成本会高于独立工具函数。综合来看我个人现在最推荐的做法还是把格式化逻辑全部收敛到utils/format.js这样的纯函数模块里组件内的单次格式化用方法组件间共享的格式化用独立函数涉及复杂依赖的用computed。这种写法无论在Vue 2还是Vue 3下都成立不会因为框架升级而推倒重来。最后再分享一个小习惯不管用哪种方案格式化函数的边界值处理一定要做尤其是null、undefined、空字符串、负数、超大数字这些情况。因为后端数据永远比你想象的更不可控一个稳定的格式化函数不会因为某天某个字段返回了个null就让整页白屏。

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

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

免费获取报价 →
↑