主题
过滤器的串联与过滤器的属性渲染(Lesson3 2/3)
本页对应 Lesson03,共 5 段视频,约 1 小时 29 分。
学习目标
- 能编写并解释多个过滤器的串联调用,明确每个过滤器接收的是上一个过滤器的输出结果。
- 能在插值表达式和属性绑定中正确使用过滤器,并完成如时间格式化、书名号包裹、图片 Base64 前缀拼接等渲染任务。
- 能声明全局过滤器,并在不同 Vue 实例中验证其可复用性,同时说明局部过滤器的使用范围限制。
- 能将过滤器抽离为独立模块导出对象,并通过遍历注册为全局过滤器。
- 能说明计算属性“本质是函数、使用时作为属性”的特点,验证其缓存机制、依赖变化后重新计算、不可传参及不支持异步。
- 能根据场景在插值表达式、methods、filters 和计算属性之间做出选择,并用计算属性实现依赖一个或多个数据状态的派生结果。
知识点
先看目标:把时间戳加工成想要的字符串
本节笔记要实现的效果是:数据 one 里存的是 Date.now() 得到的时间戳,但页面不能直接显示一长串数字,而要显示成年月日时分秒;显示时又只想保留前 4 位,也就是年份。
所以这里不是“一个过滤器干完所有事”,而是把“格式化时间”和“截取字符串”拆成两个过滤器,再用管道符串起来。
为什么需要串联?因为实际开发中,一个数据往往要经过多步加工。把每一步拆成独立过滤器,逻辑更清楚,也更容易复用。怎么想?先让 date 把时间戳变成 2023-xx-xx xx:xx:xx,再让 sliceNum(4) 从这个结果里截取前 4 位。怎么做?在插值表达式里写 {{one | date | sliceNum(4)}}。易错点是:管道符右边必须是已经定义好的过滤器名,否则 Vue 会报错;串联时前一个过滤器的返回值,就是后一个过滤器的第一个参数。
怎么想:串联的输入输出关系
转写稿里反复强调:“我把 one 通过 date 过滤一次,过滤完以后的数据,我再通过 sliceNum 再过滤一次。”
也就是说,sliceNum 过滤的不是原始时间戳,而是 date 过滤后的字符串。date(v) 中的 v 是原始时间戳;sliceNum(v,n=2) 中的 v 是上一个过滤器返回的字符串,n 是额外传入的参数。
为什么第二个过滤器要带参数?因为默认 n=2 时只截两位,而这里想保留 4 位年份,所以写成 sliceNum(4)。怎么想?过滤器第一个参数固定是“要被过滤的数据”,后面的参数才是调用时传进去的自定义参数。易错点:不要以为 sliceNum(4) 里的 4 会传给 date,它只传给 sliceNum 的 n。
怎么做:保留本节完整代码
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="lib/vue.js"></script>
</head>
<body>
<div id="app">
{{one | date | sliceNum(4)}}
</div>
</body>
<script>
// 串联:可以使用多个过滤器
new Vue({
el:"#app",
data:{
one:Date.now()
},
filters:{
date(v){
const timer = new Date(v);
return timer.getFullYear()+"-"+
(timer.getMonth()+1).toString().padStart(2,0)+"-"+
timer.getDate().toString().padStart(2,0)+" "+
timer.getHours().toString().padStart(2,0)+":"+
timer.getMinutes().toString().padStart(2,0)+":"+
timer.getSeconds().toString().padStart(2,0);
},
sliceNum(v,n=2){
return v.slice(0,n);
}
}
})
</script>
</html>这段代码里,date 负责把时间戳格式化为 年-月-日 时:分:秒,其中月份要 +1,并用 padStart(2,0) 补零。sliceNum 负责从字符串第 0 位开始截取 n 位。最后模板中先 date,再 sliceNum(4),得到年份。
调试过程与易错点
转写稿中有一个很典型的调试过程:先只写 {{one | date}},能看到年月日时分秒;再加一个管道符写 | sliceNum,但此时 sliceNum 还没在 filters 里定义,页面就报错。补上 sliceNum 后,如果函数里没有 return,页面就输出 undefined,看起来像“什么都没显示”。这说明过滤器必须有返回值,否则后一个过滤器或页面拿不到结果。
另一个易错点是截取位置。slice(0,4) 得到前 4 位,slice(2,4) 会从第 2 位开始截取,结果就不是完整年份。转写稿里还试过默认参数 n=2,结果只显示前两位,说明参数要按需求传。
最后要记住:串联顺序是从左到右,前一个过滤器的输出是后一个过滤器的输入;第一个参数永远是被过滤的数据,后面的参数才是自定义参数。
先看目标:过滤器为什么要能用于属性渲染
前面过滤器多写在插值表达式里,比如 {{ item.hosname | book }}。本节要把过滤器搬到属性绑定上:医院图片接口返回的是 base64 字符串,<img> 的 src 不能直接放裸 base64,必须拼成 data:image/jpeg;base64,...。如果每个图片都手写拼接,重复且容易漏;用过滤器 base64 把“格式化”抽出来,模板只表达“这里要一张医院图片”。
怎么想:过滤器只负责“把传入值格式化成输出值”,不负责改数据;属性渲染和插值一样可以用管道符。怎么做:定义局部 filters.base64(v),返回完整 data URL;模板写 :src="item.logoData | base64"。易错点:过滤器里 this 指向 window,不能操作 Vue 实例数据;它只做格式化,不要试图在过滤器里更新 hospitalList。
准备数据和加载状态:为什么先做 isLoading
请求是异步的,点击后到数据回来前页面是空的。转写稿里先加“获取数据”按钮,再给 isLoading:点击时置 true,请求完成置 false。这样用户能看到“正在拼命加载中……”,而不是以为按钮没反应。
怎么想:加载中与列表是互斥视图,适合 v-if="isLoading" 和 v-else。怎么做:用 <template v-else> 包住 v-for,避免多包一层无意义 div。易错点:v-if 与 v-for 不要挤在同一个标签上;template 只做包裹,不渲染真实节点。
用 fetch 拿医院列表:从 Response 到数据
转写稿里调试了三种写法。第一种回调里 value.json();第二种 return value.json() 再 .then;第三种 await fetch(...).then(value => value.json())。关键推导:fetch 成功值不是数据本身,而是 Response 对象;要调用 .json(),它返回 Promise,成功值才是接口 JSON。所以代码里先 const res = await ...,再 this.hospitalList = res.data.content。
易错点:只 console.log(value) 会看到 Response;忘了 await 会拿到 Promise;请求同协议、同域名、同端口才不跨域。调试时先打印 res,确认结构是 data.content 再渲染。
书名号过滤器和列表渲染:过滤器可复用
医院名要显示成《医院名》。可以直接拼字符串,但多处使用时过滤器更稳。定义 book(v){ return "《"+v+"》"; },模板写 {{ item.hosname | book }}。怎么想:这是典型“展示格式化”,不改变原数据。易错点:过滤器名不要和变量混淆;管道右边是过滤器名;过滤器没有返回值时页面什么都不显示。
base64 图片:属性渲染的关键调试
先写 <img :src="item.logoData"> 看不到图,因为 src 收到的是裸 base64。转写稿中改为拼接:'data:image/jpeg;base64,' + item.logoData,注意外层双引号、里面单引号。再抽成 base64(v) 过滤器,模板写 :src="item.logoData | base64",并加 height="100" 控制高度。易错点:漏 data:image/jpeg;base64, 前缀;引号写错;item.logoData 拼错;过滤器只对当前值格式化,不要写成先拼接再过滤导致语义混乱。
完整代码(保留笔记代码,图片 alt 改为中文)
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="lib/vue.js"></script>
</head>
<body>
<div id="app">
<button @click="getHospitalList">获取数据</button>
<h3 v-if="isLoading">正在拼命加载中……</h3>
<template v-else>
<div v-for="item in hospitalList">
<h3>{{item.hosname | book}}</h3>
<!-- <img height="100" :src="'data:image/jpeg;base64,'+item.logoData" alt="医院图片(拼接写法)">-->
<img height="100" :src="item.logoData | base64" alt="医院图片">
<hr/>
</div>
</template>
</div>
</body>
<script>
new Vue({
el:"#app",
data:{
hospitalList:[],
isLoading:false
},
methods:{
async getHospitalList(){
this.isLoading = true;
// 1
// fetch("./data.json").then(value=>{
// value.json().then(res=>{
// console.log(res);
// })
// })
// 2
// fetch("./data.json").then(value=>{
// return value.json()
// }).then(res=>{
// console.log(res);
// })
// 3
const res = await fetch("./data.json").then(value=>{
return value.json()
})
this.hospitalList = res.data.content;
this.isLoading = false;
}
},
filters:{
book(v){
return "《"+v+"》";
},
base64(v){
return "data:image/jpeg;base64,"+v;
}
}
})
</script>
</html>为什么需要全局过滤器
本节代码里有两个 Vue 实例:#app 渲染商品列表,#root 渲染新闻列表。两边都要把时间戳格式化成“年-月-日 时:分:秒”,商品还要把价格格式化成货币形式。
为什么需要:如果只把 date 写在某一个实例的 filters 里,它只能在那个实例的模板中使用;另一个实例里写 {{item.newsTime | date}} 就会找不到过滤器。
怎么想:过滤器本质是“对传入值做格式化并返回结果”的函数。既然多个实例、多个组件都要复用,就应该注册成全局过滤器。转写稿里也明确说:全局过滤器可以在不同的 Vue 实例以及组件中使用。
怎么做:使用 Vue.filter(名字, 函数) 注册。第一个参数是过滤器名字,第二个参数是过滤器方法。
易错点:是 Vue.filter,没有 Vue.filters;全局过滤器要在 new Vue(...) 之前注册,否则模板解析时可能还找不到。
定义 date 与 currency
为什么需要:date 解决时间戳可读性差的问题;currency 解决价格小数位和货币符号不统一的问题。
怎么想:date 接收被过滤的时间戳 t,用 new Date(t) 转成日期对象,再逐项取年月日时分秒;currency 接收价格 v,并允许调用时传小数位 n 和货币符号 type。
怎么做:date 里用 getFullYear()、getMonth()+1、getDate()、getHours()、getMinutes()、getSeconds(),并用 padStart(2, 0) 补零。currency 里写 function(v,n=2,type="$"){ return type+v.toFixed(n); },这样模板中不传参数时默认保留两位小数、加 $。
易错点:月份要 +1;toFixed 返回的是字符串;默认参数要从第二个参数开始设计,因为第一个参数永远是被过滤的数据。转写稿里也强调:第一个参数是死的,就是被过滤的数据,后面的参数才是调用时传进来的。
两个实例共用过滤器
为什么需要:笔记里 #app 用 currency 和 date,#root 只用 date。如果 date 是局部的,#root 就不能用。
怎么想:把 date、currency 都放到全局,两个实例模板里的管道右侧写同名过滤器即可。
怎么做:模板中写 {{item.goodsPrice | currency}}、{{item.addTime | date}}、{{item.newsTime | date}}。
易错点:过滤器名字要和 Vue.filter 第一个参数完全一致;多个实例的数据是各自独立的,但全局过滤器是共享的。转写稿中的调试过程是:先在局部定义 date,在另一个实例里用会报找不到;改成 Vue.filter 后两个实例都能正常显示。过滤器函数里的 this 指向 window,不要试图在过滤器里修改 Vue 实例的数据状态,它只负责格式化并返回结果。
完整代码
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="lib/vue.js"></script>
</head>
<body>
<div id="app">
<div v-for="item in goodsList" :key="item.id">
<p>商品名字:{{item.goodsName}}</p>
<p>商品价格:{{item.goodsPrice | currency}}</p>
<p>上架时间:{{item.addTime | date}}</p>
<hr/>
</div>
</div>
<div id="root">
<div v-for="item in newsList">
<p>新闻名字:{{item.newsTitle}}</p>
<p>新闻时间:{{item.newsTime | date}}</p>
</div>
</div>
</body>
<script>
// 全局过滤器:可以在不同的Vue实例以及组件中使用。
// 第一个参数是过滤器的名字,第二个参数是过滤器的方法
Vue.filter("date",function(t){
const timer = new Date(t);
return timer.getFullYear() + "-" +
(timer.getMonth() + 1).toString().padStart(2, 0) + "-" +
timer.getDate().toString().padStart(2, 0) + " " +
timer.getHours().toString().padStart(2, 0) + ":" +
timer.getMinutes().toString().padStart(2, 0) + ":" +
timer.getSeconds().toString().padStart(2, 0);
})
Vue.filter("currency",function(v,n=2,type="$"){
return type+v.toFixed(n);
})
new Vue({
el: "#app",
data: {
goodsList: [
{
id: 1,
goodsName: "葡萄",
goodsPrice: 12.345,
addTime: Date.now()
}, {
id: 2,
goodsName: "梨",
goodsPrice: 45.678,
addTime: Date.now() - 998887666
}
]
}
})
new Vue({
el:"#root",
data:{
newsList:[
{
id:1,
newsTitle:"新闻一号",
newsTime:Date.now()
},
{
id:2,
newsTitle:"新闻二号",
newsTime:Date.now()
}
]
}
})
</script>
</html>为什么要把过滤器放到外部
为什么需要:当页面里出现多个 Vue 实例,例如 #app 和 #root,如果过滤器只写在某一个实例的 filters 里,另一个实例就不能用。转写稿里就演示过:在 #app 中定义的 date,到 #root 里使用时会报“找不到过滤器”。项目里全局过滤器一多,全部堆在入口文件里也会乱。
怎么想:过滤器本质上是“对数据做格式化”的函数。date、currency 这类函数不依赖某个实例的 data,可以被多个实例、多个组件复用。因此应当:先写成独立模块导出对象;再在入口统一注册为全局过滤器。
怎么做:新建 lib/vue-filters.js,用 export default 导出对象;在 HTML 的模块脚本中 import,用 for...in 遍历对象,逐个执行 Vue.filter(key, filters[key])。
易错点:Vue.filter 没有 s;第一个参数是过滤器名,第二个参数是函数;模块导出的是对象,不是直接执行注册。
外部过滤器模块与页面代码
本节笔记代码如下,保留原代码与路径:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="lib/vue.js"></script>
</head>
<body>
<div id="app">
<div v-for="item in goodsList" :key="item.id">
<p>商品名字:{{item.goodsName}}</p>
<p>商品价格:{{item.goodsPrice | currency}}</p>
<p>上架时间:{{item.addTime | date}}</p>
<hr/>
</div>
</div>
<div id="root">
<div v-for="item in newsList">
<p>新闻名字:{{item.newsTitle}}</p>
<p>新闻时间:{{item.newsTime | date}}</p>
</div>
</div>
</body>
<script type="module">
import filters from "./lib/vue-filters.js";
for(let key in filters){
Vue.filter(key,filters[key])
}
new Vue({
el: "#app",
data: {
goodsList: [
{
id: 1,
goodsName: "葡萄",
goodsPrice: 12.345,
addTime: Date.now()
}, {
id: 2,
goodsName: "梨",
goodsPrice: 45.678,
addTime: Date.now() - 998887666
}
]
}
})
new Vue({
el:"#root",
data:{
newsList:[
{
id:1,
newsTitle:"新闻一号",
newsTime:Date.now()
},
{
id:2,
newsTitle:"新闻二号",
newsTime:Date.now()
}
]
}
})
</script>
</html>js
export default {
date(t){
const timer = new Date(t);
return timer.getFullYear() + "-" +
(timer.getMonth() + 1).toString().padStart(2, 0) + "-" +
timer.getDate().toString().padStart(2, 0) + " " +
timer.getHours().toString().padStart(2, 0) + ":" +
timer.getMinutes().toString().padStart(2, 0) + ":" +
timer.getSeconds().toString().padStart(2, 0);
},
currency(v,n=2,type="$"){
return type+v.toFixed(n);
}
}调试过程:如果只把 date 写在 #app 的 filters 中,#root 里 {{item.newsTime | date}} 就解析不到;把它抽到 vue-filters.js 后,入口循环注册,两个实例都能使用。currency 同理,goodsPrice | currency 会调用 currency(12.345),返回默认两位小数和 $。
批量注册时的推导
为什么需要:如果每加一个过滤器都写一次 Vue.filter("date", ...),代码重复。
怎么想:导出的 filters 是对象,键名就是过滤器名,键值就是函数。遍历对象即可把“名字—函数”成对注册。
怎么做:
js
for(let key in filters){
Vue.filter(key, filters[key])
}等价于:
js
Vue.filter("date", filters.date)
Vue.filter("currency", filters.currency)易错点:key 是字符串,正好作为过滤器名;filters[key] 才是函数。不要写成 Vue.filter(key) 或 Vue.filters(...)。
过滤器的使用边界与检查清单
为什么需要:外部化后虽然能复用,但过滤器只负责“格式化输出”,不应承担更新实例数据状态的职责。
怎么想:模板里 | date、| currency 的左侧是被过滤数据,右侧是过滤器名;需要额外参数时写成 | currency(2, "¥") 这种形式。转写稿也强调:过滤器函数第一个参数是“要被过滤的数据”,后面的参数才是自定义参数。
怎么做:商品价格用 currency,时间用 date;新闻时间继续用 date。两个实例共享同一套格式化逻辑。
易错点:过滤器函数中的 this 不是 Vue 实例,不能靠它改 data;padStart 的补位字符按代码原样理解即可;外部文件路径要与 import 一致;修改后刷新页面,确认两个实例都能渲染,才算注册成功。
课堂画面





