uniapp实现$router
作为 Vue 重度用户,在使用 uni-app 过程中不可避免的把 Vue 开发习惯带了过去。无论是项目目录结构,还是命名风格,甚至我还封装了一些库,如 https://zhuanlan.zhihu.com/p/141451626 提到的 _request 等。
众所周知,用 Vue 开发项目,其实就是用的 Vue 全家桶。即 Vue + Vuex + VueRouter 。在代码里的体现就是:
this + this.$store + this.$router/$route
然而由于 uni-app 为了保证跨端同时简化语法,用的是微信小程序那套 API。其中就包括路由系统。因为在 uni-app 中,没有 $router/$route。只有 uni[‘路由方法’]。讲真的,这样做确实很容易上手,但同时也是有许多问题:
- 路由传参数只支持字符串,对象参数需要手动JSON序列化
- 传参有长度限制
- 传参不支持特殊符号如 url
- 不支持路由拦截和监听
因此,需要一个工具来将现有的路由使用方式变为 vue-router 的语法,并且完美解决以上几个问题。
vue-router 的语法这里不再赘述。简单的来说就是将路由的用法由:
uni.navigateTo({
url: `../login/login?data=${JSON.stringify({ from: 'index', time:Date.now() })}`
})
变成:
this.$router.push('/login', {
data: {
from: 'index',
time: Date.now()
}
})
同时传参通过一个 $route 对象。因此我们的需求就是事现一个 $router 和 $route 对象。并给定相应方法。比如调用:
push('/login')
其实就是执行了:
uni.navigateTo({ url:`../login/login ` })
实现起来非常简单:
push 方法接收到 '/login' 将其拼接为 `../login/login` 后调用 uni.navigateTo 就可以。
然而这样并不严谨。此时的 push 方法只能在页面内使用。而不能在 pages 文件夹以外的地方使用,因为这里用的是相对路径。只要改成 `pages/login/login` 就好。
$route 的实现就是在路由发生变化时,动态改变一个公共对象 route 的内部值。
而通过全局 mixin onShow 方法,可以实现对路由变化动态监听。
通过 require.context 预引入路由列表实现更好的错误提示。
最后通过一个页面堆栈数据列表实现 route 实时更新。
最后的代码:
import Vue from 'vue'
export const route = { // 当前路由对象所在的 path 等信息。默认为首页
fullPath: '/pages/index/index',
path: '/index',
type: 'push',
query: {}
}
let onchange = () => {} // 路由变化监听函数
const _$UNI_ACTIVED_PAGE_ROUTES = [] // 页面数据缓存
let _$UNI_ROUTER_PUSH_POP_FUN = () => {} // pushPop resolve 函数
const _c = obj => JSON.parse(JSON.stringify(obj)) // 简易克隆方法
const modulesFiles = require.context('@/pages', true, /\.vue$/) // pages 文件夹下所有的 .vue 文件
Vue.mixin({
onShow() {
const pages = getCurrentPages().map(e => `/${e.route}`).reverse() // 获取页面栈
if (pages[0]) { // 当页面栈不为空时执行
let old = _c(route)
const back = pages[0] != route.fullPath
const now = _$UNI_ACTIVED_PAGE_ROUTES.find(e => e.fullPath == pages[0]) // 如果路由没有被缓存就缓存
now ? Object.assign(route, now) : _$UNI_ACTIVED_PAGE_ROUTES.push(_c(route)) // 已缓存就用已缓存的更新 route 对象
_$UNI_ACTIVED_PAGE_ROUTES.splice(pages.length, _$UNI_ACTIVED_PAGE_ROUTES.length) // 最后清除无效缓存
if (back) { // 当当前路由与 route 对象不符时,表示路由发生返回
onchange(route, old)
}
}
}
})
const router = new Proxy({
route: route, // 当前路由对象所在的 path 等信息,
afterEach: to => {}, // 全局后置守卫
beforeEach: (to, next) => next(), // 全局前置守卫
routes: modulesFiles.keys().map(e => e = e.replace(/^\./, '/pages')), // 路由表
_getFullPath(route) { // 根据传进来的路由名称获取完整的路由名称
return new Promise((resolve, reject) => {
const fullPath = this.routes.find(e => RegExp(route + '.vue').test(e))
fullPath ? resolve(fullPath.replace(/\.vue$/, '')) : reject(`路由 ${ route + '.vue' } 不存在于 pages 目录中`)
})
},
_formatData(query) { // 序列化路由传参
let queryString = '?'
Object.keys(query).forEach(e => {
if (typeof query[e] === 'object') {
queryString += `${e}=${JSON.stringify(query[e])}&`
} else {
queryString += `${e}=${query[e]}&`
}
})
return queryString.length === 1 ? '' : queryString.replace(/&$/, '')
},
_beforeEach(path, fullPath, query, type) { // 处理全局前置守卫
return new Promise(resolve => {
this.beforeEach({ path, fullPath, query, type }, resolve)
})
},
_next(next) { // 处理全局前置守卫 next 函数传经来的方法
return new Promise((resolve, reject) => {
if (typeof next === 'function') { // 当 next 为函数时, 表示重定向路由,
reject('在全局前置守卫 next 中重定向路由')
Promise.resolve().then(() => next(this)) // 此处一个微任务的延迟是为了先触发重定向的reject
} else if (next === false) { // 当 next 为 false 时, 表示取消路由
reject('在全局前置守卫 next 中取消路由')
} else {
resolve()
}
})
},
_routeTo(UNIAPI, type, path, query, notBeforeEach, notAfterEach) {
return new Promise((resolve, reject) => {
this._getFullPath(path).then((fullPath) => { // 检查路由是否存在于 pages 中
const routeTo = url => { // 执行路由
const temp = _c(route) // 将 route 缓存起来
Object.assign(route, { path, fullPath, query, type }) // 在路由开始执行前就将 query 放入 route, 防止少数情况出项的 onLoad 执行时,query 还没有合并
UNIAPI({ url }).then(([err]) => {
if (err) { // 路由未在 pages.json 中注册
Object.assign(route, temp) // 如果路由跳转失败,就将 route 恢复
reject(err)
return
} else { // 跳转成功, 将路由信息赋值给 route
resolve(route) // 将更新后的路由对象 resolve 出去
onchange({ path, fullPath, query, type }, temp)
!notAfterEach && this.afterEach(route) // 如果没有禁止全局后置守卫拦截时, 执行全局后置守卫拦截
}
})
}
if (notBeforeEach) { // notBeforeEach 当不需要被全局前置守卫拦截时
routeTo(`${fullPath}${this._formatData(query)}`)
} else {
this._beforeEach(path, fullPath, query, type).then((next) => { // 执行全局前置守卫,并将参数传入
this._next(next).then(() => { // 在全局前置守卫 next 没传参
routeTo(`${fullPath}${this._formatData(query)}`)
}).catch(e => reject(e)) // 在全局前置守卫 next 中取消或重定向路由
})
}
}).catch(e => reject(e)) // 路由不存在于 pages 中, reject
})
},
pop(data) {
if (typeof data === 'object') {
_$UNI_ROUTER_PUSH_POP_FUN(data)
}
uni.navigateBack({ delta: typeof data === 'number' ? data : 1 })
},
// path 路由名 // query 路由传参 // isBeforeEach 是否要被全局前置守卫拦截 // isAfterEach 是否要被全局后置守卫拦截
push(path, query = {}, notBeforeEach, notAfterEach) {
return this._routeTo(uni.navigateTo, 'push', path, query, notBeforeEach, notAfterEach)
},
pushPop(path, query = {}, notBeforeEach, notAfterEach) {
return new Promise(resolve => {
_$UNI_ROUTER_PUSH_POP_FUN(null)
_$UNI_ROUTER_PUSH_POP_FUN = resolve
this._routeTo(uni.navigateTo, 'pushPop', path, query, notBeforeEach, notAfterEach)
})
},
replace(path, query = {}, notBeforeEach, notAfterEach) {
return this._routeTo(uni.redirectTo, 'replace', path, query, notBeforeEach, notAfterEach)
},
switchTab(path, query = {}, notBeforeEach, notAfterEach) {
return this._routeTo(uni.switchTab, 'switchTab', path, query, notBeforeEach, notAfterEach)
},
reLaunch(path, query = {}, notBeforeEach, notAfterEach) {
return this._routeTo(uni.reLaunch, 'reLaunch', path, query, notBeforeEach, notAfterEach)
}
}, {
set(target, key, value) {
if (key == 'onchange') {
onchange = value
}
return Reflect.set(target, key, value)
}
})
Object.setPrototypeOf(route, router) // 让 route 继承 router
export default router