首先讲讲 hashHistory 是什么。
众所周知,在单页应用中,我们常用的两种路由策略是 hashHistory 和 browserHistory。其中 browserHistory 是古老的路由方式,从网页诞生一直沿用到现在,比如掘金个人文章页面的地址 https://juejin.im/user/57a0c28979bc440054958498/posts
。而 hashHistory 是单页应用(SPA)出来之后,被广为使用的一种路由方式,它使用 #
后面的路径作为页面标识,如 https://i.ai.mi.com/h5/ai-xiaoai-3-years-fe/#/home
。 前者让我们更难判断项目业务路径(xxx/static/yy-prj-name/home
),而后者可以根据 #
后面的路径轻松判断。
history 项目信息:
项目核心代码在 modules
目录下,其中关键文件是 createBrowserHistory.js
、createHashHistory.js
、createMemoryHistory.js
。
createHashHistory.js
文件导出一个 createHashHistory
函数。
createHashHistory
用法如下:
// 下面给出的是默认值 createHashHistory({ basename: '', // The base URL of the app (see below) hashType: 'slash', // The hash type to use (see below) getUserConfirmation: (message, callback) => callback(window.confirm(message)) }); 复制代码
basename
是路由改变时默认会带上的一串字符串hashType
可以忽略,用默认的getUserConfirmation
这个用来在使用 history.block
的时候处理页面离开的函数,history.block(blockInfo)
blockInfo
可以是一个函数或者字符串,函数执行的结果就是上面的 message,如果直接是字符串,则这个字符串就是传递过来的message;createHashHistory(props)
返回下面一个对象,这就是我们平常在使用 react-router 的时候会用到的 props.history
:
const history = { length: globalHistory.length, action: 'POP', location: initialLocation, createHref, push, replace, go, goBack, goForward, block, listen, }; 复制代码
我们对导出的各项逐个分析。
这个比较常用,push
代码简化之后就是下面这样。
参数:
path
对象或者字符串,字符串就是我们想要push的地址,如果是字符串则会被解析为一个对象(locationObj)。如果是对象则push之后的地址就是 /user/game?name=lxfriday#hahaha
,可以容纳下面三个属性,。
history.push({ pathname: '/user/game', search: 'name=lxfriday', hash: 'hahaha' }) 复制代码
比如 /user/game?name=lxfriday#hahaha
,如果没有设置 basename,则push之后的地址就是 /user/game?name=lxfriday#hahaha
,如果设置了 basename 为 /base
,则push之后的地址为 /base/user/game?name=lxfriday#hahaha
。
function push(path, state) { const action = 'PUSH'; const location = createLocation( path, undefined, undefined, // 地址改变之前的 location 信息 history.location ); transitionManager.confirmTransitionTo( location, action, getUserConfirmation, (ok) => { if (!ok) return; const path = createPath(location); // encodedPath 编码之后的新 hash // encodePath 加一个首 / const encodedPath = encodePath(basename + path); const hashChanged = getHashPath() !== encodedPath; if (hashChanged) { ignorePath = path; // window.location.hash = encodedPath; pushHashPath(encodedPath); // history.location 是当前的 const prevIndex = allPaths.lastIndexOf(createPath(history.location)); // nextPaths 是最新的路由栈 const nextPaths = allPaths.slice(0, prevIndex + 1); nextPaths.push(path); allPaths = nextPaths; setState({ action, location }); } else { setState(); } } ); } 复制代码
源码中 createLocation
表示创建一个 locationObj
, path
是字符串或者对 象都可以,返回一个 path 对应的 locationObj
。
比如 /user/game?name=lxfriday#haha
返回的是:
{ pathname: '/user/game', search: '?name=lxfriday', hash: '#haha' } 复制代码
关于 search
的 ?
和 hash 的 #
,内部会自动添加。
transitionManager
是一个路由跳转处理器,在没有设置 history.block
的时候我们可以理解为直接调用 confirmTransitionTo
的最后一个回调函数,并且 ok
为 true
。
createPath
可以把 locationObj 再拼回一个字符串地址:
encodePath
这里是给 basename + path 拼接的字符串添加一个首 /
。
接下来比较 push 前后 hash 是否变化。getHashPath
是获取当前浏览器地址栏中的 hash 字符串,也就是当前的地址。
所以 getHashPath() !== encodedPath
比较的是浏览器当前的地址和即将要变更的地址,如果改变了,则会调用 pushHashPath(encodedPath)
把 encodedPath
也就是最新的地址应用到 浏览器地址栏。具体怎么应用呢:
function pushHashPath(path) { window.location.hash = path; } 复制代码
没错就是这么简单,直接更改了 location.hash
。
接下来要修改 history 内部自己维护的一个路由栈了:
const prevIndex = allPaths.lastIndexOf(createPath(history.location)); // nextPaths 是最新的路由栈 const nextPaths = allPaths.slice(0, prevIndex + 1); nextPaths.push(path); allPaths = nextPaths; 复制代码
history.location
怎么理解?记住 history 里面的动力在 setState
调用之前都是旧的,也就是比前面使用 createLocation
创建的 location
旧一个节拍。
所以这里 prevIndex
是拿到push操作前的地址在路由栈中最后一次出现的位置。然后复制一个新数组,把最新的地址放到最后,重新赋值给路由栈,这样就达到了更改路由栈的目的,很简单吧。
接下来是 setState({ action, location });
, action
是 PUSH
而 location
上面说了,是push操作后应该要达到的地址,setState
要做什么呢,很简单。
function setState(nextState) { Object.assign(history, nextState); // history 是在最底部声明的那个对象,与 window.history 很类似 history.length = globalHistory.length; transitionManager.notifyListeners(history.location, history.action); } 复制代码
把 action
和 location
属性复制到 history 中,同时触发在 transitionManager
中添加的监听器(也就是路由变更的监听器)。
写了这么多,我发现用文字来描述源码是一件非常难说清楚的事情,因为代码经过多次封装之后,一行代码可能需要查还几个地方才能弄清楚,文字描述难免漏掉一些细节。如果你想更深入了解,可以加入我的交流群,在群内直接提问。
文章源码看起来会比较繁复难以理解,如果你对源码分析感兴趣,可以前往 bilibili 观看我录制的视频,另外下载一份源码到本地自己亲自调试看源码效果会更好哦。
如果觉得看文章看视频依然没有看懂,也可以关注我的公众号,加入交流群,有问必答,欢迎交流讨论~~~