主题
Vuex 的基本使用(二)
本页对应 Vuex 的基本使用,共 5 段视频,约 1 小时 1 分。
学习目标
- 能完成面经详情页开发:根据路由参数 ID 调用详情接口,将返回数据存入组件状态,并在模板中渲染标题、作者、浏览数、点赞数、创建时间等内容。
- 能实现详情内容的富文本展示:使用
v-html渲染面经正文,并保证页面展示效果符合详情页要求。 - 能实现点赞与收藏的页面交互:根据
like_flag、collect_flag控制图标激活状态,点击时切换状态、更新点赞数量并给出轻提示。 - 能封装并调用点赞收藏接口:按接口文档传递面经 ID 与操作类型参数,使点赞、取消点赞、收藏、取消收藏在刷新后仍保持服务器端结果。
- 能完成主题定制配置:在 Vue CLI 项目中引入组件库 Less 样式源文件,在
vue.config.js中覆盖内置 Less 变量并验证全局样式生效。 - 能复用面经列表组件完成收藏页:按收藏列表接口改造请求参数与分页逻辑,去掉推荐/最新切换逻辑,并实现收藏题目的列表展示与滚动加载更多。
知识点
本节要解决的问题
这一节做的是项目中的 vw 适配:把设计稿里的 px 在构建时转换成 vw,让页面在不同手机视口下按比例缩放。笔记给出的官方说明是:
https://vant-contrib.gitee.io/vant/v2/#/zh-CN/advanced-usage
转写稿本节主要讲的是详情页渲染、点赞收藏、主题定制和收藏页,并没有直接演示 vw 适配。下面只围绕笔记中的适配配置扩写;其中“改配置文件后要重启、看终端报错、回到配置来源检查”的调试经验,来自转写稿里主题定制改配置的过程,可以迁移到本节,但不把业务代码混进适配配置。
安装 postcss-px-to-viewport
为什么需要:项目里写的是设计稿的 px,但移动端设备视口宽度不同,直接用固定 px 不利于等比适配。postcss-px-to-viewport 可以在构建阶段把 px 转成 vw,不需要运行时手写换算。
怎么想:它是构建工具链里的 PostCSS 插件,不是页面运行时依赖,所以应装到开发依赖里;笔记指定了版本 1.1.1,先按笔记版本装,避免版本差异带来配置行为变化。
怎么做:
js
yarn add postcss-px-to-viewport@1.1.1 -D易错点:不要装成全局;注意 -D;安装后确认 package.json 的 devDependencies 里有它;如果终端安装失败,先解决网络或源的问题,不要继续改配置。
在项目根目录新建 postcss.config.js
为什么需要:PostCSS 会自动读取项目根目录的配置文件。插件只有写进 postcss.config.js 的 plugins 里,构建时才会参与转换。
怎么想:配置文件要放在项目根目录,和 package.json 同级;使用 CommonJS 的 module.exports 导出配置对象。笔记只要求先配置 viewportWidth,不要额外加没有依据的选项。
怎么做:项目根目录新建 postcss.config.js:
jsx
// postcss.config.js
module.exports = {
plugins: {
'postcss-px-to-viewport': {
viewportWidth: 375,
},
},
};易错点:文件名不要写错;不要放到 src 里;如果项目原本已有 PostCSS 配置,应合并而不是直接覆盖;改完配置文件要重启开发服务器再看效果。转写稿里讲师改主题配置时反复说“配置文件改了就要重启”,并出现“变量没有定义”的终端报错,先回到变量来源检查。这个调试顺序同样适用于这里:样式没变化时,先看终端有没有 PostCSS 配置报错,再检查插件是否安装、配置是否在根目录、语法是否正确,不要一上来就怀疑组件。
viewportWidth 为什么填 375
为什么需要:viewportWidth 表示设计稿的视口宽度,插件按它作为换算基准。填错会导致整页元素整体偏大或偏小。
怎么想:笔记给了两个依据:第一,vant-ui 中的组件就是按照 375 的视口宽度设计的;第二,面经项目中的设计稿也是按照 375 的视口宽度设计的。所以此时只需要配置 375。这不是“随便填一个手机宽度”,而是让组件基准和项目设计稿基准对齐。
怎么做:保持上面的配置:
jsx
viewportWidth: 375,易错点:不要把“设备实际宽度是 390 或 414”误当成 viewportWidth;也不要因为换了手机就改配置。配置基准应对应设计稿和组件库基准,而不是当前调试设备。
设计稿不是 375 时怎么判断
为什么需要:如果设计稿按 750 宽度设计,仍然填 375 就可能出现比例不对。笔记没有直接给出结论,而是给了参考链接:
怎么想:先确认设计稿的基准宽度,再结合插件规则判断,不要拍脑袋把 375 改成 750。笔记只明确说明:本项目设计稿和 vant 组件都按 375 设计,所以本项目填 375。其他设计稿要按链接文章的方法判断。
怎么做:当前项目按笔记保持 375;若换项目,先看设计稿标注基准,再按参考文章调整。
易错点:把设计稿总宽、设备视口宽、组件库基准混为一谈;看到 750 设计稿就直接改配置而不验证。
本节小结
本节流程可以记成:安装 postcss-px-to-viewport@1.1.1 -D → 根目录新建 postcss.config.js → 在 plugins 中配置 viewportWidth: 375 → 重启并检查终端与页面。核心不是“记住 375”,而是理解 viewportWidth 必须对齐设计稿和组件库基准;本项目两者都是 375,所以填 375。
一级路由的判定与路由设计
为什么需要:路由不是先把所有页面都塞进一张表,而是先判断页面层级。笔记里给了一个很实用的判定:但凡是单个页面、独立展示的,都是一级路由。登录页、注册页、文章详情页都是独立打开的页面,所以它们是一级路由;首页 Layout 是一个布局容器,本身也是一级路由,面经、收藏、喜欢、我的则挂在它下面做二级路由。
怎么想:先按“是否独立展示”分层,再按业务归类。登录 Login、注册 Register、详情 Detail 独立展示,直接放一级;首页 Layout 作为壳子承载二级页面,也放一级。这样想的好处是后续配置二级路由时,只需要在 Layout 的 children 里扩展,不会把登录、注册、详情混进首页布局。
怎么做:按笔记整理出路由设计:
- 登录页(一级)
Login - 注册页(一级)
Register - 文章详情页(一级)
Detail - 首页(一级)
Layout- 面经(二级)
Article - 收藏(二级)
Collect - 喜欢(二级)
Like - 我的(二级)
My
- 面经(二级)

