主题
面经 PC 端(上)(一)
本页对应 面经 PC 端(上),共 7 段视频,约 1 小时 34 分。
学习目标
- 能说明 Vuex 解决大范围组件数据共享的机制,并区分 state、getters、mutations、actions 的职责与调用关系。
- 能完成 Vuex 3.6.2 的安装与配置:创建 store/index.js、在 main.js 中挂载,并通过 Vue 调试工具确认 Vuex 生效。
- 能在 state 中定义共享数据、在 getters 中定义派生数据,并在组件中通过 $store 或 mapState、mapGetters 正确读取,能处理命名冲突。
- 能在 mutations 中编写同步更新 state 的方法并通过 commit 调用传参,能在 actions 中编写异步方法并通过 dispatch 调用,在 action 内 commit mutation 更新数据。
- 能完成一级路由的配置与路由出口渲染,并通过路径切换验证页面跳转生效。
- 能配置 tabbar 标签页并设置主题色,验证标签选中态与主题样式正确生效。
知识点
一级路由的设计思路
为什么需要:做后台/PC端项目时,页面不是全都平铺的。登录、注册、文章详情是独立展示的页面;首页则是一个带导航和内容区的“壳”,里面再切换面经、收藏、喜欢、我的。如果不先分清一级路由和二级路由,后面写嵌套路由时路径会乱,组件也不知道往哪里放。
怎么想:判断标准很直接——但凡是单个页面、独立展示的,都是一级路由。首页 Layout 本身也是一级路由,它负责承载二级路由。按笔记中的设计:
- 登录页(一级)Login
- 注册页(一级)Register
- 文章详情页(一级)Detail
- 首页(一级)Layout
- 面经(二级)Article
- 收藏(二级)Collect
- 喜欢(二级)Like
- 我的(二级)My

怎么做:先只配置一级路由,二级路由等 Layout 结构准备好后再配。这样调试时能先确认每个独立页面是否能通过地址访问。
易错点:不要把面经、收藏等直接配成一级路由;它们应作为 Layout 的子路由。文章详情页虽然也独立展示,但需要携带文章 id,所以路径要设计成动态参数形式。
配置 router/index.js
为什么需要:Vue Router 需要把“路径”和“组件”建立映射。一级路由配置好后,访问 /login 就渲染 Login,访问 /register 就渲染 Register。
怎么想:转写稿里回顾路由守卫时提到,导航守卫要写在“创建 router 变量之后、导出之前”,因为守卫里要用到 router 变量。这个位置意识同样适用于路由配置:先导入组件,再 Vue.use(VueRouter),再 new VueRouter({ routes }),最后导出。
怎么做:在 router/index.js 中配置一级路由,一级 views 组件从准备好的目录中直接复制过来即可:
jsx
import Vue from 'vue'
import VueRouter from 'vue-router'
import Login from '@/views/Login'
import Register from '@/views/Register'
import Detail from '@/views/Detail'
import Layout from '@/views/Layout'
Vue.use(VueRouter)
const router = new VueRouter({
routes: [
{ path: '/login', component: Login },
{ path: '/register', component: Register },
{ path: '/article/:id', component: Detail },
{
path: '/',
component: Layout
}
]
})
export default router易错点:
Vue.use(VueRouter)必须在创建 router 实例前执行。- 组件导入路径要写对,笔记中使用
@/views/...。 - 文章详情页路径是
/article/:id,id是动态参数,不能漏掉冒号。 - 根路径
/指向 Layout,是为了让首页承载二级路由;不要随意改成别的路径,否则后续子路由基准会变。 - 最后一定要
export default router,并在入口中挂载。
清理 App.vue
为什么需要:一级路由配置好后,页面内容应该由路由出口决定。如果 App.vue 里还保留默认内容或重复结构,容易出现“路由页面出来了,但外面还套着旧内容”的情况。
怎么想:App.vue 只保留最外层容器和 <router-view/>,让匹配到的一级路由组件在这里渲染。转写稿中反复强调路由相关配置要围绕 router 展开,App.vue 不需要承担页面结构。
怎么做:把 App.vue 清理成:
jsx
<template>
<div id="app">
<router-view/>
</div>
</template>
<script>
export default {
created () {
}
}
</script>易错点:<router-view/> 不要拼错;如果页面空白,先检查入口是否挂载了 router、router/index.js 是否导出、App.vue 是否保留路由出口。调试时可以先逐个访问 /login、/register、/article/1、/,确认一级路由都能独立渲染,再继续配置 Layout 下的二级路由。
本节目标与效果
本节要完成的是移动端首页架子底部的 Tabbar 标签栏:在布局组件 layout.vue 中放置四个底部标签——面经、收藏、喜欢、我的,并使用 Vant 的 Tabbar 组件完成静态展示。最终效果参考下图:

