前置知识
SDK 的全称是 Software Development Kit,翻译过来是软件开发工具包,这是一种被用来辅助开发某类软件而编写的特定软件包,其主要是为宿主系统提供服务的。
接口设计原则
- 单一职责原则(Single Responsibility Principle:SRP)
- 定义:应该有且仅有一个原因引起类的变化
- 应该根据实际业务情况而定,关注变化点,在实际使用时,类很难做到职责单一,但是接口的职责应该尽量单一
- 里氏替换原则(Liskov Substitution Principle:LSP)
- 定义:所有引用基类的地方必须可以透明的使用器子类的对象。其目的是为良好的继承定义了一个规范
- 规范:
- 在类中调用其他类时务必要使用父类或接口,如果不能使用父类或接口,则说明了类的设计已经违背开LSP原则
- 子类必须完全实现父类的方法
- 子类可以拓展自身的属性和方法
- 覆盖或重写父类的方法时输出的结果可以被缩小,输入的参数可以被放大
- 依赖倒置原则(Dependence Inversion Principle:DIP)
- 定义:High level modules should not depend upon low level modules.Both should depend upon abstractions.Abstractions should not depend upon details.Details should depend upon abstractions:面向接口编程
- 具体含义拆分
- 高层模块不应该依赖于低层模块。两者都应该依赖其抽象
- 抽象不应该依赖于细节
- 细节应该依赖于抽象
- 接口隔离原则
- 定义:
- 客户端不应该依赖其不需要的接口
- 类间的依赖关系应该建立在最小的接口之上
- 即:接口尽量细化,要建立单一的接口,同时接口中的方法尽量少
- 具体实践
- 一个接口只服务于一个子模块或业务逻辑
- 已经被污染的接口要尽量去修改,同时在必要的时候采用适配模式进行转化和处理
- 定义:
- 开闭原则
- 定义:一个软件实体如类、模块和函数应该对齐拓展开放,对修改关闭
设计初衷
- 可以减少人力成本和开发时间
- 共享一套代码,便于后期维护
- 最牛批的实现方式是开发一套可以支持所有前端框架使用的通用SDK(如React、Vue、angular、小程序等)(可以采用不同语言的SDK分别管理和发版),同时在此SDK的基础上,可以快速地根据框架的语法特性进行上层封装,是JavaScript SDK的核心要求之一;
- 最牛批的实现方式是开发一套可以支持所有前端框架使用的通用SDK(如React、Vue、angular、小程序等)(可以采用不同语言的SDK分别管理和发版),同时在此SDK的基础上,可以快速地根据框架的语法特性进行上层封装,是JavaScript SDK的核心要求之一;
设计理念
设计SDK的方式主要取决于SDK的额最终用途,如提供给网页调用和提供给服务端调用是不一样的,当然也是有一些明显的共用原则,主要有两个基本的原则:
- 最小可用性原则:即用最少的代码,尽量减少不需要的逻辑
- 最少依赖原则:即用最低额度的外部依赖,减少没必要的额外依赖
- 前端SDK一般需要考虑的问题
- 需要兼容的浏览器类型和版本(原因和理由)
- 代码在用户的html文件中是如何引入的
- 在html的什么地方引入
- 需要提前了解html的渲染方式、浏览器对资源的加载方式
- script是否需要
async
、defer
或charset
等标记
- 服务端交互的SDK需要考虑到
- api要限流、限制次数、防止盗刷
- 日志监控和数据上报
设计原则
- 安全与稳定
- 网络请求方面可以使用HTTPS,既可以提高安全性,还可以防止国内运营商常见的DNS劫持等问题
- 配置、DB数据存储方面
- SDK的配置、相关数据以及用户数据都是SDK的核心内容,一定要使用有效的合适的加密方案来存储;而对应的性能问题可以通过尽可能少的加解密次数以及不同级别的加解密算法等来解决
- 加解密使用的密钥以及加解密的额算法本身的安全性也需要高度的重视,建议放在SO上,同时再通过SO的加固增加安全性
- 少依赖与易拓展
- 支持插件化:最大限度支持拓展
- 支持Hook机制:可以满足个性化需求
SDK内容包括
- 功能模块
- 交付给客户接入、安装的产物
- API
- SDK的核心内容,提供给开发者的API包,是一切功能的入口
- 文档
- 标准统一、结构化展示形式,接入指引
- 这里的文档包括商业接入流程、接入指引、架构介绍、更新方法、API说明、测试报告、常见问题、版本历史、接入验证方法或验证工具等,尤其有的优秀的SDK文档是包含新旧版本文档记录的,有重大迭代时对应的SDK文档也会有新版本的迭代更新,并且有旧版本的维护与升级说明
- Demo
- 可以直接运行的Demo,直观体验
- 监控和告警
- 数据上报可以简单分为
关键日志上报
和异常数据上报
,前者主要是为了SDK的开发者方便定位问题;后者主要是建立一套客户端的监控和告警机制,从而提前开启关键日志分析问题 - 监控和告警主要是为SDK开发者自身服务的
- 一方面通过监控和告警可以了解到SDK的版本、接口调用量、接口失败率等数据
- 另一方面可以尽早的发现问题,比业务更早、更快的响应
- 数据上报可以简单分为
- SDK中的多线程
- 为了减少SDK对应用本身的影响,尽可能减少SDK引起的ANR(超时程序无响应)等问题,需要遵循以下两点
- SDK非必须,不要使用应用的主线程,就算非要使用也需要是一些简单的操作,不可以长时间占用
- SDK应该有一个专门的线程来处理SDK相关的操作
- 进程间通信相关
- Handle大法
- 所有耗时、异步操作都通过handle扔给SDK的线程去处理,处理结束后再将结果通过handle发送给主线程
- 任何时候主线程只做一件事即UI调整;所有耗时的操作(如读取文件、读取DB、网络数据读取、网络请求等都不可占用主线程)
- Handle大法
- 为了减少SDK对应用本身的影响,尽可能减少SDK引起的ANR(超时程序无响应)等问题,需要遵循以下两点
- 📢:SDK的相关数据有哪些
- 大前提:SDK的接入量足够多、使用用户要足够大,是一个前人栽树后人乘凉的过程
- 接口调用数据
- 目的:主要用来监控接口的稳定性、及时发现问题、以及进行数据的推广等
- 数据包括:接口调用的成功率、失败率、调用次数等
- 开发者、接入应用相关数据
- 目的:有助于了解当前开发者的活跃地带、为服务提供支持,了解目前的接入应用的分布、了解应用市场的走向等
- 数据包括:开发者的地域分布、接入应用的类型、单开发者接入应用的数据等
- SDK的用户数据
- 用户个人数据:
- 目的:使得运营更加了解用户的实际情况
- 数据包括:用户的地域分布、性别、职业等
- 用户设备数据:
- 目的:使得开发者更好的做兼容和后续方向的规划
- 数据包括:用户设备机型、系统、系统版本、内存、CPU、网络、分辨率、DPI、传感器等数据
- 用户使用习惯数据:
- 目的:使得运营更加了解用户的额使用习惯、更容易掌握推广的时机
- 数据包括:使用时段、使用时长、打开次数、打开评率、留存、活跃等数据
- 用户和应用的相关性数据:
- 目的:有助于市场的开拓
- 数据包括:单一用户手机上同类型APP的安装数量、每天的使用APP的数据等数据
- 用户个人数据:
SDK架构设计
-
SDK架构分解
- API层
- 是对整个SDK的对外封装,经过这一层后仅仅将需要暴露给外部的接口、结构体声明出来,向开发者隐藏具体的实现
- Framework层
- 是最核心层,也是SDK的最底层;完成了SDK的初始化、模块管理以及一些公共基础工具(如数据存储、网络请求处理等)
- 也可以再细分出一层libware,主要承载一些通用的方法(如字符编码、文件读取、包信息读取等)的实现和对通用库的封装(如AsyncHttp等)
- Module层
- 应该称为是中间层,这一层一般是具体模块的额业务逻辑,主要是具体的功能实现
- 一般情况下对于Module也可以又按照上述的三层结构来划分,做到各个模块之间的功能独立
- API层
-
基础架构设计
- 主要考虑方面是:可读性、可拓展性和可维护性三方面
- 基础架构分为两个层次:
- 业务层
- 可以独立成业务模块:包括开放API接口和业务功能实现
- 通用功能层
- 可以分为通用功能模块和基础工具模块
- 业务层
-
开放API接口设计
- 建议遵循以下规则
-
API接口命名规范,且通俗易懂
- 同时需要注意
命名空间
的问题,避免与其他库冲突,推荐使用(function (){...})()
将SDK代码包起来,如jQuery、NodeJS等类库通常使用的一个方法是将创建的私有命名空间的整个文件用闭包包起来,这样可以避免与其他库冲突
- 同时需要注意
-
接口职责单一:一个接口只做一件事
- 接口是SDK和用户沟通的桥梁,每一个接口对应着一个独立的SDK功能,并且应该有明确的输入和输出
- 即使有两个接口有比较接近的功能,但是用一个接口又比较麻烦,那就不进行合并处理
-
接口参数要尽量少,且做好相应的参数校验和逻辑保护
- 参数尽量少,但是也要实现可拓展的功能
- 参数尽可能使用Object封装,当有多个同一类型的参数时要使用对象的方式进行传递
- 此外所有的接口参数必须要
提前第一时间
做好合法性校验,且当需要进行转义、需要进行类型转换的参数时一定要提前
去进行处理
-
最小可用性和最少依赖原则
- 能简单实现就不炫技
- 能自己实现就不依赖第三方库
-
接口尽量保证是非阻塞的,不影响开发者正常业务逻辑
- 设计的SDK需要足够稳定、向后兼容、合适的单元测试进行质量保证
- 尽量使用try-catch去捕获错误和检测对应语法是否支持,如检测cookie、session、localStorage、sessionStorage等是否支持
jslet checkCanSessionStorage = function () { let mod = "modernizr"; try { sessionStorage.setItem(mod, mod); sessionStorage.removeItem(mod); return true; } catch (e) { return false; } };
-
接口结果最好是直接返回,尽量减少使用回调
-
错误边界处理,提高SDK的稳定性 如果 SDK 组件抛出错误,导致接入的页面崩溃了,妥妥的 p0 级 bug 。
- 所以,一定要将 SDK 的错误 catch 在组件内部。
- 对于 React 组件,用 ErrorBoundary 包裹是必不可少的
- 维护稳定性一般关注点如下
- JS异常
- 资源加载异常
- API请求异常
- 白屏异常
-
- 建议遵循以下规则
-
业务功能框架设计与开发
- 避免过度设计,主要考虑某一类具体的业务需求即可,不要太过于纠结概率性问题,SDK要有自己的具体业务需求;
- 要考虑到业务方「treeShaking」的问题
- 当前业界比较通用的方式是:将不同组件编译到不同目录,业务方通过组件目录的形式引用
jsimport SDKForA from 'SDK/dist/modern/components/SDKForA'; // 组件导出的npm包/编译后产物打包的目录/ESM 规范的打包路径/要引入的组件
-
基础核心库设计与开发
- 需要保证功能间相互独立,降低耦合度
-
除了核心的偏领域的模块外,SDK还需要有更基础的与领域无关的模块,包括SDK内核(构造方法、插件机制、与上下游服务器的额交互、上报队列机制、不同环境的管理等)和工具类库
-
构建
- webpack工具:
- 可以使用ES6模块化方式进行构建,这样可以使得业务在引入SDK时通过解构的方式减少最终业务代码的体积
- 尽量使用node的方式进行调用(即webpack.run的方式执行),原因是SDK的构建需要应对不同的参数变化,node方式比纯配置方式更加灵活的调整输入和输出的参数
webpack.run
使用回调函数的方式进行构建,当然开发者也可以封装成Promise
- Rollup工具
- 可以使用ES6模块化方式进行构建,这样可以使得业务在引入SDK时通过解构的方式减少最终业务代码的体积
- rollup.rollup会返回一个Promise,可以通过async的方式来进行构建
- webpack工具:
-
打包与发布
-
SDK版本管理机制
- 较成熟的版本管理机制是
语义化版本号
,具体表现为{主版本}.{次版本}.{补丁版本},实现简单易记好管理;- 主版本:一般涉及到重大的更新时才会更替主版本号,而且很大概率会出现新旧版本不兼容的问题出现
- 次版本:应用于新特性或较大的调整,因此可能会出现breakchange
- 补丁版本:较小的优化或者一些fixed可以通过更新补丁版本号实现更新迭代
- 较成熟的版本管理机制是
-
SDK的引用方式
- 大体分为CDN引用和NPM两种,当然还有其他的如ES Module、CommonJS、AMD/CMD/UMD等,可以采用第三方库如webpack实现自动适配所有形式的模块,同时提供最基本的CDN和NPM两种引用方式,供用户多重选择,但最终都是需要通过CDN或NPM的方式进行提供的
js//静态资源引入 <script src="/sdk/v1/wpkReporter"></script> // ES Module import wpkReporter from 'wpkReporter' // CommonJS const wpkReporter = require('wpkReporter') // AMD,requireJS引用 //AMD(异步模块定义,主用于浏览器,遵循依赖前置原则) define([jquery.js, lodash.js], function($, _){ console.log("jquery and lodash", $, _) }) //CMD(通用模块定义,用于浏览器或node中,遵循依赖就近原则) define(function(require){ const lodash = require('./a.js') console.log("lodash", lodash) }) require.config({ paths: { "wpk": "https://g.alicdn.com/woodpeckerx/jssdk/wpkReporter.js", } }) require(['wpk', 'test'], function (wpk) { // do your business })
-
-
性能方面应该考虑的问题点
- 白屏时间
- 可交互时间(TTI)
- 首屏时间
- FP/FMP/FCP等
-
关于同步和异步接口
- 可以同步的就不用异步
- 能不用全局回调的就不用全局回调,不然后续会吭哧吭哧的改为独立的局部模块回调(弃用全局回调,改为直接在接口调用时让同步添加对应的接口回调)
- 同一个回调里的接口尽可能的少,可以合并的尽量合并
-
使用建议
- 异步语法
- 应该使用异步语法去加载脚本,改善用户体验,SDK类库不应该影响主页面的加载
- 即在进行CDN引入JS脚本文件时,需要添加
async
配置从而实现异步加载SDK文件 - 异步脚本未必会按照指定的额顺序执行,且不应该使用
document.write
,因此如果脚本有依赖于执行顺序或者需要访问或修改网页的额DOM或CSSOM,那么您可能需要重新编写此类脚本 - 另外为了解决脚本引入引发的「网络请求」问题,建议将较小的脚本进行内嵌实现;不是初始化必须得代码应该异步或延迟执行
- 异步语法
JS-SDK在前端中的案例
- UI组件库
- 性能监控工具,如阿里的arms,岳鹰前端监控SDK等
- 统计分析工具
- 智能验证工具SDK
- TRTC Web SDK新架构设计
SDK优劣分析
- SDK优点
- 实现简单,可以在不允许跨域的情况下实现分析数据
- 实现业务逻辑更加灵活,可以定制化实现各种需求
- 可以实现模块化编程
- 可以提高程序的可维护性和可拓展性
- SDK缺点
- 体积较大:SDK通常含有大量的代码,可能会影响应用程序的下载速度和安装体验,一般可以尝试将SDK进行模块化编程,方便后续的Tree-Shaking操作,从而减小包体积
- 版本更新:版本更新可能会带来业务方更新与迭代
- 后续维护不方便:SDK内部逻辑复杂,后续维护有成本
- 限制自定义:由于开发者的应用程序与SDK进行了绑定,后续的自定义拓展可能会受限
JS-SDK实现
- JS-SDK常用类型
- Web的api集合(类似微信官方的JS-SDK工具)
- 分析与统计工具(类似百度统计的JS-SDK工具)
- 嵌入式类如widget
- 常用的设计模式
- 单例模式:一个类只返回一个实例,一旦创建再次调用就直接返回(如jQuery、lodash、moment等)
- 构造函数模式
- 混合模式(原型模式+构造函数模式)
- 工厂模式
- 发布订阅模式
- API接口 前端SDK的核心就是API接口,该方式以函数的形式提供所需的功能,开发人员可以通过调用接口函数来实现相应的功能
js
var SDK = {
// 接口函数1
showMessage: function(message) {
alert(message);
},
// 接口函数2
showConfirm: function(message) {
return confirm(message);
}
};
- 代码封装
前端SDK可以根据功能模块进行模块化封装,使得代码更易于管理和维护;通过将SDK代码进行重构,让SDK可以更加容易拓展、尽量减少模块之间的耦合,使得各个功能模块更加独立,甚至可以做到轻松的添加和删除模块而不会对其他的模块有影响。
模块化主要是为了方便SDK的开发者适应各种需求的变化,而插件化则主要是为了更方便SDK的使用者;插件化就是指SDK开发者可以根据使用者需求提供接入相对应功能的差异化的SDK包,开发者可以自由动态生成SDK包来适应不同的需求;
js
// 封装消息框模块
SDK.Dialog = {};
SDK.Dialog.Message = function(message) {
alert(message);
};
// 封装确认框模块
SDK.Dialog.Confirm = function(message) {
return confirm(message);
};
- 依赖注入
前端SDK中的不同模块之间可能会有依赖关系,可以通过依赖注入的方式实现依赖关系管理
js
// 声明依赖模块
SDK.Dialog = {};
SDK.Dialog.Message = function(message) {
alert(message);
};
SDK.Dialog.Confirm = function(message) {
return confirm(message);
};
// 依赖模块
SDK.Action = function(dialog) {
this.dialog = dialog;
}
// 注入依赖模块
var action = new SDK.Action(SDK.Dialog);
action.dialog.Message("Hello World");
-
使用封装的SDK
- SDK的引用
js// 在HTML页面中引入SDK库 <script type="text/javascript" src="sdk.js"></script>
- API使用
js// 调用SDK中的接口函数来实现相应功能 <button onclick="SDK.Dialog.Message('Hello World!')"> 点我显示消息框 </button> <button onclick="SDK.Dialog.Confirm('确定要删除吗?')"> 点我显示确认框 </button>
- 模块使用
js// 通过依赖注入的方式使用SDK模块 var dialog = new SDK.Dialog(); var action = new SDK.Action(dialog); action.ShowMessage('Hello World');
推荐文献
清晰详细的前端埋点SDK入门实现
前端SDK开发用法介绍
前端资源共享方案对比-笔记:iframe/JS-SDK/微前端