Skip to content

插槽(一) ​

本页对应 插槽,共 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 模式

选择 CSS 预处理器

易错点:history 模式如果部署环境没有服务端回退配置,刷新子路由可能 404;CSS 预处理器选完后,样式文件后缀和语法要统一。

ESLint 风格、校验时机与配置文件 ​

为什么需要:ESLint 是代码规范检验工具,能提前发现不符合规范的写法。笔记举例:const age = 18; 多加分号会报错,后面有工具一保存就全部格式化成最规范的样子。怎么想:规范不是为了刁难,而是减少团队差异;先选风格,再决定校验时机和配置放哪。怎么做:选择 ESLint 风格;校验时机直接回车;配置文件生成方式直接回车。

选择 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 控制台错误提示

为什么需要先制造错误:只有亲眼看到提示,才知道 ESLint 检查的是什么、错误出现在哪一行。怎么想:保存后不要急着关掉控制台,先读提示。怎么做:看控制台列出的错误代码和行号,例如 func-call-spacing、space-in-parens 等。易错点:不要一看到红字就关闭 ESLint;也不要只看“有错”却不看具体规则代码。转写稿里处理其他知识点时也反复强调“按套路来、遇到不会就查文档”,这个调试习惯同样适用于 ESLint:读错误、查规则、改代码、保存复查。

看懂错误代码并手动修复 ​

手动修复的原则是:根据错误提示一项一项修正。如果你不认识命令行中的语法报错是什么意思,可以根据错误代码去 ESLint 规则列表中查找具体含义。打开 ESLint 规则表 https://zh-hans.eslint.org/docs/latest/rules/,使用页面搜索(Ctrl + F)这个代码,查找对该规则的解释。

ESLint 规则表搜索示例

为什么需要查规则表:错误代码是规则的“名字”,知道名字才能准确理解它要求什么。怎么想:不要凭感觉把空格删掉或加上,先确认规则含义。怎么做:例如示例中的 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 提示。

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.png
  • src/components/HelloWorld.vue
  • src/views/AboutView.vue
  • src/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/ 。笔记中给了示意图:

Vant组件库示例

组件库不是唯一的。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 作用域内使用,这就是“作用域插槽”的名称来源。

课件图示 ​

课件第3页:项目收获 目标:明确做完本项目,能够收获哪些内容 脚手架自定义项目构建 组件库v

课件第17页:目标:通过 配置主题色,实现底部导航定制 路由设计配置 配置主题色: ① bab

课件第4页:创建项目 目标:基于 VueCli 自定义创建项目架子 安装脚手架 (√) 创建

课件第5页:ESlint 代码规范 目标:认识代码规范 代码规范:一套写代码的约定规则。例如

课件第7页:代码规范错误 目标:学会解决代码规范错误 两种解决方案: ① 手动修正 ② 自动

课件第10页:其他 Vue 组件库 目标:了解其他 Vue 组件库 Vue的组件库并不是唯一的

课件第11页:vant 全部导入 和 按需导入 目标:明确 全部导入 和 按需导入 的区别 v

课件第12页:目标:阅读文档,掌握 全部导入 的基本使用 官网:vant-ui 全部导入: ①

代码示例 ​

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$nextTick14:39
3插槽的基本语法13:11
4具名插槽15:12
5作用域插槽10:02

小结 ​

  1. 守卫位置比守卫逻辑更容易出错:Vue.use → 创建实例 → 挂守卫 → 导出,这个顺序记死;拦截条件必须把登录/注册页排除在外,否则必然死循环。
  2. axios 只需要吃透"万能写法"一种:axios({ method, url, params, data }) 能覆盖所有场景,别名写法属于锦上添花;真正会变的只有两处——URL 里的路径参数要拼、form-data 要先 new FormData()。
  3. request.js 已经是定式:请求拦截器加 token、响应拦截器处理 401,任何带登录的项目都一样,后面应当做到不假思索直接写完,而不是每次重新推导。
  4. ESLint 报错不是障碍而是导航:不认识的规则就去规则表搜代码名,配合插件自动修复,把"改代码"变成"改习惯",这才是它存在的意义。
  5. $nextTick 的本质是"等一等":数据是同步改的、视图是异步更新的,凡是"改完数据马上要操作 DOM",一律先 $nextTick,这是唯一可靠的判断标准。
  6. 插槽是传结构,props 是传数据:需要塞一段 HTML(表格、表单、多行结构)就用插槽;只需要塞一个值就用 props。用 props 传一大段标签结构,代码量会失控。
  7. 具名插槽的核心是"对得上号":子组件 name 与父组件 #名字 一一对应,没被 <template> 包裹的内容一律进默认插槽——出现"内容全跑到一处"时,先查这个。
  8. 作用域插槽 = 带作用域的变量:子组件通过 slot 属性往上抛对象,父组件在对应 <template> 里接收(可解构),出了这个 template 就用不了;同时切记别用 name 当数据属性名。