From d61fe28e4e9755992ccea6843066f0386e8e8da8 Mon Sep 17 00:00:00 2001 From: David Joy Date: Wed, 10 Feb 2021 13:29:51 -0500 Subject: [PATCH] Factoring the plugin code a bit better. --- src/index.jsx | 4 +- src/plugin-test/Plugin.jsx | 95 +++++++++++++++++ src/plugin-test/PluginTest.jsx | 157 ----------------------------- src/plugin-test/PluginTestPage.jsx | 46 +++++++++ src/plugin-test/messages.js | 11 ++ src/plugin-test/utils.js | 37 +++++++ 6 files changed, 191 insertions(+), 159 deletions(-) create mode 100644 src/plugin-test/Plugin.jsx delete mode 100644 src/plugin-test/PluginTest.jsx create mode 100644 src/plugin-test/PluginTestPage.jsx create mode 100644 src/plugin-test/messages.js create mode 100644 src/plugin-test/utils.js diff --git a/src/index.jsx b/src/index.jsx index 9273e3a5..9dc01be5 100755 --- a/src/index.jsx +++ b/src/index.jsx @@ -27,14 +27,14 @@ import { TabContainer } from './tab-page'; import { fetchDatesTab, fetchOutlineTab, fetchProgressTab } from './course-home/data'; import { fetchCourse } from './courseware/data'; import initializeStore from './store'; -import PluginTest from './plugin-test/PluginTest'; +import PluginTestPage from './plugin-test/PluginTestPage'; subscribe(APP_READY, () => { ReactDOM.render( - + diff --git a/src/plugin-test/Plugin.jsx b/src/plugin-test/Plugin.jsx new file mode 100644 index 00000000..28273b72 --- /dev/null +++ b/src/plugin-test/Plugin.jsx @@ -0,0 +1,95 @@ +import React, { Suspense } from 'react'; +import PropTypes from 'prop-types'; +import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; +import PageLoading from '../generic/PageLoading'; + +import messages from './messages'; +import { loadDynamicScript, loadScriptComponent } from './utils'; + +export const COMPONENT = 'component'; +export const SCRIPT = 'script'; +export const IFRAME = 'iframe'; + +const useDynamicScript = (url) => { + const [ready, setReady] = React.useState(false); + const [failed, setFailed] = React.useState(false); + const [element, setElement] = React.useState(null); + React.useEffect(() => { + if (!url) { + return () => {}; + } + + setReady(false); + setFailed(false); + + loadDynamicScript(url).then((el) => { + setElement(el); + setReady(true); + }).catch(() => { + setReady(false); + setFailed(true); + }); + + return () => { + document.head.removeChild(element); + }; + }, [url]); + + return { + ready, + failed, + }; +}; + +function Plugin({ plugin, intl }) { + const url = plugin ? plugin.url : null; + const { ready, failed } = useDynamicScript(url); + + if (!plugin) { + return null; + } + + if (!ready) { + return ( + + ); + } + + if (failed) { + return null; + } + + const PluginComponent = React.lazy( + loadScriptComponent(plugin.scope, plugin.module), + ); + + return ( + + )} + > + + + ); +} + +Plugin.propTypes = { + plugin: PropTypes.shape({ + scope: PropTypes.string.isRequired, + module: PropTypes.string.isRequired, + url: PropTypes.string.isRequired, + type: PropTypes.oneOf([COMPONENT, SCRIPT, IFRAME]).isRequired, + }), + intl: intlShape.isRequired, +}; + +Plugin.defaultProps = { + plugin: null, +}; + +export default injectIntl(Plugin); diff --git a/src/plugin-test/PluginTest.jsx b/src/plugin-test/PluginTest.jsx deleted file mode 100644 index 923c781f..00000000 --- a/src/plugin-test/PluginTest.jsx +++ /dev/null @@ -1,157 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -import { Button } from '@edx/paragon'; - -function loadPluginComponent(scope, module) { - return async () => { - // Initializes the share scope. This fills it with known provided modules from this build and all remotes - // eslint-disable-next-line no-undef - await __webpack_init_sharing__('default'); - - const container = window[scope]; // or get the container somewhere else - // Initialize the container, it may provide shared modules - // eslint-disable-next-line no-undef - await container.init(__webpack_share_scopes__.default); - const factory = await window[scope].get(module); - const Module = factory(); - return Module; - }; -} - -async function loadDynamicScript(url) { - return new Promise((resolve, reject) => { - const element = document.createElement('script'); - - element.src = url; - element.type = 'text/javascript'; - element.async = true; - - element.onload = () => { - // eslint-disable-next-line no-console - console.log(`Dynamic Script Loaded: ${url}`); - resolve(); - }; - - element.onerror = () => { - // eslint-disable-next-line no-console - console.error(`Dynamic Script Error: ${url}`); - reject(); - }; - - document.head.appendChild(element); - }); -} - -const useDynamicScript = (args) => { - const [ready, setReady] = React.useState(false); - const [failed, setFailed] = React.useState(false); - const [element, setElement] = React.useState(null); - React.useEffect(() => { - if (!args.url) { - return; - } - - setReady(false); - setFailed(false); - - loadDynamicScript(args.url).then((el) => { - setElement(el); - setReady(true); - }).catch(() => { - setReady(false); - setFailed(true); - }); - - // eslint-disable-next-line consistent-return - return () => { - // eslint-disable-next-line no-console - console.log(`Dynamic Script Removed: ${args.url}`); - document.head.removeChild(element); - }; - }, [args.url]); - - return { - ready, - failed, - }; -}; - -function Plugin({ plugin }) { - const { ready, failed } = useDynamicScript({ - url: plugin && plugin.url, - }); - - if (!plugin) { - return

No plugin specified

; - } - - if (!ready) { - return

Loading plugin script: {plugin.url}

; - } - - if (failed) { - return

Failed to load plugin script: {plugin.url}

; - } - - const PluginComponent = React.lazy( - loadPluginComponent(plugin.scope, plugin.module), - ); - - return ( - - - - ); -} - -Plugin.propTypes = { - plugin: PropTypes.shape({ - scope: PropTypes.string.isRequired, - module: PropTypes.string.isRequired, - url: PropTypes.string.isRequired, - }), -}; - -Plugin.defaultProps = { - plugin: null, -}; - -function PluginTest() { - const [plugin, setPlugin] = React.useState(undefined); - - function setPluginOne() { - setPlugin({ - url: 'http://localhost:7331/remoteEntry.js', - scope: 'plugin', - module: './PluginOne', - }); - } - - function setPluginTwo() { - setPlugin({ - url: 'http://localhost:7331/remoteEntry.js', - scope: 'plugin', - module: './PluginTwo', - }); - } - - return ( -
-

Dynamic Plugin Host

-

App 1

-

- The Dynamic Plugin will take advantage Module Federation{' '} - remotes and exposes. It will no load - components that have been loaded already. -

- - -
- -
-
- ); -} - -export default PluginTest; diff --git a/src/plugin-test/PluginTestPage.jsx b/src/plugin-test/PluginTestPage.jsx new file mode 100644 index 00000000..614afc00 --- /dev/null +++ b/src/plugin-test/PluginTestPage.jsx @@ -0,0 +1,46 @@ +import React from 'react'; + +import { Button } from '@edx/paragon'; + +import Plugin, { SCRIPT, COMPONENT } from './Plugin'; + +function PluginTestPage() { + const [plugin, setPlugin] = React.useState(undefined); + + function setPluginOne() { + setPlugin({ + url: 'http://localhost:7331/remoteEntry.js', + scope: 'plugin', + module: './PluginOne', + type: SCRIPT, + }); + } + + function setPluginTwo() { + setPlugin({ + url: 'http://localhost:7331/remoteEntry.js', + scope: 'plugin', + module: './PluginTwo', + type: COMPONENT, + }); + } + + return ( +
+

Dynamic Plugin Host

+

App 1

+

+ The Dynamic Plugin will take advantage Module Federation{' '} + remotes and exposes. It will no load + components that have been loaded already. +

+ + +
+ +
+
+ ); +} + +export default PluginTestPage; diff --git a/src/plugin-test/messages.js b/src/plugin-test/messages.js new file mode 100644 index 00000000..69fd5419 --- /dev/null +++ b/src/plugin-test/messages.js @@ -0,0 +1,11 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + loading: { + id: 'plugin.loading', + defaultMessage: 'Loading', + description: 'Message shown to screen readers while a plugin is loading.', + }, +}); + +export default messages; diff --git a/src/plugin-test/utils.js b/src/plugin-test/utils.js new file mode 100644 index 00000000..2594e1f8 --- /dev/null +++ b/src/plugin-test/utils.js @@ -0,0 +1,37 @@ +export function loadDynamicScript(url) { + return new Promise((resolve, reject) => { + const element = document.createElement('script'); + + element.src = url; + element.type = 'text/javascript'; + element.async = true; + + element.onload = () => { + // eslint-disable-next-line no-console + console.log(`Dynamic Script Loaded: ${url}`); + resolve(element, url); + }; + + element.onerror = () => { + reject(new Error(`Failed to load dynamic script with URL: ${url}`)); + }; + + document.head.appendChild(element); + }); +} + +export function loadScriptComponent(scope, module) { + return async () => { + // Initializes the share scope. This fills it with known provided modules from this build and all remotes + // eslint-disable-next-line no-undef + await __webpack_init_sharing__('default'); + + const container = window[scope]; // or get the container somewhere else + // Initialize the container, it may provide shared modules + // eslint-disable-next-line no-undef + await container.init(__webpack_share_scopes__.default); + const factory = await window[scope].get(module); + const Module = factory(); + return Module; + }; +}