小程序(六)内嵌H5的终极解决方案

这是我参与更文挑战的第3天,活动详情查看: 更文挑战html

1、小程序和H5的区别

1.1 运行环境方面

从运行环境方面开看,H5 的宿主环境是浏览器,只要有浏览器,就可使用,包括APP中的 web-view 组件,以及小程序提供的 web-view 组件小程序就不同了,它运行于特定的移动软件平台 (Wechat / 支付宝 / 字节跳动 / 百度 / QQ 等)。react

拿微信小程序来讲,它是基于浏览器内核重构的内置解析器,它并非一个完整的浏览器,官方文档中重点强调了脚本内没法使用浏览器中经常使用的 window 对象和 document 对象,就是没有 DOM 和 BOM 的相关的 API,这一条就干掉了 JQ 和一些依赖于 BOM 和 DOM 的NPM包。android

1.2 运行机制方面

H5 的运行就是一个网页的运行,这里不过多叙述,小程序仍是以微信小程序举例。ios

1.2.1 启动

若是用户已经打开过某小程序,在必定时间内再次打开该小程序,此时无需从新启动,只需将后台态的小程序切换到前台,整个过程就是所谓的 热启动。若是用户首次打开或小程序被微信主动销毁后再次打开的状况,此时小程序须要从新加载启动,就是 冷启动web

1.2.2 销毁

当小程序进入后台必定时间,或系统资源占用太高,或者是你手动销毁,才算真正的销毁正则表达式

1.3 系统权限方面

H5最被诟病的地方在哪?系统权限不够,好比网络通讯状态、数据缓存能力、通信录、或调用硬件的,如蓝牙功能等等一些APP有的功能,H5就没有这些系统权限,由于它重度依赖浏览器能力,依旧是微信小程序举例,微信客户端的这些系统级权限均可以和微信小程序无缝衔接,官方宣称拥有 Native App 的流畅性能。json

1.4 开发编码层面

H5 开发你们都知道,标准的 HTML、CSS、JavaScript ,万变不离其三剑客小程序不一样, (Wechat / 支付宝 / 字节跳动 / 百度 / QQ 等)不一样的小程序都有本身定义独特的语言最经常使用的微信小程序,自定义的 WXML、WXSS,WXML 中所有是微信自定义的标签,WXSS、JSON 和 JS 文件中的写法都稍有限制,官方文档中都有明确的使用介绍,虽容易上手,但仍是有区别的。小程序

1.5 更新机制方面

H5 的话想怎么更新就怎么更新,更新后抛开CDN/浏览器缓存啥的,基本上更新结束刷新就能够看到效果 小程序不一样,仍是微信举例,嘿嘿,微信小程序更新啥的是须要经过审核的。并且开发者在发布新版本以后,没法马上影响到全部现网用户,要在发布以后 24 小时以内才下发新版本信息到用户 小程序每次 冷启动 时,都会检查有无更新版本,若是发现有新版本,会异步下载新版本代码包,并同时用客户端本地包进行启动,因此新版本的小程序须要等下一次 冷启动 才会应用上,固然微信也有 wx.getUpdateManager 能够作检查更新windows

1.6 渲染机制方面

H5就是 web 渲染,浏览器渲染。微信小程序的宿主环境是微信,宿主环境为了执行小程序的各类文件:wxml文件、wxss文件、js文件,提供了双线模型。后端

2、小程序环境分析

小程序的渲染层和逻辑层分别由两个线程管理:

image.png

  • 渲染层的界面使用 WebView 进行渲染,一个小程序存在多个界面,因此渲染层存在多个 WebView。
  • 逻辑层采用 JSCore 线程运行 JavaScript 脚本。

这两个线程间的通讯经由小程序 Native 侧中转,逻辑层发送网络请求也经由 Native 侧转发。如此设计的初衷是为了管控和安全,微信小程序阻止开发者使用一些浏览器提供的,诸如跳转页面、操做 DOM、动态执行脚本的开放性接口。将逻辑层与视图层进行分离,视图层和逻辑层之间只有数据的通讯,能够防止开发者随意操做界面,更好的保证了用户数据安全。

三端的脚本执行环境以及用于渲染非原生组件的环境是各不相同的:

运行环境 逻辑层 渲染层
Android V8 Chromium 定制内核
IOS JavaScriptCore WKWebView
小程序开发者工具 NWJS Chrome WebView

