Open sedationh opened 5 months ago
本文来自于团队的分享,对外脱敏版本
下面来一步一步实现这个 SDK 的能力「演示版」
用 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 在页面上已经搞出来了
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 是一样的效果
如题,SDK 的设计不希望影响全局变量也不希望被全局变量环境干扰
这里技术实现前置依赖几个信息输入
(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 语句
with - JavaScript | MDN
window.a = 1; const foo = { a: "foo", }; with (foo) { console.log(a); // foo } console.log(a) // 1
Function - JavaScript | MDN
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 可以正常取到,并且页面渲染依然正常
目前的打包是把 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 呢?
simpleSandbox(scriptCode, proxyWindow);
这里的做法是用文件名
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"]; }
有没有别的解法呢?
刚刚走的算是用 gloablThis 这个判断来做的依赖组织,我们可以写个 AMD 解析来统一 key 的定义,限于篇幅我这里不再展开,实际的实现走的是 AMD 的解析模式
与 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 来解决这个问题
这一步不复杂,就是用 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 的隔离
下面来一步一步实现这个 SDK 的能力「演示版」
1. scirpt 直接挂进来
用 UMD 的方式将组件打包 生成 Comp1.umd.cjs
接下来把远程组件直接通过 script 的方式来引入看看
全局挂载正常,接着尝试进行渲染 Comp1
可以看到 Comp1 在页面上已经搞出来了
2. 上 SDK,动态引入 Comp1.umd.cjs
SDK 的用法大致如下
远程清单就是一个资源配置文件,出于演示目的,这里进行简化,直接固定从 pubilc 拿
注意到这里用 eval 来执行源代码,让 Comp1 挂载到了全局上,从目前的效果上来看和动态挂个 scirpt 是一样的效果
3. 利用 Proxy Window 防止全局变量污染
如题,SDK 的设计不希望影响全局变量也不希望被全局变量环境干扰
这里技术实现前置依赖几个信息输入
UMD (Universal Module Definition),就是一种 JavaScript 通用模块定义规范,让你的模块能在 JavaScript 所有运行环境中发挥作用
下面的 this 被映射为 global 入参,我们的文件被映射为 factory 入参
然后走判定逻辑
exports;nodejs 环境
define amd ;AMD 规范环境
兜底走 globalThis, this, self
with 语句
with - JavaScript | MDN
Function - JavaScript | MDN
前置理解说完了,接下来看核心实现
修改下业务代码,扔进去一些对全局的污染
注意到全局 window 上没有被干扰,并且我们对全局的修改被扔到了 fakeWindow 上,依赖 window 上的一些方法通过 ProxyWindow 可以正常取到,并且页面渲染依然正常
4. 重复公用依赖问题
目前的打包是把 React 、ReactDOM 都扔进业务代码的,还需要业务代码暴露一个 render 的方法进行调用(1)
从我们的预期上,组件是有很多的,并且他们之间不是一个仓库,SDK 在引入的时候也只需要引入需要的文件
这个时候如果有多个业务模块同时引用,公共依赖就是重复的(2)
为了解决这个问题,还需要把 UMD 的依赖能力用起来,要注意 ⚠️,我们在 3 的时候搞了 Proxy Window
先把 业务打包的 external 打开,观察下打包的产物
这里有俩选择,走 AMD,或者走 globalThis 来做依赖管理
先看最终预期
这里的核心问题是,通过
simpleSandbox(scriptCode, proxyWindow);
后模块就被挂到了 proxyWindow 上,但从 proxyWindow 拿的时候用什么 key 呢?这里的做法是用文件名
这个 name 是 在打包的时候构建工具来定的,react-dom -> ReactDOM
会发现人家声明的和我们通过文件名搞出来的不一致,在这简化版本的实现里,我硬编码来解决这个问题
有没有别的解法呢?
这里有俩选择,走 AMD,或者走 globalThis 来做依赖管理
刚刚走的算是用 gloablThis 这个判断来做的依赖组织,我们可以写个 AMD 解析来统一 key 的定义,限于篇幅我这里不再展开,实际的实现走的是 AMD 的解析模式
5. 样式呢
与 JavaScirpt 同理,但样式并不需要运行,这里可以利用 react 来进行组合
样式正常展示了,我这边故意搞了个会发生样式冲突的场景,接下来通过 WebComponent 来解决这个问题
6. WebComponent
这一步不复杂,就是用 WebComponent 包一层
看结果
SDK 动态加载内容,并且做到了接入网站 和 引入代码的 样式 和 JavaScript 的隔离