[實踐系列] 前端路由

webfansplz發表於2019-01-11

什麼是路由?

路由這概念最開始是在後端出現的,在以前前後端不分離的時候,由後端來控制路由,伺服器接收客戶端的請求,解析對應的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哈~

相關文章