帶你瞭解路由的底層原理,用原生js手寫一個路由

路由的基本功能是爲了保證視圖和URL的同步,而視圖能夠當作是資源的一種表現。javascript

目前,前端的主流Vue、React、Angular這些,他們都提供了有關路由的插件,通常來講,這些路由插件老是提供兩種不一樣方式的路由方式: Hash 和 History。具體內容將會在下文中提到,下面就讓咱們圍繞這兩種方式來簡單手寫一個路由:html

一、Hash模式

Hash的基本概念:Hash 方法是在路由中帶有一個 #,主要原理是經過監聽 # 後的 URL 路徑標識符的更改而觸發的瀏覽器 hashchange 事件,而後經過獲取 location.hash 獲得當前的路徑標識符,再進行一些路由跳轉的操做,參見 MDN前端

首先咱們先創建一個index.html文件,在body標籤中開始hash的編寫:java

<button type="button" onclick="history.go(-1)">返回</button>
<h2>push模式</h2>
<ul>
    <li onclick="Router.push('/')">首頁</li>
    <li onclick="Router.push('/news')">新聞</li>
    <li onclick="Router.push('/product')">產品</li>
</ul>
<div id="app"></div>
<script>
    var app = document.getElementById("app");

    function RouterClass() {
        this.routes={};
        this.curUrl=""; //初始url
        this.eventHashRouter();
    }

    RouterClass.prototype.route = function(path, callback) {
        this.routes[path] = callback || function(){}
    }

    // 監聽hash模式路由
    RouterClass.prototype.eventHashRouter = function() {
        window.addEventListener("hashchange", this.hashRouter.bind(this))
    }

    RouterClass.prototype.hashRouter = function() {
        this.curUrl = window.location.hash.slice(1) || '/';
        // console.log(this.curUrl);
        this.routes[this.curUrl]();
    }

    // push模式頁面跳轉
    RouterClass.prototype.push = function(url) {
        console.log(url);
        url = "#" +url;
        window.location.href = url;
    }

    var Router = new RouterClass() //初始化 使用

    // 構造一個函數,根據url 改變 #app 中的內容,對頁面進行渲染
    Router.route('/', function() {
        app.innerHTML="首頁"
    })
    Router.route('/news', function() {
        app.innerHTML="新聞頁面"
    })
    Router.route('/product', function() {
        app.innerHTML="產品頁面"
    })
</script>
複製代碼

這樣,hash模式的路由就基本實現啦,正常的話,點擊首頁、新聞、產品應該能夠在 #app 中顯示相關信息了,可是這裏有小個bug, 就是一但刷新頁面,數據就會丟失。另外push自己會記錄你的點擊記錄,當你想要經過返回按鈕返回時,它會根據你的點擊記錄來返回,這樣就讓你沒法實現像 在點擊許多頁面後,想經過返回按鈕直接返回首頁的功能,這裏就要用到replace來解決了。瀏覽器

接下來就讓咱們來解決上面的兩個問題,請看下面的代碼:bash

<h2>replace模式</h2>
<ul>
    <!-- hash模式 就是帶 # 號的 -->
    <li onclick="Router.replace('/')">首頁</li>
    <li onclick="Router.replace('/news')">新聞</li>
    <li onclick="Router.replace('/product')">產品</li>
</ul>
複製代碼
// 監聽hash模式路由
RouterClass.prototype.eventHashRouter = function() {
    // 監聽load事件,防止刷新頁面數據丟失
    window.addEventListener("load", this.hashRouter.bind(this));
    window.addEventListener("hashchange", this.hashRouter.bind(this))
}

//replace模式頁面跳轉
RouterClass.prototype.replace = function(url) {
    url = "#" +url;
    window.location.replace(url);
}
複製代碼

恭喜恭喜,問題解決啦,至此,路由的hash模式就圓滿結束嘍。服務器

二、history模式

