常识储备

如何操作浏览器地址?

咱们能够应用上面三种办法,来批改浏览器的地址

  • location.assign(url)
  • window.location = url
  • location.href = url(常见)

批改以下location对象的属性值,会导致以后页面从新加载

// 如果以后url为:https://www.example.com/// 把url批改为:https://www.example.com/?t=examplelocation.search = '?t=example';// 把url批改为:https://example.com/?t=examplelocation.hostname = 'example.com';// 把url批改为:https://www.example.com/examplelocation.pathname = 'example';// 把url批改为:https://www.example.com:8080location.port = 8080

批改hash时,浏览器历史中会新减少一条记录,然而并不会刷新页面。因而SPA利用中,hash也是一种切换路由的形式。

// 如果以后url为:https://www.example.com/// 把url批改为:https://www.example.com/#examplelocation.hash = '#example';

应用location.replace(url)办法跳转的url,并不会减少历史记录。

应用location.reload()办法能够从新加载以后页面,是否传参的区别如下:

location.reload(); // 从新加载,可能是从缓存加载location.reload(true); // 从新加载,从服务器加载

如何导航页面?

应用go(n)能够在用户记录中沿任何方向导航(即能够后退也能够后退)。正值示意在历史中后退,负值示意在历史中后退。

如果要后退1页,那么能够应用window.history.`go(1)。同时,也能够应用window.history.forward()`来做雷同的事件。
如果要后退1页,那么能够应用window.history.`go(-1)。同时,也能够应用window.history.back()`来做雷同的事件。

如果应用window.history.go(0)window.history.go()都会从新加载以后页面。

如何扭转页面的地址,然而不会从新加载页面并且怎么监听这些扭转?

应用hash

下面咱们说到了,批改hash能够做到扭转页面的地址,在浏览器历史中增加一条记录,然而不会从新加载页面。

咱们同时能够配合hashchange事件,监听页面地址hash的变动。

应用history.pushState()、history.replaceState()

应用history.pushState(),相似是执行了location.href = url,然而并不会从新加载页面。如果用户执行了后退操作,将会触发popstate事件。

应用history.replaceState(),相似是执行了location.replace(url),然而并不会从新加载页面。

留神,执行pushState、replaceState办法后,尽管浏览器地址有扭转,然而并不会触发popState事件

实现一个mini-router

开始编写构造函数

首先咱们须要确定的是,咱们的路由应该须要上面4个属性:

  • routes:一个数组,蕴含所有注册的路由对象
  • mode: 路由的模式,能够抉择hashhistory
  • base:根门路
  • constructor:初始化新的路由实例
class MiniRouter {  constructor(options) {    const { mode, routes, base } = options;        this.mode = mode || (window.history.pushState ? 'history' : 'hash');    this.routes = routes || [];    this.base = base || '/';  }}export default MiniRouter;

减少增加路由对象办法

路由对象中蕴含上面两个属性

  • path:由正则表达式代表的门路地址(并不是字符串,前面会具体解释)
  • cb:路由跳转后执行的回调函数
