Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Cover image for Module Federation in mobile apps powered by NativeScript
Valor Labs profile imageEduardo Speroni
Eduardo Speroni forValor Labs

Posted on

     

Module Federation in mobile apps powered by NativeScript

Module federation has been one of the most popular topics in development lately. People love the way it allows teams to develop applications independently and integrate them all into a single final application. While that seems good for the web, how could Module Federation look in a mobile native application?

Let’s get the elephant out of the room first. The whole point of module federation is that teams can deploy their applications independently, but native apps have their bundles and code shipped holistically with the app. Even if they didn’t, having the user wait or be unable to load your app in bad or no connectivity would lead to terrible UX. Before going down this path, you need careful thought and a really good reason.

Scene from Jurassic Park where the character says the scientists were more preoccupied wether they could, not should

So let’s start with a use case. One of our large enterprise clients has a WYSIWYG editor for NativeScript, complete with their own native components library. They have their own SSO and app “shell” that is common to all of their apps, but their users are able to customize the content, including pushing changes only to specific screens. To generate this they needed to be able to generate bundles dynamically and push them to the application so they could easily switch between apps, and update only the user’s bundle.

This application highlights one of the beauties of NativeScript. The users don’t need to have knowledge of native code at all, and if they need to extend something, they can do it directly in JavaScript or TypeScript, while also allowing them to add native code once they feel like they need it.

Now back to the application. This was initially built before bundlers were widely used, and once bundlers became the norm, it became a tricky situation where they’d need to map the available modules and override the require functions to provide the user code with the expected module. A mess. Enter Webpack Module Federation.

Exposing an application

For this example, I’ve decided to use Angular as it’s one of the frameworks officially supported by NativeScript. So first we create our app

import{Component,NgModule,NO_ERRORS_SCHEMA}from"@angular/core";import{RouterModule}from"@angular/router";import{NativeScriptCommonModule}from"@nativescript/angular";import{timer}from"rxjs";@Component({template:`<Label>Hello from wmf! Here's a counter: {{ timer | async }}</Label>`,})exportclassMyComponent{timer=timer(0,1000);}@NgModule({declarations:[MyComponent],imports:[NativeScriptCommonModule,RouterModule.forChild([{path:"",component:MyComponent}])],schemas:[NO_ERRORS_SCHEMA]})exportclassFederatedModule{}
Enter fullscreen modeExit fullscreen mode

Since we’ll need to download all the JS files anyway, for testing purposes I’ve made it all compile to a single chunk and discard the non-remote entrypoint. To do this I used the default NativeScript webpack config and augmented with a few details to build it directly to my current app’s assets directory.

