构建自定义 Vue 路由

Avatar of Hassan Djirdeh
Hassan Djirdeh

DigitalOcean 为您的旅程的每个阶段提供云产品。 立即开始使用 $200 的免费信用!

许多教程 存在,这些教程很好地解释了如何将 Vue 的官方路由库 vue-router 集成到现有的 Vue 应用程序中。 vue-router 通过为我们提供将应用程序组件映射到不同浏览器 URL 路由所需的内容,出色地完成了这项工作。

但是,简单的应用程序通常不需要像 vue-router 这样的完整路由库。 在本文中,我们将使用 Vue 构建一个简单的自定义客户端路由器。 通过这样做,我们将了解构建客户端路由需要处理的内容,以及可能存在哪些潜在的缺陷。

虽然本文假设您具备 Vue.js 的基本知识; 我们将在开始编写代码时详细解释这些内容!

路由

首先:让我们为那些可能不熟悉该概念的人定义 **路由**。

在 Web 开发中,路由通常指的是根据浏览器 URL 中的规则拆分应用程序的 UI。 想象一下单击链接并将 URL 从 https://website.com 更改为 https://website.com/article/。 那就是路由。

路由通常分为两个主要类别

  • **服务器端路由:** 客户端(即浏览器)在 *每次 URL 更改时* 向服务器发出请求。
  • **客户端路由:** 客户端 *只* 在初始页面加载时向服务器发出请求。 然后,根据 URL 路由对应用程序 UI 的任何更改都在客户端处理。

客户端路由是 **单页面应用程序**(或简称 SPA)一词的来源。 SPA 是仅加载 *一次* 并在用户交互时 *动态* 更新的 Web 应用程序,无需向服务器发出后续请求。 在 SPA 中的路由中,*JavaScript 是动态呈现不同 UI 的驱动力*。

现在我们对客户端路由和 SPA 有了简要的了解,让我们概述一下我们将要进行的操作!

案例研究:口袋妖怪

我们旨在构建的应用程序是一个简单的口袋妖怪应用程序,它根据 URL 路由显示特定口袋妖怪的详细信息。

该应用程序将具有三个唯一的 URL 路由:/charizard/blastoise/venusaur。 根据输入的 URL 路由,将显示不同的口袋妖怪

此外,应用程序底部存在页脚链接,用于在用户单击时将用户定向到每个相应的路由

我们真的需要为此进行路由吗?

对于像这样的简单应用程序,我们并不一定 *需要* 客户端路由器才能使我们的应用程序正常工作。 这个特定应用程序可以由一个简单的父子组件层次结构组成,该层次结构使用 Vue props 来决定应显示的信息。 这是一个展示这一点的 Pen

查看 Pen Vue 口袋妖怪 by Hassan Dj (@itslit) on CodePen.

虽然该应用程序在功能上可以正常工作,但它缺少大多数 Web 应用程序都期望的一项重要功能——*响应浏览器导航事件*。 我们希望我们的口袋妖怪应用程序可以访问,并且可以显示不同路径名的不同详细信息:/charizard/blastoise/venusaur。 这将允许用户刷新不同的页面并保留他们在应用程序中的位置,将 URL 添加到书签以便以后返回,并可能与其他人共享 URL。 这些是创建应用程序中路由的一些主要好处。

现在我们已经了解了我们将要进行的操作,让我们开始构建!

准备应用程序

按照步骤逐步执行的最简单方法(如果您想这样做)是克隆我设置的 GitHub 仓库。

在 GitHub 上下载

克隆后,使用以下命令安装项目依赖项

npm install

让我们简要看一下项目目录。

$ ls
README.md
index.html
node_modules/
package.json
public/
src/
static/
webpack.config.js

项目脚手架中还存在隐藏文件 .babelrc.gitignore

这个项目是一个简单的 webpack 配置的应用程序,使用 vue-cli(Vue 命令行界面)构建。

