Skip to content

抽离一与抽离二(外部)(Lesson11 3/3) ​

本页对应 Lesson11,共 6 段视频,约 1 小时 26 分。

学习目标 ​

  1. 能够定位并说明 beforeRouteUpdate 在详情页组件复用场景下导致数据不更新的原因,并通过放行导航或调整参数监听逻辑修复切换详情后内容不更新的问题。
  2. 能够使用动态路由参数配置二级/三级路由,并在组件中通过监听 $route.params 获取类型或 ID,完成“正在热映/即将上映”及详情页的切换与刷新保持。
  3. 能够完成 Vuex 的安装、引入、挂载与 store 创建,在 state 中定义共享数据,并在多个组件中通过 this.$store.state 读取且验证响应式更新。
  4. 能够将 store 配置抽离到独立模块并导出,在入口文件中引入挂载,使应用正常运行且共享状态可读取。
  5. 能够使用 mapState 的数组写法和对象写法将 state 映射为计算属性,使用重命名解决命名冲突,并保留自定义计算属性。
  6. 能够手写一个简化版 mapState,返回以 state 字段为键、以函数为值的对象,并说明其通过 this.$store.state 或接收 state 参数取值的原理;能完成投票功能中多候选人票数的展示与点击加票响应式更新。

知识点 ​

为什么要抽离 store ​

为什么需要:在 src/main.js 里直接写 Vue.use(Vuex)、new Vuex.Store({...}),入口文件同时承担“创建 Vue 实例”和“创建仓库”两件事。转写稿里提到,store 写在入口里“东西太多,影响阅读”。项目一大,state、mutations、actions 都堆进来,main.js 会越来越乱。

怎么想:main.js 是应用入口,最好只做三件事:引入根组件、引入仓库、创建根实例并挂载。仓库的创建和配置属于 Vuex 自己的职责,应该放到独立目录里。转写稿的做法是:在 src 下新建 store 目录,再新建 index.js,把 Vue、Vuex、Vue.use(Vuex) 和 new Vuex.Store(...) 都搬过去,最后 export default store。

怎么做:先保留笔记里的原始写法,理解它做了什么:

js
import Vue from "vue";
// 1- 引入(检查是否下载了vuex)
import Vuex from "vuex";
import App from "@/App";
// 2- 安装
Vue.use(Vuex);
const store = new Vuex.Store({
	state:{
		num:0
	}
})
new Vue({
	render:h=>h(App),
	// 3- 应用
	store
}).$mount("#app");

这段代码里,Vue.use(Vuex) 是安装插件,new Vuex.Store 是创建仓库,store 注入根实例后,组件里才能通过 this.$store 访问。抽离时把前三步搬到 src/store/index.js:

js
import Vue from "vue";
import Vuex from "vuex";

Vue.use(Vuex);

const store = new Vuex.Store({
  state: {
    num: 0
  }
});

export default store;

main.js 只保留引入和注入:

js
import Vue from "vue";
import App from "@/App";
import store from "./store";

new Vue({
  render: h => h(App),
  store
}).$mount("#app");

易错点:Vue.use(Vuex) 不要两边都写;抽离后 main.js 不能再使用 Vuex 变量;export default store 和 import store from "./store" 要对应;如果 store/index.js 存在,路径写 ./store 通常会找 index.js,但不要凭感觉改路径。

抽离后 main.js 的职责变化 ​

为什么需要:抽离不是为了“多建一个文件”,而是让入口更短、职责更清楚。转写稿里先演示“把 store 变量提出来再注入”,再进一步“处理到外边去”,说明抽离是逐步进行的:先让入口可读,再把仓库配置移出入口。

怎么想:抽离后,main.js 不需要知道 state 里有什么,也不需要知道 Vuex 怎么安装。它只需要拿到一个已经创建好的 store 实例,并注入根 Vue。这样以后新增模块、拆分 state,都改 store/index.js,入口不动。

怎么做:按上面的两段代码落地。可以检查 package.json 是否有 vuex,转写稿也提醒“检查是否下载了 vuex”;如果没有,需要单独安装。然后运行项目,确认页面不报错,组件里能正常拿到 this.$store.state.num。

易错点:不要在 main.js 里重复 new Vuex.Store;不要把 store 写成普通对象;根实例注入属性名必须是 store,否则 this.$store 取不到;抽离后如果报“Vuex is not defined”,通常是 main.js 还残留了 Vue.use(Vuex) 或 new Vuex.Store。

