前端路由实现 history|hash

2,160 阅读4分钟

前言

在技术的世界,没有奇迹,只有精妙的,令人咂舌的技术运用。 ---- 南方小菜语
看到一句话,前端的革命性事件:ajax实现主动请求局部刷新,路由控制权的掌控;前者很好理解,后者越觉得很让人惊喜,以往自己开发项目的固态思维:

  1. 后端服务器定义两类接口,页面跳转、数据返回;
  2. 前端获取数据并渲染页面,关注界面及用户体验;
  3. 再加上一些数据库加密避免http无状态而利用session等等blablabla的小点。 但前端路由横空出世,顿时天雷地火,把我后端的那点小而傻的沾沾自喜轰炸的渣都不剩,一个新事物的流行,必然在于它解决的一些问题,前端路由指出了传统的三大问题:
  • 页面跳转白屏,刷新缓慢
  • 在某些场合中,用ajax请求,可以让页面无刷新,页面变了但Url没有变化,用户就不能复制到想要的地址
  • 加大服务器的压力,代码冗合。 于是一个全栈项目成为了这样的实现:
  1. 后端服务器定义数据返回API;
  2. 前端构建SPA应用,调用接口并基于MVVM进行双向数据绑定渲染
  3. 前端路由实现无刷新内容更新

一如前端路由深似海,从此再无进度条

当然,前端路由也存在缺陷:使用浏览器的前进,后退键时会重新发送请求,来获取数据,没有合理地利用缓存。但总的来说,现在前端路由已经是实现路由的主要方式了。

思路实现

首先先不聊怎么实现,先思考

需求是什么:前端无刷新更新挂载节点的内容
变化点是什么: location

即点击链接后url发生变化,每个变化对应一个挂载点的内容(很自然的,我们需要一个路由表,即路径与挂载点内容的k-v,可用通过json实现)

找到合理的钩子函数: (现而今主要是hashchange/popstate)

技术实现

传统后端路由每次跳转都刷新页面,另发起一个新的请求,会给用户带来的白屏、耗时等较差体验。因此前端路由采用的是立即加载的方式,不再向服务器请求,而是加载路由对应的组件;而这种思路的实现主要采用两种方案:hashchange 以及 history

  • hash
    1. 基於hashchange事件,通過window.location.hash 获取地址上的hash值
    2. 通过构造Router类,构造传参配置routes对象设置hash与组件内容的对应
  • history
    1. 借助vue的mvvm,通过vue中的data的current来设置要渲染的router-view,从而达到动态的spa

history

html

<div>
        <a href="javascript:;" data-href="/">home</a>
        <a href="javascript:;" data-href="/book">book</a>
        <a href="javascript:;" data-href="/movie">movie</a>
        <div id="content"></div>
    </div>

js

//路由类
class Router {
    constructor(opts) {
        //路由表
        this.routes = {},
            this.init();
        this.bindEvent();
        opts.forEach(item => {
            this.route(item.path, () => {
                document.getElementById('content').innerHTML = item.component;
            })
        })
    }
    init() {
        //页面初始化时渲染路由
        window.addEventListener('load', this.updateView.bind(this));
        // 当活动历史记录条目更改时, 将触发popstate事件。
        // 如果被激活的历史记录条目是通过对history.pushState() 的调用创建的,
        // 或者受到对history.replaceState() 的调用的影响,
        //  popstate事件的state属性包含历史条目的状态对象的副本。
        window.addEventListener('popstate', this.updateView.bind(this));
    }
    // 路由渲染
    updateView() {
        const currentUrl = window.location.pathname || '/';
        this.routes[currentUrl] && this.routes[currentUrl]();
    }
    // 配置路由
    route(path, fn) {
        this.routes[path] = fn;
    }
    push(url){
        window.history.pushState({},null,url);
        this.updateView()
    }
    // 为超链接绑定事件
    bindEvent() {
        const _this = this;
        const links = document.getElementsByTagName('a');
        [].forEach.call(links, link => {
            link.addEventListener('click', function () {
                const url = this.getAttribute('data-href');
                console.log(url);
                
                _this.push(url);
            })

        })
    }
}
//实例化路由
const router = new Router([{
            path: '/',
            component: 'home'
        }, {
            path: '/movie',
            component: 'movie'
        }, {
            path: '/book',
            component: 'book'
        }])

hash

hash也存在下面几个特性:

URL中hash值只是客户端的一种状态,也就是说当向服务器端发出请求时,hash部分不会被发送。 hash值的改变,都会在浏览器的访问历史中增加一个记录。因此我们能通过浏览器的回退、前进按钮控制hash的切换。 我们可以使用hashchange事件来监听hash的变化。

出发hsah变化的方式也有两种,一种是通过a标签,并设置href属性,当用户点击这个标签后,URL就会发生改变,也就会触发hashchange事件了

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>

<body>
    <a href="#/" data-href="/">home</a>
    <a href="#/book" data-href="/">book</a>
    <a href="#/movie" data-href="/">movie</a>
    <div id="content">

    </div>
    <script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
    <script>
        // window.onload = function (params) {
        //     window.location.href += '#/' 
        // }

        const Home = {
            template: '<div>home</div>'
        }
        const Book = {
            template: '<div>book</div>'
        }
        const Movie = {
            template: '<div>movie</div>'
        }
        class Router {
            constructor(opts) {
                // this.path = opts.path;
                // this.component = opts.component;
                // this.routes = opts.routes;
                this.routes = {

                }
                // console.log(opts);
                
                opts.forEach(item => {
                    this.route(item.path,()=>{
                        document.getElementById('content').innerHTML = item.component;
                    })
                })
                console.log(this.routes);
                
                this.init()
            }
            bindEvent() { }
            init() {
                window.addEventListener('load',this.updateView.bind(this))
                window.addEventListener('hashchange', this.updateView.bind(this))

            }
            updateView(e) {
                // console.log(e,'updated');
                // console.log(e.newURL.indexOf(e.oldURL));

                // console.log(e.newURL.substring(e.newURL.indexOf(e.oldURL)));
                const hashTag = window.location.hash.slice(1) || '/'
                console.log(window.location.hash.slice(1));
                this.routes[hashTag] && this.routes[hashTag]()

            }
            route(path,cb){
                this.routes[path] = cb;
            }
        }
        new Router([
            {
                path: '/',
                component: 'home',
            },
            {
                path: '/book',
                component: 'book'
            },
            {
                path: '/movie',
                component: 'movie'
            }
        ])
    </script>
</body>

</html>