index.html 是我们声明 DOM 元素(#app)的位置,我们将使用该元素来挂载我们的 Vue 应用程序


  
    
    
    
    
    <script src="/dist/build.js"></script>
  

index.html 文件的 标签中,我们引入了 Bulma 作为应用程序的 CSS 框架,以及我们自己的 styles.css 文件,该文件位于 public/ 文件夹中。

由于我们的重点是使用 Vue.js,因此该应用程序已经具有所有自定义 CSS 布局。

src/ 文件夹是我们直接从其工作的地方

$ ls src/
app/
main.js

src/main.js 代表我们的 Vue 应用程序的起点。 它是实例化 Vue 实例、声明要渲染的父组件以及要挂载应用程序的 DOM 元素 #app 的位置

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

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

我们指定了来自 src/app/app.js 文件的 App 组件作为应用程序的主要父组件。

src/app 目录中,存在另外两个文件——app-custom.jsapp-vue-router.js

$ ls src/app/
app-custom.js
app-vue-router.js
app.js

app-custom.js 表示使用自定义 Vue 路由器完成的应用程序实现(即我们在本文中将要构建的内容)。 app-vue-router.js 是使用 vue-router 库完成的路由实现。

在整篇文章中,我们只会在 src/app/app.js 文件中引入代码。 也就是说,让我们看一下 src/app/app.js 中的起始代码

const CharizardCard = {
  name: 'charizard-card',
  template: `
喷火龙
hp 78
🔥
类型
199 磅
重量
1.7 米
高度
  `
};

const App = {
  name: 'App',
  template: `
  `,
  components: {
    'pokemon-card': CharizardCard
  }
};

export default App;

目前存在两个组件:CharizardCardAppCharizardCard 组件是一个简单的模板,它显示喷火龙口袋妖怪的详细信息。 App 组件在其 components 属性中声明了 CharizardCard 组件,并在其 template 中将其渲染为

我们目前只有静态内容,如果我们运行应用程序,我们将能够看到这些内容

npm run dev

并启动 localhost:8080

为了开始,让我们引入两个新组件:BlastoiseCardVenusaurCard,分别包含水箭龟和妙蛙花口袋妖怪的详细信息。 我们可以将这些组件直接放在 CharizardCard 之后

const CharizardCard = { 
  // ... 
};

const BlastoiseCard = {
  name: 'blastoise-card',
  template: `
水箭龟
hp 79
💧
类型
223 磅
重量
1.6 米
高度
  `
};

const VenusaurCard = {
  name: 'venusaur-card',
  template: `
妙蛙花
hp 80
🍃
类型
220 磅
重量
2.0 米
高度
  `
};

const App = { 
  // ... 
};

export default App;

在建立了应用程序组件之后,我们现在可以开始考虑如何在这两个组件之间创建路由了。

router-view

为了建立路由,我们将从构建一个新组件开始,该组件负责 *根据应用程序的位置渲染指定的组件*。 我们将在名为 View 的常量变量中创建此组件。

在我们创建此组件之前,让我们看看如何使用它。 在 App 组件的 template 中,我们将删除 的声明,而是渲染即将到来的 router-view 组件。 在 components 属性中; 我们将在模板中注册 View 组件常量为

const App = {
  name: 'App',
  template: `
 
  `,
  components: {
    'router-view': View
  }
};

export default App;

router-view 组件将根据 URL 路由匹配正确的口袋妖怪组件。 此匹配将在我们将创建的 routes 数组中确定。 我们将在 App 组件之前创建此数组

const CharizardCard = { 
  // ... 
};
const BlastoiseCard = { 
  // ... 
};
const VenusaurCard = { 
  // ... 
};

const routes = [
  {path: '/', component: CharizardCard},
  {path: '/charizard', component: CharizardCard},
  {path: '/blastoise', component: BlastoiseCard},
  {path: '/venusaur', component: VenusaurCard}
];

const App = { 
  // ... 
};

export default App;

我们将每个口袋妖怪路径设置为其各自的组件(例如 /blastoise 将渲染 BlastoiseCard 组件)。 我们还将根路径 / 设置为 CharizardCard 组件。

现在让我们开始创建 router-view 组件。

router-view 组件本质上是一个 *挂载点*,用于在组件之间动态切换。 我们在 Vue 中可以做到这一点的一种方法是使用保留的 元素来建立 动态组件

让我们为 router-view 创建一个起点,以了解它是如何工作的。 如前所述; 我们将在名为 View 的常量变量中创建 router-view。 也就是说,让我们在路由声明之后设置 View

const CharizardCard = { 
  // ... 
};
const BlastoiseCard = { 
  // ... 
};
const VenusaurCard = { 
  // ... 
};

const routes = [
  // ...
];

const View = {
  name: 'router-view',  
  template: ``,  
  data() {  
    return {  
      currentView: CharizardCard  
    }
  }
}; 

const App = {
// ... 
};

export default App;

保留的 `<template>` 元素将呈现 `is` 属性绑定的任何组件。在上面,我们已将 `is` 属性附加到 `currentView` 数据属性,该属性仅映射到 `CharizardCard` 组件。截至目前,无论 URL 路由是什么,我们的应用程序都类似于起点,显示 `CharizardCard`。

虽然 `router-view` 现在已适当地渲染在 `App` 中,但它目前不是动态的。我们需要 `router-view` 在页面加载时根据 URL 路径名显示正确的组件。为此,我们将使用 `created()` 钩子来过滤 `routes` 数组,并返回具有与 URL 路径匹配的 `path` 的组件。这将使 `View` 看起来像这样

const View = {
  name: 'router-view',  
  template: ``,  
  data() {  
    return {  
      currentView: {}  
    }
  },
  created() {
    this.currentView = routes.find(
      route =&gt; route.path === window.location.pathname
    ).component;
  }
};

在 `data` 函数中,我们现在使用空对象实例化 `currentView`。在 `created()` 钩子中,我们使用 JavaScript 本地 `find()` 方法来返回 `routes` 中第一个与 `route.path === window.location.pathname` 匹配的对象。然后,我们可以使用 `object.component` 获取组件(其中 `object` 是从 `find()` 返回的对象)。

在浏览器环境中,`window.location` 是一个特殊对象,包含浏览器当前位置的属性。我们从该对象中获取 `pathname`,它是 URL 的路径。

在此阶段,我们将能够根据浏览器 URL 的状态查看不同的神奇宝贝卡片组件!

`BlastoiseCard` 组件现在在 `/blastoise` 路由中呈现。

我们还应该考虑其他一些事情。如果输入了随机 URL `pathname`,我们的应用程序目前会出错,并且不会向视图显示任何内容。

为避免这种情况,让我们引入一个简单的检查,如果 URL `pathname` 与 `routes` 数组中存在的任何 `path` 不匹配,则显示“未找到”模板。我们将 `find()` 方法分离到名为 `getRouteObject()` 的组件方法中,以避免重复。这会将 `View` 对象更新为

const View = {
  name: 'router-view',  
  template: ``,  
  data() {  
    return {  
      currentView: {}  
    }
  },
  created() {  
    if (this.getRouteObject() === undefined) {
      this.currentView = {
        template: `

未找到 :(。从下面的列表中选择一只神奇宝贝!

        `
      };
    } else {
      this.currentView = this.getRouteObject().component;
    } 
  },
  methods: {
    getRouteObject() {
      return routes.find(
        route =&gt; route.path === window.location.pathname
      );
    }
  }
};

如果 `getRouteObject()` 方法返回 `undefined`,我们将显示“未找到”模板。如果 `getRouteObject()` 返回 `routes` 中的对象,我们将 `currentView` 绑定到该对象中的组件。现在,如果输入了随机 URL,则会通知用户

如果 URL `pathname` 与 routes 数组中的任何值都不匹配,则会渲染“未找到”视图。

“未找到”模板告诉用户从列表中选择一只神奇宝贝。此列表将是我们创建的链接,允许用户导航到不同的 URL 路由。

太棒了!我们的应用程序现在正在响应一些外部状态,即浏览器的 位置。`router-view` 根据应用程序的位置确定应该显示哪个组件。现在,我们需要构建一些链接,这些链接将更改浏览器的 位置而无需进行网络请求。更新位置后,我们希望重新渲染 Vue 应用程序,并依靠 `router-view` 适当地确定要渲染哪个组件。

我们将这些链接标记为 `router-link` 组件。

`router-link`

在 Web 界面中,我们使用 HTML `<a>` 标签来创建链接。我们在这里想要的是一种特殊的 `<a>` 标签。当用户单击此标签时,我们希望浏览器跳过其获取下一页的默认例程。相反,我们只想手动更新浏览器的 位置。

让我们编写一个 `router-link` 组件,该组件生成一个带有特殊 `click` 绑定的 `<a>` 标签。当用户单击 `router-link` 组件时,我们将使用浏览器的 历史记录 API 来更新浏览器的 位置。

就像我们对 `router-view` 所做的那样,让我们在构建之前看看我们将如何使用此组件。

在 `App` 组件的模板中,让我们在一个父 `<ul>` 元素中创建三个 `<li>` 元素

<code>

`<li>` 元素。我们不会在 `<a>` 中使用 `href` 属性,而是使用 `to` 属性指定链接的所需位置。我们还将在 `App` 的 `components` 属性中注册即将推出的 `router-link` 组件(来自 `Link` 常量变量)

const App = {
  name: 'App',
  template: `
  `,
  components: {
    'router-view': View,
    'router-link': Link
  }
};

我们将在 `App` 组件上方创建表示 `router-link` 的 `Link` 对象。我们已经确定 `router-link` 组件应该始终被赋予一个 `to` 属性(即道具),该属性的值为目标位置。我们可以像这样强制执行此道具验证要求

const CharizardCard = { 
 // ... 
};
const BlastoiseCard = { 
  // ... 
};
const VenusaurCard = { 
  // ... 
};

const routes = [ 
  // ... 
];

const View = { 
  // ... 
};

const Link = {
  name: 'router-link',
  props: {
    to: {
      type: String,
      required: true
    }
  }
};

const App = { 
  // ... 
};

export default App;

我们可以创建 `router-link` 的 `template`,使其包含一个带有 `@click` 处理程序属性的 `<a>` 标签。触发时,`@click` 处理程序将调用一个名为 `navigate()` 的组件方法,该方法将浏览器导航到所需位置。此导航将使用 `history.pushState()` 方法进行。这样说来,`Link` 常量对象将被更新为

const Link = {
  name: 'router-link',
  props: {
    to: {
      type: String,
      required: true
    }
  },
  template: `{{ to }}`,
  methods: {  
    navigate(evt) {  
      evt.preventDefault();  
      window.history.pushState(null, null, this.to);  
    }
  }
};

在 `<a>` 标签中,我们使用 `{{ to }}` 将 `to` 道具的值绑定到元素文本内容。

当 `navigate()` 被触发时,它首先在事件对象上调用 `preventDefault()`,以防止浏览器对新位置进行网络请求。然后调用 `history.pushState()` 方法,将用户定向到所需的路由位置。`history.pushState()` 接受三个参数

  • 一个状态对象,用于传递序列化状态信息
  • 一个标题
  • 目标 URL

在我们的例子中,不需要传递任何状态信息,所以我们将第一个参数保留为 `null`。一些浏览器(例如 Firefox)目前忽略了第二个参数 `title`,因此我们也将其保留为 `null`。

目标位置,即 `to` 道具,被传递到第三个也是最后一个参数中。由于 `to` 道具以相对状态包含目标位置,因此它将在相对于当前 URL 的位置解析。在我们的例子中,`/blastoise` 将解析为 `http://localhost:8080/blastoise`。

如果我们现在单击任何链接,我们会注意到我们的浏览器更新到正确的位置,但不会完全重新加载页面。但是,我们的应用程序不会更新并呈现正确的组件。

这种意外的行为发生是因为当 `router-link` 更新浏览器的 位置时,我们的 Vue 应用程序不会收到更改通知。我们需要在每次位置更改时触发应用程序(或只是 `router-view` 组件)进行重新渲染。

虽然可以通过几种方法实现这种行为,但我们将使用自定义的 `EventBus` 来完成此操作。`EventBus` 是一个 Vue 实例,负责允许隔离的组件相互之间订阅和发布 自定义事件

在文件的开头,我们将 `import` `vue` 库,并使用新的 `Vue()` 实例创建一个 `EventBus`

import Vue from 'vue';

const EventBus = new Vue();

当单击链接时,我们需要通知应用程序的必要部分(即 `router-view`),用户正在导航到特定路由。第一步是在 `router-link` 的 `navigate()` 方法中使用 `EventBus` 的事件接口创建一个事件发射器。我们将为这个自定义事件命名为 `navigate`

const Link = {
  // ...,
  methods: {
    navigate(evt) {
      evt.preventDefault();
      window.history.pushState(null, null, this.to);
      EventBus.$emit('navigate'); 
    }
  }
};

我们现在可以在 `router-view` 的 `created()` 钩子中设置事件侦听器/触发器。通过在 `if/else` 语句之外设置自定义事件侦听器,`View` 的 `created()` 钩子将被更新为

const View = {
  // ...,
  created() {  
    if (this.getRouteObject() === undefined) {
      this.currentView = {
        template: `

未找到 :(。从下面的列表中选择一只神奇宝贝!

        `
      };
    } else {
      this.currentView = this.getRouteObject().component;
    }

    // Event listener for link navigation
    EventBus.$on('navigate', () =&gt; {
      this.currentView = this.getRouteObject().component;
    });
  },
  // ...
};

当浏览器的位置通过单击 `<a>` 元素发生变化时,此侦听函数将被调用,重新渲染 `router-view` 以匹配最新的 URL!

太棒了!我们的应用程序现在在我们单击每个链接时都可以适当地导航。

我们还需要考虑最后一件事。如果我们尝试使用浏览器后退/前进按钮浏览浏览器历史记录,我们的应用程序目前不会正确重新渲染。虽然很意外,但这是因为没有事件通知器在用户单击 `浏览器后退` 或 `浏览器前进` 时发出。

为了使其正常工作,我们将使用 `onpopstate` 事件处理程序。

每次活动历史记录条目发生更改时,都会触发 `onpopstate` 事件。历史记录更改是通过单击 `浏览器后退` 或 `浏览器前进` 按钮,或者通过以编程方式调用 `history.back()` 或 `history.forward()` 来调用的。

在创建 `EventBus` 之后,让我们设置 `onpopstate` 事件侦听器,以便在调用历史记录更改时发出 navigate 事件

window.addEventListener('popstate', () =&gt; {  
  EventBus.$emit('navigate');  
});

我们的应用程序现在即使使用浏览器导航按钮也能适当地响应!

我们就这样完成了!我们刚刚使用 `EventBus` 和动态组件构建了一个自定义 Vue 路由器。即使我们的应用程序很小,我们也可以享受显着的性能提升。避免完全重新加载页面还可以节省数百毫秒,并防止我们的应用程序在页面更改期间“闪烁”。

结论

我爱 Vue。原因之一是 – 使用和操作 Vue 组件非常简单,就像我们在本文中看到的那样。

在介绍中,我们提到了 Vue 如何提供 `vue-router` 库作为框架的官方路由库。我们刚刚创建了 `vue-router` 中使用的相同主要项目的简单版本

  • `routes`:负责将组件映射到相应 URL 路径名的数组。
  • `router-view`:根据应用程序的位置渲染指定应用程序组件的组件
  • `router-link`:允许用户更改浏览器的 位置而无需进行网络请求的组件。

对于非常简单的应用程序,我们构建的路由(或其变体 比如这个Chris Fritz 构建)可以完成路由应用程序所需的最小工作量。

另一方面,vue-router 库的构建方式更加复杂,并引入了许多非常有用的功能,这些功能在大型应用程序中经常需要,例如

虽然 vue-router 库确实附带了额外的样板代码,但一旦你的应用程序由良好隔离且独立的组件组成,它就很容易集成。如果你有兴趣,你可以看到 vue-router 的组件用于在这个应用程序中启用路由 这里

希望这篇博文对你来说和对我编撰它一样愉快!感谢阅读!


这篇文章改编自(并总结了)我正在与 Fullstack.io 团队合作的即将出版的书籍《全栈 Vue》中的一个部分!有机会与 Fullstack 的伙伴们合作真是太棒了。按照 Fullstack 的风格,这本书涵盖了 Vue 的众多方面,包括但不限于路由、简单的状态管理、表单处理、Vuex、服务器持久化和测试。如果这引起了你的兴趣,或者你有任何问题,请关注(或给我发消息)我的推特 (@djirdehh)!如果以上内容没有引起你的兴趣,你仍然可以关注我。😛