class MiniRouter {  constructor(options) {    const { mode, routes, base } = options;        this.mode = mode || (window.history.pushState ? 'history' : 'hash');    this.routes = routes || [];    this.base = base || '/';  }    // 增加路由对象  新增代码  // routerConfig示例为:  // {path: /about/, cb(){console.log('about')}}  addRoute(routeConfig) {    this.routes.push(routeConfig);  }  ///  新增代码}export default MiniRouter;

减少路由导航性能

增加路由导航性能,实际上是location相干办法的封装

具体内容能够回看:如何导航页面?

class MiniRouter {  constructor(options) {    const { mode, routes, base } = options;        this.mode = mode || (window.history.pushState ? 'history' : 'hash');    this.routes = routes || [];    this.base = base || '/';  }    addRoute(routeConfig) {    this.routes.push(routeConfig);  }    // 增加后退、后退性能  新增代码  go(n) {    window.location.go(n);  }    back() {    window.location.back();  }    forward() {    window.location.forward();  }    ///  新增代码}export default MiniRouter;

实现导航到新路由的性能

参照vue-router,大橙子在这里设计了push、replace两种办法。其中:
push代表跳转新页面,并在历史栈中减少一条记录,用户能够后退
replace代表跳转新页面,然而不在历史栈中减少记录,用户不能够后退

如果是hash模式下
应用location.hash = newHash来实现push跳转
应用window.location.replace(url)来实现replace跳转

如果是history模式下
应用history.pushState()来实现push跳转
应用history.replaceState()来实现replace跳转

请留神:
pushStatereplaceState增加try...catch是因为Safari的某个安全策略

有趣味的同学能够查看
vue-router相干commit
Stack Overflow上的相干问题

class MiniRouter {  constructor(options) {    const { mode, routes, base } = options;        this.mode = mode || (window.history.pushState ? 'history' : 'hash');    this.routes = routes || [];    this.base = base || '/';  }    addRoute(routeConfig) {    this.routes.push(routeConfig);  }    go(n) {    window.history.go(n);  }    back() {    window.location.back();  }    forward() {    window.location.forward();  }    // 实现导航到新路由的性能  // push代表跳转新页面,并在历史栈中减少一条记录,用户能够后退  // replace代表跳转新页面,然而不在历史栈中减少记录,用户不能够后退  // 新增代码  push(url) {    if (this.mode === 'hash') {      this.pushHash(url);    } else {      this.pushState(url);    }  }    pushHash(path) {    window.location.hash = path;  }    pushState(url, replace) {    const history = window.history;    try {      if (replace) {        history.replaceState(null, null, url);      } else {        history.pushState(null, null, url);      }            this.handleRoutingEvent();    } catch (e) {      window.location[replace ? 'replace' : 'assign'](url);    }  }    replace(path) {    if (this.mode === 'hash') {      this.replaceHash(path);    } else {      this.replaceState(path);    }  }    replaceState(url) {    this.pushState(url, true);  }    replaceHash(path) {    window.location.replace(`${window.location.href.replace(/#(.*)$/, '')}#${path}`);  }  ///  新增代码}export default MiniRouter;

实现获取路由地址的性能

history模式下,咱们会应用location.path来获取以后链接门路。

如果设置了base参数,将会把base门路干掉,不便前面匹配路由地址。

hash模式下,咱们会应用正则匹配将#后的地址匹配进去。

当然所有操作之后,将会把/齐全去掉。

class MiniRouter {  constructor(options) {    const { mode, routes, base } = options;        this.mode = mode || (window.history.pushState ? 'history' : 'hash');    this.routes = routes || [];    this.base = base || '/';  }    addRoute(routeConfig) {    this.routes.push(routeConfig);  }    go(n) {    window.history.go(n);  }    back() {    window.location.back();  }    forward() {    window.location.forward();  }  push(url) {    if (this.mode === 'hash') {      this.pushHash(url);    } else {      this.pushState(url);    }  }    pushHash(path) {    window.location.hash = path;  }    pushState(url, replace) {    const history = window.history;    try {      if (replace) {        history.replaceState(null, null, url);      } else {        history.pushState(null, null, url);      }            this.handleRoutingEvent();    } catch (e) {      window.location[replace ? 'replace' : 'assign'](url);    }  }    replace(path) {    if (this.mode === 'hash') {      this.replaceHash(path);    } else {      this.replaceState(path);    }  }    replaceState(url) {    this.pushState(url, true);  }    replaceHash(path) {    window.location.replace(`${window.location.href.replace(/#(.*)$/, '')}#${path}`);  }  // 实现获取门路性能  // 新增代码  getPath() {      let path = '';      if (this.mode === 'history') {        path = this.clearSlashes(decodeURI(window.location.pathname));        path = this.base !== '/' ? path.replace(this.base, '') : path;      } else {        const match = window.location.href.match(/#(.*)$/);        path = match ? match[1] : '';      }      // 可能还有多余斜杠,因而须要再革除一遍      return this.clearSlashes(path);    };  clearSlashes(path) {    return path.toString().replace(/\/$/, '').replace(/^\//, '');  }    ///  新增代码}export default MiniRouter;

实现监听路由事件+执行路由回调

在实例化路由时,咱们将会依照mode的不同,在页面上挂载不同的事件监听器:

