许多教程 存在,这些教程很好地解释了如何将 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 仓库。
克隆后,使用以下命令安装项目依赖项
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 => h(App)
});
我们指定了来自 src/app/app.js
文件的 App
组件作为应用程序的主要父组件。
在 src/app
目录中,存在另外两个文件——app-custom.js
和 app-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: `

类型
重量
高度
`
};
const App = {
name: 'App',
template: `
`,
components: {
'pokemon-card': CharizardCard
}
};
export default App;
目前存在两个组件:CharizardCard
和 App
。 CharizardCard
组件是一个简单的模板,它显示喷火龙口袋妖怪的详细信息。 App
组件在其 components
属性中声明了 CharizardCard
组件,并在其 template
中将其渲染为 。
我们目前只有静态内容,如果我们运行应用程序,我们将能够看到这些内容
npm run dev
并启动 localhost:8080

为了开始,让我们引入两个新组件:BlastoiseCard
和 VenusaurCard
,分别包含水箭龟和妙蛙花口袋妖怪的详细信息。 我们可以将这些组件直接放在 CharizardCard
之后
const CharizardCard = {
// ...
};
const BlastoiseCard = {
name: 'blastoise-card',
template: `

类型
重量
高度
`
};
const VenusaurCard = {
name: 'venusaur-card',
template: `

类型
重量
高度
`
};
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 => 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 的状态查看不同的神奇宝贝卡片组件!

我们还应该考虑其他一些事情。如果输入了随机 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 => route.path === window.location.pathname
);
}
}
};
如果 `getRouteObject()` 方法返回 `undefined`,我们将显示“未找到”模板。如果 `getRouteObject()` 返回 `routes` 中的对象,我们将 `currentView` 绑定到该对象中的组件。现在,如果输入了随机 URL,则会通知用户

“未找到”模板告诉用户从列表中选择一只神奇宝贝。此列表将是我们创建的链接,允许用户导航到不同的 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', () => {
this.currentView = this.getRouteObject().component;
});
},
// ...
};
当浏览器的位置通过单击 `<a>` 元素发生变化时,此侦听函数将被调用,重新渲染 `router-view` 以匹配最新的 URL!

太棒了!我们的应用程序现在在我们单击每个链接时都可以适当地导航。
我们还需要考虑最后一件事。如果我们尝试使用浏览器后退/前进按钮浏览浏览器历史记录,我们的应用程序目前不会正确重新渲染。虽然很意外,但这是因为没有事件通知器在用户单击 `浏览器后退` 或 `浏览器前进` 时发出。
为了使其正常工作,我们将使用 `onpopstate` 事件处理程序。
每次活动历史记录条目发生更改时,都会触发 `onpopstate` 事件。历史记录更改是通过单击 `浏览器后退` 或 `浏览器前进` 按钮,或者通过以编程方式调用 `history.back()` 或 `history.forward()` 来调用的。
在创建 `EventBus` 之后,让我们设置 `onpopstate` 事件侦听器,以便在调用历史记录更改时发出 navigate 事件
window.addEventListener('popstate', () => {
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)!如果以上内容没有引起你的兴趣,你仍然可以关注我。😛
感谢你精彩的写作。我唯一的问题是关于这本书的发布。我快速搜索了一下,但找不到更多信息。你们有针对它的发布日期吗?我还很新 Vue,根据其他 Fullstack 的书(以及这篇文章),我相信它将是一个很好的学习资源。
谢谢丹!这本书的目标是在 3 月份的第二周发布(希望如此 :P)。
我已发送给你一封电子邮件,其中包含更多详细信息 :)。
与其为每个宝可梦构建一个单独的组件,不如将它们的数据存储在一个数组中,并使用一个宝可梦组件。另外,使用动态路由匹配将路由与其数据数组中的数据对象进行匹配。
嘿,杜桑!
是的,你说得没错。对于我展示的例子来说,使用一个单独的组件更明显,因为它们都共享相同的标记/布局。我故意构建了单独的组件,因为我想展示如何将不相关的组件(可能具有它们自己的数据()、方法等)也以这种方式进行路由 :)。
我喜欢你使用宝可梦作为你的例子,而不是一些无聊的东西!
谢谢!是的,我总是倾向于这样做。找到有趣的用例来展示例子 :)。
我认为 MVC 没有任何实际意义(我的个人观点)。我的目标是遵循标准,即 REST。如果你有一个数据库驱动的页面,使用查询参数,例如
mysite.com?color=green
,客户端 JS 可以处理 XHR,同时保持 RESTful 规范——包括查询参数在内的 URL 将始终如一地呈现。你的用户状态通过会话/本地存储来处理。
我正在尝试做这个,并且成功了,因为你提供的自定义 Vue 路由的想法很棒,而且很简单,所以感谢你提供这个独特的想法,请继续发布。