簡易路由實現——(hash路由)

前言

前陣子逛 github 的時候,看見一篇文章 《原生JS實現hash路由》, 想着照着 vue-router 的 api,參考這篇文章實現一個可直接用於 html, 支持 hash 路由和 history 路由的 js 插件。本文是 hash 路由的具體實現。html

話很少說,先上 demo&& 源碼&& 工程文件(htmlRouter文件夾下)前端

實現功能

  1. 使用 router.back() ; router.front()控制前進後退
  2. 使用 router.push(option); router.replace(option)實現路由跳轉
  3. 根據當前路徑動態顯示對應的組件

頁面結構

使用自定義屬性 data-component-name 使頁面根據當前路由名稱顯示對應組件名的 dom 元素,默認擁有此屬性的 dom 元素隱藏vue

<main>
      <div class="nav">
        <ul class="nav-list">
          <li class="nav-item"><a href="#/monday">週一</a></li>
          <li class="nav-item"><a href="#/tuesday">週二</a></li>
          <li class="nav-item"><a href="#/wednesday">週三</a></li>
          <li class="nav-item"><a href="#/thursday">週四</a></li>
          <li class="nav-item"><a href="#/friday">週五</a></li>
        </ul>
      </div>
      <div class="main-content">
        <div class="main-box" data-component-name="monday">monday</div>
        <div class="main-box" data-component-name="tuesday">tuesday</div>
        <div class="main-box" data-component-name="wednesday">wednesday</div>
        <div class="main-box" data-component-name="thursday">thursday</div>
        <div class="main-box" data-component-name="friday">friday</div>
      </div>
    </main>

    <div class="nav-area">
      <button class="nav-area-back" onclick="router.back();">後退</button>
      <button class="nav-area-front" onclick="router.front();">前進</button>
      <button class="nav-area-front" onclick="router.go(-1);">go(-1)</button>
      <button class="nav-area-front" onclick="router.push({path: '/monday', query: {name: 'suporka', age: '26'}});" >
        push path
      </button>
      <button class="nav-area-front" onclick="router.push({name: 'monday', query: {name: 'suporka', age: '26'}});" >
        push name
      </button>

      <button class="nav-area-front" onclick="router.replace({name: 'monday', query: {name: 'suporka', age: '18'}});" >
        replace
      </button>
    </div>
複製代碼

實現路由

1. 建立最外層類 Router

實現 new Router(option)建立路由,根據 vue-router 的配置選項,本文實現 mode 以及 routes 屬性node

import HashRouter from './HashRouter'
import HistoryRouter from './HistoryRouter';

class Router {
  constructor(routerConfig) {
    this._mode = routerConfig.mode || "hash"; // hash 或者 history
    this._routes = routerConfig.routes;
    // 根據不一樣的模式建立不一樣的路由類,本文是 hash 路由
    if (routerConfig.mode === "hash")
      this._router = new HashRouter(routerConfig);
    else this._router = new HistoryRouter(routerConfig);
    this._router.init(); // 路由初始化
  }
  back() {
    this._router.back();
  }
  front() {
    this._router.front();
  }
  go(n) {
    window.history.go(n);
  }
  push(option) {
    this._router.push(option);
  }
  replace(option) {
    this._router.replace(option);
  }
}
export default Router
複製代碼

2. 建立 hash 路由與 history 路由的共同父類 RouterParent

由於目前咱們還沒有實現 history 路由,不知道那些屬性或方法是共同擁有的,因此暫時將 hash 路由的屬性所有寫於父類當中,當 history 路由實現時再將共同擁有的屬性方法進行抽離,單獨擁有的屬性方法單獨歸屬。webpack

export default class RouterParent {
    constructor(routerConfig) {
        this._routes = routerConfig.routes; // 路由列表
        this.routeHistory = []; // 路由歷史
        this.currentUrl = ''; // 當前的路由地址
        this.currentIndex = -1; // 當前的路由序列號
        this.frontOrBack = false; // 是否的點擊前進後退形成的路由變化,此時不須要監聽到路由變化函數
        this.replaceRouter = false; // 是不是替換當前路由
    }
}
複製代碼

3. 實現 hash 路由

