Skip to content

过滤器的串联与过滤器的属性渲染(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 一致;修改后刷新页面,确认两个实例都能渲染,才算注册成功。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

代码示例 ​

示例一:过滤器的串联(时间戳 → 年月日时分秒 → 截取前 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

小结 ​

  1. 过滤器是"展示层的纯函数":只把入参转成输出,不碰实例状态、不支持异步、不做缓存——凡是需要这三样的场景,它就不该被选上。
  2. 串联是单向数据流,前一环的返回值就是后一环的第一个参数;链上任何一环漏 return,整条链后面全部失效,且往往静默无报错。
  3. 选局部还是全局,看的是复用范围而不是代码量:跨实例、跨组件就 Vue.filter,单实例一次性就 filters 选项,事后改造代价很高。
  4. 模板里的管道符优先级低于算术和字符串运算符,只要表达式里出现 +、三元等运算,就要么加括号、要么把运算整体收进过滤器,别指望它"先算再加"。
  5. 差值表达式 / methods / filters / computed 的取舍顺序:要缓存 → computed;要传参且只是格式化 → filter;要改状态或发请求 → methods;只做一次简单输出 → 差值表达式。
  6. 项目里过滤器应独立成模块 + 统一注册入口,否则散落在多个实例里,改一处漏一处,还容易踩到"这个实例能用那个不能用"的坑。
  7. 只要用了 ES module 做拆分,就必须配本地服务器;file:// 下的 CORS 失败会伪装成"过滤器没注册"这类假象,排查时先看 Network/Console 的加载错误。