开发hybrid应用时经常使用WebView来加载前端网页,并且现在SPA已是主流,那么路由也由前端控制,主要是使用history.pushState和history.replaceState方法。
为了进一步的让前端控制网页的内容,WebView会把原生的顶部导航栏隐藏掉,让前端自己渲染一个导航栏,并且实现返回的按钮。
一般来说,这个返回按钮需要两个功能:1. 路由的后退,2. 在第一层路由退出WebView。
通常退出WebView由客户端提供的桥实现,但是判断当前路由是否是第一层路由,即当前的页面是否是WebView刚打开时的路由,需要前端自行判断。
一个办法是在网页刚打开时,标记当前路由为一层路由。那么必须找一个可以保存与路由相关且不会刷新清楚的数据的地方。
history.state
history.pushState和history.replaceState方法可以改变路由并保存数据,从history.state可以读取保存的数据。这些数据的生命周期是伴随一次浏览,即浏览器打开到关闭的,与sessionStorage类似,它不会被刷新清除。
可以在网页刚进入时这样保存标记:
history.replaceState({ firstPage: true }, null)
在点击返回按钮时可以进行判断:
if (history.state.firstPage) {
// 关闭WebView
} else {
history.go(-1)
}
借由判断当前历史状态中的是否存在firstPageK来判断当前路由是否为第一层路由。
当在其它页面(非第一层路由)刷新时,这个操作可能会被重复执行,从而造成标记了两次的情况。为了解决这个问题,可以在sessionStorage中保存一个“第一层路由已标记”的标记,确保在整个浏览过程中标记操作不会被重复执行。示例:
let firstPageMark = sessionStorage.getItem('first-page-mark')
if (!firstPageMark) {
history.replaceState({ firstPage: true }, null)
sessionStorage.setItem('first-page-mark', '1')
}
实验
我们用Express来实验一下。
index.js
const Path = require('path')
const Express = require('express')
const app = Express()
const router = Express.Router()
router.get('/:number', (req, res, next) => {
res.sendFile(Path.resolve(__dirname, './index.html'))
})
app.use(router)
app.listen(8080)
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="number"></div>
<button id="back">Back</button>
<button id="next">Next</button>
<script>
let number
function setNumber() {
number = +location.pathname.replace('/', '')
document.querySelector('#number').innerText = number
}
setNumber()
document.querySelector('#back').addEventListener('click', () => {
if (history.state.firstPage) {
console.log('First Page')
} else {
console.log('Back')
history.go(-1)
}
setTimeout(() => {
setNumber()
})
})
document.querySelector('#next').addEventListener('click', () => {
history.pushState({}, null, `/${++number}`)
setNumber()
})
let firstPageMark = sessionStorage.getItem('first-page-mark')
if (!firstPageMark) {
history.replaceState({ firstPage: true }, null)
sessionStorage.setItem('first-page-mark', '1')
}
</script>
</body>
</html>
可以看到结果如下:
vue-router
vue-router为vue提供了前端路由的功能,其在底层也使用了history.pushState和history.replaceState方法。它在路由跳转的过程中会替换history.state,导致上述的方法失效。但是仍然可以使用上述方法的原理,因为vue-router会在history.state中为每一个路由保存一个全局唯一的key数据,只要记住第一层路由的key,就能通过它和当前页面的key比较来判断当前页面是不是第一层路由。
示例:
let firstPageKey = sessionStorage.getItem('first-page-key')
function setFirstPageKey(){
if(!firstPageKey){
firstPageKey = history.state.key
sessionStorage.setItem('first-page-key', firstPageKey)
}
}
setFirstPageKey方法必须在vue-router设置完key后调用,比如vue-router的afterEach导航守卫中。
在保存了第一层路由的key后,可以在返回按钮按下时这么判断:
if (history.state.key === firstPageKey) {
// 关闭WebView
} else {
router.back()
}
借由判断当前历史状态中的key是否与firstPageKey相同来判断当前路由是否为第一层路由。
刷新
与上一种方法不同的是,浏览器的刷新不会修改history.state,但是在使用的vue-router的页面中,刷新后vue-router会重新初始化,那么当前页面的key也会变更,使得原本记录的firstPageKey无效。而且为了防止重复记录,保存key这个操作被锁死,只能执行一次,所以firstPageKey无法被刷新。
如果刷新时的路由不是第一层路由,那么没有关系,因为这一层的key我们不关心。如果在第一层路由的页面刷新,那么在刷新前我们需要清楚sessionStorage中的first-page-key,一遍重新进入页面后,key能被重新保存。示例:
window.addEventListener('unload', () => {
if (history.state.key === firstPageKey) {
sessionStorage.set('first-page-set', '')
}
})