Multiple app bundles and service registration

I have a large enterprise app with multiple modules. I’d like to bundle each module separately. Right now, all my services are registered in app.ts. This would cause all bundles to be loaded immediately instead of when a route needing the bundle is activated. Any guidance on how to handle module specific service registration?

1 Like

you can make you modules features and do the registration in the configure method of each feature, then in your routes you can choose which features to load.
looks like you wanna do something like this: Configure feature after aurelia starts

Let’s say I have a structure similar to this:

– areas
---- area1
---- area2
–resources
–shared
– app.ts
– main.ts

Main.ts configures Aurelia and app.ts is the App component with route registration. Right now, everything is bundled in app-bundle.js. This is basically the setup you get with “au new.” I like the feature idea. In my case, each folder under areas (e.g. area1, area2, etc.) I’d like to be in a separate bundle (e.g. app-area1-bundle.js, etc.).

That I know how to do by modifying the aurelia.json file. What I’m struggling with is figuring out how to load the feature when a route that requires an area (feature) is requested. With Webpack and Angular you can use PLATFORM.moduleName() to load that bundle. We are using AureliaCLI because Webpack didn’t have good support when we started. Switching to Webpack now is a fairly big deal and the CLI is working well anyway. I just need a way to demand-load a feature and handle the service registration in the feature’s configure() method.

that’s exactly what the link above is for, say you have a route called route1 that needs to load a feature called area1, if you put area1 in its own bundle, you can load it on demand like this:

import { inject, Aurelia, FrameworkConfiguration } from 'aurelia-framework';

@inject(Aurelia)
export class Route1 {
    constructor(aurelia) {
        this.aurelia = aurelia;
    }

    activate() {
        return new FrameworkConfiguration(this.aurelia)
            .feature('areas/area1')
            .apply();
    }
}

the bundle won’t be loaded unless you hit the route.

1 Like

@arabsight, thanks for the suggestion, but I don’t believe that will work for me. Correct me if I’m wrong, but in this case, the Route1 class represents the ViewModel at the end of the route. The Route1 ViewModel needs services injected into it’s constructor. The services aren’t registered until the feature is loaded, and that doesn’t happen until the activate function of the ViewModel.

Ok, got it (I think :smile:). In this case you may use a barrel to re-export any services from your feature after doing any custom registration, something like:

// areas/area1/index

import { Container } from 'aurelia-framework';
import { ServiceA } from './service_a'; // areas/area1/service_a

let service = new ServiceA();
service.foo = 'Hello World!';

Container.instance.registerInstance(ServiceA, service);

export { ServiceA };

and import the services from the barrel:

import { inject } from 'aurelia-framework';
import { ServiceA } from 'areas/area1/index';

@inject(ServiceA)
export class Route1ViewModel {
    constructor(serviceA) {
        this.serviceA = serviceA;
    }
}

a feature won’t be needed in this case.

