Skip to content

Vuex与理解Vuex(Lesson11 2/3) ​

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

学习目标 ​

  • 能说明 Vuex 的作用,并独立完成 store 的创建、安装及 state、mutation、action 的基本读写流程。
  • 能区分全局前置守卫、后置守卫和解析守卫的执行时机,并使用 beforeEach 实现登录权限校验、参数回跳和页面标题设置。
  • 能将第三方页面资源引入项目,完成入口文件依赖引入、根组件创建及 Vue 实例挂载。
  • 能配置一级路由与嵌套二级路由,使用 router-link、router-view、重定向和默认子路由实现页面切换与导航高亮。
  • 能使用 axios 发送请求获取列表数据,并将返回数据绑定到组件中完成列表渲染。
  • 能配置带动态参数的三级路由,使用 watch 或 beforeRouteUpdate 处理组件复用时的参数变化,并使用 push、replace、back、go 完成编程式导航。

知识点 ​

先厘清本节内容 ​

需要说明:本节笔记标题写作“Vuex”,但转写稿实际覆盖的是 Vue Router 的全局守卫、多级路由、列表与详情数据获取、编程式导航等内容。下面按转写稿整理,不额外补充 Vuex 内容。

为什么需要全局路由守卫 ​

为什么需要:如果新闻列表、商品列表、个人中心都要求登录,最直接的做法是在每个组件的 beforeRouteEnter 里各写一遍判断。路由一多,代码重复,维护困难。

怎么想:登录校验是“进入多个路由前都要做”的公共逻辑,应该从组件内抽到路由实例上。Vue Router 提供全局守卫,所有路由跳转都会经过它。

怎么做:在路由配置文件中写 router.beforeEach((to, from, next) => {})。给需要校验的路由加 meta: { isAuth: true },守卫里判断 to.meta.isAuth,再检查 localStorage 中是否有用户名;有则 next() 放行,没有则跳到登录页。

易错点:守卫里必须调用 next(),否则页面白屏;登录页本身不要再加登录校验,否则会形成“要登录先登录”的死循环;初始进入时 from 可能是 /,不要误以为一定来自某个业务页面。

全局前置守卫 beforeEach ​

为什么需要:它执行最早,适合做权限判断、登录拦截。

怎么想:to 是目标路由,from 是来源路由,next 是放行函数。需要拦截就 next('/login'),不需要就 next()。

怎么做:登录拦截时可把当前目标地址带过去,例如 next('/login?cb=' + to.fullPath),登录成功后再跳回原页面。转写稿中通过 this.$route.query.cb 取回地址,再用 push 返回。

易错点:next(false) 是中断导航;next('/login') 是跳转;不要既写跳转又写放行。拼接参数时注意 ?cb= 的键名要和读取时一致。

后置守卫 afterEach 与页面标题 ​

为什么需要:有些事不需要拦截,只是导航完成后统一处理,例如修改浏览器标题。

怎么想:afterEach 在导航离开后执行,没有 next,不能拦截导航。

怎么做:给路由 meta 加 title,在 afterEach 中写 document.title = to.meta.title || '默认标题'。转写稿用它把首页、新闻、商品、个人中心、登录页的标题分别设置出来。

易错点:不要指望 afterEach 阻止跳转;meta 是自定义信息,读取时要确认目标路由是否配置了该字段。

beforeResolve 解析守卫 ​

为什么需要:它位于 beforeEach 和组件内守卫之后、导航确认之前,适合做统一收尾。

怎么想:转写稿中它用得不多,但要理解执行顺序:beforeEach → beforeResolve → afterEach。

怎么做:同样接收 to、from、next,需要放行时调用 next()。

易错点:不要把它和 afterEach 混淆;它仍可影响导航,afterEach 已是导航完成后。

一级、二级、三级路由 ​

为什么需要:页面有导航区、内容区、详情区,需要嵌套展示。

怎么想:一级路由用 router-view 承载;二级路由写在父路由 children 中;三级详情用 details/:id 这类动态段。

