knowledge

Hybrid前端开发

目录

  1. WebView相关

    1. Native提供给Hybrid宿主环境(WebView)
    2. WebView的前端处理
    3. WebView性能
  2. 其他语言2Native


Hybrid App:狭义上是App内嵌WebView组件,再在WebView上使用页面的方案。广义上包括所有App混合方案,包括WebView方案、其他语言2Native方案、等。

随着技术的发展,有新的技术替代桥协议,如:JSI、等。以下内容中描述桥协议的部分,都可以用这些新技术替代。

  1. 桥协议:JS和C++互相无感知,只能通过桥协议作为中间层,异步进行序列化/反序列化传输通讯。
  2. JSI:将C++中的常用类型、定义的对象和函数 映射到JS中,支持JS随时调用C++中方法。并且支持其他JS引擎。

WebView相关

WebView种类 1. iOS: 官方:[WKWebView](https://developer.apple.com/documentation/webkit/wkwebview)、[UIWebView](https://developer.apple.com/documentation/uikit/uiwebview)(不推荐) 2. Android: 1. 官方:[WebView](https://developer.android.com/reference/android/webkit/WebView) 2. 第三方:[X5](https://x5.tencent.com/)、[AgentWeb](https://github.com/Justson/AgentWeb)、等

Native提供给Hybrid宿主环境(WebView)

JS引擎:一个专门处理JS脚本的虚拟机 手机中的JS引擎: 1. Android运用的JS引擎:Google的[V8](https://github.com/v8/v8)。 2. iOS运行的JS引擎:Apple的[JavaScriptCore](https://developer.apple.com/documentation/javascriptcore)。 >还有其他JS引擎:SpiderMonkey、Rhino、等。

Hybrid底层依赖Native提供的容器(WebView),上层使用HTML、CSS、JS进行业务开发。

小程序是WebView+Native的双线程模型。

  1. 互相调用:

    1. Native调用WebView的JS方法(window.前端定义方法
    2. WebView调用桥协议window.客户端定义方法)、或触发自定义URL Schememyscheme://客户端定义路径
  2. 资源访问机制

    1. 以本地协议file方式访问Native内部资源。

      • 可以把前端要用的静态资源放到客户端本地(如:字体文件),本地页面通过类似file:///android_asset/fonts/myFont.ttf引用。
    2. 以远程URL方式访问线上资源(http/https)。
    3. 增量替换机制(不依赖发包更新)

      1. Native包内下载、解压线上的打包资源,再替换旧资源。
      2. manifest
    4. URL限定,限制访问、跨域问题的解决方案

      1. 可以限制WebView的能发起的请求内容。
      2. 可以代替WebView进行会触发跨域的AJAX请求。
  3. 页面在客户端内打开方式

    1. 针对产品功能性页面:

      用本地协议file方式打开客户端包内.html(.js、.css、图片等都在客户端包内)。

      • file打开的页面直接发起请求可能会有跨域问题,可以用客户端接口代理的方式请求服务端数据。
    2. 针对运营活动页面:

      用远程URL方式请求。

  4. 身份验证机制

    Native创建WebView时,根据客户端登录情况注入跟登录有关的信息(session_id或token)至WebView(可注入到全局对象或cookie)。

    cookie有同源策略,所以客户端可以对指定白名单域名添加cookie。

  5. 开发测试

    1. 提供切换成线上资源请求方式的功能,用代理工具代理成本地资源。

WebView的前端处理

  1. 与Native通信方式:

    1. 都是以字符串(数据用JSON字符串)的形式交互,向客户端传递:

      1. 全局的方法名 -> 客户端调用window.方法名(JSON数据)
      2. 匿名函数 -> 客户端调用(匿名函数(JSON数据))
    2. WebView无法判断是否安装了其他App。
    3. 可以通过查看注入的全局方法、或客户端调用回调函数(、或navigator.userAgent)来判定页面是否在具体App内打开。
    4. 桥协议仅在App内部起作用;自定义URL Scheme是系统层面,所以可以额外针对跨App起作用(如:分享去其他App);iOS的通用链接可以认为是高级的自定义URL Scheme
    5. Native和WebView交互需要时间,对时效性很高的操作会有问题。
    1. 桥协议:Native注入全局方法至WebView的window,WebView调用则客户端拦截后触发Native行为。

      1. 客户端注入方式:javascript伪协议方式javascript: 代码
      2. 注入JS代码可以在创建WebView之前([native code])或之后(全局变量JS注入)。
        • 若注入的方法为undefined,则认为不在此App内部。
    2. 自定义URL Scheme:拦截跳转(<iframe><img>设置src、点击<a>window.location.href),触发Native行为。

      URL Scheme 是iOS和Android提供给开发者的一种WAP唤醒Native App方式(客户端用DeepLink实现)。Android应用在mainfest中注册自己的Scheme;iOS应用在App属性中配置。典型的URL Scheme:`myscheme://my.hostxxxxxxx`。
      1. 客户端可以捕获、拦截任何行为(如:consolealert)。相对于注入全局变量,拦截方式可以隐藏具体JS业务代码,且不会被重载,方便针对不可控的环境。
      2. 有些App会设置允许跳转的其他App的白名单或黑名单(记录其他APP的Scheme),如:微信白名单。
      3. 除了增加回调函数且被客户端调用,否则无法准确判定是否在此App内部。
      4. 跨App使用自定义URL Scheme,其后面的字符串要产生的行为仅目的App能理解。
      5. 快速触发多次自定义URL Scheme,有时仅有最后一个产生效果。e.g. 用window.location.href快速触发多次,仅有最后一次跳转信息能够传递给客户端。
      6. 想要实现这样的功能:”关闭当前WebView,然后执行某功能”,若用window.location.href等触发自定义URL Scheme方式给客户端拦截,则可能WebView实例关闭后,事件无法被客户端捕获(类似:异步的XMLHttpRequest会随WebView实例关闭而被忽略)。可以尝试换用桥协议
      1. iOS

        1. iOS8-

           var iframe = document.createElement('iframe');
           iframe.src = '自定义URL Scheme';
           iframe.style.display = 'none';
           document.body.appendChild(iframe);
           setTimeout(function () {
             document.body.removeChild(iframe);
           }, 3000);
          
           location.href = '下载地址';
          
        2. iOS9+

          <iframe>无效。

           location.href = '自定义URL Scheme';
          
           setTimeout(function () {
             location.href = '下载地址';
           }, 250);
           setTimeout(function () {
             location.reload();
           }, 1000);
          
        3. iOS9+的Universal links(通用链接),可以从底层打开其他App客户端,跳过白名单(微信已禁用)

          需要HTTPS域名配置、iOS设置等其他端配合。

          参考:通用链接(Universal Links)的使用详解Universal Link 前端部署采坑记Support Universal Links

      2. Android

         location.href = '自定义URL Scheme';    // 也可以用`<iframe>`
        
         var start = Date.now();
         setTimeout(function () {    // 尝试通过上面的唤起方式唤起本地客户端,若唤起超时(还在这个页面),则直接跳转到下载页(或做其他未安装App的事情)(浏览器非激活时,定时器执行时间会变慢/主线程被占用,所以会大于定时器时间之后才执行定时器内回调)
           if (Date.now() - start < 3100) {  // 还在这个页面,认为没有安装App
             location.href = '下载地址';
           }
         }, 3000);
        
      • 在WebView中通过应用宝页面下载/打开其他APP

        参考:关于微信中直接调起 Native App 的调研报告

        1. iOS:应用宝页面支持跳转至目标APP的App Store
        2. Android:应用宝页面支持下载/打开目标APP

          1. 在腾讯系APP中能识别是否安装了目标APP;在其他APP中无法判断。
          2. 填写android_schema可以传递信息至目标APP:腾讯系APP打开目标APP后会带着信息;其他APP会先触发一次信息。
          • 不用安装应用宝就支持打开目标APP功能;下载是去应用宝下载(会先要求安装应用宝APP)。

        直接打开在聊天界面的URL,会有诸多限制,如:无法拉起APP、分享的URL不是卡片而是纯文本 等。打开 分享的卡片、二维码扫描 的网址,才有更多页面权限。

        • 拼接应用宝下载/打开目标APP的链接:

          1. 应用宝主链接:https://a.app.qq.com/o/simple.jsp?
          2. 跳转参数(search值,在?后面,用&分割):

            1. APP包名:pkgname= + com.xx.xxx
            2. 渠道包链接(可选):ckey= + CK1234567890123
            3. 目标APP内打开路径(可选):android_schema= + 自定义URL Scheme://具体跳转路径

              若有一些特殊字符,则可以用encodeURIComponent转义属性名和属性值。

          e.g. https://a.app.qq.com/o/simple.jsp?pkgname=com.xx.xxx&ckey=xxxx&android_schema=xxxx://xx

      1. 微信分享在部分系统(低于微信客户端Android6.2)使用pushState导致签名失败,可查询官方文档;又因为一般是异步加载、配置微信的设置,所以要等待微信第三方文件和接口完成后才能够配置成功(才能够设置成功)。
      2. Android的微信、QQ等X5内核可以用http://debugx5.qq.com/打开调试,支持「清除缓存」等操作。

        iOS可以在 设置->通用->存储空间 清理缓存。

      3. 长按没有 src<img>

        1. 在iOS微信WebView,截屏这个<img>所在位置;
        2. 其他情况,可能导致保存图片错误、或不能进行保存。
    3. WebView提供给Native调用的全局回调函数(或匿名函数)。

      对于动态创建的全局回调函数,要注意同名覆盖问题 e.g. ```javascript let _localCounter = 1 // 同一个方法名快速请求时,可能 Date.now() 还没有变化 function invokeJSBridge (method, arg, { hasCallback = true }) { return new Promise((resolve, reject) => { if (typeof window.客户端定义方法 === 'function') { let callbackName = '' // 创建客户端回调 if (hasCallback) { callbackName = `${method}CallbackName_${_localCounter}` _localCounter += 1 window[callbackName] = (res) => { // todo: 增加定时器处理长时间未被客户端回调的方法 try { resolve({ result: 'ok', data: JSON.parse(res) }) } catch (e) { resolve({ result: 'error', data: res }) } window[callbackName] = null } } // 调用客户端方法 window.客户端定义方法( // 桥协议 method, // 方法名 JSON.stringify(arg || {}), // 传参 callbackName // 回调 ) // 无客户端回调时直接完成 if (!hasCallback) { resolve({ result: 'ok', data: '' }) } } else { reject(arg) } }) } /* 使用测试 */ invokeJSBridge('方法名', '参数') .then((res) => { // 是客户端、且调用成功&&客户端执行回调 // 根据res处理客户端执行之后业务 }) .catch((res) => { // 不是客户端 // 非客户端业务 }) ```

      接口设计可以带有「透传数据」:前端调用客户端方法时多传一个透传参数,之后客户端异步调用前端方法时带着这个参数的值。

  2. 根据WebView的错误处理机制统计用户在WebView遇到的bug。
  3. WebView调试
  4. 分享到其他App

    1. 通过JS触发Native App之间的切换分享(自己Native内可用桥协议,任意App均要起作用只能用Scheme)。
    2. 带分享信息参数去访问目标App提供的分享URL。
  5. 响应式页面解决方案
  6. 客户端内的前端页面,添加半屏弹窗策略:

    针对通用的逻辑,要给多个前端页面添加半屏弹窗逻辑。如:某个后台接口返回错误码,统一弹出半屏弹窗。

    1. 提前注入弹窗DOM和逻辑,需要触发时调用。
    2. 需要触发时动态注入弹窗DOM和逻辑,再调用。
    3. 新制作一个前端的弹窗页面,需要触发时在原页面上层覆盖这个半屏弹窗页面。
    4. 客户端提供bridge或Scheme用于弹出客户端的弹窗,需要触发时唤起客户端的弹窗。

WebView性能

参考:WebView性能、体验分析与优化

  1. 原生实现 VS. 页面实现

    1. Native App

      1. 高成本开发
      2. 原生性能体验
      3. 依赖发包更新
    2. WebView的页面

      1. 低成本开发、高效率、一套代码跨平台复用(因为就是输出页面)
      2. 性能不及原生应用(因为要初始化WebView,前端「增量」原则决定)
      3. 不依赖Native发包更新(注意避免违反苹果的热更新条款)

    原生实现 VS. 页面实现

  2. WebView启动流程

    WebView启动流程

    1. 相对于Native App的流畅体验,WebView的页面瓶颈一般都卡在WebView实例初始化,可能导致App卡顿、页面加载缓慢。

      WebView的初始化、保持,占用较多内存。

    2. 与浏览器不同,App中打开WebView的第一步并不是建立连接,而是启动浏览器内核。
    3. 除了网站性能优化之外,能提升体验的办法基本要让Native配合调整(页面能做的不多,主要靠客户端开发投入,具体方案可查看:WebView性能、体验分析与优化)。

其他语言2Native

前端编写的代码,通过中间的其他语言2Native之类的方式转换为原生App代码运行。

如:Dart2Native、React Native2Native、Weex2Native。

  1. 其他语言2Native调试
  2. 适配布局(与设计师协作思路)