运行环境逻辑层渲染层 AndroidV8Chromium 定制内核IOS JavaScriptCoreWKWebView 小程序开发者工具NWJSChrome WebView小程序的视图是在WebView里渲染的,那搭建视图的方式天然就须要用到HTML语言。可是HTML语言标签众多,增长了理解成本,并且直接使用HTML语言,开发者能够利用<a>标签实现跳转到其余在线网页,也能够动画执行 JAVAScript,前面所提到的为解决管控与安全而创建的双线程模型就成摆设了。

所以,小程序设计一套组件框架—— Exparser 。基于这个框架,内置了一套组件,以涵盖小程序的基础功能,便于开发者快速搭建出任何界面。同时也提供了自定义组件的能力,开发者能够自行扩展更多的组件,以实现代码复用。值得一提的是,内置组件有一部分较复杂组件是用客户端原生渲染的,以提供更好的性能。

3、H5浏览器环境分析

你们都知道,浏览器缓存是个很是有用的特性,它可以提高性能、减小延迟,还能够减小带宽、下降网络负荷。关于浏览器的缓存机制能够总结成下面 2 句话:

  • 浏览器每次发起请求,都会先在浏览器缓存中查找该请求的结果以及缓存标识
  • 浏览器每次拿到返回的请求结果都会将该结果和缓存标识存入浏览器缓存中

更进一步,咱们能够粗略了解一下强制缓存和协商缓存的运行机理。若强制缓存(Expires 和 Cache-Control)生效则直接使用缓存,若不生效则进行协商缓存(Last-Modified/If-Modified-Since 和 Etag/If-None-Match),协商缓存由服务器决定是否使用缓存,若协商缓存失效,那么表明该请求的缓存失效,返回 200,从新返回资源和缓存标识,再存入浏览器缓存中;生效则返回 304,继续使用缓存。这段文字是想让读者拓展一下知识面,若是想要更输入了解,能够经过上面的一些关键字(强缓存、协商缓存、Expire、Cache-Control 等)去查找更详细的资料。 微信的 web-view 组件就是一个嵌在小程序里的浏览器,它在缓存上并无彻底遵守上述的规则,也即它的缓存并不能及时获得清理。想必下面的操做你们都有尝试过:

  • 手动退出小程序,再次进入;
  • 将微信从后台退出再打开并从新进入小程序;
  • 修改 Nginx 关于 Cache-Control 的配置;
  • 用 debugx5.qq.com 手动清除安卓微信浏览器缓存;
  • iOS 利用微信自带清楚缓存功能。

没法及时刷新缓存会致使发布了最新的页面,而小程序里仍然是之前的页面,从而会带来许多问题,如先后端的数据不一致,新的特性没法及时起做用,修改的问题没有获得解决等等。这里须要说明一下:web-view 在过一段时间(时间不定,一天或者几小时,无明显规律)是能够进行缓存刷新的,而本 Chat 要解决的是及时刷新的问题。

3.1 小程序中h5页面onShow和跨页面通讯的实现

首先想到的就是onShow方法的实现,以前有人提议用visibilitychange来实现onShow方法。但调研事后,这种方式在ios中表现符合预期,可是在安卓手机里,是不能按预期触发的。因而就有了下面的方案,这个方案须要h5和小程序的webview都作处理。

核心思想:利用webview的hash特性

  • 小程序经过hash传参,页面不会更新(这个和浏览器同样)
  • h5能够经过hashchange捕获最新参数,进行自定义逻辑处理
  • 最后执行window.history.go(-1)

为何要执行window.history.go(-1) ? 由于hash变动会致使webview历史栈长度+1,用户须要多一次返回操做。但这一步明显是多余的。同时window.history.go(-1)后,会把webview在hash中添加的参数去掉,还能保证和以前的url一致。

3.2 注意点

出于平滑接入的考虑,不能上来搞一刀切,要保证现有页面再不作任何修改的状况下继续访问。新能力要经过额外参数区分,如:检测url中的query部分,带有__isonshowpro=1再进行经过hash方式传参。改造原有逻辑,让__isonshowpro=1时,hash处理逻辑优先级最高参数定义,在前面加入了两个下划线,目的是为了分区url中正常的参数。咱们来看看h5端的sdk是怎么实现的

import util from './util';