验证抽离是否成功 ​

为什么需要:抽离是结构性改动,代码看起来“只是搬家”,但很容易因为路径、导出方式、重复安装出问题。转写稿里通过刷新、查看控制台、观察数据是否响应来验证,这个调试过程要保留。

怎么想:验证分两层:第一层是项目能启动、控制台无报错;第二层是仓库数据仍能被组件使用,并且修改 state 后视图能响应。转写稿中通过“加一”观察页面变化,说明仓库仍然是响应式的。

怎么做:启动项目,打开控制台看是否有 Vuex 相关报错;在组件中读取 this.$store.state.num;临时把 num 改成其他初始值,确认页面跟着变;再改回 0。如果项目能正常渲染,说明 store 已注入成功。

易错点:抽离后 store/index.js 里仍然要 Vue.use(Vuex),但只能执行一次;state 要放在 new Vuex.Store 的配置对象里,不要写成组件 data;不要直接改 state 来验证长期逻辑,本节只验证“能拿到、能响应”;如果刷新后报错,先看 import store from "./store" 的路径和文件名大小写。

本节要解决的问题是:main.js 里同时写 Vue 根实例和 Vuex 仓库,代码越堆越多,阅读和定位都变差。转写稿里的处理思路很明确:先把 store 从 new Vue({...}) 里“拎”出来,再把它放到 src/store/index.js,最后在 main.js 只负责引入和挂载。

为什么需要把 store 抽离到外部 ​

为什么需要:main.js 是入口文件,职责应该尽量单一。如果 Vuex 的 state、mutations、actions 都写在入口里,入口会越来越长,后面找仓库配置很费劲。

怎么想:先不要一步到位。转写稿里说“我先不把它处理到外边”,而是先在 main.js 里写 const store = new Vuex.Store({...}),再把 store 传给 Vue 实例。这样先确认“局部抽离”没问题,再搬到外部文件。

怎么做:在 main.js 中先形成“创建仓库 → 挂载仓库”的结构,而不是把仓库对象直接塞进 new Vue。

易错点:不要一边抽离一边改业务逻辑。先只搬位置,不改 state 内容,方便对照验证。

先做局部抽离:把 store 变成变量 ​

为什么需要:直接写 new Vue({ store: new Vuex.Store({...}) }) 会把仓库配置和根实例混在一起,后续无法直接复用。

怎么想:仓库是一个对象,可以先赋值给 const store,再在根实例里写 store。转写稿里说“我写了一个 const store = new Vuex.Store,然后这个地方直接写个 store 就行”。

怎么做:局部形式类似:

js
const store = new Vuex.Store({
  state: {
    num: 0
  }
});

new Vue({
  render: h => h(App),
  store
}).$mount("#app");

易错点:store 是变量名,不是字符串;new Vue 里写 store 等价于 store: store。

外部抽离:新建 src/store/index.js ​

为什么需要:局部抽离只是把代码从根实例里拿出来,仓库配置还在 main.js。项目变大后,入口仍然会被仓库代码占住。

怎么想:Vuex 仓库属于“全局共享数据”,应该有自己的目录。转写稿里说“在 src 里面新建一个目录,名字就叫 store,然后新建一个文件 index.js”,把 Vue.use(Vuex) 和 new Vuex.Store 都搬过去。

怎么做:新建 src/store/index.js,保留本节笔记代码:

js
import Vue from "vue";
import Vuex from "vuex";

Vue.use(Vuex);
const store = new Vuex.Store({
	state:{
		num:0
	}
});
export default store;

易错点:

  1. 目录名和文件名要对应:src/store/index.js,引入时写 @/store 可以省略 index.js。
  2. Vue.use(Vuex) 要跟着仓库文件走,不要只留在 main.js。
  3. 最后必须 export default store,否则 main.js 引入不到默认导出。

main.js 只负责引入并挂载 store ​

为什么需要:入口文件应该清楚表达“我引入了哪些全局能力”。仓库抽走后,main.js 只保留 Vue、store、App 和根实例。

怎么想:转写稿里说“我是不是在这里面不就直接引入我的就行了”,也就是 import store from "@/store",然后 new Vue 里继续传 store。

怎么做:本节笔记代码为:

