前言

移动开发的跨平台是大势所趋,可以节省开发成本,提高开发效率,迅速响应业务变化,现在主流的应用还是使用 H5 和原生的通信来实现跨平台的开发。JSBridge 框架解决 JS 和 Native 的通信问题实现移动端跨平台开发。结合业务来讲,JS SDK 作为两端连接的工具,它的使用体验决定了项目的部分效率和质量。

背景

目前内嵌在跑腿 App 中的页面与客户端进行交互的方案有三种: 1. schema 协议拦截 2. 桥接方法 3. 相互调用全局变量、方法 这三种交互方法目前都在用,并且引入方式不同,造成代码维护以及业务开发的时候比较混乱,还有文档的维护也比较难。开发业务过程中,一旦涉及到营销活动类的一些新功能,就需要客户端新增一些方法,但是这些方法只能通过判断版本号来判断是否能使用,而版本号又需要通过通配符或者其他方法来获取,这样一来就写了很多与业务无关的冗余代码,可能业务开发很快,处理兼容就占了一半的开发时间,1.版本兼容问题,导致前端代码非常冗余 2.两端表现以及行为不一致 3.API 调用方式存在多种 4.文档维护困难 5.方法定义重复 针对此类问题决定对现有的客户端文档与 API 方法进行一次较大规模的优化。

目标

作为业务方和客户端的中间层,在项目设计理念上原则是尽可能独立并, 形成一套有序、稳定的规范。在业务方调用时,减少理解和测试使用难度,能少处理一些兼容,支持多种形式调用 promise 和 callback。在回传数据时,严格约束相关字段。

  1. 支持 Commonjs、EsModule 等多种规范,提供 npm 引入和 cdn 引入的能力;
  2. 包含一份可读性良好的文档说明,并且有示例可交互;
  3. 文档库、API 发布自动化部署 实现一键更新发布;
  4. 项目开发、提交、版本管理规范;
  5. API 设计规范、设计命名规范等;
  6. API 开发 、调试 、验证 流程简单高效,不需要额外的心智负担;
  7. 直接一键打开 App 能力;

技术选型

  • 技术栈:ts
  • 包管理:father-build + simple-git
  • 打包工具:rollup + babel
  • 代码规范:prettier + lint-staged
  • 文档库:dumi + React + and

项目规范

目录规范

  1. src 文件夹下
    • app 文件夹存放 app 相关文档及接口;
    • 若需要增加小程序或其他平台可增加 miniProgram 或平台对应文件夹即可;
    • 注意方法命名不要冲突 涉及 canIUse 处理逻辑;
  2. app 文件夹下
    • base 目录存放通用方法;
    • user 目录存放用户端相关方法;
    • shop 目录存放商户端相关方法;
    • driver 目录存放跑男端相关方法;
  3. 单个 API 文件夹下
    • 文件夹命名可以参考微信小程序、支付宝小程序的实现 尽可能语义化。减少理解成本;
    • index.ts 为接口的实现;
    • READEM.md 为使用文档;
    • interface 为 ts 定义的一些 ts 的一些声明;
    • utils 为私有的公共方法;
    • 其他静态资源等也可以放在当前目录下;

出入参规范

  1. 关于入参
    • 入参格式与小程序等基本相同,按照文档说明使用即可;
    • 参数控制在 6 个以内,除去不定长参数以外,函数具备不同逻辑意义的参数建议控制在 6 个以内,过多参数会导致维护难度增大;
    • 参数过多时,可以通过增加 options 参数来传递一些参数;
    • 参数提前在声明文件中定义好,如果需要新增参数及时修改类型声明。以便于在 ts 中使用参数提示;
    • 大多方法都支持 callback 形式调用 支持 success fail 回调,一些简单项目可以通过这种形式使用;
  2. 出参
    • 遵循 restful api 风格, 返回消息结构体, State、 Body 、Msg State 值在 utils/interface APIState;
    • 非业务相关或者简单 api 可不遵循 restful api 风格, 比如 canIUse;
    • 客户端的新增的方法有固定的返回消息体,为了避免结构重复已进行处理;
export interface _APIResult<T> {
	Msg: string;
	Body: T;
	State: number;
}
export enum APIState {
	SUCCESS = 1,
	ERROR = -1,
}

export enum APIMsg {
	ERROR_NOT_APPLICATION = '未识别到客户端类型',
	ERROR_NOT_METHODS = '找不到该方法',
	ERROR_NOT_PAOTUI = '非跑腿客户端',
	ERROR_NOT_SUPPORT = '当前版本较低,不支持该方法或属性',
}