接下來就讓咱們來看看路由的另一個 history 模式吧app

咱們來修改下咱們如今的代碼:函數

<script>
    // baseUrl 是根路徑
    var app = document.getElementById("app"), baseUrl = "/router/";

    function RouterClass(opts) {
        this.routes={};
        this.curUrl="";
        this.mode=""; 
        if(opts){
            this.mode=opts.mode;
            if(this.mode==='history'){
                this.eventHistoryRouter();
            }else{
                this.eventHashRouter();
            }
        }else {
            this.eventHashRouter();
        }
    }

    RouterClass.prototype.route = function(path, callback) {
        this.routes[path] = callback || function(){}
    }

    // 監聽hash模式路由
    RouterClass.prototype.eventHashRouter = function() {
        // 監聽load事件,防止刷新頁面數據丟失
        window.addEventListener("load", this.hashRouter.bind(this));
        window.addEventListener("hashchange", this.hashRouter.bind(this))
    }

    //hash模式
    RouterClass.prototype.hashRouter = function() {
        this.curUrl = window.location.hash.slice(1) || '/';
        // console.log(this.curUrl);
        this.routes[this.curUrl]();
    }

    // history模式
    RouterClass.prototype.historyRouter = function() {
        this.curUrl = window.location.pathname;
        this.routes[this.curUrl]();
    }

    // 監聽history模式
    RouterClass.prototype.eventHistoryRouter = function() {
        window.addEventListener("load", this.historyRouter.bind(this));
        // 監聽回退事件  打個比方:就是你點瀏覽器那個返回的箭頭按鈕時觸發的事件
        window.addEventListener("popstate", this.historyRouter.bind(this));
    }

    // push模式頁面跳轉
    RouterClass.prototype.push = function(url) {
        if(this.mode === 'history') {
            window.history.pushState({},null,url);
            this.routes[url]();
        }else{
            url = "#" +url;
            window.location.href = url;
        }
    }

    //replace模式頁面跳轉
    RouterClass.prototype.replace = function(url) {
        if(this.mode==='history'){
            window.history.replaceState({},null,url);
            this.routes[url]();
        }else {
            url = "#" + url;
            window.location.replace(url);
        }
    }

    var Router = new RouterClass({
        mode:"history"  //hash:帶#號,history:不帶#號
    });

    // 構造一個函數,根據url 改變 #app 中的內容,對頁面進行渲染
    Router.route(baseUrl,function(){
        app.innerHTML="首頁"
    })
    Router.route(baseUrl+'news',function(){
        app.innerHTML="新聞頁面"
    })
    Router.route(baseUrl+'product',function(){
        app.innerHTML="產品頁面"
    })
</script>
複製代碼

我再來補充解釋一下history模式下頁面跳轉用到的 history.pushState() 和 history.replaceState() 方法,這兩個是HTML5新引入的方法,它們分別能夠添加和修改歷史記錄條目,更詳細的能夠參考一下MDN上的相關解釋:MDN學習

另外,再提示一下,若是你直接文件夾下雙擊打開 html 就會報一個這樣的錯:

這須要用服務器來運行,把html文件丟到服務器上就沒事了,我丟到了Apache上再用Apache運行,錯誤就解決了。

你覺得到這就結束了嗎,其實還有bug,你若是F5刷新的話,它就會給你報一個經典的 404 Not Found

這個問題又該如何解決呢,Apache 的話能夠經過配置.htaccess解決:

<IfModule mod_rewrite.c>
  Options Indexes FollowSymLinks ExecCGI
  RewriteEngine On
  RewriteBase /
  RewriteCond %{REQUEST_FILENAME} !-f
  RewriteCond %{REQUEST_FILENAME} !-d
  RewriteRule . /router/index.html [L]

    #ErrorDocument 404 /router/index.html
</IfModule>
複製代碼

