大量的教程在解释Vue的官方路由库vue-router如何集成到现有的Vue应用中作了很好的工做。 vue-router经过向咱们提供将应用的组件映射到不一样的浏览器URL路由所需的功能,作了出色的工做。javascript
简单的应用一般不须要彻底成熟的路由库,如vue-router。 在本文中,咱们将使用Vue构建一个简单的自定义客户端路由器。 经过这样作,咱们将了解须要处理什么来构建客户端路由以及潜在的缺点。css
虽然本文假设了Vue.js的基本知识; 在咱们开始编写代码时,咱们将一步步来解释!html
首先也是最重要的:咱们为那些可能对这个概念不熟悉的人解释一下Routing。前端
在Web开发中,路由一般是指根据从浏览器URL派生的规则来分割应用程序的UI。 想象一下,点击一个连接并让网址从https://website.com转到https://website.com/article/。 这是路由。vue
路由一般分为两个部分:java
1.服务器端路由node
客户端(即浏览器)在每次URL更改时向服务器发出请求。webpack
2.客户端路由git
客户端仅在首页加载时向服务器发出请求。 而后在客户端上处理基于URL路由的应用程序UI的任何更改。github
客户端路由是术语单页应用程序(简称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;
复制代码
随着咱们的应用组件的创建,咱们如今能够开始考虑如何在这些组件之间建立路由。
为了创建路由,咱们将首先建立一个新组件,该组件负责根据应用的位置呈现指定组件。 咱们将在一个名为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组件。
在网页界面中,咱们使用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的组件用于在此应用中启用路由。
但愿这对你来讲能和我在编写这篇文章时同样愉快! 谢谢阅读!
欢迎关注个人微信公众号【热前端】,一块儿交流成长。