解决两个问题: 1.部分字段命名的不合理的现象。需要重新定义 2. ts 类型声明和智能提示 3. 属性的版本判断。

常见 QA

  1. 如何版本号获取
    • window.PAOTUIAPPVERSION 可以优先判断这个值是否存在 如果不存在调用 getAppVer()
    • getAppVer() 获取版本号 该方法调用一次后会在 window.PAOTUIAPPVERSION 进行赋值
  2. 如何判断 API 是否可用
    提前在对应函数的原型上声明 _Support,定义好对应 API 属性的版本号
    • 新老版本方法兼容, 判断 API 是否可用 canIUse(‘showShareMenu’)
    • 新增属性兼容,判断 API 属性是否可用 canIUse(‘showShareMenu.operateType’)
    • 新增属性兼容,判断 API 属性值是否可用 canIUse(‘showShareMenu.operateType.10’)
    • 例子:

// 配置
const _Support = {
	showShareMenu: '2.1.5',
	'showShareMenu.operateType': '2.6.9',
	'showShareMenu.operateType.\\b0\\b|\\b1\\b|\\b2\\b': '2.6.9',
	'showShareMenu.shareIconStyle.\\b0\\b|\\b1\\b': '2.6.9',
	'showShareMenu.shareTypeList': '2.6.8',
};
showShareMenu.prototype._Support = _Support;
// 使用

//判断 API 是否可用 canIuse
let _canIUse = await canIUse('showShareMenu');
// 判断API属性是否可用
let _canIUse = await canIUse('showShareMenu.operateType');
//判断API属性值是否可用
let _canIUse = await canIUse('showShareMenu.operateType.10');

  1. 如何实现 Ts 约束代码 代码提示类型推断
    • 最小类型约束 对于一些可预测的类型提前声明好类型值。比如 shareIconStyle: 0 | 1;
    • 参数智能推导
      • promise 模式下字段提示 在 interface 中提前定义好接口入参和出参的类型,需要在 successReuslt 函数中传入对应的 interface 例子: <addrInfo>使用范型传入。比如: successReuslt<addrInfo>
      • callback 模式下字段提示 同样提前定义好类型, 然后在 BaseParams 中传入 例子: BaseParams<addrInfo>
    • as 使用 某些函数可能不需要参数 但是字段不能为空 需要传入一个默认空对象,这个时间就可以用 as 解决这个问题。
    • interface 不显示声明 鼠标悬浮 interface 上不显示完整对象,可以使用 type Simplify<T> = Pick<T, keyof T>;
    • 定义入参数类型 通过 extends 联合类型 范型 三种形式 可以满足参数复用、参数可选等大多数场景
    • 演示地址:https://uufefile.uupt.com/VideoLib/uunote/files/%E5%B1%8F%E5%B9%95%E5%BD%95%E5%88%B62022-02-18+%E4%B8%8A%E5%8D%8810_1645153587605.mp4
  2. 如何封装 jsCallNative & nativeCallJs
    • 避免在小程序内出现不能打开问题,方法会判断当前环境,非跑腿 APP 环境不进行调用;
    • 入参 支持 data: 传入的数据 method: 为方法名字 success: 成功回调 fail: 失败回调 四个参数 红色为必传项;
    • jsCallNative 支持 promise/callback 方式进行调用 nativeCallJs 只支持回调的形式进行调用 nativeCallJs('xxx',(event)=> {})
    • 目前这 jsCallNative 方法 ts 重构的不彻底,仅支持返回值的类型类型推导。提前需要通过范型形式传入 jsCallNative<addrinfo>;
  3. 如何设计 canIUse
    设计这个 API 参考了微信小程序 支付宝小程序 以及 H5 的 canIUse 的设计,结合现有情况 考虑只支持入参情况下三级判断逻辑。几乎所有的版本兼容都需要依赖此方法,该方法供内部和外部同时。
    • 第一级 API 判断
    • 第二级属性判断
    • 第三级属性值判断,考虑到属性存在多个情况, 第三级为一个正则规则,因为字符串转换成正则时需要 new 一下 所以需要对正则进行多次转义。
      具体实现逻辑为 对传入参数按.进行分割 然后根据第一个参数值获取对应函数的_Support 配置项, 然后获取_Support 配置项所有 key,对其进行遍历匹配,对配置项中 key 进行分割,根据分割后数组长度 如果长度为 1 那么就可以判断 API 判断。如果长度为 2 且第一项相等 那么就可以判定为属性值判断。如果长度为三 且 前两项都对应相等。那么就可以判断为具体属性值判断,然后对第三项进行正则匹配。
  4. 调试模式
    • 开启日志输出 通过全局变量进行交互 window.PAOTUISDKDEBUG = true 开启以后会把 JSBridge 日志进行打印出来;