constwebpack=require("@nativescript/webpack");constcoreWebpack=require('webpack');constpath=require(`path`);constNoEmitPlugin=require('no-emit-webpack-plugin');module.exports=(env)=>{webpack.init(env);constpackageJson=require('./package.json');// Learn how to customize:// <https://docs.nativescript.org/webpack>webpack.chainWebpack((config,env)=>{config.entryPoints.clear();config.resolve.alias.set('~',path.join(__dirname,'federated-src'));config.resolve.alias.set('@',path.join(__dirname,'federated-src'));config.plugins.delete('CopyWebpackPlugin');config.output.path(path.join(__dirname,'src','assets'));config.optimization.runtimeChunk(true);config.module.delete('bundle');config.plugin('NoEmitPlugin').use(NoEmitPlugin,['dummy.js']);config.plugin('MaxChunks').use(coreWebpack.optimize.LimitChunkCountPlugin,[{maxChunks:1}]);config.plugin('WebpackModuleFederationPlugin').use(coreWebpack.container.ModuleFederationPlugin,[{name:'federated',exposes:{'./federated.module':'./federated-src/federated.module.ts'},library:{type:'commonjs'},shared:{'@nativescript/core':{eager:true,singleton:true,requiredVersion:"*",import:false},'@nativescript/angular':{eager:true,singleton:true,requiredVersion:"*",import:false},'@angular/core':{eager:true,singleton:true,requiredVersion:"*",import:false},'@angular/router':{eager:true,singleton:true,requiredVersion:"*",import:false},}}]);});constconfig=webpack.resolveConfig();config.entry={'dummy':'./federated-src/federated.module.ts'};returnconfig;};
Enter fullscreen modeExit fullscreen mode

Loading the remote entrypoint

One of the tricky parts of this whole process is that we can’t download the app piece by piece, as underneath we’re using commonjs (node’s require) to evaluate and load the modules into memory. To do this we need to download all of the output into the application and then we can load it.
As a POC, we can start with a simple remote configuration which allows us to load the entrypoint as a normal module.

// federated webpack config{name:'federated',exposes:{'./federated.module':'./federated-src/federated.module.ts'},library:{type:'commonjs'},}// host config{remoteType:"commonjs",remotes:{"federated":"~/assets/federated.js"}}
Enter fullscreen modeExit fullscreen mode

And the import it as a route like:

{path:'federated',loadChildren:()=>import('federated/federated.module').then((m)=>m.FederatedModule),}
Enter fullscreen modeExit fullscreen mode

Unfortunately, we’d have to have all the federated modules shipped in the final application, so to load things dynamically, we should instead use the following code to load arbitrary entrypoints:

/// <reference path="../../node_modules/webpack/module.d.ts" />typeFactory=()=>any;typeShareScope=typeof__webpack_share_scopes__[string];interfaceContainer{init(shareScope:ShareScope):void;get(module:string):Factory;}exportenumFileType{Component="Component",Module="Module",Css="CSS",Html="Html",}exportinterfaceLoadRemoteFileOptions{// actual file being importedremoteEntry:string;// used as a "key" to store the file in the cacheremoteName:string;// what file to import// must match the "exposes" property of the federated bundle// Example:// exposes: {'.': './file.ts', './otherFile': './some/path/otherFile.ts'}// calling this function with '.' will import './file.ts'// calling this function with './otherFile' will import './some/path/otherFile.ts'exposedFile:string;// mostly unused for the moment, just use Module// can be used in the future to change how to load specific filesexposeFileType:FileType;}exportclassMfeUtil{// holds list of loaded scriptprivatefileMap:Record<string,boolean>={};privatemoduleMap:Record<string,Container>={};findExposedModule=async<T>(uniqueName:string,exposedFile:string):Promise<T|undefined>=>{letModule:T|undefined;// Initializes the shared scope. Fills it with known provided modules from this build and all remotesawait__webpack_init_sharing__("default");constcontainer=this.moduleMap[uniqueName];// Initialize the container, it may provide shared modulesawaitcontainer.init(__webpack_share_scopes__.default);constfactory=awaitcontainer.get(exposedFile);Module=factory();returnModule;};publicloadRootFromFile(filePath:string){returnthis.loadRemoteFile({exposedFile:".",exposeFileType:FileType.Module,remoteEntry:filePath,remoteName:filePath,});}publicloadRemoteFile=async(loadRemoteModuleOptions:LoadRemoteFileOptions):Promise<any>=>{awaitthis.loadRemoteEntry(loadRemoteModuleOptions.remoteEntry,loadRemoteModuleOptions.remoteName);returnawaitthis.findExposedModule<any>(loadRemoteModuleOptions.remoteName,loadRemoteModuleOptions.exposedFile);};privateloadRemoteEntry=async(remoteEntry:string,uniqueName?:string):Promise<void>=>{returnnewPromise<void>((resolve,reject)=>{if(this.fileMap[remoteEntry]){resolve();return;}this.fileMap[remoteEntry]=true;constrequired=__non_webpack_require__(remoteEntry);this.moduleMap[uniqueName]=requiredasContainer;resolve();return;});};}exportconstmoduleFederationImporter=newMfeUtil();
Enter fullscreen modeExit fullscreen mode

This code is able to load any .js file on the device, so it can be used in conjunction with a download strategy to download the files and then load them dynamically. For example, we can first download the full file, and then load it:

{path:"federated",loadChildren:async()=>{constfile=awaitHttp.getFile('http://127.0.0.1:3000/federated.js');return(awaitmoduleFederationImporter.loadRemoteFile({exposedFile:"./federated.module",exposeFileType:FileType.Module,remoteEntry:file.path,remoteName:"federated",})).FederatedModule;},},
Enter fullscreen modeExit fullscreen mode

Alternatively, we could also download it as a zip and extract, or you could, theoretically, override the way that webpack loads the chunks in the federated module to download them piece by piece as needed.
Sharing the common modules
The complexity of sharing modules cannot be understated. The initial Webpack Module Federation PR (https://github.com/webpack/webpack/pull/10838) that provided the full container and consumer API is smaller then the PR that introduced version shared dependencies (https://github.com/webpack/webpack/pull/10960).
A native app is not just a webpage, but the full browser itself. While the web provides a lot of APIs directly, NativeScript provides a lot of them through the @nativescript/core package so that’s one dependency that has to be a singleton and we can’t under any circumstance have multiple versions of it. In this example we’re also using angular, so let’s share that as well:

shared:{'@nativescript/core':{eager:true,singleton:true,requiredVersion:"*"},'@nativescript/angular':{eager:true,singleton:true,requiredVersion:"*"},'@angular/core':{eager:true,singleton:true,requiredVersion:"*"},'@angular/router':{eager:true,singleton:true,requiredVersion:"*"},}
Enter fullscreen modeExit fullscreen mode

Here we also share them as eager, since those packages are critical to the bootstrap of the application. For example, @nativescript/core is responsible for calling UIApplicationMain on iOS, so if you fail to call it, the app will instantly close.

Result

First, we create a simple standalone component that will show a Label and a nested page which will be loaded asynchronous:

import{Component,NO_ERRORS_SCHEMA}from"@angular/core";import{NativeScriptCommonModule,NativeScriptRouterModule,}from"@nativescript/angular";@Component({standalone:true,template:`<StackLayout>   <Label>Hello from standalone component</Label>   <GridLayout><page-router-outlet></page-router-outlet></GridLayout> </StackLayout>`,schemas:[NO_ERRORS_SCHEMA],imports:[NativeScriptCommonModule,NativeScriptRouterModule],})exportclassShellComponent{}
Enter fullscreen modeExit fullscreen mode

Then we can define the Federated Module:

@Component({template:`<Label>Hello from wmf! Here's a counter: {{ timer | async }}</Label>`,})exportclassMyComponent{timer=timer(0,1000);}@NgModule({declarations:[MyComponent],imports:[NativeScriptCommonModule,RouterModule.forChild([{path:"",component:MyComponent}])],schemas:[NO_ERRORS_SCHEMA]})exportclassFederatedModule{}
Enter fullscreen modeExit fullscreen mode

And finally, we can setup the routing:

import{NgModule}from"@angular/core";import{Routes}from"@angular/router";import{NativeScriptRouterModule}from"@nativescript/angular";import{FileType,moduleFederationImporter}from"./mfe.utils";import{Http}from"@nativescript/core";import{ShellComponent}from"./shell.component";constroutes:Routes=[{path:"",redirectTo:"/shell",pathMatch:"full"},{path:"shell",component:ShellComponent,loadChildren:async()=>{constfile=awaitHttp.getFile("http://127.0.0.1:3000/federated.js");return(awaitmoduleFederationImporter.loadRemoteFile({exposedFile:"./federated.module",exposeFileType:FileType.Module,remoteEntry:file.path,remoteName:"federated",})).FederatedModule;},},];@NgModule({imports:[NativeScriptRouterModule.forRoot(routes),ShellComponent],exports:[NativeScriptRouterModule],})exportclassAppRoutingModule{}
Enter fullscreen modeExit fullscreen mode

Which results in the following screen, fully working module federation in NativeScript!

Result of a federated module wrapped by a normal module

Conclusion

Although Module Federations is still limited on the native application side, we’re already exploring possibilities on how to import modules from the web directly, instead of having to download them manually, giving it first class support and allowing full code splitted remote modules:

constentry=awaitimport('https://example.com/remoteEntry.js');entry.get(...)// entry magically fetches https://example.com/chunk.0.js if needed
Enter fullscreen modeExit fullscreen mode

Module Federation is very promising for creating distribution of efforts and on demand releases without having to go through the pain of constant app store approval processes. While not for everyone it is a very exciting opportunity for large teams.

Need help?

Valor Software is both an official partner of both the NativeScript organization and Module Federation organization. If you're looking at using Module Federation with your NativeScript application and would like some help. Reach out to our team,sales@valor-software.com

Top comments(0)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

Modernizing the web with Module Federation and Nx

More fromValor Labs

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp