原文:zh.annas-archive.org/md5/E8B4B21F7ACD89D5DD2A27CD73B2E070

译者:飞龙

协议:CC BY-NC-SA 4.0

第二十章:Vue 路由模式

路由是任何单页面应用SPA)的重要组成部分。本章重点介绍了如何最大化使用 Vue 路由器,并从用户页面之间的路由、参数到最佳配置进行了讨论。

到本章结束时,我们将涵盖以下内容:

  • 在 Vue.js 应用程序中实现路由

  • 使用动态路由匹配创建路由参数

  • 将路由参数作为组件属性传递

单页面应用程序

现代 JavaScript 应用程序实现了一种称为 SPA 的模式。在其最简单的形式中,它可以被认为是根据 URL 显示组件的应用程序。由于模板被映射到路由,因此无需重新加载页面,因为它们可以根据用户导航的位置进行注入。

路由器的工作。

通过这种方式创建我们的应用程序,我们能够提高感知和实际速度,因为我们的应用程序更加动态。

使用路由器

让我们启动一个游乐项目并安装vue-router库。这使我们能够在应用程序内利用路由,并为我们提供现代 SPA 的功能。

在终端中运行以下命令:

# Create a new Vue project
$ vue init webpack-simple vue-router-basics

# Navigate to directory
$ cd vue-router-basics

# Install dependencies
$ npm install

# Install Vue Router
$ npm install vue-router

# Run application
$ npm run dev

由于我们在构建系统中使用 webpack,我们已经使用npm安装了路由器。然后我们可以在src/main.js中初始化路由器:

import Vue from 'vue';
import VueRouter from 'vue-router';

import App from './App.vue';

Vue.use(VueRouter);

new Vue({
  el: '#app',
  render: h => h(App)
});

这实际上将VueRouter注册为全局插件。插件只是一个接收Vueoptions作为参数的函数,并允许诸如VueRouter之类的库向我们的 Vue 应用程序添加功能。

创建路由

然后,我们可以在main.js文件中定义两个简单的组件,它们只是有一个模板,显示带有一些文本的h1

const Hello = { template: `<h1>Hello</h1>` };
const World = { template: `<h1>World</h1>`};

然后,为了在特定的 URL(如/hello/world)上在屏幕上显示这些组件,我们可以在应用程序内定义路由:

const routes = [
  { path: '/hello', component: Hello },
  { path: '/world', component: World }
];

现在我们已经定义了我们想要使用的组件以及应用程序内的路由,我们需要创建一个新的VueRouter实例并传递路由。

尽管我们使用了Vue.use(VueRouter),但我们仍需要创建一个新的VueRouter实例并初始化我们的路由。这是因为仅仅将VueRouter注册为插件,就可以让我们在 Vue 实例中访问路由选项:

const router = new VueRouter({
  routes
});

然后,我们需要将router传递给我们的根 Vue 实例:

new Vue({
  el: '#app',
  router,
  render: h => h(App)
});

最后,要在我们的App.vue组件中显示路由的组件,我们需要在template中添加router-view组件:

<template>
  <div id="app">
    <router-view/>
  </div>
</template>

如果我们导航到/#/hello//#/world,将显示相应的组件:

https://github.com/OpenDocCN/freelearn-fe-framework-zh/raw/master/docs/cpl-vue2-web-dev/img/b3d1083e-1788-45d4-a2cf-b4bdd945c407.png

动态路由

我们还可以根据特定参数动态匹配路由。这可以通过在参数名之前指定一个冒号的路由来实现。以下是使用类似问候组件的示例:

// Components
const Hello = { template: `<h1>Hello</h1>` };
const HelloName = { template: `<h1>Hello {{ $route.params.name}}` }

// Routes
const routes = [
 { path: '/hello', component: Hello },
 { path: '/hello/:name', component: HelloName },
]

如果我们的用户导航到/hello,他们将看到带有文本Helloh1。否则,如果他们导航到/hello/{name}(即 Paul),他们将看到带有文本Hello Paulh1

我们取得了很大的进展,但重要的是要知道,当我们导航到参数化的 URL 时,如果参数发生变化(即从/hello/paul/hello/katie),组件的生命周期钩子不会再次触发。我们很快会看到这一点!

路由 props

让我们将我们的/hello/name路由更改为将name参数作为component属性传递,这可以通过在路由中添加props: true标志来完成:

const routes = [
  { path: '/hello', component: Hello },
  { path: '/hello/:name', component: HelloName, props: true},
]

然后,我们可以更新我们的组件以接受一个带有nameid属性,并在生命周期钩子中将其记录到控制台中:

const HelloName = {
  props: ['name'],
  template: `<h1>Hello {{ name }}</h1>`,
  created() {
    console.log(`Hello ${this.name}`)
  }
}

如果我们尝试导航到不同的动态路由,我们会发现created钩子只会触发一次(除非我们刷新页面),即使我们的页面显示了正确的名称:

https://github.com/OpenDocCN/freelearn-fe-framework-zh/raw/master/docs/cpl-vue2-web-dev/img/dde3492f-a29a-427f-a5e8-734c819d56f3.png

组件导航守卫

