Skip to content

Vuex 的基本使用(二) ​

本页对应 Vuex 的基本使用,共 5 段视频,约 1 小时 1 分。

学习目标 ​

  1. 能完成面经详情页开发:根据路由参数 ID 调用详情接口,将返回数据存入组件状态,并在模板中渲染标题、作者、浏览数、点赞数、创建时间等内容。
  2. 能实现详情内容的富文本展示:使用 v-html 渲染面经正文,并保证页面展示效果符合详情页要求。
  3. 能实现点赞与收藏的页面交互:根据 like_flag、collect_flag 控制图标激活状态,点击时切换状态、更新点赞数量并给出轻提示。
  4. 能封装并调用点赞收藏接口:按接口文档传递面经 ID 与操作类型参数,使点赞、取消点赞、收藏、取消收藏在刷新后仍保持服务器端结果。
  5. 能完成主题定制配置:在 Vue CLI 项目中引入组件库 Less 样式源文件,在 vue.config.js 中覆盖内置 Less 变量并验证全局样式生效。
  6. 能复用面经列表组件完成收藏页:按收藏列表接口改造请求参数与分页逻辑,去掉推荐/最新切换逻辑,并实现收藏题目的列表展示与滚动加载更多。

知识点 ​

本节要解决的问题 ​

这一节做的是项目中的 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 设计稿就直接改配置而不验证。

本节小结 ​

本节流程可以记成:安装 [email protected] -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() 是返回,不是跳转新页面;本节不要顺手把详情页请求、点赞收藏等逻辑混进路由配置,保持一级路由配置清晰。

课件图示 ​

课件第15页:目标:分析项目页面,设计路由,配置一级路由 路由设计配置 登录 注册 首页 面经

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

课件第8页:调整初始化目录 目标:将目录调整成符合企业规范的目录 1. 删除 多余的文件 2

课件第18页:目标:基于底部导航,完成二级路由配置 路由设计配置 配置二级路由 配置导航链接

课件第2页:项目演示 目标:查看项目效果,明确功能模块 登录 注册 首页 面经详情 面经 收

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

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

课件第14页:目标:基于 postcss 插件 实现项目 vw 适配 官方配置 ① 安装插件

代码示例 ​

代码示例一: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 router
vue
<!-- 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 标签,用 &#123;&#123; &#125;&#125; 会把标签当字符串显示出来。
  • :class="{ active: article.likeFlag }" 里 likeFlag 是 0/1 也能当布尔用(0 为假、1 为真),不用额外转换。
  • 点赞和收藏要写两个方法:点赞要动 likeCount(+1/-1),收藏不动数量,合成一个会写乱。
  • optType 一个是 1 一个是 2,写反了就成了“点收藏却点赞”;服务器靠它区分操作类型,1=点赞、2=收藏。
  • 只改本地数据不发请求时,页面看着变了,但一刷新就恢复原样——因为刷新后重新拉服务器数据;所以“表面效果”之后必须补 await collectAndLike(...)。

常见错误 ​

  1. 配置 postcss-px-to-viewport 后页面尺寸没变化
    现象:样式仍按 px 原样输出。
    原因:只安装了依赖,未在根目录正确新建 postcss.config.js,或改完配置未重启开发服务。
    修法:确认配置文件位置与导出格式正确,重启 dev server 后再验证;设计稿宽度与 viewportWidth 必须对应。

  2. 一级路由能跳转但页面空白或布局缺失
    现象:访问 /login、/ 等路径无内容。
    原因:App.vue 未保留 <router-view/>,或一级路由组件路径/导入名写错。
    修法:清理 App.vue 只保留路由出口;逐项核对 path、component、@/views/... 导入。

  3. 详情页拿不到 ID 或请求参数为空
    现象:接口返回异常、请求 query 中 id 为 undefined。
    原因:路由设计为 /article/:id,但组件中未从 this.$route.params.id 取值,或跳转时未带参。
    修法:跳转处确认传参,详情页从 $route.params 读取;请求封装时明确 query 参数结构。

  4. API 方法放错业务模块
    现象:详情/收藏相关接口写进 user.js 等错误文件,后续维护混乱。
    原因:未按业务域拆分 API。
    修法:面经相关放 article.js,用户相关放 user.js;组件按需导入对应方法。

  5. 点赞收藏只改页面数据,刷新后恢复原样
    现象:图标和数量即时变化,但刷新后回到未点赞/未收藏状态。
    原因:只修改了本地 article.likeFlag、collectFlag 等字段,没有调用服务端接口。
    修法:先做本地交互反馈,再发请求同步;请求失败时回滚本地状态并提示。

  6. 收藏页复制面经列表后仍带“推荐/最新”逻辑
    现象:收藏页出现无关切换、排序参数或接口参数不匹配。
    原因:整段复制后未删除 sorter 相关变量、方法和模板。
    修法:删除推荐/最新切换及 sorter 逻辑;按收藏接口要求改为 page、optType 等参数。

  7. 主题定制改完变量不生效或报变量未定义
    现象:Less 变量编译报错,或颜色未变化。
    原因:未引入 Vant 的 Less 源文件、变量名拼错,或改配置后未重启。
    修法:按文档引入 Less 源文件;覆盖内置变量时核对变量名;自定义变量在 lessOptions 中声明后重启验证。

  8. 收藏页复用 ArticleItem 时组件报错
    现象:组件渲染异常或列表为空。
    原因:未给列表项传 item 数据,或数据请求未完成就渲染。
    修法:先请求收藏列表,再 v-for 传入 item;为空时给空态,加载中给 loading。

练习 ​

  1. 将项目设计稿改为 750 宽度场景,调整 viewportWidth,分别验证 Vant 组件和自定义样式在 375/750 设计稿下的适配结果,并记录配置差异。

  2. 完成详情页数据渲染:从路由取 id,调用详情接口,将标题、浏览数、创建时间、点赞数、作者头像、内容区正确绑定;补充接口失败时的提示与空态。

  3. 实现点赞/收藏联调:点击先更新图标和数量,再发请求;请求失败时回滚 likeFlag、collectFlag、likeCount,并给出轻提示。

  4. 基于面经列表改造收藏页:复用 ArticleItem,删除推荐/最新逻辑,按收藏接口参数实现分页与“加载更多”,并验证超过一页的数据。

  5. 定制主题:覆盖一个 Vant 内置 Less 变量,再新增一个全局自定义变量并在登录页使用;说明改配置后需要重启的验证步骤。

原视频对照 ​

#视频时长
1详情-渲染详情页面14:28
2点赞收藏-页面效果11:47
3点赞收藏-发送请求07:00
4主题定制10:28
5收藏页18:01

小结 ​

  1. 适配配置要同时核对依赖、配置文件、设计稿宽度和重启动作,缺一项都可能“看起来配了但没生效”。

  2. 路由先定页面层级,再写 router/index.js;一级路由对应独立页面,二级路由挂在 Layout 下。

  3. 请求链路按“request 已配置 → API 按业务分模块 → 组件按需导入调用”推进,能显著降低维护成本。

  4. 详情页的核心是稳定拿到路由参数,并把接口返回字段与模板绑定逐项核对,避免字段名和层级写错。

  5. 点赞收藏要先做本地交互反馈,再发请求同步;服务端失败必须回滚,否则用户会看到“假成功”。

  6. 复用列表组件时,模板、数据、接口参数三处都要改;尤其要删除原页面独有的筛选/排序逻辑。

  7. 主题定制本质是覆盖 Less 变量;内置变量和自定义变量都要在配置中声明,改完配置必须重启验证。