class WASDK {
  /** * Create a instance. * @ignore */
  constructor(){
    // hashchang事件处理
    if('onhashchange' in window && window.addEventListener && !WASDK.hashInfo.isInit){
      // 更新标志位
      WASDK.hashInfo.isInit = true;
      // 绑定hashchange
      window.addEventListener('hashchange', ()=>{
        // 若是小程序webview修改的hash,才进行处理
        if (util.getHash(window.location.href, '__wachangehash') === '1') {
          // 这块有个坑:
          // ios小程序webview在修改完url的hash以后,页面hashchange和更新均可以正常触发
          // 可是:h5调用部分小程序能力会失败(如:ios在设置完hash后,调用wx.uploadImg会失败,须要从新设置wx.config)
          // 由于ios小程序的逻辑是,url只要发生变化,wx.config中的appId就找不到了
          // 因此须要从新进行wx.config配置
          // 这一步是获取以前设置wx.config的参数(须要从服务端拿,由于以前已经获取过了,这里从缓存直接取)
          const jsticket = window.native && window.native.adapter && window.native.adapter.jsticket || null;
          const ua = navigator.userAgent;
          // 非安卓系统要从新设置wx.config
          if (jsticket && !(ua.indexOf('Android') > -1 || ua.indexOf('Adr') > -1)) {
            window.wx.config({
              debug: false,
              appId: jsticket.appId,
              timestamp: jsticket.timestamp,
              nonceStr: jsticket.noncestr,
              signature: jsticket.signature,
              jsApiList: ['onMenuShareTimeline', 'onMenuShareAppMessage', 'onMenuShareQQ',
                'onMenuShareQZone', 'onMenuShareWeibo', 'scanQRCode', 'chooseImage', 'uploadImage', 'previewImage', 'getLocation', 'openLocation']
            })
          }
          // 触发缓存数组的回调
          WASDK.hashInfo.callbackArr.forEach(callback=>{
            callback();
          })
          // 执行返回操做(这一步是重点!!)
          // 由于webview设置完hash参数后,会使webview历史栈+1
          // 而实际并不须要此次多余的历史记录,因此须要执行返回操做把它去掉
          // 即使是返回操做,也仅仅是hash层面的变动,因此不会触发页面刷新
          // 用setTimeout表示在下一次事件循环进行返回操做。若是后面有对dom操做能够在当前次事件循环完成
          setTimeout(()=>{
            window.history.go(-1);
          }, 0);
        }
      }, false)
    }
  }

  /** * hash相关信息 */
  static hashInfo = {
    // 是否已经初始化
    isInit: false,
    // hash回调数组
    callbackArr: []
  }

  /** * 页面再次展现时钩子方法 * @param {Function} callback - 必填, callback回调方法, 回传参数为hash部分问号后面的参数解析对象 */
  @execLog
  onShow(callback){
    if (typeof callback === 'function') {
      // 对回调方法进行onshow逻辑包装,并推入缓存数组
      WASDK.hashInfo.callbackArr.push(function(){
        // 检查是不是指定参数发生变化
        if(util.getHash(window.location.href, '__isonshow') === '1'){
          // 触发onShow回调
          callback();
        }
      })
    } else {
      util.console.error(`参数错误,调用onShow请传入正确callback回调`);
    }
  }

  /** * 业务处理完成并发送消息 * @param {Object} obj - 必填项,消息对象 * @param {String} obj.key - 必填项,消息名称 * @param {String} obj.content - 可选项,消息内容,默认空串,若是是内容对象,请转换成字符串 * @param {String|Number} condition - 可选项,默认仅进行postMessage * String - 能够传指定url的路径,当小程序webview打开指定的url或者onshow时,会触发该消息 * 也可传小程序path,这个为之后预留 * Number - 返回到指定的测试,相似history.go(-1),如: -1,-2 */
  @execLog
  serviceDone(obj, condition){
    if(obj && obj.key){
      // 消息体
      const message = {
        // 消息名称
        key: obj.key,
        // 消息体
        content: obj.content || '',
        // 触发条件
        trigger: {
          // 类型 'immediately'在下一次onshow中马上触发, 'url',在找到指定h5连接时触发,'path'在打开指定小程序路径时触发
          type: 'immediately',
          // 条件内容,immediately是为空,url是为h5连接地址,path是为小程序路径
          content: ''
        }
      };
      // 解析触发条件
      condition = condition || 0;
      // 若是是路径
      if(typeof condition === 'string' && (condition.indexOf('http') > -1 || condition.indexOf('pages/') > -1)){
        // 设置消息触发条件
        message.trigger = {
          type: condition.indexOf('http') > -1 ? 'url' : 'path',
          content: condition
        }
      }
      // 发送消息
      wx.miniProgram.postMessage({
        data: {
          messageData: message
        }
      });
      // 若是不是url或者path触发,则对conditon是否须要返回进行判断
      if(message.trigger.type === 'immediately'){
        // 查看是否须要返回指定的层级,兼容传入'-1'字符串这种类型的场景
        try{
          condition = parseInt(condition, 10);
        }catch(e){}
        // 保证返回级数的正确性
        if(condition && typeof condition === 'number' && !isNaN(condition)){
          this.handler.navigateBack({delta: Math.abs(condition)});
        }
      }
    }else{
      util.console.error(`参数错误,调用serviceDone方法,传入的对象中不包含key值`);
    }
  }

