浙里办踩坑之路

  • 前言
  • 前期准备
  • 开发过程
  • 1、引入插件
  • 2、适老化
  • 3、请求封装及rpc接入
  • 1)MGOP接口配置
  • 2)封装MGOP请求
  • 4、单点登录
  • 5、二次回退
  • 6、埋点
  • 总结


前言

现在距离浙里办微应用上架也过去了很久,也想写一篇博客记录这次悲催的踩坑之路,遇到的问题不多,好像原因也很奇葩,用到的功能也不多,但是都是泪~
介绍下这次的项目吧,是科普类的h5微应用,页面项目是原来的微信公众号的手机端项目, 要求浙里办app、支付宝、微信三端上架。

前期准备

接到开发任务的时候,首先加入了浙里办的答疑群,答疑基本上也只能提供文档,具体的坑还是得自己填啦。

1、群文件可以下载相关的开发文档和debug工具;
2、下载政务中台debug工具,登录以后前端可以进行本地调试(需要手机端浙里办app与pc端同宽带,并保证开启状态);
3、先登录应用商开发平台(pc网页通过浙里钉登录)获取appkey,servicecode,servicePassword;
4、由于浙里办前端是通过上传前端源代码进行上传的,,所以前端只能由npm run
build命令且生成文件夹名称为build目录,需要在前端打包配置文件内设置outputDir:‘build’;

开发过程

1、引入插件

在入口的/public/index.html中插入JSBridge,ZWJSBridge使用1.1.0版本,zwlog是埋点使用

<!-- index.html -->
 <script type="text/javascript" src="https://assets.zjzwfw.gov.cn/assets/ZWJSBridge/1.1.0/zwjsbridge.js"></script>
 <script src="https://assets.zjzwfw.gov.cn/assets/zwlog/1.0.0/zwlog.js"></script>

2、适老化

由于浙里办上架需要在原本的项目基础上,在用户切换浙里办App中的长辈版后切换到“大字体”版本。
我的思路是在app.vue里面调用下列工具类方法,在进入应用时获取当前模式normal or elder,(下列代码省略vuex 存储uiStyle过程)

// 浙里办工具类js文件
// src\utils\zlb.js
import store from '@/store'
export const getUIStyle = () => {
  ZWJSBridge.onReady(() => {
    // console.log('zwjsbrige 可调用')
    ZWJSBridge.getUiStyle()
      .then((result) => {
        if (result.uiStyle == 'elder') {
          store.dispatch('zlb/changeUiStyle', result.uiStyle);
        } else {
          store.dispatch('zlb/changeUiStyle', 'normal');
        }
        // console.log(result, 'getUiStyle result---')
      })
      .catch((error) => {
        console.log(error, '模式error');
      });
  })
}

页面中长辈版获取方式

<script>
export default {
  computed: {
    ...mapGetters(['uiStyle']),
  }
}
</script>

<style lang="less" scoped>
  .elder-mode{

  } 
</style>

<template>
  <div>
  <div :class="{ 'elder-mode': uiStyle == 'elder' }">
      标题
    </div>
  </div>
  </template>

3、请求封装及rpc接入

1)MGOP接口配置

可以交由后端在工作台配置接口名称传参,API名称固定 mgop.浙政钉稳定监控不成功_前端{api_name}这种形式
tips:前端就通过mgop这个方法请求rpc接口来请求服务器接口,可以参照demo项目尝试请求一个demo接口
由于当前项目是原先微信公众号的h5页面,项目文件中有一个微信的js文件,导致按照demo怎么请求都不通,通过js的debug一步步才得知由于那个文件导致工具包判断当前环境为微信环境从而接口一直不通,也是出现的一个特殊并且奇葩的坑。

2)封装MGOP请求

类似于其他项目axios请求封装方法,我对mgop也进行了封装如下。(此处可以用一个未登录拦截的接口进行测试)