组件文档地址:https://vant-contrib.gitee.io/vant/v2/#/zh-CN/tabbar
为什么需要:移动端项目通常需要底部固定导航,让用户在几个一级页面之间快速切换。把 Tabbar 放在 layout.vue 这类“首页架子”里,可以让多个页面共用同一套底部导航,避免每个页面重复写一遍。
怎么想:先不要自己从零写结构和样式,优先复制 Vant 官方示例,保证组件结构正确,再改文本和图标。转写稿里讲师反复强调配置类代码要“按固定位置、固定语法写,先按文档来”,Tabbar 也遵循这个思路。
怎么做:先注册组件,再在布局组件中复制官方 Tabbar 代码,最后替换成项目需要的四个标签。
易错点:Vue2 项目要对应 Vant2 文档,不要混用新版语法;组件必须先注册;图标名要使用 Vant 图标库中的名称,不能随便写图片路径。
组件引入与注册
为什么需要:Vant 的组件不是全局自动可用的,尤其在 Vue2 项目中,需要通过 Vue.use() 注册后才能直接在模板里写 <van-tabbar>、<van-tabbar-item>。
怎么想:项目里已经有统一的 vant-ui.js 入口,就继续在这个文件里按需引入和注册,保持组件管理集中。
怎么做:在 vant-ui.js 中引入 Tabbar、TabbarItem,并调用 Vue.use():
jsx
import { Button, Icon, Tabbar, TabbarItem } from 'vant'
Vue.use(Tabbar)
Vue.use(TabbarItem)这里保留原来的 Button、Icon,新增的是 Tabbar 和 TabbarItem。
易错点:只引入不注册,页面会提示组件未注册;Tabbar 和 TabbarItem 是两个组件,不能只注册外层而漏掉子项;不要从错误版本的 Vant 包中引入。
在 layout.vue 中落地
为什么需要:layout.vue 是“首页架子”,负责内容区域和公共底部导航。Tabbar 放在这里,后续页面切换时底部栏可以保持稳定。
怎么想:先复制官方代码,保证 <van-tabbar> 内部有多个 <van-tabbar-item>,再按项目需求改文字和图标。
怎么做:在 layout.vue 中写入以下结构:
jsx
<template>
<div class="layout-page">
首页架子 - 内容区域
<van-tabbar>
<van-tabbar-item icon="notes-o">面经</van-tabbar-item>
<van-tabbar-item icon="star-o">收藏</van-tabbar-item>
<van-tabbar-item icon="like-o">喜欢</van-tabbar-item>
<van-tabbar-item icon="user-o">我的</van-tabbar-item>
</van-tabbar>
</div>
</template>易错点:不要漏掉 van-tabbar-item 的闭合标签;文本要写在标签内部,图标通过 icon 属性指定;本节只完成静态标签栏,路由跳转不要提前硬编,按后续路由配置继续完成。
修改文本与图标
为什么需要:官方示例只是通用结构,项目需要的是“面经、收藏、喜欢、我的”四个业务入口,所以必须替换文本和图标。
怎么想:文本直接改标签内容;图标从 Vant 文档的图标列表中选语义接近的:面经用 notes-o,收藏用 star-o,喜欢用 like-o,我的用 user-o。
怎么做:按上面代码逐个替换。完成后启动项目,检查底部是否出现四个标签,文字是否对应,图标是否显示。
易错点:图标名写错时通常不显示图标;不要把 icon 写成 src;如果四个标签挤在一起或样式异常,先检查是否使用了 Vant2 的 Tabbar 结构,而不是自己改坏了 van-tabbar-item。
调试与检查
为什么需要:组件配置类问题往往不是逻辑错误,而是注册、版本、名称、结构其中之一出错。
怎么想:按“组件是否注册 → 模板结构是否完整 → 图标名是否正确 → 页面是否在 layout 中渲染”的顺序排查。
怎么做:先确认 vant-ui.js 已执行,再确认 layout.vue 中标签结构完整,最后看浏览器页面底部效果。
易错点:不要因为图标不显示就重写整个组件;先查 Vue.use(Tabbar)、Vue.use(TabbarItem) 是否遗漏。完成本节后,Tabbar 只是静态展示,后续再结合路由配置实现点击切换。
为什么需要配置主题色
为什么需要:笔记开头指出,整体网站风格其实都是橙色的。如果继续使用 Vant 默认主题色,页面中的按钮、导航、选中态等主色会与项目整体风格不一致。逐个组件写样式覆盖,既零散又容易漏。
怎么想:不要直接改 node_modules 里的 Vant 源码,也不要在每个页面里重复覆盖颜色。Vant 支持通过变量覆盖的方式制定主题色,所以思路是:让 Vant 的按需引入走 Less 源样式,再在 Vue CLI 的 Less 配置里用 modifyVars 覆盖主题变量。参考文档:
https://vant-contrib.gitee.io/vant/v2/#/zh-CN/theme
怎么做:分两步,先改 babel.config.js 指定样式路径,再改 vue.config.js 覆盖变量。配置完成后重启服务器生效。
易错点:不要直接改 node_modules/vant;不要只在某个组件里改颜色;变量覆盖要写在 Vue CLI 的 Less loader 配置中,而不是随便写在业务样式里。
在 babel.config.js 中指定样式路径
为什么需要:Vant 按需引入时,默认可能引入编译后的样式。要让 Less 变量覆盖真正参与编译,需要让按需引入插件去加载 Vant 的 Less 源样式,所以笔记在 babel.config.js 中指定样式路径。
怎么想:babel-plugin-import 的 style 选项可以控制引入哪种样式。这里把样式路径写成 ${name}/style/less,意思是让 Vant 组件按需引入时走 Less 样式入口。
怎么做:保留笔记中的配置:
jsx
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
],
plugins: [
['import', {
libraryName: 'vant',
libraryDirectory: 'es',
// 指定样式路径
style: (name) => `${name}/style/less`
}, 'vant']
]
}易错点:libraryName 要写 'vant';libraryDirectory 是 'es';style 是函数,返回的是模板字符串 ${name}/style/less,不要漏掉反引号或写成普通字符串;插件数组最后的 'vant' 也不要漏。改完 Babel 配置后不能只刷新页面,要重启开发服务器。
在 vue.config.js 中覆盖 Less 变量
为什么需要:上一步只是让样式走 Less,真正把默认主题色换成项目橙色,需要在 Less 编译时覆盖 Vant 的主题变量。笔记中通过 modifyVars 直接覆盖变量。
怎么想:Vue CLI 项目可以在 vue.config.js 的 css.loaderOptions.less 中配置 Less loader。lessOptions.modifyVars 用来修改变量值。笔记示例把 'blue' 覆盖为 '#FA6D1D',也就是把对应的主色变量改成橙色。
怎么做:保留笔记中的配置:
jsx
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
css: {
loaderOptions: {
less: {
lessOptions: {
modifyVars: {
// 直接覆盖变量
'blue': '#FA6D1D',
},
},
},
},
},
})易错点:层级不能少,必须是 css.loaderOptions.less.lessOptions.modifyVars;变量名要和 Vant 主题文档中的变量名一致,笔记这里覆盖的是 'blue';颜色值要带 #;不要写成 css.modifyVars 或 loaderOptions.modifyVars;如果项目里已有 vue.config.js,要在原有配置上合并,不要粗暴覆盖掉其他配置。
重启服务器并验证是否生效
为什么需要:babel.config.js 和 vue.config.js 都属于构建配置,修改后开发服务器不会像普通业务代码那样可靠地热更新。笔记明确写了“重启服务器生效”。
怎么想:先停止当前开发服务器,再重新启动;然后打开页面看 Vant 的主色相关元素是否变成橙色。笔记没有给出本节截图,验证时以页面主色是否变为 #FA6D1D 为准。
怎么做:重启项目后检查按钮、导航、选中态等使用主题色的位置。如果仍是默认色,按顺序排查:babel.config.js 是否走了 style/less;vue.config.js 的 Less 配置层级是否正确;变量名是否与 Vant 文档一致;是否真的重启了服务器。
易错点:只刷新浏览器不算重启;只改 vue.config.js 而忘了 babel.config.js 的 Less 样式路径,变量覆盖可能不生效;改完配置后看到旧颜色,先确认终端里的开发服务器已经重新启动,而不是继续怀疑变量值写错。
课件图示