我们如何解决生命周期钩子问题?在这种情况下,我们可以使用所谓的导航守卫。这允许我们钩入到路由器的不同生命周期中,比如beforeRouteEnterbeforeRouteUpdatebeforeRouteLeave方法。

beforeRouteUpdate

让我们使用beforeRouteUpdate方法来访问有关路由更改的信息:

const HelloName = {
  props: ['name'],
  template: `<h1>Hello {{ name }}</h1>`,
  beforeRouteUpdate(to, from, next) {
    console.log(to);
    console.log(from);
    console.log(`Hello ${to.params.name}`)
  },
}

如果我们在导航到/hello/{name}下的不同路由后检查 JavaScript 控制台,我们将能够看到用户要去的路由以及他们来自哪里。tofrom对象还为我们提供了对params、查询、完整路径等的访问权限。

虽然我们正确地得到了日志记录,但如果我们尝试在路由之间导航,你会注意到我们的应用程序不会使用参数name属性进行更新。这是因为我们在守卫内完成任何计算后没有使用next函数。让我们添加进去:

  beforeRouteUpdate(to, from, next) {
    console.log(to);
    console.log(from);
    console.log(`Hello ${to.params.name}`)
    next();
  },

beforeRouteEnter

我们还可以利用beforeRouteEnter来在进入组件路由之前执行操作。这里有一个例子:

 beforeRouteEnter(to, from, next) {
  console.log(`I'm called before entering the route!`)
  next();
 }

我们仍然必须调用next来将堆栈传递给下一个路由处理程序。

beforeRouteLeave

我们还可以钩入beforeRouteLeave来在离开路由时执行操作。由于我们已经在这个钩子的上下文中在这个路由上,我们可以访问组件实例。让我们看一个例子:

 beforeRouteLeave(to, from, next) {
 console.log(`I'm called before leaving the route!`)
 console.log(`I have access to the component instance, here's proof! 
 Name: ${this.name}`);
 next();
 }

再次,我们必须在这种情况下调用next

全局路由钩子

我们已经研究了组件导航守卫,虽然这些守卫是基于组件的,但你可能想要建立全局钩子来监听导航事件。

beforeEach

我们可以使用router.beforeEach来全局监听应用程序中的路由事件。如果你有认证检查或其他应该在每个路由中使用的功能,这是值得使用的。

这里有一个例子,简单地记录用户要去的路由和来自的路由。以下每个示例都假定路由器存在于类似以下范围的上下文中:

const router = new VueRouter({
  routes
})

router.beforeEach((to, from, next) => {
 console.log(`Route to`, to)
 console.log(`Route from`, from)
 next();
});

再次,我们必须调用next()来触发下一个路由守卫。

beforeResolve

beforeResolve全局路由守卫在确认导航之前触发,但重要的是要知道,这只是在所有特定于组件的守卫和异步组件已经解析之后。

这里有一个例子:

router.beforeResolve((to, from, next) => {
 console.log(`Before resolve:`)
 console.log(`Route to`, to)
 console.log(`Route from`, from)
 next();
});

afterEach

我们还可以钩入全局的afterEach函数,允许我们执行操作,但我们无法影响导航,因此只能访问tofrom参数:

router.afterEach((to, from) => {
 console.log(`After each:`)
 console.log(`Route to`, to)
 console.log(`Route from`, from)
});

解析堆栈

现在我们已经熟悉了提供的各种不同的路由生命周期钩子,值得调查的是,每当我们尝试导航到另一个路由时,整个解析堆栈。

  1. 触发路由更改:这是任何路由生命周期的第一阶段,当我们尝试导航到新路由时触发。一个例子是从/hello/Paul/hello/Katie。在这一点上还没有触发任何导航守卫。

  2. 触发组件离开守卫:接下来,任何离开守卫都会被触发,比如在加载的组件上的beforeRouteLeave

  3. 触发全局 beforeEach 守卫:由于全局路由中间件可以通过beforeEach创建,这些函数将在任何路由更新之前被调用。

  4. 在重用组件中触发本地 beforeRouteUpdate 守卫:正如我们之前看到的,每当我们使用不同的参数导航到相同的路由时,生命周期钩子不会被触发两次。相反,我们使用beforeRouteUpdate来触发生命周期更改。

  5. 在组件中触发 beforeRouteEnter:这在导航到任何路由之前每次都会被调用。在这个阶段,组件还没有被渲染,因此无法访问this组件实例。

  6. 解析异步路由组件:然后尝试解析项目中的任何异步组件。这里有一个例子:

const MyAsyncComponent = () => ({
component: import ('./LazyComponent.vue'),
loading: LoadingComponent,
error: ErrorComponent,
delay: 150,
timeout: 3000
})
  1. 在成功激活的组件中触发 beforeRouteEnter

现在我们可以访问beforeRouteEnter钩子,并在解析路由之前执行任何操作。

  1. 触发全局 beforeResolve 钩子:提供了组件内的守卫和异步路由组件已经解析,我们现在可以钩入全局的router.beforeResolve方法,允许我们在这个阶段执行操作。

  2. 导航:所有先前的导航守卫都已触发,用户现在成功导航到了一个路由。

  3. 触发 afterEach 钩子:虽然用户已经导航到了路由,但事情并没有到此为止。接下来,路由器会触发一个全局的afterEach钩子,该钩子可以访问tofrom参数。由于路由在这个阶段已经解析,它没有下一个参数,因此不能影响导航。

  4. 触发 DOM 更新:路由已经解析,Vue 可以适当地触发 DOM 更新。

  5. 在 beforeRouteEnter 中的 next 中触发回调:由于beforeRouteEnter无法访问组件的this上下文,next参数接受一个回调函数,在导航时解析为组件实例。一个例子可以在这里看到:

