大家好啊!今天又来写写写了!
今天继续上次的话题,正式说明如何从0开始开发一个js-sdk。
来个图片镇一下场子。
效果还是可以的。对于这种,首先需要进行环境准备,比如安装nodeJs,并且配置好相关的依赖仓库等等,然后新建项目,这里不多说,默认懂得都懂。不懂的也可以使用我准备好的种子项目,如果遇到安装依赖报错,请降低nodejs的版本(我电脑上安装的版本为v16.20.2),种子项目直达地址https://gitee.com/dream-sk/js-sdk-module.git。这个种子文件项目比较精简,我相信看我这篇文章的人都是有基础的,都能看得懂(虽然简单,但是也是花了很大功夫的)。里面集成了多环境配置,webpack打包,代码混淆,测试页面等功能,完成了一个基本sdk开发的框架。
下面正式开始介绍项目代码。这是本项目的文件目录,dist目录为最终打包完成的sdk文件,对于使用方来说,只需要将这个sdk放入自己项目中,使用srcipt的标签引用到项目中,即可通过sdk内部的函数进行调用。
1、面向对象抽取属性
在测试页的效果图中,我们发现这个sdk需要有两种不同的弹窗页面,这两种弹窗都有一些通用的参数。两种弹窗出了形态上不一致,其他的地方基本上一模一样。所以我们可以根据这种特性,抽取sdk的相关父类,也是上图中出现的base.js文件。
父类当中在初始化的时候,需要做好兼容,因为不是每个参数都需要调用方传过来,我们自己的sdk需要有一套自己的默认样式,调用方只需要传入一个协议编号,就可以直接打开我们的sdk,其他的参数都是可选,这样也可以防止造成用户胡乱使用导致的软件风格不一致。
2、子类对象设计
子类对象因为有两种,所以需要处理两种不同的逻辑。但是处理方法都是类似的,先构造自己的方法,导出自己需要使用的参数以及回调,在接下来的逻辑当中调用。
3、初始化
协议sdk的初始化是非常重要的,这里需要将软件的基础页面进行搭建,排布好各种样式布局,定义好各个dom元素的事件函数等等任务。
其实仔细想想,对于sdk就是需要拿到html元素中的head与body,然后往head中引入自己的css标签,往body中插入自己的dom元素,这样自然就可以被接入方使用。所以我的代码就是创建新的div元素,然后往body中append自己的元素,就可以达到目的。
4、iframe加载协议内容
之前在公司中做这个项目的时候,我也想了不少方式,正常来讲大家都是在iframe使用src属性直接引用地址,但是这种方式就少了很多交互,整个过程非常生硬,大家可以看看我是如何做的。
我是在创建iframe之前append加载动画,在进行http请求完成或失败之时,将动画移除,而不能直接使用src属性。
initBody() {
const body = document.createElement("div");
body.setAttribute("class", "sdk__css_draw_body");
const iframeElement = document.createElement("iframe");
iframeElement.setAttribute("id", "sdk__css_draw_body_iframe");
const data = {
agreementNo: this.agreementNo,
version: this.version
};
// alert('draw:'+JSON.stringify(data));
// 加载框
this.loading = document.createElement("div");
this.loading.setAttribute("class","sdk__loader");
body.append(this.loading);
// const contentDocument = iframeElement.contentDocument || (iframeElement.contentWindow && iframeElement.contentWindow.document);
GET_AGREEMENT(ENV_CONFIG.REQUEST_ADDRESS, Constant.GET_AGREEMENT, data, this.timeout).then((resp) => {
// contentDocument.open();
// contentDocument.write(resp);
// contentDocument.close();
iframeElement.srcdoc = resp;
this.loading.remove();
}, (error) => {
// contentDocument.open();
// contentDocument.write(super.initErrorTips());
// contentDocument.close();
iframeElement.srcdoc = super.initErrorTips();
this.loading.remove();
typeof this.onError === "function" && this.onError();
});
body.append(iframeElement);
return body;
}
5、网络请求
对于sdk项目,根本不知道自己到底运行在什么环境中,所以不能指望接入方给你安装一些网络请求组件,我们需要使用原生js的方式,完成http请求,原生请求对象也就是XMLHttpRequest,我这里提供一个参考案例,给有需要的人。
function createXMLHttpRequest() {
let xmlHttp;
// 适用于大多数浏览器,以及IE7和IE更高版本
try {
xmlHttp = new XMLHttpRequest();
// 前端设置是否带cookie
xmlHttp.withCredentials = true;
} catch (e) {
// 适用于IE6
try {
xmlHttp = new ActiveXObject("Msxml2.XMLHTTP");
} catch (e) {
// 适用于IE5.5,以及IE更早版本
try {
xmlHttp = new ActiveXObject("Microsoft.XMLHTTP");
} catch (e) {
}
}
}
return xmlHttp;
}
/**
* 异步请求JSON数据
* @param serverAddress
* @param url
* @param data
* @param timeout
* @param requestBefore
* @param requestAfter
* @returns {Promise<unknown>}
*/
const POST = function (serverAddress, url, data, timeout = 10000, requestBefore, requestAfter) {
return new Promise(function (resolve, reject) {
// 超时将强制abort,直接退出
const timeoutHandle = setTimeout(function () {
if (XMLHttpRequest.readyState !== 4) {
reject(new Error('connect network timeout!'));
XMLHttpRequest.abort();
}
}, timeout);
// readyState changed
const onReadyStateChangeHandler = function () {
if (this.readyState !== 4) {
return;
}
// 清除超时定时器
clearTimeout(timeoutHandle);
// 响应码200
if (this.status === 200) {
const respPlainText = this.response;
resolve(respPlainText);
} else {
reject(new Error(this.statusText));
}
};
// 创建xhr
const XMLHttpRequest = new createXMLHttpRequest();
// 请求前回调
typeof requestBefore === 'function' && requestBefore();
XMLHttpRequest.open("POST", serverAddress + url);
XMLHttpRequest.timeout = 1000;
XMLHttpRequest.onreadystatechange = onReadyStateChangeHandler;
XMLHttpRequest.responseType = "json";
XMLHttpRequest.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
const params = new URLSearchParams();
for (let key in data) {
if (Object.hasOwnProperty.call(data, key)) {
if (data[key]) {
const value = encodeURIComponent(data[key]); // 编码值
params.append(encodeURIComponent(key), value); // 编码键名
}
}
}
// 获取 x-www-form-urlencoded 格式的字符串
const formUrlEncodedString = params.toString();
XMLHttpRequest.send(formUrlEncodedString);
typeof requestAfter === 'function' && requestAfter();
});
};
由于我使用的是application/x-www-form-urlencoded,所以需要将数据转换为AAA=aaa&BBB=bbb。
6、挂载全局对象
这里最重要的就是需要将唤起方法定义好,并且将相关的对象挂载到启动函数上,并且需要在这里进行初始化和启动,如下面代码。这里用到了代理,这个代理如果不懂,可以去看看阮一峰的《ECMAScript 6 入门教程》,Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截。只要自己写一个测试demo,就可以理解了。
import Draw from "./agreement/draw";
import Modal from "./agreement/modal";
global.AgreementSDK = {};
const widgetClassMap = new Map();
widgetClassMap.set("startModal", Modal);
widgetClassMap.set("startDraw", Draw);
const functionList = ["startModal", "startDraw"];
functionList.forEach((func) => {
const currentWidget = widgetClassMap.get(func);
global.AgreementSDK[func] = (options) => {
const currentProxy = new Proxy(new currentWidget(options), {
get(target, propKey) {
return target[propKey];
}
})
currentProxy.init().then(()=> {
currentProxy.start();
})
};
})
好了,到此结束,我在这里将地址发在后面,并且附上后台管理系统,请大家多多转发支持一下。
sdk:https://gitee.com/dream-sk/agreement-sdk.git