首页 > 杂记 > 正文

1、Web路由需要实现的目标

上一篇文章中我们谈到了SPA(Single-page application)的出现,但SPA的应用有个需要解决的问题,就是浏览器只加载记录了一个html,所以当刷新浏览器时js会重新执行,当前页面的内容便会丢失;页面跳转时浏览器不会向服务器发出新的页面请求,浏览器也就无法前进、后退页面。

所以前端web路由需要实现以下目标:

(1)能根据页面URL来获取不同的模块,但不发起新的页面请求;

(2)能监听URL的变化。

而hash和history这两种模式便是其实现原理。

2、hash模式

URL的hash属性是一个可读可写的字符串,该字符串是URL的锚部分(即#后面的部分)。例如http://abc.com/#fragment,fragment便是hash值。

‘#’是用来指导浏览器动作的,对服务器完全无用,其值的改变不会导致浏览器发起http请求,也不会引起页面的重载。但每次hash值的改变,都会在浏览器的访问历史栈里增加一个记录,使用’后退’键便能返回上一个位置。在H5的history模式出现之前,hash是前端路由的实现方式。

核心API:

1、window.location.hash

是个可读可写属性,读取时可以校验hash的变化,写入时可以不重载页面修改浏览器记录

2、onhashchange事件

这是一个H5新增的事件,当#值发生变化时,就会触发这个事件。IE8+、Firefox 3.6+、Chrome 5+、Safari 4.0+支持该事件。

实现方式如下:

window.addEventListener(‘onhashchange’, func, false);

当浏览器不兼容时,可以用setInterval监控location.hash的变化。

核心功能的简单实现:

首先要实现一个router对象来管理页面的回调,

1
2
3
4
5
6
7
8
9
10
11
12
class HashRouter{
    constructor(routeArr = []){
        // 管理页面的回调
        this.routers = {};
        routeArr.forEach(item => this.register(item));
    }
    // 注册
    register(item){
        const {name, content} = item;
        this.routers[name] = typeof content === 'function' ? content : function(){};
    }
}

然后添加hashchange事件的监听,定义事件触发时的回调函数,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class HashRouter{
    constructor(routeArr = []){
        // 管理页面的回调
        this.routers = {};
        routeArr.forEach(item => this.register(item));
                     window.addEventListener('hashchange',this.load.bind(this),false);
    }
    // 注册
    register(item){
        const {name, content} = item;
        this.routers[name] = typeof content === 'function' ? content : function(){};
    }
 
    load(){
        let hash = location.hash.slice(1),
            handler = this.routers[hash];
        // 执行注册的回调函数
        try{
            handler.apply(this);
        }catch(e){
            console.error(e);
        }
    }
}

最后添加上对象的初始化和页面内容,

1
2
3
4
5
6
7
const container = document.getElementById('container');
const routeArr = [
{name: 'index', content: ()=> container.innerHTML = '这是首页'},
{name: 'about', content: ()=> container.innerHTML = '这是关于页'},
{name: 'detail', content: ()=> container.innerHTML = '这是详情页'}
];
cosnt router = new HashRouter(routeArr);
1
2
3
4
5
6
7
8
<body>
  <div id="header">
    <a href="#index">index</a>
    <a href="#about">about</a>
    <a href="#detail">detail</a>
  </div>
  <div id="container"></div>
</body>

当点击页面上的按钮时,页面内容便会变换,这样就基本介绍了hash模式下路由的实现原理。接下来介绍一下history模式。

3、history模式

history接口允许操作浏览器曾经在标签页或者框架里访问的会话历史记录。在H5之前其实存在history接口了,但只是用于页面的跳转,比如:

1
2
3
4
history.go(-1);       // 后退一页
history.go(2);        // 前进两页
history.forward();     // 前进一页
history.back();      // 后退一页

在H5规范中引入了三个新的API,

1
2
3
4
5
6
// 按指定的名称和URL(如果提供该参数)将数据push进会话历史栈
history.pushState();
// 按指定的数据,名称和URL(如果提供该参数),更新历史栈上最新的入口
history.replaceState();
// 返回当前状态对象
history.state

因为pushState和replaceState都可以改变URL的同时,不引起页面重载,所以history符合了目标一的条件。

回顾hash模式,在hash被改变时会触发hashchange事件,而window上也有一个popstate事件。当活动历史记录条目更改时,将触发popstate事件。然而调用history.pushState()/history.replaceState()不会触发popstate事件,只有在做出浏览器动作时,才会触发该事件,比如用户点击浏览器的回退/前进按钮,或者在JS代码中调用history.back()/history.forward()方法。

既然pushState和replaceState不会触发事件,那么我们需要换个思路来监听URL的变化。在单页应用中能改变URL的操作其实可以归为以下几种:

  1. 点击浏览器的前进或后退按钮;
  2. 点击 a 标签;
  3. 在JS代码中触发history.pushState函数;
  4. 在JS代码中触发history.replaceState函数;

只要我们能控制以上的操作,就可以实现history模式的路由管理了。核心功能的简单实现如下:

首先创建一个router对象,并添加popstate事件监听,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class HistoryRouter{
    constructor(routeArr = []){
        // 管理页面的回调
        this.routers = {};
        routeArr.forEach(item => this.register(item));
 
        this.listenPopState();
    }
    // 注册
    register(item){
        const {path, content} = item;
        this.routers[path] = typeof content === 'function' ? content : function(){};
    }
 
    // 监听popstate事件,点击浏览器的前进后退按钮触发
    listenPopState(){
        window.addEventListener('popstate',(e)=>{             
            let state = e.state || {},
                path = state.path || '';
                this.load(path);
        },false)
    }
 
    load(path){
        let handler = this.routers[path];
        // 执行注册的回调函数
        try{
            handler.apply(this);
        }catch(e){
            console.error(e);
        }
    }
}

然后添加对a标签的劫持,

1
2
3
4
5
6
7
8
9
10
// 全局监听a标签的点击事件     
listenALink(){
    window.addEventListener('click',(e)=>{
        let dom = e.target;
        if(dom.tagName.toUpperCase() === 'A' && dom.getAttribute('href')){
           e.preventDefault(); // 阻止原生事件
           this.push(dom.getAttribute('href'));
        }
    },false)
}

再添加pushState和replaceState的实现,

1
2
3
4
5
6
7
8
9
10
// 跳转到path     
push(path){
    history.pushState({path},null,path);
    this.load(path); // 需要手动加载页面回调
}
// 替换为path
replace(path){
    history.replaceState({path},null,path);
    this.load(path);
}

最后添加上对象的初始化和页面内容,

1
2
3
4
5
6
7
8
9
10
11
12
const container = document.getElementById('container');
 
const routeArr = [
{path: '/index', content: ()=> container.innerHTML = '这是首页'},
{path: '/about', content: ()=> container.innerHTML = '这是关于页'},
{path: '/detail', content: ()=> container.innerHTML = '这是详情页'}
];
cosnt router = new HistoryRouter(routeArr);
 
document.getElementById('push_btn').onclick = () => router.push('/detail');
 
document.getElementById('replace_btn').onclick = () => router.replace('/detail');
1
2
3
4
5
6
7
8
9
10
<body>
  <div id="header">
    <a href="/index">index</a>
    <a href="/about">about</a>
    <a href="/detail">detail</a>
  </div>
  <div id="container"></div>
  <div id="push_btn"></div>
  <div id="replace_btn"></div>
</body>

最后提一点,由于history是通过改变URL来进行路由的,当刷新页面时浏览器会向服务器访问当前地址,而服务器上不存在该页面,所以会出现404。为解决这个问题,我们需要修改web服务器的配置,让其在匹配不到页面时返回单页应用的页面。

4、memory模式

SPA的路由模式还有一种叫memory的模式,其特点是内容变化,但URL始终不变。由于其不符合上述的目标,所以这里只是简单介绍其实现原理。实现方式就是利用window.localstorage保存当前的路径,根据路径映射出页面内容。合适的使用场景比如react-native。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
const routes = {
  "/index": '这是首页',
  "/about": '这是关于页',
  "/detail": '这是详情页',
};
 
const container = document.getElementById('container');
 
function route() {
  let href = window.localStorage.getItem('cur-route');
 
  if (!href) {
    href = "/index";
  }
 
  // 展示内容
  container.innerHTML = routes[href];
}
 
// 获取到所有class为link的a标签
const allA = document.querySelectorAll('a.link');
// 遍历a标签
for (let a of allA) {
  a.addEventListener('click', (e) => {
    e.preventDefault();
    const href = a.getAttribute('href');
    window.localStorage.setItem('cur-route', href);
    // 通知变化
    onStateChange();
  });
}
 
function onStateChange() {
  route();
}
 
// 初始化
route();

5、结语

下面总结一下几种方式的优缺点:

hash模式兼容性更好,且不需要服务器配合修改,但SEO不友好,并且hash模式的地址比较丑陋。

history模式对于SEO更友好,但需要服务端进行配置,并且IE8及以下不支持。

memeory模式的路由信息保存在内存中,浏览器的前进后退操作无效,更适合运用在单机应用中。

以上便是web路由管理的几种常见实现方式,实现过程比较粗糙,希望能有助于大家在使用现代优秀的路由组件,如vue-router、react-router时能更好的运用在项目中。

至此,我们了解到了web路由是如何去实现路由管理的,那么,就请期待我们下一篇文章《大前端开发中的路由管理之三:Android篇》吧,下篇文章将为大家揭秘Android端是如何去做路由管理的。

版权声明:部分文章、图片等内容为用户发布或互联网整理而来,仅供学习参考。如有侵犯您的版权,请联系我们,将立刻删除。