深刻了解 HTML5 History API,前端路由的生成,解讀 webpack-dev-server 的 historyApiFallback 原理

深刻了解 HTML5 History API,前端路由的生成,解讀 webpack-dev-server 的 historyApiFallback 原理javascript

一、history

History 接口,容許操做瀏覽器的 session history,好比在當前tab下瀏覽的全部頁面或者當前頁面的會話記錄。html

history屬性 前端

在這裏插入圖片描述

一、length(只讀)vue

返回一個總數,表明當前窗口下的全部會話記錄數量,包括當前頁面。若是你在新開的一個tab裏面輸入一個地址,當前的length1,若是再輸入一個地址,就會變成2java

假設當前總數已是6,不管是瀏覽器的返回仍是 history.back(), 當前總數都不會改變。react

二、scrollRestoration(實驗性API)webpack

容許web應用在history導航下指定一個默認返回的頁面滾動行爲,就是是否自動滾動到頁面頂部;默認是 auto, 另外能夠是 manual(手動)web

三、 state (當前頁面狀態)json

返回一個任意的狀態值,表明當前處在歷史記錄`棧`裏最高的狀態。其實就是返回當前頁面的`state`,默認是 null
複製代碼

history 方法後端

History不繼承任何方法;

一、 back()

返回歷史記錄會話的上一個頁面,同瀏覽器的返回,同 history.go(-1)

二、forward()

前進到歷史會話記錄的下一個頁面,同瀏覽器的前進,同 history.go(1)

三、go()

session history裏面加載頁面,取決於當前頁面的相對位置,好比 go(-1) 是返回上一頁,go(1)是前進到下一個頁面。 若是你直接一個超過當前總length的返回,好比初始頁面,沒有前一個頁面,也沒有後一個頁面,這個時候 go(-1)go(1),都不會有任何做用; 若是你不指定任何參數或者go(0),將會從新加載當前頁面;

四、pushState(StateObj, title, url)

把提供的狀態數據放到當前的會話棧裏面,若是有參數的話,通常第二個是title,第三個是URL。 這個數據被DOM當作透明數據;你能夠傳任何能夠序列號的數據。不過火狐如今忽略 title 這個參數; 這個方法引發會話記錄length的增加。

五、replaceState(StateObj, title, url)

把提供的狀態數據更新到當前的會話棧裏面最近的入口,若是有參數的話,通常第二個是title,第三個是URL。 這個數據被DOM當作透明數據;你能夠傳任何能夠序列號的數據。不過火狐如今忽略 title 這個參數; 這個方法不會引發會話記錄length的增加。


綜上所述,pushStatereplaceState 是修改當前session history的兩個方法,他們都會觸發一個方法 onpopstate 事件;

history.pushState({demo: 12}, "8888", "en-US/docs/Web/API/XMLHttpRequest")
複製代碼

在這裏插入圖片描述
如圖 pushState 會改變當你在後面創建的頁面發起XHR請求的時候, 請求header裏面的 referrer;這個地址就是你在pushState裏面的URL;

另外URL en-US/docs/Web/API/XMLHttpRequest(並不是真實存在的URL), 在pushState完成以後,並不觸發頁面的從新加載或者檢查當前URL的目錄是否存在

只有當你此刻從這個頁面跳轉到 google.com, 而後再點擊返回按鈕,此時的頁面就是你如今pushState的頁面,state也會是當前的state, 也同時會加載當前的頁面資源,oops,此刻會顯示不存在;

在這裏插入圖片描述
replaceState 同理;

關於 onpopstate:

window.onpopstate = function(event) {
  alert("location: " + document.location + ", state: " + JSON.stringify(event.state));
};

history.pushState({page: 1}, "title 1", "?page=1");
history.pushState({page: 2}, "title 2", "?page=2");
history.replaceState({page: 3}, "title 3", "?page=3");
history.back(); // alerts "location: http://example.com/example.html?page=1, state: {"page":1}"
history.back(); // alerts "location: http://example.com/example.html, state: null
history.go(2);  // alerts "location: http://example.com/example.html?page=3, state: {"page":3}

複製代碼

二、兩種路由模式的生成

如下說明僅存在於當前路由是 history 模式; 說道 webpack-dev-serverhistoryApiFallback 就不得不說下 VUE 前端路由,路由跳轉原理;

傳統的web開發中,大可能是多頁應用,每一個模塊對應一個頁面,在瀏覽器輸入相關頁面的路徑,而後服務端處理相關瀏覽器的請求,經過HTTP把資源返回給客戶端瀏覽器進行渲染。

