带你了解路由的底层原理,用原生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 标准参考教程

相关文章
相关标签/搜索