wujie在第三方SDK开发中的应用实践
背景
业务介绍
目前作者所开发的项目都涉及对外以 SDK 的形式嵌入到用户系统中,在保证 SDK 不影响用户环境 的同时还要保证 SDK 的整个接入流程必须是 简单可靠的,尽量的降低用户的接入成本,但是实现情况是是:
- SDK 经过 webpack 构建出 umd 格式的产物,产物中会包含 React、antd、mobx 等依赖,直接运行在用户环境会对用户环境造成污染。
- 客户的系统技术差、老旧、存在许多全局污染,会影响 SDK 的正常工作,同时我们也很难推动用户系统进行改造。
因此将 SDK 运行在一个与用户系统隔离的沙箱中是有必要的,这既能保证 SDK 有一个独立且纯净的运行环境,也能防止 SDK 污染用户环境。、
需求场景概述
-
SDK 数据导出
例如 文档标注工具SDK,图片编辑器 SDK,用户希望 SDK 能够将处理好的结果数据直接进行导出,由用户自行做进一步的处理。在传统的 iframe 方案中都需要借助浏览器的 postMessage 进行属于的传递。
-
SDK 鉴权
SDK 交付的另一痛点就是缺少鉴权机制,对于一些对系统安全性要高的客户来说,他们不希望 SDK 能够直接被访问,这就要求 SDK 需要去对接用户的权限管理机制。
第三方javascript概念介绍
SDK通常被称为"第三方javascript"。相较于微前端所强调的能够集成各方模块组成系统, 第三方javascript强调的是如何将已有功能快速无缝集成入客户已存在的系统中,且彼此无冲突。
整体集成入客户系统中的功能既包括前端视图也包含后端服务, 二者需要配合实现整体业务功能, 嵌入用户系统的SDK整体看更像是一个“微系统”。
常见的第三方javascript应用包括
- 百度统计 -- 寄宿于客户站点内运行, 不包含图形界面
- 百度爱番番 -- 沟通窗口、留言板等功能寄宿于客户站点内运行, 包含图形界面, 更复杂
第三方SDK开发之殇
需要考虑依赖使用
开发运行于第三方的SDK时, 需要考虑SDK本身的依赖与宿主环境的依赖是否存在冲突。
举例来说, 假如SDK使用了React, 那么就需要考虑宿主环境的React的版本, 如果与宿主环境React冲突或者重叠都将带来Bug, 其他依赖亦然。这给SDK开发者带来了极大的设计负担。
在过去,社区SDK常采取zero dependency策略, 但是对于一些包含复杂功能的SDK而言, 不使用任何依赖基本不现实。
易产生环境冲突
SDK寄宿于第三方运行环境。第三方环境未知性强。部分甲方系统存在质量低劣情况, 如全局调整div样式。
div {
box-sizing: border-box
}
运行与第三方环境的SDK常常会受到宿主影响, 虽自身代码规范无误, 但因受到宿主环境干扰而导致无法正常使用的例子也较为常见。
难以要求使用方做修改适配
此问题对于一些交付目标客群为金融场景的项目中表现尤为突出, 如国有银行, 具备极强的甲方色彩。
基于此场景, 实际交互过程中较难要求客户对于已有系统进行过多改造以适配SDK集成。这更进一步要求SDK能够开箱即用、接入成本低。
沙箱需要具备的能力
环境隔离
环境隔离包括 js 隔离和 css 隔离,避免和宿主环境出现如全局变量冲突、样式冲突,这些冲突可能会导致应用样式异常,甚至功能不可用。
通信
基于用户的实际场景,沙箱需要提供一套完整的通信系统,宿主环境通过调用沙箱暴露出去的 API 和沙箱进行双向通信。
便捷
沙箱除了具备环境隔离和通信功能以外,还需要考虑用户的接入成本,大部分的第三方客户比较青睐 iframe 这种接入方式,因为这种方式最简单、学习成本低,所以我们提供的沙箱在使用方式上需要尽可能的向 iframe 靠拢。
为什么选择wujie
wujie定义
无界微前端是一款基于 Web Components + iframe 微前端框架,具备接入成本低、应用隔离、速度快等一系列优点,内部提供了一套开箱即用的安全沙箱机制,更多具体特性可查看文档。
相较于iframe的优势
在过去, 一些项目在面对包含复杂视图层功能的SDK的需求时, 为了避免与宿主环境产生依赖及运行时的冲突,使用了iframe方案开发。即在集成方页面创建一个iframe, 加载包含视图层功能的页面, 以实现将视图层功能嵌入至宿主环境局部。
iframe方案存在诸多问题。 对此, 国内微前端框架qiankun产出过一篇文档 — Why Not Iframe 文章对于iframe的问题做了讲解。
而基于wujie做第三方SDK开发,能够杜绝使用iframe遇到的问题,如dom割裂严重,弹窗只能在iframe
内部展示,无法覆盖全局等。同时具备更优质的通信能力
- 启动程序时为SDK注入参数
startApp({
props: {
param1: 'xxx',
param2: 'yyy'
}
});
- 运行时双向通信
wujie提供了事件总线模块, 基于此能够快速实现运行时双向通信,在此不赘述。
相较于qiankun沙箱的优势
qiankun 是阿里开源的一款微前端框架,和wujie一样他们也实现了一套自己的沙箱机制,用于 css 和 js 隔离,相比于wujie它的不足点如下:
-
适配成本高
用户需要按照qiankun的要求对工程化、生命周期、静态资源路径、路由等做一系列的适配工作,接入成本高。
-
沙箱机制不成熟
qiankun 沙箱在处理 css 隔离上还存在诸多问题,比如挂载到 ducument.body 的组件样式无法生效 、无法适配 React<17 的事件系统、无法配合 styled-components 使用等,
-
不成熟的通信机制
qiankun 提供了一套自己的通信机制,但是这种机制并不完善,这里不过多赘述了,官方准备在 v3 版本中废弃这套机制。
qiankun 所暴露出来的不足在 wujie 中都得到了很好的解决。
使用方式
流程
研发侧
- 提供 SDK 打包产物
wujie 采用的是 html-entry 的方式去加载子应用,所以 SDK 构建的产物必须是一个包含 html 文件的完整项目,例如下面例子:
-
提供 SDK 接入工具
SDK 接入工具需要遵守两个原则:
-
能够适配用户所有框架的项目(React、Vue、Angular、无框架)
-
不需要进行依赖管控
wujie 为每个框架都提供了相应的快速接入子应用的工具,例如 wujie-vue2、wujie-vue2、wujie-react,但这种接入工具需要对用户的依赖环境有要求,比如说 wujie-react。
{ "name": "wujie-react", "version": "1.0.17", "description": "无界微前端React组件封装", "main": "./lib/index.js", "module": "./esm/index.js", // ... "peerDependencies": { "react": ">=16.0.0" //要求react版本一定要大于react }, // ... }
他要求用户环境的 react 版本必须要大于等于 16,这对于客户的一些老项目来说是不现实的。
基于这两条原则,我们将采用 wujie 提供的原生启动子项目的方案,下面是基于该方案的上层封装,最终的构建产物以 IIFE 的形式运行在用户的环境,将 SDK 接入工具挂载到 window 对象上:
1、安装wujie
npm install wujie -S
2、封装接入工具
import { startApp, bus , EventBus} from "wujie"; const sdkSandbox = { startApp({name: sting, url: string, container: HTMLElement}): void; bus: EventBus; }; // 给用户提供启动 SDK 的方法 sdkSandbox.startApp = function({name, url, container}) { startApp({ name, url, el: container, sync: true }); } // 为用户提供bus通信工具 sdkSandbox.bus = bus; // 将sdkSandbox挂载到用户环境的window对象中 window.sdkSandbox = sdkSandbox;
3、产出 umd 格式的产物
-
用户侧
- 引入 SDK 接入工具
在 html 文件中通过 script 标签从用户服务器上拉取 sdk 接入工具,在 window 对象上完成 sdk 接入方法的挂载。
<!doctype html>
<html ang="en" translate="no">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width,initial-scale=1,maximum-scale=1,minimum-scale=1,user-scalable=no,viewport-fit=cover">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link rel="shortcut icon" href="">
// 引入sdk接入工具
<script src="/xxx/sdkRegister.js"></script>
</head>
<body>
<div id="react-app"></div>
</body>
</html>
- 渲染 SDK
下面以 React 项目为例展示 SDK 的渲染
const TextDiffPage = () => {
useEffect(() => {
window.sdkSandbox.startApp('/xxx/textdiffSdk', document.getElementById('textdiffContainer'))
// 监听事件
window.sdkSandbox.bus.$on("事件名字", function (arg1, arg2, ...) {});
return () => {
window.sdkSandbox.bus.$off("事件名字", function (arg1, arg2, ...) {});
}
})
return (
<div>
<Header />
<div id="textdiffContainer"></div>
</div>
)
}
问题和挑战
跨域请求
子应用的资源和接口的请求都在主域名发起,所以会有跨域问题,子应用必须做 CORS 设置:
app.use((req, res, next) => {
// 路径判断等等
res.set({
"Access-Control-Allow-Credentials": true,
"Access-Control-Allow-Origin": req.headers.origin || "*",
"Access-Control-Allow-Headers": "X-Requested-With,Content-Type",
"Access-Control-Allow-Methods": "PUT,POST,GET,DELETE,OPTIONS",
"Content-Type": "application/json; charset=utf-8",
});
// 其他操作
});
身份丢失
identity discontinuity(身份丢失问题)在基于 iframe 的沙箱中是一个凸现的问题。所有非简单类型(Non-primitive)在跨 iframe 传导中均存在此问题。
const globalOne = window;
let iframe = document.createElement('iframe');
document.body.appendChild(iframe);
const iframeArray = iframe.contentWindow.Array;
console.assert(iframeArray !== Array);
const list = iframeArray('a', 'b', 'c');
list instanceof Array; // false
[] instanceof iframeArray; // false
Array.isArray(list); // true
identity discontinuity(身份丢失问题)是TC39对此问题的官方命名, 上述例子也来源于TC39,身份丢失问题最大影响是阻碍应用间通信。非简单类型在通信时会出现错误判断、无法判断的问题。
cookie鉴权
wujie 的 js 沙箱是基于 iframe 实现的,即子应用的所有请求都会在 iframe 中被发起,如果此时子应用和主应用域名不同,那么子应用在发起请求时拿不到主应用的 cookie,也就无法做鉴权。目前作者准备往 idass 方向进行探索。
总结
本篇文章主要是探讨了 wujie 在 SDK 交付的场景下的一些实践,相比于传统的 iframe 来说 wujie 不仅拥有更全面的功能,而且更是一种技术趋势,但是 wujie 的沙箱机制也不是十全十美的,也存在一些暂时解决不了的问题,比如身份丢失,同时在处理 cookie 鉴权方面也没有提供好的处理方式,这些都需求开发着自己结合当前场景去探索。
转载自:https://juejin.cn/post/7245558239332335671