// 浙里办工具类js文件
// src\utils\zlb.js
import Vue from 'vue'
import { showLoading, hideLoading } from './loading';//loading组件 
import qs from 'qs'
// 请求
export const mgopRequest = (payload) => {
  return new Promise((resolve, reject) => { 
     !payload.noLoading&&showLoading()  
    let headerObj = {};
    if (!payload.noToken) {
      headerObj = Vue.prototype.$utils.getMgopToken() //获取登录的token,在单点登录时候再行介绍
    }
    const param = payload.jsonParam ? qs.stringify(payload.data) : payload.data 
    mgop({
      api: payload.zwApi, // 必须,政务中台rpc接口的制定名字。例如‘mgop.xxx.app.login’
      host: 'https://mapi.zjzwfw.gov.cn/',// 固定为这个地址
      dataType: payload.dataType || 'JSON', // 目前官方只支持JSON格式
      data: param || {}, // 不管入参是query还是body都同意用data传入,到时候在工作台里配置即可
      type: payload.method || 'POST', // 和axios一样
      header: payload.noToken ?
        {} :
        { timestamp: headerObj.timestamp + '', token: headerObj.token, signs: headerObj.signs },//需要拦截的接口设置noToken:false
      appKey: 'qe66wu25+2002243131+ouneox', // 必须,政务中台appKey
      // 成功后的回调
      onSuccess: data => {
        console.log(data, payload.zwApi, '成功了------')
        !payload.noLoading&&hideLoading()
        if (data.data.status != 200) {
          Vue.prototype.$toast.fail(data.data.message)
        }
        // console.log(data.data,'失败了------')
        resolve(data.data)
      },
      // 请求失败后的回调
      onFail: err => {
        !payload.noLoading&&hideLoading()
        // alert('失败了'+payload.zwApi)
        console.log(param, err, payload.zwApi, '失败了------')
        reject(err)
      }
    })
  })
}
//main.js全局挂载
import { mgopRequest } from '@/utils/zlb.js'
Vue.prototype.$mgopRequest = mgopRequest;
//页面调用
this.$mgopRequest({ zwApi: 'mgop.**.**.**', noToken: false, method: 'GET' }).then((successRes) => {
        if (successRes.status == 200) {
            //成功
        }
      });

4、单点登录

和其他平台微应用一样,浙里办进入微应用需要通过跳转提供的sso地址,登录获取应用登录人信息然后跳转回调页面(单点登录有对应的组件对接人,需要提供应用的相关信息让对接人 进行回调配置,未上架之前可以配置成测试地址。开发阶段可以设置前端开发跳转的ip白名单,从而通过redirectUrl传参传本地页面地址)

如下图所示,本应用默认登录回调页面就是业务登录页,根据三端获取方式不同获取业务token。

浙政钉稳定监控不成功_前端_02

// src/utils/index.js
  $router.beforeEach(async (to, from, next) => {
      document.title = process.env.VUE_APP_TITLE;
      let url = window.location.href
      if (($utils.getMgopToken() && $utils.getMgopToken().token) || to.path === '/disappear') {
        next()
      } else { 
        // 获取环境
        if ($store.state.zlb.appOrMini == 0) {
          let sUserAgent = window.navigator.userAgent.toLowerCase();

          if (sUserAgent.indexOf('dtdreamweb') > -1) {
            // 浙里办APP
            $store.dispatch('zlb/changeAppOrMini', 1);
            // sUserAgent.indexOf('miniprogram') > -1 && 
          } else if ( sUserAgent.indexOf('alipay') > -1) {
            //支付宝浙里办小程序
            $store.dispatch('zlb/changeAppOrMini', 2);
          } else if (sUserAgent.includes('miniprogram/wx') || window.__wxjs_environment === 'miniprogram') {
            //微信
            $store.dispatch('zlb/changeAppOrMini', 3);
          }
        } 
        const servicecode = $store.state.zlb.servicecode
        // 微信端
         if ($store.state.zlb.appOrMini == 3&&ZWJSBridge.ssoTicket) {  
           next()
        } else {
          // console.log(url) 
          // url无票据信息
          if (url.indexOf('ticket') < 0) {
            // 跳转验证页面,redirectUrl为回调地址
            let ssoUrl = ''
            if ($store.state.zlb.appOrMini == 1) {
              // &redirectUrl= 
              ssoUrl = `https://puser.zjzwfw.gov.cn/sso/mobile.do?action=oauth&scope=1&servicecode=${servicecode}`
            } else if ($store.state.zlb.appOrMini == 2) {
              ssoUrl = `https://puser.zjzwfw.gov.cn/sso/alipay.do?action=ssoLogin&servicecode=${servicecode} `
            }
            // 二次回退
              $utils.gobackByPageshow()
              window.location.replace(ssoUrl)
          }
          else {
            next();
          }
        }

      }

    });
