大量的教程在解释Vue的官方路由库vue-router如何集成到现有的Vue应用中做了很好的工作。 vue-router通过向我们提供将应用的组件映射到不同的浏览器URL路由所需的功能,做了出色的工作。
简单的应用通常不需要完全成熟的路由库,如vue-router。 在本文中,我们将使用Vue构建一个简单的自定义客户端路由器。 通过这样做,我们将了解需要处理什么来构建客户端路由以及潜在的缺点。
虽然本文假设了Vue.js的基本知识; 在我们开始编写代码时,我们将一步步来解释!
Routing
首先也是最重要的:我们为那些可能对这个概念不熟悉的人解释一下Routing。
在Web开发中,路由通常是指根据从浏览器URL派生的规则来分割应用程序的UI。 想象一下,点击一个链接并让网址从https://website.com转到https://website.com/article/。 这是路由。
路由通常分为两个部分:
1.服务器端路由
客户端(即浏览器)在每次URL更改时向服务器发出请求。
2.客户端路由
客户端仅在首页加载时向服务器发出请求。 然后在客户端上处理基于URL路由的应用程序UI的任何更改。
客户端路由是术语单页应用程序(简称SPA)产生的概念。SPA是Web应用,它只加载一次,并通过用户交互动态更新,而无需向服务器发出后续请求。 通过在SPA中进行路由,JavaScript动态呈现不同的UI。
现在我们对客户端路由和SPA进行了简要的了解,让我们来概述一下我们将要开展的工作!
案例分析:神奇宝贝
我们打算构建的应用是一个简单的Pokémon应用程序,基于URL路线显示特定神奇宝贝的详细信息。
我们甚至需要路由吗?
对于这样的简单应用,我们不一定需要客户端路由器才能使我们的应用正常工作。 这个特定的应用可以由一个简单的父子组件层次结构组成,该层次结构使用Vue支持来指示应该显示的信息。 这里简单写一下:
//HTML代码
<div id="app" class="container">
<div class="container">
<div class="columns is-mobile">
<div class="pokemon column">
<pokemon-card :pokemon="pokemon"></pokemon-card>
<div class="pokemon-links">
<a @click=setPokemon('charizard') :class="{ active: pokemon === 'charizard' }">Charizard</a>
<a @click=setPokemon('blastoise') :class="{ active: pokemon === 'blastoise' }">Blastoise</a>
<a @click=setPokemon('venusaur') :class="{ active: pokemon === 'venusaur' }">Venusaur</a>
</div>
</div>
</div>
</div>
</div>
复制代码
//sass代码
@import url('https://fonts.googleapis.com/css?family=Cinzel+Decorative:400,700|Nunito:600');
html, body {
height: 100%;
padding-top: 10px;
background: linear-gradient(to bottom right,#024,#402);
}
#app {
height: 100%;
padding-top: 0px;
font-family: Cinzel Decorative, sans-serif;
}
.container, .columns {
height: 100%;
}
.pokemon {
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.pokemon .card {
border-radius: 20px;
border: 1px solid #ffdd56;
margin-bottom: 2.5rem;
background: none;
}
.pokemon .card--charizard {
border-color: #ffdd56;
.card-image-container {
position: absolute;
width: 290px;
top: -85px;
}
.card-content .main .hp::before {
background: linear-gradient(to right, #a86e3c, #f5ae67);
}
.card-content .stats .tag {
background-color: #ffdd57;
}
}
.pokemon .card--blastoise {
border-color: #72d0fb;
.card-image-container {
position: absolute;
width: 200px;
top: -10px;
left: 40px;
}
.card-content .main .hp::before {
background: linear-gradient(to right, #c3fcff, #00a5f8);
}
.card-content .stats .tag {
background-color: azure;
}
}
.pokemon .card--venusaur {
border-color: #ff3860;
.card-image-container {
position: absolute;
width: 290px;
top: -10px;
left: -6px;
}
.card-content .main .hp::before {
background: linear-gradient(to right, #92df00, #4ea13f);
}
.card-content .stats .tag {
background-color: #ff3860;
color: #fff;
}
}
.pokemon .card .card-image {
position: relative;
display: block;
height: 185px;
}
.pokemon .card-content .main {
padding-bottom: 10px
}
.pokemon .card-content .title {
font-family: Cinzel Decorative, sans-serif;
font-size: 25px;
margin-bottom: 1rem;
letter-spacing: 4px;
}
.pokemon .card-content .stats {
font-size: 15px;
}
.pokemon .card-content .stats .tag {
font-size: 10px;
border-radius: 10px;
}
.pokemon .card-content .stats .column {
width: 75px;
}
.pokemon .card-content .stats .center-column {
min-width: 100px;
border-left: 1px solid #ccc;
border-right: 1px solid #ccc;
}
.pokemon .hp {
position: relative;
font-size: 15px;
}
.pokemon .hp::before {
position: absolute;
top: -8px;
left: 50%;
width: 50%;
height: 5px;
border-radius: 3px;
content: ' ';
transform: translateX(-50%);
}
.pokemon-links a {
letter-spacing: 1px;
color: #68c8b7;
margin: 0 20px;
}
.pokemon-links a.active {
color: #FFF;
font-weight: 600;
}
// For thumbnail preview; hack :P
@media(max-width: 758px) and (max-height: 500px) {
.pokemon .card {
margin-bottom: 1.5rem;
}
.pokemon .card-content {
padding: 1.0rem;
}
}
复制代码
//vue代码
const pokemonData = {
"charizard": {
name: "Charizard",
imageTag: "6-Charizard.png",
hp: 78,
type: '?',
weight: 199,
height: 1.7
},
"blastoise": {
name: "Blastoise",
imageTag: "9-Blastoise.png",
hp: 79,
type: '?',
weight: 223,
height: 1.6
},
"venusaur": {
name: "Venusaur",
imageTag: "8003-Mega-Venusaur.png",
hp: 80,
type: '?',
weight: 220,
height: 2.0
}
}
const PokemonCard = {
template: `
<div class="card has-text-weight-bold has-text-white" :class="['card--' + pokemon]">
<div class="card-image">
<div class="card-image-container">
<img :src="'http://static.pokemonpets.com/images/monsters-images-800-800/' + getPokemon.imageTag"/>
</div>
</div>
<div class="card-content has-text-centered">
<div class="main">
<div class="title has-text-white">{{ getPokemon.name }}</div>
<div class="hp">hp {{ getPokemon.hp }}</div>
</div>
<div class="stats columns is-mobile">
<div class="column has-text-centered">{{ getPokemon.type }}<br><span class="tag">Type</span></div>
<div class="column has-text-centered center-column">{{ getPokemon.weight }} lbs<br><span class="tag">Weight</span></div>
<div class="column has-text-centered">{{ getPokemon.height }} m <br><span class="tag">Height</span></div>
</div>
</div>
</div>
`,
props: ['pokemon'],
computed: {
getPokemon() {
return pokemonData[this.pokemon];
}
}
}
new Vue({
el: '#app',
data: {
pokemon: 'charizard'
},
methods: {
setPokemon(pokemon) {
this.pokemon = pokemon;
}
},
components: {
'pokemon-card': PokemonCard
}
})
复制代码
Result:
现在我们已经了解了我们将要开展的工作,让我们开始构建吧!
准备应用程序
一步一步遵循的最简单的方法(如果你愿意这样做)是克隆我设置的GitHub仓库。
克隆时,通过以下方式安装项目依赖关系:
npm install
复制代码
我们来看一下项目目录。
$ ls
README.md
index.html
node_modules/
package.json
public/
src/
static/
webpack.config.js
复制代码
项目脚手架中还存在隐藏文件,.babelrc和.gitignore。
这个项目是一个简单的webpack配置的应用,用Vue命令行界面vue-cli搭建。
index.html是我们声明DOM元素的地方 - #app-我们将用它来定义我们的Vue应用:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<link rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.5.3/css/bulma.css">
<link rel="stylesheet"
href="../public/styles.css" />
<title>Pokémon - Routing</title>
</head>
<body>
<div id="app"></div>
<script src="/dist/build.js"></script>
</body>
</html>
复制代码
在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: `
<div class="card card--charizard has-text-weight-bold has-text-white">
<div class="card-image">
<div class="card-image-container">
<img src="../../static/charizard.png"/>
</div>
</div>
<div class="card-content has-text-centered">
<div class="main">
<div class="title has-text-white">Charizard</div>
<div class="hp">hp 78</div>
</div>
<div class="stats columns is-mobile">
<div class="column">🔥<br>
<span class="tag is-warning">Type</span>
</div>
<div class="column center-column">199 lbs<br>
<span class="tag is-warning">Weight</span>
</div>
<div class="column">1.7 m <br>
<span class="tag is-warning">Height</span>
</div>
</div>
</div>
</div>
`
};
const App = {
name: 'App',
template: `
<div class="container">
<div class="pokemon">
<pokemon-card></pokemon-card>
</div>
</div>
`,
components: {
'pokemon-card': CharizardCard
}
};
export default App;
复制代码
目前,存在两个组件:CharizardCard和App。 CharizardCard组件是一个简单的模板,显示Charizard神奇宝贝的细节。 App组件在其组件属性中声明了CharizardCard组件,并在其模板中将其呈现为 </ pokemon-card>。
我们目前只有静态内容,我们可以看到我们是否运行我们的应用:
npm run dev
复制代码
并启动localhost:8080:
const CharizardCard = {
// ...
};
const BlastoiseCard = {
name: 'blastoise-card',
template: `
<div class="card card--blastoise has-text-weight-bold has-text-white">
<div class="card-image">
<div class="card-image-container">
<img src="../../static/blastoise.png"/>
</div>
</div>
<div class="card-content has-text-centered">
<div class="main">
<div class="title has-text-white">Blastoise</div>
<div class="hp">hp 79</div>
</div>
<div class="stats columns is-mobile">
<div class="column">💧<br>
<span class="tag is-light">Type</span>
</div>
<div class="column center-column">223 lbs<br>
<span class="tag is-light">Weight</span>
</div>
<div class="column">1.6 m<br>
<span class="tag is-light">Height</span>
</div>
</div>
</div>
</div>
`
};
const VenusaurCard = {
name: 'venusaur-card',
template: `
<div class="card card--venusaur has-text-weight-bold has-text-white">
<div class="card-image">
<div class="card-image-container">
<img src="../../static/venusaur.png"/>
</div>
</div>
<div class="card-content has-text-centered">
<div class="main">
<div class="title has-text-white">Venusaur</div>
<div class="hp hp-venusaur">hp 80</div>
</div>
<div class="stats columns is-mobile">
<div class="column">🍃<br>
<span class="tag is-danger">Type</span>
</div>
<div class="column center-column">220 lbs<br>
<span class="tag is-danger">Weight</span>
</div>
<div class="column">2.0 m<br>
<span class="tag is-danger">Height</span>
</div>
</div>
</div>
</div>
`
};
const App = {
// ...
};
export default App;
复制代码
随着我们的应用组件的建立,我们现在可以开始考虑如何在这些组件之间创建路由。
router-view
为了建立路由,我们将首先创建一个新组件,该组件负责根据应用的位置呈现指定组件。 我们将在一个名为View的常量变量中创建该组件。
在我们创建这个组件之前,让我们看看我们如何使用它。 在App组件的模板中,我们将删除的声明,而是渲染即将到来的router-view组件。 在组件属性中,我们将视图组件常量注册为以在模板中声明。
const App = {
name: 'App',
template: `
<div class="container">
<div class="pokemon">
<router-view></router-view>
</div>
</div>
`,
components: {
'router-view': View
}
};
export default App;
复制代码
router-view组件将根据URL路由匹配正确的神奇宝贝组件。 这个匹配将在我们将创建的路由数组中指定。 我们将在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;
复制代码
我们已经将每个Pokémon路径设置为各自的组件(例如/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: `<component :is="currentView"></component>`,
data() {
return {
currentView: CharizardCard
}
}
};
const App = {
// ...
};
export default App;
复制代码
保留的元素将呈现is属性绑定到的任何组件。 在上面,我们已经将is属性附加映射到CharizardCard组件的currentView属性。 所以,无论URL路径是什么,我们的应用通过显示CharizardCard作为开始点。
虽然router-view现在在应用内可以呈现,但它目前不是动态的。 我们需要router-view在加载页面时根据URL路径名显示正确的组件。 为此,我们将使用created()来过滤routes数组,并返回具有与URL路径匹配的路径的组件。 这会使View看起来像这样:
const View = {
name: 'router-view',
template: `<component :is="currentView"></component>`,
data() {
return {
currentView: {}
}
},
created() {
this.currentView = routes.find(
route => route.path === window.location.pathname
).component;
}
};
复制代码
在数据函数中,我们现在用一个空对象实例化currentView。 在created()中,我们使用JavaScript的本地find()方法返回匹配route.path === window.location.pathname的路由中的第一个对象。 然后我们可以用object.component(其中object是find()返回的对象)获取组件。
在浏览器环境中,window.location是一个包含浏览器当前位置属性的特殊对象。我们从该对象中获取路径名,该对象是URL的路径。
在这个阶段,我们将能够根据我们的浏览器URL的状态查看不同的神奇宝贝卡组件!
为避免这种情况,我们介绍一个简单的检查,如果URL路径名不匹配路径数组中存在的任何路径,则显示“未找到”模板。 我们将find()方法分离到名为getRouteObject()的组件方法,以避免重复。 这会将视图对象更新为:
const View = {
name: 'router-view',
template: `<component :is="currentView"></component>`,
data() {
return {
currentView: {}
}
},
created() {
if (this.getRouteObject() === undefined) {
this.currentView = {
template: `
<h3 class="subtitle has-text-white">
Not Found :(. Pick a Pokémon from the list below!
</h3>
`
};
} else {
this.currentView = this.getRouteObject().component;
}
},
methods: {
getRouteObject() {
return routes.find(
route => route.path === window.location.pathname
);
}
}
};
复制代码
如果getRouteObject()方法返回undefined,我们将显示一个“未找到”模板。 如果getRouteObject()从路由中返回一个对象,我们将currentView绑定到该对象的组件。 现在,如果输入一个随机URL,用户将收到通知:
漂亮!我们的应用正在响应某些外部状态,即浏览器的位置。 router-view根据应用的位置确定应该显示哪个组件。 现在,我们需要构建链接,以便在不发出Web请求的情况下更改浏览器的位置。 随着位置更新,我们希望重新渲染我们的Vue程序,并依靠router-view来适当确定要渲染的组件。
我们将这些链接标记为router-link组件。
router-link
在网页界面中,我们使用HTML a标签创建链接。 我们想要的是一种特殊的a标签。 当用户点击这个标签时,我们希望浏览器跳过它的默认链接,使得Web请求获取下一页。 相反,我们只是想手动更新浏览器的位置。
让我们来编写一个router-link,它会生成带有特殊点击绑定的a标签。 当用户点击router-link组件时,我们将使用浏览器的历史API来更新浏览器的位置。
就像我们使用router-view一样,让我们看看在构建它之前我们将如何使用这个组件。
在App组件的模板中,我们在父元素
const App = {
name: 'App',
template: `
<div class="container">
<div class="pokemon">
<router-view></router-view>
<div class="pokemon-links has-text-centered">
<router-link to="/charizard"></router-link>
<router-link to="/blastoise"></router-link>
<router-link to="/venusaur"></router-link>
</div>
</div>
</div>
`,
components: {
'router-view': View,
'router-link': Link
}
};
复制代码
我们将在App组件上方创建表示router-link的Link对象。 我们已经建立了router-link组件,应该总是赋予一个具有目标位置值的属性(即prop)。 我们可以像这样强制执行来验证需求:
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模板,使其包含具有@click处理程序属性的a标记。 触发后,@click处理程序将调用标记为navigate()的组件方法,该方法将浏览器导航到所需的位置。 此导航将使用history.pushState()方法进行。 就是说,链接常量对象将被更新为:
const Link = {
name: 'router-link',
props: {
to: {
type: String,
required: true
}
},
template: `<a @click="navigate" :href="to">{{ to }}</a>`,
methods: {
navigate(evt) {
evt.preventDefault();
window.history.pushState(null, null, this.to);
}
}
};
复制代码
在a标签中,我们用{{to}}将to prop的值绑定到元素文本内容。
触发navigate()时,它首先调用事件对象上的preventDefault(),以防止浏览器为新指向发出Web请求。 然后调用history.pushState()方法将用户引导至所需的路由位置。 history.pushState()有三个参数: 1.一个状态对象来传递序列化的状态信息
2.一个标题
3.目标网址
在我们的例子中,没有需要传递的状态信息,所以我们将第一个参数留为空。 某些浏览器(例如Firefox)目前忽略第二个参数title,因此我们也将它保留为null。
目标位置,即prop,被传递到第三个也是最后一个参数。 由于to prop包含目标位置处于相对状态,因此将相对于当前URL进行解析。 在我们的例子中,/blastoise将解析为http://localhost:8080/blastoise。
如果我们现在点击任何链接,我们会注意到我们的浏览器更新到正确的位置,没有完整的页面重新加载。但是,我们的应用不会更新并呈现正确的组件。
虽然有几种方法可以完成这种行为,但我们将通过使用自定义EventBus来完成此操作。 EventBus是一个Vue实例,负责允许隔离的组件在彼此之间订阅和发布自定义事件。
在文件的开头,我们将导入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: `
<h3 class="subtitle has-text-white">
Not Found :(. Pick a Pokémon from the list below!
</h3>
`
};
} else {
this.currentView = this.getRouteObject().component;
}
// Event listener for link navigation
EventBus.$on('navigate', () => {
this.currentView = this.getRouteObject().component;
});
},
// ...
};
复制代码
当通过单击元素更改浏览器的位置时,将调用此侦听函数,重新渲染router-view以匹配最新的URL!
还有一件事我们需要考虑。 如果我们尝试使用浏览器后退/前进按钮浏览浏览器历史记录,我们的应用目前不会正确重新呈现。 这是因为当用户点击浏览器后退或浏览器前进时未发出事件通知程序。
为了完成这个工作,我们将使用onpopstate事件处理程序。
每当活动历史记录条目更改时,就会触发onpopstate事件。 通过单击浏览器后退或浏览器前进按钮或调用history.back或history.forward()来调用历史记录更改。
在我们的EventBus创建之后,让我们设置onpopstate事件侦听器,以便在调用历史记录更改时发出导航事件:
window.addEventListener('popstate', () => {
EventBus.$emit('navigate');
});
复制代码
即使浏览器导航按钮被使用,我们的应用现在也能够正确响应!
总结
我爱Vue。 原因之一 - 就像我们在本文中看到的那样,使用和操作Vue组件非常简单。
在介绍中,我们提到了Vue如何提供vue-router库作为框架的官方路由库。 我们刚刚创建了vue-router中使用的简单版本:
1.routes
该数组负责将组件映射到相应的URL路径名。
2.router-view
基于应用程序位置呈现指定应用组件的组件
3.router-link
该组件允许用户在不发出Web请求的情况下更改浏览器的位置。
对于非常简单的应用,我们构建的路由(或其类似于Chris Fritz构建的这种路由)可以完成路由应用程序所需的最少量工作。
另一方面,vue路由器库以更复杂的方式构建,并引入了令人难以置信的有用功能,这在大型应用程序中经常需要:
不同浏览器之间的一致性;嵌套路线;导航卫兵;过渡效应
虽然vue-router库确实附带了额外的样板,但一旦你的应用由完全隔离且不同的组件组成,就很容易进行集成。 如果有兴趣,可以在这里[github.com/djirdehh/po…]看到vue-router的组件用于在此应用中启用路由。
希望这对你来说能和我在编写这篇文章时一样愉快! 谢谢阅读!
最后
欢迎关注我的微信公众号【热前端】,一起交流成长。