  • hash:对hashchange事件进行监听
  • history:对popstate事件进行监听

在监听到变动后,回调办法将会遍历咱们的路由表,如果合乎路由的正则表达式,就执行相干路由的回调办法。

class MiniRouter {  constructor(options) {    const { mode, routes, base } = options;        this.mode = mode || (window.history.pushState ? 'history' : 'hash');    this.routes = routes || [];    this.base = base || '/';        this.setupListener(); //  新增代码  }    addRoute(routeConfig) {    this.routes.push(routeConfig);  }    go(n) {    window.history.go(n);  }    back() {    window.location.back();  }    forward() {    window.location.forward();  }  push(url) {    if (this.mode === 'hash') {      this.pushHash(url);    } else {      this.pushState(url);    }  }    pushHash(path) {    window.location.hash = path;  }    pushState(url, replace) {    const history = window.history;    try {      if (replace) {        history.replaceState(null, null, url);      } else {        history.pushState(null, null, url);      }            this.handleRoutingEvent();    } catch (e) {      window.location[replace ? 'replace' : 'assign'](url);    }  }    replace(path) {    if (this.mode === 'hash') {      this.replaceHash(path);    } else {      this.replaceState(path);    }  }    replaceState(url) {    this.pushState(url, true);  }    replaceHash(path) {    window.location.replace(`${window.location.href.replace(/#(.*)$/, '')}#${path}`);  }  getPath() {      let path = '';      if (this.mode === 'history') {        path = this.clearSlashes(decodeURI(window.location.pathname));        path = this.base !== '/' ? path.replace(this.base, '') : path;      } else {        const match = window.location.href.match(/#(.*)$/);        path = match ? match[1] : '';      }      // 可能还有多余斜杠,因而须要再革除一遍      return this.clearSlashes(path);    };  clearSlashes(path) {    return path.toString().replace(/\/$/, '').replace(/^\//, '');  }  // 实现监听路由,及解决回调性能  // 新增代码  setupListener() {    this.handleRoutingEvent();    if (this.mode === 'hash') {      window.addEventListener('hashchange', this.handleRoutingEvent.bind(this));    } else {      window.addEventListener('popstate', this.handleRoutingEvent.bind(this));    }  }  handleRoutingEvent() {    if (this.current === this.getPath()) return;    this.current = this.getPath();    for (let i = 0; i < this.routes.length; i++) {      const match = this.current.match(this.routes[i].path);      if (match) {        match.shift();        this.routes[i].cb.apply({}, match);        return;      }    }  }  ///  新增代码}export default MiniRouter;

试试刚刚实现的路由

实例化之前实现的MiniRouter,是不是和平时写的router很像(除了性能少了很多)?

相干代码如下:

import MiniRouter from './MiniRouter';const router = new MiniRouter({    mode: 'history',    base: '/',    routes: [        {            path: /about/,            cb() {                app.innerHTML = `<h1>这里是对于页面</h1>`;            }        },        {            path: /news\/(.*)\/detail\/(.*)/,            cb(id, specification) {                app.innerHTML = `<h1>这里是新闻页</h1><h2>您正在浏览id为${id}<br>渠道为${specification}的新闻</h2>`;            }        },        {            path: '',            cb() {                app.innerHTML = `<h1>欢送来到首页!</h1>`;            }        }    ]});

残缺的代码,请跳转至:github传送门

下载代码后,执行上面的代码,进行调试:

npm inpm run dev

如何优化路由

path-to-regexp

常见的react-routervue-router传入的门路都是字符串,而下面实现的例子中,应用的是正则表达式。那么如何能力做到解析字符串呢?

看看这两个开源路由,咱们都不难发现,它们都应用了path-to-regexp这个库。如果咱们传入了一个门路:

/news/:id/detail/:channel

应用match办法

import { match } from "path-to-regexp";const fn = match("/news/:id/detail/:channel", {  decode: decodeURIComponent});// {path: "/news/122/detail/baidu", index: 0, params: {id: "122", channel: "baidu"}}console.log(fn("/news/122/detail/baidu")); // falseconsole.log(fn("/news/122/detail"));

是不是很眼生?和咱们平时应用路由库时,应用相干参数的门路统一。有趣味的同学,能够沿着这个思路将路由优化一下

咱们发现,当满足咱们越来越多需要的时候,代码库也变得越来越宏大。然而最外围的内容,永远只有那一些,次要抓住了主线,实际上分支的了解就会简略起来。

写在最初

本文的代码次要参考自开源作者navigo的文章,在此基础上,为了贴合vue-router的相干配置。做了一些改变,因为程度受限,文内如有谬误,还望大家在评论区内提出,免得误人子弟。

参考资料

  • A modern JavaScript router in 100 lines:本文根底代码是基于该模板进行改写,该作者是开源库navigo的作者。倡议能够读一读原文,同样能够启发你的思维。
  • vue-router
  • JavaScript高级程序设计(第四版)第12章:常识储备的局部内容摘抄起源
  • MDN-History
  • MDN-Location