likes
comments
collection
share

uniapp+vue3 setup+ts 开发小程序实战(路由篇)

作者站长头像
站长
· 阅读数 13

前言

在这一节,我们先回顾下小程序路由基础知识,然后针对小程序路由存在的问题做相关封装优化。

小程序路由基础知识

API

小程序路由一共有5个API:

  • switchTab:跳转到 tabBar 页面,并关闭其他所有非 tabBar 页面
  • reLaunch: 关闭所有页面,打开到应用内的某个页面
  • redirectTo: 关闭当前页面,跳转到应用内的某个页面。但是不允许跳转到 tabbar 页面
  • navigateTo: 保留当前页面,跳转到应用内的某个页面。但是不能跳到 tabbar 页面。
  • navigateBack: 关闭当前页面,返回上一页面或多级页面。

页面间传递方式

页面间传递参数是通过 url 拼接参数的方式,如传递一个id=1的参数写法如下:

uni.navigateTo({
  url: '[目标页面地址]?id=1'
})

然后在目标页面的onLoad生命周期钩子获取到上个页面传递的参数。

路由优化封装目的

针对以上路由基础知识,以下的路由封装方法,主要出于以下5个目的:

1、简化写法:

通过上面的基础知识我们了解到,页面间跳转需要这么写:

uni.navigateTo({
  url: '/pages/index/index'
})

这种写法无疑过于繁琐,目标希望简化成:router.navigate('index')

2、typescript类型提示

既然是typescript项目,我们就需要尽可能给代码完善类型提示:

1、在路由跳转时,封装的写法能够提供路由别名候选提示,并在我们写错别名时给予错误提示

router.navigate('details') // 写成detalis出现错误提示

2、获取路由参数时,路由参数应该有类型提示

3、优化传参:

原生通过参数拼接的方式,主要有以下几个弊端问题:

  • 在写法上不便:需要我们先将参数转化后再拼接在链接后面,如果遇到较复杂点的json数据还需做进一步处理
  • 取值被约束:获取参数只能在onLoad生命周期钩子里面获取。
  • 无法跨页面使用参数:uniapp+vue3 setup+ts 开发小程序实战(路由篇)

如上图。假设页面a传递参数给到b页面使用,而c页面也恰好需要用到a页面传递给b页面的某些参数,那么这时候就需要b页面跳去c页面时再做一次传递,这样不仅写法麻烦,也给后续的维护带来一定的麻烦。

  • reLaunch和switchTab方法不支持传递参数:

不知道官方设计的用意如何,不支持传参不代表实际场景不需要用到。这里举个真实场景例子,证明switchTab也是有需要支持传递参数的时候:

uniapp+vue3 setup+ts 开发小程序实战(路由篇)

如上图:假设我们的小程序在一进入时需要进行一个初始化设置,用户在完成某些选择之后(如选择性别、年龄等基础信息),才进入到小程序tab页面,这时候点击“点击进入”调用的是switchTab方法,我们需要将用户初始设置的参数传递到主页做进一步处理。

当然,解决的方法有多种,例如我们可以调用本地存储的方法暂存,然后再取值,但是通过路由传参统一管理参数无疑是更适合的写法

综合以上弊端问题的分析,这里的解决的方案也很简单:抛弃原生路由拼接参数传递方式,直接定义一个全局对象存储。

4、navigateTo方法处理用户多次点击

小程序调用navigateTo方法,实际上会创建一个新的webview,接着初始化新页面的生命周期里面的代码逻辑,如果遇到页面比较复杂的情况,初始化就可能比较耗时,导致用户点击跳转下一页会可能就会出现卡顿。此时用户可能多次点击触发navigateTo方法,进而有可能导致重复加载同一个页面。

5、路由返回方法增加携带参数

在某些场景中,我们希望页面返回时,能够携带参数给上一级页面。例如用户在A页面点击选择地址,然后跳转去一个地址列表页面,在地址列表页面用户完成选择某项地址后,携带该地址返回,A页面接收到后再更新地址。目标希望可以这么调用:

