Web前端之家今天介绍下Vue Router的基本知识,一起探讨Vue Router的神奇之旅。我们将研究如何使用Vue Router在Vue应用程序中实现路由。所以我们可以动手实践,我们将使用 Vue 和 Vue Router 构建一个简单的 Pokedex 应用程序。
具体来说,我们将涵盖以下内容:
-
设置路由器
-
路由参数
-
声明式和程序化导航
-
嵌套路由
-
404页
每个允许创建单页应用程序的 JavaScript UI 框架都需要一种将用户从一个页面导航到另一个页面的方法。所有这一切都需要在客户端通过将页面上当前显示的视图与地址栏中的 URL 同步来进行管理。在 Vue 世界中,管理此类任务的【官方库】是 Vue Router。
先决条件 {#prerequisites}
以下是必需的,以便您可以充分利用本教程:
-
HTML、CSS、JavaScript 和 Vue 的基本知识。如果您知道如何使用 Vue 在页面上渲染某些内容,那么您应该能够跟上。对 API 有一点了解也会有所帮助。
应用概览 {#appoverview}
我们将构建一个 Pokedex 应用程序。它将包含三个页面:
口袋妖怪列表页面。这是列出所有原始 151 Pokemon 的默认页面。
口袋妖怪页面。这是我们显示基本详细信息的地方,例如类型和描述。
口袋妖怪详情页。这是我们展示进化链、能力和动作的地方。
设置应用程序 {#settinguptheapp}
vue create poke-vue-router
从列出的选项中选择 Vue 3:
完成后,在项目文件夹中导航并安装我们需要的库:
cd poke-vue-router
npm install vue-router@4 axios
请注意,我们使用的是 Vue Router 4 而不是 3,这是您在 Google 上搜索时显示的默认结果。它next.router.vuejs.org
与router.vuejs.org
. 我们正在使用 Axios 向PokeAPI v2发出请求。
此时,最好运行项目以确保默认的 Vue 应用程序正常工作:
npm run serve
访问http://localhost:8080/
您的浏览器并检查默认 Vue 应用程序是否正在运行。它应该显示如下内容:
接下来,您需要添加sass-loader
为开发依赖项。就本教程而言,最好只安装我使用的相同版本。这是因为在撰写本文时,最新版本与 Vue 3 不兼容:
npm install sass-loader@10.1.1 --save-dev
node-sass
出于与上述相同的原因,您还需要安装。最好坚持使用与我相同的版本:
npm install node-sass@4.14.1 --save
注意:如果以这种方式安装 Sass 对您不起作用,您也可以在使用 CLI 创建 Vue 应用程序时选择手动选择功能。然后,选择CSS Preprocessors并选择Sass/SCSS (with dart-sass)。
创建应用程序 {#creatingtheapp}
现在我们准备开始构建应用程序。在您继续操作时,请记住根目录是src
文件夹。
从更新main.js
文件开始。这是我们导入根组件App.vue
和router/index.js
声明所有与路由相关的文件的地方:
// main.js
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
const app = createApp(App);
app.use(router);
app.mount("#app");
设置路由器 {#settinguparouter}
在App.vue
文件中,使用router-view
Vue Router提供的组件。这是 Vue Router 使用的最顶层组件,它为用户访问的当前路径呈现相应的组件:
// App.vue
<template>
<div id="app">
<router-view />
</div>
</template>
<script>
export default {
name: "App",
};
</script>
接下来,创建一个新router/index.js
文件并添加以下内容。要创建路由器,我们需要从 Vue Router 中提取createRouter
和createWebHistory
。createRouter
允许我们创建一个新的路由器实例,同时createWebHistory
创建一个 HTML5 历史记录,它基本上是History API的包装器。当我们在页面之间导航时,它允许 Vue Router 操作地址栏中的地址:
// router/index.js
import { createRouter, createWebHistory } from "vue-router";
在此之下,导入我们将使用的所有页面:
import PokemonList from "../views/PokemonList.vue";
Vue的路由器需要含有对象的数组的path
,name
和component
作为其属性:
-
path
:这是您要匹配的模式。在下面的代码中,我们匹配根路径。因此,如果用户尝试访问http://localhost:8000
,则匹配此模式。 -
name
: 页面名称。这是页面的唯一标识符,当您想从其他页面导航到此页面时,将使用此标识符。 -
component
:当path
与用户访问的 URL 匹配时要呈现的组件。
const routes = [
{
path: "/",
name: "PokemonList",
component: PokemonList,
},
];
最后,通过提供一个包含history
和routes
to的对象来创建路由器实例createRouter
:
const router = createRouter({
history: createWebHistory(),
routes,
});
export default router;
这就是我们现在所需要的。您可能想知道其他页面在哪里。我们稍后会添加它们。现在,让我们先处理默认页面。
创建页面 {#creatingapage}
创建页面实际上并不需要任何特殊代码。所以如果你知道如何在 Vue 中创建自定义组件,你应该能够创建一个页面供 Vue Router 使用。
创建一个views/PokemonList.vue
文件并添加下面的代码。在此文件中,我们使用自定义List
组件来呈现 Pokemon 列表。我们真正需要做的唯一一件事就是为List
组件提供要使用的数据。一旦组件被挂载,我们就会向 PokeAPI 发出请求。我们不希望列表变得太大,因此我们将结果限制为原始的 151 Pokemon。一旦我们得到结果,我们只需将它分配给组件的items
数据。这将依次更新List
组件:
<template>
<List :items="items" />
</template>
<script>
import axios from "axios";
import List from "../components/List.vue";
export default {
name: "PokemonList",
data() {
return {
items: null,
};
},
mounted() {
axios.get(`https://pokeapi.co/api/v2/pokemon?limit=151`).then((res) => {
if (res.data && res.data.results) {
this.items = res.data.results;
}
});
},
components: {
List,
},
};
</script>
这是List
组件的代码。组件存储在components
目录中,因此创建一个components/List.vue
文件并添加以下内容:
<template>
<div v-if="items">
<router-link
:to="{ name: 'Pokemon', params: { name: row.name } }"
class="link"
v-for="row in items"
:key="row.name"
>
<div class="list-item">
{{ row.name }}
</div>
</router-link>
</div></template><script>export default {
name: "List",
props: {
items: {
type: Array,
},
},};
</script>
<style lang="scss" scoped>@import "../styles/list.scss";</style>
您可以styles/list.scss
在GitHub repo【https://github.com/sitepoint-editors/pokedex-vue-router/blob/master/src/styles/list.scss】 中查看该文件的代码。
此时,您现在可以在浏览器中查看更改。除非您收到以下错误:
这是因为 Vue 正在尝试生成 Pokemon 页面的链接,但还没有。Vue CLI 足够聪明,可以警告你这一点。您可以通过使用 a<div>
代替components/List.vue
文件模板来临时解决此问题:
<template>
<div v-if="items">
<div v-for="row in items" :key="row.name">{{ row.name }}</div>
</div>
</template>
有了这个,你应该能够看到口袋妖怪的列表。一旦我们添加了 Pokemon 页面,请记住稍后将其更改回来。
声明式导航 {#declarativenavigation}
使用 Vue Router,您可以通过两种方式导航:声明式和编程式。声明式导航与我们在 HTML 中使用锚标记所做的几乎相同。您只需声明您希望链接导航到的位置。另一方面,编程导航是通过在执行用户操作(例如单击按钮按钮)时显式调用 Vue Router 以导航到特定页面来完成的。
让我们快速分解一下这是如何工作的。要导航,您需要使用该router-link
组件。这需要的唯一属性是:to
。这是一个包含name
要导航到的页面的对象,以及一个params
用于指定要传递给页面的参数的可选对象。在这种情况下,我们传入 Pokemon 的名称:
<router-link
:to="{ name: 'Pokemon', params: { name: row.name } }"
class="link"
v-for="row in items"
:key="row.name"
>
<div class="list-item">
{{ row.name }}
</div>
</router-link>
要可视化这是如何工作的,您需要知道Pokemon
屏幕使用的模式。这里是什么样子:/pokemon/:name
。:name
表示name
您传入的参数。例如,如果用户想查看皮卡丘,则 URL 将如下所示http://localhost:8000/pokemon/pikachu
。我们很快就会更详细地回到这一点。
路线参数 {#routeparameters}
我们已经看到了如何为我们的路由匹配特定的模式,但我们还没有了解如何传入自定义参数。我们已经通过router-link
前面的例子简要地看到了它。
我们将使用下一页 ( Pokemon
) 来说明路由参数在 Vue Router 中是如何工作的。为此,您需要做的就是在参数名称前加上冒号 ( :
)。在下面的例子中,我们想传入 Pokemon 的名字,所以我们添加了:name
. 这意味着如果我们想导航到这个特定的路线,我们需要为这个参数传入一个值。正如我们在router-link
前面的示例中看到的,这是我们传递 Pokemon 名称的地方:
// router/index.js
import PokemonList from "../views/PokemonList.vue";
import Pokemon from "../views/Pokemon"; // add this
const routes = [
{
path: "/",
name: "PokemonList",
component: PokemonList,
},
// add this:
{
path: "/pokemon/:name",
name: "Pokemon",
component: Pokemon,
}
]
这是Pokemon
页面 ( views/Pokemon.vue
)的代码。就像之前的 PokemonList 页面一样,我们将渲染 UI 的任务委托给一个单独的组件BasicDetails
。当组件被挂载时,我们向 API 的/pokemon
端点发出请求。要获取作为路由参数传入的 Pokemon 名称,我们使用this.$route.params.name
. 我们正在访问的属性应该与您为router/index.js
文件中的参数提供的名称相同。在这种情况下,它是name
. 如果您用于代替,则可以使用/pokemon/:pokemon_name
以下命令path
访问它this.$route.params.pokemon_name
:
<template>
<BasicDetails :pokemon="pokemon" />
</template>
<script>
import axios from "axios";
import BasicDetails from "../components/BasicDetails.vue";
export default {
name: "Pokemon",
data() {
return {
pokemon: null,
};
},
mounted() {
const pokemon_name = this.$route.params.name;
axios
.get(`https://pokeapi.co/api/v2/pokemon/${pokemon_name}`)
.then((res) => {
const data = res.data;
axios
.get(`https://pokeapi.co/api/v2/pokemon-species/${pokemon_name}`)
.then((res) => {
Object.assign(data, {
description: res.data.flavor_text_entries[0].flavor_text,
specie_id: res.data.evolution_chain.url.split("/")[6],
});
this.pokemon = data;
});
});
},
components: {
BasicDetails,
},
};
</script>
这是BasicDetails
组件 ( components/BasicDetails.vue
)的代码:
<template>
<div v-if="pokemon">
<img :src="pokemon.sprites.front_default" :alt="pokemon.name" />
<h1>{{ pokemon.name }}</h1>
<div class="types">
<div
class="type-box"
v-for="row in pokemon.types"
:key="row.slot"
v-bind:class="row.type.name.toLowerCase()"
>
{{ row.type.name }}
</div>
</div>
<div class="description">
{{ pokemon.description }}
</div>
<a @click="moreDetails" class="link">More Details</a>
</div></template><script>export default {
name: "BasicDetails",
props: {
pokemon: {
type: Object,
},
},
methods: {
moreDetails() {
this.$router.push({
name: "PokemonDetails",
params: {
name: this.pokemon.name,
specie_id: this.pokemon.specie_id,
},
});
},
},};</script>
<style lang="scss" scoped>@import "../styles/types.scss";@import "../styles/pokemon.scss";</style>
您可以在GitHub 存储库【https://github.com/sitepoint-editors/pokedex-vue-router/blob/master/src/styles】中查看styles/types.scss
和styles/pokemon.scss
文件的代码。
此时,您应该能够再次在浏览器中看到更改。您还可以将components/List.vue
文件更新回其原始代码,router-link
而不是<div>
.
程序化导航 {#programmaticnavigation}
您可能已经注意到我们在BasicDetails
组件中做了一些不同的事情。我们并没有真正使用 导航到PokemonDetails
页面router-link
。相反,我们使用了一个锚元素并拦截了它的点击事件。这就是程序化导航的实现方式。我们可以通过 访问路由器this.$router
。然后我们调用push()
方法在历史堆栈的顶部推送一个新页面。路由器将显示顶部的任何页面。当用户单击浏览器的后退按钮时,此方法允许导航回上一页,因为单击它只是将当前页面"弹出"到历史堆栈的顶部。此方法接受一个包含name
和params
属性的对象,因此它与您传递给to
财产在router-link
:
methods: {
moreDetails() {
this.$router.push({
name: "PokemonDetails",
params: {
name: this.pokemon.name,
specie_id: this.pokemon.specie_id,
},
});
},
},
嵌套路由 {#nestedroutes}
接下来,更新路由器文件以包含 Pokemon 详细信息页面的路径。在这里,我们使用嵌套路由传入多个自定义参数。在这种情况下,我们传入name
and specie_id
:
import Pokemon from "../views/Pokemon";
import PokemonDetails from "../views/PokemonDetails"; // add this
const routes = [
// ..
{
path: "/pokemon/:name",
// ..
},
// add these
{
path: "/pokemon/:name/:specie_id/details",
name: "PokemonDetails",
component: PokemonDetails,
},
];
这是PokemonDetails
页面 ( views/PokemonDetails.vue
)的代码:
<template>
<MoreDetails :pokemon="pokemon" />
</template>
<script>
import axios from "axios";
import MoreDetails from "../components/MoreDetails.vue";
export default {
name: "PokemonDetails",
data() {
return {
pokemon: null,
};
},
mounted() {
const pokemon_name = this.$route.params.name;
axios
.get(`https://pokeapi.co/api/v2/pokemon/${pokemon_name}`)
.then((res) => {
const data = res.data;
axios.get(`https://pokeapi.co/api/v2/evolution-chain/${this.$route.params.specie_id}`)
.then((res) => {
let evolution_chain = [res.data.chain.species.name];
if (res.data.chain.evolves_to.length > 0) {
evolution_chain.push(
res.data.chain.evolves_to[0].species.name
);
if (res.data.chain.evolves_to.length > 1) {
const evolutions = res.data.chain.evolves_to.map((item) => {
return item.species.name;
}
);
evolution_chain[1] = evolutions.join(" | ");
}
if (
res.data.chain.evolves_to[0].evolves_to.length >
0
) {
evolution_chain.push(res.data.chain.evolves_to[0].evolves_to[0].species.name);
}
Object.assign(data, {
evolution_chain,
});
}
this.pokemon = data;
});
});
},
components: {
MoreDetails,
},
};
</script>
这是MoreDetails
组件 ( components/MoreDetails.vue
)的代码:
<template>
<div v-if="pokemon">
<h1>{{ pokemon.name }}</h1>
<div v-if="pokemon.evolution_chain" class="section">
<h2>Evolution Chain</h2>
<span v-for="(name, index) in pokemon.evolution_chain" :key="name">
<span v-if="index">-></span>
{{ name }}
</span>
</div>
<div v-if="pokemon.abilities" class="section">
<h2>Abilities</h2>
<div v-for="row in pokemon.abilities" :key="row.ability.name">
{{ row.ability.name }}
</div>
</div>
<div v-if="pokemon.moves" class="section">
<h2>Moves</h2>
<div v-for="row in pokemon.moves" :key="row.move.name">
{{ row.move.name }}
</div>
</div>
</div>
</template>
<script>
export default {
name: "MoreDetails",
props: {
pokemon: {
type: Object,
},
},
};
</script>
<style lang="scss" scoped>
@import "../styles/more-details.scss";
</style>
此时,您可以单击任何 Pokemon 名称并查看单个 Pokemon 的详细信息。您可能需要重新启动服务器才能看到更改。
404页面 {#404page}
我们已经为所有页面添加了代码。但是如果用户在浏览器的地址栏中输入无效的 URL 会发生什么?在这些情况下,它只会出错或根本不显示任何内容。我们需要添加一种拦截这些请求的方法,以便我们可以显示"404 not found"页面。
为此,请打开路由器文件并导入NotFound
页面:
import NotFound from "../views/NotFound";
路由根据它们在路由数组中添加的顺序进行优先级排序。这意味着首先添加的那些是第一个与用户在地址栏上输入的 URL 匹配的。所以 404 页面的模式必须最后添加。
在routes
数组中,添加以下内容:
const routes = [
const routes = [
// ..
{
path: "/pokemon/:name/:specie_id/details",
// ..
},
// add this
{
path: "/:pathMatch(.*)*",
name: "NotFound",
component: NotFound,
},
];
path
看着是不是很眼熟?我们使用一个自定义参数pathMatch
来匹配输入的任何 URL。因此,如果用户输入http://localhost:8000/hey
或http://localhost:8000/hey/jude
,它将呈现NotFound
页面。
这一切都很好。但是,如果包罗万有模式之上的模式实际上是匹配的,会发生什么呢?例如:
-
http://localhost:8000/pokemon/someinvalidpokemon
-
http://localhost:8000/pokemon/someinvalidpokemon/99999/details
在这些情况下,包罗万象的模式将不匹配,因此我们需要一种拦截此类请求的方法。
这类请求的主要问题是用户假设某个 Pokemon 或物种 ID 存在,但事实并非如此。检查的唯一方法是拥有有效 Pokemon 的列表。在您的路由文件中,导入有效 Pokemon 的列表:
import NotFound from "../views/NotFound";
import valid_pokemon from "../data/valid-pokemon.json"; // add this
您可以在GitHub【https://github.com/sitepoint-editors/pokedex-vue-router/blob/master/src/data/valid-pokemon.json】存储库上找到此文件。
为了拦截这些类型的请求,Vue Router 提供了导航守卫。将它们视为导航过程的"挂钩",允许您在 Vue Router 导航到某个页面之前或之后执行某些操作。我们将只经历导航完成之前执行的那个,因为这允许我们在导航到该页面的条件不匹配时重定向到另一个页面。
为了在导航完成之前挂钩当前请求,我们调用实例beforeEach()
上的方法router
:
const router = createRouter({
// ..
});
router.beforeEach(async (to) => {
// next: add the condition for navigating to the 404 page
});
Vue Router 传递两个参数给它:
-
to
: 目标路线位置 -
from
:当前路线位置
每一个都包含这些属性。我们感兴趣的是params,因为它包含用户在 URL 中传递的任何参数。
这是我们的情况。我们首先检查我们要检查的参数是否存在。如果是,我们继续检查它是否有效。Pokemon
页面的第一个条件匹配。我们使用之前的valid_pokemon
数组。我们将它与to.params.name
包含用户传递的 Pokemon 名称的进行比较。另一方面,PokemonDetails
页面的第二个条件匹配。在这里,我们正在检查物种 ID。因为我们只想匹配原始的 101 Pokemon,任何大于这个的 ID 都被认为是无效的。如果它符合这些条件中的任何一个,我们只需返回 404 页面的路径。如果条件不匹配,它将导航到它最初打算导航到的位置:
if (
to.params &&
to.params.name &&
valid_pokemon.indexOf(to.params.name) === -1
) {
return "/404";
}
if (
(to.params &&
to.params.name &&
to.params.specie_id &&
valid_pokemon.indexOf(to.params.name) === -1 &&
to.params.specie_id < 0) ||
to.params.specie_id > 101
) {
return "/404";
}
这是 404 页面 ( views/NotFound.vue
)的代码:
<template>
<h1>404 Not Found</h1>
</template>
<script>
export default {
name: "Not Found",
};
</script>
<style lang="scss" scoped>
@import "../styles/notfound.scss";
</style>
您可以styles/notfound.scss
在GitHub【https://github.com/sitepoint-editors/pokedex-vue-router/tree/master/src/styles/notfound.scss】存储库上查看该文件的代码。
至此,应用程序完成!您可以尝试访问无效页面,它会返回 404 页面。
结论 {#conclusion}
而已!在本教程中,您学习了使用 Vue Router 的基础知识。设置路由器、传递自定义参数、在页面之间导航以及实现 404 页面等事情将为您带来很长的路要走。如果您想了解从这里开始的方向,我建议您探索以下主题:
-
将道具传递给路由组件:允许您将视图组件与路由参数分离。这提供了一种将路由参数与可以从组件访问的道具交换的方法。这样你就可以在任何没有
$route.params
. -
Transitions:用于动画页面之间的过渡。
-
延迟加载:这更多是一种性能改进,因此打包器不会为单个文件中的所有页面编译代码。相反,它会延迟加载,以便浏览器仅在需要时才下载特定页面的代码。