ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
[TOC] # vue-router ## \<router-link> 与 \<router-view> ```html <script src="https://unpkg.com/vue/dist/vue.js"></script> <script src="https://unpkg.com/vue-router/dist/vue-router.js"></script> <div id="app"> <h1>Hello App!</h1> <p> <!-- 使用 router-link 组件来导航. --> <!-- 通过传入 `to` 属性指定链接. --> <!-- <router-link> 默认会被渲染成一个 `<a>` 标签 --> <router-link to="/foo">Go to Foo</router-link> <router-link to="/bar">Go to Bar</router-link> </p> <!-- 路由出口 --> <!-- 路由匹配到的组件将渲染在这里 --> <router-view></router-view> </div> ``` ## 定义路由及路由匹配 项目中一般会单独将路由抽离为一个 store.js 文件,如下: ```js import Vue from 'vue' import Router from 'vue-router' Vue.use(Router) export default new Router({ routes: [ { path: '/', redirect: '/store' }, { path: '/ebook', component: () => import('./views/ebook/index.vue'), // 路由懒加载,这里用的是ES6的语法 import()函数是动态加载 import 是静态加载 children: [ { path: ':fileName', // 动态路由, 可以传递路径参数 component: () => import('./components/ebook/EbookReader.vue') } ] }, { path: '/store', component: () => import('./views/store/index.vue'), redirect: '/store/shelf', // #/store -> #/store/home children: [ { path: 'home', // children 使用相对路径 可以通过 store/home 来找到 component: () => import('./views/store/StoreHome.vue') }, { path: 'list', component: () => import('./views/store/StoreList.vue') }, { path: 'detail', // /store/detail 加斜杠则只能匹配/detail component: () => import('./views/store/StoreDetail.vue') }, ] } ] }) ``` 然后还需要再 main.js 中引入: ```js import router from './router' new Vue({ router, store, ... render: h => h(App) }).$mount('#app') ``` 上面有涉及 <span style="font-family:楷体;font-weight:700;">动态路径参数</span> 的概念:你可以在一个路由中设置多段“路径参数”,对应的值都会设置到`$route.params`中。例如 | 模式 | 匹配路径 | $route.params | | --- | --- | --- | | /user/:username | /user/evan | `{ username: 'evan' }` | | /user/:username/post/:post\_id | /user/evan/post/123 | `{ username: 'evan', post_id: '123' }` | 同时也涉及到了 <span style="font-family:楷体;font-weight:700;">嵌套路由</span> : ```js const router = new VueRouter({ routes: [ { path: '/user/:id', component: User, children: [ { // 当 /user/:id/profile 匹配成功, // UserProfile 会被渲染在 User 的 <router-view> 中 path: 'profile', component: UserProfile }, { // 当 /user/:id/posts 匹配成功 // UserPosts 会被渲染在 User 的 <router-view> 中 path: 'posts', component: UserPosts } ] } ] }) ``` >[danger] 注意:以 / 开头的嵌套路径会被当作根路径 ## 组件访问路由对象 可以在任何组件内通过`this.$router`访问路由器,也可以通过`this.$route`访问当前路由。 `$route`是 <span style="font-family:楷体;font-weight:700;">路由信息对象</span>,包括 path,params,hash,query,fullPath,matched,name 等路由信息参数。而 `$router` 是 <span style="font-family:楷体;font-weight:700;">路由实例对象</span>,包括了路由的跳转方法,钩子函数等。 ## 路由跳转及获取参数 | 声明式 | 编程式 | | --- | --- | | `<router-link :to="...">` | `router.push(...)` | ```js // 字符串 router.push('home') // 一般是在组件内部使用,即 this.$router.push() // 对象 router.push({ path: 'home' }) // 命名的路由 router.push({ name: 'user', params: { userId: '123' }}) // 带查询参数,变成 /register?plan=private router.push({ path: 'register', query: { plan: 'private' }}) ``` ```js // 在浏览器记录中前进一步,等同于 history.forward() router.go(1) // 一般是在组件内部使用,即 this.$router.go() // 后退一步记录,等同于 history.back() router.go(-1) // 前进 3 步记录 router.go(3) ``` 前面有提到过:`route`是路由信息对象,包括 path,params,hash,query,fullPath,matched,name 等路由信息参数,那么显然我们获取当前路由的一些信息就需要通过`route`对象,下面主要看看`params、query`这两个参数。 ```js // query 传参,使用 name 跳转 (name 是你为路由取的别名) this.$router.push({ name: 'detailPage', query: { queryId: '12345', queryName: 'query' } }) // query 传参,使用 path 跳转 this.$router.push({ path: '/detail', query: { queryId: '12345', queryName: 'query' } }) // 跳转到的路由组件接收参数 const id = this.$route.query.queryId const name = this.$route.query.queryName ``` 用 query 传参你的 URL 会类似这样`localhost:8080/#/detail?queryId=12345&queryName=query` 而 params 实际上是与动态路径参数相对应的,这个概念我们在上面也提到过 | 模式 | 匹配路径 | $route.params | | --- | --- | --- | | /user/:username | /user/evan | `{ username: 'evan' }` | | /user/:username/post/:post\_id | /user/evan/post/123 | `{ username: 'evan', post_id: '123' }` | 来看看官方给出的例子:如果提供了`path`,`params`会被忽略,你需要提供路由的`name`或手写完整的带有参数的`path`: ```js const userId = '123' router.push({ name: 'user', params: { userId }}) // -> /user/123 router.push({ path: `/user/${userId}` }) // -> /user/123 // 这里的 params 不生效 router.push({ path: '/user', params: { userId }}) // -> /user ``` 看到这就很明显了,如果你定义路由时没有采用动态路径参数的形式,那么一般都是采用 query 来传递信息,否则,可以使用 params 来传递信息。 ## hash 模式与 history 模式 <span style="font-size:20px; color: #42b983;">hash 模式</span> 在浏览器中符号 “#”,# 以及 # 后面的字符称之为 hash,用 window.location.hash 读取; 特点:hash 虽然在 URL 中,但不被包括在 HTTP 请求中;用来指导浏览器动作,对服务端安全无用,hash 不会重加载页面。 hash 模式下,仅 hash 符号之前的内容会被包含在请求中,如 `http://www.xxx.com`,因此对于后端来说,即使没有做到对路由的全覆盖,也不会返回 404 错误。 ***** <span style="font-size:20px; color: #42b983;">history 模式</span> history 采用 HTML5 的新特性;且提供了两个新方法:pushState(),replaceState()可以对浏览器历史记录栈进行修改,以及 popState 事件的监听到状态变更。 history 模式下,前端的 URL 必须和实际向后端发起请求的 URL 一致,如 `http://www.xxx.com/items/id`。后端如果缺少对 `/items/id` 的路由处理,将返回 404 错误。 使用该模式一般需要在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是你 app 依赖的页面。 hash 模式和 history 模式并没有改变路由跳转的语法,只是 history 模式的路由需要后台的支持,关键是 URL:hash 模式仅 hash 符号('#’)之前的内容会被包含在请求中。 ***** `vue-router`默认 hash 模式 —— 使用 URL 的 hash 来模拟一个完整的 URL,于是当 URL 改变时,页面不会重新加载。 如果想使用 history 模式,可以在 cli 构建时更换模式即可,或者: ```js const router = new VueRouter({ mode: 'history', routes: [...] }) ``` ## 导航守卫 还没用过....[https://router.vuejs.org/zh/guide/advanced/navigation-guards.html#%E7%BB%84%E4%BB%B6%E5%86%85%E7%9A%84%E5%AE%88%E5%8D%AB](https://router.vuejs.org/zh/guide/advanced/navigation-guards.html#%E7%BB%84%E4%BB%B6%E5%86%85%E7%9A%84%E5%AE%88%E5%8D%AB) # vuex ## 基础概念 每一个 Vuex 应用的核心就是 **store(仓库)**。store 基本上就是一个容器,它包含着你的应用中大部分的 **状态 (state)**。 Vuex 和单纯的全局对象有以下两点不同: - Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到更新。 - 你不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) ![](https://box.kancloud.cn/5145ed5562bb79d0e9b02155a5c1f2ac_787x569.png) 这个状态自管理应用包含以下几个部分: * **state**,驱动应用的数据源; * **view**,以声明方式将 **state** 映射到视图; * **actions**,响应在 **view** 上的用户输入导致的状态变化。 一般将 store 放在一个单独的 store.js 文件或 store 文件夹下,创建一个最简单的 store: ```js // store.js import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) export default new Vuex.Store({ state: { privilege: 0 }, /* 每个 mutation 都有一个字符串的事件类型 (type) 和 一个回调函数 (handler); 这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数 */ mutations: { 'SET_PRIVILEGE': (state, newPrivilege) => { state.privilege = newPrivilege } }, /* Action 提交的是 mutation,而不是直接变更状态。 Action 可以包含任意异步操作。 Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,因此你可以调用 context.commit 提交一个 mutation, 或者通过 context.state 和 context.getters 来获取 state 和 getters。 当我们在之后介绍到 Modules 时,你就知道 context 对象为什么不是 store 实例本身了。 */ actions: { setPrivilege({ commit }, newPrivilege) { return commit('SET_PRIVILEGE', newPrivilege) } } }) ``` 然后我们还需要在 main.js 中引入: ```js import store from './store' new Vue({ router, store, render: h => h(App) }).$mount('#app') ``` > 为什么 vuex 不像 redux 需要引入中间件而可以直接处理异步操作呢? ## 组件中获取 vuex 的 state 跟 redux 一样,vuex 使用单一状态树,即每个应用仅应该包含一个 store 实例。 方法 1:子组件在 **计算属性** 中返回某个状态(基于 Vuex 的状态存储是响应式的) 如下面的代码,每当`store.state.count`变化的时候, 都会重新求取计算属性,并且触发更新相关联的 DOM。 ```js // 创建一个 Counter 组件 const Counter = { template: `<div>{{ count }}</div>`, computed: { count () { return store.state.count } } } ``` 方法 2:通过 store 选项将状态从根组件"注入"到每个子组件(使用 vue-cli3 构建项目时自动使用该方式) 通过在根实例中注册 store 选项,该 store 实例会注入到根组件下的所有子组件中,且子组件能通过 `this.$store `访问到 ```js const app = new Vue({ el: '#app', // 把 store 对象提供给 "store" 选项,这可以把 store 的实例注入所有的子组件 store, components: { Counter }, template: ` <div class="app"> <counter></counter> </div> ` }) ``` ```js const Counter = { template: `<div>{{ count }}</div>`, computed: { count () { return this.$store.state.count } } } ``` 方法 3:使用 mapState 辅助函数(感觉可以被 mapGetter 替代?) 方法 4:使用 mapGetter(并不是单纯的映射关系,可以做一些处理) Vuex 允许我们在 store 中定义“getter”(可以认为是 store 的计算属性)。就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。 Getter 接受 state 作为其第一个参数: ```js const store = new Vuex.Store({ state: { todos: [ { id: 1, text: '...', done: true }, { id: 2, text: '...', done: false } ] }, getters: { doneTodos: state => { return state.todos.filter(todo => todo.done) } } }) ``` `mapGetters`辅助函数仅仅是将 store 中的 getter 映射到局部计算属性: ```js import { mapGetters } from 'vuex' export default { // ... computed: { // 使用对象展开运算符将 getter 混入 computed 对象中 ...mapGetters([ 'doneTodosCount', 'anotherGetter', // ... ]) } } ``` 如果你想将一个 getter 属性另取一个名字,使用对象形式: ```js mapGetters({ // 把 `this.doneCount` 映射为 `this.$store.getters.doneTodosCount` doneCount: 'doneTodosCount' }) ``` ## Mutation 与 Action ### 在组件中分发 action 在组件中使用`this.$store.dispatch('xxx')`分发 action,或者使用`mapActions`辅助函数将组件的 methods 映射为`store.dispatch`调用(需要先在根节点注入`store`): ```js import { mapActions } from 'vuex' export default { // ... methods: { ...mapActions([ 'increment', // 将 `this.increment()` 映射为 `this.$store.dispatch('increment')` // `mapActions` 也支持载荷: 'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.dispatch('incrementBy', amount)` ]), ...mapActions({ add: 'increment' // 将 `this.add()` 映射为 `this.$store.dispatch('increment')` }) } } ``` ### 组合 action `store.dispatch`可以处理被触发的 action 的处理函数返回的 Promise,并且`store.dispatch`仍旧返回 Promise ```js actions: { // ... actionB ({ dispatch, commit }) { return dispatch('actionA').then(() => { commit('someOtherMutation') }) } } ``` 使用 async / await: ```js // 假设 getData() 和 getOtherData() 返回的是 Promise actions: { async actionA ({ commit }) { commit('gotData', await getData()) }, async actionB ({ dispatch, commit }) { await dispatch('actionA') // 等待 actionA 完成 commit('gotOtherData', await getOtherData()) } } ``` ## 划分 Module Vuex 允许我们将 store 分割成**模块(module)**。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割 ```js const moduleA = { state: { ... }, mutations: { ... }, actions: { ... }, getters: { ... } } const moduleB = { state: { ... }, mutations: { ... }, actions: { ... } } const store = new Vuex.Store({ modules: { a: moduleA, b: moduleB } }) store.state.a // -> moduleA 的状态 store.state.b // -> moduleB 的状态 ``` 对于模块内部的 mutation 和 getter,接收的第一个参数是**模块的局部状态对象**。 ```js const moduleA = { state: { count: 0 }, mutations: { increment (state) { // 这里的 `state` 对象是模块的局部状态 state.count++ } }, getters: { doubleCount (state) { return state.count * 2 } } } ``` 项目结构: ```shell ├── index.html ├── main.js ├── api │ └── ... # 抽取出API请求 ├── components │ ├── App.vue │ └── ... └── store ├── index.js # 我们组装模块并导出 store 的地方 ├── actions.js # 根级别的 action ├── mutations.js # 根级别的 mutation └── modules ├── cart.js # 购物车模块 └── products.js # 产品模块 ``` 完整的 vuex 项目结构示例 [点击这里](https://github.com/ChenMingK/epub-Proj/tree/master/src/store)