js
import Vue from "vue";
import store from "@/store";
import App from "@/App";
new Vue({
	render:h=>h(App),
	store
}).$mount("#app");

易错点:

  1. 不要写成 import store from "@/store/index.js" 也可以,但 @/store 更简洁;关键是路径别写错。
  2. main.js 里不要再重复 Vue.use(Vuex),仓库文件已经执行过。
  3. 忘记在 new Vue 里写 store,组件里 this.$store 就拿不到仓库。

验证抽离是否成功 ​

为什么需要:抽离不是“代码搬完就行”,要确认仓库仍然挂载成功、数据仍然可读、响应式仍然生效。

怎么想:转写稿里通过“把 num 加个一”观察视图变化,说明仓库里的数据变,视图会跟着变。这里不要编造复杂业务,只验证 num。

怎么做:在组件中读取 this.$store.state.num,或在模板中写 $store.state.num;把 num 从 0 改成 1,观察页面是否更新;同时看控制台是否无报错。

易错点:如果页面不更新,先检查 store 是否真的传进根实例;如果报“属性或方法未定义”,先检查 state 字段名和组件读取路径是否一致。

小结:抽离顺序与检查清单 ​

为什么需要:这类“搬代码”的练习最容易出现路径、默认导出、挂载遗漏。

怎么想:按“局部变量 → 外部文件 → 入口引入 → 页面验证”的顺序走,每一步只做一件事。

怎么做:检查四件事:src/store/index.js 是否存在并默认导出;main.js 是否 import store from "@/store";new Vue 是否传入 store;组件是否能读到 $store.state.num。

易错点:不要为了抽离而改 state 结构;不要漏掉 Vue.use(Vuex);不要把 store 写成普通对象而漏掉 new Vuex.Store。转写稿里反复强调“先弄明白这一块”,本质就是先把仓库挂载链路打通,再谈后续的简写和原理。

从详情页不刷新说起:beforeRouteUpdate 的拦截 ​

为什么需要:详情页切换电影时,地址栏参数变了,但页面内容不跟着变。课堂里点击“神兽魔王大作战”能显示内容,再点“雪豹和他的朋友”却不刷新,根因就在路由更新被拦住了。
怎么想:先看路由参数是否变化。to.params.id 是即将进入的详情 ID;如果组件复用了,路由更新会走 beforeRouteUpdate。这里没有调用 next(),更新就被拦下,详情接口自然不重新请求。
怎么做:在详情组件中放行更新,再用新参数取数据:

js
beforeRouteUpdate(to, from, next) {
  console.log(to.params.id) // 即将切换到的电影 ID
  // 原来这里没有 next(),路由更新被拦下
  next()
}

易错点:如果当前路由参数和目标参数相同,beforeRouteUpdate 可能不触发;不要把它当成“每次点击必执行”的钩子。还有,它拦截的是路由信息更新,不是你手动调用的接口方法。

正在热映/即将上映:用动态参数复用电影列表 ​

为什么需要:如果“正在热映”和“即将上映”各写一套列表组件,代码重复度很高,还要重复写三级详情路由。
怎么想:两个列表只有请求类型不同,可以共用一个电影列表组件,用路由参数区分。地址设计成 /film/:type,例如 /film/1、/film/2。
怎么做:在电影列表组件中监听路由参数,按 type 发请求;进入电影时默认重定向到 /film/1,并用 router-link 切换。课堂中用 watch 监听 $route.params.type,初次加载也要取一次数据。
易错点:只写 router-link 不配默认重定向,进入电影页可能没有选中项;刷新后要保持当前选中,需要依赖路由参数而不是临时变量。

Vuex:为什么要集中式状态管理 ​

为什么需要:页面 A、F 都要用商品列表时,各组件各存一份,数据重复且难同步。Vuex 是专门给 Vue 做集中式状态管理的插件,把共享数据放进仓库,谁需要谁取。
怎么想:仓库像大公司的财务部、开发部,要分模块管理;不是把数据随便堆在一起。小项目页面少可以不用,页面多、共享状态多时收益明显。
怎么做:先安装并引入 Vuex,创建 store,在 Vue 实例中挂载;组件里通过 this.$store.state.xxx 读取。仓库数据是响应式的,状态变,视图跟着变。
易错点:不要把 Vuex 当成“所有数据都必须放进去”;组件私有状态仍放组件内。读的时候注意是 $store.state,不是 $store 直接取属性。

