如何經過100行代碼實現一個迷你router?

知識儲備

如何操做瀏覽器地址?

咱們可使用下面三種方法,來修改瀏覽器的地址javascript

  • location.assign(url)
  • window.location = url
  • location.href = url(常見)

修改如下location對象的屬性值,會致使當前頁面從新加載vue

// 假如當前url爲:https://www.example.com/

// 把url修改成:https://www.example.com/?t=example
location.search = '?t=example';

// 把url修改成:https://example.com/?t=example
location.hostname = 'example.com';

// 把url修改成:https://www.example.com/example
location.pathname = 'example';

// 把url修改成:https://www.example.com:8080
location.port = 8080

修改hash時,瀏覽器歷史中會新增長一條記錄,可是並不會刷新頁面。所以SPA應用中,hash也是一種切換路由的方式。java

// 假如當前url爲:https://www.example.com/

// 把url修改成:https://www.example.com/#example
location.hash = '#example';

使用location.replace(url)方法跳轉的url,並不會增長曆史記錄。react

使用location.reload()方法能夠從新加載當前頁面,是否傳參的區別以下:ios

location.reload(); // 從新加載,多是從緩存加載
location.reload(true); // 從新加載,從服務器加載

如何導航頁面?

使用go(n)能夠在用戶記錄中沿任何方向導航(便可之前進也能夠後退)。正值表示在歷史中前進,負值表示在歷史中後退。git

假如要前進1頁,那麼可使用window.history.`go(1)。同時,也可使用window.history.forward()`來作相同的事情。
假如要後退1頁,那麼可使用window.history.`go(-1)。同時,也可使用window.history.back()`來作相同的事情。github

若是使用window.history.go(0)window.history.go()都會從新加載當前頁面。正則表達式

如何改變頁面的地址,可是不會從新加載頁面而且怎樣監聽這些改變?

使用hash

上面咱們說到了,修改hash能夠作到改變頁面的地址,在瀏覽器歷史中添加一條記錄,可是不會從新加載頁面。vue-router

咱們同時能夠配合hashchange事件,監聽頁面地址hash的變化。npm

使用history.pushState()、history.replaceState()

使用history.pushState(),相似是執行了location.href = url,可是並不會從新加載頁面。假如用戶執行了後退操做,將會觸發popstate事件。

使用history.replaceState(),相似是執行了location.replace(url),可是並不會從新加載頁面。

注意,執行pushState、replaceState方法後,雖然瀏覽器地址有改變,可是並不會觸發popState事件

實現一個mini-router

開始編寫構造函數

首先咱們須要肯定的是,咱們的路由應該須要下面4個屬性:

  • routes:一個數組,包含全部註冊的路由對象
  • mode: 路由的模式,能夠選擇hashhistory
  • base:根路徑
  • constructor:初始化新的路由實例
class MiniRouter {
  constructor(options) {
    const { mode, routes, base } = options;
    
    this.mode = mode || (window.history.pushState ? 'history' : 'hash');
    this.routes = routes || [];
    this.base = base || '/';
  }
}

export default MiniRouter;

增長添加路由對象方法

路由對象中包含下面兩個屬性

  • path:由正則表達式表明的路徑地址(並非字符串,後面會詳細解釋)
  • cb:路由跳轉後執行的回調函數
class MiniRouter {
  constructor(options) {
    const { mode, routes, base } = options;
    
    this.mode = mode || (window.history.pushState ? 'history' : 'hash');
    this.routes = routes || [];
    this.base = base || '/';
  }
  
  // 添加路由對象 👇 新增代碼
  // routerConfig示例爲:
  // {path: /about/, cb(){console.log('about')}}
  addRoute(routeConfig) {
    this.routes.push(routeConfig);
  }
  /// 👆 新增代碼
}

export default MiniRouter;

增長路由導航功能

添加路由導航功能,其實是location相關方法的封裝

詳細內容能夠回看:如何導航頁面?

class MiniRouter {
  constructor(options) {
    const { mode, routes, base } = options;
    
    this.mode = mode || (window.history.pushState ? 'history' : 'hash');
    this.routes = routes || [];
    this.base = base || '/';
  }
  
  addRoute(routeConfig) {
    this.routes.push(routeConfig);
  }
  
  // 添加前進、後退功能 👇 新增代碼
  go(n) {
    window.location.go(n);
  }
  
  back() {
    window.location.back();
  }
  
  forward() {
    window.location.forward();
  }
  
  /// 👆 新增代碼
}

export default MiniRouter;

實現導航到新路由的功能

參照vue-router,大橙子在這裏設計了push、replace兩種方法。其中:
push表明跳轉新頁面,並在歷史棧中增長一條記錄,用戶能夠後退
replace表明跳轉新頁面,可是不在歷史棧中增長記錄,用戶不能夠後退

若是是hash模式下
使用location.hash = newHash來實現push跳轉
使用window.location.replace(url)來實現replace跳轉

