Skip to content

匿名插槽与具名插槽(Lesson13 1/3) ​

本页对应 Lesson13,共 7 段视频,约 2 小时 25 分。

学习目标 ​

  1. 能使用 Vuex Getters 统计购物车商品总价,并在组件中通过非简写与 mapGetters 简写两种方式正确展示合计结果。
  2. 能配置 Vue Router 全局前置/后置守卫,结合 meta、本地 token 实现路由权限控制,并在未登录跳转登录页时携带来源地址、登录成功后完成回跳。
  3. 能分别用“未使用 actions”和“使用 actions”的方式实现登录流程,并说明 dispatch 调用 action 的同步执行特征、异步请求处理方式及返回 Promise 的成功值。
  4. 能调用个人信息与退出登录接口,完成个人信息展示、退出登录后清理 token/用户信息并跳转登录页。
  5. 能开启 Vuex 命名空间,并按模块名正确改写和调用 state、getters、mutations、actions 及对应简写写法。
  6. 能使用 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>

五、易错点与调试顺序 ​

调试时按“子组件有没有留位置 → 名字是否一致 → 父组件内容是否包在组件标签内”检查:

  1. 先看 BookList.vue 是否有 <slot name="one"></slot>;
  2. 再看 App.vue 是否写了 v-slot:one 或 #one;
  3. 最后确认标题 h3 是否写在 BookList 开始标签和结束标签之间。

常见错误:把 v-slot:one 写到 h3 上;slot name 拼成 onee;使用默认插槽写法却期待进入具名插槽;多个插槽时仍把内容直接写在组件标签内。只要名字一致、内容位置正确,两个列表就会分别显示蓝色“男生爱看”和粉色“女生爱看”标题。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

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"> 完全一致,否则内容不会进入目标插槽。

常见错误 ​

  1. 匿名插槽内容不显示
    现象:父组件在 <BookList> 标签内写了标题,但页面没有渲染。
    原因:子组件没有放 <slot>,或父组件内容写到了具名插槽但子组件仍是匿名 <slot>。
    修法:对齐插槽类型;匿名插槽父组件直接包裹内容,子组件保留 <slot></slot>。

  2. 具名插槽名字对不上
    现象:<BookList #one> 传了内容,子组件不显示。
    原因:子组件写的是 <slot name="one"> 之外的名字,或父组件 #one 与子组件 name 不一致。
    修法:统一名称,父组件 #one / v-slot:one,子组件 <slot name="one"></slot>。

  3. 模块化后 getters 取不到值
    现象:this.$store.getters.somePrice 返回 undefined。
    原因:模块开启 namespaced 后,getter 键变成 cart/somePrice,点语法拿不到带斜杠的键。
    修法:用 this.$store.getters['cart/somePrice'],或 ...mapGetters('cart', ['somePrice'])。

  4. mapGetters / mapActions 简写失效
    现象:模板能渲染但合计为空,或点击登录提示找不到 action。
    原因:开启命名空间后仍按旧写法 mapGetters(['somePrice'])、mapActions(['login'])。
    修法:传模块名:...mapGetters('cart', ['somePrice']);重名时用对象写法:...mapActions({ loginAction: 'user/login' })。

  5. 全局前置守卫死循环
    现象:访问 /login 仍被跳到 /login,或页面一直卡住。
    原因:守卫没有先判断白名单/meta,所有路由都校验 token;分支里 next() 调用不唯一或未 return。
    修法:先判断 to.meta.isAuth,不需要鉴权直接 next();需要鉴权再查 token,每个分支 next() 后 return。

  6. 登录成功后不跳转或失败也跳转
    现象:点击登录后停在原页,或密码错误仍进入个人中心。
    原因:没有 await 异步 action/请求,未判断接口结果,replace 写在请求完成之前。
    修法:await 登录 action,成功后再 router.replace(redirect);失败 return 并提示,不执行跳转。

  7. 刷新后登录态/购物车丢失
    现象:登录后刷新,token 没了;加购后刷新,购物车清空。
    原因:只存在 Vuex 内存里,刷新后 state 重新初始化;或只写 localStorage 但未回填 state。
    修法:state 初始化时从 localStorage 读取,或用 vuex-persistedstate 配置 key 和 paths 只持久化必要字段。

  8. 退出登录清理不完整
    现象:退出后刷新又恢复登录态,或页面仍显示用户头像/昵称。
    原因:只清了 localStorage 没清 Vuex,或只清 token 没清 user.info;未调用退出接口或未跳转登录页。
    修法:统一封装 logout:调用退出接口、清 token、清用户信息、清对应 storage,再 replace 到登录页。

练习 ​

  1. 插槽改造:给 BookList 增加 header、footer 两个具名插槽,父组件分别为男生列表和女生列表传入不同标题与统计文案,同时保留一个匿名插槽用于默认提示。验收:匿名内容、具名内容互不串位;#header 与 name="header" 完全对应。

  2. 购物车合计两种写法:在 cart 模块开启命名空间后,实现合计 getter,并分别用 this.$store.getters['cart/somePrice'] 和 mapGetters('cart', ['somePrice']) 在组件中取值;购物车为空时显示“购物车空空如也”。验收:加购、删除、刷新后合计正确,控制台无 undefined。

  3. 路由权限闭环:实现未登录访问 /cart、/my 时跳转 /login?redirect=目标地址;登录成功后回到目标页;/login、/goods、/add 不被拦截。验收:直接输入 /cart 能跳登录,登录后回 /cart;不会出现 /login 跳 /login 死循环。

  4. 登录 action 改造:把登录请求封装到 user/login action,页面用 mapActions 重命名调用;要求 await 成功后再保存 token 并跳转,失败只提示不跳转。验收:错误密码停留登录页;正确密码进入目标页;Vuex 与 localStorage 中 token 一致。

  5. 命名空间与缓存排查:给 goods、cart、user 全部开启 namespaced,修正项目内所有 commit、dispatch、mapGetters、mapActions;用持久化插件只保存 user.token 和 cart.cartList。验收:刷新后 token 和购物车保留,退出登录后二者都被清空;Vuex DevTools 中 action/getter 名称均带模块前缀。

原视频对照 ​

#视频时长
1合计13:54
2路由权限控制20:27
3登陆-未使用actions25:17
4登陆-使用actions19:18
5显示个人详情21:25
6命名空间29:54
7缓存15:20

小结 ​

  1. 插槽排查优先看“父子名称是否对齐”和“内容是否写在组件标签内部”,匿名与具名不要混用。
  2. Vuex 一旦模块化,commit、dispatch、getters、map* 的路径必须统一带模块前缀,否则不是找不到,就是误触发同名逻辑。
  3. 路由权限应放在全局前置守卫,先白名单/meta 再鉴权,所有分支保证只 next() 一次并及时 return。
  4. 登录跳转必须等异步 action 完成且结果成功后再执行;失败要中断流程,redirect 参数比 router.go(-1) 更可靠。
  5. token 要统一来源:守卫判断、请求头、Vuex、持久化存储不能各用一套,否则刷新和接口鉴权必出问题。
  6. 退出登录是完整清理流程:服务端退出接口、Vuex 状态、本地存储、页面跳转,缺一项都会留下脏状态。
  7. 缓存只持久化必要字段,敏感信息和临时 UI 状态不要全量落盘;调试顺序先看控制台/网络,再看 Vuex DevTools,最后查路由守卫与组件命名。