  ...
}

window.native = new Native();
export default native;
复制代码

这个看着也挺多,总结下来是两点:

onShow方法的实现

绑定一个hashchange事件(这里作了防止重复绑定事件的处理),将传入的onShow自定义事件缓存在一个数组中,hashchange触发时,根据特有的标志位__isonshow和__wachangehash肯定是否触发

serviceDone方法的实现

处理传过来的数据,处理该数据的触发条件:immediately表示最近的一次onShow触发,或者本身指定url经过wx.miniProgram.postMessage发送数据

浏览器访问资源是经过 URL 地址,若是内嵌 H5 的地址不发生变化,那么 web-view 访问资源会从缓存里取,而缓存里并无最新的数据,这就致使了服务端的最新资源根本没法到达浏览器,这也就解释了为何修改 Nginx 的 Cache-Control 配置也没法生效的缘由。因此,要想完全解决及时刷新,必须让 web-view 去访问新的地址。咱们假定小程序访问的 URL 地址为:https://www.yourdomain.com/101/#/index其中 101 就是构建的一个版本号,每次递增,保证次次不一样便可。

3.4 如何判断小程序当前页面所处的环境

这部分须要在H5页面种下一个sdk,好比名字就叫bridge.js,下面是我作了几年小程序总结出来的经常使用方法:

// bridge.js
let ua = window.navigator.userAgent.toLowerCase();
const globalObj = {
    testDataArr: [],
    doJSReadyFuncExecuted: false,
    errorInfo: '',
    miniappSDK: null,
    miniappType: '',
    actionQueue: [],
    MINIAPP_TYPE: {
        WECHATMINIAPP:  'WECHATMINIAPP',// miniprogram
        WECHATAPP:      'WECHATAPP',    // miniprogram + offiaccount
        OLDQUICKAPP:    'OLDQUICKAPP',  // old
        NEWQUICKAPP:    'NEWQUICKAPP',  // new
        ALIPAYAPP:      'ALIPAYAPP',
        BAIDUAPP:       'BAIDUAPP',
        TOUTIAOAPP:     'TOUTIAOAPP',
        QQAPP:          'QQAPP'         // No longer maintained
    },
    JSSDK_URL_OBJ: {
        WECHATMINIAPP: 'https://res.wx.qq.com/open/js/jweixin-1.6.0.js',
        WECHATAPP: 'https://res.wx.qq.com/open/js/jweixin-1.6.0.js',
        OLDQUICKAPP: 'https://xxxxxxxx/amsweb/quickApp/mixBridge.js',
        NEWQUICKAPP: 'https://quickapp/jssdk.webview.min.js',
        ALIPAYAPP: 'https://appx/web-view.min.js',
        BAIDUAPP: 'https://b.bdstatic.com/searchbox/icms/searchbox/js/swan-2.0.21.js',
        TOUTIAOAPP: 'https://s3.pstatp.com/toutiao/tmajssdk/jssdk-1.0.1.js',
        QQAPP: 'https://qqq.gtimg.cn/miniprogram/webview_jssdk/qqjssdk-1.0.0.js'
    },
    bversion: '1.0.0'
}

if(typeof window['__bfi'] == 'undefined') {
    window['__bfi'] = [];
};

window['__bfi'].push([
    '_tracklog', 
    '174537', 
    `ua=${ua}&pageId=\${page_id}`
]);

function isAndroid () {
    return ua.includes('android');
}

function isWechatMiniapp () {
    // @source https://developers.weixin.qq.com/community/develop/doc/00022e37c78b802f186750b5751000
    // in wechat && (in android || in ios)
    return isWechat() && (ua.includes('miniprogram') || window.__wxjs_environment === 'miniprogram');
}

