移动端混合式开发实践

173 阅读4分钟

简述

结合了原生应用(Native App)和网页应用(Web App)的开发方式。它允许开发者利用HTML5、CSS3和JavaScript等Web技术来编写应用程序,同时也可以调用原生API以实现更丰富的功能和更优的性能。简而言之就是原生提供壳+底层能力api,页面由h5开发。可以做到快速迭代,符合产品业务快速上线的需求。

优点

  1. 跨平台性:可以同时在iOS和Android等多个平台上运行,节省了开发成本和时间。
  2. 快速迭代:使用Web技术,可以快速开发和部署更新,不需要经过应用商店的审核流程。
  3. 开发效率:利用现有的Web开发技能和工具,可以快速构建应用原型和功能。
  4. 成本效益:相比于原生开发,混合开发可以减少人力成本,因为不需要为每个平台分别开发。
  5. 易于维护:由于代码复用率高,维护和更新起来更加方便。
  6. 即时更新:可以像网页一样随时更新内容,不需要用户下载更新。

缺点

  1. 性能问题:混合应用的性能通常不如原生应用,尤其是在复杂的用户界面和动画效果上。
  2. 用户体验:可能无法提供与原生应用完全一致的用户体验,特别是在触摸反馈和手势操作上。
  3. 依赖原生功能:对于一些特定的原生功能,如复杂的图形处理或硬件级别的操作,可能需要额外的插件或定制开发。
  4. 插件依赖性:混合开发通常依赖于第三方插件来实现原生功能,这些插件可能存在兼容性问题或不再维护的风险。
  5. 学习曲线:虽然对于Web开发者来说更容易上手,但对于不熟悉Web技术的开发者来说,仍然存在一定的学习曲线。

架构设计

Pax架构图.png

原生&h5交互流程图

pax.png

桥sdk代码设计

  • 原生部分(hramony为例)
// 初始化
private initBaseBridges() {
  this.registerBridge('InnerBridge',InnerBridge)
  this.registerBridge('InnerWebRouterBridge',InnerWebRouterBridge)
  this.registerBridge('HttpBridge',HttpBridge)
}

// 加载桥方法(具体功能)
public registerBridge(name: string, plugin: typeof BaseBridge) {
  this.bridges.set(name, plugin);
  // 扫描插件类下的方法,并添加到 pluginMethodMaps
  const methods = Object.getOwnPropertyNames(plugin.prototype);
  for (const method of methods) {
    if (method !== 'constructor' && typeof plugin.prototype[method] === 'function') {
      this.pluginMethodMaps.set(method, name);
    }
  }

}

// 桥方法例子-getVersion
public getVersion(entry: BridgeEntry): string {
  const params: string = entry.params as string
  Log.i(InnerBridge.TAG, 'getVersion');
  new Promise(async (resolve, reject) => {
    const obj = {}
    obj['status'] = "ok"
    obj['msg'] = "ok"
    try {
      const version = await AppHelper.getAppVersion()
      obj['version'] = version
    } catch (error) {
      obj['status'] = "ok"
      obj['msg'] = error.message
      Log.error(InnerBridge.TAG, error);
    }
    resolve(obj)
  }).then(result => {
    entry.callBack({ result: JSON.stringify(result) })
  }).catch(err => {
    let error = ErrorManager.generateCustomErrorMessage(InnerError.getError(InnerErrorName.UNKNOWN_ERROR)
      .code, err.getMessage())
    entry.callBack({ error })
  })
  return ""
}
// 初始化webview容器时挂载桥对象(注入到页面中,提供给h5调用)
Web({ src: this.src, controller: this.webController })
  .javaScriptProxy({
    object: this.bridgeCaller,
    name: getName(),
    methodList: getMethods(),
    controller: this.webController
  })
getName(): string {
  return "bradge_harmony";
}

getMethods(): string[] {
  let methods: string[] = ['bradge_check', 'bradge_call'];
  return methods;
}
// 暴露给h5的方法
// bradge_check用于检查桥功能是否存在
bradge_check: Function = (message: string) => {
  LogUtil.getInstance().info(message)
  var obj = {}
  let bridgeTrace: BridgeTrace = new BridgeTrace(this.bridgeEnv?.mPageActionId);
  try {
    //解码前端桥调用消息
    var argObj = JSON.parse(message)

    //获取方法名称
    let componentName: string | undefined = undefined;
    componentName = argObj['bradge_call_func']

    bridgeTrace.actionName = componentName
    obj['version'] = 1
    obj['status'] = 'error'

    const hasMethod = bradgeApp.getInstance().getBridgeManager().contains(componentName);
    if (hasMethod) {
      obj['status'] = 'ok'
    }
    LogUtil.getInstance().info("bradge_check:"+JSON.stringify(obj))
    //分发到各个桥
    return JSON.stringify(obj);
  } catch (e) {
    LogUtil.getInstance()
      .error(`[BridgeTrace] ${bridgeTrace.actionName} err: ${JSON.stringify(e)}`)
  }

  return "";
}

