大量的教程在解釋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的組件用於在此應用中啓用路由。
但願這對你來講能和我在編寫這篇文章時同樣愉快! 謝謝閱讀!
歡迎關注個人微信公衆號【熱前端】,一塊兒交流成長。