主题
插槽(一)
本页对应 插槽,共 5 段视频,约 1 小时 17 分。
学习目标
- 能够说明全局前置守卫、请求拦截器和响应拦截器在登录鉴权、Token 携带及过期处理中的作用,并在项目中定位对应配置。
- 能够按接口文档使用 Axios 完整写法发送请求,正确区分
params与data,并处理FormData上传和路径参数拼接。 - 能够解释
$nextTick的执行时机与 Vue 异步更新视图机制,并在修改数据后操作更新后 DOM 的场景中正确使用this.$nextTick。 - 能够使用默认插槽封装可变内容组件,在父组件中向插槽提供结构并验证组件复用效果。
- 能够为多个插槽设置
name,使用v-slot:name或#name配合template向具名插槽传递内容,并区分默认插槽。 - 能够使用作用域插槽在子组件插槽上传递数据,在父组件中通过
v-slot接收、解构并使用数据,说明其作用域限制。
知识点
启动代码,先看移动端面经效果
为什么需要: 先启动准备好的代码、把移动端面经内容完整看一遍,是为了先建立“项目整体长什么样、有哪些交互”的印象。不要一上来就钻代码,否则容易只见局部不见流程。
怎么想: 演示时按用户真实访问顺序看:进入页面 → 是否被登录访问拦截 → 登录后能否看到面经列表 → 列表数据是否正常请求和渲染 → 点击条目是否带参跳转 → 收藏、喜欢等交互是否可用。
怎么做: 用平时启动该素材的方式把项目跑起来,打开移动端面经页面,先看列表内容,再按上述顺序逐项点一遍。演示时重点不是“点得快”,而是能说清每个动作背后对应哪个功能模块。

易错点: 页面能打开不等于功能正常;本地没有 token 时被导航守卫拦到登录页是正常现象;不要先把“接口没数据”误判成页面写错,要先看请求是否发出、是否带 token。
按访问链路拆功能模块
为什么需要: 演示之后必须明确功能模块,否则后面写代码不知道自己在补哪一块。这个项目的模块不是孤立的,而是围绕“登录态 + 请求 + 列表展示 + 路由传参”串起来的。
怎么想: 从访问链路拆:路由守卫负责“没 token 不让进其他页”;请求拦截器负责“每次请求统一带 token”;响应拦截器负责“token 过期、401 等统一处理”;面经列表负责“取数据存到 list 并渲染”;路由传参负责“点进具体内容时把参数带过去”;收藏、喜欢属于列表上的交互功能。
怎么做: 演示时逐项对照:未登录访问其他页面,观察地址是否被直接改到登录页且不发请求;登录后看请求头是否带 token;进入面经列表看数据是否存入 list;点击条目看路由参数;再演示收藏、喜欢等操作。
易错点: 导航守卫要写在 router 变量创建之后、导出之前,放错位置用不了;next(false) 是不允许访问,next('/login') 是跳到登录页;请求拦截器别漏 token;响应拦截器处理 401 时别只弹提示不跳转。转写里说的“头款/投盔卡”就是 token,“be报一roer点”就是 beforeEach。
对照项目收获,建立学习地图
为什么需要: “项目收获”不是口号,而是告诉你这个项目练了哪些可复用的套路。后面换项目,很多代码是定式。
怎么想: 把收获分成四类:权限链路(导航守卫、请求/响应拦截器)、数据请求(axios 固定写法)、页面更新($nextTick)、组件复用(插槽)。这样演示时看到的是功能,复盘时能落到知识点。