创建、抽离 store 与投票数据读取 ​

为什么需要:store 写在入口文件里会越来越长,影响阅读。抽离后入口只负责引入。
怎么想:把 new Vuex.Store({...}) 放到 src/store/index.js,导出后由入口引入。投票功能中,把刘德华等票数放进 state。
怎么做:

js
// src/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    刘德华: 100
    // 黎明、郭富城、张学友同理
  }
})

组件读取票数:

js
this.$store.state.刘德华

易错点:抽离后忘了 export default,或入口忘了 import store from './store' 并挂到 Vue 实例上,都会导致 $store 取不到。

mapState 简写、命名冲突与原理 ​

为什么需要:每个票数都写 this.$store.state.刘德华 很啰嗦,属于同质化代码。
怎么想:官方提供 mapState,把仓库状态映射成计算属性。数组写法最简单;需要改名时用对象写法。
怎么做:

js
import { mapState } from 'vuex'

computed: {
  ...mapState(['刘德华', '黎明', '郭富城', '张学友'])
}

若和 data 里的名字冲突,可改名:

js
computed: {
  ...mapState({
    刘德华2: state => state.刘德华
  })
}

原理上,mapState 返回一个对象:数组元素作为属性名,属性值是读取 state 的函数;对象写法则用你给的名字作为计算属性名。
易错点:数组写法属性名写死,容易和 data、其他计算属性冲突;对象写法要保证函数返回的是 state.xxx,不是普通常量。课堂里还验证过:用 data 中转仓库数据,仓库变后 data 不会重新生成,视图不更新;用计算属性才会随仓库状态重新计算。

说明:本节转写稿未提供可引用的图片,故未插入图片;以上保留课堂关键代码片段。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

1. 抽离一:main.js 内联 store ​

js
// src/main.js
import Vue from "vue";
// 1- 引入(检查是否下载了 vuex)
import Vuex from "vuex";
import App from "@/App";

// 2- 安装
Vue.use(Vuex);

const store = new Vuex.Store({
  state: {
    num: 0
  }
});

new Vue({
  render: h => h(App),
  // 3- 应用
  store
}).$mount("#app");
vue
<!-- src/App.vue -->
<template>
  <div>{{ $store.state.num }}</div>
</template>

要点解释:

  • Vue.use(Vuex) 必须在 new Vuex.Store() 之前执行。
  • store 要挂到根实例 new Vue({ store }) 上,组件里才能用 $store。
  • 易错点:只引入 Vuex 但不 Vue.use(Vuex),后续 store 相关能力不生效。

2. 抽离二:外部 src/store/index.js ​

js
// src/store/index.js
import Vue from "vue";
import Vuex from "vuex";

Vue.use(Vuex);

const store = new Vuex.Store({
  state: {
    num: 0
  }
});

export default store;
js
// src/main.js
import Vue from "vue";
import store from "@/store";
import App from "@/App";

new Vue({
  render: h => h(App),
  store
}).$mount("#app");
vue
<!-- src/App.vue -->
<template>
  <div>{{ $store.state.num }}</div>
</template>

要点解释:

  • import store from "@/store" 默认会找 src/store/index.js。
  • 抽离后 main.js 只负责引入并挂载 store,阅读更清晰。
  • 易错点:Vue.use(Vuex) 放在 store/index.js 后,main.js 不要重复创建 store;路径写错会找不到默认导出。

3. 投票功能:mapState 简写 ​

js
// src/store/index.js
import Vue from "vue";
import Vuex from "vuex";

Vue.use(Vuex);

const store = new Vuex.Store({
  state: {
    刘德华: 100,
    黎明: 0,
    郭富城: 0,
    张学友: 0
  }
});

export default store;
js
// src/main.js
import Vue from "vue";
import store from "@/store";
import App from "@/App";

new Vue({
  render: h => h(App),
  store
}).$mount("#app");
vue
<!-- src/App.vue -->
<template>
  <div>
    <h3>投票</h3>
    <p>刘德华:{{ 刘德华 }} <button @click="add('刘德华')">+</button></p>
    <p>黎明:{{ 黎明 }} <button @click="add('黎明')">+</button></p>
    <p>郭富城:{{ 郭富城 }} <button @click="add('郭富城')">+</button></p>
    <p>张学友:{{ 张学友 }} <button @click="add('张学友')">+</button></p>
    <p>合计:{{ total }}</p>
  </div>
