共计 8675 个字符,预计需要花费 22 分钟才能阅读完成。
常识储备
如何操作浏览器地址?
咱们能够应用上面三种办法,来批改浏览器的地址
- location.assign(url)
- window.location = url
- location.href = url(常见)
批改以下 location
对象的属性值,会导致以后页面从新加载
// 如果以后 url 为:https://www.example.com/
// 把 url 批改为:https://www.example.com/?t=example
location.search = '?t=example';
// 把 url 批改为:https://example.com/?t=example
location.hostname = 'example.com';
// 把 url 批改为:https://www.example.com/example
location.pathname = 'example';
// 把 url 批改为:https://www.example.com:8080
location.port = 8080
批改 hash
时,浏览器历史中会新减少一条记录,然而并不会刷新页面。因而 SPA 利用中,hash
也是一种切换路由的形式。
// 如果以后 url 为:https://www.example.com/
// 把 url 批改为:https://www.example.com/#example
location.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
: 路由的模式,能够抉择hash
或history
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 跳转
请留神:
为pushState
与replaceState
增加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 i
npm run dev
如何优化路由
path-to-regexp
常见的 react-router
和vue-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"));
// false
console.log(fn("/news/122/detail"));
是不是很眼生?和咱们平时应用路由库时,应用相干参数的门路统一。有趣味的同学,能够沿着这个思路将路由优化一下
咱们发现,当满足咱们越来越多需要的时候,代码库也变得越来越宏大。然而最外围的内容,永远只有那一些,次要抓住了主线,实际上分支的了解就会简略起来。
写在最初
本文的代码次要参考自开源作者 navigo 的文章,在此基础上,为了贴合 vue-router 的相干配置。做了一些改变,因为程度受限,文内如有谬误,还望大家在评论区内提出,免得误人子弟。
参考资料
- A modern JavaScript router in 100 lines:本文根底代码是基于该模板进行改写,该作者是开源库
navigo
的作者。倡议能够读一读原文,同样能够启发你的思维。 - vue-router
- JavaScript 高级程序设计(第四版)第 12 章:常识储备的局部内容摘抄起源
- MDN-History
- MDN-Location