怎么做: 对照转写稿回顾:导航守卫判断本地存储没有 token 且访问的不是登录、注册页,就拦回登录;请求拦截器统一加 token;响应拦截器处理 token 过期;axios 用完整写法 axios({ method, url, params, data }) 最稳,特殊情况下 form-data 要先 new FormData(),路径参数要把 ID 拼到 URL 后面;$nextTick 用于数据更新后、DOM 也更新后再拿 DOM;插槽分默认插槽、具名插槽、作用域插槽。
易错点: axios 里 params 和 data 别混;提交 form-data 不能直接写大括号对象;路径参数不是查询参数;具名插槽的 name 要和 v-slot:title 对应;作用域插槽传属性时不能把属性叫 name,否则会被当成插槽名。
调试复盘:演示中要能说清“为什么”
为什么需要: 演示不是把页面点一遍就结束,而是要能解释“为什么这里会失败、怎么定位”。转写稿里的调试过程正好说明:很多问题不是语法不会,而是没理解执行时机和位置。
怎么想: 比如输入框聚焦失败,不是 ref 写错,而是数据改了以后 DOM 不会立即更新,当前这次事件循环还在收集数据,要等下次循环才更新页面;所以要在 $nextTick 回调里找输入框。再比如插槽内容串位,是因为三个插槽没区分,副组件传的结构都进了默认插槽;加上具名插槽后,再用 template 包裹并写 v-slot:title、v-slot:btn 才能对上。转写里的“句名插槽”就是具名插槽,“作用预插件”就是作用域插槽,“盗码/到某”就是 DOM。
怎么做: 演示后回到代码按链路复盘:先看导航守卫位置和 to/from/next;再看请求、响应拦截器;再看面经列表取数和传参;最后看 $nextTick、插槽这些组件层面的细节。遇到问题先判断是“请求没发”“数据没到”“DOM 没更新”还是“结构传错位置”。
易错点: 不要在数据刚改完就立刻找 DOM;不要给普通标签乱加 v-slot,它要加在自定义组件或 template 上;不要把函数传参的形参、实参搞混——实参是调用时传进去的值,形参是函数定义位置接收的值,传对象时形参拿到的就是那个对象。演示时能把这些点说清,才算真正明确了功能模块和项目收获。
先明确:目录初始化为什么交给脚手架
转写稿开头回顾了登录访问拦截(路由守卫)以及请求/响应拦截器:请求拦截器统一加 token,响应拦截器处理 token 过期。这些配置在每个项目中都会反复出现,所以项目开始时要先把骨架和规范定好,后面写 request、路由守卫时才不会反复补配置。本节笔记只处理一件事:用 Vue CLI 创建项目并完成目录初始化。
为什么需要:手写目录、构建配置、依赖管理容易漏文件,也不利于团队统一。怎么想:用官方脚手架生成标准结构,再把 ESLint、CSS 预处理等选项一次选好。怎么做:先确认脚手架已安装,再执行创建命令。易错点:命令要在目标父目录执行;项目名按命令行可用名称输入,避免后续路径和依赖问题。
bash
npm i @vue/cli -g用 vue create 进入交互式创建
为什么需要:vue create 会交互式询问预设和功能,决定项目初始目录与配置文件。怎么想:不要一路回车,先看清每一步是在选预设还是选功能。怎么做:执行:
bash
vue create hm-vant-h5出现预设选择时,笔记选择“手动选择功能”:
js
Vue CLI v5.0.8
? Please pick a preset:
Default ([Vue 3] babel, eslint)
Default ([Vue 2] babel, eslint)
> Manually select features 选自定义易错点:交互界面用方向键移动、回车确认;需要多选时按空格勾选。不要把“默认预设”和“手动选择”混淆。
手动选择功能与 Vue 版本
为什么需要:不同项目需要 Router、状态管理、CSS 预处理、Linter 等不同能力,手动选择可以按需生成配置。怎么想:先按笔记截图进入功能勾选,再在版本询问处选择 Vue 2.x。怎么做:按截图勾选项目所需功能;版本选择如下:

jsx
3.x
> 2.x易错点:版本一旦选错,后续代码写法、依赖版本都会受影响;本节笔记明确选择 2.x,就不要再混入 Vue 3 写法。
history 模式与 CSS 预处理器
为什么需要:路由模式影响地址形态和部署方式;CSS 预处理器影响样式文件写法。怎么想:这两步都属于项目基础约定,按团队技术栈选择,不要只看默认值。怎么做:在 history 模式询问处按项目部署要求选择;在 CSS 预处理列表中选择项目约定项。


易错点:history 模式如果部署环境没有服务端回退配置,刷新子路由可能 404;CSS 预处理器选完后,样式文件后缀和语法要统一。
ESLint 风格、校验时机与配置文件
为什么需要:ESLint 是代码规范检验工具,能提前发现不符合规范的写法。笔记举例:const age = 18; 多加分号会报错,后面有工具一保存就全部格式化成最规范的样子。怎么想:规范不是为了刁难,而是减少团队差异;先选风格,再决定校验时机和配置放哪。怎么做:选择 ESLint 风格;校验时机直接回车;配置文件生成方式直接回车。



易错点:不要为了消错随手关规则;保存自动格式化后仍要看清分号、引号、缩进是否符合所选风格。
是否保存预设与安装完成
为什么需要:保存预设可以把本次选项复用到下一个项目,但也会让后续创建受旧配置影响。怎么想:本节按笔记不保存,输入 N,保持每次创建都重新确认。怎么做:在询问是否保存预设时输入 N,等待依赖安装和项目初始化完成。


易错点:不要把 N 误输入到其他选项;安装过程不要随意中断,否则依赖可能不完整。
启动项目与本节收口
为什么需要:初始化完成后要验证脚手架生成的目录和依赖能正常运行。怎么想:先启动开发服务器,看到页面再继续写业务。怎么做:进入项目目录后执行:
bash
npm run serve易错点:启动命令是 serve,不是随意改成其他脚本;如果端口被占用,按终端提示处理。本节完成后,项目目录已由脚手架初始化,后续再按转写稿回顾的路由守卫、请求/响应拦截器继续配置。
为什么需要代码规范
代码规范是一套写代码的约定规则,例如赋值符号左右要不要空格、一句结束要不要加分号、字符串用单引号还是双引号。没有规矩不成方圆:一个人写代码可以凭习惯,团队协作时如果每个人都按自己的习惯来,代码就会越来越难读、难改、难 review。
怎么想:规范不是为了限制你,而是为了减少沟通成本和低级错误。先接受“团队有统一规则”这件事,再谈具体规则是什么。怎么做:项目创建时使用 JavaScript Standard Style 作为代码风格规则,写代码时尽量遵守;遇到不确定的写法,先查规则再下结论。易错点:不要把“我平时这么写”当成理由;也不要把规范当成背单词,重点是遇到报错会查、会改。
ESLint 与 JavaScript Standard Style
ESLint 是一个代码检查工具,用来检查代码是否符合指定规则。你和团队可以自行约定一套规则;当前项目使用的是 JavaScript Standard Style。它会在你保存代码后提示不符合规范的地方。
为什么需要:人工检查容易漏,ESLint 可以稳定、重复地检查同一套规则。怎么想:ESLint 是来帮助你的,不是故意找麻烦;有错就改,心态要好。怎么做:先了解 Standard 的常见约定,建议把 https://standardjs.com/rules-zhcn.html 看一遍,写的时候遇到错误再查询解决。常见规则包括:字符串使用单引号,需要转义的地方除外;无分号;关键字后加空格,如 if (condition) { ... };函数名后加空格,如 function name (arg) { ... };坚持使用全等 ===,摒弃 ==,但在需要检查 null || undefined 时可以使用 obj == null。易错点:不要只记住“无分号”却忘了单引号、空格和全等;obj == null 是特殊例外,不要随意扩大使用。
制造一次规范错误:保存后看控制台
为了理解 ESLint 的提示方式,可以在 main.js 中随意做一些改动:添加一些空行、空格。示例代码如下:
js
import Vue from 'vue'
import App from './App.vue'
import './styles/index.less'
import router from './router'
Vue.config.productionTip = false
new Vue ( {
render: h => h(App),
router
}).$mount('#app')按下保存代码之后,控制台中会输出错误提示:

为什么需要先制造错误:只有亲眼看到提示,才知道 ESLint 检查的是什么、错误出现在哪一行。怎么想:保存后不要急着关掉控制台,先读提示。怎么做:看控制台列出的错误代码和行号,例如 func-call-spacing、space-in-parens 等。易错点:不要一看到红字就关闭 ESLint;也不要只看“有错”却不看具体规则代码。转写稿里处理其他知识点时也反复强调“按套路来、遇到不会就查文档”,这个调试习惯同样适用于 ESLint:读错误、查规则、改代码、保存复查。
看懂错误代码并手动修复
手动修复的原则是:根据错误提示一项一项修正。如果你不认识命令行中的语法报错是什么意思,可以根据错误代码去 ESLint 规则列表中查找具体含义。打开 ESLint 规则表 https://zh-hans.eslint.org/docs/latest/rules/,使用页面搜索(Ctrl + F)这个代码,查找对该规则的解释。

