Factoring the plugin code a bit better.
This commit is contained in:
@@ -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">
|
||||
|
||||
95
src/plugin-test/Plugin.jsx
Normal file
95
src/plugin-test/Plugin.jsx
Normal 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);
|
||||
@@ -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;
|
||||
46
src/plugin-test/PluginTestPage.jsx
Normal file
46
src/plugin-test/PluginTestPage.jsx
Normal 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;
|
||||
11
src/plugin-test/messages.js
Normal file
11
src/plugin-test/messages.js
Normal 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
37
src/plugin-test/utils.js
Normal 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;
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user