代码示例
示例一:过滤器的串联(时间戳 → 年月日时分秒 → 截取前 n 位)
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>过滤器的串联</title>
<script src="lib/vue.js"></script>
</head>
<body>
<div id="app">
{{one | date | sliceNum(4)}}
</div>
</body>
<script>
// 串联:可以使用多个过滤器
new Vue({
el:"#app",
data:{
one:Date.now()
},
filters:{
date(v){
const timer = new Date(v);
return timer.getFullYear()+"-"+
(timer.getMonth()+1).toString().padStart(2,0)+"-"+
timer.getDate().toString().padStart(2,0)+" "+
timer.getHours().toString().padStart(2,0)+":"+
timer.getMinutes().toString().padStart(2,0)+":"+
timer.getSeconds().toString().padStart(2,0);
},
sliceNum(v,n=2){
return v.slice(0,n);
}
}
})
</script>
</html>关键行与易错点
{{one | date | sliceNum(4)}}:管道符左侧是数据,右侧是过滤器名,从左往右依次执行。sliceNum接收到的v已经不是Date.now()的时间戳,而是date过滤器返回的字符串(如"2024-05-21 10:23:45"),所以sliceNum才能对它调用slice。- 过滤器函数的第一个参数固定是被过滤的数据,从第二个参数开始才是你在模板里传入的实参(这里的
4对应n)。 sliceNum(v, n = 2)用了默认参数,所以{{one | date | sliceNum}}不传参时也能跑,默认截取 2 位;如果写成sliceNum(4)得到的就是前 4 位(年份)。- 易错点:如果过滤器忘记写
return(比如转写稿里提到的“没有写返回值”的情况),模板里就什么也显示不出来,因为过滤器的返回值才是最终要渲染的内容。 padStart(2, 0)用于把月份、日期、时分秒补成两位;getMonth()是 0 起始的,所以必须+1。
示例二:过滤器用于属性渲染(书名号 + base64 图片前缀)
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>过滤器的属性渲染</title>
<script src="lib/vue.js"></script>
</head>
<body>
<div id="app">
<button @click="getHospitalList">获取数据</button>
<h3 v-if="isLoading">正在拼命加载中……</h3>
<template v-else>
<div v-for="item in hospitalList">
<h3>{{item.hosname | book}}</h3>
<img height="100" :src="item.logoData | base64" alt="">
<hr/>
</div>
</template>
</div>
</body>
<script>
new Vue({
el:"#app",
data:{
hospitalList:[],
isLoading:false
},
methods:{
async getHospitalList(){
this.isLoading = true;
// 3
const res = await fetch("./data.json").then(value=>{
return value.json()
})
this.hospitalList = res.data.content;
this.isLoading = false;
}
},
filters:{
book(v){
return "《"+v+"》";
},
base64(v){
return "data:image/jpeg;base64,"+v;
}
}
})
</script>
</html>关键行与易错点
:src="item.logoData | base64":这是属性绑定中使用过滤器的写法,和双花括号里一样,加管道符即可,base64的v就是item.logoData。- 图片渲染必须带前缀,
"data:image/jpeg;base64," + v拼接这一步是重点。如果直接在模板里拼,要注意外层是双引号,里层得用单引号::src="'data:image/jpeg;base64,'+item.logoData"——两种写法效果一致,项目里同类图片多时用过滤器更合适。 v-if="isLoading"与v-else配合<template>做加载态切换,isLoading初始值必须是false(写错初始值会导致状态切换不生效)。await fetch(...).then(v => v.json()):fetch返回的是 Promise,.json()也返回 Promise,await拿到的才是真正的响应体数据。- 易错点:不要在同一个元素上同时用
v-if和v-else配对错误,v-else必须紧跟在v-if元素后面(这里用<template v-else>包住循环,就是为此)。 - 易错点:过滤器不能用来“更新数据”。它只负责把接收到的值格式化后返回,
this指向window,无法操作 Vue 实例里的数据状态。
示例三:全局过滤器 + 抽离到外部模块
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>全局过滤器与外部模块</title>
<script src="lib/vue.js"></script>
</head>
<body>
<div id="app">
<div v-for="item in goodsList" :key="item.id">
<p>商品名字:{{item.goodsName}}</p>
<p>商品价格:{{item.goodsPrice | currency}}</p>
<p>上架时间:{{item.addTime | date}}</p>
<hr/>
</div>
</div>
<div id="root">
<div v-for="item in newsList">
<p>新闻名字:{{item.newsTitle}}</p>
<p>新闻时间:{{item.newsTime | date}}</p>
</div>
</div>
</body>
<script type="module">
import filters from "./lib/vue-filters.js";
for(let key in filters){
Vue.filter(key, filters[key])
}
new Vue({
el: "#app",
data: {
goodsList: [
{
id: 1,
goodsName: "葡萄",
goodsPrice: 12.345,
addTime: Date.now()
}, {
id: 2,
goodsName: "梨",
goodsPrice: 45.678,
addTime: Date.now() - 998887666
}
]
}
})
new Vue({
el:"#root",
data:{
newsList:[
{
id:1,
newsTitle:"新闻一号",
newsTime:Date.now()
},
{
id:2,
newsTitle:"新闻二号",
newsTime:Date.now()
}
]
}
})
</script>
</html>配套的 lib/vue-filters.js:
js
export default {
date(t){
const timer = new Date(t);
return timer.getFullYear() + "-" +
(timer.getMonth() + 1).toString().padStart(2, 0) + "-" +
timer.getDate().toString().padStart(2, 0) + " " +
timer.getHours().toString().padStart(2, 0) + ":" +
timer.getMinutes().toString().padStart(2, 0) + ":" +
timer.getSeconds().toString().padStart(2, 0);
},
currency(v, n = 2, type = "$"){
return type + v.toFixed(n);
}
}关键行与易错点
Vue.filter("date", function(t){...}):全局过滤器用Vue.filter注册(注意没有s),第一个参数是过滤器名字,第二个参数是函数。注册后可以在这两个new Vue实例(#app和#root)里都用,这就是它和filters局部写法的核心区别。- 外部模块导出的是一个对象,属性名即过滤器名,属性值即过滤器函数,所以用
for(let key in filters)遍历后逐个Vue.filter(key, filters[key])注册,等价于手写一遍Vue.filter("date", ...)、Vue.filter("currency", ...)。 - 易错点:
<script type="module">必须写type="module",否则import会直接报语法错误;同时Vue仍是从lib/vue.js挂到全局的,模块里可直接用。 - 易错点:局部
filters只在当前实例生效。如果在#app里定义date,却在#root里写{{item.newsTime | date}},会报“过滤器未定义”——这也是要把公共过滤器提升为全局的原因。 currency(v, n = 2, type = "$")中默认参数让{{item.goodsPrice | currency}}不传参也能用,得到$12.35;toFixed(2)是四舍五入保留两位。
常见错误
1. 串联后整段渲染为空白,控制台却不报错
- 现象:
{{ one | date | sliceNum(4) }}页面什么都不显示,也没有红色报错。 - 原因:链上某个过滤器(多为
sliceNum)只写了处理逻辑、忘写return,返回undefined,后面的过滤器拿到undefined继续处理,最终输出空。 - 修法:每个过滤器结尾必须有
return;排查时把链条从右往左一段段砍掉,先只留| date看有没有值,再逐个加回。
2. 图片 src 前缀被加了两遍,图片裂开
- 现象:
:src="'data:image/jpeg;base64,'+item.logoData | base64"渲染出的地址是data:image/jpeg;base64,data:image/jpeg;base64,...。 - 原因:
|的优先级低于+,过滤器作用在整个拼接结果上,前缀被当成了普通字符串又拼了一次。 - 修法:二者只选一种——要么删掉过滤器直接拼接,要么删掉拼接只写
item.logoData | base64,把前缀放进过滤器里。
3. 换一个 Vue 实例,过滤器就报 "Failed to resolve filter"
- 现象:
#app里{{ goodsPrice | currency }}正常,#root里{{ newsTime | date }}直接报错。 - 原因:写在某个实例
filters选项里的是局部过滤器,作用域只限该实例;另一个实例的模板里根本查不到这个名字。 - 修法:跨实例复用的一律用
Vue.filter(name, fn)全局注册;确实只用一次的再留在filters里。
4. 全局注册写法写错,直接抛 TypeError
- 现象:
Vue.filters('date', fn)报Vue.filters is not a function;Vue.filter(filtersObj)也不生效。 - 原因:全局 API 是单数
Vue.filter,复数filters是实例选项名;且Vue.filter一次只能注册一个。 - 修法:
Vue.filter(name, fn)逐个注册;拿到的是对象就for (let key in filters) Vue.filter(key, filters[key])。
5. 过滤器抽到外部文件后 import 成功,模板里依然报找不到
- 现象:
import filters from './lib/vue-filters.js'没报错,但模板里| date仍然失败;或者浏览器控制台先报模块加载错误。 - 原因:只做了 import 没有注册,对象躺在变量里没人用;另外若页面用
file://直接打开,ES module 会被 CORS 拦截,注册代码压根没执行。 - 修法:import 之后立刻遍历注册;同时用本地服务器(http)打开页面,别双击 html 文件。
6. 在过滤器里改实例数据,改了没反应甚至报错
- 现象:过滤器里写
this.isLoading = false或this.hospitalList = res.data,页面不动,严格模块下还报Cannot set properties of undefined。 - 原因:过滤器函数的
this不是 Vue 实例(普通脚本下是window,type="module"下是undefined),它只负责"入参 → 返回值",不参与状态更新。 - 修法:请求和状态更新留在
methods/created里;过滤器只做格式化,确实需要额外数据就用参数传进去。
7. 计算属性当方法调、当表单值绑
- 现象:
{{ reverseStr() }}报reverseStr is not a function;v-model="reverseStr"报Computed property "reverseStr" was assigned to but it has no setter。 - 原因:计算属性在模板里是属性,不加括号;默认只有 getter,是只读的。
- 修法:去掉括号;需要写回时改成
{ get(){...}, set(v){...} }的对象写法,在set里反向还原到源数据。
练习
1. 串联改参数 数据 one: Date.now(),模板要求只输出年份后两位(如 24)。在不修改 date 过滤器返回值的前提下,通过串联和传参实现;再改成输出 2024-06,说说 sliceNum 的参数该给多少、为什么。
2. 全局化 把商品价格(12.345 → $12.35)和上架时间(时间戳 → 2024-06-01 10:20:30)两个过滤器改成全局注册,然后在页面上挂两个 Vue 实例(#app 商品列表、#root 新闻列表),验证两个实例都能用 | date。
3. 抽模块 把第 2 题的过滤器抽到 lib/vue-filters.js,用 export default 导出对象,在页面里 import 后循环注册。要求:注册代码不超过 3 行,且页面必须能通过 http 访问(说明为什么不能直接双击打开)。
4. 两种写法对比 医院列表的 logoData 需要拼上 data:image/jpeg;base64, 前缀。分别用「模板内拼接」和「过滤器」两种写法实现,并故意写出管道符与 + 混用的错误版本,记录它在浏览器里渲染出的 src 到底是什么。
5. 三方案对比 实现字符串反转,分别用差值表达式、methods、computed 三种方式渲染同一个 str。在每个方案里加 console.log 计数,然后连续修改输入框内容 3 次,统计各自的执行次数,并解释 computed 次数为什么不同。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 过滤器的串联 | 04:56 |
| 2 | 过滤器的属性渲染 | 16:45 |
| 3 | 全局过滤器 | 09:21 |
| 4 | 将过滤器放置在外部 | 05:04 |
| 5 | 计算属性 | 52:57 |
小结
- 过滤器是"展示层的纯函数":只把入参转成输出,不碰实例状态、不支持异步、不做缓存——凡是需要这三样的场景,它就不该被选上。
- 串联是单向数据流,前一环的返回值就是后一环的第一个参数;链上任何一环漏
return,整条链后面全部失效,且往往静默无报错。 - 选局部还是全局,看的是复用范围而不是代码量:跨实例、跨组件就
Vue.filter,单实例一次性就filters选项,事后改造代价很高。 - 模板里的管道符优先级低于算术和字符串运算符,只要表达式里出现
+、三元等运算,就要么加括号、要么把运算整体收进过滤器,别指望它"先算再加"。 - 差值表达式 / methods / filters / computed 的取舍顺序:要缓存 → computed;要传参且只是格式化 → filter;要改状态或发请求 → methods;只做一次简单输出 → 差值表达式。
- 项目里过滤器应独立成模块 + 统一注册入口,否则散落在多个实例里,改一处漏一处,还容易踩到"这个实例能用那个不能用"的坑。
- 只要用了 ES module 做拆分,就必须配本地服务器;
file://下的 CORS 失败会伪装成"过滤器没注册"这类假象,排查时先看 Network/Console 的加载错误。