Skip to content

Latest commit

 

History

History
371 lines (280 loc) · 21.2 KB

File metadata and controls

371 lines (280 loc) · 21.2 KB

Hybrid前端开发

目录

  1. WebView相关

    1. Native提供给Hybrid宿主环境(WebView)
    2. WebView的前端处理
    3. 前端-客户端协议方案(前端方向)
    4. WebView性能
  2. 其他语言2Native


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

随着技术的发展,有新的技术替代桥协议,如:JSI、等。

  1. 桥协议:JS和C++互相无感知,只能通过桥协议作为中间层,异步进行序列化/反序列化传输通讯
  2. JSI:将C++中的常用类型、定义的对象和函数 映射到JS中,支持JS随时调用C++中方法,有更高的性能、更低的通信开销(不需要再异步进行序列化/反序列化传输通讯,可以同步直接调用方法、传输多种类型数据)。并且支持其他JS引擎。
  • postMessage实现通讯的方案,不影响底层实现是:桥协议的异步进行序列化/反序列化传输通讯 或 JSI实现。

约定:本篇内容中描述桥协议的部分,都可以用这些新技术替代。

WebView相关

WebView种类
  1. iOS:

    官方:WKWebViewUIWebView

  2. Android:

    1. 官方:WebView
    2. 第三方:X5AgentWeb、等

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

JS引擎:一个专门处理JS脚本的虚拟机

手机中的JS引擎:

  1. Android运用的JS引擎:Google的V8
  2. iOS运行的JS引擎:Apple的JavaScriptCore

还有其他JS引擎:SpiderMonkey、Rhino、Hermes等。

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. 桥协议(JSI没有这个限制)都是以字符串(数据用JSON字符串)的形式交互,向客户端传递:

      1. 全局的方法名 -> 客户端调用window.方法名(JSON数据)
      2. 匿名函数 -> 客户端调用(匿名函数(JSON数据))
    2. WebView无法判断是否安装了其他App。

    3. 可以通过查看注入的全局方法、或客户端调用回调函数(、或navigator.userAgent)来判定页面是否在具体App内打开。

    4. 桥协议仅在App内部起作用;自定义URL Scheme是系统层面,所以可以额外针对跨App起作用(如:分享去其他App);iOS的通用链接可以认为是高级的自定义URL Scheme

    5. 桥协议自定义URL Scheme方式的Native和WebView交互需要时间,对时效性很高的操作会有问题;JSI认为是同步的。

    6. 对前端操作(如:跳转自定义URL Scheme、全局方法调用)的监听拦截,都需要和客户端确认是否支持,以客户端描述为准。

    1. 桥协议:Native注入全局方法至WebView的window,若WebView调用则触发Native行为。

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

      URL Scheme

      是iOS和Android提供给开发者的一种WAP唤起Native App方式(客户端用DeepLink、Deferred 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. JS触发

        1. iOS8-支持<iframe src="自定义URL Scheme">window.location.href="自定义URL Scheme"跳转;iOS9+不支持 <iframe>,仅支持window.location.href="自定义URL Scheme"跳转

        2. iOS9+的Universal links(通用链接),可以从底层打开其他App客户端,跳过白名单(微信已禁用),若未安装则打开配置好的落地页(落地页再提示下载app,不需要JS判断是否页面被隐藏从而跳转落地页

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

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

        3. Android支持<iframe src="自定义URL Scheme">window.location.href="自定义URL Scheme"跳转

        • 使用自定义URL Scheme唤起APP,若页面没有被隐藏则认为未安装app,从而重定向至落地页
          1. 方法一(推荐):

            // 尝试跳转URL Scheme
            location.href = '自定义URL Scheme'; // 若支持则也可以用`<iframe>`
            
            const delay = 2000; // 按实际情况设置2~5秒
            
            const timer = setTimeout(() => {
              location.href = '落地页';
            
              removeEventListener();
            }, delay);
            
            // visibilitychange 事件方法(webkitvisibilitychange - webkitHidden,mozvisibilitychange - mozHidden)
            function clearFunc1() {
              if (document.hidden) {
                clearTimeout(timer);
            
                removeEventListener();
              }
            }
            
            // pagehide 事件方法
            function clearFunc2() {
              clearTimeout(timer);
            
              removeEventListener();
            }
            
            function removeEventListener() {
              window.removeEventListener('visibilitychange', clearFunc1);
              window.removeEventListener('pagehide', clearFunc2);
            }
            
            // 若检测到隐藏页面,则阻止跳转落地页(认为app已安装、已成功跳转URL Scheme)
            window.addEventListener('visibilitychange', clearFunc1);
            window.addEventListener('pagehide', clearFunc2);
          2. 方法二:

            原理:浏览器非激活时,定时器执行时间会变慢/主线程被占用,所以会大于定时器时间之后才执行定时器内回调。

            // 尝试跳转URL Scheme
            location.href = '自定义URL Scheme';    // 若支持则也可以用`<iframe>`
            
            const delay = 2000;         // 按实际情况设置2~5秒
            
            const start = Date.now();
            setTimeout(function () {
              if (Date.now() - start < delay + 100) {  // 还在这个页面,认为没有安装App
                location.href = '落地页';
              }
            }, delay);

        落地页跳转App下载地址,除了iOS的企业包要注意用itms-services://?action=download-manifest&url=网站链接.plist之外,其他情况都可以直接打开下载地址。

      2. 在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

    3. WebView提供给Native调用的全局回调函数(或匿名函数)。

      对于动态创建的全局回调函数,要注意同名覆盖问题

      e.g.

      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) => {    // fixme: 增加定时器处理长时间未被客户端回调的方法
               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) => { // 不是客户端
         // 非客户端业务
       })

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

    1. 以 全双工通讯协议 作为桥梁通信,如:前端的websocket,客户端启动在后台的通信服务。

    2. RN前端和<WebView>组件通信:postMessage

      postMessage实现通讯的方案,不影响底层实现是:桥协议的异步进行序列化/反序列化传输通讯 或 JSI实现。

  2. 根据WebView的错误处理机制统计用户在WebView遇到的bug。

  3. WebView调试

  4. 分享到其他App

    1. 通过JS触发Native App之间的切换分享(自己Native内可用桥协议,任意App均要起作用只能用自定义URL Scheme)。
    2. 带分享信息参数去访问目标App提供的分享URL。
  5. 响应式页面解决方案

  6. 客户端内的前端页面,添加半屏弹窗策略:

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

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