router
    .navigate('page', { id: 1 })
    .then((data) => {
      // 接收到上个页面返回的数据
      console.log(data)
    })
    .catch((err) => {
      console.log(err)
    })

上手封装

有了以上封装设计目的,接下来就开始正式上手封装代码。

我们在src目录下新建个router文件夹,里面新建index.ts、pages.ts、types.ts三个文件。

在上面的路由优化设计目标中,我们第一个希望优化的目标是希望简化路由写法成:

router.navigate('index')

这就需要我们存放一个路由页面集合,给每个页面路径设置别名,pages.ts内容增加:

// 主包
const mainPackage = {
  index: '/pages/index/index',
}

// 分包
const subPackage = {
  subIndex: '/package-sub/pages/index/index',
}

const pages = {
  ...mainPackage,
  ...subPackage,
}

export default pages

在index.ts中引入,并定义页面别名类型:

import pages from './pages'
type PageNames = keyof typeof pages

接下来处理路由传参,先定义一个store存放所有页面路由参数:

const routeStore: Record<PageNames, unknown> = {}

假设我们从首页点击进入课程详情页,路由需要传递一个id代表课程id,我们可以在types.ts新增:

export interface CourseDetails {
  id: number
}

然后在index.ts中引入:

import { CourseDetails } from './types'

接着定义ObjectType泛型:

type ObjectType<T> = T extends 'courseDetails' ? CourseDetails : never

路由方法:

function navigate<T extends PageNames>(page: T, params: ObjectType<T>) {
  routeStore[page] = params
  uni.navigateTo({ url: pages[page] })
}

定义获取路由参数的方法:这里需要说明的是获取的路由参数应该是只读的,可以用vue的readonly方法包裹后返回:

import { readonly, DeepReadonly } from 'vue'

export function getRouteParams<T extends PageNames>(
  page: T,
): DeepReadonly<ObjectType<T>> {
  const p = routeStore[page] as ObjectType<T>
  return readonly(p)
}

处理navigateTo方法处理用户多次点击

let navigateLock = false
function navigate<T extends PageNames>(page: T, params?: ObjectType<T>) {
  if (navigateLock) return
  navigateLock = true
  routeStore[page] = params
  uni.navigateTo({
    url: pages[page],
    complete() {
      navigateLock = false
    },
  })
}

路由返回增加参数

要实现这个功能,我们可以借助uniapp跨页面通信API:uni.emit及uni.emit 及uni.emituni.once。