傳統開發,後端定義好路由的路徑和請求數據的地址;

隨着前端的發展,前端也承擔着愈來愈大的責任,好比Ajax局部刷新數據,前端能夠操控一些歷史會話,而不用每次都從服務端進行數據交互。

history.pushStatehistory.replaceState ,這兩個history新增的api,爲前端操控瀏覽器歷史棧提供了可能性

/** * @data {object} state對象 最大640KB, 若是須要存很大的數據,考慮 sessionStorage localStorage * @title {string} 標題 * @url {string} 必須同一個域下,相對路徑和絕對路徑均可以 */
history.pushState(data, title, url) //向瀏覽器歷史棧中增長一條記錄。
history.replaceState(data, title, url) //替換歷史棧中的當前記錄。

複製代碼

這兩個Api都會操做瀏覽器的歷史棧,而不會引發頁面的刷新。不一樣的是,pushState會增長一條新的歷史記錄,而replaceState則會替換當前的歷史記錄。所需的參數相同,在將新的歷史記錄存入棧後,會把傳入的data(即state對象)同時存入,以便之後調用。同時,這倆api都會更新或者覆蓋當前瀏覽器的titleurl爲對應傳入的參數。

// 假設當前的URL: http://test.com

history.pushState(null, null, "/login");
// http://test.com ---->>> http://test.com/login

history.pushState(null, null, "http://test.com/regiest");
// http://test.com ---->>> http://test.com/regiest


// 錯誤用法
history.pushState(null, null, "http://baidu.com/regiest");
// error 跨域報錯

複製代碼

也正是基於瀏覽器的hitroy,慢慢的衍生出來如今的前端路由好比vuehistory路由,reactBrowseHistory

==如今讓咱們手動寫一個history路由模式==:

Html

<div>
		<a href="javascript:;" data-link="/">login</a>
		<a href="javascript:;" data-link="/news">news</a>
		<a href="javascript:;" data-link="/contact">contact</a>
</div>
複製代碼

js

// history 路由
class HistoryRouter {
  constructor(options = {}) {
    // store all router
    this.routers = {};
    // 遍歷路由參數,保存到 this.routers
    if (options.router) {
      options.router.forEach(n => {
        this.routers[n.path] = () => {
          document.getElementById("content").innerHTML = n.component;
        }
      });
    }
    // 綁定到 this.routers
    this.updateContent = this.updateContent.bind(this);
    // 初始化事件
    this.init();
    this.bindClickEvent();
  }
  init() {
    // 頁面初始化的時候,初始化當前匹配路由
    // 監聽 load
    window.addEventListener('load', this.updateContent, false);
    // pushState replaceState 不能觸發 popstate 事件
    // 當瀏覽器返回前進或者刷新,都會觸發 popstate 更新
    window.addEventListener("popstate", this.updateContent, false);
  }
  // 更新內容
  updateContent(e) {
    alert(e ? e.type : "click");
    const currentPath = location.pathname || "/";
    this.routers[currentPath] && this.routers[currentPath]();
  }
  // 綁定點擊事件
  bindClickEvent() {
    const links = document.querySelectorAll('a');
    Array.prototype.forEach.call(links, link => {
      link.addEventListener('click', e => {
        const path = e.target.getAttribute("data-link");
        // 添加到session history
        this.handlePush(path);
      })
    });
  }
  // pushState 不會觸發 popstate
  handlePush(path){
    window.history.pushState({path}, null, path);
    this.updateContent();
  }
}
// 實例
new HistoryRouter({
  router: [{
    name: "index",
    path: "/",
    component: "Index"
  }, {
    name: "news",
    path: "/news",
    component: "News"
  }, {
    name: "contact",
    path: "/contact",
    component: "Contact"
  }]
});
複製代碼

第一次渲染的時候,會根據當前的 pathname 進行更新對應的 callback 事件,而後更新 content , 這個時候無需服務器的請求;

若是這個時候,咱們點擊瀏覽器的返回🔙前進按鈕,發現依然會依次渲染相關 content ,這就是history歷史堆棧的魅力所在。

在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
最後咱們發現當咱們切換到非loading page的時候,咱們刷新頁面,會報出 Get 404,這個時候就是請求了server , 卻發現不存在這個目錄的資源;

這個時候咱們就須要 historyApiFallback


三、historyApiFallback

Webpack-dev-server 的背後的是connect-history-api-fallback

關於 connect-history-api-fallback 中間件,解決這個404問題