vue-router 默認使用 Hash 模式。git

使用 url 的 hash 來模擬一個完整的 url。此時 url 變化時,瀏覽器是不會從新加載的。github

Hash(即#)是 url 的錨點,表明的是網頁中的一個位置,僅僅改變#後面部分,瀏覽器只會滾動對應的位置,而不會從新加載頁面。web

Hash僅僅只是對瀏覽器進行指導,而對服務端是徹底沒有做用的!它不會被包括在 http 請求中,故也不會從新加載頁面。同時 hash 發生變化時,url 都會被瀏覽器記錄下來,這樣你就能夠使用瀏覽器的後退了。算法

所以,咱們須要監聽頁面hash的變化,經過 window.addEventListener('hashchange', func, false);實現vue-router

init()

哈希路由繼承父類RouterParent,咱們在其 init() 方法時監聽 hashchange 事件,初始化

class HashRouter extends RouterParent {
    constructor(routerConfig) {
        super(routerConfig);
    }
    init() {
        // 監聽hash的變化
        // refresh 實現對應組件和當前路由綁定顯示
        // bind(this) 傳入此實例對象,不然this指向有問題
        window.addEventListener('hashchange', this.refresh.bind(this), false);
    }
}
複製代碼

由於在頁面加載時,也須要根據此路由顯示對應組件,所以加入 load 監聽事件

class HashRouter extends RouterParent {
    constructor(routerConfig) {
        super(routerConfig);
    }
    init() {
        // 監聽hash的變化
        // refresh 實現對應組件和當前路由綁定顯示
        // bind(this) 傳入此實例對象,不然this指向有問題
        window.addEventListener('hashchange', this.refresh.bind(this), false);
        window.addEventListener('load', this.refresh.bind(this), false);
    }
}
複製代碼

refresh()

在此實例中,咱們使用 frontOrBack 屬性判斷當前是否處於前進後退,若是是前進後退,則路由歷史列表 routeHistory 不變化

根據當前 hash 路徑,從 routes 列表中找出對應的路由 name, 在操做對應的 dom 元素使其顯示或隱藏

refresh() {
    if (this.frontOrBack) {
        // 前進後退形成的路由變化,此時不須要改變routeHistory的數據
        this.frontOrBack = false;
    } else {
        this.currentUrl = location.hash.slice(1) || '/';
        this.routeHistory = this.routeHistory.slice(0,this.currentIndex + 1); // 捨棄掉當前索引後的路由歷史
        this.routeHistory.push(this.currentUrl); // 添加當前路徑
        this.currentIndex++; // 當前索引自增
    }
    let path = getPath(),
        currentComponentName = '',
        nodeList = document.querySelectorAll('[data-component-name]');
    // 找出當前路由的名稱
    for (let i = 0; i < this._routes.length; i++) {
        if (this._routes[i].path === path) {
            currentComponentName = this._routes[i].name;
            break;
        }
    }
    // 根據當前路由的名稱顯示對應的組件
    nodeList.forEach(item => {
        if (item.dataset.componentName === currentComponentName) {
            item.style.display = 'block';
        } else {
            item.style.display = 'none';
        }
    });
}

// 獲取路徑
function getPath() {
    let href = window.location.href;
    const index = href.indexOf('#');
    // empty path
    if (index < 0) return '';

    href = href.slice(index + 1);
    const searchIndex = href.indexOf('?');
    if (searchIndex < 0) return href;
    else {
        return href.slice(0, searchIndex);
    }
}
複製代碼

back() && front()

back() front() 都是經過修改當前路由索引和 hash,從而觸發 hashchange 事件調用 refresh 方法

back() {
    if (this.currentIndex > 0) {
        this.frontOrBack = true; // 在refresh中會重置爲false
        this.currentIndex--; // 修改索引
        this.currentUrl = this.routeHistory[this.currentIndex]; // 修改當前url
        window.location.hash = this.currentUrl; // 修改實際hash
    }
}

front() {
    const historyLength = this.routeHistory.length;
    if (this.currentIndex < historyLength - 1) {
        this.frontOrBack = true;
        this.currentIndex++;
        this.currentUrl = this.routeHistory[this.currentIndex];
        window.location.hash = this.currentUrl;
    }
}
複製代碼

push(option)

在vue-router中,能夠經過 path, name 修改當前路由,而且能夠攜帶 query 參數

所以優先判斷 path, 若是有path,則直接修改 hash; 沒有 path, 則根據 name 從 routes 中找出 path, 再修改 hash

push(option) {
    if (option.path) {
        changeHash(option.path, option.query);
    } else if (option.name) {
        let path = '';
        // 根據路由名稱找路由path
        for (let i = 0; i < this._routes.length; i++) {
            if (this._routes[i].name === option.name) {
                path = this._routes[i].path;
                break;
            }
        }
        if (!path) {
            error('組件名稱不存在');
        } else {
            changeHash(path, option.query);
        }
    }
}

// 報錯
function error(message) {
    typeof console !== 'undefined' && console.error(`[html-router] ${message}`);
}
// 根據path和query修改hash
function changeHash(path, query) {
    if (query) {
        let str = '';
        for (let i in query) {
            str += '&' + i + '=' + query[i];
        }
        (str && (window.location.hash = path + '?' + str.slice(1))) ||
            (window.location.hash = path);
    } else {
        window.location.hash = path;
    }
}
複製代碼

replace(option)

其實 replace 和 push 很類似,參數也一致,惟一不一樣的是 replace 是替換當前路徑,並且不會往 routerHistory 添加新的歷史。能夠通用 push 方法,經過 this.replaceRouter 聲明當前爲"替換路徑"

replace(option) {
    this.replaceRouter = true;
    this.push(option);
}
複製代碼

所以在 refresh 方法中,咱們也須要對 this.replaceRouter = true 這種狀態進行單獨處理

refresh() {
    if (this.frontOrBack) {
        // 前進後退形成的路由變化,此時不須要改變routeHistory的數據
        this.frontOrBack = false;
    } else {
        this.currentUrl = location.hash.slice(1) || '/';
        // 當前爲replace狀態
        if (this.replaceRouter) {
            this.routeHistory[this.currentIndex] = this.currentUrl;
            this.replaceRouter = false; // 重置replaceRouter
        } else {
            this.routeHistory.push(this.currentUrl);
            this.currentIndex++;
        }
        this.routeHistory = this.routeHistory.slice(
            0,
            this.currentIndex + 1
        );
    }
    let path = getPath(),
        currentComponentName = '',
        nodeList = document.querySelectorAll('[data-component-name]');
    // 找出當前路由的名稱
    for (let i = 0; i < this._routes.length; i++) {
        if (this._routes[i].path === path) {
            currentComponentName = this._routes[i].name;
            break;
        }
    }
    // 根據當前路由的名稱顯示對應的組件
    nodeList.forEach(item => {
        if (item.dataset.componentName === currentComponentName) {
            item.style.display = 'block';
        } else {
            item.style.display = 'none';
        }
    });
}
複製代碼

Demo測試

import Router from './htmlRouter-dev'
window.router = new Router({
    mode: 'hash',
    routes: [
        {
            path: '/monday',
            name: 'monday',
        },
        {
            path: '/tuesday',
            name: 'tuesday',
        },
        {
            path: '/wednesday',
            name: 'wednesday',
        },
        {
            path: '/thursday',
            name: 'thursday',
        },
        {
            path: '/friday',
            name: 'friday',
        },
    ],
});
複製代碼

效果以下:

以上即是hash路由的實現,關於history路由的實現,我會在下篇文章中詳細介紹,敬請期待

更多推薦

前端進階小書(advanced_front_end)

前端每日一題(daily-question)

webpack4 搭建 Vue 應用(createVue)

Canvas 進階(一)二維碼的生成與掃碼識別

Canvas 進階(二)寫一個生成帶logo的二維碼npm插件

Canvas 進階(三)ts + canvas 重寫」辨色「小遊戲

Canvas 進階(四)實現一個「刮刮樂」遊戲

Canvas 進階(五)實現圖片濾鏡效果

VUI建立日誌(一)——圖片懶加載指令的實現

VUI建立日誌(二)——防抖節流組件的實現

前端算法題目解析(一)

前端算法題目解析(二)

相關文章
相關標籤/搜索