主题
匿名插槽与具名插槽(Lesson13 1/3)
本页对应 Lesson13,共 7 段视频,约 2 小时 25 分。
学习目标
- 能使用 Vuex Getters 统计购物车商品总价,并在组件中通过非简写与
mapGetters简写两种方式正确展示合计结果。 - 能配置 Vue Router 全局前置/后置守卫,结合
meta、本地 token 实现路由权限控制,并在未登录跳转登录页时携带来源地址、登录成功后完成回跳。 - 能分别用“未使用 actions”和“使用 actions”的方式实现登录流程,并说明
dispatch调用 action 的同步执行特征、异步请求处理方式及返回 Promise 的成功值。 - 能调用个人信息与退出登录接口,完成个人信息展示、退出登录后清理 token/用户信息并跳转登录页。
- 能开启 Vuex 命名空间,并按模块名正确改写和调用
state、getters、mutations、actions及对应简写写法。 - 能使用
vuex-persistedstate配置 Vuex 缓存,验证刷新后数据保留,并能通过key与paths指定缓存键名和缓存字段。
知识点
匿名插槽要解决什么问题
匿名插槽的核心是:子组件只负责结构占位,父组件在使用子组件时把一段模板内容“填”进去。本节笔记给的定义很直接:匿名插槽就是 slot 组件,没有 name 属性。
为什么需要它?看 BookList 这个组件:它负责根据 bookList 渲染书籍列表,但“男生必读”“女生爱看”这种标题并不是书籍数据本身,而是父组件在不同使用场景下想额外显示的内容。如果不用插槽,就只能再加一个 title 属性,或者把标题写死在子组件里,复用性会变差。
怎么想?把子组件想成一个“带缺口的模板”:缺口位置放 <slot></slot>,父组件在 <BookList>...</BookList> 标签内部写的内容,就会出现在这个缺口处。
怎么做?先在子组件模板中写 <slot></slot> 占位,再在父组件中把 <h3> 写在 BookList 标签内部。父组件写了几组 BookList,就可以分别填入不同标题,而书籍列表仍然由 props 中的 bookList 决定。
易错点:插槽内容和 props 是两条线。bookList 是数据传递,插槽是模板内容分发;父组件包裹的内容只有在子组件存在 <slot> 时才会显示。
子组件怎么留口
为什么先在子组件改?因为插槽的“占位”发生在子组件模板里。父组件写得再对,如果子组件没有 slot,包裹内容也不会被渲染到子组件结构中。
怎么想?BookList 的职责是渲染列表,标题位置是不确定的,所以把标题位置空出来。<slot></slot> 放在 v-for 前面,表示标题先显示,书籍列表随后显示。
怎么做?在 src/components/BookList.vue 中保留如下结构:
vue
<template>
<div>
<slot></slot>
<div v-for="item in bookList" :key="item.id">
<p>《{{item.bookName}}》-->{{item.author}}</p>
</div>
</div>
</template>
<script>
export default {
name: "BookList",
props:["bookList"]
}
</script>
<style scoped>
</style>易错点:slot 是 Vue 内置组件,不需要在 components 中注册;它写在子组件模板里,不是写在父组件模板里。另一个常见错误是把 <slot> 写到 v-for 内部,这样每个列表项都会插入一次标题,通常不是本节想要的效果。
父组件怎么填内容
为什么在父组件填?因为同一份 BookList 要被男生列表和女生列表复用,标题颜色和文字不同,这部分差异应由使用方决定。
怎么想?父组件使用 BookList 时,标签内部写什么,子组件的 <slot> 就显示什么。两次使用分别包裹不同的 <h3>,就形成两个不同标题。
怎么做?在 src/App.vue 中按笔记代码使用:
vue
<template>
<div>
<BookList :bookList="boy">
<h3 style="color:blue;">男生必读</h3>
</BookList>
<hr/>
<BookList :bookList="girl">
<h3 style="color:pink;">女生爱看</h3>
</BookList>
</div>
</template>
<script>
// 插槽:在子组件当中通过内置的slot组件进行占位。该占位可以呈现使用子组件时所包裹的内容。
import BookList from "@/components/BookList";
export default {
name: "App",
components: {BookList},
data(){
return {
boy:[
{
id:"1",
bookName:"这个世界很危险",
author:"叶知风"
},
{
id:"2",
bookName:"三国:我为刘禅,霄汉永灿",
author:"麻货"
}
],
girl:[
{
id:"4",
bookName:"不装了,财阀大佬暗恋我很久了",
author:"我爱干饭"
},
{
id:"5",
bookName:"误入豪门,闪婚老公每天花式吃醋",
author:"这一剑惊鸿"
}
]
}
}
}
</script>
<style lang="less">
</style>易错点:<h3> 必须写在 <BookList>...</BookList> 内部;如果写到外面,它只是普通标题,不会进入插槽。还要注意,插槽内容的作用域默认在父组件,不要误以为它能直接访问子组件内部变量。
调试与易错点
为什么需要调试?插槽不显示时,问题通常不在数据,而在“占位”和“包裹位置”两处。
怎么想?按渲染链路排查:父组件有没有包裹内容 → 子组件模板有没有 <slot> → BookList 是否注册并正确使用 → 数据是否传入。本节代码中,两个 BookList 分别传入 boy、girl,标题分别写在标签内部,因此应看到蓝色“男生必读”和粉色“女生爱看”,下面各自渲染对应书籍。
怎么做?先临时确认子组件模板中存在 <slot></slot>;再确认父组件没有把 <h3> 写在 BookList 外面;最后检查 import BookList 与 components: {BookList} 是否一致。
易错点:匿名插槽没有 name 属性,不要写成 name="default" 去模仿命名插槽;命名插槽是另一类用法。多个匿名插槽同时存在时容易造成内容重复分发,本节只保留一个占位即可。样式上,父组件给 <h3> 写的行内样式会直接生效;若使用 scoped,要理解插槽内容属于父组件模板作用域,不要误判为子组件样式失效。
一、为什么需要具名插槽
BookList 是一个可复用的图书列表组件:它负责根据 bookList 渲染书名和作者。但“男生爱看”“女生爱看”这类标题,并不应该写死在 BookList 内部。否则每换一个使用场景,就要改子组件,复用性很差。
所以思路是:子组件只负责预留位置,父组件决定往这个位置放什么内容。普通默认插槽只能解决“一个占位”的问题;当组件里需要多个不同位置时,就要给插槽起名字,这就是具名插槽。
怎么想:先看子组件需要预留几个位置;再给每个位置用 name 命名;父组件使用时用 v-slot:名字 或缩写 #名字 把内容送进去。
怎么做:本节 BookList.vue 中只预留了一个具名插槽:
vue
<slot name="one"></slot>父组件 App.vue 中这样填内容:
vue
<BookList v-slot:one :bookList="boy">
<h3 style="color:blue;">男生爱看</h3>
</BookList>易错点:v-slot:one 要写在 BookList 组件标签上,写在里面的 h3 上无效;slot 的 name 和父组件的 v-slot:one 必须一致。
二、子组件:用 name 预留位置
BookList.vue 的结构是:先放具名插槽,再循环渲染图书列表。这样父组件传进来的标题会出现在列表上方。
vue
<template>
<div>
<slot name="one"></slot>
<div v-for="item in bookList" :key="item.id">
<p>《{{item.bookName}}》-->{{item.author}}</p>
</div>
</div>
</template>这里 slot name="one" 只是占位,不会自己产生内容;真正显示什么由父组件包裹的内容决定。bookList 仍然通过 props 接收,和插槽是两件事:数据用 props,结构内容用插槽。
易错点:如果子组件写的是 <slot></slot>,那就是默认插槽;父组件写 v-slot:one 时内容不会进入这个默认插槽。
三、父组件:用 v-slot:one 或 #one 填内容
App.vue 中两个 BookList 分别传入 boy 和 girl,并用插槽提供不同标题。第二个使用 #one 缩写:
vue
<BookList #one :bookList="girl">
<h3 style="color:pink;">女生爱看</h3>
</BookList>#one 等价于 v-slot:one。本案例每个 BookList 只有一个具名插槽,所以可以直接把 v-slot:one 写在组件标签上,组件内部包裹的内容整体进入 one 插槽。若一个组件里有多个具名插槽,通常要写成:
vue
<template v-slot:one>...</template>
<template v-slot:two>...</template>易错点:#one 中间不要加空格;name="one" 与 #one 大小写要一致;不要把 :bookList="boy" 写成 bookList="boy",否则传过去的是字符串而不是数组。
四、完整代码
src/App.vue:
vue
<template>
<div>
<!-- 将包裹的内容放置到名字为one的插槽中 -->
<BookList v-slot:one :bookList="boy">
<h3 style="color:blue;">男生爱看</h3>
</BookList>
<hr/>
<!-- v-slot:可以用#代替 -->
<BookList #one :bookList="girl">
<h3 style="color:pink;">女生爱看</h3>
</BookList>
</div>
</template>
<script>
// 插槽:在子组件当中通过内置的slot组件进行占位。该占位可以呈现使用子组件时所包裹的内容。
import BookList from "@/components/BookList";
export default {
name: "App",
components: {BookList},
data(){
return {
boy:[
{
id:"1",
bookName:"这个世界很危险",
author:"叶知风"
},
{
id:"2",
bookName:"三国:我为刘禅,霄汉永灿",
author:"麻货"
}
],
girl:[
{
id:"4",
bookName:"不装了,财阀大佬暗恋我很久了",
author:"我爱干饭"
},
{
id:"5",
bookName:"误入豪门,闪婚老公每天花式吃醋",
author:"这一剑惊鸿"
}
]
}
}
}
</script>
<style lang="less">
</style>src/components/BookList.vue:
vue
<template>
<div>
<slot name="one"></slot>
<div v-for="item in bookList" :key="item.id">
<p>《{{item.bookName}}》-->{{item.author}}</p>
</div>
</div>
</template>
<script>
export default {
name: "BookList",
props:["bookList"]
}
</script>
<style scoped>
</style>五、易错点与调试顺序
调试时按“子组件有没有留位置 → 名字是否一致 → 父组件内容是否包在组件标签内”检查:
- 先看
BookList.vue是否有<slot name="one"></slot>; - 再看
App.vue是否写了v-slot:one或#one; - 最后确认标题
h3是否写在BookList开始标签和结束标签之间。
常见错误:把 v-slot:one 写到 h3 上;slot name 拼成 onee;使用默认插槽写法却期待进入具名插槽;多个插槽时仍把内容直接写在组件标签内。只要名字一致、内容位置正确,两个列表就会分别显示蓝色“男生爱看”和粉色“女生爱看”标题。
课堂画面