As @BrianPAdams mentioned earlier, Webpack allows for “code splitting” (and does it well) using PLATFORM.moduleName(‘ViewModelName’, ‘my-bundle-name’) and the viewmodel and it’s dependencies are included in that bundle. It sounds like you are using Aurelia’s CLI (RequireJS)? If true, I can say now that after 2 apps using Aurelia CLI, it was very easy to convert it to use Webpack (then just call webpack’s cmd lines bypassing aurelia’s tasks. Webpack is a very verbose tool but spend a few days mucking with the webpackconfig file and you’ll thank the stars you did. It works GREAT for code splitting within your routes (and child routes).

@rkever, we’ve actually built the aureliaCLI’s tasks into our CI build framework now. Moving to Webpack may be a bit difficult for us. What I’ve done so far is use a navigationStrategy like this:

    let loadedFeatures: string[] = [];
    const featureLoaderStrat = function(instruction: NavigationInstruction) {
        debugger;
        let inst = instruction.getAllInstructions()[0];
        if (inst.config.settings.feature) {
            let featureToLoad = `areas/${inst.config.settings.feature.area}`;
            if (!loadedFeatures.includes(featureToLoad)) {
                new FrameworkConfiguration(this.aurelia)
                    .feature(featureToLoad)
                    .apply();
                loadedFeatures.push(featureToLoad);
            }

            if (inst.config.settings.feature.module) {
                let featureToLoad = `areas/${inst.config.settings.feature.area}/modules/${inst.config.settings.feature.module}`;
                if (!loadedFeatures.includes(featureToLoad)) {
                    new FrameworkConfiguration(this.aurelia)
                        .feature(featureToLoad)
                        .apply();
                    loadedFeatures.push(featureToLoad);
                }
            }
        }
    }.bind(this);

    { navigationStrategy: featureLoaderStrat, route: 'myroute', name: 'myname', moduleId: 'mymodule', settings: { feature: { area: 'clients', module: 'module1' } } }

This was working really well until I split things into multiple bundles. I have a root app-bundle.js which contains shared code for all areas. Then a bundle for each area with shared code that all modules in the area will use and a separate bundle for each module under each area. Once it’s in multiple bundles, it won’t load the bundle.

–app-shared
–areas
----clients
-----clients–shared
------modules
--------module1

I totally understand the difficulties in changing the backend (of the frontend) and I of course do not know all the customizations you have in place. All that aforementioned code though could be pulled out if you went with webpack since all that could be managed right in your routes configuration that you already have with simply a single argument added to each route declaration. This also means you don’t have to put any extra logic within your views just to manage DI.

@rkever, It’s clear now that I need to convert to WebPack. But now I’m back to my original question. Where do I register module specific services? Is there a module version of the Feature’s configure method?

Are these “services” 3rd party plugins or your own? Just wondering what they are? Components, services, gateways, factories, etc.??

They are our own. Just normal services to fetch data from the server, etc. Nothing special, but each module has a set that only apply to that module. So I don’t want to deliver them until the user visits a route that needs the module to be loaded. But I need some kind of module loading function that will register the services so that when the view-model is instantiated the services are available for injection. We are defining interfaces (actually abstract classes because typescript complies out the interfaces) and then implementing them. We Test Drive everything and have found this gives us an easier way of faking services when necessary. We just inject the Fake implementation of the interface. Just like normal C#/Java TDD coding.

I have just about everything wired up now except for fonts. We use bootstrap, etc. and the css has references to fonts. Webpack is parsing this and copying the fonts to the dist folder just fine. But when the fonts are requested, they are requested at the root of the website instead of in the dist folder. This has turned into a real nightmare for us. We picked Aurelia over Angular 4 because it didn’t get in the way like Angular does. Now I’m seeing that like anything, the frameworks that are easy, ultimately get in the way when the system depending on them begins to get complex.

Nothing to do with Aurelia. Have a look here


You need to correctly configure the plugin so fonts are resolved from the right path

@MaximBalaganskiy, thanks for the post. I think I’m just being really dense here. I’m not seeing the solution. It looks to me like this is helping webpack resolve the path to the font, etc. assuming the font still resides in its original location. For me, Webpack is copying the font from it’s original location, changing the name to a Guid (but retaining the extension), placing the copied/munged file in the dist folder, and updating references to the font in my source code so that they request the font from the root instead of from the dist.

For instance, I have a font in src/core-styles/fonts/somefont.woff2. The css file lives in src/core-styes/stylesheets/styles.css and references the font via a relative path url(). Webpack is changing the name of the font to something like 448c34a56d699c29117adc64c43affeb.woff2 and copying it to wwwroot/dist. It’s then requesting the font from http://www.mydomain.com/448c34a56d699c29117adc64c43affeb.woff2 instead of http://www.mydomain.com/dist/448c34a56d699c29117adc64c43affeb.woff2

I had similar issue at first. What I’ve done though was to set it up so that any fonts/images that are within my src folder (my own stuff) NOT be bundled but for all 3rd party (font awesome, bootstrap, etc.) they are bundled and shoved into dist.

I’ll paste my webconfig rules below but my disclaimer is that I’m a newbie still with webpack so keep that in mind. Maybe others can chime in with the rules I’ve setup if there’s something not right. What I have below though works well for me.

Please feel free to comment is something doesn’t look right…

rules: [
    // CSS required in JS/TS files should use the style-loader that auto-injects it into the website
    // only when the issuer is a .js/.ts file, so the loaders are not applied inside html templates
    {
        test: /\.css$/i,
        issuer: [{ not: [{ test: /\.html$/i }] }],
        use: ['style-loader', ...cssRules]
    },
    {
        test: /\.css$/i,
        issuer: [{ test: /\.html$/i }],
        // CSS required in templates cannot be extracted safely
        // because Aurelia would try to require it again in runtime
        use: cssRules
    },
    {
        test: /\.scss$/,
        use: ['style-loader', 'css-loader', 'sass-loader'],
        issuer: /\.[tj]s$/i
    },
    {
        test: /\.scss$/,
        use: ['css-loader', 'sass-loader'],
        issuer: /\.html?$/i
    },
    { test: /\.html$/i, loader: 'html-loader' },
    {
        test: /\.js$/i, loader: 'babel-loader', exclude: nodeModulesDir, options: {}
    },
    { test: /\.json$/i, loader: 'json-loader' },

    // use Bluebird as the global Promise
    { test: /[\/\\]node_modules[\/\\]bluebird[\/\\].+\.js$/, loader: 'expose-loader?Promise' },

    // exposes jQuery globally as $ and as jQuery
    { test: require.resolve('jquery'), loader: 'expose-loader?$!expose-loader?jQuery' },

    // load these img files normally
    { test: /\.(png|gif|jpg|svg|cur)$/i, loader: 'file-loader', options: { emitFile: false } },]

    // load these fonts normally
    { test: /\.woff2(\?v=[0-9]\.[0-9]\.[0-9])?$/i, loader: 'file-loader', options: { mimetype: 'application/font-woff2' } },
    { test: /\.woff(\?v=[0-9]\.[0-9]\.[0-9])?$/i, loader: 'file-loader', options: { mimetype: 'application/font-woff' } },
    { test: /\.(ttf|eot)(\?v=[0-9]\.[0-9]\.[0-9])?$/, use: [{ loader: 'file-loader' }] }
]