若是是history模式下
使用history.pushState()來實現push跳轉
使用history.replaceState()來實現replace跳轉

請注意:
pushStatereplaceState添加 try...catch是因爲Safari的某個安全策略

有興趣的同窗能夠查看
vue-router相關commit
Stack Overflow上的相關問題

class MiniRouter {
  constructor(options) {
    const { mode, routes, base } = options;
    
    this.mode = mode || (window.history.pushState ? 'history' : 'hash');
    this.routes = routes || [];
    this.base = base || '/';
  }
  
  addRoute(routeConfig) {
    this.routes.push(routeConfig);
  }
  
  go(n) {
    window.history.go(n);
  }
  
  back() {
    window.location.back();
  }
  
  forward() {
    window.location.forward();
  }
  
  // 實現導航到新路由的功能
  // push表明跳轉新頁面,並在歷史棧中增長一條記錄,用戶能夠後退
  // replace表明跳轉新頁面,可是不在歷史棧中增長記錄,用戶不能夠後退
  //👇 新增代碼
  push(url) {
    if (this.mode === 'hash') {
      this.pushHash(url);
    } else {
      this.pushState(url);
    }
  }
  
  pushHash(path) {
    window.location.hash = path;
  }
  
  pushState(url, replace) {
    const history = window.history;

    try {
      if (replace) {
        history.replaceState(null, null, url);
      } else {
        history.pushState(null, null, url);
      }
      
      this.handleRoutingEvent();
    } catch (e) {
      window.location[replace ? 'replace' : 'assign'](url);
    }
  }
  
  replace(path) {
    if (this.mode === 'hash') {
      this.replaceHash(path);
    } else {
      this.replaceState(path);
    }
  }
  
  replaceState(url) {
    this.pushState(url, true);
  }
  
  replaceHash(path) {
    window.location.replace(`${window.location.href.replace(/#(.*)$/, '')}#${path}`);
  }

  /// 👆 新增代碼
}

export default MiniRouter;

實現獲取路由地址的功能

history模式下,咱們會使用location.path來獲取當前連接路徑。

若是設置了base參數,將會把base路徑幹掉,方便後面匹配路由地址。

hash模式下,咱們會使用正則匹配將#後的地址匹配出來。

固然全部操做以後,將會把/徹底去掉。

class MiniRouter {
  constructor(options) {
    const { mode, routes, base } = options;
    
    this.mode = mode || (window.history.pushState ? 'history' : 'hash');
    this.routes = routes || [];
    this.base = base || '/';
  }
  
  addRoute(routeConfig) {
    this.routes.push(routeConfig);
  }
  
  go(n) {
    window.history.go(n);
  }
  
  back() {
    window.location.back();
  }
  
  forward() {
    window.location.forward();
  }

  push(url) {
    if (this.mode === 'hash') {
      this.pushHash(url);
    } else {
      this.pushState(url);
    }
  }
  
  pushHash(path) {
    window.location.hash = path;
  }
  
  pushState(url, replace) {
    const history = window.history;

    try {
      if (replace) {
        history.replaceState(null, null, url);
      } else {
        history.pushState(null, null, url);
      }
      
      this.handleRoutingEvent();
    } catch (e) {
      window.location[replace ? 'replace' : 'assign'](url);
    }
  }
  
  replace(path) {
    if (this.mode === 'hash') {
      this.replaceHash(path);
    } else {
      this.replaceState(path);
    }
  }
  
  replaceState(url) {
    this.pushState(url, true);
  }
  
  replaceHash(path) {
    window.location.replace(`${window.location.href.replace(/#(.*)$/, '')}#${path}`);
  }