为什么需要查规则表:错误代码是规则的“名字”,知道名字才能准确理解它要求什么。怎么想:不要凭感觉把空格删掉或加上,先确认规则含义。怎么做:例如示例中的 new Vue ( { 这类写法,会触发函数调用空格、括号内空格相关提示;按规则改成 new Vue({,再保存复查。易错点:不要只改行号附近而不理解规则;不要一次改太多导致新的错误难以定位;改完要再次保存,看控制台是否还有报错。
常见规则与易错点小结
Standard 的规则不少,但入门阶段先抓住高频点:单引号、无分号、关键字后空格、函数名后空格、全等。为什么需要:这些规则直接影响代码一致性和可读性。怎么想:写的时候就按规则写,比事后逐条改更省力。怎么做:保存后逐条看错误,按错误代码查规则表,改完再保存;遇到不会的规则,先查再改。易错点:不要为了“看起来整齐”随意加空行和空格;不要忽略 func-call-spacing、space-in-parens 这类细节;不要把 == 当成省事写法,除非是 obj == null 这种明确例外。最后记住:ESLint 是来帮助你的,有错就改。
本节定位:让 ESLint 先“看见错误”,再“自动修错误”
本节笔记只有两个核心目标:第一,ESLint 插件在 VS Code 中自动高亮错误;第二,通过保存配置让 ESLint 自动修复可修复的错误。转写稿里多次出现“excel/excels”这类同音口误,结合本节笔记应纠正为 ESLint;转写稿中关于 axios、路由守卫、nextTick、插槽的内容不属于本节,不混入本节推导。讲师反复强调“配置是固定套路、位置要对、忘了就翻以前项目的配置文件”,迁移到本节就是:先确认插件生效,再确认配置文件位置,再确认保存动作有没有触发。
为什么需要:只靠肉眼看代码,错误发现得晚;只高亮不修复,又要手动改很多格式问题。
怎么想:把 ESLint 当成“检查器 + 修复器”,插件负责显示,配置负责保存时修复。
怎么做:先安装插件,再写保存配置。
易错点:插件没启用、配置没被找到、保存配置没生效,都会让人误以为 ESLint 没用。
自动高亮错误:先让问题可见
为什么需要:错误如果不可见,就谈不上修复。ESLint 插件的第一价值是让不符合规则的代码在编辑器里直接暴露。
怎么想:先不要急着配自动修复,先确认编辑器能不能标出 ESLint 问题。
怎么做:按笔记截图安装 ESLint 插件,安装后打开项目文件观察是否出现 ESLint 提示。

本节截图只给出安装入口,不额外编造插件命令;按截图在 VS Code 扩展面板中搜索并安装即可。
易错点:不要把所有波浪线都当成 ESLint;先确认提示来源,再判断是规则问题还是其他插件提示。
配置保存时自动修复
为什么需要:很多 ESLint 问题是可自动修复的,例如格式、引号、分号等。保存时自动修复能减少手动操作。
怎么想:保存动作触发 source.fixAll,同时关闭保存时自动格式化,避免两个工具互相抢。
怎么做:在 VS Code 的 settings.json 中写入笔记给出的配置:
js
// 当保存的时候,eslint自动帮我们修复错误
"editor.codeActionsOnSave": {
"source.fixAll": true
},
// 保存代码,不自动格式化
"editor.formatOnSave": false易错点:只开自动修复但没装/没启用 ESLint 插件,保存不会修;只开自动格式化,可能被其他格式化器改写,和 ESLint 规则冲突。
根目录与一次打开一个项目
为什么需要:ESLint 插件要找到项目里的 ESLint 配置文件,才能按规则检查和修复。
怎么想:把“项目根目录”当成 VS Code 的工作区根目录;配置文件必须在根目录下,插件才容易生效。
怎么做:打开项目时直接打开项目根目录,不要打开它的上一层文件夹;一次打开一个项目。
易错点:打开父目录导致插件找不到配置;多个项目混在一个窗口里,配置和作用范围容易串。转写稿里讲师强调“位置要对、创建顺序要对”,这里同样适用:先确认根目录和配置文件位置,再谈保存修复。
禁用 VS Code 自带格式化工具
为什么需要:ESLint 接管校验和修复后,如果 Beatify 等格式化工具同时工作,可能一边修一边改,结果不稳定。
怎么想:保存时只让 ESLint 做修复,不让自带格式化工具抢动作。
怎么做:按笔记要求,使用 ESLint 校验后,把 VS Code 自带的那些格式化工具禁用,例如 Beatify;并把 editor.formatOnSave 设为 false。
易错点:以为关了 formatOnSave 就等于关了所有格式化;还要检查是否装了其他保存时格式化插件。
settings.json 参考与验证
笔记给出的完整参考如下,可直接对照自己的 VS Code 配置:
jsx
{
"window.zoomLevel": 2,
"workbench.iconTheme": "vscode-icons",
"editor.tabSize": 2,
"emmet.triggerExpansionOnTab": true,
// 当保存的时候,eslint自动帮我们修复错误
"editor.codeActionsOnSave": {
"source.fixAll": true
},
// 保存代码,不自动格式化
"editor.formatOnSave": false
}为什么需要:参考配置能减少漏项。
怎么想:先保证 ESLint 相关两项正确,再对照其他个人设置。
怎么做:保存文件后故意写一个可修复的小问题,按 Ctrl+S 观察是否被修复。
易错点:配置写在了错误作用域;项目没以根目录打开;插件被禁用。排查顺序按“插件是否启用 → 配置文件是否在根目录 → 是否以根目录打开 → 是否被其他格式化工具干扰”逐项确认,不要一上来就改规则。
先明确本节要做什么
这一节不是写业务功能,而是把 Vue 脚手架初始化出来的“默认样例”整理成企业项目更常见的骨架。笔记里强调:强烈建议严格按照步骤调整,为了符合企业规范。目标只有三件事:删除初始化默认文件、修改没删除的文件、新增我们需要的目录结构。转写稿开头的回顾提到,后续要写登录访问拦截、请求拦截器和响应拦截器,其中导航守卫要放在 router 相关代码里,请求配置要统一管理;所以这一步先把默认路由和示例组件清干净,后面才不会在样例代码上反复返工。转写稿里“头款/偷分”应理解为 token,“如er/rooter”即 router,“阿里克斯/excel”即 axios,本节只做目录准备,不展开这些代码。
删除初始化默认文件
为什么需要:脚手架自带的 logo.png、HelloWorld.vue、AboutView.vue、HomeView.vue 是示例内容,企业项目一般不会保留;留着会干扰路由配置和目录阅读。
怎么想:先删“确定不用”的示例文件,再改还保留的入口文件,最后新增业务目录。删除范围要严格按笔记来,不要顺手删 main.js、App.vue、router/index.js。
怎么做:删除以下文件:
src/assets/logo.pngsrc/components/HelloWorld.vuesrc/views/AboutView.vuesrc/views/HomeView.vue
易错点:删完文件后如果路由里还 import HomeView、import AboutView,启动时会找不到模块;如果 App.vue 还引用 HelloWorld,同样会报错。所以删除后必须接着改路由和 App.vue。
修改 router/index.js
为什么需要:默认路由指向 HomeView、AboutView,这两个文件已经删除;同时本节要把默认路由配置清空,为后续真实路由让位。
怎么想:main.js 不需要修改,说明入口挂载逻辑保持不动;只改 router/index.js,保留 VueRouter 的安装、实例创建和导出结构,把 routes 清成空数组。转写稿提到导航守卫要写在创建 router 变量之后、导出之前,本节先把结构留好,后面加守卫才不破坏位置。
怎么做:把 router/index.js 改成:
js
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const routes = [
]
const router = new VueRouter({
routes
})
export default router易错点:不要只删数组里的路由对象,却忘记删上面的 import HomeView from '../views/HomeView.vue' 等语句;也不要改 main.js 里已有的挂载逻辑。routes 为空是本节预期状态,不是漏写。
修改 App.vue
为什么需要:App.vue 是根组件,默认模板里常带示例结构;本节只保留路由出口,让页面内容由路由控制。
怎么想:既然示例组件已删除,根组件就不应再引用它们;最简结构是 #app 容器加 <router-view/>。
怎么做:把 App.vue 改成:
html
<template>
<div id="app">
<router-view/>
</div>
</template>易错点:不要保留 HelloWorld 的引入和注册;不要多包一层无意义结构。main.js 不需要修改,不要因为 App.vue 变短又去改入口。
新增 api 和 utils 目录
为什么需要:笔记明确:src/api 存储接口模块,也就是发送 ajax 请求接口的模块;src/utils 存储一些工具模块,也就是自己封装的方法。转写稿回顾请求/响应拦截器时也说明,接口配置和工具方法后续会反复使用,先给它们固定落脚点。
怎么想:目录名按企业规范统一用小写:api、utils。不要临时把接口写在组件里,也不要把工具方法散落在页面中。
怎么做:在 src 下新增:
src/api目录:存储接口模块(发送 ajax 请求接口的模块)src/utils目录:存储一些工具模块(自己封装的方法)
调整后的目录效果如下:

易错点:目录名不要写成 Api、Utils 造成大小写混乱;不要在本节提前编造接口文件内容,笔记只要求建目录。
最后检查
按“删除→修改→新增”的顺序检查:默认示例文件是否删净;router/index.js 是否还有默认路由残留;App.vue 是否只保留 router-view;api、utils 是否已建好;main.js 是否保持不动。这样整理后,后续再加路由、导航守卫、请求模块时,目录和入口都是干净的。
组件库与 Vant:为什么用、怎么选
组件库,就是把第三方已经封装好的大量组件整合到一起。它的价值在于:日历、键盘、打分、登录这类常见但细节多的界面,不必每次从零写。Vant 的地址是:https://vant-contrib.gitee.io/vant/v2/#/zh-CN/ 。笔记中给了示意图:

组件库不是唯一的。PC 端常见有 element-ui、element-plus、iview、ant-design;移动端常见有 vant-ui、Mint UI、Cube UI。怎么想:先看项目端型,再看团队技术栈和组件覆盖度。怎么做:按官方文档引入、按需使用。易错点:不要为了一个按钮引入整套库;移动端别照搬 PC 组件库。
路由守卫与请求/响应拦截器:登录权限的定式
为什么需要:没有 token 时,用户不应进入需要登录的页面;请求要自动带 token,响应要统一处理 token 过期。怎么想:路由守卫放在创建 router 变量之后、导出之前;请求拦截器统一加请求头,响应拦截器统一判断状态码。怎么做,转写稿中的核心写法可整理为:
js
router.beforeEach((to, from, next) => {
const token = localStorage.getItem('token')
if (!token && to.path !== '/login' && to.path !== '/register') {
next('/login')
} else {
next()
}
})js
// request.js 思路:请求拦截器加 token,响应拦截器处理 401
service.interceptors.request.use(config => {
const token = localStorage.getItem('token')
if (token) config.headers.Authorization = token
return config
})易错点:守卫位置不能在 router 创建前;next(false) 是禁止访问,next('/login') 是跳到指定地址;拦截器是“定式”,但状态码和字段名要按接口文档来,不能死记。
axios 万能写法与两类特殊参数
为什么需要:接口文档每天不同,不能只背 axios.get。怎么想:先掌握完整写法,能发任何请求;再看参数放在 query 还是 body。怎么做:
js
axios({
method: 'post',
url: '/api/student',
params: { id: 1 }, // query/查询参数
data: { name: '张三' } // body/请求体
})两种特殊情况:提交 FormData 时,先 const fd = new FormData(),用 fd.append('username', '张三') 追加,再 data: fd;路径参数如 /student/:id,把具体 id 拼到 URL 后面。易错点:params 和 data 别混;FormData 不能直接写大括号对象;name 在插槽里有特殊含义,不要拿来当普通传值属性。转写稿反复强调:axios 学一个万能写法就够用,别名写法会更好,不会也不影响完成。
$nextTick:数据更新后立刻操作 DOM
为什么需要:数据改了,页面不会“立即”更新,而是等下次事件循环再更新视图,避免多次渲染浪费性能。怎么想:如果改完 flag 马上找输入框,DOM 还没出来,所以找不到。怎么做:把操作放进 this.$nextTick:
js
showInput() {
this.flag = true
this.$nextTick(() => {
this.$refs.ipt.focus()
})
}易错点:$nextTick 不是“延迟随便等”,而是等 DOM 更新后执行;最早可做 DOM 操作的生命周期是 mounted,不是 created;在 created 里操作 DOM 也要放进 $nextTick。
插槽基础:把可变结构挖坑
为什么需要:封装对话框时,标题、内容、按钮都可能变化,内容不能写死。怎么想:子组件用 <slot> 占位,父组件在使用组件时把结构传进去。怎么做:
html
<!-- 子组件 -->
<div class="dialog">
<h3>友情提示</h3>
<div class="content"><slot></slot></div>
<button>确认</button>
</div>
<!-- 父组件 -->
<my-dialog>
<p>你确定要删除吗?</p>
</my-dialog>易错点:插槽传的是“结构”,父子传值主要传“数据”;如果内容可能是表格、表单,用 props 传 HTML 结构会很臃肿。
具名插槽与作用域插槽:区分坑位和传数据
为什么需要:一个对话框有三个坑位,不命名就会串位。怎么想:给 <slot name="title">、<slot name="btn"> 起名,内容槽可不写名字,默认名是 default。父组件用 template 包裹并写 v-slot:title,可简写为 #title。怎么做:
html
<slot name="title"></slot>
<slot></slot>
<slot name="btn"></slot>html
<template #title><h3>温馨提示</h3></template>
<p>你确定要删除吗?</p>
<template #btn><button>确定</button></template>作用域插槽是子组件通过插槽向父组件传数据:<slot :username="username" :age="age"></slot>,父组件 #default="{ username, age }" 接收。易错点:v-slot 只能加在自定义标签或 template 上,不能直接加在 h3、div 等原生标签上;接收到的数据只能在该 template 作用域内使用,这就是“作用域插槽”的名称来源。
课件图示








代码示例
1. $nextTick —— 数据更新后操作 DOM(Vue2 可直接运行)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>$nextTick 示例</title>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
</head>
<body>
<div id="app">
<h1>{{ title }}</h1>
<button @click="showIpt">编辑</button>
<!-- v-if:数据为 true 才渲染出 DOM -->
<div v-if="flag">
<input type="text" ref="ipt" v-model="newTitle" />
<button @click="confirm">确认</button>
</div>
</div>
<script>
new Vue({
el: '#app',
data: {
title: '原标题',
newTitle: '',
flag: false
},
methods: {
showIpt () {
this.flag = true
// ❌ 错误写法:this.$refs.ipt.focus()
// 此时页面还没更新,输入框根本不存在,会报 undefined
this.$nextTick(() => {
// ✅ 等 DOM 更新完,ref 才拿得到输入框
this.$refs.ipt.focus()
})
},
confirm () {
this.title = this.newTitle
this.flag = false
}
}
})
</script>
</body>
</html>要点解释
this.$nextTick(fn):Vue 内置方法,名字带$是为了和开发者自己的属性区分(同$emit、$refs、$route)。- 数据是同步更新的,视图是异步更新的:
this.flag = true之后立刻访问$refs.ipt拿到的是undefined。 - 为什么要异步:如果数据一改就渲染,
flag反复 true/false 会导致多次渲染,浪费性能;Vue 收集完本轮所有数据变化,在下次事件循环里一次性更新页面。 - 易错点:
$nextTick必须传一个函数;想要在created里操作 DOM,同样要放进$nextTick里。
2. 插槽:默认插槽 / 具名插槽 / 作用域插槽(对话框组件)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>插槽示例</title>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<style>
.dialog { width: 320px; border: 1px solid #ddd; border-radius: 6px; }
.header { display: flex; justify-content: space-between; padding: 8px 12px; border-bottom: 1px solid #eee; }
.content { padding: 16px 12px; }
.footer { padding: 8px 12px; border-top: 1px solid #eee; text-align: right; }
</style>
</head>
<body>
<div id="app">
<my-dialog>
<!-- 具名插槽:给 name="title" 的坑位传结构 -->
<template #title>
<h3>温馨提示</h3>
</template>
<!-- 默认插槽:接收子组件通过 slot 传来的数据 -->
<template #default="obj">
<p>你确定要删除吗?{{ obj.userName }},{{ obj.age }} 岁</p>
</template>
<!-- 具名插槽:#btn 等价于 v-slot:btn -->
<template #btn>
<button>取消</button>
<button>确认</button>
</template>
</my-dialog>
</div>
<script>
Vue.component('my-dialog', {
// 子组件:三处内容都不写死,全部用 slot 挖坑
template: `
<div class="dialog">
<div class="header">
<slot name="title"></slot>
<span class="close">×</span>
</div>
<div class="content">
<!-- 作用域插槽:在 slot 上写自定义属性,把数据传给父组件 -->
<slot user-name="张三" age="20"></slot>
</div>
<div class="footer">
<slot name="btn"></slot>
</div>
</div>
`
})
new Vue({ el: '#app' })
</script>
</body>
</html>要点解释
<slot>只是占位(挖坑),最终不会渲染成任何标签,所以不会多出一层div。- 不写
name的插槽就是默认插槽,它其实有隐藏名字default;写了name的叫具名插槽。 - 父组件里没被
<template>包裹的结构,全部会塞进默认插槽——所以三个区域都要传内容时,必须用<template #xxx>区分开。 v-slot:title可简写为#title,这是最后一个能简写的指令。- 易错点①:
v-slot只能写在<template>或组件标签上,不能直接加到h3、div这类原生标签上(会报v-slot can only be used on components or <template>)。 - 易错点②:作用域插槽里,子组件
slot上的属性名不能叫name,name是插槽名字,会被当成具名插槽。 - 易错点③:父组件
#default="obj"拿到的是一个对象({ userName, age }),只能在当前<template>内使用,这就是“作用域”的含义;也可以直接解构#default="{ userName, age }"。
3. axios 万能写法:只有 data 和 url 会变
js
import axios from 'axios'
// ① 万能写法:任何请求都用这一套,method / url / params / data 按接口文档填
axios({
method: 'post',
url: '/api/student',
data: { name: '张三', age: 18 } // 请求体(body)参数放 data
})
// ② query 参数 / 查询参数 —— 放 params
axios({
method: 'get',
url: '/api/student',
params: { id: 1 }
})
// ③ 路径参数:接口文档里 URL 写成 /api/student/:id,就把具体 id 拼到路径后面
axios({
method: 'delete',
url: '/api/student/1001'
})
// ④ 提交 form-data(上传文件):data 不能直接写对象,要先 new FormData()
const fd = new FormData()
fd.append('username', '张三')
fd.append('sex', '男')
axios({
method: 'post',
url: '/api/upload',
data: fd
})
// ⑤ 统一配置:请求拦截器加 token,响应拦截器处理 token 过期
const request = axios.create({ baseURL: 'http://localhost:3000' })
request.interceptors.request.use(config => {
const token = localStorage.getItem('token')
if (token) config.headers.Authorization = `Bearer ${token}`
return config
})
request.interceptors.response.use(
res => res.data,
err => {
if (err.response && err.response.status === 401) {
// token 过期:清掉本地存储,跳登录
localStorage.removeItem('token')
location.href = '#/login'
}
return Promise.reject(err)
}
)要点解释
- 只记住一种语法就能应付所有接口:
axios({ method, url, params, data }),method和url直接照抄接口文档,不用背。 params对应文档里的 query / 查询参数;data对应 body / 请求体。- 唯一两种特殊情况:① 路径参数(URL 里写了
:id),把具体值直接拼到路径后面;② 提交form-data,先new FormData()再append,最后把fd交给data。 - 拦截器几乎是固定模板:请求拦截器统一带 token,响应拦截器统一处理 401 过期,任何用 token 的项目都是这个处理方案。
常见错误
1. 保存后 ESLint 没自动修复,控制台依旧一堆红
- 现象:按下 Ctrl+S,代码纹丝不动,终端照样报
Extra semicolon/Unexpected space before function parentheses。 - 原因:三种可能——没装 ESLint 插件;
settings.json里没写editor.codeActionsOnSave;或者 VSCode 打开的是"父文件夹"(一次开了好几个项目),插件在根目录找不到.eslintrc.js;也可能 Beautify/Prettier 抢着格式化,把 ESLint 的修复又改回去了。 - 修法:装 ESLint 插件 → 配置
"editor.codeActionsOnSave": { "source.fixAll": true }与"editor.formatOnSave": false→ 禁用 Beautify 等格式化工具 → 一次只开一个项目,且以项目根目录打开。
2. 路由守卫不生效,或报 router is not defined
- 现象:没登录也能直接进列表页;或者一保存就报错。
- 原因:
beforeEach写在了new VueRouter()之前(此时 router 变量还不存在),或写在了export default router之后(模块已经导出,再挂守卫无效)。 - 修法:固定顺序 ——
Vue.use(VueRouter)→const router = new VueRouter({...})→router.beforeEach(...)→export default router。
3. 守卫里无限重定向,页面白屏打转
- 现象:地址栏疯狂跳转,页面空白,控制台报最大调用栈。
- 原因:判断条件只写了"没有 token 就
next('/login')",没排除"当前访问的就是登录/注册页",于是登录页也被拦,又跳登录页。 - 修法:条件写成"没有 token 且 目标不是 /login、/register"才拦截,其余情况一律
next()放行。
4. axios 参数放错位置,后端收不到值
- 现象:接口 200 但返回"参数缺失",或后端说 body 是空的。
- 原因:把 query 参数塞进
data、把 body 参数塞进params;或者传对象时漏了大括号,直接写id: 1当裸值;又或者上传接口没用 FormData。 - 修法:对照接口文档——"查询参数/query"→
params;"请求体/body"→data;Content-Type: multipart/form-data→ 先const fd = new FormData(),fd.append(...)逐个追加,再data: fd。参数无论几个,永远保持对象写法。
5. 路径参数接口 404 或提示 id 为空
- 现象:
/students/:id这类接口请求失败。 - 原因:把
:id当成普通参数丢进 params/data,或者原样保留冒号没替换。 - 修法:用模板字符串把真实 id 拼到 URL 上,如
`/students/${id}`。
6. $nextTick 忘写,ref 拿到 undefined
- 现象:点"编辑"后输入框出来了,但
this.$refs.ipt是 undefined,focus()报错;或者设置背景色无效。 - 原因:改完
flag立刻去取 DOM,此时视图还没更新(Vue 是异步更新视图),元素根本还不存在。 - 修法:把依赖新 DOM 的代码整段放进
this.$nextTick(() => { ... })里。
7. 具名插槽内容全挤进默认插槽
- 现象:标题、内容、按钮三块内容全跑到内容区,其他位置空白。
- 原因:父组件传结构时没加
<template #xxx>包裹,未包裹的内容一律默认投递给默认插槽;或者#title与子组件name="title"拼写不一致。 - 修法:每个具名插槽的内容都用
<template #名字>包起来,名字与子组件 slot 的name严格一致(v-slot:title可简写为#title)。
8. v-slot 直接加在 h3/div 上编译报错
- 现象:提示 "v-slot can only be used on components or
<template>"。 - 原因:
v-slot只能加在组件或<template>上,不能加在原生标签。 - 修法:外面套一层
<template #title>;<template>是虚拟标签,最终不会渲染出多余节点,不会破坏结构。
9. 作用域插槽传值传不过去,反而变成了具名插槽
- 现象:子组件
<slot name="张三" age="20">,父组件收不到数据。 - 原因:
name是插槽的保留属性,一写就把默认插槽变成了名为"张三"的具名插槽。 - 修法:换成
username、userName等非保留属性名;父组件在对应位置的<template #default="{ username }">里接收,解构后只能在当前 template 内使用。
练习
练习 1|脚手架与规范落地 用 vue-cli 手动模式创建一个 Vue2 + less + Standard 规范的项目,跑起来后故意在 main.js 里制造 3 处格式错误(多余空格、双引号、多余分号),先根据报错代码去 ESLint 规则表查出含义并手动改掉;再配置 VSCode 让它保存即自动修复,最后把两套流程的差异写进一句话笔记。
练习 2|request 封装定式 新建 src/utils/request.js,基于 axios 完成:baseURL 统一配置、请求拦截器自动带 token、响应拦截器判断状态码 401 时清空本地 token 并跳登录页。要求写完后能说出:这个文件在下一个项目里你能不看笔记重写出来吗?
练习 3|三种接口写法各写一遍 假设接口文档给出三个接口:① GET /students(query: page, size)② POST /students(body JSON)③ POST /upload(form-data: file, username)。用 axios 的"万能写法"分别写出请求代码,并额外把 ③ 用 new FormData() 实现一次。
练习 4|$nextTick 场景复现 做一个"标题 + 编辑按钮"的组件:点击编辑后显示输入框并让它立刻获得焦点、加一个背景色。先故意不写 $nextTick 观察失败现象,再补上 $nextTick 观察成功现象,最后用一句话解释失败原因。
练习 5|插槽三连 封装一个 MyDialog 组件,包含头部、内容区、底部三个区域:内容区用默认插槽,头部用 #title,底部用 #btn;再让头部插槽通过作用域插槽向父组件抛出 { title, count } 两个值,父组件解构后渲染。要求至少使用一次 # 简写和一次解构赋值。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 反馈和回顾 | 24:15 |
| 2 | $nextTick | 14:39 |
| 3 | 插槽的基本语法 | 13:11 |
| 4 | 具名插槽 | 15:12 |
| 5 | 作用域插槽 | 10:02 |
小结
- 守卫位置比守卫逻辑更容易出错:
Vue.use→ 创建实例 → 挂守卫 → 导出,这个顺序记死;拦截条件必须把登录/注册页排除在外,否则必然死循环。 - axios 只需要吃透"万能写法"一种:
axios({ method, url, params, data })能覆盖所有场景,别名写法属于锦上添花;真正会变的只有两处——URL 里的路径参数要拼、form-data 要先new FormData()。 request.js已经是定式:请求拦截器加 token、响应拦截器处理 401,任何带登录的项目都一样,后面应当做到不假思索直接写完,而不是每次重新推导。- ESLint 报错不是障碍而是导航:不认识的规则就去规则表搜代码名,配合插件自动修复,把"改代码"变成"改习惯",这才是它存在的意义。
$nextTick的本质是"等一等":数据是同步改的、视图是异步更新的,凡是"改完数据马上要操作 DOM",一律先$nextTick,这是唯一可靠的判断标准。- 插槽是传结构,props 是传数据:需要塞一段 HTML(表格、表单、多行结构)就用插槽;只需要塞一个值就用 props。用 props 传一大段标签结构,代码量会失控。
- 具名插槽的核心是"对得上号":子组件
name与父组件#名字一一对应,没被<template>包裹的内容一律进默认插槽——出现"内容全跑到一处"时,先查这个。 - 作用域插槽 = 带作用域的变量:子组件通过 slot 属性往上抛对象,父组件在对应
<template>里接收(可解构),出了这个 template 就用不了;同时切记别用name当数据属性名。