主题
面经 PC 端(下)(二)
本页对应 面经 PC 端(下),共 7 段视频,约 1 小时 30 分。
学习目标
- 能说明 Sass/SCSS 与 Less 在语法格式(分号、花括号、缩进、变量声明符)上的差异,并在 Vue 单文件组件中通过
<style lang="scss" scoped>正确编写嵌套样式与变量。 - 能在
src下新建 SCSS 主题变量文件并在入口文件中引入,通过覆盖 Element Plus 的 SCSS 变量将全局主题色替换为指定颜色,并验证按钮、文字链接等组件颜色同步生效。 - 能使用
el-card的具名插槽与默认插槽搭建登录卡片布局,并通过:deep()样式穿透修改子组件内部(如el-card__header)的样式,实现标题居中与背景色设置。 - 能使用
el-form、el-form-item、el-input搭建登录表单,正确配置:model、:rules、prop、v-model之间的对应关系,并写出必填、长度等校验规则使错误提示正常显示。 - 能通过
ref获取表单实例并调用validate与resetFields,实现点击登录时的完整校验(校验通过才提交、不通过不提交)以及点击重置按钮恢复初始值。 - 能按“先封装 API、再在组件中调用”的套路在
api/user.js中基于request封装登录接口(含 URL、POST 方法、body 参数),并在登录方法中用async/await调用该接口获取响应结果。
知识点
一、先把问题看清楚:为什么要封装 API
为什么需要:以前的写法是页面里到处写请求代码,页面既要管交互,又要管接口地址、请求方法、参数。这样可阅读性不高,相同请求没有复用,也没有统一管理。注册功能如果也把请求直接写在 register.vue 里,后面登录、退出、获取用户信息就会继续复制,问题会越来越多。
怎么想:把“发请求”看成数据访问层,页面只负责收集数据、提示结果、跳转页面;接口地址、请求方法、参数交给 api 模块统一维护。也就是笔记里的目标:将请求封装成方法,统一存放到 api 模块,与页面分离。
怎么做:先对比旧模式和新模式。