怎么做:第三方页面资源可放 public,入口 main.js 引入路由并 Vue.use(VueRouter);导航链接用 router-link 的 to,选中样式用 active-class。父路由组件里也要放 router-view。默认子路由可用 redirect。

易错点:父级没有 router-view,子路由不显示;相对路径容易层层叠加,转写稿中就出现过多拼一段 coming 的问题,必要时写完整地址;动态参数名要和读取时一致。

列表与详情数据 ​

为什么需要:列表页要展示接口数据,详情页要根据 ID 请求详情。

怎么想:列表在 mounted 中请求并赋值;详情组件复用同一实例时,ID 变化不会重新挂载,需要监听 $route.params.id 或使用 beforeRouteUpdate。

怎么做:用 axios 发请求,带上必要请求头;详情页在 watch 或 beforeRouteUpdate 中根据新 ID 重新请求。加载中可用 isLoading 控制。

易错点:beforeRouteUpdate 里忘记 next() 会卡住;只写 mounted 会导致点击不同电影详情不刷新;请求地址拼接错误会拿不到数据。

编程式导航 push、replace、go ​

为什么需要:除了 router-link,还常需要在方法中跳转。

怎么想:push 会产生历史记录,replace 替换当前记录,go(-1) 后退、go(1) 前进。

怎么做:this.$router.push('/film/nowplaying');返回用 this.$router.go(-1)。

易错点:replace 不产生记录,连续使用后回退可能回到更早页面;前进后退前要确认历史栈里是否有对应记录。

先核对本节材料:标题与转写稿并不一致 ​

本节笔记标题是“16-1- 理解Vuex”,但提供的转写稿实际讲的是 Vue Router 的全局路由守卫、页面引入、一级/二级/三级路由、列表数据获取、详情页复用与前进后退,通篇没有出现 Vuex 的 store、state、mutation、action、getter 等可验证内容。按“不得编造”的要求,下面不补写转写稿未讲的 Vuex 知识,而是把转写稿中可验证的路由守卫与路由案例整理成教程正文;若后续要写 Vuex,需要另给对应转写稿。转写稿中未出现图片,故不插入图片。

为什么需要全局路由守卫 ​

转写稿从一个需求切入:个人中心要判断登录,新闻列表、商品列表也要判断登录。最初做法是在每个组件的 beforeRouteEnter 里各写一份判断,但讲师明确说“几十个都加上同一段代码就不太合适”,于是撤掉组件内写法,改用全局守卫。这里的“怎么想”是:权限判断是横切逻辑,不应该散落在每个页面;只要路由跳转都要经过全局守卫,就可以把“是否登录、是否需要鉴权、登录后回跳”集中处理。

全局守卫写在路由实例上,而不是组件内部,也不是单独配置文件里。转写稿中的结构是:

js
const router = new VueRouter({ ... })

router.beforeEach((to, from, next) => {
  // 全局前置守卫
  next()
})

export default router

讲师强调先定义 router 再导出,因为后面还要在实例上追加 beforeEach、afterEach、beforeResolve,直接导出后再追加不方便。

前置守卫 beforeEach 的执行与放行 ​

beforeEach 是全局前置守卫,进入任何路由组件前都会经过它。转写稿的调试过程很典型:只写 router.beforeEach((to, from, next) => { console.log(to) }) 却没有调用 next(),刷新后页面“啥都看不到了”。原因是守卫没有放行,所有路由都进不去。补上 next() 后才恢复。

参数含义:to 是“你要去哪”,from 是“你从哪来”,next 是放行函数。转写稿中反复打印 to、from,观察到首次进入时 from 可能是斜线或空来源,点击新闻列表时 from 才是首页。易错点:忘记 next() 导致白屏;在条件分支中只写跳转不写 next(false) 或 next('/login'),导致逻辑不清晰。

鉴权做法是给需要验证的路由加自定义 meta 属性,转写稿中叫 isAuth,也提到有人叫 requireAuth 等,名字可自定但必须统一:

js
{ path: '/news', component: News, meta: { isAuth: true } }

守卫中判断:

js
router.beforeEach((to, from, next) => {
  if (to.meta.isAuth) {
    if (localStorage.getItem('username')) {
      next()
    } else {
      next('/login')
    }
  } else {
    next()
  }
})