As far as your services DI concern, all your dependencies that are part of your routed viewmodel, and all child classes and their depenencies, will be bundled together in your route declaration PLATFORM.moduleName('views/area1/myview/index', 'area1.myview').

For example, if you have the below structure…

src
    -areas
        -area1
            -view1 (with importing service1, service2, childViewModel--with it's imports)

and in your route you have…

{
    route: ['my/url/route'],
    name: 'area1-view1',
    moduleId: PLATFORM.moduleName('areas/area1/view1/index', 'areas.area1.view1'),
    title: 'Area1 > My View 1',
}

Then you’d end up with a bundle in your dist folder called areas.area1.view1.chunk.js and if you inspect it you’ll find all your js classes in the aforementioned structure. And when you inspect your dev toolbar network tab and hit your route “my/url/route” you’ll see the areas.area1.view1.chunk.js file is only loaded when you hit that page. It’s so easy to code split that it seems like it shouldn’t just work, but it does. It even works with child routes too.

Thanks for all your help! I believe I have resolved the dependency loading issues. As for the code splitting and services, yeah, I have that all set up just fine and service code is bundled into the module. The problem is where/how I make the call like this:

container.registerSingleton(IDocumentStorageDataService, DocumentStorageDataService);

Right now that’s happening in main.ts as part of the Aurelia configuration. I can’t do it here if some of the services live in a module bundle that I don’t want to be loaded until a route is requested. With Features I had a configure method that was called when the feature was loaded, which could be done on-demand. But I’m not seeing how to do that with a module that lives in a different bundle. I need some type of configure method to be called the first time the bundle that contains a module is fetched.

I’ll try reproducing this if get some spare time today.

This depends heavily on how you’re loading the module. It seems like you’re splitting “areas” of your application, so I’m assuming if you’re going to lazy load area2 it will happen when the user navigates to an area2/route.

If that is the case, my best recommendation would be to have a base area2 route and all area2 routes as child routes of the base. This would afford you the opportunity to do some bootstrapping, including registering objects with the DI container, on the area2 shell. It’s also an excellent practice in general since it is likely that all of your routes in area2 have common features in the view that can be pulled into the area2 shell, keeping your code DRY.

Edit:

After a bit of playing around, I found that you can technically lazy load a feature. It would be possible to subscribe to the navigation event on your app router and then load a feature from a different module.

app.ts

activate() {
  var subscription = this.router.events.subscribe('router:navigation:processing', ({ instruction }) => {

    // whenever someone tried to load an area2 route
    if (/area2/i.test(instruction.fragment)) {

      // load the area2 feature, which calls the configure function in area2/index.ts
      let config = new FrameworkConfiguration(this.aurelia);
      config.feature('app2').apply();

      // and end the subscription
      subscription.dispose();
    }
  })
}

I’m not sure this is a good idea, though. If you’re getting this far into the app that you need to start splitting off entire features, it might make sense to have all of your models available in the core application and registered on the app container immediately.

Thanks everyone! @davismj, I’ll take door #1. :smile: I think that one’s going to be my solution. That way if all modules in an area share area specific code I have a way to load that, then load the module specific code.