Skip to content

Commit

Permalink
docs(nf): update tutorial in readme
Browse files Browse the repository at this point in the history
  • Loading branch information
manfredsteyer committed Aug 5, 2023
1 parent 36e4e16 commit 61414f7
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 119 deletions.
207 changes: 99 additions & 108 deletions libs/native-federation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,21 +50,29 @@ We migrated our webpack Module Federation example to Native Federation:

![Example](https://raw.githubusercontent.com/angular-architects/module-federation-plugin/main/libs/native-federation/example.png)

Please find the example [here (branch: nf-solution)](https://github.com/manfredsteyer/module-federation-plugin-example/tree/nf-solution):
Please find the example [here (branch: nf-standalone-solution)](https://github.com/manfredsteyer/module-federation-plugin-example/tree/nf-standalone-solution):

```
git clone https://github.com/manfredsteyer/module-federation-plugin-example.git --branch nf-solution
git clone https://github.com/manfredsteyer/module-federation-plugin-example.git --branch nf-standalone-solution
cd module-federation-plugin-example
npm i
npm run build
npm start
```

Then, open http://localhost:3000 in your browser.
Start the Micro Frontend:

Please note, that the current **experimental** version does **not** support `ng serve`. Hence, you need to build it and serve it from the `dist` folder (this is what npm run build && npm run start in the above shown example do).
```
ng serve mfe1 -o
```

Wait until the Micro Frontend is started.

Open another console and start the shell:

```
ng serve shell -o
```

## About the Mental Model

Expand All @@ -77,41 +85,43 @@ For this, the mental model introduces several concepts:
- **Shared Dependencies:** If a several remotes and the host use the same library, you might not want to download it several times. Instead, you might want to just download it once and share it at runtime. For this use case, the mental model allows for defining such shared dependencies.
- **Version Mismatch:** If two or more applications use a different version of the same shared library, we need to prevent a version mismatch. To deal with it, the mental model defines several strategies, like falling back to another version that fits the application, using a different compatible one (according to semantic versioning) or throwing an error.

## Usage
## Usage/ Tutorial

> You can checkout the [nf-starter branch](https://github.com/manfredsteyer/module-federation-plugin-example/tree/nf-solution) to try out Native Federation.
### Adding Native Federation
You can checkout the [nf-standalone-starter branch](https://github.com/manfredsteyer/module-federation-plugin-example/tree/nf-standalone-starter) to try out Native Federation:

```
npm i @angular-architects/native-federation -D
git clone https://github.com/manfredsteyer/module-federation-plugin-example.git --branch nf-standalone-starter
cd module-federation-plugin-example
npm i
```

Making an application a host:
### Adding Native Federation

```
ng g @angular-architects/native-federation:init --project shell --type host
npm i @angular-architects/native-federation -D
```

A dynamic host is a host reading the configuration data at runtime from a `.json` file:
Making an application a remote (Micro Frontend):

```
ng g @angular-architects/native-federation:init --project shell --type dynamic-host
ng g @angular-architects/native-federation:init --project mfe1 --port 4201 --type remote
```

Making an application a remote:
Making an application a host (shell):

```
ng g @angular-architects/native-federation:init --project mfe1 --type remote
ng g @angular-architects/native-federation:init --project shell --port 4200 --type dynamic-host
```

A dynamic host reads the configuration data at runtime from a `.json` file.

### Configuring the Host

The host configuration looks like what you know from our Module Federation plugin:
The host configuration (`projects/shell/federation.config.js`) looks like what you know from our Module Federation plugin:

```javascript
// projects/shell/federation.config.js

const {
withNativeFederation,
shareAll,
Expand All @@ -125,16 +135,24 @@ module.exports = withNativeFederation({
requiredVersion: 'auto',
}),
},

skip: [
'rxjs/ajax',
'rxjs/fetch',
'rxjs/testing',
'rxjs/webSocket',
// Add further packages you don't need at runtime
],
});
```

> Our `init` schematic shown above generates this file for you.
### Configuring the Remote

Also the remote configuration looks familiar:
Also, the remote configuration (`projects/mfe1/federation.config.js`) looks familiar:

```javascript
// projects/mfe1/federation.config.js

const {
withNativeFederation,
shareAll,
Expand All @@ -144,7 +162,7 @@ module.exports = withNativeFederation({
name: 'mfe1',

exposes: {
'./Module': './projects/mfe1/src/app/flights/flights.module.ts',
'./Component': './projects/mfe1/src/app/app.component.ts',
},

shared: {
Expand All @@ -154,37 +172,24 @@ module.exports = withNativeFederation({
requiredVersion: 'auto',
}),
},
});
```

### Initializing the Host

Call `initFederation` before bootstrapping your `main.ts`:

```typescript
// projects/shell/src/main.ts

import { initFederation } from '@angular-architects/native-federation';

initFederation({
mfe1: 'http://localhost:3001/remoteEntry.json',
})
.catch((err) => console.error(err))
.then((_) => import('./bootstrap'))
.catch((err) => console.error(err));
skip: [
'rxjs/ajax',
'rxjs/fetch',
'rxjs/testing',
'rxjs/webSocket',
// Add further packages you don't need at runtime
],
});
```

> Our `init` schematic shown above generates all of this if you pass `--type host`.
You can directly pass a mapping between remote names and their `remoteEntry.json`. The `remoteEntry.json` contains the necessary metadata. It is generated when compiling the remote.
> Our `init` schematic shown above generates this file for you.
Please note that in Native Federation, the remote entry is just a `.json` file while its a `.js` file in Module Federation.
### Initializing the Host

However, you don't need to hardcode this mapping. Feel free to point to the file name of a federation manifest:
When bootstrapping the host (shell), Native Federation (`projects\shell\src\main.ts`) is initialized:

```typescript
// projects/shell/src/main.ts

import { initFederation } from '@angular-architects/native-federation';

initFederation('/assets/federation.manifest.json')
Expand All @@ -193,23 +198,27 @@ initFederation('/assets/federation.manifest.json')
.catch((err) => console.error(err));
```

This manifest can be exchanged when deploying the solution. Hence, you can adopt the solution to the current environment.
> This file is generated by the schematic described above.
> Our `init` schematic shown above generates this variation if you pass `--type dynamic-host`.
The function points to a federation manifest. This manifest points to the individual Micro Frontends. It can be exchanged when deploying the solution. Hence, you can adopt the solution to the current environment.

Credits: The Nx team originally came up with the idea for the manifest.
**Credits:** The Nx team originally came up with the idea for the manifest.

This is what the (also generated) federation manifest looks like:
This is what the (also generated) federation manifest (`projects\shell\src\assets\federation.manifest.json`) looks like:

```json
{
"mfe1": "http://localhost:3001/remoteEntry.json"
"mfe1": "http://localhost:4201/remoteEntry.json"
}
```

Native Federation generates the `remoteEntry.json`. It contains metadata about the individual remote.

If you follow this tutorial, ensure this entry points to port `4201` (!).

### Initializing the Remote

Also, the remote needs to be initialized. If a remote doesn't load further remotes, you don't need to pass any mappings to `initFederation`:
When bootstrapping your remote (`projects\mfe1\src\main.ts`), Native Federation is initialized too:

```typescript
import { initFederation } from '@angular-architects/native-federation';
Expand All @@ -220,77 +229,59 @@ initFederation()
.catch((err) => console.error(err));
```

### Loading a Remote

Use the helper function `loadRemoteModule` to load a configured remote:
> Our `init` schematic shown above also generates this file.
```typescript
import { loadRemoteModule } from '@angular-architects/native-federation';
[...]

export const APP_ROUTES: Routes = [
[...]

{
path: 'flights',
loadChildren: () => loadRemoteModule({
remoteName: 'mfe1',
exposedModule: './Module'
}).then(m => m.FlightsModule)
},

[...]
}
```
After the initialization, it loads the file `bootstrap.ts` starting your Angular application.

This can be used with and without routing; with `NgModule`s but also with **standalone** building blocks. Just use it instead of dynamic imports.
### Loading a Remote

For the sake of compatibility with our Module Federation API, you can also use the `remoteEntry` to identify the remote in question:
For loading a component (or any other building block) exposed by a remote into the host, use Native Federation's `loadRemoteModule` function together with lazy loading (`projects\shell\src\app\app.routes.ts`):

```typescript
import { Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { NotFoundComponent } from './not-found/not-found.component';

// Add this import:
import { loadRemoteModule } from '@angular-architects/native-federation';
[...]

export const APP_ROUTES: Routes = [
[...]

{
path: 'flights',
loadChildren: () => loadRemoteModule({
// Alternative: You can also use the remoteEntry i/o the remoteName:
remoteEntry: 'http://localhost:3001/remoteEntry.json',
exposedModule: './Module'
}).then(m => m.FlightsModule)
},

[...]
}
```
However, we prefer the first option where just the `remoteName` is passed.
{
path: '',
component: HomeComponent,
pathMatch: 'full',
},

### Polyfill
// Add this route:
{
path: 'flights',
loadComponent: () =>
loadRemoteModule('mfe1', './Component').then((m) => m.AppComponent),
},

This library uses Import Maps. As of today, not all browsers support this emerging browser feature, we need a polyfill. We recommend the polyfill `es-module-shims` which has been developed for production use cases. Our schematics install it via npm and add it to your `polyfills.ts`.
{
path: '**',
component: NotFoundComponent,
},

Also, the schematics add the following to your `index.html`:
// DO NOT insert routes after this one.
// { path:'**', ...} needs to be the LAST one.
];
```

```html
<script type="esms-options">
{
"shimMode": true,
"mapOverrides": true
}
</script>
### Starting your example

<script type="module" src="polyfills.js"></script>
Start the remote:

<script type="module-shim" src="main.js"></script>
```
ng serve mfe1 -o
```

The script with the type `esms-options` configures the polyfill. This library was built for shim mode. In this mode, the polyfill provides some additional features beyond the proposal for Import Maps. These features, for instance, allow for dynamically creating an import map after loading the first EcmaScript module. Native Federation uses this possibility.
Once, the remote is started, start the shell:

To make the polyfill to load your EcmaScript modules (bundles) in shim mode, assign the type `module-shim`. However, please just use module for the polyfill bundle itself to prevent an hen/egg-issue.
```
ng serve shell -o
```

## FAQ

Expand Down
21 changes: 14 additions & 7 deletions libs/native-federation/src/builders/build/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,20 @@ export async function runBuilder(

options.externalDependencies = externals.filter((e) => e !== 'tslib');

const builderRun = await context.scheduleBuilder(
'@angular-devkit/build-angular:browser-esbuild',
options as any,
{ target }
);
// const builderRun = await context.scheduleBuilder(
// '@angular-devkit/build-angular:browser-esbuild',
// options as any,
// { target }
// );

const builderRun = await context.scheduleTarget(target, options as any);

let first = true;
builderRun.output.subscribe(async () => {
builderRun.output.subscribe(async (output) => {
if (!output.success) {
return;
}

updateIndexHtml(fedOptions);

if (first) {
Expand All @@ -83,7 +89,8 @@ export async function runBuilder(
rebuildEvents.rebuildExposed.emit(),
]);
logger.info('Done!');
reloadShell(nfOptions.shell);

setTimeout(() => reloadShell(nfOptions.shell), 0);
}, nfOptions.rebuildDelay);
}

Expand Down
3 changes: 2 additions & 1 deletion libs/native-federation/src/builders/build/schema.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { JsonObject } from '@angular-devkit/core';
export interface NfBuilderSchema extends JsonObject {
target: string;
dev: boolean;
devServerPort: number;
port: number;
open: boolean;
rebuildDelay: number;
shell: string;
} // eslint-disable-line
8 changes: 7 additions & 1 deletion libs/native-federation/src/builders/build/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,16 @@
"description": "Set this to true to start the builder in dev mode",
"default": false
},
"devServerPort": {
"port": {
"type": "number",
"default": 4200
},
"open": {
"type": "boolean",
"default": true,
"description": "Open browser?",
"alias": "o"
},
"rebuildDelay": {
"type": "number",
"default": 2000,
Expand Down
Loading

0 comments on commit 61414f7

Please sign in to comment.