代码示例
vue
<!-- src/components/BookList.vue -->
<template>
<div>
<!-- 匿名插槽:父组件未指定 v-slot 的内容进入这里 -->
<slot></slot>
<!-- 具名插槽:父组件 v-slot:one / #one 的内容进入这里 -->
<slot name="one"></slot>
<div v-for="item in bookList" :key="item.id">
<p>《{{ item.bookName }}》-->{{ item.author }}</p>
</div>
</div>
</template>
<script>
export default {
name: "BookList",
props: ["bookList"]
};
</script>
<style scoped>
</style>要点:
<slot></slot>是匿名插槽,没有name属性,父组件包裹的普通内容默认进入这里。<slot name="one"></slot>是具名插槽,父组件必须用v-slot:one或#one才能把内容放进来。- 易错点:子组件没有写
<slot>占位时,父组件包裹的内容不会渲染;props必须声明bookList,否则v-for取不到数据。
vue
<!-- src/App.vue:匿名插槽用法 -->
<template>
<div>
<BookList :bookList="boy">
<h3 style="color: blue;">男生必读</h3>
</BookList>
<hr />
<BookList :bookList="girl">
<h3 style="color: pink;">女生爱看</h3>
</BookList>
</div>
</template>
<script>
import BookList from "@/components/BookList";
export default {
name: "App",
components: { BookList },
data() {
return {
boy: [
{
id: "1",
bookName: "这个世界很危险",
author: "叶知风"
},
{
id: "2",
bookName: "三国:我为刘禅,霄汉永灿",
author: "麻货"
}
],
girl: [
{
id: "4",
bookName: "不装了,财阀大佬暗恋我很久了",
author: "我爱干饭"
},
{
id: "5",
bookName: "误入豪门,闪婚老公每天花式吃醋",
author: "这一剑惊鸿"
}
]
};
}
};
</script>
<style>
</style>要点:
<BookList>标签内部包裹的<h3>会进入子组件BookList.vue的匿名插槽<slot></slot>。- 父组件不需要写
v-slot,所以默认匹配匿名插槽;此时子组件里的具名插槽name="one"没有内容,不显示。 - 易错点:插槽内容写在子组件标签内部,不是写在子组件模板里;
v-for必须保留:key="item.id"。
vue
<!-- src/App.vue:具名插槽用法 -->
<template>
<div>
<!-- 将包裹内容放入 name="one" 的插槽 -->
<BookList v-slot:one :bookList="boy">
<h3 style="color: blue;">男生爱看</h3>
</BookList>
<hr />
<!-- v-slot: 可以用 # 简写 -->
<BookList #one :bookList="girl">
<h3 style="color: pink;">女生爱看</h3>
</BookList>
</div>
</template>
<script>
import BookList from "@/components/BookList";
export default {
name: "App",
components: { BookList },
data() {
return {
boy: [
{
id: "1",
bookName: "这个世界很危险",
author: "叶知风"
},
{
id: "2",
bookName: "三国:我为刘禅,霄汉永灿",
author: "麻货"
}
],
girl: [
{
id: "4",
bookName: "不装了,财阀大佬暗恋我很久了",
author: "我爱干饭"
},
{
id: "5",
bookName: "误入豪门,闪婚老公每天花式吃醋",
author: "这一剑惊鸿"
}
]
};
}
};
</script>
<style>
</style>要点:
v-slot:one表示把<BookList>内部包裹的内容放进子组件name="one"的插槽。#one是v-slot:one的简写,二者等价。- 易错点:
v-slot不能写在普通 DOM 元素上,应写在组件标签或<template>上;具名插槽名必须和子组件<slot name="one">完全一致,否则内容不会进入目标插槽。
常见错误
匿名插槽内容不显示
现象:父组件在<BookList>标签内写了标题,但页面没有渲染。
原因:子组件没有放<slot>,或父组件内容写到了具名插槽但子组件仍是匿名<slot>。
修法:对齐插槽类型;匿名插槽父组件直接包裹内容,子组件保留<slot></slot>。具名插槽名字对不上
现象:<BookList #one>传了内容,子组件不显示。
原因:子组件写的是<slot name="one">之外的名字,或父组件#one与子组件name不一致。
修法:统一名称,父组件#one/v-slot:one,子组件<slot name="one"></slot>。模块化后
getters取不到值
现象:this.$store.getters.somePrice返回undefined。
原因:模块开启namespaced后,getter 键变成cart/somePrice,点语法拿不到带斜杠的键。
修法:用this.$store.getters['cart/somePrice'],或...mapGetters('cart', ['somePrice'])。mapGetters/mapActions简写失效
现象:模板能渲染但合计为空,或点击登录提示找不到 action。
原因:开启命名空间后仍按旧写法mapGetters(['somePrice'])、mapActions(['login'])。
修法:传模块名:...mapGetters('cart', ['somePrice']);重名时用对象写法:...mapActions({ loginAction: 'user/login' })。全局前置守卫死循环
现象:访问/login仍被跳到/login,或页面一直卡住。
原因:守卫没有先判断白名单/meta,所有路由都校验 token;分支里next()调用不唯一或未return。
修法:先判断to.meta.isAuth,不需要鉴权直接next();需要鉴权再查 token,每个分支next()后return。登录成功后不跳转或失败也跳转
现象:点击登录后停在原页,或密码错误仍进入个人中心。
原因:没有await异步 action/请求,未判断接口结果,replace写在请求完成之前。
修法:await登录 action,成功后再router.replace(redirect);失败return并提示,不执行跳转。刷新后登录态/购物车丢失
现象:登录后刷新,token 没了;加购后刷新,购物车清空。
原因:只存在 Vuex 内存里,刷新后 state 重新初始化;或只写localStorage但未回填 state。
修法:state 初始化时从localStorage读取,或用vuex-persistedstate配置key和paths只持久化必要字段。退出登录清理不完整
现象:退出后刷新又恢复登录态,或页面仍显示用户头像/昵称。
原因:只清了localStorage没清 Vuex,或只清 token 没清user.info;未调用退出接口或未跳转登录页。
修法:统一封装 logout:调用退出接口、清 token、清用户信息、清对应 storage,再replace到登录页。
练习
插槽改造:给
BookList增加header、footer两个具名插槽,父组件分别为男生列表和女生列表传入不同标题与统计文案,同时保留一个匿名插槽用于默认提示。验收:匿名内容、具名内容互不串位;#header与name="header"完全对应。购物车合计两种写法:在
cart模块开启命名空间后,实现合计 getter,并分别用this.$store.getters['cart/somePrice']和mapGetters('cart', ['somePrice'])在组件中取值;购物车为空时显示“购物车空空如也”。验收:加购、删除、刷新后合计正确,控制台无undefined。路由权限闭环:实现未登录访问
/cart、/my时跳转/login?redirect=目标地址;登录成功后回到目标页;/login、/goods、/add不被拦截。验收:直接输入/cart能跳登录,登录后回/cart;不会出现/login跳/login死循环。登录 action 改造:把登录请求封装到
user/loginaction,页面用mapActions重命名调用;要求await成功后再保存 token 并跳转,失败只提示不跳转。验收:错误密码停留登录页;正确密码进入目标页;Vuex 与localStorage中 token 一致。命名空间与缓存排查:给
goods、cart、user全部开启namespaced,修正项目内所有commit、dispatch、mapGetters、mapActions;用持久化插件只保存user.token和cart.cartList。验收:刷新后 token 和购物车保留,退出登录后二者都被清空;Vuex DevTools 中 action/getter 名称均带模块前缀。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 合计 | 13:54 |
| 2 | 路由权限控制 | 20:27 |
| 3 | 登陆-未使用actions | 25:17 |
| 4 | 登陆-使用actions | 19:18 |
| 5 | 显示个人详情 | 21:25 |
| 6 | 命名空间 | 29:54 |
| 7 | 缓存 | 15:20 |
小结
- 插槽排查优先看“父子名称是否对齐”和“内容是否写在组件标签内部”,匿名与具名不要混用。
- Vuex 一旦模块化,
commit、dispatch、getters、map*的路径必须统一带模块前缀,否则不是找不到,就是误触发同名逻辑。 - 路由权限应放在全局前置守卫,先白名单/
meta再鉴权,所有分支保证只next()一次并及时return。 - 登录跳转必须等异步 action 完成且结果成功后再执行;失败要中断流程,
redirect参数比router.go(-1)更可靠。 - token 要统一来源:守卫判断、请求头、Vuex、持久化存储不能各用一套,否则刷新和接口鉴权必出问题。
- 退出登录是完整清理流程:服务端退出接口、Vuex 状态、本地存储、页面跳转,缺一项都会留下脏状态。
- 缓存只持久化必要字段,敏感信息和临时 UI 状态不要全量落盘;调试顺序先看控制台/网络,再看 Vuex DevTools,最后查路由守卫与组件命名。