// src/utils/index.js
 /**
        * 获取票据
        **/
          getTicket(){
            let ticket = null 
            const url = window.location.href
            if (url.indexOf('ticket') !== -1) {
              const params = url.split('?')[1].split('&')// 截取路由
              for (let index = 0; index < params.length; index++) { 
                if (params[index].indexOf("ticket=") != -1||params[index].indexOf("ticketId=") != -1) {
                  ticket = params[index].split('=')[1].split('#/')[0]// 获取路由ticket
                  break
                }
              }
            } 
            return { ticket }  
          } 
        /**
        * 验证Mgop请求Header参数
        **/
        getMgopToken(){
          const { server } = this.$vm.$store.state;
          const { zlb } = this.$vm.$store.state;
          const timestamp = new Date().getTime() + server.info.timestamp;
          const encry = `${timestamp},${zlb.token}`;
          const key = crypto.enc.Utf8.parse("abcdefgabcdefg12");
          let signs = crypto.AES.encrypt(encry, key, {
              mode: crypto.mode.ECB,
              padding: crypto.pad.Pkcs7
          }).toString();
          return { timestamp, signs, token:zlb.token }
        }
//业务登录页登录js
   async login() {
      const wxApi = 'mgop.**.**.**';
      const appApi = 'mgop.**.**.**';
      if (this.$store.state.zlb.appOrMini == 3 && ZWJSBridge.ssoTicket) {
        // 微信端
        const ssoFlag = await ZWJSBridge.ssoTicket({});
        // 微信单点登录
        if (ssoFlag && ssoFlag.result === true) {
          //使用 IRS“浙里办”单点登录组件
          if (ssoFlag.ticketId) {
            // const ticketId = ssoFlag.ticketId;
            // alert('第一次获取微信ticketId'+ssoFlag.ticketId)
            this.doLogin(wxApi, ssoFlag.ticketId);
          } else {
            // 当“浙里办”单点登录失败或登录状态失效时调用ZWJSBridge.openLink方法重新获取ticketId
            ZWJSBridge.openLink({ type: 'reload' })
              .then((res) => {
                // res.ticketId
                this.doLogin(wxApi, res.ticketId);
              })
              .catch((err) => {});
          }
        }
      } else {
        const { ticket } = this.$utils.getTicket();
        // 支付宝和微信使用wxApi,app使用appApi
        let api = this.$store.state.zlb.appOrMini == 2 ? wxApi : appApi;
        this.doLogin(api, ticket);
      }
    },
    // 调用登录api
    doLogin(api, ticket) {
    // vuex登录存储
      this.$store
        .dispatch('zlb/ZWLogin', { zwApi: api, noToken: true, method: 'GET', data: { ticketId: ticket } }) // ! 关键步骤
        .then((res) => {
          // alert(JSON.stringify(res))
          // console.log(res); 
          this.$router.replace('/home');
        })
        .catch((err) => {
          console.log('err', err); 
          this.$router.replace('/disappear');
        });
    },
  },
// src/store/modules/zlb.js
import { mgopRequest } from '@/utils/zlb.js'
//...
//actions
 ZWLogin({ commit }, payload) { 
      return new Promise((resolve, reject) => {
        mgopRequest(payload)
          .then((successRes) => {
            //console.log(successRes, 'ZWLogin 成功');
            if (successRes.status === 500) return reject(successRes.message)
            if (successRes.status != 200) return reject('票据已过期,请重新登录')
            const response = successRes.data 
            commit('_changeZlbUserInfo', response)
            commit('_changeToken', response.token)
            resolve(response)
          },
            (fail) => {
              //console.log(fail, 'ZWLogin api失败');
              return reject('ZWLogin api失败')
            }).catch((err) => {
              //console.log(err, 'ZWLogin -action失败');
            })
      })
    },

