Factoring the plugin code a bit better.

This commit is contained in:
David Joy
2021-02-10 13:29:51 -05:00
parent 76a85224b8
commit d61fe28e4e
6 changed files with 191 additions and 159 deletions

View File

@@ -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(
<AppProvider store={initializeStore()}>
<UserMessagesProvider>
<Switch>
<Route exact path="/" component={PluginTest} />
<Route exact path="/" component={PluginTestPage} />
<PageRoute path="/redirect" component={CoursewareRedirectLandingPage} />
<PageRoute path="/course/:courseId/home">
<TabContainer tab="outline" fetch={fetchOutlineTab} slice="courseHome">

View File

@@ -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 (
<PageLoading
srMessage={intl.formatMessage(messages.loading)}
/>
);
}
if (failed) {
return null;
}
const PluginComponent = React.lazy(
loadScriptComponent(plugin.scope, plugin.module),
);
return (
<Suspense
fallback={(
<PageLoading
srMessage={intl.formatMessage(messages.loading)}
/>
)}
>
<PluginComponent />
</Suspense>
);
}
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);

View File

@@ -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 <h2>No plugin specified</h2>;
}
if (!ready) {
return <h2>Loading plugin script: {plugin.url}</h2>;
}
if (failed) {
return <h2>Failed to load plugin script: {plugin.url}</h2>;
}
const PluginComponent = React.lazy(
loadPluginComponent(plugin.scope, plugin.module),
);
return (
<React.Suspense fallback="Loading Plugin">
<PluginComponent />
</React.Suspense>
);
}
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 (
<div>
<h1>Dynamic Plugin Host</h1>
<h2>App 1</h2>
<p>
The Dynamic Plugin will take advantage Module Federation{' '}
<strong>remotes</strong> and <strong>exposes</strong>. It will no load
components that have been loaded already.
</p>
<Button className="mr-3" onClick={setPluginOne}>Load Plugin One</Button>
<Button onClick={setPluginTwo}>Load Plugin Two</Button>
<div style={{ marginTop: '2em' }}>
<Plugin plugin={plugin} />
</div>
</div>
);
}
export default PluginTest;

View File

@@ -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 (
<div>
<h1>Dynamic Plugin Host</h1>
<h2>App 1</h2>
<p>
The Dynamic Plugin will take advantage Module Federation{' '}
<strong>remotes</strong> and <strong>exposes</strong>. It will no load
components that have been loaded already.
</p>
<Button className="mr-3" onClick={setPluginOne}>Load Plugin One</Button>
<Button onClick={setPluginTwo}>Load Plugin Two</Button>
<div style={{ marginTop: '2em' }}>
<Plugin plugin={plugin} />
</div>
</div>
);
}
export default PluginTestPage;

View File

@@ -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;

37
src/plugin-test/utils.js Normal file
View File

@@ -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;
};
}