从图里可以看出,旧模式是页面直接承担请求细节;期望模式是把请求抽到 API 层,页面调用方法即可。
易错点:不要只“搬代码”不设计目录;不要一个接口在多个页面重复写;不要页面里还残留 request.post 或 Ajax 细节。转写稿里说的“阿杰克斯”应写为 Ajax,“结口”应写为接口。
二、设计 api/user.js:注册请求应该放在哪里
为什么需要:注册、登录、退出、获取管理员信息都属于用户相关请求。如果散落在各个页面,地址改了要全项目搜;参数格式变了也容易漏改。按业务分模块,用户相关统一放 api/user.js,后续好找、好复用、好维护。
怎么想:api/user.js 只做一件事:导入统一请求工具 request,导出具体接口函数。函数名用业务名,比如 register;请求方法、地址、参数在这里固定下来。页面不需要知道 /user/register 这个细节。
怎么做:新建 api/user.js,提供注册 API 函数:
jsx
import request from '@/utils/request'
// 注册接口
export const register = (data) => {
return request.post('/user/register', data)
}这里 data 是注册页提交的数据,request.post 表示用 POST 提交,地址是 /user/register。注意必须 return,否则页面调用 await register(values) 拿到的是 undefined,后续无法判断请求结果。
易错点:忘记 import request;函数没有 return;请求方法写成 GET;地址、参数名与接口文档不一致;把注册函数写到登录模块里,职责混乱。转写稿封装登录 API 时也是同一套路:先导入 request,再导出函数,函数里 return request.post(...),这个推导可以直接迁移到注册。
三、register.vue 中调用:页面只处理交互
为什么需要:页面是用户操作入口,应该关注“提交注册→提示成功→跳转登录”。如果页面里直接拼接口地址,页面逻辑和接口细节耦合,复用和测试都困难。
怎么想:页面先按需导入 register,在提交方法中把表单数据 values 传进去。因为请求是异步的,方法要写成 async,请求前加 await,等请求完成后再提示和跳转。
怎么做:register.vue 页面中调用测试:
jsx
methods: {
async onSubmit (values) {
// 往后台发送注册请求了
await register(values)
alert('注册成功')
this.$router.push('/login')
}
}这段代码体现三点:导入 register 后直接调用;values 作为注册数据传入;请求成功后再 alert 并跳转 /login。
易错点:忘记从 @/api/user 导入;onSubmit 没写 async 却使用 await;await 加错位置。转写稿里特别提醒过:await 要加在离它最近的 async 函数前面,不要加到普通代码块或错误层级。还有一点:示例只写了成功路径,实际项目中失败情况要按统一请求工具的错误处理机制处理,不能把“请求发出去了”当成“注册成功”。
四、调试与验收:按请求链路排查
为什么需要:封装后页面代码变短了,但出问题时不能只看页面。要确认请求有没有发出、地址和方法对不对、参数有没有带上、响应是什么、成功后有没有跳转。转写稿演示登录请求时,就是通过控制台和网络面板看请求成功/失败;错误请求能看到请求方式、地址、状态码和响应消息。注册调试也应沿这个链路查。
怎么想:先看 register(values) 是否被调用;再看 Network 里有没有 /user/register 请求;然后看请求方法是不是 POST、参数是不是表单数据;最后看响应和页面跳转。若请求没发出,多半是导入、方法名、事件绑定或 await 位置问题;若请求发出但报错,按接口文档核对地址、方法、参数。
怎么做:提交注册时观察控制台和 Network;成功时确认 alert('注册成功') 和 this.$router.push('/login') 执行;失败时先不要继续跳转,按项目统一错误提示处理。转写稿里封装登录 API 的过程也说明:组件中按需导入 API 方法,在验证通过后调用,并用 await 等结果;这套调试思路同样适用于注册。
易错点:只看 alert 不看请求结果;把接口地址写错还以为是页面问题;请求参数没传或字段名不对;失败时仍然跳转登录页;在页面里又手写一份请求,导致 API 模块形同虚设。
一、先明确 Toast 的定位与文档入口
为什么需要:注册、登录、加载数据这类操作,用户需要即时反馈,但又不想被弹窗打断。Toast 轻提示适合“轻量、短时、不阻塞流程”的提示。
怎么想:先看官方文档,确认当前项目用的是 Vant 2。笔记给出的入口是:https://vant-contrib.gitee.io/vant/v2/#/zh-CN/toast 。不要凭印象写 API,先以文档和本节笔记为准。
怎么做:把 Toast 当成“函数调用”来用,而不是先写一个组件标签。它有两种接入方式:组件内/JS 文件内按需导入调用;或者在 main.js 里 Vue.use(Toast) 后通过 this.$toast 调用。
易错点:不要把 Toast 和 Dialog 混用;不要在没有导入、也没有全局挂载的情况下直接写 this.$toast。本节笔记没有提到的属性不要自行编造。
二、方式一:组件内或 JS 文件内导入后调用
为什么需要:只在真正用到提示的文件里导入,依赖关系清楚,适合按需使用。
怎么想:既然 Toast 是函数,就先 import { Toast } from 'vant',再直接调用。它不依赖组件模板。
jsx
import { Toast } from 'vant';
Toast('提示内容');怎么做:在业务逻辑处调用 Toast('提示内容')。例如表单提交成功后给一句轻提示。
易错点:Toast 是具名导入,不要写成默认导入;调用时不要漏掉括号。若在 Vue 组件的方法里使用,注意当前文件是否已经导入。
三、方式二:在 main.js 全局挂载,组件内用 this.$toast
为什么需要:项目里提示很多,如果每个组件都导入一次,代码会重复。全局挂载后,组件内调用更统一。
怎么想:把 Toast 注册到 Vue 原型上,之后组件实例就能通过 this.$toast 访问。
怎么做:在 main.js 中写:
js
import { Toast } from 'vant';
Vue.use(Toast)组件内调用:
jsx
this.$toast('提示内容')易错点:只写 import 不写 Vue.use(Toast),this.$toast 不可用;Vue.use 只执行一次即可,不要在每个组件里重复挂载。全局挂载后仍要注意:this 必须指向 Vue 组件实例。
四、在注册/登录异步流程中组合 loading、success、fail
为什么需要:异步请求有等待过程,也有成功和失败结果。只用一句普通 Toast,用户不知道请求是否在进行;请求失败时也需要明确结果。
怎么想:转写稿里表单验证部分强调过一个结论:先做完整校验,校验通过才提交;不通过就什么也不做,因为页面已经有提示。迁移到 Toast 就是:请求前给 loading,await 成功后再 success,进入 catch 再 fail。转写稿里提到的“阿杰克斯/Ajax 错误”要看请求方式、地址和状态码,这里对应到业务上就是把后端返回的失败结果用 fail 提示出来。
怎么做:保留笔记中的代码演示:
js
this.$toast.loading({
message:'拼命加载中...',
forbidClick:true
})
try{
await register(values)
this.$toast.success('注册成功')
this.$router.push('/login')
}catch(e){
this.$toast.fail('注册失败')
}forbidClick:true 的作用是加载期间禁止背景点击,避免用户重复提交。await register(values) 表示等待注册请求完成;成功后提示“注册成功”并跳转登录页;失败进入 catch,提示“注册失败”。
易错点:第一,await 必须写在 async 函数里,转写稿里也强调“离 await 最近的函数前面加 async”,否则会报语法错误。第二,不要把 success 写在 await 前面,否则请求还没回来就提示成功。第三,catch(e) 不要空着,至少给出失败提示;但也不要吞掉所有错误后继续跳转。第四,loading 适合放在请求前,success/fail 放在结果处,不要三处同时乱弹。第五,表单校验没通过时不要直接发请求,这与转写稿中“完整校验通过再提交”的调试结论一致。
五、调试与易错点小结
为什么需要:Toast 代码很短,但位置放错就会造成“没请求先成功”“失败还跳转”“重复点击”等问题。
怎么想:按“校验 → loading → await 请求 → success/fail → 跳转或停留”的顺序检查。转写稿里出现的“表验证”应理解为“表单验证”,“同置”应理解为“重置”,“阿杰克斯/阿里克斯”应理解为“Ajax”,这些同音字在调试时要还原成正确概念。
怎么做:先确认导入或全局挂载;再确认 this.$toast 的 this 正确;最后检查 async/await 和 try/catch 的位置。请求失败时先看请求方式、地址、状态码,再把业务失败信息交给 fail。
易错点:不要编造本节笔记没有的 Toast 配置;不要用 Toast 代替表单校验;不要在 catch 后继续执行成功跳转。把这几条守住,Toast 的两种用法和异步提示流程就能稳定落地。
先理解:响应拦截器是拿到数据的第一站

