Skip to content

Commit

Permalink
Merge pull request #25 from navikt/async-api
Browse files Browse the repository at this point in the history
feat: 🎸 endre async-api til å bruke react's suspend/lazy
  • Loading branch information
nutgaard authored Jan 21, 2021
2 parents 08b7637 + 2e3a5f8 commit 5f80233
Show file tree
Hide file tree
Showing 9 changed files with 105 additions and 165 deletions.
22 changes: 12 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<ChildProps>({
const AsyncChild1 = AsyncNavspa.importer<ChildProps>({
appName: 'child-1',
appBaseUrl: 'https://url-to-microfrontend1.com/'
});

const AsyncChild2 = NAVSPA.importerAsync<ChildProps>({
const AsyncChild2 = AsyncNavspa.importer<ChildProps>({
appName: 'child-2',
appBaseUrl: 'https://url-to-microfrontend2.com/',
assetManifestParser: (manifest: { [k: string]: any }) => {/*...*/},
Expand All @@ -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: <div>loading</div>
}

// Do the preloading somewhere before child-1 needs to be rendered
NAVSPA.preloadAsync(config);
AsyncNavspa.preload(config);

const AsyncChild1 = NAVSPA.importerAsync<ChildProps>(config);
const AsyncChild1 = AsyncNavspa.importer<ChildProps>(config);

function Wrapper() {
return (
Expand Down
18 changes: 8 additions & 10 deletions example/index.tsx
Original file line number Diff line number Diff line change
@@ -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<DecoratorProps>('internarbeidsflatefs');
const OldApp = importer<DecoratorProps>('oldapp');
const NewApp = importer<DecoratorProps>('newapp');
const Dekorator = Navspa.importer<DecoratorProps>('internarbeidsflatefs');
const OldApp = Navspa.importer<DecoratorProps>('oldapp');
const NewApp = Navspa.importer<DecoratorProps>('newapp');

const asyncConfig = {
const asyncConfig: AsyncSpaConfig = {
appName: 'cra-test',
appBaseUrl: 'http://localhost:5000',
loader: (<div>Laster...</div>)
};

const AsyncApp = importerAsync(asyncConfig);
const AsyncApp = AsyncNavspa.importer(asyncConfig);

function App() {
const [mount, setMount] = React.useState<boolean>(true);
const [mountAsync, setMountAsync] = React.useState<boolean>(false);

useEffect(() => {
preloadAsync(asyncConfig);
AsyncNavspa.preload(asyncConfig);

setTimeout(() => {
setMountAsync(true);
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
123 changes: 0 additions & 123 deletions src/async-navspa.tsx

This file was deleted.

63 changes: 63 additions & 0 deletions src/async/async-navspa.tsx
Original file line number Diff line number Diff line change
@@ -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<ReactNode>;
}

function createLoadJsBundleId(appName: string): string {
return `async_navspa_${appName}`;
}

function fetchAssetUrls(appBaseUrl: string, assetManifestParser: AssetManifestParser): Promise<string[]> {
return fetch(joinPaths(appBaseUrl, ASSET_MANIFEST_NAME))
.then(res => res.json())
.then(manifest => assetManifestParser(manifest));
}

async function loadAssets(config: PreloadConfig): Promise<void> {
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<P>(config: AsyncSpaConfig): Promise<{ default: React.ComponentType<P> }> {
return loadAssets(config)
.catch(console.error)
.then(() => ({ default: importerSync<P>(config.appName, config.wrapperClassName) }));
}

export function importer<P>(config: AsyncSpaConfig): React.ComponentType<P> {
const LazyComponent = React.lazy(() => importerLazy(config));
const loader = config.loader || <></>;
return (props: P) => (
<React.Suspense fallback={loader}>
<LazyComponent {...props} />
</React.Suspense>
);
}
2 changes: 1 addition & 1 deletion src/utils.ts → src/async/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
18 changes: 11 additions & 7 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -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;
16 changes: 6 additions & 10 deletions src/navspa.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<PROPS>(name: string, component: React.ComponentType<PROPS>) {
scope[name] = (element: HTMLElement, props: PROPS) => {
Expand Down Expand Up @@ -51,11 +46,12 @@ export function importer<P>(name: string, wrapperClassName?: string): React.Comp
};
}

return (props: P) => <NavSpa navSpaApp={app} navSpaProps={props} wrapperClassName={wrapperClassName}/>;
return (props: P) => <NavSpa name={name} navSpaApp={app} navSpaProps={props} wrapperClassName={wrapperClassName}/>;
}

interface NavSpaWrapperProps<P> {
navSpaApp: NAVSPAApp
name: string;
navSpaApp: NAVSPAApp;
navSpaProps: P;
wrapperClassName?: string;
}
Expand All @@ -82,13 +78,13 @@ class NavSpa<P> extends React.Component<NavSpaWrapperProps<P>, 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() {
Expand All @@ -109,7 +105,7 @@ class NavSpa<P> extends React.Component<NavSpaWrapperProps<P>, NavSpaState> {

public render() {
if (this.state.hasError) {
return <div className="navspa--applikasjonsfeil">Feil i {name}</div>;
return <div className="navspa--applikasjonsfeil">Feil i {this.props.name}</div>;
}
return <div className={this.props.wrapperClassName} ref={this.saveRef}/>;
}
Expand Down
4 changes: 2 additions & 2 deletions test/utils.test.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -48,4 +48,4 @@ describe('extractPathsFromCRAManifest', () => {
const manifestParser = createAssetManifestParser('http://localhost:1234');
expect(() => manifestParser({})).toThrow('Invalid manifest: {}');
})
});
});

0 comments on commit 5f80233

Please sign in to comment.