</template>

<script>
import { mapState } from "vuex";

export default {
  computed: {
    ...mapState(["刘德华", "黎明", "郭富城", "张学友"]),
    total() {
      return this.刘德华 + this.黎明 + this.郭富城 + this.张学友;
    }
  },
  methods: {
    add(name) {
      this.$store.state[name]++;
    }
  }
};
</script>

要点解释:

  • ...mapState([...]) 是官方简写,内部会生成计算属性,取值来自 this.$store.state。
  • total 是自定义计算属性,要放在 computed 中;...mapState 后仍可追加自定义计算属性。
  • 易错点:mapState 生成的是计算属性,不要写 this.刘德华++ 直接修改;若开启严格模式,应通过 mutation 修改 state。
  • 若属性名和 data 中名字冲突,可用对象形式重命名,例如 ...mapState({ 刘德华票数: state => state.刘德华 })。

常见错误 ​

  1. 现象:抽离 store 后,组件拿到的仍是旧数据或修改不生效。
    原因:main.js 里还保留着 new Vuex.Store(...),同时又从 @/store 引入了一份,实际存在两个 store 实例。
    修法:只在 src/store/index.js 创建并 export default store,main.js 仅 import store from "@/store" 后注入根实例。

  2. 现象:详情页从 A 切到 B 时页面不更新,再点回 A 才恢复;地址栏有时也不变。
    原因:beforeRouteUpdate 中做了拦截但没有 next() 放行,路由信息未更新,后续依赖新路由参数的请求/渲染不执行。
    修法:确认需要继续跳转时调用 next(),再基于 to.params.id 发请求;不需要继续时才阻断。

  3. 现象:投票页点击加号,票数在 Vuex 中已变,但页面数字不动。
    原因:在 data 中解构了 this.$store.state,data 只初始化一次,不会随仓库状态变化重新生成。
    修法:改用 computed 读取,或使用 mapState 映射;不要用 data 缓存仓库状态。

  4. 现象:使用 mapState 后控制台出现重复属性/冲突警告,页面值被覆盖。
    原因:映射出的计算属性名与本地 data、computed 或已有属性同名。
    修法:改用对象形式起别名,如 { localName: state => state.xxx };或删除本地同名属性。

  5. 现象:import store from "@/store" 报错或注入后 this.$store 为 undefined。
    原因:目录/文件名不匹配,或抽离后忘记在入口安装/注入 Vuex。
    修法:确认存在 src/store/index.js 且默认导出 store;入口保留 Vue.use(Vuex) 与根实例 store 注入。

练习 ​

  1. 把 16-3 中写在 main.js 的 store 抽离到 src/store/index.js,要求 main.js 只保留引入与注入,并说明如何验证没有产生两份 store。
  2. 复现“详情页 A→B 不更新”的路由场景,补上 beforeRouteUpdate 的放行逻辑,记录 next() 放在什么位置以及请求应使用哪个参数。
  3. 完成投票功能:四个候选人初始票数不同,点击各自加号后票数更新;分别用 computed 和 mapState 各实现一版并比较。
  4. 用 mapState 对象形式给“刘德华”起别名,避免与本地属性冲突;再增加一个“合计票数”的自定义计算属性。
  5. 手写一个简化版 mapState,支持数组和对象两种入参,并说明它返回的对象如何绑定到组件的 computed。

原视频对照 ​

#视频时长
1问题06:23
2练习14:04
3Vuex的基本使用19:10
4store抽离02:41
5简写28:28
6原理15:51

小结 ​

  1. store 应抽离为单一模块文件,入口只负责安装、引入和注入,避免多实例。
  2. 路由守卫里“拦截”和“放行”必须成对考虑,否则会出现地址、参数、请求不同步。
  3. 仓库状态进入组件视图时,优先用 computed/mapState,不要用 data 做一次性拷贝。
  4. mapState 的数组写法适合无冲突场景,对象写法适合改名和避免同名覆盖。
  5. mapState 的本质是生成一组读取 store 状态的计算属性函数。
  6. 投票功能的关键不是按钮本身,而是共享状态变更后视图能持续响应。
  7. 验证时要覆盖“首次进入、同级切换、刷新页面、重复点击当前项”四类场景。