主题
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 访问。
怎么想:安装、注册、创建、注入、读取
按笔记代码,顺序可以拆成五步:
- 引入 Vuex:
import Vuex from "vuex"; - 安装插件:
Vue.use(Vuex); - 创建 store:
new Vuex.Store({ state: { num: 0 } }); - 注入根实例:
store: ...; - 组件读取:
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: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(),会导致导航卡住。
常见错误
现象:组件里
this.$store为undefined。
原因: 根实例未注入store,或Vue.use(Vuex)未执行、导入路径写错。
修法: 按“引入 Vuex →Vue.use(Vuex)→ 创建 Store → 根实例注入store”逐项核对。现象:直接改
state后页面不更新,或调试时状态来源混乱。
原因: 绕过mutation直接修改state。
修法: 组件只提交mutation/派发action;开发期开启严格模式定位直接修改。现象:多个组件读到的
num不一致。
原因: 重复new Vuex.Store(...),产生了多个 Store 实例。
修法: 将 Store 抽到src/store/index.js导出单例,入口只注入一次。现象:模板中
$store.state.xxx渲染为空。
原因:state字段名拼错,或组件读取路径与 Store 结构不一致。
修法: 先固定state结构,再用computed/mapState读取,减少模板里长路径硬写。现象:异步请求后状态更新难追踪。
原因: 在mutation中写异步逻辑。
修法: 异步放action,请求完成后再commit对应mutation。现象:模块化后组件取不到模块状态。
原因: 未开启namespaced,或访问路径缺少模块名。
修法: 模块设置namespaced: true,读取和提交时使用完整模块路径。
练习
- 在
main.js中完成 Vuex 安装与 Store 注入,并在App.vue中显示num。 - 增加一个按钮,点击后通过
mutation让num + 1,并在Child组件中读取同一个num。 - 把 Store 从入口文件抽离到
src/store/index.js,保证全项目只创建一个 Store 实例。 - 增加一个异步
action:1 秒后将num改为10,组件通过dispatch触发。 - 新增
user模块并开启命名空间,组件读取user.name,并提供更新该字段的提交方式。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 全局路由守卫 | 35:36 |
| 2 | 页面引入 | 04:16 |
| 3 | 实现一级路由 | 06:30 |
| 4 | 实现二级路由 | 09:05 |
| 5 | 获取列表数据 | 06:42 |
| 6 | 实现三级路由 | 26:31 |
小结
- Vuex 接入要保证“安装、创建、注入”三步完整,缺一步就容易出现
$store异常。 - Store 应抽离为单例,入口只负责注入,避免多实例导致状态不同步。
state是唯一数据源,组件不要直接改;同步变更走mutation,异步流程走action。- 简单读取可用
$store.state,复杂场景优先用computed/mapState,降低模板耦合。 - 字段名和层级要先定好,组件读取路径必须与 Store 结构严格一致。
- 出现“读不到、改不动、不同步”时,优先排查注入链、字段路径、是否绕过
mutation。 - 项目变大后再引入模块化和命名空间,但模块边界和访问路径要同步规范。
- 开发期开启严格模式有助于尽早发现直接修改
state的问题。