Skip to content

面经 PC 端(下)(二) ​

本页对应 面经 PC 端(下),共 7 段视频,约 1 小时 30 分。

学习目标 ​

  1. 能说明 Sass/SCSS 与 Less 在语法格式(分号、花括号、缩进、变量声明符)上的差异,并在 Vue 单文件组件中通过 <style lang="scss" scoped> 正确编写嵌套样式与变量。
  2. 能在 src 下新建 SCSS 主题变量文件并在入口文件中引入,通过覆盖 Element Plus 的 SCSS 变量将全局主题色替换为指定颜色,并验证按钮、文字链接等组件颜色同步生效。
  3. 能使用 el-card 的具名插槽与默认插槽搭建登录卡片布局,并通过 :deep() 样式穿透修改子组件内部(如 el-card__header)的样式,实现标题居中与背景色设置。
  4. 能使用 el-form、el-form-item、el-input 搭建登录表单,正确配置 :model、:rules、prop、v-model 之间的对应关系,并写出必填、长度等校验规则使错误提示正常显示。
  5. 能通过 ref 获取表单实例并调用 validate 与 resetFields,实现点击登录时的完整校验(校验通过才提交、不通过不提交)以及点击重置按钮恢复初始值。
  6. 能按“先封装 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 实例的创建在前文,不要凭空补不存在的配置。把这一层统一好,后面登录、获取管理员信息等请求都能复用同一套错误提示逻辑。

课件图示 ​

课件第24页:目标:阅读文档 toast轻提示,完成 注册功能 提示&跳转 登录 & 注册功能

课件第25页:目标:响应拦截器中统一处理错误提示 登录 & 注册功能 try { await

课件第21页:目标:将 axios 请求方法,封装到 request 模块 登录 & 注册功能

课件第26页:目标:封装 api 完成登录功能 登录 & 注册功能 封装请求API 登录操作

课件第22页:目标:将请求封装成方法,统一存放到 api 模块,与页面分离 登录 & 注册功能

课件第23页:目标:将请求封装成方法,统一存放到 api 模块,与页面分离 登录 & 注册功能

课件第27页:目标:local 模块,本地存储封装 登录 & 注册功能 // 将token存入

课件第19页:目标:阅读文档,实现 登录页 & 注册页 静态布局 登录 & 注册功能

代码示例 ​

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(...) 调用。

常见错误 ​

  1. <style lang="sass"> 里仍写 {} 和 ;,页面编译报错
    现象:样式不生效,终端提示缩进/语法错误。
    原因:SASS 缩进语法不允许花括号和分号。
    修法:改用 lang="scss",或删除 {}、; 并用缩进表达层级。

  2. 改了主题色变量但按钮/链接颜色不变
    现象:自定义 SCSS 变量文件已写,页面仍是默认蓝。
    原因:入口文件未引入该 SCSS,或仍引入编译后的 CSS;变量名与组件库不一致。
    修法:在入口引入自定义变量文件,移除原 CSS 引入,核对变量名后刷新验证。

  3. SCSS 中用 /deep/ 或 >>> 改子组件样式无效
    现象:el-card__head 背景色、居中样式不生效。
    原因:样式穿透语法与预处理器不匹配,且未命中子组件内部类名。
    修法:SCSS 使用 ::v-deep,先右键检查确认子组件真实类名,再写穿透选择器。

  4. 表单校验不触发,或校验不通过仍能提交
    现象:输入不合规但点击登录仍执行请求。
    原因:el-form 误写 v-model 而非 :model;ref、prop、rules 字段名未对应;提交前未调用 validate。
    修法:保证 :model="user"、:rules="rules"、ref="form"、prop 与字段名一致,提交前用 this.$refs.form.validate(valid => { if (valid) ... })。

  5. await 加错位置,登录请求结果拿不到
    现象:loginAPI 返回 Promise,后续逻辑未等待;或报 await is only valid in async function。
    原因:await 不在最近的 async 函数内,调用 API 的方法未加 async。
    修法:给 login 方法加 async,在调用 loginAPI 的语句前加 await。

  6. 拦截器已提示错误,页面 catch 又提示一次
    现象:一次失败弹出两条错误提示。
    原因:响应拦截器统一 Toast 后,页面又重复处理。
    修法:约定错误提示唯一出口;通用错误放拦截器,页面只处理业务差异,避免重复 Toast。

练习 ​

  1. 把登录页样式从 SASS 改为 SCSS,保证编译通过,并列出你改动的语法点(花括号、分号、变量前缀)。
  2. 新建主题变量 SCSS 文件并在入口引入,把主色改为指定颜色,再新增全局 body 背景;验证按钮和文字链接颜色是否同步变化。
  3. 用 ::v-deep 修改 el-card__head 的背景色和文字居中,要求写出完整选择器,并说明为什么不能直接写 .el-card__head。
  4. 完成登录表单的对应关系::model、:rules、ref、prop、v-model,并实现“校验通过才发请求,不通过不提交”。
  5. 封装 login API,在登录方法中 await 调用;分别测试成功和失败,说明错误提示应放在拦截器还是页面,并给出理由。

原视频对照 ​

#视频时长
1scss语法介绍13:06
2配置主题色08:01
3登录页布局-样式穿透标题19:25
4登录页布局-表单布局10:11
5登录表单验证13:51
6点击登录按钮进行完整校验16:05
7封装登录的API方法09:43

小结 ​

  1. 页面负责交互与展示,请求收敛到 api 模块,地址、参数和错误处理才有统一入口。
  2. 表单校验的关键不是背 API,而是保证 model / rules / ref / prop / v-model 的对应关系。
  3. 提交前必须显式做完整校验,不能把“输入框有红字”当成已校验。
  4. 样式穿透只在修改子组件内部样式时使用,语法随预处理器变化,选择器必须命中真实类名。
  5. 主题色本质是改变量,变量文件必须在入口正确引入,否则改完不生效。
  6. 使用 SASS/SCSS 前先确认 lang,再决定花括号、分号和变量前缀,否则容易一写就报错。
  7. 错误提示要有唯一出口,拦截器和页面重复提示会让体验变差。
  8. 调试顺序建议:静态布局 → 双向绑定 → 表单校验 → 接 API,每步都能单独验证。