window.PAOTUISDKDEBUG = true;

if (window.PAOTUISDKDEBUG) {
	console.log('[uu sdk log]:开始调用客户端方法');
	console.log('[uu sdk log]:函数名称:' + params.method);
	console.log('[uu sdk log]:发送参数:', JSON.stringify(params.data || ''));
}
if (window.PAOTUISDKDEBUG) {
	console.log('[uu sdk log]:开始调用JS方法');
	console.log('[uu sdk log]:函数名称:' + method);
}

  1. 关于注释
    可参考现有的代码注释规范 或者遵循 jsDOC 代码规范。
/**
 * @method 页面直接掉起用户端原生分享组件  //用户端2.6.9及以上版本支持!
 * @param {number} params.operateType //operateType,int 类型,0 隐藏分享按钮,1 显示分享按钮,2 直接显示分享弹框	//  2.6.9.0
 * @param {number} params.shareIconStyle  //  2.6.9.0 	0 白,1 黑
 * @param {object[]} params.shareTypeList 分享列表;
 * @param {number}  shareTypeList.showType 按钮类型:0分享到微信好友;1:分享到朋友圈;2保存图片;3复制链接;
 * @param {number}  shareTypeList.actionType 分享方式:1 分享网页    2 分享小程序  3 分享图片   4  保存图片  5 复制链接
 * @param {string} shareTypeList.imgUrl: 保存图片时用到的url  有该字段则为保存图片  actionType为4 时读取该字段
 * @param {string} shareTypeList.copyLinkUrl: 复制链接用到的链接url  有该字段则为复制链接  actionType为5 时读取该字段
 * @param {object} shareTypeList.webConfig    //分享网页配置信息  有该字段则为分享网页 actionType为1 读取该字段
 * @param {string} webConfig.shareTitle   分享网页的标题
 * @param {string} webConfig.shareUrl  分享网页的链接
 * @param {string} webConfig.shareContent   分享网页的描述
 * @param {string} webConfig.shareIcon   分享网页的icon图标
 * @param {object} shareTypeList.imgConfig    //分享网页配置信息  有该字段则为分享网页 actionType为1 读取该字段
 * @param {string} imgConfig.title   分享标题
 * @param {string} imgConfig.imgUrl  分享网页的链接
 * @param {object} shareTypeList.weappConfig    //分享网页配置信息  有该字段则为分享网页 actionType为1 读取该字段
 * @param {string} weappConfig.id // : 小程序原始id
 * @param {string} weappConfig.title:标题
 * @param {string} weappConfig.subTitle: 副标题
 * @param {string} weappConfig.url // : 小程序页面地址
 * @param {string} weappConfig.img //: 分享的封面图
 * @param {string} weappConfig.path //: 小程序页面路径
 * @param {string} weappConfig.type // : 0正式版  1开发版  2 体验版
 */

如何使用

通过 npm 安装

项目中.npmrc 如果已经修改地址可以直接安装

在现有项目中使用时,可以通过 cnpm 进行安装:

# 内网地址
$ cnpm set registry http://192.168.5.66:7001
# 外网地址
$ cnpm set registry http://xxxxxxxxx
# 安装
$cnpm i @uupt/jssdk@latest

当然,你也可以通过 yarn 或 pnpm 进行安装:

# 通过 yarn 安装yarn add @uupt/jssdk
# 通过 pnpm 安装pnpm add @uupt/jssdk

通过 CDN 使用

使用最简单的方法是直接在 html 文件中引入 CDN 链接,之后你可以通过全局变量 uupt 访问到所有组件。

<!-- 引入  JsSDK 的 JS 文件 -->
<script src="http://XXXXXXXXXXXXXXXXXX"></script>

<script>
	uupt.makePhoneCall({
		phoneNumber: 10086,
		success(result) {
			console.log('succesCallBack', result);
		},
		fail(result) {
			console.log('failCallBack', result);
		},
	});
</script>

成品展示

内部文档参考

  • 客户端方法调用文档
  • 客户端与前端交互方法目前存在的问题
  • 客户端 2.9.0 客户端方法变更记录
  • 用户端 2.6.8.1 新增桥接方法文档
  • 客户端上传图片、保存图片方法优化记录

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注