易错点:不要把“首页布局容器”和“首页里的某个二级页面”混成同一个一级路由;本节只配置一级路由,二级路由不要提前编造 children 代码。详情页虽然是内容页,但它独立展示,仍然是一级路由。
在 router/index.js 中配置一级路由
为什么需要:路由表是页面切换的入口。没有在 router/index.js 里注册,访问 /login、/register、/article/:id、/ 时就不会渲染对应组件。
怎么想:先把一级 views 组件准备好,再在路由表里“路径 → 组件”一一对应。详情页需要带文章 ID,所以用动态参数 /article/:id;首页用 / 指向 Layout。本节只挂一级路由,二级路由后续再放到 Layout 下处理。
怎么做:router/index.js 配置如下,保留笔记代码:
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) 要在创建实例前;routes 不要写成 routers;component 不要写成 components;/article/:id 的动态参数名要和详情页取值一致。转写稿里提醒过 $router 和 $route 别混:取参数用 this.$route.params.id,不是 this.$router.params.id。
清理 App.vue,保证路由出口唯一
为什么需要:App.vue 是根组件,路由匹配到的页面要通过 <router-view/> 渲染出来。如果根组件里还残留旧模板内容,页面可能被遮挡或出现“路由配了但看不到”的假象。
怎么想:根组件只保留路由出口,把展示交给具体路由组件。这样登录、注册、详情、首页布局都通过同一个出口切换。
怎么做:清理 App.vue,保留笔记代码:
jsx
<template>
<div id="app">
<router-view/>
</div>
</template>
<script>
export default {
created () {
}
}
</script>易错点:<router-view/> 拼写不要错;不要在 App.vue 里继续写某个页面的固定内容;created 不要写成转写稿里类似的“可日记的”这种同音误写。
详情页动态参数与调试提醒
为什么需要:详情页是一级路由,但它不是固定页面,需要根据文章 ID 展示不同内容。转写稿里说“把这个 ID 传过来……关键点是已经到达详情页了,ID 也有了”,说明路由先把 ID 带过去,页面再根据 ID 取数据。
怎么想:路径用 /article/:id,详情页里通过 this.$route.params.id 取动态参数;返回上一页时,转写稿里的“rur 点 bag”应纠正为 router.back()。调试时依次访问 /login、/register、/article/1、/,看是否分别渲染 Login、Register、Detail、Layout。
怎么做:先确认路由表配置正确,再在详情页取参数;如果页面空白,先检查 App.vue 是否有 <router-view/>,再检查组件导入路径和 path 是否匹配。
易错点:动态路由参数不要漏写冒号;详情页取参不要用 $router;router.back() 是返回,不是跳转新页面;本节不要顺手把详情页请求、点赞收藏等逻辑混进路由配置,保持一级路由配置清晰。
课件图示








