Skip to content

Latest commit

 

History

History
874 lines (655 loc) · 24.5 KB

File metadata and controls

874 lines (655 loc) · 24.5 KB

二十、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,将显示相应的组件:

动态路线

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

// 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),他们将看到h1和文本Hello Paul

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

路线道具

让我们改变我们的/hello/name路线,将name参数作为component道具传递,这可以通过向路线添加props: true标志来实现:

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

然后,我们可以更新我们的组件,以接收一个名为id的道具,并将其记录到生命周期挂钩中的控制台:

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

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

组件导航卫士

我们如何解决生命周期挂钩问题?在这种情况下,我们可以使用所谓的导航卫士。这使我们能够连接到路由的不同生命周期,例如beforeRouteEnterbeforeRouteUpdatebeforeRouteLeave方法。

路由更新前

让我们使用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的访问、查询、完整路径等。

虽然我们正确地获得了 log 语句,但如果我们尝试在路由之间导航,您会注意到我们的应用不会使用参数nameprop 进行更新。这是因为在我们完成了守卫中的任何计算之后,我们还没有使用next函数。我们再加上:

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

饭前

我们还可以利用beforeRouteEnter在进入组件路径之前执行操作。下面是一个例子:

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

我们仍然需要调用next将堆栈向下传递给下一个路由处理程序。

临行前

我们还可以在离开路线时挂接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

全局路由挂钩

我们已经研究了组件导航卫士,虽然这些卫士是逐个组件工作的,但您可能希望建立侦听导航事件的全局钩子。

之前

我们可以使用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全局路由保护是在确认导航之前触发的,但重要的是要知道,只有在解决了所有特定于组件的保护和异步组件之后才会触发。

下面是一个例子:

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

之后

我们还可以连接到全局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. 触发全局 beforeach guards:由于可以使用beforeEach创建全局路由中间件,所以在任何路由更新之前都会调用这些函数。
  4. 触发本地 beforeRouteUpdate****重用组件中的防护:正如我们前面看到的,每当我们使用不同参数导航到同一路线时,生命周期挂钩不会触发两次。相反,我们使用beforeRouteUpdate触发生命周期更改。
  5. 组件:中的路由前触发器每次导航到任何路由之前都会调用该触发器。在这个阶段,组件没有呈现,因此它没有访问this组件实例的权限。
  6. 解析异步路由组件:然后尝试解析项目中的任何异步组件。下面是一个例子:
const MyAsyncComponent = () => ({
component: import ('./LazyComponent.vue'),
loading: LoadingComponent,
error: ErrorComponent,
delay: 150,
timeout: 3000
})
  1. 路由前触发成功激活组件: 我们现在可以访问beforeRouteEnter钩子,并可以在解析路由之前执行任何操作。

  2. Trigger global beforeResolve hooks:提供的组件内保护和异步路由组件已经解决,我们现在可以挂接到全局router.beforeResolve方法中,该方法允许我们在此阶段执行操作。

  3. 导航:之前的所有导航防护都已被触发,用户现在成功导航到一条路线。

  4. 每次钩住后触发:虽然用户已经导航到了路线,但没有停在那里。接下来,路由触发一个全局afterEach钩子,该钩子可以访问tofrom参数。由于此阶段已解决路线问题,因此没有下一个参数,因此不会影响导航。

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

  6. 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替换当前历史堆栈,而不是在堆栈上推送导航项。下面是一个例子:

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

路由,开始

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

// 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

首先,我们在src/components内部创建两个组件,分别命名为Hello.vueWorld.vue

// 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巴别塔插件。通过运行以下项目将其安装到终端:

$ 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 中查看结果:

每个路由都会添加到其自己的捆绑包文件中,并随后提高了性能,因为初始捆绑包要小得多:

水疗项目

让我们创建一个使用 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)

定义路线

为了在我们的应用中将路由划分为不同的文件,我们可以首先在名为user.routes.jssrc/components/user下创建一个文件。每次我们有一个不同的功能集(需要路由),我们可以创建自己的*.routes.js文件,该文件可以导入路由的index.js

现在,我们只需导出一个新的空数组:

export const userRoutes = [];

然后我们可以将路线添加到我们的index.js(尽管我们尚未定义):

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

const routes = [...userRoutes];

我们使用的是 ES2015+spread 操作符,它允许我们使用数组中的每个对象,而不是数组本身。

为了初始化路由,我们可以创建一个新的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),
});

创建用户列表路由

我们应用的第一部分将是一个主页,其中显示来自 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 获取数据

在名为api.jssrc/utils下创建一个新文件。这将用于创建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组件,注册详细路线,并找到链接到该路线的方法:

创建详细信息页面

为了创建一个详细页面,我们可以创建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>

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

子路径

我们还可以从 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>

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

塔达!我们现在已经创建了一个具有多个路由、子路由、参数等的 Vue 应用!

总结

在本节中,我们了解了 Vue 路由以及如何使用它创建单页应用。因此,我们涵盖了从初始化路由插件到定义路由、组件、导航保护等所有内容。现在,我们具备了创建可扩展到单个组件的 Vue 应用的必要知识。

现在我们已经扩展了我们的知识并了解了如何使用 Vue 路由,我们可以在下一章中继续使用Vuex处理状态管理。