單頁應用(SPA)通常只有一個index.html, 導航的跳轉都是基於HTML5 History API,當用戶在越過index.html 頁面直接訪問這個地址或是經過瀏覽器的刷新按鈕從新獲取時,就會出現404問題;

好比 直接訪問/login, /login/online,這時候越過了index.html,去查找這個地址下的文件。因爲這是個一個單頁應用,最終結果確定是查找失敗,返回一個404錯誤

這個中間件就是用來解決這個問題的

只要知足下面四個條件之一,這個中間件就會改變請求的地址,指向到默認的index.html:

1 GET請求

2 接受內容格式爲text/html

3 不是一個直接的文件請求,好比路徑中不帶有 .

4 沒有 options.rewrites 裏的正則匹配


connect-history-api-fallback 源碼:

'use strict';

var url = require('url');

exports = module.exports = function historyApiFallback(options) {
  options = options || {};
  var logger = getLogger(options);

  return function(req, res, next) {
    var headers = req.headers;
    if (req.method !== 'GET') {
      logger(
        'Not rewriting',
        req.method,
        req.url,
        'because the method is not GET.'
      );
      return next();
    } else if (!headers || typeof headers.accept !== 'string') {
      logger(
        'Not rewriting',
        req.method,
        req.url,
        'because the client did not send an HTTP accept header.'
      );
      return next();
    } else if (headers.accept.indexOf('application/json') === 0) {
      logger(
        'Not rewriting',
        req.method,
        req.url,
        'because the client prefers JSON.'
      );
      return next();
    } else if (!acceptsHtml(headers.accept, options)) {
      logger(
        'Not rewriting',
        req.method,
        req.url,
        'because the client does not accept HTML.'
      );
      return next();
    }

    var parsedUrl = url.parse(req.url);
    var rewriteTarget;
    options.rewrites = options.rewrites || [];
    for (var i = 0; i < options.rewrites.length; i++) {
      var rewrite = options.rewrites[i];
      var match = parsedUrl.pathname.match(rewrite.from);
      if (match !== null) {
        rewriteTarget = evaluateRewriteRule(parsedUrl, match, rewrite.to);
        logger('Rewriting', req.method, req.url, 'to', rewriteTarget);
        req.url = rewriteTarget;
        return next();
      }
    }

    if (parsedUrl.pathname.indexOf('.') !== -1 &&
        options.disableDotRule !== true) {
      logger(
        'Not rewriting',
        req.method,
        req.url,
        'because the path includes a dot (.) character.'
      );
      return next();
    }

    rewriteTarget = options.index || '/index.html';
    logger('Rewriting', req.method, req.url, 'to', rewriteTarget);
    req.url = rewriteTarget;
    next();
  };
};

function evaluateRewriteRule(parsedUrl, match, rule) {
  if (typeof rule === 'string') {
    return rule;
  } else if (typeof rule !== 'function') {
    throw new Error('Rewrite rule can only be of type string of function.');
  }

  return rule({
    parsedUrl: parsedUrl,
    match: match
  });
}

function acceptsHtml(header, options) {
  options.htmlAcceptHeaders = options.htmlAcceptHeaders || ['text/html', '*/*'];
  for (var i = 0; i < options.htmlAcceptHeaders.length; i++) {
    if (header.indexOf(options.htmlAcceptHeaders[i]) !== -1) {
      return true;
    }
  }
  return false;
}

function getLogger(options) {
  if (options && options.logger) {
    return options.logger;
  } else if (options && options.verbose) {
    return console.log.bind(console);
  }
  return function(){};
}
複製代碼

其實代碼也挺簡單的,最主要先符合上面四個原則,而後先匹配自定義rewrites規則,再匹配點文件規則;

getLogger, 默認不輸出,options.verbose若是爲true,則輸出,默認console.log.bind(console)

若是req.method != 'GET',結束 若是!headers || !headers.accept != 'string' ,結束 若是headers.accept.indexOf('application/json') === 0 結束

acceptsHtml函數a判斷headers.accept字符串是否含有['text/html', '/']中任意一個 固然不夠這兩個不夠你能夠自定義到選項options.htmlAcceptHeaders!acceptsHtml(headers.accept, options),結束

而後根據你定義的選項rewrites, 沒定義就至關於跳過了 按定義的數組順序,字符串依次匹配路由rewrite.from,匹配成功則走rewrite.to,to能夠是字符串也能夠是函數,結束

判斷dot file,即pathname中包含.(點),而且選項disableDotRule !== true,即沒有關閉點文件限制規則, 結束

rewriteTarget = options.index || '/index.html'

大體如此;

相關文章
相關標籤/搜索