function isWechat () {
    // in wechat-web-browser
    // https://blog.csdn.net/daipianpian/article/details/86543080
    // @source blog ( https://www.jianshu.com/p/6a10f833b099 )
    return /micromessenger/i.test(ua) || /windows phone/i.test(ua);
}

function isOldQuickapp () {
    return (/(hap|OPPO\/Hybrid)\/\d/i.test(ua)) && !isNewQuickapp();
}

function isNewQuickapp () {
    // @source 2020.04.10, Vivo( Li Chunjiao ) has confirmed that this method is feasible
    return ua.includes('mode-quickapp');
}

function isAlipay () {
    // @source 2020.06.15, Alipay has confirmed that this method is feasible
    let isAli = (/APXWebView/i.test(ua)) || (/MiniProgram/i.test(ua) && !ua.includes('micromessenger'));
    // @source 2020.11.17, https://www.yuque.com/books/share/6d822c34-9121-47d8-a805-4c57b0b2d2f0/hiv1tn
    let isUCKuake = ua.includes('aliapp') && (ua.includes('uc') || ua.includes('quark'));
    // @source 2021.03.26
    let isGaode = ua.includes('aliapp') && ua.includes('amapclient');
    return isAli || isUCKuake || isGaode;
}

function isBaidu () {
    // @source 2020.11.05, baidu's doc ( https://smartprogram.baidu.com/docs/develop/component/open_web-view/ )
    return /swan\//.test(ua) || /^webswan-/.test(window.name);
}

function isToutiao () {
    // @source 2020.11.05, toutiao's doc ( https://microapp.bytedance.com/docs/zh-CN/mini-app/develop/component/open-capacity/web-view/ )
    return ua.includes("toutiaomicroapp");
}

function isQQ () {
    // @source 2021.04.21, add ua.includes('miniprogram'), qq's doc ( https://q.qq.com/wiki/develop/miniprogram/component/open-ability/web-view.html )
    return ua.includes('qq') && ua.includes('miniprogram');
}

// return miniapp type of the environment
function isMiniProgram () {
    let appType = false;
    let typeNameObj = globalObj.MINIAPP_TYPE

    try {
        if (isWechatMiniapp()) {
            appType = typeNameObj.WECHATMINIAPP;
        } else if (isOldQuickapp()) {
            appType = typeNameObj.OLDQUICKAPP;
        } else if (isNewQuickapp()) {
            appType = typeNameObj.NEWQUICKAPP;
        } else if (isAlipay()) {
            appType = typeNameObj.ALIPAYAPP;
        } else if (isBaidu()) {
            appType = typeNameObj.BAIDUAPP;
        } else if (isToutiao()) {
            appType = typeNameObj.TOUTIAOAPP;
        } else if (isQQ()) {
            appType = typeNameObj.QQAPP;
        }

        console.log('判断所处环境,isMiniProgram 返回值: ', appType);
        window['__bfi'].push([
            '_tracklog', 
            '174537', 
            `api_name=isMiniProgram&miniappType=${appType}&pageId=\${page_id}`
        ]);

        return appType;
    } catch (e) {
        window['__bfi'].push([
            '_tracklog', 
            '174537', 
            `api_name=isMiniProgram&err_msg=${e.message}&err_stack=${e.stack}`
        ]);
        return false; // 'catch error'
    }
}

export {
    isAndroid,       // 判断H5页面是否处于安卓系统
    isWechatMiniapp, // 判断H5页面是否处于微信小程序环境
    isWechat,        // 判断H5页面是否处于微信环境
    isOldQuickapp,   // 判断H5页面是否处于【老版快应用】小程序环境
    isNewQuickapp,   // 判断H5页面是否处于【新版快应用】小程序环境
    isAlipay,        // 判断H5页面是否处于支付宝小程序环境
    isBaidu,         // 判断H5页面是否处于百度小程序环境
    isToutiao,       // 判断H5页面是否处于头条小程序环境
    isQQ,            // 判断H5页面是否处于QQ小程序环境
    isMiniProgram    // 返回H5页面所处环境的应用名
}
复制代码

使用时的注意事项

使用前,最好查阅相应小程序的文档,由于各个小程序对API的支持程度不一样。js文件的引用不能放在里,bridge.js 里面对当前页面的head进行操做了。由于 bridge.js 引入JSSDK的方式是 为 head标签添加 script标签,若在 head标签中引入bridge.js,就会报错。

若打开h5,显示“页面访问受限”之类的提示信息,可尝试下方的操做:(这种状况,通常是打开测试环境的h5 url 时出现)勾选IDE中的“忽略webview域名合法性检查” 和 “忽略request域名合法性检查”。

