先验
最近在看vue-router源码,先了解一下两种前端路由模式的具体实现。
hash路由:监听url的hash部分变化,渲染相应组件,hash是完全的前端路由,不会向后端发起请求。
hash路由的变化需要监听hashchange事件,事件对象里有oldURL、newURL属性表示变化前后的url
window.addEventListener("hashchange", function (e) {
console.log(e.oldURL, e.newURL);
})
封装一个函数,从获得的url里面,需要提取出hash部分
function getHashFromURL(url) {
let index = url.indexOf("#");
if (index >= 0) return url.slice(index + 1);
return "/";
}
当hash地址发生变化,需要重新获取当前hash,并触发回调
function refresh(event) {
let newhash = "";
let oldhash = null;
if (event.newURL) {
oldhash = this.getHashFromURL(event.oldURL);
newhash = this.getHashFromURL(event.newURL);
this.emit("change", oldhash, newhash);
} else {
// 第一次进入页面的时候不触发hashchange事件,所以事件对象里没有newURL属性
newhash = this.getHashFromURL(window.location.hash);
}
}
订阅事件
const handler = {};
function on(eventName,callback){
handler[eventName] = callback;
}
发布事件
function emit(eventName,...args){
let callback=handler[eventName];
if(callback) callback(...args);
}
创建HashRouter类
class HashRouter {
// 1 类里面要保存当前的hash地址,以及注册的回调
currentHash = "";
handler = {};
// 4 刚进入页面触发load事件,后续hash地址变化才会触发hashchange事件
constructor(){
this.refresh=this.refresh.bind(this);// 避免因为事件监听触发回调改变了this
window.addEventListener("load",this.refresh);
window.addEventListener("hashchange",this.refresh);
}
// 2 从url获取hash
getHashFromURL(url) {
let index = url.indexOf("#");
if (index >= 0) return url.slice(index + 1);
return "/";
}
// 3 每次hash地址变化需要重新获取hash,并emit触发回调(这个回调里面应该去渲染对应组件)
refresh(event) {
let newhash = "";
let oldhash = null;
if (event.newURL) {
oldhash = this.getHashFromURL(event.oldURL);
newhash = this.getHashFromURL(event.newURL);
this.emit("change", oldhash, newhash);
} else {
// 第一次进入页面的时候不触发hashchange事件,所以事件对象里没有newURL属性
newhash = this.getHashFromURL(window.location.hash);
}
this.currentHash = newhash;
}
// 发布事件
emit(eventName,...args){
let callback=this.handler[eventName];
if(callback) callback(...args);
}
// 订阅事件
on(eventName,callback){
this.handler[eventName] = callback;
}
}
简单测试结果----OK
const router = new HashRouter();
router.on("change",(oldhash,newhash)=>{
console.log(oldhash,newhash);
})
举个稍微完整点的例子
路由规则:
const routes = [
{path: '/',name: 'home',component: <Home />,},
{path: '/about',name: 'about',component: <About />,},
{path: '*',name: '404',component: <NotFound404 />,},
];
创建router实例,封装回调函数,注册监听事件change
const router = new HashRouter();
// hash变化的回调函数---找到对应组件,重新渲染
const callback = (oldHash, newHash) => {
let route = null;
// 匹配路由
for (let item of routes) {
if (newHash === item.path) {
route = item;
break;
}
}
// 若没有匹配到,则使用最后一个路由
if (!route) {
route = routes[routes.length - 1];
}
// 渲染当前的组件
ReactDOM.render(route.component, document.getElementById('app'));
}
// 监听change事件
router.on('change', callback);
history路由
history历史记录管理是HTML5新增的API,常见的方法:window.history.
1)back():回到历史记录中的上一条路由
2)forward():如果有的话前进到下一条路由
3)go(n):跳转到任意一条记录
4)pushState(obj,title,url):跳转到指定同源url,并向历史记录中添加
5)replaceState(obj,title.url):跳转到指定同源url,替换掉当前的url记录
这些方法都只是单纯的替换url,1)不会向服务器发起请求
,2)页面内容不变,页面的内容变化需要通过监听url变化事件触发回调
1) 不会向服务器发起请求:history只是单纯替换url。但是一刷新还是会向服务器发起请求,所以需要服务端也做一些配置,不然没找到对应的资源就会返回404.
2)页面内容想变化:要监听 popState
事件,但是只有前三种方式跳转才会触发这个事件,pushState
和replaceState
这两种行为就需要通过dispatchEvent
添加事件
const listener = function (type) {
var orig = history[type];
return function () {
// 规范
var rv = orig.apply(this, arguments);
// 添加一层事件
var e = new Event(type);
e.arguments = arguments;
window.dispatchEvent(e);
return rv;
};
};
window.history.pushState = listener('pushState');
window.history.replaceState = listener('replaceState');
所以history类事件监听的初始化应该是
window.addEventListener('load', this.refresh, false);
window.addEventListener('popstate', this.refresh, false);
window.addEventListener('pushState', this.refresh, false);
window.addEventListener('replaceState', this.refresh, false);
其他的和hash模式差不多
class HistoryRouter {
// 保存当前的url和注册的回调
currentUrl = '';
handlers = {};
constructor() {
this.refresh = this.refresh.bind(this);
this.addStateListener();
window.addEventListener('load', this.refresh, false);
window.addEventListener('popstate', this.refresh, false);
window.addEventListener('pushState', this.refresh, false);
window.addEventListener('replaceState', this.refresh, false);
}
// 添加自定义事件
addStateListener() {
const listener = function (type) {
var orig = history[type];
return function () {
var rv = orig.apply(this, arguments);
var e = new Event(type);
e.arguments = arguments;
window.dispatchEvent(e);
return rv;
};
};
// 因为这两个行为不会触发 popState
window.history.pushState = listener('pushState');
window.history.replaceState = listener('replaceState');
}
// 获取当前url并触发回调的过程
refresh(event) {
this.currentUrl = location.pathname;
this.emit('change', location.pathname);
}
on(evName, listener) {
this.handlers[evName] = listener;
}
emit(evName, ...args) {
const handler = this.handlers[evName];
if (handler) {
handler(...args);
}
}
}
const router = new HistoryRouter();
router.on('change', function (curUrl) {
console.log(curUrl);
});