SDK 远程拉取组件组件进行加载
本文来自于团队的分享,对外脱敏版本
下面来一步一步实现这个 SDK 的能力「演示版」
1. scirpt 直接挂进来
用 UMD 的方式将组件打包 生成 Comp1.umd.cjs

接下来把远程组件直接通过 script 的方式来引入看看


全局挂载正常,接着尝试进行渲染 Comp1
<!-- 引入 Comp1 -->
<script src="https://gw.alipayobjects.com/os/lib/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://gw.alipayobjects.com/os/lib/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<script src="/Comp1.umd.cjs"></script>
<h1>👇是 open 节点</h1>
<div id="open"></div>
<script>
const vDom = React.createElement(Comp1);
ReactDOM.render(vDom, document.getElementById('open'));
</script>
可以看到 Comp1 在页面上已经搞出来了

2. 上 SDK,动态引入 Comp1.umd.cjs

SDK 的用法大致如下
Sdk.init({/* 鉴权 */});
const ms = Sdk.create({ name: "Com1" });
ms.render(document.querySelector("#open"));
远程清单就是一个资源配置文件,出于演示目的,这里进行简化,直接固定从 pubilc 拿
import axios from "axios";
export class Sdk {
static init() {}
static create() {
return new Comp({ js: "/Comp1.umd.cjs" });
}
}
class Comp {
js: string;
constructor({ js }) {
this.js = js;
}
async render(dom) {
const { data: scriptCode } = await axios.get<string>(this.js, {
responseType: "text",
});
eval(scriptCode);
window.Comp1.render(dom);
}
}
import { Sdk } from "./sdk/index.ts";
Sdk.init();
const ms = Sdk.create();
ms.render(document.getElementById("open"));

注意到这里用 eval 来执行源代码,让 Comp1 挂载到了全局上,从目前的效果上来看和动态挂个 scirpt 是一样的效果
3. 利用 Proxy Window 防止全局变量污染
如题,SDK 的设计不希望影响全局变量也不希望被全局变量环境干扰
这里技术实现前置依赖几个信息输入
- UMD 规范是怎么回事
(function (global, factory) {
typeof exports === "object" && typeof module !== "undefined"
? (module.exports = factory())
: typeof define === "function" && define.amd
? define(factory)
: ((global =
typeof globalThis !== "undefined" ? globalThis : global || self),
(global.Comp1 = factory()));
})(this, function () {
"use strict";
function Comp1() {
return /* @__PURE__ */ React.createElement("div", null, "Comp1");
}
return Comp1;
});
UMD (Universal Module Definition),就是一种 JavaScript 通用模块定义规范,让你的模块能在 JavaScript 所有运行环境中发挥作用
下面的 this 被映射为 global 入参,我们的文件被映射为 factory 入参
然后走判定逻辑
-
exports;nodejs 环境
-
define amd ;AMD 规范环境
-
兜底走 globalThis, this, self
-
with 语句
window.a = 1;
const foo = {
a: "foo",
};
with (foo) {
console.log(a); // foo
}
console.log(a) // 1
- Function
const fn = new Function("foo", `console.log(foo)`);
fn(66); // 66
前置理解说完了,接下来看核心实现
function simpleSandbox(code: string, globalThisCtx: any) {
const withedCode = `with(ctx) { eval(${JSON.stringify(code)}) }`;
withedCode;
const fn = new Function("ctx", withedCode);
fn.call(globalThisCtx, globalThisCtx);
}
export const proxyWindow = new Proxy(window, {
// 获取属性
get(target, key) {
return fakeWindow[key] || target[key];
},
// 设置属性
set(_, key, value) {
fakeWindow[key] = value;
return true;
},
});
const fakeWindow = {
window: proxyWindow,
globalThis: proxyWindow,
self: proxyWindow,
};
修改下业务代码,扔进去一些对全局的污染
function Comp1() {
window.a = 1;
console.log(
"%c seda [ a ]-7",
"font-size:13px; background:pink; color:#bf2c9f;",
window.a
);
return <div>Comp1</div>;
}
export default Comp1;
const render = (dom) => [ReactDOM.createRoot(dom!).render(<Comp1 />)];
export { render };