易错点:登录页本身不能加 isAuth,否则“进登录页先要求登录”,形成死循环。转写稿用“先有鸡还是先有蛋”说明这个逻辑错误。

登录后回跳:把来源地址带过去 ​

转写稿进一步优化体验:未登录跳登录页时,把当前要去的地点通过 query 带过去,登录成功后再回到原页面。代码形态是:

js
next({ path: '/login', query: { redirect: to.fullPath } })

登录页读取 this.$route.query.redirect,登录成功后 this.$router.push(redirect || '/')。调试时讲师先打印 query,发现参数名写错,纠正后才拿到值。易错点是参数名前后不一致;query 适合放可分享的地址参数,params 适合路由路径中的动态段,二者不要混。

后置守卫 afterEach 与解析守卫 beforeResolve ​

afterEach 在离开当前路由、进入目标路由之后执行,转写稿中验证它“在 beforeRouteLeave 之后执行”,并且它没有 next,不能在这里做拦截。典型用途是改标题:

js
router.afterEach((to, from) => {
  document.title = to.meta.title || '默认标题'
})

给路由加 meta: { title: '首页' },后置守卫统一设置 document.title,避免每个页面手写。易错点:在 afterEach 里写 next();以为它能阻止跳转。

beforeResolve 是全局解析守卫,转写稿验证它在 beforeEach 之后、组件挂载前执行,也有 to、from、next。讲师说它“用的不是很多”,可作为了解:多个守卫都要做的公共收尾可放这里,但仍要 next() 放行。

三级路由、组件复用与相对路径调试 ​

转写稿后半段做电影列表到详情:三级路由写成 details/:id,点击列表项用 router-link 跳到详情。调试中出现两个关键问题:一是详情组件复用,点击不同电影时 mounted 不再执行,解决方式是用 watch 监听 $route,或用 beforeRouteUpdate(to, from, next) 重新请求并 next();二是三级路由用相对路径拼接导致地址重复,讲师改为完整地址后才正常。前进后退用 this.$router.go(-1)、go(1);push 产生历史记录,replace 不产生记录,调试时通过连续点击再后退观察历史栈变化。易错点:相对路径与完整路径混用;组件复用后忘记重新发请求;replace 与 push 的使用场景混淆。

本节目标与笔记边界 ​

本节笔记标题是“Vuex 的基本使用”,核心只有两件事:在 src/main.js 中安装并注入 Vuex,在 src/App.vue 中通过 $store.state.num 读取共享状态。

需要先说明:你给的转写稿主要内容是全局路由守卫、一级/二级/三级路由、路由参数、编程式导航等,并没有出现 Vuex 的安装、state、$store 调试过程。因此下面只依据本节笔记扩写,不把转写稿里的路由守卫步骤硬套成 Vuex 推导,也不编造不存在的 Vuex 调试记录。转写稿中可复用的思想是“先注册插件/能力,再在实例或组件中使用”,但具体 Vuex 步骤仍以笔记代码为准。

为什么需要 Vuex:先解决“共享状态放哪” ​

当多个组件都要读同一个数据时,如果靠父子传参,层级一深就会很麻烦;如果每个组件各自维护一份,又容易出现数据不一致。Vuex 的作用是把这类需要共享的状态集中放到一个 store 中,组件统一从 store 读取。

本节只演示最基础的共享数字 num。它的价值不是“把 0 存起来”,而是建立一条清晰链路:main.js 创建 store → 注入根 Vue 实例 → 所有组件通过 this.$store 访问。

怎么想:安装、注册、创建、注入、读取 ​

按笔记代码,顺序可以拆成五步:

  1. 引入 Vuex:import Vuex from "vuex";
  2. 安装插件:Vue.use(Vuex);
  3. 创建 store:new Vuex.Store({ state: { num: 0 } });
  4. 注入根实例:store: ...;
  5. 组件读取:this.$store.state.num 或模板中 $store.state.num。

这里的关键是“先 Vue.use(Vuex),再 new Vue 时注入 store”。如果顺序反了,或者只在组件里 import 却没有 Vue.use,Vuex 的全局注入能力就不完整。