请求发出去之后,服务端返回的数据不会直接落到组件里,而是先经过响应拦截器。笔记里说它是我们拿到数据的第一个“数据流转站”,原因就在这里:成功数据要先在这里“卸一层壳”,错误信息也要先在这里“过一道手”。
为什么需要:如果每个页面都自己判断 response.data、自己判断错误、自己弹提示,代码会重复,而且容易漏。怎么想:把“成功时返回什么”“失败时提示什么”统一放到拦截器里。怎么做:在 request.interceptors.response.use 里写两个回调,第一个处理成功响应,第二个处理失败响应。易错点:不要把拦截器当成“发请求的地方”,它只负责在响应回来时做统一加工。
成功分支:把 response.data 交出去
jsx
import { Toast } from 'vant'
...
// 添加响应拦截器
request.interceptors.response.use(function (response) {
// 对响应数据做点什么
return response.data
}, function (error) {
if (error.response) {
// 有错误响应, 提示错误提示
Toast(error.response.data.message)
}
// 对响应错误做点什么
return Promise.reject(error)
})为什么需要:axios 返回的 response 里包含 data、status、headers 等,业务代码通常只关心 response.data。怎么想:拦截器成功分支直接把 response.data 返回,后面组件里拿到的就是业务数据,而不是完整响应对象。怎么做:如代码所示,成功回调 return response.data。易错点:返回 response 而不是 response.data,组件里就要多写一层 .data;如果接口本身返回结构特殊,也要按项目约定处理,不要盲目照搬。
错误分支:统一用 Toast 提示后端消息
为什么需要:登录失败、密码错误、管理员不存在等错误,后端通常会在响应体里给 message。如果每个请求都写一遍提示,既啰嗦又容易漏。怎么想:先判断 error.response 是否存在——有响应说明服务端返回了错误状态码;没有响应可能是网络断开、请求超时等。怎么做:代码里用 Toast(error.response.data.message) 弹出提示,然后 return Promise.reject(error) 把错误继续抛给调用处。易错点:不要吞掉错误。只 Toast 不 reject,调用处的 catch 就收不到;也不要假设所有错误都有 error.response.data.message,实际项目里可做兜底,但本节按笔记保留原写法。
转写稿视频7的调试过程可以印证这一点:先正常登录,请求成功;再故意把密码写错,页面出现报错,转写稿里说“这种就是 axios 性的错误”,这里应纠正为 axios 错误;到网络面板看“响应结果”,里面能看到“管理员不存在或者是用户密码错误”。所以拦截器读取的正是 error.response.data.message,而不是随便编一句提示。
调试顺序:先成功,再故意失败
为什么需要:只看代码不容易确认拦截器到底有没有生效。怎么想:先用正确账号密码走成功分支,确认组件拿到的是 response.data;再故意输错密码走失败分支,确认 Toast 弹出的是后端 message。怎么做:转写稿里的操作是“登录啊登录”,请求成功后,再“随便写一个”错误密码登录,观察报错;随后在浏览器 Network 里点开失败请求,看响应结果中的错误消息。易错点:看到带状态码的请求地址就慌,其实这类是 axios 错误,先看 error.response 和响应体;另外“相应结果”应写作“响应结果”,“结口”应写作“接口”。
易错点小结
第一,成功分支返回 response.data,不要返回整个 response。第二,错误分支要 Promise.reject(error),否则调用处无法继续处理。第三,Toast 提示的是后端 message,调试时以 Network 响应结果为准。第四,本节只截取了响应拦截器片段,request 实例的创建在前文,不要凭空补不存在的配置。把这一层统一好,后面登录、获取管理员信息等请求都能复用同一套错误提示逻辑。
课件图示