注意到全局 window 上没有被干扰,并且我们对全局的修改被扔到了 fakeWindow 上,依赖 window 上的一些方法通过 ProxyWindow 可以正常取到,并且页面渲染依然正常
4. 重复公用依赖问题
目前的打包是把 React 、ReactDOM 都扔进业务代码的,还需要业务代码暴露一个 render 的方法进行调用(1)
从我们的预期上,组件是有很多的,并且他们之间不是一个仓库,SDK 在引入的时候也只需要引入需要的文件
这个时候如果有多个业务模块同时引用,公共依赖就是重复的(2)
为了解决这个问题,还需要把 UMD 的依赖能力用起来,要注意 ⚠️,我们在 3 的时候搞了 Proxy Window
先把 业务打包的 external 打开,观察下打包的产物
(function (global, factory) {
typeof exports === "object" && typeof module !== "undefined"
? (module.exports = factory(require("react")))
: typeof define === "function" && define.amd
? define(["react"], factory)
: ((global =
typeof globalThis !== "undefined" ? globalThis : global || self),
(global.Comp1 = factory(global.React)));
})(this, function (React) {
"use strict";
function Comp1() {
return /* @__PURE__ */ React.createElement("div", null, "Comp1");
}
return Comp1;
});
这里有俩选择,走 AMD,或者走 globalThis 来做依赖管理
先看最终预期
export const COMPONENT_DEP_URLS = {
REACT_CDN_URL:
"https://gw.alipayobjects.com/os/lib/react/17.0.2/umd/react.production.min.js",
REACT_DOM_CDN_URL:
"https://gw.alipayobjects.com/os/lib/react-dom/17.0.2/umd/react-dom.production.min.js",
};
class Comp {
js: string;
constructor({ js }) {
this.js = js;
}
async render(dom) {
const React = await importer.importScript(COMPONENT_DEP_URLS.REACT_CDN_URL);
const ReactDOM = await importer.importScript(
COMPONENT_DEP_URLS.REACT_DOM_CDN_URL
);
const vDom = await importer.importScript(this.js);
ReactDOM.render(React.createElement(vDom), dom);
}
}
这里的核心问题是,通过 simpleSandbox(scriptCode, proxyWindow); 后模块就被挂到了 proxyWindow 上,但从 proxyWindow 拿的时候用什么 key 呢?
这里的做法是用文件名
class Importer {
async importScript(url) {
const { data: scriptCode } = await axios.get<string>(url, {
responseType: "text",
});
simpleSandbox(scriptCode, proxyWindow);
const defaultModuleName = getDefaultModuleName(url);
if (defaultModuleName === "ReactDom") {
return proxyWindow["ReactDOM"];
}
return proxyWindow[defaultModuleName];
}
}

这个 name 是 在打包的时候构建工具来定的,react-dom -> ReactDOM

会发现人家声明的和我们通过文件名搞出来的不一致,在这简化版本的实现里,我硬编码来解决这个问题
if (defaultModuleName === "ReactDom") {
return proxyWindow["ReactDOM"];
}
有没有别的解法呢?
这里有俩选择,走 AMD,或者走 globalThis 来做依赖管理
刚刚走的算是用 gloablThis 这个判断来做的依赖组织,我们可以写个 AMD 解析来统一 key 的定义,限于篇幅我这里不再展开,实际的实现走的是 AMD 的解析模式
5. 样式呢
与 JavaScirpt 同理,但样式并不需要运行,这里可以利用 react 来进行组合
const js = await importer.importScript(this.js);
const css = await importer.importStyle(this.css);
const cssCom = React.createElement(
"style",
{
key: "cssCom",
},
[css]
);
const vDom = React.createElement(
React.Fragment,
null,
cssCom,
React.createElement(js)
);
ReactDOM.render(vDom, dom);

样式正常展示了,我这边故意搞了个会发生样式冲突的场景,接下来通过 WebComponent 来解决这个问题
6. WebComponent
这一步不复杂,就是用 WebComponent 包一层
function reactToWebComponent(
ReactComponent: React.ComponentType,
React: typeof ReactType,
ReactDOM: typeof ReactDOMType
): CustomElementConstructor {
class WebComponent extends HTMLElement {
reactToWebComponent;
constructor() {
super();
const container = this.attachShadow({ mode: "open" });
ReactDOM.render(React.createElement(ReactComponent), container);
}
}
return WebComponent;
}
const h = () => {
return React.createElement(
React.Fragment,
null,
cssCom,
React.createElement(js)
);
};
const webComponentName = `web-com-${js.name.toLowerCase()}`;
const webComponent = reactToWebComponent(h, React, ReactDOM);
const dom = document.createElement(webComponentName);
customElements.define(webComponentName, webComponent);
container.appendChild(dom);

看结果
SDK 动态加载内容,并且做到了接入网站 和 引入代码的 样式 和 JavaScript 的隔离