代码示例
代码一:一级路由配置 + 清理 App.vue
js
// src/router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Login from '@/views/Login'
import Register from '@/views/Register'
import Detail from '@/views/Detail'
import Layout from '@/views/Layout'
Vue.use(VueRouter)
const router = new VueRouter({
routes: [
{ path: '/login', component: Login },
{ path: '/register', component: Register },
{ path: '/article/:id', component: Detail },
{
path: '/',
component: Layout
}
]
})
export default routervue
<!-- src/App.vue -->
<template>
<div id="app">
<router-view/>
</div>
</template>
<script>
export default {
created () {
}
}
</script>关键行与易错点
Vue.use(VueRouter)必须在new VueRouter之前调用,否则$route/$router全都不存在。- 一级路由的判定标准就是“单个页面、独立展示”:登录、注册、详情、Layout 都是平级的四个一级路由;面经/收藏/喜欢/我的属于 Layout 内部的二级路由。
{ path: '/', component: Layout }这里写的是/,不是/layout,首页地址才会是根路径。'/article/:id'是动态路由参数,详情页取参数用this.$route.params.id($route是取参数,$router是跳转,别混)。@是 src 别名,路径写成@/views/Login而不是相对路径,避免层级变化后 import 全部失效。- App.vue 必须清掉脚手架自带的 logo/HelloWorld,只保留
<router-view/>,否则每个页面顶部都会多出一块内容。
代码二:Layout 里挂 tabbar + 覆盖 vant 主题色
js
// src/utils/vant-ui.js
import Vue from 'vue'
import { Button, Icon, Tabbar, TabbarItem } from 'vant'
Vue.use(Button)
Vue.use(Icon)
Vue.use(Tabbar)
Vue.use(TabbarItem)vue
<!-- src/views/Layout/index.vue -->
<template>
<div class="layout-page">
首页架子 - 内容区域
<van-tabbar>
<van-tabbar-item icon="notes-o">面经</van-tabbar-item>
<van-tabbar-item icon="star-o">收藏</van-tabbar-item>
<van-tabbar-item icon="like-o">喜欢</van-tabbar-item>
<van-tabbar-item icon="user-o">我的</van-tabbar-item>
</van-tabbar>
</div>
</template>js
// babel.config.js
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
],
plugins: [
['import', {
libraryName: 'vant',
libraryDirectory: 'es',
// 指定样式路径
style: (name) => `${name}/style/less`
}, 'vant']
]
}js
// vue.config.js
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
css: {
loaderOptions: {
less: {
lessOptions: {
modifyVars: {
// 直接覆盖变量
'blue': '#FA6D1D'
}
}
}
}
}
})关键行与易错点
- tabbar 的组件必须在
vant-ui.js里Vue.use(Tabbar)、Vue.use(TabbarItem)注册,只把模板贴过来会报Unknown custom element: <van-tabbar>。 icon="notes-o"这些图标名来自 vant 图标库,改文本时要连图标一起改,别只改文字。style: (name) => \${name}/style/less`是主题定制的开关:样式路径必须指向 less,后面modifyVars` 才有东西可覆盖。modifyVars里的变量名按 vant 官方主题文档的变量写,覆盖后整站按钮、tabbar 选中色一起变。- 改完
babel.config.js或vue.config.js必须重启 dev server,热更新不会生效,这是最容易卡住的一步。
代码三:Vuex 仓库(state / getters / mutations / actions)+ 组件使用
js
// src/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
strict: true,
state: {
age: 20,
uname: '张三',
list: [
{ id: 1, name: '吃饭', isDone: true },
{ id: 2, name: '睡觉', isDone: false },
{ id: 3, name: '听歌', isDone: true }
]
},
getters: {
// 第一个参数固定是 state
abc: (state) => state.age * state.age,
isAll: (state) => state.list.every(item => item.isDone === true)
},
mutations: {
// 第一个参数固定是 state
updateAge (state, n) {
state.age = n
}
},
actions: {
// 第一个参数固定是 store,不是 state
updateAgeAfterSomeTime (store, n) {
setTimeout(() => {
store.commit('updateAge', n)
}, 3000)
}
}
})
export default storejs
// src/main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
Vue.config.productionTip = false
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')vue
<!-- src/views/Test/index.vue -->
<template>
<div>
<p>直接使用:{{ $store.state.age }} / {{ $store.getters.abc }}</p>
<p>简化使用:{{ age }} {{ uname }} {{ abc }} {{ isAll }}</p>
<button @click="$store.commit('updateAge', 1000)">更新 age</button>
<button @click="$store.dispatch('updateAgeAfterSomeTime', 500)">3 秒后更新</button>
</div>
</template>
<script>
import { mapState, mapGetters } from 'vuex'
export default {
computed: {
// 数组写法:名字和 state 里的 key 一致
...mapState(['age', 'uname', 'list']),
...mapGetters(['abc', 'isAll'])
// 有重名冲突时改成对象写法改名,例如:
// ...mapState({ nianling: 'age' })
}
}
</script>关键行与易错点
- 四个选项是并列关系:
state存数据、getters相当于 store 的计算属性(只读,不支持 set)、mutations是同步修改 state 的唯一途径、actions只放异步任务。 mutations的第一个参数固定是state;actions的第一个参数固定是store——因为 action 里不能直接改 state,只能store.commit('updateAge', n)去调 mutation 改,这一点是最容易写错的。- 在组件里直接
$store.state.age++虽然值会变,但 devtools 抓不到变化;开了strict: true还会直接报错,所以一律走commit。 - 组件调用语法是固定的:改数据用
this.$store.commit('方法名', 参数),异步用this.$store.dispatch('方法名', 参数)。 mapState/mapGetters必须展开写在computed里,mapMutations/mapActions写在methods里;写到data里是无效的。- 用辅助方法后,如果和组件自身
data/computed重名(比如组件自己也有age),要写成对象格式改名:...mapState({ nianling: 'age' }),否则报“已定义过”。
常见错误
导航守卫不生效
现象:未登录仍能进入页面,或守卫代码写了但无反应。
原因:守卫写在export default router之后,或未放在创建router实例之后。
修法:在new VueRouter()之后、export default router之前注册beforeEach。主题色覆盖无效
现象:修改了modifyVars,页面仍是默认色。
原因:babel.config.js未把 Vant 样式路径改为style/less,或改完未重启。
修法:确认style: (name) => \${name}/style/less`,修改vue.config.js` 后重启开发服务器。Tabbar 不显示或报组件未注册
现象:<van-tabbar>无渲染或控制台提示未知组件。
原因:只在页面中写了标签,未在vant-ui.js中Vue.use(Tabbar/TabbarItem)。
修法:按需引入并注册Tabbar、TabbarItem,确认入口已引入vant-ui.js。axios 参数位置写错
现象:接口返回参数缺失、查询条件无效或后端收不到请求体。
原因:查询参数误写进data,请求体误写进params,路径参数未拼到 URL。
修法:查询参数用params,请求体用data,/article/:id这类路径参数直接拼到 URL 后。FormData 提交失败
现象:上传文件或表单接口收不到字段。
原因:仍用普通对象作为data,未使用FormData,或字段未append。
修法:const fd = new FormData()后逐项fd.append(key, value),请求配置中data: fd。Vuex 中直接修改 state
现象:开启严格模式后报错,调试工具也抓不到变更。
原因:在组件或 actions 中直接写state.age = xxx。
修法:统一通过mutations修改;组件用commit,actions 内用commit调 mutation。mutation 中写异步逻辑
现象:定时器后数据变了但调试工具状态异常或报错。
原因:mutations只能同步更新,异步任务放错位置。
修法:异步逻辑放actions,拿到结果后再commit对应 mutation。mapState/mapGetters 重名冲突
现象:页面数据被覆盖或提示重复定义。
原因:组件自身data/computed与映射名称相同。
修法:映射改用对象格式重命名,如{ age: 'age' }改为{ localAge: 'age' }。
练习
- 给出现有
router/index.js,把登录访问守卫放到正确位置,并实现:无 token 且访问非/login、/register时跳转登录页。 - 根据接口文档写请求:
GET /students带page、size查询参数;DELETE /students/:id带路径参数;POST /upload提交FormData。 - 在 Vuex 中实现:点击按钮后 3 秒把
age改为传入值,要求不违背“mutation 是唯一修改入口”的规则,并写出组件调用代码。 - 组件中同时使用
mapState映射age,但组件自身已有data.age,请改写映射并说明页面中如何取值。 - 排查一个主题色不生效项目:列出你会检查的 3 个关键文件和对应检查点。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 反馈和回顾 | 24:15 |
| 2 | vuex介绍 | 09:43 |
| 3 | vuex安装配置 | 08:18 |
| 4 | state和getters定义和直接使用 | 15:37 |
| 5 | mutations更新数据 | 11:59 |
| 6 | actions异步方法 | 14:21 |
| 7 | 借助辅助方法使用state和getters | 10:02 |
小结
- 一级路由按“独立页面”拆分,Layout 作为首页架子承载二级内容与 Tabbar。
App.vue只保留路由出口,页面结构交给对应路由组件。- 全局样式、Babel、Vue 配置类改动,改完要重启并验证实际效果。
- axios 写法固定,真正要判断的是参数类型:查询、请求体、路径参数、FormData。
- Vuex 数据修改链路应保持为:组件/actions → commit → mutation → state。
- 组件侧调用要区分:读 state/getters 用映射或
$store,改数据用commit,异步用dispatch。 - 辅助方法只是简化写法,遇到重名用对象格式重命名,不要与组件自身数据/计算属性冲突。