From f9dfbe79bac335202a1ea4ad3a513c4f8831e45a Mon Sep 17 00:00:00 2001 From: Michael Lin Date: Wed, 30 Oct 2024 10:11:24 +0800 Subject: [PATCH] feat(worker): support shared worker in MFE (#11) --- examples/basic/app2/src/bootstrap.tsx | 8 ++- examples/basic/app2/src/bootstrapWorker.tsx | 3 + examples/basic/app2/src/worker.ts | 1 + examples/basic/app3/site.config.json | 3 +- examples/basic/app3/src/testValue.ts | 1 + examples/basic/app3/webpack.config.js | 16 ++++- packages/builder/src/make.ts | 73 +++++++++++++++++++-- packages/core/src/importer.ts | 3 +- packages/core/src/index.ts | 2 +- packages/core/src/meta.ts | 8 +++ packages/react/src/index.ts | 1 + packages/shared/src/injectScript.ts | 10 +++ 12 files changed, 117 insertions(+), 12 deletions(-) create mode 100644 examples/basic/app2/src/bootstrapWorker.tsx create mode 100644 examples/basic/app2/src/worker.ts create mode 100644 examples/basic/app3/src/testValue.ts diff --git a/examples/basic/app2/src/bootstrap.tsx b/examples/basic/app2/src/bootstrap.tsx index 8802a6d..867b929 100644 --- a/examples/basic/app2/src/bootstrap.tsx +++ b/examples/basic/app2/src/bootstrap.tsx @@ -1,6 +1,6 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import { expose, getMeta } from '@ringcentral/mfe-react'; +import { expose, getMeta, getWorkerName } from '@ringcentral/mfe-react'; import { getGlobalTransport, PickListeners } from '@ringcentral/mfe-transport'; import { useSentry } from '@ringcentral/mfe-sentry'; import { @@ -8,10 +8,16 @@ import { ConsoleTransport, StorageTransport, } from '@ringcentral/mfe-logger'; +import { testValue } from '@example/app3/src/testValue'; import type App3 from '@example/app3/src/bootstrap'; import App from './App'; +console.log('testValue in main thread', testValue); +const worker = new SharedWorker(new URL('./worker', import.meta.url), { + name: getWorkerName('app2'), +}); + (window as any)._log2 = useLogger({ name: 'app2', transports: [ diff --git a/examples/basic/app2/src/bootstrapWorker.tsx b/examples/basic/app2/src/bootstrapWorker.tsx new file mode 100644 index 0000000..797de92 --- /dev/null +++ b/examples/basic/app2/src/bootstrapWorker.tsx @@ -0,0 +1,3 @@ +import { testValue } from '@example/app3/src/testValue'; + +console.log('testValue in worker thread', testValue); diff --git a/examples/basic/app2/src/worker.ts b/examples/basic/app2/src/worker.ts new file mode 100644 index 0000000..2609298 --- /dev/null +++ b/examples/basic/app2/src/worker.ts @@ -0,0 +1 @@ +import('./bootstrapWorker'); diff --git a/examples/basic/app3/site.config.json b/examples/basic/app3/site.config.json index 72b2bf5..738363d 100644 --- a/examples/basic/app3/site.config.json +++ b/examples/basic/app3/site.config.json @@ -2,7 +2,8 @@ "$schema": "../../../node_modules/@ringcentral/mfe-shared/site-schema.json", "name": "@example/app3", "exposes": { - "./src/bootstrap": "./src/bootstrap" + "./src/bootstrap": "./src/bootstrap", + "./src/testValue": "./src/testValue" }, "shared": { "react": { diff --git a/examples/basic/app3/src/testValue.ts b/examples/basic/app3/src/testValue.ts new file mode 100644 index 0000000..1ecf5c0 --- /dev/null +++ b/examples/basic/app3/src/testValue.ts @@ -0,0 +1 @@ +export const testValue = 'module federation test value'; diff --git a/examples/basic/app3/webpack.config.js b/examples/basic/app3/webpack.config.js index 7d1bbbb..441eabb 100644 --- a/examples/basic/app3/webpack.config.js +++ b/examples/basic/app3/webpack.config.js @@ -10,7 +10,7 @@ const { GenerateManifestWebpackPlugin, } = require('@ringcentral/mfe-service-worker/dist/webpack-plugin/generate-manifest-webpack-plugin'); -module.exports = { +const config = { entry: './src/index', mode: 'development', devServer: { @@ -26,7 +26,7 @@ module.exports = { }, }, output: { - publicPath: 'auto', + publicPath: 'http://localhost:3003/', }, resolve: { extensions: ['.ts', '.tsx', '.js', '.jsx'], @@ -64,3 +64,15 @@ module.exports = { }), ], }; + +module.exports = [ + config, + { + ...config, + target: 'webworker', + output: { + path: path.resolve(__dirname, 'dist/worker'), + publicPath: 'http://localhost:3003/', + }, + }, +]; diff --git a/packages/builder/src/make.ts b/packages/builder/src/make.ts index 7dcbd97..5befad8 100644 --- a/packages/builder/src/make.ts +++ b/packages/builder/src/make.ts @@ -27,16 +27,36 @@ export const makeRemoteScript = ( new Promise((resolve) => { const _global = getGlobal(); if ((_global as any)[name]) return resolve((_global as any)[name]); + if (_global.SharedWorkerGlobalScope) { + // decode rule mapping with `core/src/meta.ts` getWorkerName function + let remoteEntry = ''; + try { + remoteEntry = JSON.parse(atob(_global.name.split('#')[1]))[name].entry; + } catch (e) { + console.error( + `[MFE] Failed to parse remote entry for ${name}, please check that the worker name must be wrapped using 'getWorkerName'.` + ); + throw e; + } + const url = new URL(remoteEntry); + const pathname = url.pathname.split('/'); + pathname.splice(-1, 0, 'worker'); + // replace the default remote with the worker remote + // e.g. http://localhost:3000/remoteEntry.js -> http://localhost:3000/worker/remoteEntry.js + const workerRemote = `${url.origin}${pathname.join('/')}`; + importScripts(workerRemote); + } const toBeResolved = () => { - resolve( + const container = + (_global as any)[name] ?? _global[identifierContainer].dynamicImport({ dependency: name, defaultRemote, name: packageName, version: version || '', dependencyVersion, - }) - ); + }); + resolve(container); }; if ( !_global[identifierContainer] || @@ -75,7 +95,48 @@ export const makeBannerScript = ( retryDelay: number; } ) => { + class MemoryStorage implements Storage { + private _data: Map = new Map(); + + getItem(key: string) { + return this._data.get(key); + } + + setItem(key: string, value: any) { + this._data.set(key, value); + } + + removeItem(key: string) { + this._data.delete(key); + } + + get length() { + return this._data.size; + } + + key(idx: number) { + return [...this._data.keys()][idx]; + } + + keys() { + return [...this._data.keys()]; + } + + ready() { + return true; + } + + clear() { + this._data.clear(); + } + } + const _global = getGlobal(); + if (_global.WorkerGlobalScope) { + // use window instead of self in worker + (_global as any).window = _global; + } + _global.localStorage = _global.localStorage || new MemoryStorage(); _global[identifierContainer] = _global[identifierContainer] ?? {}; _global[identifierContainer].main = _global[identifierContainer].main ?? mfeConfig.name; @@ -85,7 +146,7 @@ export const makeBannerScript = ( _global[identifierContainer].defaultMode ?? mfeConfig.defaultMode ?? '*'; const { main, defaultMode } = _global[identifierContainer]; const storageKey = [identifier, main].join(':'); - const mode = localStorage.getItem(storageKey) ?? defaultMode; + const mode = _global.localStorage.getItem(storageKey) ?? defaultMode; const _prefix = typeof mfeConfig.prefix === 'object' ? mfeConfig.prefix[mode] @@ -104,7 +165,7 @@ export const makeBannerScript = ( if (_registry) Object.assign(mfeConfig, { registry: _registry }); _global[identifierContainer].registry = _global[identifierContainer].registry ?? - localStorage.getItem(`${storageKey}:registry`) ?? + _global.localStorage.getItem(`${storageKey}:registry`) ?? mfeConfig.registry ?? '*'; _global[identifierContainer].registryType = @@ -133,7 +194,7 @@ export const makeBannerScript = ( _global[identifierContainer].styles || {}; _global[identifierContainer].loads = _global[identifierContainer].loads || {}; _global[identifierContainer].storage = - _global[identifierContainer].storage ?? localStorage; + _global[identifierContainer].storage ?? _global.localStorage; _global[identifierContainer]._toBeResolvedUpdateStorage = _global[identifierContainer]._toBeResolvedUpdateStorage === undefined ? new Set() diff --git a/packages/core/src/importer.ts b/packages/core/src/importer.ts index aca3f21..3494e33 100644 --- a/packages/core/src/importer.ts +++ b/packages/core/src/importer.ts @@ -8,7 +8,8 @@ import { getEntry } from './getEntry'; import { ExposeOptions } from './interface'; // container for module federation dynamic import -const getContainer = (name: string) => (window as Record)[name]; +const getContainer = (name: string) => + (globalThis as Record)[name]; export const getModule = async ({ name, diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 582bb4a..81e462a 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -8,7 +8,7 @@ export { export { satisfiesVersion } from '@ringcentral/mfe-shared'; export { identifierAttribute, customElementName } from './constants'; -export { getMeta } from './meta'; +export { getMeta, getWorkerName } from './meta'; export { loadApp } from './loadApp'; export { uuid, diff --git a/packages/core/src/meta.ts b/packages/core/src/meta.ts index a30f5e2..7e283b9 100644 --- a/packages/core/src/meta.ts +++ b/packages/core/src/meta.ts @@ -34,3 +34,11 @@ export const getMeta = (name?: string) => { rendered: Object.keys(renderContainers), }; }; + +export const getWorkerName = (name: string) => { + const metaData = getMeta(); + const dependencies = metaData + ? metaData.data.modules[metaData.data.main]?.dependencies ?? {} + : {}; + return `${name}#${btoa(JSON.stringify(dependencies))}`; +}; diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index bf5fdde..d69b825 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -2,6 +2,7 @@ export { expose, dynamicLoad, getMeta, + getWorkerName, GlobalTransport, globalTransport, getGlobalTransport, diff --git a/packages/shared/src/injectScript.ts b/packages/shared/src/injectScript.ts index e0574cf..63e9382 100644 --- a/packages/shared/src/injectScript.ts +++ b/packages/shared/src/injectScript.ts @@ -16,6 +16,16 @@ export const injectScript = ({ }) => Promise; }): Promise => { return new Promise((resolve, reject) => { + if (globalThis.SharedWorkerGlobalScope) { + try { + importScripts(url); + resolve(); + } catch (e) { + reject(e); + console.error(`[MFE] Script Error: ${url}`); + } + return; + } const element = document.createElement('script'); element.src = url; element.async = true;