(如果有不了解该的同学可以移步uniapp文档了解:点击这里

然后在调用navigateTo进行跳转时,将事件名传递给下个页面,下个页面在调用navigateBack返回时,通过传递过来的事件名调用uni.$emit触发事件。代码逻辑如下:

let navigateLock = false
function navigate<T extends PageNames>(
  page: T,
  params?: ObjectType<T>,
): Promise<any> {
  if (navigateLock) return
  const eventName = Math.floor(Math.random() * 1000) + new Date().getTime() + '' // 生成唯一事件名
  navigateLock = true
  routeStore[page] = params
  uni.navigateTo({
    url: `${pages[page]}?eventName=${eventName}`, // 这里将触发事件名传递给下个页面
    complete() {
      navigateLock = false
    },
  })

  return new Promise<any>(
    (resolve, reject) => (
      uni.$once(eventName, resolve), uni.$once(eventName, reject)
    ),
  )
}

interface BackParams {
  /** 返回页面层级 */
  delta: number
  /** 返回携带的数据 */
  data: any
}

function back({ delta, data }: BackParams = { delta: 1, data: null }) {
  // 获取当前路由信息
  const currentRoute = getCurrentPages().pop()
  // 拿到路由事件名参数
  const eventName = currentRoute.options.eventName
  uni.$emit(eventName, data)
  uni.navigateBack({
    delta,
  })
}

完整内容如下:

import { CourseDetails } from './types'
import pages from './pages'

type PageNames = keyof typeof pages

type ObjectType<T> = T extends 'courseDetails' ? CourseDetails : never

const routeStore = {} as Record<PageNames, unknown>

export function getRouteParams<T extends PageNames>(
  page: T,
): DeepReadonly<ObjectType<T>> {
  const p = routeStore[page] as ObjectType<T>
  return readonly(p)
}

let navigateLock = false
function navigate<T extends PageNames>(
  page: T,
  params?: ObjectType<T>,
): Promise<any> {
  if (navigateLock) return
  const eventName = Math.floor(Math.random() * 1000) + new Date().getTime() // 生成唯一事件名
  navigateLock = true
  routeStore[page] = params
  uni.navigateTo({
    url: `${pages[page]}?eventName=${eventName}`,
    complete() {
      navigateLock = false
    },
  })

  return new Promise<any>(
    (resolve, reject) => (
      uni.$once(eventName, resolve), uni.$once(eventName, reject)
    ),
  )
}

function redirect<T extends PageNames>(page: T, params?: ObjectType<T>) {
  routeStore[page] = params
  uni.redirectTo({ url: pages[page] })
}

function reLaunch<T extends PageNames>(page: T, params?: ObjectType<T>) {
  routeStore[page] = params
  uni.reLaunch({ url: pages[page] })
}

function switchTab<T extends PageNames>(page: T, params?: ObjectType<T>) {
  routeStore[page] = params
  uni.switchTab({ url: pages[page] })
}

interface BackParams {
  /** 返回页面层级 */
  delta?: number
  /** 返回携带的数据 */
  data?: any
}

function back({ delta, data }: BackParams = { delta: 1, data: null }) {
  const currentRoute = getCurrentPages().pop()
  const eventName = currentRoute.options.eventName
  uni.$emit(eventName, data)
  uni.navigateBack({
    delta,
  })
}

const router = {
  navigate,
  redirect,
  reLaunch,
  switchTab,
  back,
}

export default router

使用示例:

路由跳转

uniapp+vue3 setup+ts 开发小程序实战(路由篇)

下一页获取路由参数

uniapp+vue3 setup+ts 开发小程序实战(路由篇)

返回携带参数

function next() {
  router
    .navigate('courseDetails', { id: 1 })
    .then((data) => {
      console.log('上个页面返回的数据')
      console.log(data)
    })
    .catch((err) => {
      console.log(err)
    })
}


function pageBack() {
   router.back({
    data: {
      msg: '这是返回携带的数据',
    },
  })
}

原生路由传参简化取参

以上路由传参是假设在用户从首页打开小程序的情况下,但是有些场景下,如分享进入,消息通知点击进入,这时候携带的参数只能在url中。对此,uniapp也针对获取路由参数方式做了简化,可以通过定义 props 来直接接收 url 传入的参数,而不必在onload生命周期钩子中获取。

<script setup>
  // 页面可以通过定义 props 来直接接收 url 传入的参数
  // 如:uni.navigateTo({ url: '/pages/index/index?id=10' })
  const props = defineProps({
    id: String,
  });
  console.log("id=" + props.id); // id=10
</script>

这时,对于一个多入口打开的页面,要采取哪种方式获取路由参数,可以根据场景值判断,详见文档场景值

总结

至此,我们已经完成了路由的优化封装目标。现在总结下使用步骤:

  • pages.json中新增页面路径
  • router -> pages.ts 新增页面别名配置
  • 如需传参,在types中增加参数类型接口
  • router -> index.ts中修改ObjectType,如新增一个Order,ObjectType修改如下:
type ObjectType<T> = T extends 'courseDetails'
  ? CourseDetails
  : T extends 'order'
  ? Order
  : never
转载自:https://juejin.cn/post/7120160996475289607
评论
请登录