uniapp中实现vue-router的路由功能

5,574 阅读2分钟

由于 uni-app 为了保证跨端同时简化语法,用的是微信小程序那套 API。其中就包括路由系统。因为在 uni-app 中,没有 router/router/route。只有 uni[‘路由方法’]。讲真的,这样做确实很容易上手,但同时也是有许多问题:

  1. 路由传参数只支持字符串,对象参数需要手动JSON序列化
  2. 传参有长度限制
  3. 传参不支持特殊符号如 url
  4. 不支持路由拦截和监听

因此,需要一个工具来将现有的路由使用方式变为 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对象。因此我们的需求就是事现一个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