  // 實現獲取路徑功能
  //👇 新增代碼
  getPath() {
      let path = '';
      if (this.mode === 'history') {
        path = this.clearSlashes(decodeURI(window.location.pathname));
        path = this.base !== '/' ? path.replace(this.base, '') : path;
      } else {
        const match = window.location.href.match(/#(.*)$/);

        path = match ? match[1] : '';
      }

      // 可能還有多餘斜槓,所以須要再清除一遍
      return this.clearSlashes(path);
    };


  clearSlashes(path) {
    return path.toString().replace(/\/$/, '').replace(/^\//, '');
  }
  
  /// 👆 新增代碼
}

export default MiniRouter;

實現監聽路由事件+執行路由回調

在實例化路由時,咱們將會按照mode的不一樣,在頁面上掛載不一樣的事件監聽器:

  • hash:對hashchange事件進行監聽
  • history:對popstate事件進行監聽

在監聽到變化後,回調方法將會遍歷咱們的路由表,若是符合路由的正則表達式,就執行相關路由的回調方法。

class MiniRouter {
  constructor(options) {
    const { mode, routes, base } = options;
    
    this.mode = mode || (window.history.pushState ? 'history' : 'hash');
    this.routes = routes || [];
    this.base = base || '/';
    
    this.setupListener(); // 👈 新增代碼
  }
  
  addRoute(routeConfig) {
    this.routes.push(routeConfig);
  }
  
  go(n) {
    window.history.go(n);
  }
  
  back() {
    window.location.back();
  }
  
  forward() {
    window.location.forward();
  }

  push(url) {
    if (this.mode === 'hash') {
      this.pushHash(url);
    } else {
      this.pushState(url);
    }
  }
  
  pushHash(path) {
    window.location.hash = path;
  }
  
  pushState(url, replace) {
    const history = window.history;

    try {
      if (replace) {
        history.replaceState(null, null, url);
      } else {
        history.pushState(null, null, url);
      }
      
      this.handleRoutingEvent();
    } catch (e) {
      window.location[replace ? 'replace' : 'assign'](url);
    }
  }
  
  replace(path) {
    if (this.mode === 'hash') {
      this.replaceHash(path);
    } else {
      this.replaceState(path);
    }
  }
  
  replaceState(url) {
    this.pushState(url, true);
  }
  
  replaceHash(path) {
    window.location.replace(`${window.location.href.replace(/#(.*)$/, '')}#${path}`);
  }

  getPath() {
      let path = '';
      if (this.mode === 'history') {
        path = this.clearSlashes(decodeURI(window.location.pathname));
        path = this.base !== '/' ? path.replace(this.base, '') : path;
      } else {
        const match = window.location.href.match(/#(.*)$/);

        path = match ? match[1] : '';
      }

      // 可能還有多餘斜槓,所以須要再清除一遍
      return this.clearSlashes(path);
    };


  clearSlashes(path) {
    return path.toString().replace(/\/$/, '').replace(/^\//, '');
  }
  // 實現監聽路由,及處理回調功能
  //👇 新增代碼
  setupListener() {
    this.handleRoutingEvent();

    if (this.mode === 'hash') {
      window.addEventListener('hashchange', this.handleRoutingEvent.bind(this));
    } else {
      window.addEventListener('popstate', this.handleRoutingEvent.bind(this));
    }
  }

  handleRoutingEvent() {
    if (this.current === this.getPath()) return;
    this.current = this.getPath();

    for (let i = 0; i < this.routes.length; i++) {
      const match = this.current.match(this.routes[i].path);
      if (match) {
        match.shift();
        this.routes[i].cb.apply({}, match);

        return;
      }
    }
  }
  /// 👆 新增代碼
}

export default MiniRouter;

試試剛剛實現的路由

實例化以前實現的MiniRouter,是否是和日常寫的router很像(除了功能少了不少😝)?

相關代碼以下:

import MiniRouter from './MiniRouter';

const router = new MiniRouter({
    mode: 'history',
    base: '/',
    routes: [
        {
            path: /about/,
            cb() {
                app.innerHTML = `<h1>這裏是關於頁面</h1>`;
            }
        },
        {
            path: /news\/(.*)\/detail\/(.*)/,
            cb(id, specification) {
                app.innerHTML = `<h1>這裏是新聞頁</h1><h2>您正在瀏覽id爲${id}<br>渠道爲${specification}的新聞</h2>`;
            }
        },
        {
            path: '',
            cb() {
                app.innerHTML = `<h1>歡迎來到首頁!</h1>`;
            }
        }
    ]
});

完整的代碼,請跳轉至:github傳送門

下載代碼後,執行下面的代碼,進行調試:

npm i
npm run dev

如何優化路由

path-to-regexp

常見的react-routervue-router傳入的路徑都是字符串,而上面實現的例子中,使用的是正則表達式。那麼如何才能作到解析字符串呢?

看看這兩個開源路由,咱們都不難發現,它們都使用了path-to-regexp這個庫。假如咱們傳入了一個路徑:

/news/:id/detail/:channel

使用match方法

import { match } from "path-to-regexp";

const fn = match("/news/:id/detail/:channel", {
  decode: decodeURIComponent
});

// {path: "/news/122/detail/baidu", index: 0, params: {id: "122", channel: "baidu"}}
console.log(fn("/news/122/detail/baidu")); 
// false
console.log(fn("/news/122/detail"));

是否是很眼熟?和咱們日常使用路由庫時,使用相關參數的路徑一致。有興趣的同窗,能夠沿着這個思路將路由優化一下

咱們發現,當知足咱們愈來愈多需求的時候,代碼庫也變得愈來愈龐大。可是最核心的內容,永遠只有那一些,主要抓住了主線,實際上分支的理解就會簡單起來。

寫在最後

本文的代碼主要參考自開源做者navigo的文章,在此基礎上,爲了貼合vue-router的相關配置。作了一些改動,因爲水平受限,文內若有錯誤,還望你們在評論區內提出,以避免誤人子弟。

參考資料

相關文章
相關標籤/搜索