怎么做:main.js 完整写法 ​

笔记中的 src/main.js 如下,保留原代码:

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

这里的 store 不是普通对象,而是 new Vuex.Store(...) 创建的 store 实例。根实例注入后,子组件不需要再层层传 num,直接通过 $store 就能拿到。

怎么做:App.vue 读取共享状态 ​

笔记中的 src/App.vue 如下,保留原代码:

vue
<template>
    <div>
        <h3>App:{{$store.state.num}}</h3>
        <hr/>
        <Child/>
    </div>
</template>

<script>
import Child from "@/components/Child";
export default {
    name: "App.vue",
    components: {Child},
    mounted(){
        console.log("app",this.$store.state.num);
    }
}
</script>

<style scoped>

</style>

模板里写 {{$store.state.num}},脚本里写 this.$store.state.num。按笔记中的初始值,页面应显示 App:0,控制台应输出 app 0。这是本节最直接的验证方式。

易错点与调试检查 ​

第一,Vue.use(Vuex) 必须在创建根实例前执行。第二,store 要写成 new Vuex.Store({...}),不要写成普通 { state: { num: 0 } }。第三,组件中访问的是 this.$store.state.num,不是 this.state.num。第四,模板中可以省略 this,写 $store.state.num。第五,num 不要在组件 data 里再定义一份,否则你看到的可能不是 store 里的值。第六,若页面报 $store 未定义,优先检查根实例是否注入了 store,以及是否执行了 Vue.use(Vuex)。

本节调试不需要复杂工具:先看页面是否显示 App:0,再看 mounted 中 console.log("app", this.$store.state.num) 是否输出 app 0。如果两者一致,说明“安装—注入—读取”链路已经打通。后续再学习 mutations、actions、getters 时,仍是从这个最小 store 逐步扩展。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

代码1:Vuex 基本使用(完整 HTML)

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>Vuex 基本使用</title>
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vuex.js"></script>
</head>
<body>
  <div id="app"></div>

  <script>
    // 1- 安装 Vuex
    Vue.use(Vuex);

    const Child = {
      name: "Child",
      template: `<div><h4>Child: {{ $store.state.num }}</h4></div>`
    };

    const App = {
      name: "App",
      components: { Child },
      template: `
        <div>
          <h3>App:{{ $store.state.num }}</h3>
          <hr/>
          <Child/>
        </div>
      `,
      mounted() {
        console.log("app", this.$store.state.num);
      }
    };

    new Vue({
      render: h => h(App),
      // 2- 应用 store
      store: new Vuex.Store({
        state: {
          num: 0
        }
      })
    }).$mount("#app");
  </script>
</body>
</html>

关键行与易错点:

  • Vue.use(Vuex) 必须先执行,否则 new Vuex.Store 相关能力不可用。
  • store 要挂到 new Vue({...}) 根实例上,组件里才能用 this.$store / 模板 $store。
  • state 必须是对象;模板中访问状态写 $store.state.num。
  • 易错点:只在组件里写 $store,但根实例忘记传 store,会报 Cannot read properties of undefined。

