-
-
Notifications
You must be signed in to change notification settings - Fork 8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[app] Imporove federated module core #405
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
import React, { memo } from 'react'; | ||
import React, { ComponentProps, ComponentType, ReactElement, memo } from 'react'; | ||
import { ErrorBoundary } from 'react-error-boundary'; | ||
|
||
import { useDynamicScript } from '../../hooks/useDynamicScript'; | ||
|
@@ -13,9 +13,18 @@ import { useDynamicScript } from '../../hooks/useDynamicScript'; | |
// return; | ||
// }; | ||
|
||
const loadComponent = (scope: string, module: string) => { | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
return async (): Promise<any> => { | ||
/** | ||
* Load and initialize a federated module via webpack container. | ||
* @param scope the module scope. In kobs it's usually the plugin name: e.g. 'kiali' | ||
* @param module the name of the module entry point. e.g. './Page' | ||
* @returns the module component or method | ||
*/ | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
const loadComponent = <T extends ComponentType<any>>( | ||
scope: string, | ||
module: string, | ||
): (() => Promise<{ default: T }>) => { | ||
return async (): Promise<{ default: T }> => { | ||
// wait can be used to simulate long loading times | ||
// await wait(); | ||
|
||
|
@@ -31,8 +40,8 @@ const loadComponent = (scope: string, module: string) => { | |
// eslint-disable-next-line @typescript-eslint/ban-ts-comment | ||
// @ts-ignore | ||
const factory = await window[scope].get(module); | ||
const Module = factory(); | ||
return Module; | ||
const ModuleInstance = factory(); | ||
return ModuleInstance; | ||
}; | ||
}; | ||
|
||
|
@@ -41,25 +50,26 @@ declare function errorContentRenderer(props: { | |
children: React.ReactElement; | ||
}): React.ReactElement<unknown, string | React.FunctionComponent | typeof React.Component> | null; | ||
|
||
export interface IModuleProps { | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
export interface IModuleProps<P = any> { | ||
version: string; | ||
name: string; | ||
module: string; | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
props: any; | ||
props: P; | ||
loadingContent: React.FunctionComponent; | ||
errorContent: typeof errorContentRenderer; | ||
} | ||
|
||
const Module: React.FunctionComponent<IModuleProps> = ({ | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
const Module = <M extends ComponentType<any>, P extends ComponentProps<M>>({ | ||
version, | ||
name, | ||
module, | ||
props, | ||
errorContent, | ||
loadingContent, | ||
}: IModuleProps) => { | ||
const { ready, failed } = useDynamicScript(name, version); | ||
}: IModuleProps<P>): ReactElement => { | ||
const { isReady, isFailed } = useDynamicScript(name, version); | ||
|
||
const ErrorContent = errorContent; | ||
const LoadingContent = loadingContent; | ||
|
@@ -74,11 +84,7 @@ const Module: React.FunctionComponent<IModuleProps> = ({ | |
); | ||
} | ||
|
||
if (!ready) { | ||
return <LoadingContent />; | ||
} | ||
|
||
if (failed) { | ||
if (isFailed) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've re-ordered the cases so that the error case always apprears first. |
||
return ( | ||
<ErrorContent title="Failed to load module"> | ||
<p> | ||
|
@@ -88,7 +94,11 @@ const Module: React.FunctionComponent<IModuleProps> = ({ | |
); | ||
} | ||
|
||
const Component = React.lazy(loadComponent(name, module)); | ||
if (!isReady) { | ||
return <LoadingContent />; | ||
} | ||
|
||
const Component = React.lazy(loadComponent<M>(name, module)); | ||
|
||
return ( | ||
<ErrorBoundary | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,21 +1,49 @@ | ||
import { useEffect, useState } from 'react'; | ||
|
||
const loadedScripts: { [name: string]: Promise<void> } = {}; | ||
|
||
export const useDynamicScript = ( | ||
name: string, | ||
version: string, | ||
): { | ||
failed: boolean; | ||
ready: boolean; | ||
} => { | ||
const loadedScripts: Record<string, Promise<void>> = {}; | ||
|
||
export type ScriptStatus = { isFailed: boolean; isReady: boolean }; | ||
|
||
/** | ||
* Load a kobs specific federated module from a remote server. | ||
* @param name the plugin name aka. federated module name | ||
* @param version version of the module. Used as cache buster for CDNs. | ||
* Note: the version can't be changed during runtime. | ||
* @returns the status of the script/module | ||
*/ | ||
export const useDynamicScript = (name: string, version: string): ScriptStatus => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've separated the Kobs business logic and the script loading into two functions. Should make it easier to re-use it later |
||
const url = | ||
process.env.NODE_ENV === 'production' | ||
? `/plugins/${name}/remoteEntry.js?version=${version}` | ||
: `http://localhost:3001/remoteEntry.js?version=${version}`; | ||
|
||
const [ready, setReady] = useState(false); | ||
const [failed, setFailed] = useState(false); | ||
return useScript(name, url); | ||
}; | ||
|
||
/** | ||
* Load any javascript file from a remote server and execute it in the DOM. | ||
* @param name a unique name of the script so that it's only loaded once | ||
* @param url the URL where the script is located | ||
* @returns the status of the script | ||
*/ | ||
export const useScript = (name: string, url: string): ScriptStatus => { | ||
const [isReady, setReady] = useState(false); | ||
const [isFailed, setFailed] = useState(false); | ||
|
||
const setLoading = (): void => { | ||
setReady(false); | ||
setFailed(false); | ||
}; | ||
|
||
const setSuccess = (): void => { | ||
setReady(true); | ||
setFailed(false); | ||
}; | ||
|
||
const setError = (): void => { | ||
setReady(false); | ||
setFailed(true); | ||
}; | ||
|
||
useEffect(() => { | ||
if (!name) { | ||
|
@@ -25,12 +53,10 @@ export const useDynamicScript = ( | |
if (name in loadedScripts) { | ||
loadedScripts[name] | ||
.then(() => { | ||
setReady(true); | ||
setFailed(false); | ||
setSuccess(); | ||
}) | ||
.catch(() => { | ||
setReady(false); | ||
setFailed(true); | ||
setError(); | ||
}); | ||
return; | ||
} | ||
|
@@ -42,17 +68,15 @@ export const useDynamicScript = ( | |
element.type = 'text/javascript'; | ||
element.async = true; | ||
|
||
setReady(false); | ||
setFailed(false); | ||
setLoading(); | ||
|
||
element.onload = (): void => { | ||
setReady(true); | ||
setSuccess(); | ||
resolve(); | ||
}; | ||
|
||
element.onerror = (): void => { | ||
setReady(false); | ||
setFailed(true); | ||
setError(); | ||
reject(); | ||
}; | ||
|
||
|
@@ -65,7 +89,7 @@ export const useDynamicScript = ( | |
}, [name, url]); | ||
|
||
return { | ||
failed, | ||
ready, | ||
isFailed, | ||
isReady, | ||
}; | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added some documentation.
In a later PR, I'll try to type this with generics so that we can get rid ofany
.