代码示例
代码示例一:vw 适配 + Vant 主题定制(工程配置)
js
// postcss.config.js —— 放在项目根目录,和 package.json 同级
module.exports = {
plugins: {
'postcss-px-to-viewport': {
// 设计稿的视口宽度;vant 组件与面经项目设计稿都是按 375 设计的
viewportWidth: 375
}
}
}js
// vue.config.js
module.exports = {
css: {
loaderOptions: {
less: {
// 覆盖 vant 内置的 less 变量,实现主题定制
modifyVars: {
// 覆盖内置变量:原来蓝色相关的样式全部变成橙色
blue: '#ff8f1f',
// 自己新增的全局 less 变量,页面里用 @abc 即可
abc: 'red'
}
}
}
}
}js
// main.js —— 步骤一:先引入 vant 的 less 源文件,变量覆盖才生效
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import Vant from 'vant'
// 注意:必须引 less 源文件,不能引编译好的 css,否则改不动主题
import 'vant/lib/index.less'
Vue.use(Vant)
Vue.config.productionTip = false
new Vue({
router,
render: h => h(App)
}).$mount('#app')关键行与易错点
viewportWidth: 375填的是设计稿的视口宽度,不是屏幕宽度;设计稿是 750 就得填 750。postcss.config.js必须放在项目根目录,放错位置 px 不会转 vw,且不报错,很容易查半天。- 主题定制的两个步骤缺一不可:①
main.js引入vant/lib/index.less;②vue.config.js里写modifyVars。 modifyVars里写的是已存在的内置变量名就是覆盖(如blue),写一个文档里没有的名字就是新增全局变量(如abc),其他页面都能用@abc。- 改了
vue.config.js必须重启 dev server(转写稿里重启特别慢,是因为加了 less 预处理),改postcss.config.js同理。
代码示例二:一级路由配置 + 清理 App.vue
js
// 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 },
// 详情页需要 id,用动态路由参数
{ path: '/article/:id', component: Detail },
// 首页:二级路由(面经 / 收藏 / 喜欢 / 我的)挂在这里
{
path: '/',
component: Layout
}
]
})
export default routervue
<!-- App.vue -->
<template>
<div id="app">
<router-view />
</div>
</template>
<script>
export default {
created () {}
}
</script>关键行与易错点
- 一级路由的判定标准:能单独整页展示的就是一级,挂在
Layout里的面经/收藏/喜欢/我的才是二级。 - 详情页写成
'/article/:id',跳转时用/article/${id},取值时用this.$route.params.id(注意是params不是query)。 App.vue只留一个<router-view />,把脚手架默认的 logo、HelloWorld等全部删掉,否则每个页面都会多出一块内容。- 引入路径统一用
@/views/xxx,@已在 vue-cli 中指向src,不要写成相对路径导致层级错乱。
代码示例三:面经详情页(取详情 + 点赞收藏表面效果 + 发请求)
js
// api/article.js —— 和面经相关的接口都放这里,别放到 user.js
import request from '@/utils/request'
// 获取单个面经详情(GET,query 参数只有一个 id)
// 注意:URL 以接口文档为准,这里按“请求方式 get + query 传 id”的约定写
export const getArticleDetail = (id) => {
return request.get('/article/detail', { params: { id } })
}
// 点赞 / 收藏 / 取消点赞 / 取消收藏(POST,body 参数 id + optType)
// optType 传 1 = 点赞或取消点赞,传 2 = 收藏或取消收藏
// 到底是“加”还是“取消”,由服务器根据当前状态自动判断
export const collectAndLike = (data) => {
return request.post('/article/collect', data)
}vue
<!-- views/Detail.vue -->
<template>
<div class="article-detail">
<!-- 返回:@click 里挂 $router.back() -->
<div class="back" @click="$router.back()">
<van-icon name="arrow-left" />
</div>
<!-- 导航栏标题,沿用登录/注册页的写法 -->
<van-nav-bar :title="article.stem" />
<div class="header">
<img class="avatar" :src="article.avatar" alt="" />
<div class="title">{{ article.stem }}</div>
<div class="meta">
<span>浏览 {{ article.views }}</span>
<span>创建时间 {{ article.createTime }}</span>
</div>
</div>
<!-- 富文本内容必须用 v-html,否则 h1、p 等标签会被当纯文本显示 -->
<div class="content" v-html="article.content"></div>
<!-- 底部两个图标:类名由 collectFlag / likeFlag 控制是否点亮 -->
<div class="footer">
<div class="item" @click="toggleLike">
<van-icon name="like" :class="{ active: article.likeFlag }" />
<span>{{ article.likeCount }}</span>
</div>
<div class="item" @click="toggleCollect">
<van-icon name="star" :class="{ active: article.collectFlag }" />
</div>
</div>
</div>
</template>
<script>
// 按需导入,别忘了 import,否则方法未定义
import { getArticleDetail, collectAndLike } from '@/api/article'
export default {
name: 'ArticleDetail',
data () {
return {
// 默认给个空对象,模板里访问 article.xxx 才不会报错
article: {}
}
},
created () {
// created 里自动执行,进页面就请求
this.getDetail()
},
methods: {
async getDetail () {
// 路由上 /article/:id 传过来的 id
const res = await getArticleDetail(this.$route.params.id)
// res.data 本身就是一个对象,直接整体赋值给 article
this.article = res.data
},
// 点赞:表面效果 + 请求
async toggleLike () {
// 1. 先改当前页面数据(表面效果),类名会自动跟着变
this.article.likeFlag = !this.article.likeFlag
if (this.article.likeFlag) {
this.article.likeCount++ // 点赞,数量 +1
} else {
this.article.likeCount-- // 取消点赞,数量 -1
}
// 2. 再发请求让服务器真正记录,optType 传 1
await collectAndLike({ id: this.article.id, optType: 1 })
// 3. 轻提示,成功与否看当前 flag
this.$toast(this.article.likeFlag ? '点赞成功' : '取消点赞')
},
// 收藏:只需要切换图标,不涉及数量,所以单独写一个方法
async toggleCollect () {
this.article.collectFlag = !this.article.collectFlag
// optType 传 2
await collectAndLike({ id: this.article.id, optType: 2 })
this.$toast(this.article.collectFlag ? '收藏成功' : '取消收藏')
}
}
}
</script>
<style lang="less" scoped>
.article-detail {
padding-bottom: 60px;
.content {
padding: 0 16px;
}
.footer {
position: fixed;
left: 0;
right: 0;
bottom: 0;
display: flex;
justify-content: space-around;
background: #fff;
// 点亮状态:靠 active 类名控制
.active {
color: #ff8f1f;
}
}
}
</style>关键行与易错点
this.$route.params.id取的是path: '/article/:id'的动态段;写成this.$route.query.id会拿到undefined,请求参数就成了空。created里调this.getDetail(),钩子函数会自动执行;漏写this.或忘了 import API 方法,控制台直接报未定义。v-html="article.content"是详情页的关键:面经内容带h1/h2/p标签,用{{ }}会把标签当字符串显示出来。:class="{ active: article.likeFlag }"里likeFlag是 0/1 也能当布尔用(0 为假、1 为真),不用额外转换。- 点赞和收藏要写两个方法:点赞要动
likeCount(+1/-1),收藏不动数量,合成一个会写乱。 optType一个是 1 一个是 2,写反了就成了“点收藏却点赞”;服务器靠它区分操作类型,1=点赞、2=收藏。- 只改本地数据不发请求时,页面看着变了,但一刷新就恢复原样——因为刷新后重新拉服务器数据;所以“表面效果”之后必须补
await collectAndLike(...)。
常见错误
配置
postcss-px-to-viewport后页面尺寸没变化
现象:样式仍按 px 原样输出。
原因:只安装了依赖,未在根目录正确新建postcss.config.js,或改完配置未重启开发服务。
修法:确认配置文件位置与导出格式正确,重启 dev server 后再验证;设计稿宽度与viewportWidth必须对应。一级路由能跳转但页面空白或布局缺失
现象:访问/login、/等路径无内容。
原因:App.vue未保留<router-view/>,或一级路由组件路径/导入名写错。
修法:清理App.vue只保留路由出口;逐项核对path、component、@/views/...导入。详情页拿不到 ID 或请求参数为空
现象:接口返回异常、请求 query 中id为undefined。
原因:路由设计为/article/:id,但组件中未从this.$route.params.id取值,或跳转时未带参。
修法:跳转处确认传参,详情页从$route.params读取;请求封装时明确 query 参数结构。API 方法放错业务模块
现象:详情/收藏相关接口写进user.js等错误文件,后续维护混乱。
原因:未按业务域拆分 API。
修法:面经相关放article.js,用户相关放user.js;组件按需导入对应方法。点赞收藏只改页面数据,刷新后恢复原样
现象:图标和数量即时变化,但刷新后回到未点赞/未收藏状态。
原因:只修改了本地article.likeFlag、collectFlag等字段,没有调用服务端接口。
修法:先做本地交互反馈,再发请求同步;请求失败时回滚本地状态并提示。收藏页复制面经列表后仍带“推荐/最新”逻辑
现象:收藏页出现无关切换、排序参数或接口参数不匹配。
原因:整段复制后未删除sorter相关变量、方法和模板。
修法:删除推荐/最新切换及sorter逻辑;按收藏接口要求改为page、optType等参数。主题定制改完变量不生效或报变量未定义
现象:Less 变量编译报错,或颜色未变化。
原因:未引入 Vant 的 Less 源文件、变量名拼错,或改配置后未重启。
修法:按文档引入 Less 源文件;覆盖内置变量时核对变量名;自定义变量在lessOptions中声明后重启验证。收藏页复用
ArticleItem时组件报错
现象:组件渲染异常或列表为空。
原因:未给列表项传item数据,或数据请求未完成就渲染。
修法:先请求收藏列表,再v-for传入item;为空时给空态,加载中给 loading。
练习
将项目设计稿改为 750 宽度场景,调整
viewportWidth,分别验证 Vant 组件和自定义样式在 375/750 设计稿下的适配结果,并记录配置差异。完成详情页数据渲染:从路由取
id,调用详情接口,将标题、浏览数、创建时间、点赞数、作者头像、内容区正确绑定;补充接口失败时的提示与空态。实现点赞/收藏联调:点击先更新图标和数量,再发请求;请求失败时回滚
likeFlag、collectFlag、likeCount,并给出轻提示。基于面经列表改造收藏页:复用
ArticleItem,删除推荐/最新逻辑,按收藏接口参数实现分页与“加载更多”,并验证超过一页的数据。定制主题:覆盖一个 Vant 内置 Less 变量,再新增一个全局自定义变量并在登录页使用;说明改配置后需要重启的验证步骤。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 详情-渲染详情页面 | 14:28 |
| 2 | 点赞收藏-页面效果 | 11:47 |
| 3 | 点赞收藏-发送请求 | 07:00 |
| 4 | 主题定制 | 10:28 |
| 5 | 收藏页 | 18:01 |
小结
适配配置要同时核对依赖、配置文件、设计稿宽度和重启动作,缺一项都可能“看起来配了但没生效”。
路由先定页面层级,再写
router/index.js;一级路由对应独立页面,二级路由挂在Layout下。请求链路按“request 已配置 → API 按业务分模块 → 组件按需导入调用”推进,能显著降低维护成本。
详情页的核心是稳定拿到路由参数,并把接口返回字段与模板绑定逐项核对,避免字段名和层级写错。
点赞收藏要先做本地交互反馈,再发请求同步;服务端失败必须回滚,否则用户会看到“假成功”。
复用列表组件时,模板、数据、接口参数三处都要改;尤其要删除原页面独有的筛选/排序逻辑。
主题定制本质是覆盖 Less 变量;内置变量和自定义变量都要在配置中声明,改完配置必须重启验证。