你要改的就是RewriteRule這裏,也就是url重寫,把這個保存爲.htaccess文件後跟你的html一塊兒放在Apache文件夾裏面,如:

我在router文件夾中放入了這兩項。

終於,咱們完成了使用原生js實現路由,恭喜恭喜。

最後來把完整的代碼發一下吧:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>手寫router</title>
</head>
<body>
    <button type="button" onclick="history.go(-1)">返回</button>
    <h2>push模式</h2>
    <ul>
        <li onclick="Router.push(baseUrl)">首頁</li>
        <li onclick="Router.push(baseUrl+'news')">新聞</li>
        <li onclick="Router.push(baseUrl+'product')">產品</li>
    </ul>
    <h2>replace模式</h2>
    <ul>
        <li onclick="Router.replace(baseUrl)">首頁</li>
        <li onclick="Router.replace(baseUrl+'news')">新聞</li>
        <li onclick="Router.replace(baseUrl+'product')">產品</li>
    </ul>
    <div id="app"></div>
    <script>
        // baseUrl 是根路徑
        var app = document.getElementById("app"), baseUrl = "/router/";

        function RouterClass(opts) {
            this.routes={};
            this.curUrl="";
            this.mode=""; 
            if(opts){
                this.mode=opts.mode;
                if(this.mode==='history'){
                    this.eventHistoryRouter();
                }else{
                    this.eventHashRouter();
                }
            }else {
                this.eventHashRouter();
            }
        }

        RouterClass.prototype.route = function(path, callback) {
            this.routes[path] = callback || function(){}
        }

        // 監聽hash模式路由
        RouterClass.prototype.eventHashRouter = function() {
            // 監聽load事件,防止刷新頁面數據丟失
            window.addEventListener("load", this.hashRouter.bind(this));
            window.addEventListener("hashchange", this.hashRouter.bind(this))
        }

        //hash模式
        RouterClass.prototype.hashRouter = function() {
            this.curUrl = window.location.hash.slice(1) || '/';
            // console.log(this.curUrl);
            this.routes[this.curUrl]();
        }

        // history模式
        RouterClass.prototype.historyRouter = function() {
            this.curUrl = window.location.pathname;
            this.routes[this.curUrl]();
        }
    
        // 監聽history模式
        RouterClass.prototype.eventHistoryRouter = function() {
            window.addEventListener("load", this.historyRouter.bind(this));
            // 監聽回退事件  打個比方:就是你點瀏覽器那個返回的箭頭按鈕時觸發的事件
            window.addEventListener("popstate", this.historyRouter.bind(this));
        }

        // push模式頁面跳轉
        RouterClass.prototype.push = function(url) {
            if(this.mode === 'history') {
                window.history.pushState({},null,url);
                this.routes[url]();
            }else{
                url = "#" +url;
                window.location.href = url;
            }
        }

        //replace模式頁面跳轉
        RouterClass.prototype.replace = function(url) {
            if(this.mode==='history'){
                window.history.replaceState({},null,url);
                this.routes[url]();
            }else {
                url = "#" + url;
                window.location.replace(url);
            }
        }

        //初始化 使用
        var Router = new RouterClass({
            mode:"history"  //hash:帶#號,history:不帶#號
        });

        // 構造一個函數,根據url 改變 #app 中的內容,對頁面進行渲染
        Router.route(baseUrl,function(){
            app.innerHTML="首頁"
        })
        Router.route(baseUrl+'news',function(){
            app.innerHTML="新聞頁面"
        })
        Router.route(baseUrl+'product',function(){
            app.innerHTML="產品頁面"
        })
    </script>
</body>
</html>
複製代碼

到這裏就結束啦,但願你能經過本文有所收穫,個人文章都是學習過程當中的總結,若有錯誤,歡迎指出,感謝閱讀。

參考:

history | MDN

hashchange | MDN

Manipulating the browser history | MDN

History 對象 -- JavaScript 標準參考教程

相關文章
相關標籤/搜索