代码示例
1. utils/request.js —— 响应拦截器统一处理错误提示
js
// utils/request.js
import axios from 'axios'
import { Toast } from 'vant'
// 创建 axios 实例(baseURL 按项目实际接口地址配置)
const request = axios.create({
baseURL: '/api',
timeout: 5000
})
// 添加响应拦截器
request.interceptors.response.use(function (response) {
// 对响应数据做点什么
return response.data
}, function (error) {
if (error.response) {
// 有错误响应, 提示错误提示
Toast(error.response.data.message)
}
// 对响应错误做点什么
return Promise.reject(error)
})
export default request关键行与易错点
return response.data:响应拦截器是拿到数据的第一个“数据流转站”,这里剥一层壳后,api层和页面层拿到的就是业务数据,不用再写res.data.data。- 错误回调里必须先
if (error.response)判断:断网、超时、跨域时error.response是undefined,直接error.response.data.message会二次报错。 - 末尾一定要
return Promise.reject(error):漏掉的话 Promise 会被当成成功,页面里的try/catch永远进不了catch,this.$toast.fail也不会执行。 - 这里用了
Toast(...)函数式调用,所以是import { Toast } from 'vant';如果想让页面里能用this.$toast,还要在main.js中Vue.use(Toast)。
2. api/user.js —— 将请求封装成方法,与页面分离
js
// api/user.js
import request from '@/utils/request'
// 注册接口
export const register = (data) => {
return request.post('/user/register', data)
}关键行与易错点
- 以前页面里充斥着请求代码,可阅读性不高、相同请求没法复用、也没有统一管理;抽到
api模块后,请求与页面逻辑分离,相同请求直接复用。 return不能省:不写return,await register(values)拿到的是undefined,后续判断全部失效。- 用
request.post而不是axios.post:走的是自己创建的实例,才会带上baseURL和上面写好的拦截器。 - 后续的登录、退出、获取管理员信息等接口,都是同一套路:
export const 方法名 = (data) => request.xxx('/地址', data),get请求不需要传参时直接request.get('/地址')。
3. register.vue —— 页面中调用 api + toast 轻提示
vue
<template>
<div class="register-page">
<form @submit.prevent="onSubmit(form)">
<input v-model="form.username" placeholder="请输入账号" />
<input v-model="form.password" type="password" placeholder="请输入密码" />
<button type="submit">注册</button>
</form>
</div>
</template>
<script>
import { register } from '@/api/user'
export default {
name: 'RegisterPage',
data () {
return {
form: {
username: '',
password: ''
}
}
},
methods: {
async onSubmit (values) {
this.$toast.loading({
message: '拼命加载中...',
forbidClick: true
})
try {
// 往后台发送注册请求了
await register(values)
this.$toast.success('注册成功')
this.$router.push('/login')
} catch (e) {
this.$toast.fail('注册失败')
}
}
}
}
</script>
<style lang="scss" scoped>
.register-page {
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
</style>关键行与易错点
import { register } from '@/api/user':页面里看不到任何 url 和 axios,这就是“请求与页面逻辑分离”的落地写法。loading提示一定要加forbidClick: true,否则加载过程中还能重复点击按钮,造成重复提交。loading是单例的,后面的this.$toast.success/this.$toast.fail会自动把它替换掉,不需要手动clear();但成功和失败两条分支都要有提示,否则 loading 会一直转。await必须放在try里面,catch才能接到拦截器Promise.reject抛出来的错误;catch里不用再自己弹错误原因,拦截器已经统一Toast(error.response.data.message)了。- 只有
await register(values)成功之后才能this.$router.push('/login'),把它写在try的最后一行,不要写在catch后面。 - 页面里能用
this.$toast的前提是main.js里已Vue.use(Toast);如果没注册,就改成import { Toast } from 'vant'后直接Toast.success(...)调用。
常见错误
<style lang="sass">里仍写{}和;,页面编译报错
现象:样式不生效,终端提示缩进/语法错误。
原因:SASS 缩进语法不允许花括号和分号。
修法:改用lang="scss",或删除{}、;并用缩进表达层级。改了主题色变量但按钮/链接颜色不变
现象:自定义 SCSS 变量文件已写,页面仍是默认蓝。
原因:入口文件未引入该 SCSS,或仍引入编译后的 CSS;变量名与组件库不一致。
修法:在入口引入自定义变量文件,移除原 CSS 引入,核对变量名后刷新验证。SCSS 中用
/deep/或>>>改子组件样式无效
现象:el-card__head背景色、居中样式不生效。
原因:样式穿透语法与预处理器不匹配,且未命中子组件内部类名。
修法:SCSS 使用::v-deep,先右键检查确认子组件真实类名,再写穿透选择器。表单校验不触发,或校验不通过仍能提交
现象:输入不合规但点击登录仍执行请求。
原因:el-form误写v-model而非:model;ref、prop、rules字段名未对应;提交前未调用validate。
修法:保证:model="user"、:rules="rules"、ref="form"、prop与字段名一致,提交前用this.$refs.form.validate(valid => { if (valid) ... })。await加错位置,登录请求结果拿不到
现象:loginAPI返回 Promise,后续逻辑未等待;或报await is only valid in async function。
原因:await不在最近的async函数内,调用 API 的方法未加async。
修法:给login方法加async,在调用loginAPI的语句前加await。拦截器已提示错误,页面
catch又提示一次
现象:一次失败弹出两条错误提示。
原因:响应拦截器统一Toast后,页面又重复处理。
修法:约定错误提示唯一出口;通用错误放拦截器,页面只处理业务差异,避免重复Toast。
练习
- 把登录页样式从 SASS 改为 SCSS,保证编译通过,并列出你改动的语法点(花括号、分号、变量前缀)。
- 新建主题变量 SCSS 文件并在入口引入,把主色改为指定颜色,再新增全局
body背景;验证按钮和文字链接颜色是否同步变化。 - 用
::v-deep修改el-card__head的背景色和文字居中,要求写出完整选择器,并说明为什么不能直接写.el-card__head。 - 完成登录表单的对应关系:
:model、:rules、ref、prop、v-model,并实现“校验通过才发请求,不通过不提交”。 - 封装
loginAPI,在登录方法中await调用;分别测试成功和失败,说明错误提示应放在拦截器还是页面,并给出理由。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | scss语法介绍 | 13:06 |
| 2 | 配置主题色 | 08:01 |
| 3 | 登录页布局-样式穿透标题 | 19:25 |
| 4 | 登录页布局-表单布局 | 10:11 |
| 5 | 登录表单验证 | 13:51 |
| 6 | 点击登录按钮进行完整校验 | 16:05 |
| 7 | 封装登录的API方法 | 09:43 |
小结
- 页面负责交互与展示,请求收敛到
api模块,地址、参数和错误处理才有统一入口。 - 表单校验的关键不是背 API,而是保证
model / rules / ref / prop / v-model的对应关系。 - 提交前必须显式做完整校验,不能把“输入框有红字”当成已校验。
- 样式穿透只在修改子组件内部样式时使用,语法随预处理器变化,选择器必须命中真实类名。
- 主题色本质是改变量,变量文件必须在入口正确引入,否则改完不生效。
- 使用 SASS/SCSS 前先确认
lang,再决定花括号、分号和变量前缀,否则容易一写就报错。 - 错误提示要有唯一出口,拦截器和页面重复提示会让体验变差。
- 调试顺序建议:静态布局 → 双向绑定 → 表单校验 → 接 API,每步都能单独验证。