最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 统一路由,让小程序跳转更智能

    正文概述 掘金(胖纳特)   2021-02-20   561

    背景

    我们在小程序开发及运营过程中,不可避免的需要进行页面之间的跳转。如果使用小程序自带的路由功能来实现这个功能,是非常简单的,如:

    // 根据不同的场景选择 navigateTo、redirectTo、switchTab 等
    wx.navigateTo({
      url: "pages/somepage?id=1",
      success: function (res) {},
    });
    

    但这里面存在几个问题:

    • 需要代码里面写死或者运营人员维护小程序页面的长长的具体路径,这显然是很不友好的
    • 需要知道页面是否为 tabbar 页面(switchTab)
    • 如果某个页面在 tabbar 和非 tabbar 页面之间发生了变化,或路径因为重构、主包瘦身等各种原因发生变化,原来的代码就会报错导致无法运行
    • navigateBack 不支持传参

    为了解决以上问题,我们在项目中实现了一套基于命令别名(cmd)的统一路由跳转方式(以下称为统一路由),很好解决了遇到的实际问题,统一路由特点如下:

    • 页面别名声明使用注释方式,不侵入业务代码
    • 页面可以存在多个别名,方便新老版本页面的流量切换
    • 路由内自动判断是否 tabbar 页面,自行处理跳转及传参,业务代码无需关心
    • 支持纯 js api 的页面跳转及需要用户点击的任意类型跳转(如联系客服、打开小程序等等)
    • 对于页面栈中存在相同页面时,可以自动返回并根据参数是否相同决定是否需要刷新页面,可有效减少页面栈层级,规避小程序 10 层限制

    实现思路

    step1. 资源描述约定

    小程序内的跳转类操作存在以下几种

    1. js api 直接可以操作的内部页面间跳转(wx.navigateTo、wx.navigateBack、wx.redirectTo、wx.reLaunch、wx.switchTab)
    2. js api 直接可以操作的打开微信原生功能的跳转(扫码、拨打电话等)
    3. 需要借助点击操作的跳转(如打开小程序及客服等需要 open-type 配合的场景 )

    针对这三类操作,我们使用常见的 URL(统一资源定位系统)方式描述不同的待跳转资源

    1. 内部页面
    https://host?cmd=${pagename}&param1=a  // 打开普通页面并传参,标准的H5容器也算在普通页面内
    
    1. 微信原生 API
    https://host?cmd=nativeAPI&API=makePhoneCall&phoneNumber=123456  // 拨打电话
    https://host?cmd=nativeAPI&API=scanCode&callback=scanCallback // 扫码并执行回调
    
    1. 需要借助按钮 open-type 的微信原生能力
    https://host?cmd=nativeButtonAPI&openType=contact  // 在线客服
    
    1. 打开另一个小程序
    https://host?cmd=miniProgram&appId=wx637bb****&path=pages/order/index&version=trial&uid=${uid} 
    

    step2. 在页面内定义需要的数据

    在每个页面的顶部添加注释,注意 cmd 不能重复,支持多个 cmd。为了方便后续解析,我们的注释大体上遵循 JSDoc 注释规范

    // pages/detail/index.tsx
    
    /**
     * @cmd detail, newdetail
     * @description 详情
     * @param skuid {number} skuid
     */
    

    step3. 在编译阶段扫描并生成配置文件

    根据入口文件的页面定义,匹配出需要的注释部分,使用 doctrine 解析需要的数据,解析后的数据如下:

    // config/router.config.ts
    export default {
      index: {
        description: "首页", // 页面描述
        path: "/pages/index/index", // 真实路径
        isTabbar: true, // 是否tabbar页面
        ensureLogin: false, // 是否需要强制登录
      },
      detail: {
        description: "详情",
        path: "/pages/detail/index",
        isTabbar: false,
        ensureLogin: true,
      },
    };
    

    这里顺便可以使用 param 等生成详细的页面名称及入参文档,提供给其他研发或运营同学使用。

    step4. 资源描述解析为标准数据

    根据上面的资源描述约定及扫描得到的配置文件,我们可以将其转换为方便在小程序内解析的数据定义,基本格式如下

    {
        origin: 'https://host?cmd=detail&skuid=1',  // 原始数据
        parsed: {
            type: 'PAGE',  // 类型,PAGE,NATIVE_API,NATIVE_BUTTON_API,UNKNOW
            data: {
                path: 'pages/detail/index',  // 实际的页面路径,如果type是PAGE则会解析出此字段
                action: undefined, // 动作,scanCode,makePhoneCall,openType,miniprogram ……。如果type是NATIVE_API,NATIVE_BUTTON_API,则会解析出此字段
                params: {
                    skuid: '1'  // 需要携带的参数
                }
            }
        }
    }
    

    step5. 根据标准数据执行对应逻辑

    由于我们的项目使用的是 Taro 框架,以下伪代码都是以 Taro 为例。

    // utils/router.ts
    
    // 用于解析原始链接为标准数据
    const parseURL = (origin) => {
      // balabala,一顿操作格式化成上文的数据
      const data = {
          ...
      };
      return data;
    };
    
    // 执行除 NATIVE_BUTTON_API 之外的跳转
    const routeURL = (origin) => {
        const parsedData = parseURL(origin)
        const {parsed: {type, data}} = parsedData
    
        switch(type){
            case 'PAGE':
                ...
                break;
            case 'NATIVE_API':
                ...
                break;
            case 'UNKNOW':
                ...
                break;
        }
    };
    
    export default {
      parseURL,
      routeURL,
    };
    

    对于需要点击的类型,我们需要借助 UI 组件实现

    // components/router.tsx
    
    import router from "/utils/router";
    import { Button } from "@tarojs/components";
    import Taro, { Component, eventCenter } from "@tarojs/taro";
    
    export default class Router extends Component {
      componentWillMount() {
        const { path } = this.props;
        const data = router.parseURL(path);
        const { parsed, origin } = data;
        const openType =
          (parsed &&
            parsed.data &&
            parsed.data.params &&
            parsed.data.params.openType) ||
          false;
        this.setState({
          parsed,
          openType,
        });
      }
    
      // 点击事件
      async handleClick(parsed, origin) {
        // 点击执行动作
        let {
          type,
          data: { action, params },
        } = parsed;
        if (!type) {
          return;
        }
    
        // 内部页面
        if (["PAGE", "CMD_UNKNOW"].includes(type)) {
          console.log(`CMD_NATIVE_PAGE 参数:`, origin, options);
          router.routeURL(origin);
          return;
        }
    
        // 拨打电话、扫码等原生API
        if (["NATIVE_API"].includes(type) && action) {
          if (action === "makePhoneCall") {
            let { phoneNumber = "" } = params;
            if (!phoneNumber || phoneNumber.replace(/\s/g, "") == "") {
              Taro.showToast({
                icon: "none",
                title: "未查询到号码,无法呼叫哦~",
              });
              return;
            }
          }
    
          let res = await Taro[action]({ ...params });
    
          // 扫码事件,需要在扫码完成后发送全局广播,业务内自行处理
          if (action === "scanCode" && params.callback) {
            let eventName = `${params.callback}_event`;
            eventCenter.trigger(eventName, res);
          }
        }
    
        // 打开小程序
        if (
          ["NATIVE_BUTTON_API"].includes(type) &&
          ["miniprogram"].includes(action)
        ) {
          await Taro.navigateToMiniProgram({
            ...params,
          });
        }
      }
    
      render() {
        const { parsed, openType, origin } = this.state;
    
        return (
          <Button
            onClick={this.handleClick.bind(this, parsed, origin)}
            hoverClass="none"
            openType={openType}
          >
            {this.props.children}
          </Button>
        );
      }
    }
    

    在具体业务中使用

    // pages/index/index.tsx
    import router from "/utils/router";
    import Router from "/components/router";
    
    // js方式直接跳转
    router.routeURL('https://host?cmd=detail&skuid=1')
    
    // UI组件方式
    ...
    render(){
        return <Router path='https://host?cmd=detail&skuid=1'></Router>
    }
    ...
    

    当然这里面可以附加你自己需要的功能,比如:增加跳转方式控制、数据处理、埋点、加锁防连续点击,相对来说并不复杂。甚至你还可以顺手实现一下上面提到的 navigateBack 传参。

    结语

    上文的思考及实现过程比较简单,纯属抛砖引玉,欢迎大家交流互动。


    起源地下载网 » 统一路由,让小程序跳转更智能

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元