【快应用相关】 目前Vivo,Oppo,华为三家厂商已支持新版快应用,VivoOPPO已上线,小米不支持。对于新版快应用,若H5页面须要调用新版快应用JS-SDK中提供的API,须要提早将该H5连接的域名配置到可信任的网址里(应写成正则表达式的形式进行配置)。

【头条相关】 头条小程序的redirectTo、navigateTo 等页面跳转的 api 只支持 url 为 / 开始的绝对路径

【支付宝相关】 目前的1.0.73版 bridge.js 判断是否处于支付宝小程序的方法,会将h5处于支付宝小程序、h5处于支付宝内置浏览器都判断为处于支付宝小程序内。所以,在调my.XXXX以前,须要先调判断环境工具函数 判断一下,确保确实是处于支付宝小程序内,而非支付宝内置浏览器内。

3.5 小程序获取最新版本号

在小程序中,咱们利用 app 的 onShow 钩子函数来完成最新的 URL 获取,同时还要保证只有获取了版本号以后才能加载其余的页面,所以这里要用到同步接口调用。请参考下面代码:

//这里加入同步请求到服务器获取最新路径
onShow: function (options) {
    this.getFEVersion()
},
getFEVersion: function () {
    //下面是利用Promise进行同步调用的写法
    return new Promise(function (resolve, reject) {
      wx.request({
        //下面是本机调试的一个地址,上线时请改为本身服务端的地址
        url: 'http://192.168.0.168:8090/getFEVersion',
        data: {},
        method: 'POST',
        header: {
          'content-type': 'application/json',
        },
        success: function (res) {
          if (res.data.success) {
            const app = getApp();
            //res.data.version 是从服务端返回的最新fe的版本号,即上面的数字101
            app.globalData.feUrl = 'https://www.yourdomain.com/' + res.data.version + '/#/index'
          }
          resolve();
        },
        fail: function (error) {
          console.log(error);
          reject();
        }

      })
    });
  },
webview动态处理
/** * @file 根据入参的小程序类型,动态加载相应的 JavaScript文件 * 指定<script>元素的src属性,指定事件处理程序(onload事件 onerror事件) */

const globalObj = {
    testDataArr: [],
    doJSReadyFuncExecuted: false,
    errorInfo: '',
    miniappSDK: null,
    miniappType: '',
    actionQueue: [],
    MINIAPP_TYPE: {
        WECHATMINIAPP:  'WECHATMINIAPP',// miniprogram
        WECHATAPP:      'WECHATAPP',    // miniprogram + offiaccount
        OLDQUICKAPP:    'OLDQUICKAPP',  // old
        NEWQUICKAPP:    'NEWQUICKAPP',  // new
        ALIPAYAPP:      'ALIPAYAPP',
        BAIDUAPP:       'BAIDUAPP',
        TOUTIAOAPP:     'TOUTIAOAPP',
        QQAPP:          'QQAPP'         // No longer maintained
    },
    JSSDK_URL_OBJ: {
        WECHATMINIAPP: 'https://res.wx.qq.com/open/js/jweixin-1.6.0.js',
        WECHATAPP: 'https://res.wx.qq.com/open/js/jweixin-1.6.0.js',
        OLDQUICKAPP:'https://xxxxxxxxx/amsweb/quickApp/mixBridge.js',
        NEWQUICKAPP: 'https://quickapp/jssdk.webview.min.js',
        ALIPAYAPP: 'https://appx/web-view.min.js',
        BAIDUAPP: 'https://b.bdstatic.com/searchbox/icms/searchbox/js/swan-2.0.21.js',
        TOUTIAOAPP: 'https://s3.pstatp.com/toutiao/tmajssdk/jssdk-1.0.1.js',
        QQAPP: 'https://qqq.gtimg.cn/miniprogram/webview_jssdk/qqjssdk-1.0.0.js'
    },
    bversion: '1.0.0'
}

let n = 0;
function loadListener (type) {
    // 先执行一次,再进入setTimeout
    // 多加几个埋点,记录不一样类型的信息
    console.log(`====== 重试次数:${n} ======`);
    if(n === 0) {
        processAddRes(type);
    } else {
        setTimeout(function () {
            processAddRes(type);
        }, 200)
    }
}