注:我在对接单点登录的时候还有一个坑,就是微信小程序端有时候用wifi登录不了,有时候可以访问,最后发现是@aligov/jssdk-mgop版本的问题,之前是3.0.0,后来改成了3.1.9

5、二次回退

在页面返回的时候会发现返回到浙里办目录页之前需要按两次返回,针对这个也进行了监听处理,并在路由拦截时候进行了监听调用

// src/utils/index.js
  // 监听二次回退
         gobackByPageshow() {
        window.onpageshow = (event) => {
          if (
            event.persisted ||
            (window.performance && window.performance.navigation.type == 2)
          ) {
            ZWJSBridge.close();
          }
        };
         }

6、埋点

由于上线基本要求就是获取埋点数据,记录每个页面的相关信息

const appid = "******"; // 此处填上你自己的 appid
    let reponsedDuration = 0; // 页面响应时长
    let loadedDuration = 0; // 页面加载完成时长
    // 在声明Zwlog对象实例时,可以传入一些app或者用户信息
    window.zwlog = new ZwLog();
    window.onload = function () {
      showNavigationDetails();
      //加载时间 
      //onReady表示zwlog加载完成后的函数,它接收一个匿名函数,而sendPV与record方法均要在匿名函数内调用。eg:
      zwlog.onReady(function () {
        //PV日志
        zwlog.sendPV({
          miniAppId: appid, // IRS 服务侧应用 appid
          t2: loadedDuration, // 页面启动到加载完成的时间
          t0: reponsedDuration, // 页面启动到页面响应完成的时间
          log_status: appid, // IRS 服务侧应用 appid
        });
      });
    };

    function showNavigationDetails() {
      // 入口
      const [entry] = performance.getEntriesByType("navigation");
      console.log("showNavigationDetails", entry.toJSON());
      reponsedDuration = entry.responseEnd - entry.responseStart;
      loadedDuration = entry.domComplete;
      console.log("页面响应时长:", reponsedDuration);
      console.log("页面加载完时长:", loadedDuration);
    }

由于要对每个页面进行埋点操作,所以需要提取一个全局复用的方法,那么我就想起了mixin,vue3的项目也可以用hook

Vue.mixin({
      data() {
        return {
          zwlog: null,
          pageTime: {
            created: Date.now(),
            mounted: '',
            deactived: ''
          }
        }
      },
      mounted() { 
        this.pageTime.mounted = Date.now()
      }, 
      computed: {
        // userStore() {
        //   return this.$store.state.user
        // },
        pageStore() {
          return this.$store.state.page
        }
      },

      methods: {

      },

      filters: {

      },
      beforeRouteLeave(to,form,next) {
        // 浙里办埋点
        const that =this;   
        this.pageTime.deactived = Date.now()
        window.zwlog.sendPV({
          pageId:'**'+that.$route.meta.appid,
          pageName:'***'+that.$route.name,
          miniAppId: '***', // IRS 服务侧应用 appid
          miniAppName: '***',
          Page_duration:(that.pageTime.deactived-that.pageTime.created)/1000,// 从进入到离开当前页面的时长
          t2: (that.pageTime.mounted-that.pageTime.created)/1000, // 页面启动到加载完成的时间
          t0:  (that.pageTime.mounted-that.pageTime.created)/1000, // 页面启动到页面响应完成的时间
          log_status:  store.state.zlb.token?'02':'01', // 登录为02 未登录为01
          _user_id: store.state.zlb.zlbUserInfo? store.state.zlb.zlbUserInfo.zlbuserId:''
        })
        next()
      },
       
    })

总结

浙里办微应用开发的工作也算告一段落了,整理文档的过程也算是一次简单的复习(都是辛酸泪哇),主要还是开发平台没有一个很好的综合平台,没有类似于小程序开发这样的版本更新平台,指不定哪天更新一个api,更新一个文档。没有一个好的平台,却要花大量运维人员去帮忙解决问题,实际上好像也得不到很好的解决,也不知道是为什么。唉~