前言
移动开发的跨平台是大势所趋,可以节省开发成本,提高开发效率,迅速响应业务变化,现在主流的应用还是使用 H5 和原生的通信来实现跨平台的开发。JSBridge 框架解决 JS 和 Native 的通信问题实现移动端跨平台开发。结合业务来讲,JS SDK 作为两端连接的工具,它的使用体验决定了项目的部分效率和质量。
背景
目前内嵌在跑腿 App 中的页面与客户端进行交互的方案有三种: 1. schema 协议拦截 2. 桥接方法 3. 相互调用全局变量、方法 这三种交互方法目前都在用,并且引入方式不同,造成代码维护以及业务开发的时候比较混乱,还有文档的维护也比较难。开发业务过程中,一旦涉及到营销活动类的一些新功能,就需要客户端新增一些方法,但是这些方法只能通过判断版本号来判断是否能使用,而版本号又需要通过通配符或者其他方法来获取,这样一来就写了很多与业务无关的冗余代码,可能业务开发很快,处理兼容就占了一半的开发时间,1.版本兼容问题,导致前端代码非常冗余 2.两端表现以及行为不一致 3.API 调用方式存在多种 4.文档维护困难 5.方法定义重复 针对此类问题决定对现有的客户端文档与 API 方法进行一次较大规模的优化。
目标
作为业务方和客户端的中间层,在项目设计理念上原则是尽可能独立并, 形成一套有序、稳定的规范。在业务方调用时,减少理解和测试使用难度,能少处理一些兼容,支持多种形式调用 promise 和 callback。在回传数据时,严格约束相关字段。
- 支持 Commonjs、EsModule 等多种规范,提供 npm 引入和 cdn 引入的能力;
- 包含一份可读性良好的文档说明,并且有示例可交互;
- 文档库、API 发布自动化部署 实现一键更新发布;
- 项目开发、提交、版本管理规范;
- API 设计规范、设计命名规范等;
- API 开发 、调试 、验证 流程简单高效,不需要额外的心智负担;
- 直接一键打开 App 能力;
技术选型
- 技术栈:ts
- 包管理:father-build + simple-git
- 打包工具:rollup + babel
- 代码规范:prettier + lint-staged
- 文档库:dumi + React + and
项目规范
目录规范
- src 文件夹下
- app 文件夹存放 app 相关文档及接口;
- 若需要增加小程序或其他平台可增加 miniProgram 或平台对应文件夹即可;
- 注意方法命名
不要冲突
涉及 canIUse 处理逻辑;
- app 文件夹下
- base 目录存放通用方法;
- user 目录存放用户端相关方法;
- shop 目录存放商户端相关方法;
- driver 目录存放跑男端相关方法;
- 单个 API 文件夹下
- 文件夹命名可以参考微信小程序、支付宝小程序的实现 尽可能语义化。减少理解成本;
- index.ts 为接口的实现;
- READEM.md 为使用文档;
- interface 为 ts 定义的一些 ts 的一些声明;
- utils 为私有的公共方法;
- 其他静态资源等也可以放在当前目录下;
出入参规范
- 关于入参
- 入参格式与小程序等基本相同,按照文档说明使用即可;
- 参数控制在 6 个以内,除去不定长参数以外,函数具备不同逻辑意义的参数建议控制在 6 个以内,过多参数会导致维护难度增大;
- 参数过多时,可以通过增加 options 参数来传递一些参数;
- 参数提前在声明文件中定义好,如果需要新增参数及时修改类型声明。以便于在 ts 中使用参数提示;
- 大多方法都支持 callback 形式调用 支持 success fail 回调,一些简单项目可以通过这种形式使用;
- 出参
- 遵循 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
- 如何版本号获取
- window.PAOTUIAPPVERSION 可以优先判断这个值是否存在 如果不存在调用 getAppVer()
- getAppVer() 获取版本号 该方法调用一次后会在 window.PAOTUIAPPVERSION 进行赋值
- 如何判断 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');
- 如何实现 Ts 约束代码 代码提示类型推断
- 最小类型约束 对于一些可预测的类型提前声明好类型值。比如 shareIconStyle: 0 | 1;
- 参数智能推导
- promise 模式下字段提示 在 interface 中提前定义好接口入参和出参的类型,需要在 successReuslt 函数中传入对应的 interface 例子:
<addrInfo>
使用范型传入。比如:successReuslt<addrInfo>
- callback 模式下字段提示 同样提前定义好类型, 然后在 BaseParams 中传入 例子:
BaseParams<addrInfo>
- promise 模式下字段提示 在 interface 中提前定义好接口入参和出参的类型,需要在 successReuslt 函数中传入对应的 interface 例子:
- 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
- 如何封装 jsCallNative & nativeCallJs
- 避免在小程序内出现不能打开问题,方法会判断当前环境,非跑腿 APP 环境不进行调用;
- 入参 支持 data: 传入的数据
method
: 为方法名字 success: 成功回调 fail: 失败回调 四个参数 红色为必传项; - jsCallNative 支持 promise/callback 方式进行调用 nativeCallJs 只支持回调的形式进行调用
nativeCallJs('xxx',(event)=> {})
- 目前这 jsCallNative 方法 ts 重构的不彻底,仅支持返回值的类型类型推导。提前需要通过范型形式传入
jsCallNative<addrinfo>
;
- 如何设计 canIUse
设计这个 API 参考了微信小程序 支付宝小程序 以及 H5 的 canIUse 的设计,结合现有情况 考虑只支持入参
情况下三级判断逻辑。几乎所有的版本兼容都需要依赖此方法,该方法供内部和外部同时。- 第一级 API 判断
- 第二级属性判断
- 第三级属性值判断,考虑到属性存在多个情况, 第三级为一个正则规则,因为字符串转换成正则时需要 new 一下 所以需要对正则进行多次转义。
具体实现逻辑为 对传入参数按.
进行分割 然后根据第一个参数值获取对应函数的_Support 配置项, 然后获取_Support 配置项所有 key,对其进行遍历匹配,对配置项中 key 进行分割,根据分割后数组长度 如果长度为 1 那么就可以判断 API 判断。如果长度为 2 且第一项相等 那么就可以判定为属性值判断。如果长度为三 且 前两项都对应相等。那么就可以判断为具体属性值判断,然后对第三项进行正则匹配。
- 调试模式
- 开启日志输出 通过全局变量进行交互
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);
}
- 关于注释
可参考现有的代码注释规范 或者遵循 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 新增桥接方法文档
- 客户端上传图片、保存图片方法优化记录