function processAddRes(type) {
    let curMiniappType = globalObj.miniappType;
    let curLoadJsUrl = globalObj.JSSDK_URL_OBJ[curMiniappType];

    if(!addJSSDKToGlobalObj()){
        n++;

        loadListener();

        if(n % 10 === 0) {
            const msg = `重试达到【${n}】次`
            console.log(msg);
            console.log(globalObj.errorInfo || '======');
        }
        return;
    }

    let actionQueue = globalObj.actionQueue;
    if (actionQueue && actionQueue.length) {
        let aItem = null;
        while (aItem = actionQueue.shift()) {
            try {
                globalObj.miniappSDK[aItem.apiName].apply(globalObj.miniappSDK, aItem.args)
            } catch (e) {
                //
            }
        }
    }
}

// 将JSSDK提供的方法保存到global
function addJSSDKToGlobalObj () {
    let curMiniappType = globalObj.miniappType;

    try{
        let _miniappSDK = null;
        switch(curMiniappType) {
            case 'WECHATMINIAPP':
            case 'WECHATAPP':
            case 'OLDQUICKAPP':
                _miniappSDK = typeof wx !== 'undefined' && wx.miniProgram;
                break;
            case 'NEWQUICKAPP':
                _miniappSDK = qa;
                break;
            case 'ALIPAYAPP':
                _miniappSDK = my;
                break;
            case 'BAIDUAPP':
                _miniappSDK = typeof swan !== 'undefined' && swan.webView;
                break;
            case 'TOUTIAOAPP':
                _miniappSDK = typeof tt !== 'undefined' && tt.miniProgram;
                break;
            case 'QQAPP':
                _miniappSDK = typeof qq !== 'undefined' && qq.miniProgram;
                break;
        }

        if(_miniappSDK) {
            globalObj.miniappSDK = _miniappSDK
        }

        if (!globalObj.miniappSDK || !globalObj.miniappSDK.navigateTo) {
            console.log(globalObj)
            let g_errmsg = (!globalObj.miniappSDK ? 'miniappSDK_is_undefined' : 'API_is_undefined');
            let g_errstack = 'none'
            globalObj.errorInfo = 'g_errmsg=' + g_errmsg + '&g_errstack=' + g_errstack;

            return false;
        }
    } catch (e) {
        // 记录下是什么缘由return的false: 在return false 的地方,将缘由挂到全局变量上,loadListener触发埋点时,记录下来
        globalObj.errorInfo = 'g_errmsg=' + e.message + '_have_catch_error' + '&g_errstack=' + e.stack;

        return false;
    }
    globalObj.errorInfo = 'g_errmsg=outof_try-catch_return_true';

    return true;
}

function parseQuery(url) {
    let query = {};
    let idx = url.indexOf("?");
    let str = url.substr(idx + 1);
    if (str == "" || idx == -1) {
        return {};
    }
    let pairs = str.split('&');
    for (let i = 0; i < pairs.length; i++) {
        let pair = pairs[i].split('=');
        // 当根据 = 号分割后有多条数据时,从数组第1位起以后的要所有保留。
        // 好比 src=/issue/create?type=1752,要处理成为:src: '/issue/create?type=1752',而不是 src: '/issue/create?type'
        if (pair.length > 2) {
            pair[1] = pair.slice(1).join('=');
        }
        query[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1] || '');
    }
    return query;
};

