[實踐系列] 前端路由

什麼是路由?

路由這概念最開始是在後端出現的,在之前先後端不分離的時候,由後端來控制路由,服務器接收客戶端的請求,解析對應的url路徑,並返回對應的頁面/資源。javascript

簡單的說 路由就是根據不一樣的url地址來展現不一樣的內容或頁面.html

前端路由的來源

在好久好久之前~ 用戶的每次更新操做都須要從新刷新頁面,很是的影響交互體驗,後來,爲了解決這個問題,便有了Ajax(異步加載方案),Ajax給體驗帶來了極大的提高。前端

雖然Ajax解決了用戶交互時體驗的痛點,可是多頁面之間的跳轉同樣會有很差的體驗,因此便有了spa(single-page application)使用的誕生。而spa應用即是基於前端路由實現的,因此便有了前端路由。vue

現在比較火的vue-router/react-router 也是基於前端路由的原理實現的~java

前端路由的兩種實現原理

1.Hash模式

window對象提供了onhashchange事件來監聽hash值的改變,一旦url中的hash值發生改變,便會觸發該事件。react

window.onhashchange = function(){
    
    // hash 值改變 
    
    // do you want
}
複製代碼

2.History 模式

HTML5的History API 爲瀏覽器的全局history對象增長的擴展方法。webpack

簡單來講,history其實就是瀏覽器歷史棧的一個接口。這裏不細說history的每一個API啦。具體可查閱 傳送門git

window對象提供了onpopstate事件來監聽歷史棧的改變,一旦歷史棧信息發生改變,便會觸發該事件。github

須要特別注意的是,調用history.pushState()或history.replaceState()不會觸發popstate事件。只有在作出瀏覽器動做時,纔會觸發該事件。web

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哈~

相關文章
相關標籤/搜索