beforeRouteEnter (to, from, next) {   
 next(comp => {
  // 'comp' inside this closure is equal to the component instance
 }) 

程序化导航

我们不仅限于使用router-link进行模板导航;我们还可以在 JavaScript 中以编程方式将用户导航到不同的路由。在我们的App.vue中,让我们暴露<router-view>并让用户能够选择一个按钮,将他们导航到/hello/hello/:name路由:

<template>
  <div id="app">
    <nav>
      <button @click="navigateToRoute('/hello')">/Hello</button>
      <button 
       @click="navigateToRoute('/hello/Paul')">/Hello/Name</button>
    </nav>
    <router-view></router-view>
  </div>
</template>

然后我们可以添加一个方法,将新的路由推送到路由堆栈中*:*

<script>
export default {
  methods: {
    navigateToRoute(routeName) {
      this.$router.push({ path: routeName });
    },
  },
};
</script>

此时,每当我们选择一个按钮,它应该随后将用户导航到适当的路由。$router.push()函数可以接受各种不同的参数,取决于你如何设置你的路由。这里有一些例子:

// Navigate with string literal
this.$router.push('hello')

// Navigate with object options
this.$router.push({ path: 'hello' })

// Add parameters
this.$router.push({ name: 'hello', params: { name: 'Paul' }})

// Using query parameters /hello?name=paul
this.$router.push({ path: 'hello', query: { name: 'Paul' }})

router.replace

不要推送导航项到堆栈上,我们也可以用 router.replace 替换当前的历史堆栈。以下是一个例子:

this.$router.replace({ path: routeName });

router.go

如果我们想要向后或向前导航用户,我们可以使用 router.go;这本质上是对 window.history API 的抽象。让我们看一些例子:

// Navigate forward one record
this.$router.go(1);

// Navigate backward one record
this.$router.go(-1);

// Navigate forward three records
this.$router.go(3);

// Navigate backward three records
this.$router.go(-3);

延迟加载路由

我们还可以延迟加载我们的路由,以利用 webpack 的代码拆分。这使我们比急切加载路由时拥有更好的性能。为此,我们可以创建一个小型的试验项目。在终端中运行以下命令来执行:

# Create a new Vue project
$ vue init webpack-simple vue-lazy-loading

# Navigate to directory
$ cd vue-lazy-loading

# Install dependencies
$ npm install

# Install Vue Router
$ npm install vue-router

# Run application
$ npm run dev

让我们开始创建两个组件,名为 Hello.vueWorld.vue,在 src/components 中:

// Hello.vue
<template>
  <div>
    <h1>Hello</h1>
    <router-link to="/world">Next</router-link>
  </div>
</template>

<script>
export default {};
</script>

现在我们已经创建了我们的 Hello.vue 组件,让我们创建第二个 World.vue

// World.vue
<template>
  <div>
    <h1>World</h1>
    <router-link to="/hello">Back</router-link>
  </div>
</template>

<script>
export default {};
</script>

然后我们可以像通常一样初始化我们的路由器,在 main.js 中:

import Vue from 'vue';
import VueRouter from 'vue-router';

Vue.use(VueRouter);

主要区别在于导入组件的方式。这需要使用 syntax-dynamic-import Babel 插件。通过在终端中运行以下命令将其安装到项目中:

$ npm install --save-dev babel-plugin-syntax-dynamic-import

然后我们可以更新 .babelrc 来使用新的插件:

{
 "presets": [["env", { "modules": false }], "stage-3"],
 "plugins": ["syntax-dynamic-import"]
}

最后,这使我们能够异步导入我们的组件,就像这样:

const Hello = () => import('./components/Hello');
const World = () => import('./components/World');

然后我们可以定义我们的路由并初始化路由器,这次引用异步导入:

const routes = [
 { path: '/', redirect: '/hello' },
 { path: '/hello', component: Hello },
 { path: '/World', component: World },
];

const router = new VueRouter({
 routes,
});

new Vue({
 el: '#app',
 router,
 render: h => h(App),
});

然后我们可以通过在 Chrome 中查看开发者工具 | 网络选项卡来查看其结果,同时浏览我们的应用程序:

https://github.com/OpenDocCN/freelearn-fe-framework-zh/raw/master/docs/cpl-vue2-web-dev/img/ff7dcc30-147c-4a86-a2de-d3d406500244.png

每个路由都添加到自己的捆绑文件中,随后使我们的性能得到改善,因为初始捆绑文件要小得多:

https://github.com/OpenDocCN/freelearn-fe-framework-zh/raw/master/docs/cpl-vue2-web-dev/img/cd3f60f3-23a1-4d33-b4ac-3238740e8329.png

一个单页应用项目

让我们创建一个使用 RESTful API 和我们刚学到的路由概念的项目。在终端中运行以下命令来创建一个新项目:

# Create a new Vue project
$ vue init webpack-simple vue-spa

# Navigate to directory
$ cd vue-spa

# Install dependencies
$ npm install

# Install Vue Router and Axios
$ npm install vue-router axios

# Run application
$ npm run dev

启用路由

我们可以通过在应用程序中启用 VueRouter 插件来开始。为此,我们可以在 src/router 中创建一个名为 index.js 的新文件。我们将使用这个文件来包含所有特定于路由的配置,但根据底层功能将每个路由分离到不同的文件中。

让我们导入并添加路由插件:

import Vue from 'vue';
import VueRouter from 'vue-router';

Vue.use(VueRouter)

定义路由

为了将路由分离到应用程序中的不同文件中,我们首先可以在 src/components/user 下创建一个名为 user.routes.js 的文件。每当我们有一个需要路由的不同功能集时,我们可以创建自己的 *.routes.js 文件,然后将其导入到路由的 index.js 中。

目前,我们只需导出一个新的空数组:

export const userRoutes = [];

然后我们可以将路由添加到我们的 index.js 中(即使我们还没有定义任何路由):

import { userRoutes } from '../components/user/user.routes';

const routes = [...userRoutes];

我们正在使用 ES2015+ 的展开运算符,它允许我们使用数组中的每个对象而不是数组本身。

然后,我们可以初始化路由,创建一个新的 VueRouter 并传递路由,如下所示:

const router = new VueRouter({
  // This is ES2015+ shorthand for routes: routes
  routes,
});

最后,让我们导出路由,以便它可以在我们的主 Vue 实例中使用:

export default router;

main.js 中,让我们导入路由并将其添加到实例中,如下所示:

import Vue from 'vue';
import App from './App.vue';
import router from './router';

new Vue({
 el: '#app',
 router,
 render: h => h(App),
});

创建 UserList 路由

我们应用程序的第一部分将是一个主页,显示来自 API 的用户列表。我们过去曾使用过这个例子,所以你应该对涉及的步骤很熟悉。让我们在 src/components/user 下创建一个名为 UserList.vue 的新组件。

组件将看起来像这样:

<template>
  <ul>
    <li v-for="user in users" :key="user.id">
      {{user.name}}
    </li>
  </ul> 
</template>

<script>
export default {
  data() {
    return {
      users: [
        {
          id: 1,
          name: 'Leanne Graham',
        }
      ],
    };
  },
};
</script>

此时可以随意添加自己的测试数据。我们将很快从 API 请求这些数据。

由于我们已经创建了组件,我们可以在 user.routes.js 中添加一个路由,当激活 '/'(或您选择的路径)时显示此组件:

import UserList from './UserList';

export const userRoutes = [{ path: '/', component: UserList }];

为了显示这个路由,我们需要更新 App.vue,随后将内容注入到 router-view 节点中。让我们更新 App.vue 来处理这个问题:

<template>
 <div>
  <router-view></router-view>
 </div>
</template>

<script>
export default {};
</script>

<style>

</style>

我们的应用程序应该显示单个用户。让我们创建一个 HTTP 实用程序来从 API 获取数据。

从 API 获取数据

src/utils 下创建一个名为 api.js 的新文件。这将用于创建 Axios 的基本实例,然后我们可以在其上执行 HTTP 请求:

import axios from 'axios';

export const API = axios.create({
 baseURL: `https://jsonplaceholder.typicode.com/`
})

然后我们可以使用 beforeRouteEnter 导航守卫,在某人导航到 '/' 路由时获取用户数据:

<template>
  <ul>
    <li v-for="user in users" :key="user.id">
      {{user.name}}
    </li>
  </ul> 
</template>

<script>
import { API } from '../../utils/api';
export default {
  data() {
    return {
      users: [],
    };
  },
  beforeRouteEnter(to, from, next) {
    API.get(`users`)
      .then(response => next(vm => (vm.users = response.data)))
      .catch(error => next(error));
  },
};
</script>

然后我们发现屏幕上显示了用户列表,如下截图所示,每个用户都表示为不同的列表项。下一步是创建一个 detail 组件,注册详细路由,并找到链接到该路由的方法:

https://github.com/OpenDocCN/freelearn-fe-framework-zh/raw/master/docs/cpl-vue2-web-dev/img/7eeabc93-4aaa-486c-8426-2df9838d78cb.png

创建详细页面

为了创建详细页面,我们可以创建 UserDetail.vue 并按照与上一个组件类似的步骤进行操作:

<template>
  <div class="container">
    <div class="user">
      <div class="user__name">
        <h1>{{userInfo.name}}</h1>
        <p>Person ID {{$route.params.userId}}</p>
        <p>Username: {{userInfo.username}}</p>
        <p>Email: {{userInfo.email}}</p>
      </div>
      <div class="user__address" v-if="userInfo && userInfo.address">
        <h1>Address</h1>
        <p>Street: {{userInfo.address.street}}</p>
        <p>Suite: {{userInfo.address.suite}}</p>
        <p>City: {{userInfo.address.city}}</p>
        <p>Zipcode: {{userInfo.address.zipcode}}</p>
        <p>Lat: {{userInfo.address.geo.lat}} Lng: 
        {{userInfo.address.geo.lng}} </p>
      </div>

      <div class="user__other" >
        <h1>Other</h1>
        <p>Phone: {{userInfo.phone}}</p>
        <p>Website: {{userInfo.website}}</p>
        <p v-if="userInfo && userInfo.company">Company: 
        {{userInfo.company.name}}</p>
      </div>
    </div>
  </div>
</template>

<script>
import { API } from '../../utils/api';

export default {
  data() {
    return {
      userInfo: {},
    };
  },
  beforeRouteEnter(to, from, next) {
    next(vm => 
      API.get(`users/${to.params.userId}`)
        .then(response => (vm.userInfo = response.data))
        .catch(err => console.error(err))
    )
  },
};
</script>

<style>
.container {
 line-height: 2.5em;
 text-align: center;
}
</style>

由于在我们的详细页面中永远不应该有多个用户,因此userInfo变量被创建为 JavaScript 对象而不是数组。

然后我们可以将新组件添加到我们的user.routes.js中:

import UserList from './UserList';
import UserDetail from './UserDetail';

export const userRoutes = [
 { path: '/', component: UserList },
 { path: '/:userId', component: UserDetail },
];

为了链接到这个组件,我们可以在我们的UserList组件中添加router-link

<template>
  <ul>
    <li v-for="user in users" :key="user.id">
      <router-link :to="{ path: `/${user.id}` }">
      {{user.name}}
      </router-link>
    </li>
  </ul> 
</template>

如果我们然后在浏览器中查看,我们可以看到只有一个用户列出,下面的信息来自于与该用户关联的用户详细信息:

https://github.com/OpenDocCN/freelearn-fe-framework-zh/raw/master/docs/cpl-vue2-web-dev/img/362ddaab-acc1-4f18-bb20-8129a048dcfe.png

子路由

我们还可以从我们的 API 中访问帖子,因此我们可以同时显示帖子信息和用户信息。让我们创建一个名为UserPosts.vue的新组件:

<template>
  <div>
    <ul>
      <li v-for="post in posts" :key="post.id">{{post.title}}</li>
    </ul>
  </div>
</template>

<script>
import { API } from '../../utils/api';
export default {
  data() {
    return {
      posts: [],
    };
  },
  beforeRouteEnter(to, from, next) {
       next(vm =>
          API.get(`posts?userId=${to.params.userId}`)
          .then(response => (vm.posts = response.data))
          .catch(err => console.error(err))
     )
  },
};
</script>

这允许我们根据我们的userId路由参数获取帖子。为了将此组件显示为子视图,我们需要在user.routes.js中注册它:

import UserList from './UserList';
import UserDetail from './UserDetail';
import UserPosts from './UserPosts';

export const userRoutes = [
  { path: '/', component: UserList },
  {
    path: '/:userId',
    component: UserDetail,
    children: [{ path: '/:userId', component: UserPosts }],
  },
];

然后我们可以在UserDetail.vue组件内部添加另一个<router-view>标签来显示子路由。模板现在看起来像这样:

<template>
  <div class="container">
    <div class="user">
        // Omitted
    </div>
    <div class="posts">
      <h1>Posts</h1>
      <router-view></router-view>
    </div>
  </div>
</template>

最后,我们还添加了一些样式,将用户信息显示在左侧,帖子显示在右侧:

<style>
.container {
  line-height: 2.5em;
  text-align: center;
}
.user {
  display: inline-block;
  width: 49%;
}
.posts {
  vertical-align: top;
  display: inline-block;
  width: 49%;
}
ul {
  list-style-type: none;
}
</style>

如果我们然后转到我们的浏览器,我们可以看到数据的显示方式正如我们计划的那样,用户信息显示在左侧,帖子显示在右侧:

https://github.com/OpenDocCN/freelearn-fe-framework-zh/raw/master/docs/cpl-vue2-web-dev/img/8bfff65d-623a-47b0-98c0-fef25fe81962.png

哒哒!我们现在创建了一个具有多个路由、子路由、参数等的 Vue 应用程序!

总结

在这一部分,我们学习了关于 Vue Router 以及如何使用它来创建单页面应用程序。因此,我们涵盖了从初始化路由插件到定义路由、组件、导航守卫等方面的所有内容。我们现在有了必要的知识来创建超越单一组件的 Vue 应用程序。

既然我们扩展了我们的知识并了解了如何使用 Vue Router,我们可以继续在下一章节中处理Vuex中的状态管理。

第二十一章:使用 Vuex 进行状态管理

在本章中,我们将研究使用Vuex进行状态管理模式。Vuex可能并非每个应用程序都需要,但当适合使用它时,了解它的重要性以及如何实现它非常重要。

到本章结束时,您将完成以下工作:

  • 了解了Vuex是什么以及为什么应该使用它

  • 创建了您的第一个 Vuex 存储

  • 调查了 actions、mutations、getters 和 modules

  • 使用 Vue 开发工具逐步执行Vuex的 mutations。

什么是 Vuex?

状态管理是现代 Web 应用程序的重要部分,随着应用程序的增长,管理这种状态是每个项目都面临的问题。Vuex旨在通过强制使用集中式存储来帮助我们实现更好的状态管理,本质上是应用程序中的单一真相来源。它遵循类似于 Flux 和 Redux 的设计原则,并与官方 Vue 开发工具集成,为开发体验提供了很好的支持。

让我们更深入地定义这些术语。

状态管理模式(SMP)

我们可以将状态定义为组件或应用程序中变量/对象的当前值。如果我们将我们的函数视为简单的输入->输出机器,那么这些函数之外存储的值构成了我们应用程序的当前状态。

请注意,我在组件级别应用程序级别状态之间做出了区分。组件级状态可以定义为限定在一个组件内的状态(即,组件内的数据函数)。应用程序级状态类似,但通常在多个组件或服务之间使用。

随着我们的应用程序不断增长,跨多个组件传递状态变得更加困难。我们在本书前面看到,我们可以使用事件总线(即全局 Vue 实例)来传递数据,虽然这样做可以工作,但最好将我们的状态定义为一个集中式存储的一部分。这使我们能够更容易地推理应用程序中的数据,因为我们可以开始定义总是生成状态的新版本的actionsmutations,并且管理状态变得更加系统化。

事件总线是一种简单的状态管理方法,依赖于一个单一的视图实例,并且在小型 Vuex 项目中可能是有益的,但在大多数情况下,应该使用 Vuex。随着我们的应用程序变得更大,使用 Vuex 清晰地定义我们的操作和预期的副作用,使我们能够更好地管理和扩展项目。

如何将所有这些组合在一起的一个很好的例子可以在以下截图中看到(vuex.vuejs.org/en/intro.html):

https://github.com/OpenDocCN/freelearn-fe-framework-zh/raw/master/docs/cpl-vue2-web-dev/img/5eb4d0ef-ec8c-4060-870a-d76a7160c0c9.pngVuex 状态流

让我们将这个例子分解成一个逐步过程:

  1. 初始State呈现在 Vue 组件中。

  2. 一个 Vue 组件发送一个Action来从后端 API获取一些数据。

  3. 然后触发一个Commit事件,由一个Mutation处理。这个Mutation返回一个包含来自后端 API的数据的新版本的状态。

  4. 然后可以在 Vue Devtools中看到该过程,并且您可以在应用程序中发生的先前状态的不同版本之间“时间旅行”。

  5. 新的State然后呈现在Vue 组件中。

因此,我们的 Vuex 应用程序的主要组件是存储,它是我们所有组件的单一真相来源。存储可以被读取,但不能直接改变;它必须有变化函数来进行任何更改。虽然这种模式一开始可能看起来很奇怪,如果您以前从未使用过状态容器,但这种设计允许我们以一致的方式向我们的应用程序添加新功能。

由于 Vuex 是原生设计用于与 Vue 一起工作的,存储默认是响应式的。这意味着在存储内部发生的任何更改都可以实时看到,而无需任何黑客技巧。

思考状态

作为一个思考练习,让我们首先定义我们应用程序的目标以及任何状态、操作和潜在的变化。您现在不必将以下代码添加到您的应用程序中,所以请随意阅读,我们将在最后将所有内容整合在一起。

让我们首先将状态视为键/值对的集合:

const state = {
 count: 0 // number
}

对于我们的计数器应用程序,我们只需要一个状态元素 - 当前计数。这可能有一个默认值为0,并且是一个数字类型。由于这可能是应用程序内唯一的状态,您可以考虑此状态在这一点上是应用程序级别的。

接下来,让我们考虑用户可能想要执行的任何动作类型。

然后,这三种动作类型可以被分派到存储中,因此我们可以执行以下变异,每次返回一个新版本的状态:

  • 增量:将当前计数器加一(0 -> 1)

  • 减量:从当前计数器中减去一个(1 -> 0)

  • 重置:将当前计数器重置为零(n -> 0)

我们可以想象,在这一点上,我们的用户界面将被更新为我们计数的正确绑定版本。让我们实现这一点,使其成为现实。

使用 Vuex

现在我们已经详细了解了由Vuex驱动的应用程序的组成部分,让我们创建一个游乐项目来充分利用这些功能!

在您的终端中运行以下命令:

# Create a new Vue project
$ vue init webpack-simple vuex-counter

# Navigate to directory
$ cd vuex-counter

# Install dependencies
$ npm install

# Install Vuex
$ npm install vuex

# Run application
$ npm run dev

创建一个新存储

让我们首先创建一个名为index.js的文件,在src/store内。这是我们将用来创建新存储并汇集各种组件的文件。

我们可以首先导入VueVuex,并告诉 Vue 我们想要使用Vuex插件:

import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

然后,我们可以导出一个包含所有应用程序状态的状态对象的新Vuex.Store。我们导出这个,以便在必要时在其他组件中导入状态:

export default new Vuex.Store({
  state: {
    count: 0,
  },
}); 

定义动作类型

然后,我们可以在src/store内创建一个名为mutation-types.js的文件,其中包含用户可能在我们的应用程序中执行的各种动作:

export const INCREMENT = 'INCREMENT';
export const DECREMENT = 'DECREMENT';
export const RESET = 'RESET';

虽然我们不必明确地定义我们的动作,但尽可能使用常量是一个好主意。这使我们能够更好地利用工具和 linting 技术,并且使我们能够一目了然地推断整个应用程序中的动作。

动作

我们可以使用这些动作类型来提交一个新的动作,随后由我们的 mutations 处理。在src/store内创建一个名为actions.js的文件:

import * as types from './mutation-types';

export default {
  types.INCREMENT {
    commit(types.INCREMENT);
  },
  types.DECREMENT {
    commit(types.DECREMENT);
  },
  types.RESET {
    commit(types.RESET);
  },
};

在每个方法内部,我们正在解构返回的store对象,只取commit函数。如果我们不这样做,我们将不得不像这样调用commit函数:

export default {
 types.INCREMENT {
  store.commit(types.INCREMENT);
 }
}

如果我们重新查看我们的状态图,我们可以看到在提交一个动作后,该动作被变异器接收。

变异

变异是存储状态可以改变的唯一方法;这是通过提交/分派一个动作来完成的,如前所述。让我们在src/store内创建一个名为mutations.js的新文件,并添加以下内容:

import * as types from './mutation-types';

export default {
  types.INCREMENT {
    state.count++;
  },
  types.DECREMENT {
    state.count--;
  },
  types.RESET {
    state.count = 0;
  },
};

您会注意到,我们再次使用我们的操作类型来定义方法名;这是可能的,因为 ES2015+中的一个新功能名为计算属性名。现在,每当提交/分发一个操作时,变更器将知道如何处理这个操作并返回一个新的状态。

Getter

现在我们可以提交操作,并使这些操作返回状态的新版本。下一步是创建 getter,以便我们可以在整个应用程序中返回状态的切片部分。让我们在src/store内创建一个名为getters.js的新文件,并添加以下内容:

export default {
  count(state) {
    return state.count;
  },
};

由于我们有一个微小的示例,因此并不完全需要为此属性使用 getter,但是随着应用程序的扩展,我们将需要使用 getter 来过滤状态。将这些视为状态中值的计算属性,因此,如果我们想要返回此属性的修改版本以供视图层使用,我们可以这样做:

export default {
  count(state) {
    return state.count > 3 ? 'Above three!' : state.count;
  },
};

组合元素

为了将所有这些内容整合在一起,我们必须重新访问我们的store/index.js文件,并添加适当的stateactionsgettersmutations

import Vue from 'vue';
import Vuex from 'vuex';

import actions from './actions';
import getters from './getters';
import mutations from './mutations';

Vue.use(Vuex);

export default new Vuex.Store({
  state: {
    count: 0,
  },
  actions,
  getters,
  mutations,
});

在我们的App.vue中,我们可以创建一个template,它将给我们当前的计数以及一些按钮来增加减少重置状态:

<template>
  <div>
    <h1>{{count}}</h1>
    <button @click="increment">+</button>
    <button @click="decrement">-</button>
    <button @click="reset">R</button>
  </div>
</template>

每当用户点击按钮时,都会从以下方法之一内部分发一个操作。

import * as types from './store/mutation-types';

export default {
  methods: {
    increment() {
      this.$store.dispatch(types.INCREMENT);
    },
    decrement() {
      this.$store.dispatch(types.DECREMENT);
    },
    reset() {
      this.$store.dispatch(types.RESET);
    },
  },
}

再次使用常量可以提供更好的开发体验。接下来,为了利用我们之前创建的 getter,让我们定义一个computed属性:

export default {
  // Omitted
  computed: {
    count() {
      return this.$store.getters.count;
    },
  },
}

然后,我们有一个应用程序,显示当前计数并可以增加、减少或重置:

https://github.com/OpenDocCN/freelearn-fe-framework-zh/raw/master/docs/cpl-vue2-web-dev/img/25dfbbb3-75d4-4ad2-a3fe-08654b179023.png

负载

如果我们想让用户决定要增加计数的数量怎么办?假设我们有一个文本框,我们可以在其中添加一个数字并按照该数字增加计数。如果文本框设置为0或为空,我们将增加计数1

因此,我们的模板将如下所示:

<template>
  <div>
    <h1>{{count}}</h1>

    <input type="text" v-model="amount">

    <button @click="increment">+</button>
    <button @click="decrement">-</button>
    <button @click="reset">R</button>
  </div>
</template>

我们将在本地组件状态上放置金额值,因为这并不一定需要成为主要的 Vuex 存储的一部分。这是一个重要的认识,因为这意味着如果有必要,我们仍然可以拥有本地数据/计算值。我们还可以更新我们的方法,将金额传递给我们的操作/变更:

export default {
  data() {
    return {
      amount: 0,
    };
  },
  methods: {
    increment() {
      this.$store.dispatch(types.INCREMENT, this.getAmount);
    },
    decrement() {
      this.$store.dispatch(types.DECREMENT, this.getAmount);
    },
    reset() {
      this.$store.dispatch(types.RESET);
    },
  },
  computed: {
    count() {
      return this.$store.getters.count;
    },
    getAmount() {
      return Number(this.amount) || 1;
    },
  },
};

然后我们必须更新actions.js,因为现在它接收state对象和我们的amount作为参数。当我们使用commit时,让我们也将amount传递给 mutation:

import * as types from './mutation-types';

export default {
  types.INCREMENT {
    commit(types.INCREMENT, amount);
  },
  types.DECREMENT {
    commit(types.DECREMENT, amount);
  },
  types.RESET {
    commit(types.RESET);
  },
};

因此,我们的突变看起来与以前类似,但这次我们根据数量递增/递减:

export default {
  types.INCREMENT {
    state.count += amount;
  },
  types.DECREMENT {
    state.count -= amount;
  },
  types.RESET {
    state.count = 0;
  },
};

哒哒!现在我们可以根据文本值递增计数:

https://github.com/OpenDocCN/freelearn-fe-framework-zh/raw/master/docs/cpl-vue2-web-dev/img/ec4b3aa9-7881-4a79-a85f-f833e074c639.png

Vuex 和 Vue 开发工具

现在,我们有了一种一致的通过操作与存储进行交互的方式,我们可以利用 Vue 开发工具来查看我们随时间推移的状态。

我们将使用计数器应用程序作为示例,以确保您已经运行了这个项目,并在 Chrome(或您的浏览器的等效部分)中右键单击检查元素。如果我们转到 Vue 选项卡并选择 Vuex,我们可以看到计数器已加载初始应用程序状态:

https://github.com/OpenDocCN/freelearn-fe-framework-zh/raw/master/docs/cpl-vue2-web-dev/img/1d4129af-12fd-4142-a41e-bee1e9330e9c.png

从前面的屏幕截图中,您可以看到计数状态成员以及任何 getter 的值。让我们点击递增按钮几次,看看会发生什么:

https://github.com/OpenDocCN/freelearn-fe-framework-zh/raw/master/docs/cpl-vue2-web-dev/img/07f60794-5391-4b00-a18e-c44be12d7bf9.png

太棒了!我们可以看到 INCREMENT 操作以及对状态和 getter 的后续更改,以及有关突变本身的更多信息。让我们看看如何在我们的状态中进行时间旅行:

https://github.com/OpenDocCN/freelearn-fe-framework-zh/raw/master/docs/cpl-vue2-web-dev/img/9a539edf-c6c5-4c80-81da-1c70a99d4111.png

在前面的屏幕截图中,我选择了第一个操作上的时间旅行按钮。然后您可以看到我们的状态恢复为计数:1,并且这反映在其余的元数据中。然后应用程序将更新以反映状态的更改,因此我们可以逐个步骤地查看每个操作并在屏幕上查看结果。这不仅有助于调试,而且我们向应用程序添加的任何新状态都将遵循相同的过程,并以这种方式可见。

让我们点击一个操作上的提交按钮:

https://github.com/OpenDocCN/freelearn-fe-framework-zh/raw/master/docs/cpl-vue2-web-dev/img/4316b92c-fc0e-4096-aff9-1fde89c91586.png

正如您所看到的,这将合并我们的所有操作,直到我们点击提交,然后成为我们的基本状态的一部分。因此,计数属性等于您提交给基本状态的操作。

模块和可扩展性

目前,我们的所有东西都在根状态中。随着我们的应用程序变得越来越大,利用模块是个好主意,这样我们可以适当地将我们的容器分成不同的块。让我们通过在store文件夹中创建一个名为modules/count的新文件夹,将我们的计数状态转换为自己的模块。

然后,我们可以将actions.jsgetters.jsmutations.jsmutation-types.js文件移动到计数模块文件夹中。这样做后,我们可以在文件夹内创建一个index.js文件,仅导出这个模块的stateactionsgettersmutations

import actions from './actions';
import getters from './getters';
import mutations from './mutations';

export const countStore = {
  state: {
    count: 0,
  },
  actions,
  getters,
  mutations,
};

export * from './mutation-types';

我还选择从index.js文件中导出突变类型,这样我们可以通过仅从store/modules/count导入,按模块的方式在我们的组件中使用这些类型。由于在这个文件中导入了多个东西,我给存储起了名字countStore。让我们在store/index.js中定义新模块:

import Vue from 'vue';
import Vuex from 'vuex';
import { countStore } from './modules/count';

Vue.use(Vuex);

export default new Vuex.Store({
  modules: {
    countStore,
  },
});

我们的App.vue稍作修改;我们不再引用 types 对象,而是直接从这个模块中引用 types:

import * as fromCount from './store/modules/count';

export default {
  data() {
    return {
      amount: 0,
    };
  },
  methods: {
    increment() {
      this.$store.dispatch(fromCount.INCREMENT, this.getAmount);
    },
    decrement() {
      this.$store.dispatch(fromCount.DECREMENT, this.getAmount);
    },
    reset() {
      this.$store.dispatch(fromCount.RESET);
    },
  },
  computed: {
    count() {
      return this.$store.getters.count;
    },
    getAmount() {
      return Number(this.amount) || 1;
    },
  },
};

然后,我们可以通过与我们的计数示例相同的文件/结构来向我们的应用程序添加更多的模块。这使我们能够随着应用程序的持续增长而扩展。

总结

在这一章中,我们利用了Vuex库来实现 Vue 中的一致状态管理。我们定义了什么是状态,以及组件状态和应用程序级状态。我们学会了如何适当地将我们的操作、获取器、突变和存储在不同的文件中进行分割,以便扩展,以及如何在组件中调用这些项目。

我们还学习了如何使用Vuex的 Vue devtools 来逐步查看应用程序中发生的突变。这使我们能够更好地调试/推理我们在开发应用程序时所做的决定。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