代码2:Vue Router 全局前置守卫 + 登录权限控制(完整 HTML)

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>全局前置守卫-权限控制</title>
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue-router.js"></script>
</head>
<body>
  <div id="app"></div>

  <script>
    Vue.use(VueRouter);

    const Home = {
      template: `
        <div>
          <h3>首页</h3>
          <router-link to="/news">新闻列表</router-link> |
          <router-link to="/goods">商品列表</router-link> |
          <router-link to="/mine">个人中心</router-link> |
          <router-link to="/login">登录</router-link>
        </div>
      `
    };

    const NewsList = {
      template: `<div><h3>新闻列表</h3><router-link to="/">回首页</router-link></div>`
    };

    const GoodsList = {
      template: `<div><h3>商品列表</h3><router-link to="/">回首页</router-link></div>`
    };

    const Mine = {
      template: `<div><h3>个人中心</h3><router-link to="/">回首页</router-link></div>`
    };

    const Login = {
      data() {
        return { username: "" };
      },
      template: `
        <div>
          <h3>登录</h3>
          <input v-model="username" placeholder="请输入用户名" />
          <button @click="login">登录</button>
          <button @click="logout">退出登录</button>
        </div>
      `,
      methods: {
        login() {
          if (!this.username) return alert("请输入用户名");
          localStorage.setItem("username", this.username);

          // 登录成功后回到来源页;没有来源页就回首页
          const redirect = this.$route.query.redirect || "/";
          this.$router.push(redirect);
        },
        logout() {
          localStorage.removeItem("username");
          this.$router.push("/");
        }
      }
    };

    const routes = [
      { path: "/", component: Home },
      { path: "/news", component: NewsList, meta: { isAuth: true } },
      { path: "/goods", component: GoodsList, meta: { isAuth: true } },
      { path: "/mine", component: Mine, meta: { isAuth: true } },
      { path: "/login", component: Login }
    ];

    const router = new VueRouter({ routes });

    // 全局前置守卫
    router.beforeEach((to, from, next) => {
      console.log("to:", to.path, "from:", from.path);

      if (to.meta.isAuth) {
        if (localStorage.getItem("username")) {
          next();
        } else {
          // 把目标地址带过去,登录成功后回跳
          next(`/login?redirect=${encodeURIComponent(to.fullPath)}`);
        }
      } else {
        next();
      }
    });

    new Vue({
      router,
      template: `<div><router-view/></div>`
    }).$mount("#app");
  </script>
</body>
</html>

关键行与易错点:

  • to.meta.isAuth 只检查“要去的目标路由”是否需要登录。
  • 登录页不要配置 meta: { isAuth: true },否则会形成“登录前先登录”的死循环。
  • next() 必须调用,且一次导航中不要重复调用多次。
  • 未登录跳登录页时用 redirect 保存来源,登录成功后 this.$router.push(redirect) 回跳。
  • 易错点:localStorage.getItem("username") 的键名要和登录时 setItem 完全一致。

代码3:二级/三级路由 + beforeRouteUpdate 参数变化(完整 HTML)

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>二级/三级路由与 beforeRouteUpdate</title>
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue-router.js"></script>
</head>
<body>
  <div id="app"></div>

  <script>
    Vue.use(VueRouter);

    const Home = {
      template: `
        <div>
          <h3>首页</h3>
          <router-link to="/film/comingsoon">进入电影</router-link>
        </div>
      `
    };

    const Film = {
      template: `
        <div>
          <h3>电影</h3>
          <router-link to="/film/nowplaying">正在热映</router-link> |
          <router-link to="/film/comingsoon">即将上映</router-link>
          <hr/>
          <router-view/>
        </div>
      `
    };

    const NowPlaying = {
      template: `<div><h4>正在热映</h4></div>`
    };

    const ComingSoon = {
      data() {
        return {
          films: [
            { id: 1, name: "神兽魔王大作战" },
            { id: 2, name: "雪豹和他的朋友们" }
          ]
        };
      },
      template: `
        <div>
          <h4>即将上映</h4>
          <ul>
            <li v-for="item in films" :key="item.id">
              <router-link :to="'/film/comingsoon/details/' + item.id">
                {{ item.name }}
              </router-link>
            </li>
          </ul>
          <router-view/>
        </div>
      `
    };

    const FilmDetails = {
      data() {
        return {
          id: this.$route.params.id,
          film: { name: "", desc: "" }
        };
      },
      template: `
        <div>
          <h4>电影详情</h4>
          <p>ID:{{ id }}</p>
          <p>名称:{{ film.name }}</p>
          <p>介绍:{{ film.desc }}</p>
        </div>
      `,
      mounted() {
        this.getDetail(this.$route.params.id);
      },
      beforeRouteUpdate(to, from, next) {
        // 同一组件复用、参数变化时,mounted 不会再执行
        this.getDetail(to.params.id);
        next();
      },
      methods: {
        getDetail(id) {
          this.id = id;
          const list = {
            "1": { name: "神兽魔王大作战", desc: "详情1" },
            "2": { name: "雪豹和他的朋友们", desc: "详情2" }
          };
          this.film = list[id] || { name: "", desc: "" };
        }
      }
    };

    const routes = [
      { path: "/", component: Home },
      {
        path: "/film",
        component: Film,
        children: [
          { path: "nowplaying", component: NowPlaying },
          {
            path: "comingsoon",
            component: ComingSoon,
            children: [
              { path: "details/:id", component: FilmDetails }
            ]
          }
        ]
      }
    ];

    const router = new VueRouter({ routes });

    new Vue({
      router,
      template: `<div><router-view/></div>`
    }).$mount("#app");
  </script>
