From 2e3a5f8432988ceb1c70d4ca8deab99ec0e6f9d7 Mon Sep 17 00:00:00 2001 From: Nicklas Utgaard Date: Thu, 21 Jan 2021 17:24:47 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20endre=20async-api=20til?= =?UTF-8?q?=20=C3=A5=20bruke=20react's=20suspend/lazy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit endret api for bruk av async-biten av navspa. Bruker nå React.suspend og React.lazy by default BREAKING CHANGE: 🧨 Importering av async-biten av navspa er lagt til AsyncNavspa named-export --- README.md | 22 ++++--- example/index.tsx | 18 +++--- package.json | 4 +- src/async-navspa.tsx | 123 ------------------------------------- src/async/async-navspa.tsx | 63 +++++++++++++++++++ src/{ => async}/utils.ts | 2 +- src/index.ts | 18 +++--- src/navspa.tsx | 16 ++--- test/utils.test.ts | 4 +- 9 files changed, 105 insertions(+), 165 deletions(-) delete mode 100644 src/async-navspa.tsx create mode 100644 src/async/async-navspa.tsx rename src/{ => async}/utils.ts (99%) diff --git a/README.md b/README.md index 9409142..4262976 100644 --- a/README.md +++ b/README.md @@ -40,19 +40,19 @@ function Wrapper() { } ``` -Det er også mulig å importere inn child applikasjoner asynkront ved bruk av `importerAsync`. +Det er også mulig å importere inn child applikasjoner asynkront ved bruk av `AsyncNavspa.importer`. Hvis en applikasjon importeres inn async så trengs det ikke å laste inn css/js gjennom tags i html-filen til parent-appen. Istedenfor så vil async-navspa lese asset-manifest.json og finne ut hvilken filer den trenger å hente derfra. Som default så må manifestet være på samme format som det som blir opprettet av CRA, men det er mulig å overskrive parsingen av manifestet ved behov. ```typescript jsx -import NAVSPA from '@navikt/navspa'; +import { AsyncNavspa } from '@navikt/navspa'; -const AsyncChild1 = NAVSPA.importerAsync({ +const AsyncChild1 = AsyncNavspa.importer({ appName: 'child-1', appBaseUrl: 'https://url-to-microfrontend1.com/' }); -const AsyncChild2 = NAVSPA.importerAsync({ +const AsyncChild2 = AsyncNavspa.importer({ appName: 'child-2', appBaseUrl: 'https://url-to-microfrontend2.com/', assetManifestParser: (manifest: { [k: string]: any }) => {/*...*/}, @@ -70,22 +70,24 @@ function Wrapper() { } ``` -Når man bruker `importerAsync` så starter innhentingen av ressursene når komponenten først mountes. +Når man bruker `AsyncNavspa.importer` så starter innhentingen av ressursene når komponenten først mountes. Dette vil som regel kun medføre 1 kall for å hente asset-manifestet og 1 pr ressurs (er som regel cachet ganske bra). -For å gjøre innlasting raskere så er det mulig å bruke `preloadAsync`. Da vil ressursene bli lastet inn asynkront slik at child appen kan rendres raskere. +For å gjøre innlasting raskere så er det mulig å bruke `AsyncNavspa.preload`. +Da vil ressursene bli lastet inn asynkront slik at child appen kan rendres raskere. ```typescript jsx -import NAVSPA from '@navikt/navspa'; +import { AsyncNavspa } from '@navikt/navspa'; const config: AsyncSpaConfig = { appName: 'child-1', - appBaseUrl: 'https://url-to-microfrontend1.com/' + appBaseUrl: 'https://url-to-microfrontend1.com/', + loader:
loading
} // Do the preloading somewhere before child-1 needs to be rendered -NAVSPA.preloadAsync(config); +AsyncNavspa.preload(config); -const AsyncChild1 = NAVSPA.importerAsync(config); +const AsyncChild1 = AsyncNavspa.importer(config); function Wrapper() { return ( diff --git a/example/index.tsx b/example/index.tsx index 5b7db43..4e8616d 100644 --- a/example/index.tsx +++ b/example/index.tsx @@ -1,31 +1,29 @@ -import * as React from 'react'; +import React, { useEffect } from 'react'; import { render } from 'react-dom'; -import { importer } from '../src/navspa'; -import { importerAsync, preloadAsync } from '../src/async-navspa'; -import { useEffect } from 'react'; +import { Navspa, AsyncNavspa, AsyncSpaConfig } from '../src'; interface DecoratorProps { appname: string; } -const Dekorator = importer('internarbeidsflatefs'); -const OldApp = importer('oldapp'); -const NewApp = importer('newapp'); +const Dekorator = Navspa.importer('internarbeidsflatefs'); +const OldApp = Navspa.importer('oldapp'); +const NewApp = Navspa.importer('newapp'); -const asyncConfig = { +const asyncConfig: AsyncSpaConfig = { appName: 'cra-test', appBaseUrl: 'http://localhost:5000', loader: (
Laster...
) }; -const AsyncApp = importerAsync(asyncConfig); +const AsyncApp = AsyncNavspa.importer(asyncConfig); function App() { const [mount, setMount] = React.useState(true); const [mountAsync, setMountAsync] = React.useState(false); useEffect(() => { - preloadAsync(asyncConfig); + AsyncNavspa.preload(asyncConfig); setTimeout(() => { setMountAsync(true); diff --git a/package.json b/package.json index fcfa6cc..980fe8f 100644 --- a/package.json +++ b/package.json @@ -20,8 +20,8 @@ "author": "NAV", "license": "MIT", "peerDependencies": { - "react": "*", - "react-dom": "*" + "react": ">=16.6.0", + "react-dom": ">=16.6.0" }, "devDependencies": { "@types/jest": "^26.0.5", diff --git a/src/async-navspa.tsx b/src/async-navspa.tsx deleted file mode 100644 index 27e6603..0000000 --- a/src/async-navspa.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import loadjs from 'loadjs'; -import { importer } from './navspa'; -import { createAssetManifestParser, joinPaths } from './utils'; - -enum AssetLoadState { - LOADING_ASSETS, - ASSETS_LOADED, - FAILED_TO_LOAD_ASSETS -} - -interface LoadState

{ - navSpa?: React.ComponentType

; - state: AssetLoadState; -} - -interface Props

{ - appName: string; - appBaseUrl: string; - spaProps: P; - assetManifestParser: AssetManifestParser; - wrapperClassName?: string; - loader?: React.ReactNode; -} - -interface AsyncSpaConfig { - appName: string; - appBaseUrl: string; - assetManifestParser?: AssetManifestParser; - wrapperClassName?: string; - loader?: React.ReactNode; -} - -interface PreloadConfig { - appName: string; - appBaseUrl: string; - assetManifestParser?: AssetManifestParser; -} - -export type ManifestObject = { [k: string]: any }; - -// Takes a parsed asset manifest and returns a list of all URLs that must be loaded -export type AssetManifestParser = (manifestObject: ManifestObject) => string[]; - -const ASSET_MANIFEST_NAME = 'asset-manifest.json'; - - -export function importerAsync

(config: AsyncSpaConfig): React.FunctionComponent

{ - return (props: P) => { - return ( - - ); - }; -} - -export function preloadAsync(config: PreloadConfig): void { - const loadJsBundleId = createLoadJsBundleId(config.appName); - const assetManifestParser = config.assetManifestParser || createAssetManifestParser(config.appBaseUrl); - - fetchAssetUrls(config.appBaseUrl, assetManifestParser) - .then(urls => loadjs(urls, loadJsBundleId)) - .catch((err) => console.warn('Failed to async preload ' + config.appName, err)); -} - -function fetchAssetUrls(appBaseUrl: string, assetManifestParser: AssetManifestParser): Promise { - return fetch(joinPaths(appBaseUrl, ASSET_MANIFEST_NAME)) - .then(res => res.json()) - .then(manifest => assetManifestParser(manifest)); -} - -function createLoadJsBundleId(appName: string): string { - return `async_navspa_${appName}`; -} - -function AsyncNavSpa

( - {appName, appBaseUrl, spaProps, wrapperClassName, assetManifestParser, loader }: Props

-): JSX.Element { - const loadJsBundleId = createLoadJsBundleId(appName); - const [loadState, setLoadState] = useState>({state: AssetLoadState.LOADING_ASSETS}); - - function setAssetsLoaded() { - setLoadState({state: AssetLoadState.ASSETS_LOADED, navSpa: importer

(appName, wrapperClassName)}); - } - - useEffect(() => { - if (loadjs.isDefined(loadJsBundleId)) { - setAssetsLoaded(); - } else { - fetchAssetUrls(appBaseUrl, assetManifestParser) - .then(urls => { - // Since preload might be used, we need to check again if the assets were already loaded asynchronously - if (loadjs.isDefined(loadJsBundleId)) { - setAssetsLoaded(); - } else { - loadjs(urls, loadJsBundleId, { - success: setAssetsLoaded, - error: () => setLoadState({state: AssetLoadState.FAILED_TO_LOAD_ASSETS}) - }); - } - }) - .catch(() => setLoadState({state: AssetLoadState.FAILED_TO_LOAD_ASSETS})) - } - }, []); - - if (loadState.state === AssetLoadState.LOADING_ASSETS) { - return <>{loader}; - } else if (loadState.state === AssetLoadState.FAILED_TO_LOAD_ASSETS || !loadState.navSpa) { - return ( -

- Klarte ikke å laste inn {appName} -
- ); - } - - return ; -} diff --git a/src/async/async-navspa.tsx b/src/async/async-navspa.tsx new file mode 100644 index 0000000..ad3503b --- /dev/null +++ b/src/async/async-navspa.tsx @@ -0,0 +1,63 @@ +import React, {ReactNode} from "react"; +import loadjs from 'loadjs'; +import {createAssetManifestParser, joinPaths} from "./utils"; +import {importer as importerSync} from '../navspa' + + +const ASSET_MANIFEST_NAME = 'asset-manifest.json'; +export type ManifestObject = { [k: string]: any }; +export type AssetManifestParser = (manifestObject: ManifestObject) => string[]; + +export interface PreloadConfig { + appName: string; + appBaseUrl: string; + assetManifestParser?: AssetManifestParser; +} + +export interface AsyncSpaConfig extends PreloadConfig { + wrapperClassName?: string; + loader?: NonNullable; +} + +function createLoadJsBundleId(appName: string): string { + return `async_navspa_${appName}`; +} + +function fetchAssetUrls(appBaseUrl: string, assetManifestParser: AssetManifestParser): Promise { + return fetch(joinPaths(appBaseUrl, ASSET_MANIFEST_NAME)) + .then(res => res.json()) + .then(manifest => assetManifestParser(manifest)); +} + +async function loadAssets(config: PreloadConfig): Promise { + const loadJsBundleId = createLoadJsBundleId(config.appName); + const assetManifestParser = config.assetManifestParser || createAssetManifestParser(config.appBaseUrl); + + if (!loadjs.isDefined(loadJsBundleId)) { + const assets: string[] = await fetchAssetUrls(config.appBaseUrl, assetManifestParser) + if (!loadjs.isDefined(loadJsBundleId)) { + await loadjs(assets, loadJsBundleId, {returnPromise: true}) + } + } +} + +export function preload(config: PreloadConfig) { + loadAssets(config) + .catch(console.error); +} + +export function importerLazy

(config: AsyncSpaConfig): Promise<{ default: React.ComponentType

}> { + return loadAssets(config) + .catch(console.error) + .then(() => ({ default: importerSync

(config.appName, config.wrapperClassName) })); +} + +export function importer

(config: AsyncSpaConfig): React.ComponentType

{ + const LazyComponent = React.lazy(() => importerLazy(config)); + const loader = config.loader || <>; + return (props: P) => ( + + + + ); +} diff --git a/src/utils.ts b/src/async/utils.ts similarity index 99% rename from src/utils.ts rename to src/async/utils.ts index c96e0fd..c558a5c 100644 --- a/src/utils.ts +++ b/src/async/utils.ts @@ -61,4 +61,4 @@ export function joinPaths(...paths: string[]): string { function makeAbsolute(baseUrl: string, maybeAbsolutePath: string): string { const isAbsoluteUrl = maybeAbsolutePath.startsWith('http'); return isAbsoluteUrl ? maybeAbsolutePath : joinPaths(baseUrl, maybeAbsolutePath); -} \ No newline at end of file +} diff --git a/src/index.ts b/src/index.ts index 3e6391b..1d24180 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,12 +1,16 @@ import { importer, eksporter } from './navspa'; -import { importerAsync, preloadAsync } from './async-navspa'; +import { importer as importerAsync, importerLazy, preload } from './async/async-navspa'; +export { AsyncSpaConfig } from './async/async-navspa'; -const NAVSPA = { - importer, - importerAsync, - preloadAsync, - eksporter, +export const AsyncNavspa = { + importer: importerAsync, + importerLazy, + preload }; -export default NAVSPA; +export const Navspa = { + importer, + eksporter +}; +export default Navspa; diff --git a/src/navspa.tsx b/src/navspa.tsx index afc52a7..af40868 100644 --- a/src/navspa.tsx +++ b/src/navspa.tsx @@ -16,14 +16,9 @@ type NAVSPAApp = { mount(element: HTMLElement, props: any): void; unmount(element: HTMLElement): void; } -type Frontendlogger = { error(e: Error): void; }; const scope: DeprecatedNAVSPAScope = (global as any)['NAVSPA'] = (global as any)['NAVSPA'] || {}; // tslint:disable-line const scopeV2: NAVSPAScope = (global as any)['NAVSPA-V2'] = (global as any)['NAVSPA-V2'] || {}; // tslint:disable-line -const logger: Frontendlogger = (global as any).frontendlogger = (global as any).frontendlogger || { - error() { - } -}; // tslint:disable-line export function eksporter(name: string, component: React.ComponentType) { scope[name] = (element: HTMLElement, props: PROPS) => { @@ -51,11 +46,12 @@ export function importer

(name: string, wrapperClassName?: string): React.Comp }; } - return (props: P) => ; + return (props: P) => ; } interface NavSpaWrapperProps

{ - navSpaApp: NAVSPAApp + name: string; + navSpaApp: NAVSPAApp; navSpaProps: P; wrapperClassName?: string; } @@ -82,13 +78,13 @@ class NavSpa

extends React.Component, NavSpaState> { } } catch (e) { this.setState({hasError: true}); - logger.error(e); + console.error(e); } } public componentDidCatch(error: Error) { this.setState({hasError: true}); - logger.error(error); + console.error(error); } public componentDidMount() { @@ -109,7 +105,7 @@ class NavSpa

extends React.Component, NavSpaState> { public render() { if (this.state.hasError) { - return

Feil i {name}
; + return
Feil i {this.props.name}
; } return
; } diff --git a/test/utils.test.ts b/test/utils.test.ts index d4cf1c5..472399b 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -1,4 +1,4 @@ -import { createAssetManifestParser, joinPaths } from '../src/utils'; +import { createAssetManifestParser, joinPaths } from '../src/async/utils'; describe('joinPaths', () => { it('should join url with path', () => { @@ -48,4 +48,4 @@ describe('extractPathsFromCRAManifest', () => { const manifestParser = createAssetManifestParser('http://localhost:1234'); expect(() => manifestParser({})).toThrow('Invalid manifest: {}'); }) -}); \ No newline at end of file +});