什麼是路由?
路由這概念最開始是在後端出現的,在以前前後端不分離的時候,由後端來控制路由,伺服器接收客戶端的請求,解析對應的url路徑,並返回對應的頁面/資源。
簡單的說 路由就是根據不同的url地址來展示不同的內容或頁面.
前端路由的來源
在很久很久以前~ 使用者的每次更新操作都需要重新重新整理頁面,非常的影響互動體驗,後來,為了解決這個問題,便有了Ajax(非同步載入方案),Ajax給體驗帶來了極大的提升。
雖然Ajax解決了使用者互動時體驗的痛點,但是多頁面之間的跳轉一樣會有不好的體驗,所以便有了spa(single-page application)使用的誕生。而spa應用便是基於前端路由實現的,所以便有了前端路由。
如今比較火的vue-router/react-router 也是基於前端路由的原理實現的~
前端路由的兩種實現原理
1.Hash模式
window物件提供了onhashchange事件來監聽hash值的改變,一旦url中的hash值發生改變,便會觸發該事件。
window.onhashchange = function(){
// hash 值改變
// do you want
}
複製程式碼
2.History 模式
HTML5的History API 為瀏覽器的全域性history物件增加的擴充套件方法。
簡單來說,history其實就是瀏覽器歷史棧的一個介面。這裡不細說history的每個API啦。具體可查閱 傳送門
window物件提供了onpopstate事件來監聽歷史棧的改變,一旦歷史棧資訊發生改變,便會觸發該事件。
需要特別注意的是,呼叫history.pushState()或history.replaceState()不會觸發popstate事件。只有在做出瀏覽器動作時,才會觸發該事件。
window.onpopstate = function(){
// 歷史棧 資訊改變
// do you want
}
複製程式碼
history提供了兩個操作歷史棧的API:history.pushState 和 history.replaceState
history.pushState(data[,title][,url]);//向歷史記錄中追加一條記錄
複製程式碼
history.replaceState(data[,title][,url]);//替換當前頁在歷史記錄中的資訊。
複製程式碼
// data: 一個JavaScript物件,與用pushState()方法建立的新歷史記錄條目關聯。無論何時使用者導航到新建立的狀態,popstate事件都會被觸發,並且事件物件的state屬性都包含歷史記錄條目的狀態物件的拷貝。
//title: FireFox瀏覽器目前會忽略該引數,雖然以後可能會用上。考慮到未來可能會對該方法進行修改,傳一個空字串會比較安全。或者,你也可以傳入一個簡短的標題,標明將要進入的狀態。
//url: 新的歷史記錄條目的地址。瀏覽器不會在呼叫pushState()方法後載入該地址,但之後,可能會試圖載入,例如使用者重啟瀏覽器。新的URL不一定是絕對路徑;如果是相對路徑,它將以當前URL為基準;傳入的URL與當前URL應該是同源的,否則,pushState()會丟擲異常。該引數是可選的;不指定的話則為文件當前URL。
複製程式碼
兩種模式優劣對比
對比 | Hash | History |
---|---|---|
觀賞性 | 醜 | 美 |
相容性 | >ie8 | >ie10 |
實用性 | 直接使用 | 需後端配合 |
名稱空間 | 同一document | 同源 |
造(cao) 一個簡單的前端路由
本demo只是想說幫助我們通過實踐更進一步的理解前端路由這個概念,所以只做了簡單的實現~
history模式404
當我們使用history模式時,如果沒有進行配置,重新整理頁面會出現404。
原因是因為history模式的url是真實的url,伺服器會對url的檔案路徑進行資源查詢,找不到資源就會返回404。
這個問題的解決方案這裡就不細說了,google一下,你就知道~ 我們在以下demo使用webpack-dev-server的裡的historyApiFallback屬性來支援HTML5 History Mode。
檔案結構
|-- package.json
|-- webpack.config.js
|-- index.html
|-- src
|-- index.js
|-- routeList.js
|-- base.js
|-- hash.js
|-- history.js
複製程式碼
1.搭建環境
廢話不多說,直接上程式碼~
package.json
{
"name": "web_router",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "webpack-dev-server --config ./webpack.config.js"
},
"author": "webfansplz",
"license": "MIT",
"devDependencies": {
"html-webpack-plugin": "^3.2.0",
"webpack": "^4.28.1",
"webpack-cli": "^3.2.1",
"webpack-dev-server": "^3.1.14"
}
}
複製程式碼
webpack.config.js
'use strict';
const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
filename: '[name].js'
},
devServer: {
clientLogLevel: 'warning',
hot: true,
inline: true,
open: true,
//在開發單頁應用時非常有用,它依賴於HTML5 history API,如果設定為true,所有的跳轉將指向index.html (解決histroy mode 404)
historyApiFallback: true,
host: 'localhost',
port: '6789',
compress: true
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
new HtmlWebpackPlugin({
filename: 'index.html',
template: 'index.html',
inject: true
})
]
};
複製程式碼
2.開擼
首先我們先初始化定義我們需要實現的功能及配置引數。
前端路由 | 引數 | 方法 |
---|---|---|
- | 模式(mode) | push(壓入) |
- | 路由列表(routeList) | replace(替換) |
- | - | go(前進/後退) |
src/index.js
const MODE='';
const ROUTELIST=[];
class WebRouter {
constructor() {
}
push(path) {
...
}
replace(path) {
...
}
go(num) {
...
}
}
new WebRouter({
mode: MODE,
routeList: ROUTELIST
});
複製程式碼
前面我們說了前端路由有兩種實現方式。
1.定義路由列表
2.我們分別為這兩種方式建立對應的類,並根據不同的mode引數進行例項化,完成webRouter類的實現。
src/routeList.js
export const ROUTELIST = [
{
path: '/',
name: 'index',
component: 'This is index page'
},
{
path: '/hash',
name: 'hash',
component: 'This is hash page'
},
{
path: '/history',
name: 'history',
component: 'This is history page'
},
{
path: '*',
name: 'notFound',
component: '404 NOT FOUND'
}
];
複製程式碼
src/hash.js
export class HashRouter{
}
複製程式碼
src/history.js
export class HistoryRouter{
}
複製程式碼
src/index.js
import { HashRouter } from './hash';
import { HistoryRouter } from './history';
import { ROUTELIST } from './routeList';
//路由模式
const MODE = 'hash';
class WebRouter {
constructor({ mode = 'hash', routeList }) {
this.router = mode === 'hash' ? new HashRouter(routeList) : new HistoryRouter(routeList);
}
push(path) {
this.router.push(path);
}
replace(path) {
this.router.replace(path);
}
go(num) {
this.router.go(num);
}
}
const webRouter = new WebRouter({
mode: MODE,
routeList: ROUTELIST
});
複製程式碼
前面我們已經實現了webRouter的功能,接下來我們來實現兩種方式。
因為兩種模式都需要呼叫一個方法來實現不同路由內容的重新整理,so~
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>前端路由</title>
</head>
<body>
<div id="page"></div>
</body>
</html>
複製程式碼
js/base.js
const ELEMENT = document.querySelector('#page');
export class BaseRouter {
//list = 路由列表
constructor(list) {
this.list = list;
}
render(state) {
//匹配當前的路由,匹配不到則使用404配置內容 並渲染~
let ele = this.list.find(ele => ele.path === state);
ele = ele ? ele : this.list.find(ele => ele.path === '*');
ELEMENT.innerText = ele.component;
}
}
複製程式碼
ok,下面我們來實現兩種模式。
Hash模式
src/hash.js
import { BaseRouter } from './base.js';
export class HashRouter extends BaseRouter {
constructor(list) {
super(list);
this.handler();
//監聽hash變化事件,hash變化重新渲染
window.addEventListener('hashchange', e => {
this.handler();
});
}
//渲染
handler() {
this.render(this.getState());
}
//獲取當前hash
getState() {
const hash = window.location.hash;
return hash ? hash.slice(1) : '/';
}
//獲取完整url
getUrl(path) {
const href = window.location.href;
const i = href.indexOf('#');
const base = i >= 0 ? href.slice(0, i) : href;
return `${base}#${path}`;
}
//改變hash值 實現壓入 功能
push(path) {
window.location.hash = path;
}
//使用location.replace實現替換 功能
replace(path) {
window.location.replace(this.getUrl(path));
}
//這裡使用history模式的go方法進行模擬 前進/後退 功能
go(n) {
window.history.go(n);
}
}
複製程式碼
History模式
src/history.js
import { BaseRouter } from './base.js';
export class HistoryRouter extends BaseRouter {
constructor(list) {
super(list);
this.handler();
//監聽歷史棧資訊變化,變化時重新渲染
window.addEventListener('popstate', e => {
this.handler();
});
}
//渲染
handler() {
this.render(this.getState());
}
//獲取路由路徑
getState() {
const path = window.location.pathname;
return path ? path : '/';
}
//使用pushState方法實現壓入功能
//PushState不會觸發popstate事件,所以需要手動呼叫渲染函式
push(path) {
history.pushState(null, null, path);
this.handler();
}
//使用replaceState實現替換功能
//replaceState不會觸發popstate事件,所以需要手動呼叫渲染函式
replace(path) {
history.replaceState(null, null, path);
this.handler();
}
go(n) {
window.history.go(n);
}
}
複製程式碼
3.小功告成
就這樣,一個簡單的前端路由就完成拉。
如果覺得有幫助到你的話,給個star哈~