</body>
</html>

关键行与易错点:

  • 父级路由组件里必须写 <router-view/>,否则二级/三级路由组件没有出口。
  • 子路由 path 不要随意写 / 开头;/ 开头表示绝对路径,容易和父路径叠加错。
  • 三级路由用 details/:id 接收参数,组件里通过 this.$route.params.id 获取。
  • 同一组件内只改变 :id 时,mounted 不会再执行,要用 beforeRouteUpdate(to, from, next) 重新取数据。
  • 易错点:beforeRouteUpdate 里忘记 next(),会导致导航卡住。

常见错误 ​

  1. 现象:组件里 this.$store 为 undefined。
    原因: 根实例未注入 store,或 Vue.use(Vuex) 未执行、导入路径写错。
    修法: 按“引入 Vuex → Vue.use(Vuex) → 创建 Store → 根实例注入 store”逐项核对。

  2. 现象:直接改 state 后页面不更新,或调试时状态来源混乱。
    原因: 绕过 mutation 直接修改 state。
    修法: 组件只提交 mutation/派发 action;开发期开启严格模式定位直接修改。

  3. 现象:多个组件读到的 num 不一致。
    原因: 重复 new Vuex.Store(...),产生了多个 Store 实例。
    修法: 将 Store 抽到 src/store/index.js 导出单例,入口只注入一次。

  4. 现象:模板中 $store.state.xxx 渲染为空。
    原因: state 字段名拼错,或组件读取路径与 Store 结构不一致。
    修法: 先固定 state 结构,再用 computed/mapState 读取,减少模板里长路径硬写。

  5. 现象:异步请求后状态更新难追踪。
    原因: 在 mutation 中写异步逻辑。
    修法: 异步放 action,请求完成后再 commit 对应 mutation。

  6. 现象:模块化后组件取不到模块状态。
    原因: 未开启 namespaced,或访问路径缺少模块名。
    修法: 模块设置 namespaced: true,读取和提交时使用完整模块路径。

练习 ​

  1. 在 main.js 中完成 Vuex 安装与 Store 注入,并在 App.vue 中显示 num。
  2. 增加一个按钮,点击后通过 mutation 让 num + 1,并在 Child 组件中读取同一个 num。
  3. 把 Store 从入口文件抽离到 src/store/index.js,保证全项目只创建一个 Store 实例。
  4. 增加一个异步 action:1 秒后将 num 改为 10,组件通过 dispatch 触发。
  5. 新增 user 模块并开启命名空间,组件读取 user.name,并提供更新该字段的提交方式。

原视频对照 ​

#视频时长
1全局路由守卫35:36
2页面引入04:16
3实现一级路由06:30
4实现二级路由09:05
5获取列表数据06:42
6实现三级路由26:31

小结 ​

  1. Vuex 接入要保证“安装、创建、注入”三步完整,缺一步就容易出现 $store 异常。
  2. Store 应抽离为单例,入口只负责注入,避免多实例导致状态不同步。
  3. state 是唯一数据源,组件不要直接改;同步变更走 mutation,异步流程走 action。
  4. 简单读取可用 $store.state,复杂场景优先用 computed/mapState,降低模板耦合。
  5. 字段名和层级要先定好,组件读取路径必须与 Store 结构严格一致。
  6. 出现“读不到、改不动、不同步”时,优先排查注入链、字段路径、是否绕过 mutation。
  7. 项目变大后再引入模块化和命名空间,但模块边界和访问路径要同步规范。
  8. 开发期开启严格模式有助于尽早发现直接修改 state 的问题。