前端-客户端协议方案(前端方向)

  1. js调用app的中间层代码,兼容实现不同平台、不同app、环境准备成功与否的逻辑

    1. 支持:主动调用、事件通知、URL scheme
    2. 支持:新app低成本接入
  2. 限制js调用的入参和出参(回调函数的类型 或 返回Promise的类型),参数结构统一

    1. 协议规范(强类型)
    2. 客户端返回的错误码规则(错误码决定错误类型,如:调用未注册接口、导致crash、参数问题)
    3. js错误兜底(try-catch、超时时间),错误码处理(如:提示版本问题、提示升级、静默失败无反馈)
  3. 接口文档,如何根据代码自动生成(jsdoc、typedoc)

    1. 接口demo调试页面 支持 页面实时运行的代码编辑体验(参考ant-design:利用sandpack
  4. *3端之间 降低沟通成功、提高变更同步效率

    控制唯一的修改入口(前端、iOS、Android,仅选其一),由某一端人员定义协议,自动生成3端接口代码

  5. *接口成功率监控、告警,异常上报

  6. *流程接入工程化

WebView性能

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

  1. 移动端技术演进:Native -> Hybrid -> Native容器 -> 自绘

    1. Native App

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

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

    原生实现 VS. 页面实现

    1. Native容器方案

      以Weex(已掉队)和RN最具代表,原理类似,上层JS利用Vue/React框架维护虚拟节点,通过bridge(或JSI等优化)与Native通信,最终在Native渲染组件。

    Native容器方案 与 自绘方案,就目前而言没有谁优谁劣:对于Native容器方案来说,更多的是代表了研发效率与动态性;对于Flutter这类框架而言,虽然实现了高性能、多端一致的自绘,但却缺少一套较为完备的动态化方案。

    1. 自绘方案

      客户端只需提供一个画布,框架自带渲染引擎在画布上进行UI渲染和逻辑(不需要客户端帮忙渲染,因此不需要RN那样的一层Bridge进行JS和Native的通信数据转发,渲染效率更高)。

      e.g. Flutter:既不使用WebView,也不使用操作系统的原生控件。原生系统提供2D画布(Canvas),Flutter使用自己的高性能渲染引擎Skia在画布上绘制Widget。这样不仅可以保证在Android和iOS上UI的一致性,而且可以避免因对原生控件依赖而带来的限制及高昂的维护成本。

  2. WebView启动流程

    WebView启动流程

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

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

    2. 与浏览器不同,App中打开WebView的第一步并不是建立连接,而是启动浏览器内核。

    3. 除了网站性能优化之外,能提升体验的办法基本要让Native配合调整(页面能做的不多,主要靠客户端开发投入,具体方案可查看:WebView性能、体验分析与优化)。


其他语言2Native

前端编写的代码,通过中间的其他语言2Native之类的方式(bridge)转换为原生App代码运行,原生App代码也会通过Native2其他语言(bridge)回调前端编写的代码。

如:Dart2Native、React Native2Native、Weex2Native。

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