最近做的业务展现在 app 重的 webview 里,app 提供的 webview 框架是默认有底部返回按钮的。pm 希望前端外部页面的框架保持不变,框架中展现的卡片页面可以点击返回按钮进行切换返回。于是想到了用一个简易的前端 router 来实现。
需求
我们这里其实只是想要控制浏览器的 history, 有 push、replace、back 功能就好了,看上去并不需要一个完整的router,但防止之后有需要,页面匹配也加上了。
原理
使用 hash 模式实现前端 router 的原理主要是利用了 url 中 hash 的以下特性:
- hash 变化时,浏览器不会刷新页面。
- hash 变化时,会在浏览器中产生一条浏览记录。
- hash 的变化可以通过 hashchange 事件进行监听。
实现
现在我们分别来看看各个功能是如何实现的
页面控制
add(re, handler) {
if (typeof re === 'function') {
handler = re;
re = '';
}
this.routes.push({re, handler});
return this;
},
check(f) {
const fragment = f || this.getFragment();
for (let i = 0; i < this.routes.length; i++) {
const match = fragment.match(this.routes[i].re);
if (match) {
match.shift();
this.routes[i].handler.apply({}, match);
return this;
}
}
return this;
},
listen() {
let current = this.getFragment();
window.addEventListener('hashchange', () => {
if (current !== this.getFragment()) {
current = this.getFragment();
this.check(current);
}
});
return this;
},
复制代码
页面控制主要是通过三个函数来实现的。
add
函数用来添加一个页面,一个页面通过一个正则和对应的 handler 来定义。
check
函数在 hash 变化时进行正则匹配找到当前 hash 值对应的 handler 执行。
listen
函数监听 hash 的变化,调用 check 函数。
push
push(path = '') {
window.location.hash = path;
return this;
},
复制代码
push 方法通过直接改变 window.location.hash 的值来实现,这个和在浏览器中手动改变 hash 是一样的。
replace
replace(path = '') {
const i = window.location.href.indexOf('#');
window.location.replace(
window.location.href.slice(0, i >= 0 ? i : 0) + '#' + path
);
},
复制代码
replace 的实现利用了 indow.location.replace
方法,这里需要只进行 hash 的替换。
back
back() {
window.history.back();
}
复制代码
back 更加简单。
code
/**
* @file simple router
* @author meixuguang
*/
export default {
routes: [],
getFragment() {
let fragment = '';
const match = window.location.href.match(/#(.*)$/);
fragment = match ? match[1] : '';
return this.clearSlashes(fragment);
},
clearSlashes(path) {
return path.toString().replace(/\/$/, '').replace(/^\//, '');
},
add(re, handler) {
if (typeof re === 'function') {
handler = re;
re = '';
}
this.routes.push({re, handler});
return this;
},
remove(param) {
for (let i = 0, r; i < this.routes.length, r = this.routes[i]; i++) {
if (r.handler === param || r.re.toString() === param.toString()) {
this.routes.splice(i, 1);
return this;
}
}
return this;
},
flush() {
this.routes = [];
return this;
},
check(f) {
const fragment = f || this.getFragment();
for (let i = 0; i < this.routes.length; i++) {
const match = fragment.match(this.routes[i].re);
if (match) {
match.shift();
this.routes[i].handler.apply({}, match);
return this;
}
}
return this;
},
listen() {
let current = this.getFragment();
window.addEventListener('hashchange', () => {
if (current !== this.getFragment()) {
current = this.getFragment();
this.check(current);
}
});
return this;
},
push(path = '') {
window.location.hash = path;
return this;
},
replace(path = '') {
const i = window.location.href.indexOf('#');
window.location.replace(
window.location.href.slice(0, i >= 0 ? i : 0) + '#' + path
);
},
back() {
window.history.back();
},
go(n) {
window.history.go(n);
}
};