Vue过滤器详解:从Vue 2应用到Vue 3迁移
上周有个同学去面一家做中后台系统的公司回来跟我吐槽面试官问了一道“Vue 过滤器”他满脑子都是布隆过滤器硬生生把话题扯到哈希函数上去了结果自然是答偏了。这个场景我太熟了。Vue 过滤器filter是 Vue 2 里一个专做文本格式化的特性用管道符直接写在模板里专门解决“同一个数据要在多个地方显示成不同格式”的需求。今天这篇就把这道面试题彻底拆透过滤器到底是什么、能用在哪些场景、Vue 3 里为什么没了、以及项目里怎么优雅替代它。适合正在刷 Vue 面试题的前端同学也适合在维护 Vue 2 老项目、想把格式化代码理顺的同行。1. 从一道面试题说起Vue 过滤器到底是什么1.1 面试官问这道题到底想考什么很多同学一听到“过滤器”就开始背定义结果面试官追问两句就露馅。其实这道题真正考察的点不是“你会不会写一个 filter”而是你有没有把 Vue 的模板语法体系理解透。它至少能够拆出四层含义第一你是否知道过滤器本质上是一个函数只是被 Vue 在模板渲染时以管道符的语法调用第二你是否知道它在 Vue 2 和 Vue 3 里的命运完全不同第三你是否能说清楚它和计算属性、methods 的边界第四你是否踩过“过滤器不能访问 this”这种坑。这四层你要是都能接住这道题基本就是送分题。反之只背一个{{ message | capitalize }}的语法面试官一追问题就露怯因为他想看你到底是在背 API 还是在写业务。换句话说这道题考的是你对“模板表达能力边界”的把握。1.2 管道的底层逻辑数据流过一道道工序Vue 2 官方对过滤器的定义非常朴素用于对文本进行格式化。它借用了 Unix 管道的思想数据像流水线上的半成品经过一个函数处理后输出再交给下一个函数继续处理。模板里的写法长这样{{ message | capitalize }}这里的|就是管道符左边是要被处理的数据右边是处理函数的名字。渲染时Vue 会把message作为第一个参数传给capitalize函数再把函数的返回值放到插值位置显示出来。所以过滤器本质上就是“接受一个值返回一个新值”的纯函数只不过 Vue 给它在模板里开了一条专用的语法快车道。不止插值表达式能用v-bind的动态绑定里也支持过滤器从 Vue 2.1.0 开始就可以这样写div :data-rolerole | roleText/div这个冷门知识点面试里偶尔会考但实际项目用得不多。大多数时候过滤器出现在{{ }}里负责把后端返回的原始数据变成用户能看懂的文案。1.3 全局注册与局部注册过滤器也有作用域过滤器可以全局注册也可以组件内局部注册。全局注册适合那些每个页面都要用的通用格式化逻辑像金额、日期、脱敏这一类做完一次注册到处可用Vue.filter(formatDate, function (value) { if (!value) return -- const d new Date(value) return ${d.getFullYear()}-${d.getMonth() 1}-${d.getDate()} })局部注册则是写在组件选项的filters字段里只对当前组件模板生效export default { filters: { uppercase(value) { return value.toUpperCase() } } }两者优先级有个规则局部过滤器和全局过滤器重名时局部优先。因为 Vue 在解析过滤器名时会先在组件自己的filters里找找不到才去全局注册表里查。这个细节看起来不起眼但实际项目里全局过滤器一多不注意命名规范很容易出冲突。2. 过滤器的主要使用场景与实战案例2.1 文本类格式化首字母大写、截断、脱敏文本格式化是过滤器最直观的应用场景。比如用户昵称需要首字母大写或者列表里超过一定长度的标题要加省略号这些逻辑如果直接写在模板表达式里会很难看写在methods里调用又显得绕过滤器恰好能把“格式化”这件事以声明式的方式留在模板层。一个常用的截断过滤器可以这样写Vue.filter(truncate, function (value, length 30, tail ...) { if (!value || typeof value ! string) return if (value.length length) return value return value.slice(0, length) tail })模板里一行搞定{{ article.title | truncate(20, …) }}脱敏也是高频需求。后台系统里最常见的手机号脱敏、身份证号打码、邮箱打码用过滤器做非常顺手。手机号脱敏的经典写法是正则替换Vue.filter(mobileMask, function (value) { if (!value || typeof value ! string) return -- return value.replace(/^(\d{3})\d{4}(\d{4})$/, $1****$2) })这里有个容易被忽略的细节过滤器内部一定要做空值兜底。因为过滤器拿到的第一个参数就是模板里传进来的原始数据如果后端返回null或undefined不做判断就直接操作字符串会当场炸掉。我在项目里见过不止一次因为value.replace在空值上报错导致的线上事故所以所有过滤器函数第一行习惯性写空值判断这比任何优化都重要。2.2 数值与单位格式化金额、千分位、字节换算中后台项目里金额和字节的格式化几乎绕不开。后端的金额经常以“分”为单位存储前端显示时要转成“元”并且加上千分位文件大小从后端拿到的是一串字节数展示时要转成 KB、MB、GB。这些逻辑有三个共同的槽点每个页面都要用、格式化规则完全一致、但散落在不同组件的methods里各有各的写法。用过滤器统一收敛后代码会干净很多。举个例子一个支持自定义货币符号和保留位数的金额过滤器Vue.filter(currency, function (value, symbol ¥, digits 2) { if (value null || value undefined || value ) return -- const num Number(value) if (isNaN(num)) return value const amount (num / 100).toFixed(digits) const [intPart, decimalPart] amount.split(.) const formattedInt intPart.replace(/\B(?(\d{3})(?!\d))/g, ,) return symbol formattedInt (decimalPart ? . decimalPart : ) })模板里传参使用p订单金额{{ order.amount | currency(, 2) }}/p字节换算的过滤器同样简单直观Vue.filter(fileSize, function (bytes) { if (bytes 0) return 0 B if (!bytes) return -- const k 1024 const sizes [B, KB, MB, GB, TB] const i Math.floor(Math.log(bytes) / Math.log(k)) return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) sizes[i] })注意上面的fileSize过滤器我用了一个单位数组和对数运算来定位单位而不是写死一长串if...else这样扩展新单位时只需要往数组里追加代码更短也更好维护。2.3 状态字段的语义化展示数字映射成人类语言后台系统另一个高频场景是状态码映射。接口返回的订单状态大概率是0、1、2、3、4这种数字但页面上要展示的是“待支付、已支付、已发货、已完成、已取消”。这种枚举转换用过滤器要比用计算属性轻量得多因为它是纯展示逻辑不涉及任何业务计算。我惯用的写法是维护一个内部映射表Vue.filter(orderStatus, function (value) { const map { 0: 待支付, 1: 已支付, 2: 已发货, 3: 已完成, 4: 已取消 } return map[value] || 未知状态 })模板里直接使用el-tag :typestatusTag{{ order.status | orderStatus }}/el-tag顺带说一句这类映射过滤器和标签颜色配合使用效果很好。状态文字用过滤器搞定标签颜色可以在同一份映射表里维护也可以单独定义一个statusTag过滤器返回danger、success、warning之类的 tag 类型。映射逻辑放在一处改起来不用满项目找。2.4 链式调用与多参数传递什么时候值得用过滤器可以串联使用前一个过滤器的输出会作为后一个过滤器的输入。最典型的场景是刚拿到的 HTML 片段先去掉标签再按长度截断最后补上前缀或后缀{{ article.content | removeHtml | truncate(100, …) }}这里的执行顺序是从左到右removeHtml先跑结果传给truncate再得到最终输出。链式写法的可读性比嵌套函数调用好了不止一个量级这个优势在后端返回的数据需要多步清洗时特别明显。不过我要提醒一句链式调用一旦超过三级模板里的表达式就会变得很难一眼看懂这时候你就得考虑是不是应该把一串逻辑合并成一个过滤器或者在组件里用计算属性收敛。多参数传递是过滤器的另一个特性。除了第一个参数是要处理的数据外调用时写在括号里的参数会依次流入过滤函数Vue.filter(prefix, function (value, prefixStr) { return prefixStr value })模板调用{{ title | prefix(【精】) }}这种带参数过滤器的价值在于同一个过滤器可以配置出多种效果不需要为每个配置单独写一个过滤器。比如上面的truncate过滤器就同时支持自定义长度和省略号无论列表标题要 20 字还是 50 字都可以通过传参直接复用。3. 实操演示在 Vue 2 项目中落地一个过滤器3.1 先从需求出发而不是从 API 出发我以自己做过的一个订单管理后台为例带你完整走一遍过滤器的落地流程。先看需求订单列表页需要展示三个字段分别是订单金额后端返回单位是分、订单状态数字枚举、支付时间时间戳。展示规则分别是“金额转元并加千分位”“数字转中文状态”“时间戳转YYYY-MM-DD HH:mm:ss格式”而且这三个规则在订单详情页、对账单页、导出记录页都要用到。这个场景非常典型同样的格式化逻辑分布在多个页面如果每个页面都在computed里写一遍改动展示规则时就得同时改几个组件既费时又容易漏。用全局过滤器收敛成一份公共代码是最直接的解法。3.2 搭建 filters 目录并统一注册很多 Vue 2 项目的src目录里没有filters文件夹我习惯建一个src/filters/index.js把项目里所有全局过滤器集中放到这个文件里然后在main.js里统一注册。这样后续加过滤器只需要进这一个文件维护成本非常低。// src/filters/index.js import Vue from vue import { formatMoney, formatDate } from ../utils/format Vue.filter(formatMoney, formatMoney) Vue.filter(formatDate, formatDate) Vue.filter(orderStatus, function (value) { const statusMap { 0: 待支付, 1: 已支付, 2: 已发货, 3: 已完成, 4: 已取消 } return statusMap[value] || 未知状态 })在main.js里引入一次就够了// main.js import Vue from vue import ./filters这里有个关键认知Vue 2 的全局过滤器注册和应用是自动的不需要在组件里额外声明注册完整个项目模板里都能直接用。这一点跟局部注册的逻辑不同。全局注册适合“跨页面通用”的规则局部注册适合“这个组件特有的格式化”比如某个组件内部的私有展示逻辑不要所有东西都扔全局否则过滤器文件会越来越膨胀。3.3 在订单列表页中实际使用以订单状态为例模板中直接串联使用多个过滤器template el-table :dataorders el-table-column label订单金额 template slot-scope{ row } {{ row.amount | formatMoney(, 2) }} /template /el-table-column el-table-column label订单状态 template slot-scope{ row } {{ row.status | orderStatus }} /template /el-table-column el-table-column label支付时间 template slot-scope{ row } {{ row.paidAt | formatDate(YYYY-MM-DD HH:mm:ss) }} /template /el-table-column /el-table /template这个页面里完全没有在data里做预格式化也没有写任何计算属性。数据从接口拿到是什么样就存成什么样显示时通过过滤器临时转换。这种做法的好处是原始数据保持纯粹展示规则完全由模板决定后面加字段、改规则都是模板和过滤器文件的事不会污染数据层。3.4 过滤器与计算属性、methods 的选型原则有些同学会问同一个格式化需求用过滤器、computed、methods 都能实现到底该怎么选。我按自己的实践整理了一张对比表对比维度过滤器计算属性 computed方法 methods模板写法{{ val | fmt }}管道符风格{{ fmtVal }}简洁{{ fmt(val) }}带括号是否有缓存否每次渲染都执行是依赖变化才重算否每次渲染都会调用是否可传参支持传额外参数不支持直接传参可用闭包实现支持传任意参数能否访问 this不能this 为 undefined能能跨组件复用全局注册后任意组件可用每个组件需单独声明每个组件需单独声明Vue 3 支持已移除继续支持继续支持我的选型原则很简单纯展示型的、需要跨页面复用的文本格式化优先用过滤器格式化逻辑依赖组件的响应式数据、成本较高的用计算属性事件处理、交互逻辑用 methods。要是你项目的 Vue 版本已经切到 3.x过滤器这个选项就直接划掉换成普通函数和计算属性。4. Vue 3 时代过滤器被移除了项目里该怎么办4.1 为什么 Vue 3 官方决定移除过滤器很多同学看到 Vue 3 文档里没有 filters 会有点慌其实官方移除它是有明确考量的。核心原因有两个一是过滤器能做的事普通函数调用在模板里也能做多一层管道语法反而会增加学习成本二是过滤器的执行时机和缓存机制跟 computed 有重叠但从语义上和调试体验上都比 computed 弱保留两个能力近似的特性会让框架说明书变厚。更深一层的原因是模板表达式的定位变化。Vue 3 的思路是“模板里的表达式应当保持轻量复杂逻辑交给计算属性或组合式函数”。过滤器本质上是一种模板内嵌函数调用但它又不能访问this限制多、能力弱官方就把这个语法糖摘掉了。说白了它活着的时候是实用工具但它的职责完全可以用更常规的 JavaScript 语法覆盖。4.2 Vue 2 老项目升级时批量迁移的方法如果你正在把 Vue 2 项目升级到 Vue 3过滤器迁移是一个绕不开的体力活。我建议按三个步骤来做不容易漏。第一步先盘点全局注册表。打开src/filters/index.js把所有Vue.filter注册的函数整理成一张清单记录每个过滤器的名字、参数、用途。第二步把每个过滤器函数抽成独立的工具函数放到src/utils/format.js里这样它们在组件中可以按需引入。第三步在模板里做批量替换。简单场景直接把管道符写法替换成函数调用写法!-- Vue 2 写法 -- {{ order.amount | formatMoney() }} !-- Vue 3 替换方案 -- {{ formatMoney(order.amount, ) }}这里有个稍微麻烦的点Vue 2 过滤器写法里“管道符左边的值”会自动成为第一个参数而函数调用写法要求你显式把数据写在括号里。批量替换时最容易漏的就是把参数位置写错比如把{{ amount | formatMoney() }}替换成{{ formatMoney(, amount) }}输出结果就全乱了。我的经验是替换后立刻跑一遍全项目模板的编译Vue 3 遇到未定义的formatMoney会直接报错报错清单能帮你快速定位所有需要改的模板。4.3 Vue 3 项目的推荐写法函数调用 计算属性Vue 3 里我的习惯是把格式化函数放到src/utils/format.js然后在需要的地方通过组合式 API 引入再结合computed把结果缓存起来。// src/utils/format.js export function formatMoney(value, symbol , digits 2) { // 格式化逻辑同 Vue 2 过滤器 } export function formatDate(value, template YYYY-MM-DD HH:mm:ss) { // 格式化逻辑 }在组件中使用import { computed } from vue import { formatMoney } from /utils/format const props defineProps({ price: Number }) const displayPrice computed(() formatMoney(props.price, ¥, 2))模板里只保留一个名字p{{ displayPrice }}/p如果你的格式化逻辑只在当前模板用一次也可以直接在模板里调用导入的格式化函数。关键点是Vue 3 给了你两种选择要么把格式化逻辑推进计算属性里做缓存要么用普通函数做轻量转换但不要为了保留“管道符风格”去折腾自定义指令那不但偏离官方设计意图还会增加未来维护者的理解成本。5. 过滤器的常见报错、性能坑与实战心得5.1 高频报错与排查速查表我在实际项目里遇到过的过滤器相关报错基本都能归为下面四类整理成速查表方便你排查报错或现象根本原因解决方案Failed to resolve filter: xxx过滤器没有注册或作用域不在当前组件检查全局注册是否在main.js中引入检查局部注册是否写错组件选项Cannot read properties of null (reading replace)过滤器内部没做空值兜底所有过滤器函数第一行补上if (!value value ! 0) return --过滤器里访问this或this.$store过滤器函数没有被绑定组件实例不要在过滤器中访问this确实需要组件状态时改用计算属性串在 v-model 赋值链上引发无限循环过滤器误用在v-model中过滤器只用于展示v-model绑定必须用原始数据或 computed 的 get/set表格最后一条值得展开说。Vue 2 的v-model底层是value属性加input事件如果你把v-modelvalue | someFilter这种写法塞进去赋值的时候又会拿过滤后的数据回写原始字段很容易造成渲染上的死循环。这个坑比较隐蔽遇到页面卡死或内存暴增时可以先怀疑一下是不是模板里混用了过滤器和v-model。5.2 性能与维护性的三个隐形坑第一个坑是过滤器每次渲染都会执行。Vue 2 对过滤器没有任何缓存机制只要组件发生渲染模板里的过滤器函数就会重新跑一遍。所以过滤器内部不要写高成本的逻辑比如大数组遍历、异步请求、console.log大面积输出更不要在里面生成随机数否则渲染一次结果变一次还会干扰依赖收集的判断。第二个坑是全局过滤器的命名冲突。项目大了以后formatDate这种名字很可能在某个局部组件里被重复定义。Vue 的解析规则是局部优先所以通常不会直接报错但两个同名过滤器输出规则不一致时排查起来会非常恼人。我建议项目里定一条命名规范通用格式化工具统一放在utils目录过滤器的命名只做薄封装不带任何业务语义业务相关的状态映射过滤器则要有清晰的前缀比如orderStatus、payStatus避免泛化命名。第三个坑是滥用链式过滤器。链式调用虽然可读性好但每多一级就是一次函数调用数据量大时叠加起来也有一定开销。我见过有人把“日期格式化 → 区间拼接 → 添加单位”写成四级链式调用模板难读报错定位也麻烦。链式调用用在两到三级是合理的超过三级就合并成一个函数或者把逻辑移动进computed。5.3 实战中的独家建议和面试加分表述最后讲点个人体会。我在项目里实际使用过滤器时越到后面越倾向于把“过滤器的函数体”和“注册本身”分离。具体来说src/utils/format.js里放纯函数src/filters/index.js只做注册和薄封装。这样将来无论继续用 Vue 2还是升级到 Vue 3核心格式化逻辑都是同一份换个壳就能迁移不会因为框架版本变化而推翻重写。面试时如果被问到这道题我会这样组织回答先一句话定位——Vue 2 的过滤器是模板层的文本格式化工具本质是函数通过管道符调用再举一个自己项目里的具体场景比如订单状态映射或金额千分位然后主动抛出 Vue 3 的差异说明过滤器已被移除我现在的习惯是抽成普通工具函数配合 computed 使用最后补一个踩过的坑像是过滤器里访问this拿到undefined所以复杂的联动格式化必须用计算属性。这套回答既有深度又有细节比单纯背概念更能让面试官认可。踩过几次坑之后我的体会是过滤器从来不是一个必须掌握的复杂特性但它背后折射出的“模板表达能力边界”和“框架版本差异意识”才是这道面试题真正的价值。把这些想明白你不仅能答好题写业务代码时也会更清晰地知道哪块逻辑该放在模板、哪块该放在脚本里。