function loadScript() {
    // 埋点信息,增长加载的jssdk-url,后续可能能够从url中获取到微信的版本号
    // 设备品牌、设备型号、微信版本号、操做系统及版本、客户端平台、客户端基础库版本 Object wx.getSystemInfoSync()
    let curMiniappType = globalObj.miniappType;
    let curLoadJsUrl = globalObj.JSSDK_URL_OBJ[curMiniappType];

    let jSBridgeReady = function(type) {
        console.log('jSBridgeReady, event type: ', type);

        // 保证后续逻辑只会执行一次
        if (globalObj.doJSReadyFuncExecuted) {
            return;
        }
        globalObj.doJSReadyFuncExecuted = true;
        console.log('script is onload, doJSReadyFuncExecuted')

        loadListener(type);
    }


    if (curMiniappType === "WECHATMINIAPP" || curMiniappType === "WECHATAPP" || curMiniappType === "OLDQUICKAPP") {
        // 监听WeixinJSBridgeReady 和 onload 前,发个埋点,看下当前是否已经有wx 和 wx.miniProgram(由于目前nfes 只引入了微信jssdk)
        document.addEventListener('WeixinJSBridgeReady', function() {
            console.log('WeixinJSBridgeReady ======');
            jSBridgeReady('WeixinJSBridgeReady')
        }, false)
    }

    if (curMiniappType === "NEWQUICKAPP") {
        document.addEventListener('QaJSBridgeReady', function() {
            console.log('QaJSBridgeReady ======');
            jSBridgeReady('QaJSBridgeReady')
        }, false)
    }

    let script = document.createElement("script");
    script.src = curLoadJsUrl;
    script.async = false; // 注释掉,由于添加async的话,执行顺序没法保证

    let scriptArr = document.getElementsByTagName('script');
    console.log(scriptArr);

    for(let i = 0; i < scriptArr.length; i++) {
        let item = scriptArr[i];
        if(item.src.includes('/ares2/market/mixappBridge/') && item.src.includes('/default/bridge')) {
            // 取参数,动态设置async
            let queryObj = parseQuery(item.src); // 兜底的值为 {}
            console.log('queryObj: ', queryObj);
            if(typeof queryObj.bridgeAsync !== 'undefined') {
                script.async = queryObj.bridgeAsync === '1' ? true : false;
            }
        }
    }
    console.log('最终,script.async: ', script.async);

    script.onload = function(e) {
        console.log('script is onload ======')
        jSBridgeReady('onload')
    }

    script.onerror = function(e) {
        console.log('script is onerror')
    }

    window.onerror = function(message, source, lineNo, columnNo, error) {
        // to do track
    }
    document.getElementsByTagName('head')[0].appendChild(script)
}

export {
    loadScript
}
复制代码

工做中小程序webview业务细节总结

5.1 区分环境

微信提供了一个环境变量,加载h5之后第一个页面能够及时拿到,但后续的页面都须要在微信的sdk加载完成之后才能拿到,所以建议你们在wx.ready或者是weixinjsbridgeready事件里面去判断,区别就在于前者须要加载jweixin.js才有,但这里有坑,坑在于h5的开发者可能并不知道你这个检测过程须要时间,是一个异步的过程,他们可能页面一加载就须要调用一些api,这时候就可能会出错,所以你必定要提供一个api调用的队列和等待机制。具体作法见上面代码。

5.2 支付

第二个常见问题是支付,由于小程序webview里面不支持直接调起微信支付,因此基本上须要支付的时候,都须要来到小程序里面,支付完再回去。上面作好了之后,在h5这块调用就一句话就能够了。针对产品有大量内嵌H5页面的状况下,最好根据业务分两种支付页面,一是有的业务h5有本身完善的交易体系,下单动做在h5里面就能够完成,他们只须要小程序付款,所以咱们有一个精简的支付页,进来直接就拉起微信支付,还有一种状况是业务须要小程序提供完整的下单支付流程,那么久能够直接进入咱们小程序的收银台来,图上就是sdk里面的基本逻辑,咱们经过payOnly这个参数来决定进到哪一个页面。

咱们再看下小程序里面精简支付怎么实现的,就是onload以后直接调用api拉起微信支付,支付成功之后根据h5传回来的参数,若是是个小程序页面,那直接跳转过去,不然就刷新上一个webview页面,而后返回回去。

5.3 左上角返回

那怎么解决这种流失呢,咱们加了一个左上角返回的功能。首先进入的是一个空白的中转页,而后进入h5页面,这样左上角就会出现返回按钮了,当用户按左上角的返回按钮时候,页面会被重载到小程序首页去,这个看似简单又微小的动做,对业务其实有很大的影响,咱们看两个数字,通过咱们的数据统计发现,左上角返回按钮点击率高达70%以上,由于这种落地页通常是被用户分享出来的,之前纯h5的时候只能经过左上角返回,因此在小程序里用户也习惯如此,第二个数字,重载到首页之后,后续页面访问率有10%以上,这两个数字对业务提高其实蛮大的。其实现原理很简单,都是经过第二次触发onShow时进行处理。

Q: 可能出现的登陆登出同步问题

A: 跳到我的页登陆完成,此时是新开的webview同步两端登陆态,点返回,到上一个webview,此时这个webview嵌套的首页,没有触发react-imvc onshow事件。这个页面是老的,退出登陆也是同样,因此在首页会去跳h5的登陆而不是小程序登陆,致使登陆态不一样步。 解决思路:须要返回首页刷一下h5页面。

误区:直接在我的登陆以后,relaunch到首页,会致使没有直接调用注销webview把token置换,没法退出 解决方案:判段从我的页返回的时候,设置webview的url加个参数,从新刷一下。

相关文章
相关标签/搜索