// bradge_call 调用桥功能并回调h5的callback
bradge_call: Function = (message: string) => {
    LogUtil.getInstance().info(message)
    let bridgeTrace: BridgeTrace = new BridgeTrace(this.bridgeEnv?.mPageActionId);
    try {

    //获取传递的数据
    var argObj = JSON.parse(message)

    let actionName: string | undefined = undefined;
    actionName = argObj['bradge_call_func'];
    bridgeTrace.actionName = actionName
    LogUtil.getInstance()
    .info(`[BridgeTrace] ${actionName} [aId:${bridgeTrace.pageActionId}, bId:${bridgeTrace.bridgeId}]-> start time=${bridgeTrace.startTime}`)

    let callback: string | undefined = undefined;
    callback = argObj['bradge_callback_func'];
    let callbackId: string | undefined = undefined;
    callbackId = argObj['bradge_callback_id'];

    const bridgeContext = new BridgeContext(this.bridgeEnv, callback, callbackId,bridgeTrace);
    const entry: BridgeEntry = new BridgeEntry(bridgeContext, message,this.webControllerProxy);
    //分发到各个桥
    const handleData = BradgeApp.getInstance().getBridgeManager().handle(actionName, entry);
    return handleData.toString()
    } catch (e) {
    LogUtil.getInstance().error(`[BridgeTrace] ${bridgeTrace.actionName} err: ${JSON.stringify(e)}`)
    }
    return null;
}

  • js-bradge部分
// 提供给业务页面调用的方法
/**
 * @param args 参数
 * @param call_func 调用的方法名称
 * @param callback 回调方法
 */
const callPax = function callPax(args, call_func, callback) {
  const arg = genArgs(args, call_func, callback);
  // log(arg);
  return genPromise(arg, call_func);
};
/**
 * 统一处理传入参数和回调函数
 * @param args 原参数
 * @param call_func 调用的方法名称
 * @param callback 回调方法
 * @returns {*} 处理后的参数
 */
 // eslint-disable-next-line camelcase
const genArgs = function genArgs(args, call_func, callback) {
  // log(args);
  if (args) {
    // do nothing
  } else {
    args = {};
  }
  if (isUndefined(args)) {
    args = {};
  } else if (typeof args === 'string') {
    try {
      args = jsan.parse(args);
    } catch (e) {
      // log(e);
      args = {};
    }
  } else if (isObject(args)) {
    // log('isObj', args);
  }
  args.bradge_call_func = call_func;
  args.bradge_call_origin = window.location.origin;
  if (typeof callback === 'function') {
    const id = genGUID();
    args.bradge_callback_id = id;
    const func_name = `func_${genGUID()}`;
    args.bradge_callback_func = func_name;
    window[id] = {};
    window[id][func_name] = function func_name(value) {
      try {
        try {
          reporter.log(`bradge回调流水[method:${call_func},cbId:${id},cbName:${args.bradge_callback_func}];v:${isString(value) ? value : jsan.stringify(value)}`);
        } catch (e) {
          reporter.log(`bradge回调流水[method:${call_func},cbId:${id},cbName:${args.bradge_callback_func}];v:${value}`);
        }
        // 这里可以加入更多的日志采集信息
        // log('回调函数被调用', id, func_name);
        // log('回调函数被调用,返回值', value);
        let obj = {};
        if (typeof value === 'string') {
          try {
            obj = jsan.parse(value);
          } catch (e) {
            obj = value;
          }
        } else {
          obj = value;
        }
        callback(obj);
      } finally {
        // 回调方法时,移除该id对应的所有方法
        eval(`delete window["${id}"]`);
      }
    };
  }
  return jsan.stringify(args);
};
/**
 * 统一处理方法调用
 * @param args 原参数
 * @param call_func 调用的方法名
 * @returns {Promise}
 */
const genPromise = function genPromise(args, call_func) {
  let argObject = {};
  if (isAndroid()) {
    eval(`bradge_android.bradge_call(${jsan.stringify(args)})`);
    return Promise.resolve()
  }
  if (isIos()) {
    eval(`window.webkit.messageHandlers.bradge_call.postMessage({"key":${jsan.stringify(args)}})`);
    return Promise.resolve()
  }
};