├── README.md └── uni_router.js /README.md: -------------------------------------------------------------------------------- 1 | # 欢迎使用 uni_router.js(因作者精力有限且不在从事相关技术栈研发,故本工具不再维护,感谢关注) 2 | 3 | 对 uni-app 中的路由 api 进行了简单封装,定义了全局导航守卫方法、路由状态保存、错误捕获,pop 传参,变化监听等。使用起来及其方便,无需配置路由表, 也无需进行二次封装。代码也不复杂,只有不到100来行,比较适合中小型项目快速开发。很实用。如有BUG,还请不吝指出,非常感谢。 4 | 5 | ---- 6 | 7 | ## 基本使用 8 | 9 | ```javascript 10 | // 在 main.js 中注册 11 | import Vue from 'vue' 12 | import App from '@/App' 13 | import $router, { $route } from '@/common/js/uni_router.js' 14 | 15 | $router.beforeEach = (to, next) => { // 注册全局前置守卫 16 | console.log('全局前置守卫', to) 17 | if (to.path.includes('/test')) { 18 | // 可以通过传一个回调给 next 来访问 $router 实例, 会返回一个 reject('在全局前置守卫 next 中重定向路由') 19 | next(vm => { 20 | vm.push('/redirect') 21 | }) 22 | } else if (to.path.includes('/redirect')) { 23 | next(false) // 中断当前的导航,会返回一个 reject('在全局前置守卫 next 中取消路由') 24 | } else { 25 | next() // 一定要调用该方法来 resolve 这个钩子 26 | } 27 | } 28 | 29 | $router.afterEach = to => { // 注册一个全局后置守卫 30 | console.log('全局后置守卫', to) 31 | } 32 | 33 | $router.onchange = (n, o) => { // 注册一个全局路由监听方法(可以监听小程序的物理返回),实现原理见最下方 34 | console.log(n) // 路由变化后的 $route 对象 35 | console.log(o) // 路由变化前的 $route 对象 36 | } 37 | 38 | Vue.prototype.$iRoute = $route // 当前路由对象,保存路由当前信息 39 | Vue.prototype.$iRouter = $router // 路由对象,保存了实例方法 40 | 41 | /////////////////////////////////////////////////////////////////////////////////////// 42 | // 注意:将方法挂载到 Vue 原型上不要使用 $router 作为名字。在 H5 端会和 vue-router 冲突 // 43 | /////////////////////////////////////////////////////////////////////////////////////// 44 | 45 | // 在组件中使用 46 | methods: { 47 | to() { 48 | this.$iRouter.push('/test', { 49 | id: 2333, 50 | test: { 51 | a: 1, 52 | b: 2 53 | } 54 | }).then(e => { 55 | console.log('路由结束,当前路由对象为', e) 56 | }).catch(e => { 57 | console.warn(e) 58 | }) 59 | } 60 | } 61 | 62 | // test 页面中 63 | onLoad() { 64 | console.log(this.$iRoute) // 可以通过 $iRoute 获取当前路由对象、参数、路径信息 65 | // { 66 | // fullPath: "/pages/test/test" 67 | // path: "/test" 68 | // query: { 69 | // id: 2333, 70 | // test: { 71 | // a: 1, 72 | // b: 2 73 | // } 74 | // } 75 | // } 76 | } 77 | 78 | // this.$iRouter 上一共有 6 个方法, 分别是 79 | this.$iRouter.pop() 80 | this.$iRouter.push() 81 | this.$iRouter.pushPop() 82 | this.$iRouter.replace() 83 | this.$iRouter.reLaunch() 84 | this.$iRouter.switchTab() 85 | 86 | // 除了 pop 方法只接受一个参数 delta 或 data 对象外,其余 5 个方法接受的参数分别是 87 | fun( 88 | path, // path 路由名 89 | query = {}, // query 路由传参 90 | notBeforeEach, // isBeforeEach 是否要被全局前置守卫拦截,默认false,拦截,设置为 true 禁止拦截 91 | notAfterEach // isAfterEach 是否要被全局后置守卫拦截,默认false,拦截,设置为 true 禁止拦截 92 | ).then(...) 93 | 94 | // 这 5 个方法除 pushPop 外,then 方法都会接收到当前页面的路由对象,包含参数、路径信息 95 | 96 | // pop 方法接受一个数字 或 对象。为数字时表示返回的页面层数,为对象时,表示返回上层并将此对象作为上层 pushPop 方法 then 的参数实现 pop 传参 97 | 98 | ``` 99 | 100 | ## pop 传参 101 | 102 | 当使用 pushPop 方法时,会得到一个 promise 对象,then 方法接受的是返回该页面时 pop 方法传递的参数 103 | 104 | pop 105 | 106 | ```javascript 107 | // 在组件中使用 108 | methods: { 109 | to() { 110 | this.$iRouter.pushPop('/test').then(e => { 111 | if (e) { 112 | console.log('路由返回,接收到的数据为', e) // { data: 'data' } 113 | } 114 | }) 115 | } 116 | } 117 | 118 | // test 页面中 119 | methods: { 120 | back() { 121 | this.$iRouter.pop({ data: 'data' }) // pop 传参时必须为对象 122 | } 123 | } 124 | ``` 125 | 126 | PS:pushPop 方法在 pop(data) 之前只能使用一次。即:在当前页面全局第一次调用 pushPop 方法跳转到第二个页面后,不能在第二个页面再次调用 pushPop 方法,且只能在第二个页面调用 pop(data 方法。否则会出现一些意料之外的BUG。即:pushPop 方法和 pop(data) 只能出现在相邻页面,且调用顺序互为对称。 127 | 128 | 如果你使用了 pushPop 方法,但是没有使用 pop(data) 却导致了页面回退。此使 pushPop 的 then 方法将永远不会被执行。直到下次 pushPop 才会 resolve,且参数为 null。 129 | 130 | 因为 pushPop 返回的 promise 对象的 resolve 会被保存为全局唯一。直到执行 pop 并传对象时,resolve 才会被释放。为了防止滥用,出现过多的 pending 状态的 promise 导致内存泄漏。这里将 resolve 设置为全局唯一。 131 | 132 | 如果你有复杂的需求,并且能够完美处理多个 promise 以及返回。请自行拓展。 133 | 134 | ## 注意事项 135 | 136 | 1 . 当使用分包时,跳转路由必须使用 /分包名/路由名。不使用分包时跳转路由必须为 /路由名 137 | 138 | 2 . 全局导航守卫并不能也没有对 pop 方法或者说所有的返回行为做拦截。因为目前个人用 uni-app 开发几乎都是小程序方面,小程序没办法对返回行为做拦截,所有就没做 139 | 140 | 3 . 可能不支持 nvue 。没测试过。具体原因看第二条。 141 | 142 | 4 . 虽然小程序没有相关事件去监听页面返回操作,通过 uni_router 可以巧妙地做到返回监听,简单的原理如下: 143 | 144 | ```javascript 145 | Vue.mixin({ 146 | onShow() { 147 | const pages = getCurrentPages().map(e => `/${e.route}`).reverse() // 获取页面栈 148 | if (pages[0]) { // 当页面栈不为空时执行 149 | ... 150 | if (back) { // 当当前路由与 route 对象不符时,表示路由发生返回 151 | onchange(route, old) 152 | } 153 | } 154 | } 155 | }) 156 | ``` 157 | 简单来说就是在全局混入一个 onShow 方法。已实现返回监听。但是一定要记住,只是实现了监听,并不能拦截。 158 | 因此全局的 onchang 方法在页面返回时,会在返回的页面的 onShow 时执行 159 | 在页面非返回是,会在uni[路由方法]的 success 方法调用时候执行。 160 | 161 | ---- 162 | -------------------------------------------------------------------------------- /uni_router.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | export const route = { // 当前路由对象所在的 path 等信息。默认为首页 4 | fullPath: '/pages/index/index', 5 | path: '/index', 6 | type: 'push', 7 | query: {} 8 | } 9 | 10 | let _$ROUTING = false // 标记路由状态 防止连点 11 | let onchange = () => {} // 路由变化监听函数 12 | const _$UNI_ACTIVED_PAGE_ROUTES = [] // 页面数据缓存 13 | let _$UNI_ROUTER_PUSH_POP_FUN = () => {} // pushPop resolve 函数 14 | const _c = obj => JSON.parse(JSON.stringify(obj)) // 简易克隆方法 15 | 16 | Vue.mixin({ 17 | onShow() { 18 | _$ROUTING = false 19 | const pages = getCurrentPages().map(e => `/${e.route}`).reverse() // 获取页面栈 20 | if (pages[0]) { // 当页面栈不为空时执行 21 | let old = _c(route) // 保存旧路由 22 | const back = pages[0] != route.fullPath 23 | const now = _$UNI_ACTIVED_PAGE_ROUTES.find(e => e.fullPath == pages[0]) // 如果路由没有被缓存就缓存 24 | now ? Object.assign(route, now) : _$UNI_ACTIVED_PAGE_ROUTES.push(_c(route)) // 已缓存就用已缓存的更新 route 对象 25 | _$UNI_ACTIVED_PAGE_ROUTES.splice(pages.length, _$UNI_ACTIVED_PAGE_ROUTES.length) // 最后清除无效缓存 26 | if (back) { // 当当前路由与 route 对象不符时,表示路由发生返回 27 | if (pages.length === 1) { // 如果页面栈只有一个页面,表示正处于 tabbar 页面 28 | Object.assign(route, { fullPath: pages[0], path: `/${pages[0].split('/')[3]}`, query: {}, type: "switchTab" }) 29 | } 30 | onchange(route, old) 31 | } 32 | } 33 | } 34 | }) 35 | 36 | const router = new Proxy({ 37 | route: route, // 当前路由对象所在的 path 等信息, 38 | afterEach: to => {}, // 全局后置守卫 39 | beforeEach: (to, next) => next(), // 全局前置守卫 40 | _getFullPath(route) { // 根据传进来的路由名称获取完整的路由名称 41 | return new Promise((resolve, reject) => { 42 | const split = route.split('/') 43 | const name = `/${split[split.length - 1]}` 44 | resolve(`/pages${route}${name}`) 45 | }) 46 | }, 47 | _formatData(query) { // 序列化路由传参 48 | let queryString = '?' 49 | Object.keys(query).forEach(e => { 50 | if (typeof query[e] === 'object') { 51 | queryString += `${e}=${JSON.stringify(query[e])}&` 52 | } else { 53 | queryString += `${e}=${query[e]}&` 54 | } 55 | }) 56 | return queryString.length === 1 ? '' : queryString.replace(/&$/, '') 57 | }, 58 | _beforeEach(path, fullPath, query, type) { // 处理全局前置守卫 59 | return new Promise(resolve => { 60 | this.beforeEach({ path, fullPath, query, type }, resolve) 61 | }) 62 | }, 63 | _next(next) { // 处理全局前置守卫 next 函数传经来的方法 64 | return new Promise((resolve, reject) => { 65 | if (typeof next === 'function') { // 当 next 为函数时, 表示重定向路由, 66 | reject('在全局前置守卫 next 中重定向路由') 67 | Promise.resolve().then(() => next(this)) // 此处一个微任务的延迟是为了先触发重定向的reject 68 | } else if (next === false) { // 当 next 为 false 时, 表示取消路由 69 | reject('在全局前置守卫 next 中取消路由') 70 | } else { 71 | resolve() 72 | } 73 | }) 74 | }, 75 | _routeTo(UNIAPI, type, path, query, notBeforeEach, notAfterEach) { 76 | return new Promise((resolve, reject) => { 77 | if (_$ROUTING) { 78 | reject('路由进行中') 79 | return 80 | } 81 | this._getFullPath(path).then((fullPath) => { // 检查路由是否存在于 pages 中 82 | const routeTo = url => { // 执行路由 83 | const temp = _c(route) // 将 route 缓存起来 84 | Object.assign(route, { path, fullPath, query, type }) // 在路由开始执行前就将 query 放入 route, 防止少数情况出项的 onLoad 执行时,query 还没有合并 85 | _$ROUTING = true 86 | UNIAPI({ url }).then(([err]) => { 87 | if (err) { // 路由未在 pages.json 中注册 88 | Object.assign(route, temp) // 如果路由跳转失败,就将 route 恢复 89 | _$ROUTING = false 90 | reject(err) 91 | return 92 | } else { // 跳转成功, 将路由信息赋值给 route 93 | resolve(route) // 将更新后的路由对象 resolve 出去 94 | onchange({ path, fullPath, query, type }, temp) 95 | !notAfterEach && this.afterEach(route) // 如果没有禁止全局后置守卫拦截时, 执行全局后置守卫拦截 96 | } 97 | }) 98 | } 99 | if (notBeforeEach) { // notBeforeEach 当不需要被全局前置守卫拦截时 100 | routeTo(`${fullPath}${this._formatData(query)}`) 101 | } else { 102 | this._beforeEach(path, fullPath, query, type).then((next) => { // 执行全局前置守卫,并将参数传入 103 | this._next(next).then(() => { // 在全局前置守卫 next 没传参 104 | routeTo(`${fullPath}${this._formatData(query)}`) 105 | }).catch(e => reject(e)) // 在全局前置守卫 next 中取消或重定向路由 106 | }) 107 | } 108 | }).catch(e => reject(e)) // 路由不存在于 pages 中, reject 109 | }) 110 | }, 111 | pop(data) { 112 | if (typeof data === 'object') { 113 | _$UNI_ROUTER_PUSH_POP_FUN(data) 114 | } 115 | uni.navigateBack({ delta: typeof data === 'number' ? data : 1 }) 116 | }, 117 | // path 路由名 // query 路由传参 // isBeforeEach 是否要被全局前置守卫拦截 // isAfterEach 是否要被全局后置守卫拦截 118 | push(path, query = {}, notBeforeEach, notAfterEach) { 119 | return this._routeTo(uni.navigateTo, 'push', path, query, notBeforeEach, notAfterEach) 120 | }, 121 | pushPop(path, query = {}, notBeforeEach, notAfterEach) { 122 | return new Promise(resolve => { 123 | _$UNI_ROUTER_PUSH_POP_FUN(null) 124 | _$UNI_ROUTER_PUSH_POP_FUN = resolve 125 | this._routeTo(uni.navigateTo, 'pushPop', path, query, notBeforeEach, notAfterEach) 126 | }) 127 | }, 128 | replace(path, query = {}, notBeforeEach, notAfterEach) { 129 | return this._routeTo(uni.redirectTo, 'replace', path, query, notBeforeEach, notAfterEach) 130 | }, 131 | switchTab(path, query = {}, notBeforeEach, notAfterEach) { 132 | return this._routeTo(uni.switchTab, 'switchTab', path, query, notBeforeEach, notAfterEach) 133 | }, 134 | reLaunch(path, query = {}, notBeforeEach, notAfterEach) { 135 | return this._routeTo(uni.reLaunch, 'reLaunch', path, query, notBeforeEach, notAfterEach) 136 | } 137 | }, { 138 | set(target, key, value) { 139 | if (key == 'onchange') { 140 | onchange = value 141 | } 142 | return Reflect.set(target, key, value) 143 | } 144 | }) 145 | 146 | Object.setPrototypeOf(route, router) // 让 route 继承 router 147 | 148 | export default router --------------------------------------------------------------------------------