Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Coding splitting child apps #102

Closed
mbanting opened this issue Jul 7, 2017 · 52 comments
Closed

Coding splitting child apps #102

mbanting opened this issue Jul 7, 2017 · 52 comments

Comments

@mbanting
Copy link

mbanting commented Jul 7, 2017

Hi there, great framework. Thanks for sharing!

Do child apps need to be a single bundle or can they be code splitted?

As an exercise I converted https://github.com/AngularClass/angular-starter into a single-spa child and it successfully mounted onto my root single-spa app. However, it fails on certain routes within the child app, as it tries loading its chunk from the root app

GET http://127.0.0.1:8080/1.chunk.js

rather than the location of the child app on the filesystem.

What's the best strategy to enable on-demand loading of chunks for child apps? Or do I import every chunk in declareChildApplication?

@joeldenning
Copy link
Member

joeldenning commented Jul 7, 2017

A few questions:

  • What do your declareChildApplication() calls look like? Are they something like this? singleSpa.declareChildApplication('app-name', import('./child-app.js'), () => true)? The important thing there is if you're using import() (or System.import()) to code split the child apps.
  • Do you have one webpack config for everything (the root application and the child applications)? Or separate webpack configs for each child app? The answer to that question changes how you pull off code splitting.
  • How are you doing code splitting within child apps? Route based with import()?

Some resources to look at that may or may not be helpful:

  • The publicPath config option for webpack. This might come into play if you have multiple webpack configs.
  • The webpack 2 docs in single-spa
  • The tests for single-spa that use webpack code splitting successfully. It might be a slightly different situation than the one you're in with angular-starter, but hopefully will be helpful.

@mbanting
Copy link
Author

mbanting commented Jul 7, 2017

Thanks for the quick response!

We're using separate webpack configs. In our use case, we'd like for each child application to be managed separately by different teams, each with their own release cycle. Eventually we'd like to load them from a CDN.

For now, in our proof of concept using angular-start as a sample child app, it comes with it's own webpack config that generates this in its dist directory

> ls
0.chunk.js		3.chunk.js		index.html
0.chunk.js.map		3.chunk.js.map		main.bundle.js
1.chunk.js		4.chunk.js		main.bundle.js.map
1.chunk.js.map		4.chunk.js.map		robots.txt
2.chunk.js		assets
2.chunk.js.map		humans.txt

In our root application, we're only importing main.bundle.js like this:

import {declareChildApplication, start} from 'single-spa';
declareChildApplication('angular', import('../angular-starter/dist/main.bundle'), () => true)

which leads to the error when trying to load 1.chunk.js.

@joeldenning
Copy link
Member

joeldenning commented Jul 7, 2017

That's awesome, having separate projects for each child application with their own release cycle and webpack config really helps. That is how we do it for our production application at Canopy.

To pull it off, you have a few options, but first let me explain why what you are doing doesn't work. The reason is that webpack import() does not let you import a totally separately webpack bundle. Webpack really is great if all the source code uses a single webpack config, but it is not a "module loader" that can dynamically load other bundles in the browser. Here's a twitter conversation and another one with Sean Larkin about this. Basically it boils down to webpack being a "bundler" but not being a dynamic "module loader."

Your options basically fall into a few categories:

  1. One webpack config for everything
  2. A webpack config per child application, using a module loader like SystemJS to dynamically load the webpack bundles in the browser (this is what we do at Canopy for our production app). What this would look like is you would do singleSpa.declareChildApplication('app-name', SystemJS.import('https://url-to-webpack-bundle.js'), () => true). If you go down this route, it will probably take a bit of extra work to get the initial proof of concept going, but once you do you will have the ability to have separate projects and separate release cycles. @blittle and I would be happy to help with it as well, there are a couple of other projects that might help with it and be worth considering.
  3. Implement your own little "module loader" that simply script tags webpack bundles. This would look something like this:
singleSpa.declareChildApplication('app-name', () => loadApp('https://url-to-app'), () => true)

function loadApp(url) {
  return new Promise((resolve, reject) => {
    const script = document.createElement('script')
    script.src = url;
    script.onload = resolve;
    script.onerror = reject;
    document.head.appendChild(script)
  });
}
  1. Start out with code splits for each child app, but not code splits within each child app. And then figure out how you want to pull off code splits within a child application later down the road.

@mbanting
Copy link
Author

mbanting commented Jul 7, 2017

Actually I had a typo in my snippet. I'm actually doing this:
declareChildApplication('angular', System.import('../angular-starter/dist/main.bundle'), () => true)

Which I believe still uses webpack.

Thank you for the explanation! Makes sense! I will give SystemJS a try.
Much appreciated!

@joeldenning
Copy link
Member

No problem, happy to help if you have any more questions. And yeah System.import() inside of a webpack project is still handled by webpack. SystemJS also lets you do System.import, but if you want to make sure you're using SystemJS for loading, you can try SystemJS.import().

@blittle
Copy link
Contributor

blittle commented Jul 7, 2017

@mbanting we load child applications with a SystemJS plugin called sofe. The child app declarations look like:

singleSpa.declareChildApplication('app-name', SystemJS.import('awesome-child-app!sofe'), () => true)

Sofe at runtime will figure out a url where awesome-child-app is located. It does this through a simple manifest declaration {'awesome-child-app': 'https://some-cdn.com/app.js'}. This way, when a team wants to independently deploy their child app, they do so in a two step process: 1) upload a new js bundle to a cdn, 2) update the manifest resolution. Because that resolution happens at runtime, your single-spa root application doesn't need to be rebuilt and deployed.

@mbanting
Copy link
Author

mbanting commented Jul 7, 2017

That's great @blittle. That's exactly what we're going for too. I'll take a look at sofe. Thanks for sharing!

@mbanting
Copy link
Author

I was able to solve my issue by building the child app with a fully qualified URL set for its publicPath in its webpack config! Thanks @joeldenning for the hint!

More detail:
I changed over to SystemJS, and am loading the child app via an external URL as part of this proof of concept.
ie. Root app is running on localhost:8080 and the child app is hosted on localhost:8081

The root app loads the child-app (angular-starter) as follows:

declareChildApplication('angular', SystemJS.import('http://localhost:8081/dist/main.bundle.js'), () => true)

Child app is mounted successfully, but still fails when unsuccessfully loading its subsequent chunks. The issue was the child app, built with Webpack, attempted to load with a relative URL, resulting in a 404:

GET http://127.0.0.1:8080/0.chunk.js 404 (Not Found)

Using @joeldenning's hint of setting the publicPath (in this case to http://localhost:8081/dist) fixes this! We can use this in our child app's production webpack configs.

Is this how you solved this on your end?

@joeldenning
Copy link
Member

joeldenning commented Jul 11, 2017

We solved it a little differently, actually. Since we're using sofe, the urls for our child applications are actually dynamic, so we need a dynamic publicPath calculated in the browser instead of the static one set by webpack. We're doing that with https://github.com/CanopyTax/webpack-system-register (specifically the useSystemJSLocateDir option). I am confident it could be done better than that, since compiling to a system-register format is unnecessary to change the publicPath. But that's how we're doing it for one of our child applications.

Also, I noticed in your last comment that your code sample might have a bug:

// Code with bug
declareChildApplication('angular', SystemJS.import('http://localhost:8081/dist/main.bundle.js'), () => true)

// Correct code
declareChildApplication('angular', () => SystemJS.import('http://localhost:8081/dist/main.bundle.js'), () => true)

This might just be an artifact of writing dummy code, but if you're doing this in real code it won't work because the second argument to declareChildApplication needs to be a function that returns a promise. It's intended for lazy loading.

Let us know if there are other questions, feedback, or roadblocks. Happy to help however we can!

@mbanting
Copy link
Author

Thanks @joeldenning!
Yes that code was just bad dummy code. Good catch though!
Thanks again for all your help!

@daniele-zurico
Copy link

daniele-zurico commented Sep 5, 2017

Hey @mbanting @joeldenning guys sorry about that, I know that the issue has been closed however I would to have some explanation about how to do:
root-application.js

 singleSpa.declareChildApplication(
	'app-3', 
	() => scriptTagApp('http://127.0.0.1:9091/dist/child-application.js', 'app-3'), 
	hashPrefix('/app3')
);

function scriptTagApp(url, globalVarName) {
	return new Promise((resolve, reject) => {
	  var script = document.createElement('script');
	  script.onload = function() {
		console.log('done', window[globalVarName]);
		resolve(window[globalVarName]);
	  };
	  script.onerror = err => {
		console.log('err');  
		reject(err);
	  };
	  
	  script.src = url;
	  script.async = true;
	  document.getElementsByTagName('head')[0].appendChild(script);	  
	});
}

The child app:

const ngLifecycles = singleSpaAngular({
	domElementGetter,
	mainModule,
	angularPlatform: platformBrowserDynamic(),
	template: `<app3 />`,
});

export function bootstrap() {
	return ngLifecycles.bootstrap();
}

export function mount() {
	return ngLifecycles.mount();
}

export function unmount() {
	return ngLifecycles.unmount();
}

function domElementGetter() {
	// Make sure there is a div for us to render into
	let el = document.getElementById('app-3');
	if (!el) {
		el = document.createElement('div');
		el.id = 'app-3';
		document.body.appendChild(el);
	}

	return el;
}

It complains that say no bootstrap function exported.
What I do wrong?

@joeldenning
Copy link
Member

@daniele-zurico the scriptTagApp function is the reason why it is saying that. If you use a real module loader or code splitter like webpack or systemjs (for example() => import('./path-to-child-app.js'), then your module loader / bundler will take care of handling exports. However, the scriptTagApp function is really just not a very sophisticated module loader at all. Notice that you have to pass in globalVarName. This means that the child app needs to create a global variable when it gets script tagged, and export won't really work. Here is an example of how it could look.

singleSpa.declareChildApplication('child-app1', () => scriptTagApp('/child-application.js', 'childApp1'), () => true)
// child-application.js

window.childApp1 = {
  bootstrap: function(props) {...},
  mount: function(props) {...},
  unmount: function(props) {...},
}

Having to create global variables is not ideal, which is why it is recommended to use webpack or systemjs so that you don't have to do that.

@rivamadan
Copy link

I'm having similar issues. It would be great if I could get some help.

The root application is angular 4 (root-app) and I have a child application that is also angular 4 (child-app). They are in two separate directories. We are just using angular cli, but I know that behind the scenes it uses webpack.

I have the single-spa-angular-2 code in /child-app/app.js (following the example from the repo). The main module is imported as AppModule from ./src/app/app.module.ts

In my root-app, I have the declareChildApplication in app.component.ts.

declareChildApplication('child-app', () => System.import('../../../child-app/app.js', ()=> true)

At first I tried to just use "import" but it kept telling me "expression expected." Then when I added System, it said cannot find name System. I fixed that by adding 'declare var System: any;' to typings.d.ts. Do you know anything about that? I think it is a typescript related issue.

I am currently getting an error: "died in status LOADING_SOURCE_CODE: single-spaangular must be passed opts.mainModule, which is the Angular module to bootstrap. If I console log AppModule in /child-app/app.js, it returns undefined. I'm guessing that's why I'm getting an error and I'm getting undefined because it can't get the module file. Does this have to do with angular cli using webpack? Should I use SystemJS? Do I have to build the child app first and use main.bundle.js like mbanting?

I don't understand passing in bundle.js to declareChildApplication. How does it see the
app.js?

Let me know if you need any more details.

By the way, this metaframework is super awesome and useful; exactly what we need at my work.

@joeldenning
Copy link
Member

joeldenning commented Oct 13, 2017

@rivamadan

Thanks for saying you find it awesome and useful! Always good to hear 👍. And thanks for commenting here, we're very happy to try to help

At first I tried to just use "import" but it kept telling me "expression expected." Then when I added System, it said cannot find name System. I fixed that by adding 'declare var System: any;' to typings.d.ts. Do you know anything about that? I think it is a typescript related issue.

Yeah that sounds like a typescript issue. It needs to know about the System global variable or it thinks there might be a bug in your code.

I am currently getting an error: "died in status LOADING_SOURCE_CODE: single-spaangular must be passed opts.mainModule, which is the Angular module to bootstrap. If I console log AppModule in /child-app/app.js, it returns undefined. I'm guessing that's why I'm getting an error and I'm getting undefined because it can't get the module file. Does this have to do with angular cli using webpack? Should I use SystemJS? Do I have to build the child app first and use main.bundle.js like mbanting?

Could you paste the file for your child application here? Also you can check out https://github.com/joeldenning/simple-single-spa-webpack-example/blob/master/src/app2/app2.js as an example of how to implement an angular2 child application.

I don't understand passing in bundle.js to declareChildApplication. How does it see the app.js?

I'm actually not sure what you're referring to here. Are you talking about something that was discussed earlier in the thread?

@rivamadan
Copy link

This is app.js in my child application. I was looking at that link when implementing my child application.

import singleSpaAngular2 from 'single-spa-angular2';
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import AppModule from './src/app/app.module.ts';

console.log(AppModule)

const ngLifecycles = singleSpaAngular2({
	domElementGetter,
	AppModule,
	angularPlatform: platformBrowserDynamic(),
	template: `<child-app />`
})

export function bootstrap() {
	return ngLifecycles.bootstrap();
}

export function mount() {
	return ngLifecycles.mount();
}

export function unmount() {
	return ngLifecycles.unmount();
}

function domElementGetter() {
	// Make sure there is a div for us to render into
  let el = document.getElementById('child-app');
	if (!el) {
		el = document.createElement('div');
		el.id = 'files-delivery';
		document.body.appendChild(el);
	}

	return el;
}

This is my root application, which is also Angular 4.

import { Component } from '@angular/core';
import { declareChildApplication, start } from 'single-spa';

declareChildApplication('delivery', () => System.import('../../../child-app/app.js'), activityFunction('/child'));
start();

function activityFunction(prefix: string) {
	return function(location): boolean {
		return location.pathname.startsWith(prefix);
	}
}

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {
  title = 'parent-app';
}

Do I have to build the child app first and use main.bundle.js like mbanting?
I don't understand passing in bundle.js to declareChildApplication. How does it see the
app.js?

Yes, I was referring to what was discussed earlier in this thread.

@joeldenning
Copy link
Member

@rivamadan in your app.module.ts file, do you export default AppModule? Or do you export const AppModule. The Angular convention is usually to export named things instead of exporting default. In your code, you are expecting the default export to be the app module, but if it is named that would explain why it is undefined.

@rivamadan
Copy link

rivamadan commented Oct 13, 2017

Ahh, I didn't know about the difference. Yeah, I am exporting a named thing, so I changed my import to import {AppModule} from './src/app/app.module.ts'; and it isn't undefined anymore. Thanks!

But I'm still getting this error:

Error: 'delivery' died in status LOADING_SOURCE_CODE: single-spa-angular2 must be passed opts.mainModule, which is the Angular module to bootstrap
    at singleSpaAngular2 (single-spa-angular2.js:38)
    at Object.../../../../../../child-app/app.js (app.js:6)
    at __webpack_require__ (bootstrap 2764b253e1fecdfe3d19:54)
    at ZoneDelegate.webpackJsonp.../../../../zone.js/dist/zone.js.ZoneDelegate.invoke (zone.js:392)
    at Zone.webpackJsonp.../../../../zone.js/dist/zone.js.Zone.run (zone.js:142)
    at zone.js:873
    at ZoneDelegate.webpackJsonp.../../../../zone.js/dist/zone.js.ZoneDelegate.invokeTask (zone.js:425)
    at Zone.webpackJsonp.../../../../zone.js/dist/zone.js.Zone.runTask (zone.js:192)
    at drainMicroTaskQueue (zone.js:602)
    at <anonymous>

@joeldenning
Copy link
Member

Try changing the following:

const ngLifecycles = singleSpaAngular2({
	domElementGetter,
	AppModule,
	angularPlatform: platformBrowserDynamic(),
	template: `<child-app />`
})

to this:

const ngLifecycles = singleSpaAngular2({
	domElementGetter,
	mainModule: AppModule,
	angularPlatform: platformBrowserDynamic(),
	template: `<child-app />`
})

Also, just for reference, the full documentation for using single-spa-angular2 can be found at https://github.com/CanopyTax/single-spa-angular2

@rivamadan
Copy link

That solved it! Thanks so much!

@joeldenning
Copy link
Member

Glad it worked! Let me know if you run into other hiccups!

@eullerpereira94
Copy link

@rivamadan Is this project for your company or confidential of some sort? I would love to have a look on your project. Because I'm literally doing the same thing as you, and my child application can only bootstrap, and it's not mounting. BTW, I live on the bleeding edge of frontend, so yes, the beta version packages may be the cause of my frustration.

@rivamadan
Copy link

@euller88 The project is for my company and confidential, but it's just a simple structure at this point so I can change a few names of things and you can take a look at it. But it might be easier if you just share your project. Could you explain in more detail what you think the problem is? Is your root and child app both angular 4?

@eullerpereira94
Copy link

No, both of them are on beta versions, Angular 5 rc2. Most part of the packages are also on beta stages. I have a cool little script on my computer that installs beta packages if they're available. It is fun.

The only thing the child app has is the app.js file and the single-spa package for angular 2. And the root app is identical to yours.

I will rebuild both projects from scratch this weekend, using only stable components. Maybe that is the solution. If that doesn't work, I will come back to say my results. But thank you anyway.

@rivamadan
Copy link

@euller88 I see. It might be the single-spa package for angular 2 that doesn't work. I haven't looked into Angular 5 so I don't know the differences between that and Angular 4, but my guess would be something in the single-spa package for angular 2 needs to be changed.

@eullerpereira94
Copy link

eullerpereira94 commented Oct 23, 2017

Hello everyone, it was the beta packages. They were interacting in a way that was breaking the application.
I've cleaned up my project and only installed the stable versions of each one. And now it's working fine. As rad as I can be, from now on I will avoid beta packages when working with single-spa. Thank you for your attention.

@joeldenning
Copy link
Member

@euller88 glad to hear you got things working!

@rivamadan
Copy link

rivamadan commented Oct 27, 2017

@joeldenning Thanks for your help before! I was wondering if you have any insight into this new situation I'm facing.

I'm trying to put angular 1 child app into an angular 4 root app. This angular 1 child app uses bower and gulp.

This is my angular 1 child app.js file:

import singleSpaAngular1 from 'single-spa-angular1';
import angular from 'angular';
import './app/app.js';

const ng1Lifecycles = singleSpaAngular1({
  angular,
  domElementGetter,
  mainAngularModule: 'analytics',
  uiRouter: true,
  preserveGlobal: true
});

export const bootstrap = [
  ng1Lifecycles.bootstrap,
];

export const mount = [
  ng1Lifecycles.mount,
];

export const unmount = [
  ng1Lifecycles.unmount,
];

function domElementGetter() {
	// Make sure there is a div for us to render into
  let el = document.getElementById('analytics');
	if (!el) {
		el = document.createElement('div');
		el.id = 'analytics';
		document.body.appendChild(el);
	}
	return el;
}

When I run my parent application, I get this error:

ERROR in ../a8-fe/app.js
Module not found: Error: Can't resolve 'angular' in '/Users/rmadan/a8-fe'
resolve 'angular' in '/Users/rmadan/a8-fe'
  Parsed request is a module
  using description file: /Users/rmadan/a8-fe/package.json (relative path: .)
    Field 'browser' doesn't contain a valid alias configuration
  after using description file: /Users/rmadan/a8-fe/package.json (relative path: .)
    resolve as module
      /Users/rmadan/node_modules doesn't exist or is not a directory
      /Users/node_modules doesn't exist or is not a directory
      /node_modules doesn't exist or is not a directory
      looking for modules in /Users/rmadan/a8-fe/node_modules
        using description file: /Users/rmadan/a8-fe/package.json (relative path: ./node_modules)
          Field 'browser' doesn't contain a valid alias configuration
        after using description file: /Users/rmadan/a8-fe/package.json (relative path: ./node_modules)
          using description file: /Users/rmadan/a8-fe/package.json (relative path: ./node_modules/angular)
            no extension
              Field 'browser' doesn't contain a valid alias configuration
              /Users/rmadan/a8-fe/node_modules/angular doesn't exist
            .ts
              Field 'browser' doesn't contain a valid alias configuration
              /Users/rmadan/a8-fe/node_modules/angular.ts doesn't exist
            .js
              Field 'browser' doesn't contain a valid alias configuration
              /Users/rmadan/a8-fe/node_modules/angular.js doesn't exist
            as directory
              /Users/rmadan/a8-fe/node_modules/angular doesn't exist
      looking for modules in /Users/rmadan/root-platform/node_modules
        using description file: /Users/rmadan/root-platform/package.json (relative path: ./node_modules)
          Field 'browser' doesn't contain a valid alias configuration
        after using description file: /Users/rmadan/root-platform/package.json (relative path: ./node_modules)
          using description file: /Users/rmadan/root-platform/package.json (relative path: ./node_modules/angular)
            no extension
              Field 'browser' doesn't contain a valid alias configuration
              /Users/rmadan/root-platform/node_modules/angular doesn't exist
            .ts
              Field 'browser' doesn't contain a valid alias configuration
              /Users/rmadan/root-platform/node_modules/angular.ts doesn't exist
            .js
              Field 'browser' doesn't contain a valid alias configuration
              /Users/rmadan/root-platform/node_modules/angular.js doesn't exist
            as directory
              /Users/rmadan/root-platform/node_modules/angular doesn't exist
      looking for modules in /Users/rmadan/root-platform/src
        using description file: /Users/rmadan/root-platform/package.json (relative path: ./src)
          Field 'browser' doesn't contain a valid alias configuration
        after using description file: /Users/rmadan/root-platform/package.json (relative path: ./src)
          using description file: /Users/rmadan/root-platform/package.json (relative path: ./src/angular)
            no extension
              Field 'browser' doesn't contain a valid alias configuration
              /Users/rmadan/root-platform/src/angular doesn't exist
            .ts
              Field 'browser' doesn't contain a valid alias configuration
              /Users/rmadan/root-platform/src/angular.ts doesn't exist
            .js
              Field 'browser' doesn't contain a valid alias configuration
              /Users/rmadan/root-platform/src/angular.js doesn't exist
            as directory
              /Users/rmadan/root-platform/src/angular doesn't exist
      looking for modules in /Users/rmadan/root-platform/src
        using description file: /Users/rmadan/root-platform/package.json (relative path: ./src)
          Field 'browser' doesn't contain a valid alias configuration
        after using description file: /Users/rmadan/root-platform/package.json (relative path: ./src)
          using description file: /Users/rmadan/root-platform/package.json (relative path: ./src/angular)
            no extension
              Field 'browser' doesn't contain a valid alias configuration
              /Users/rmadan/root-platform/src/angular doesn't exist
            .ts
              Field 'browser' doesn't contain a valid alias configuration
              /Users/rmadan/root-platform/src/angular.ts doesn't exist
            .js
              Field 'browser' doesn't contain a valid alias configuration
              /Users/rmadan/root-platform/src/angular.js doesn't exist
            as directory
              /Users/rmadan/root-platform/src/angular doesn't exist
[/Users/rmadan/node_modules]
[/Users/node_modules]
[/node_modules]
[/Users/rmadan/a8-fe/node_modules/angular]
[/Users/rmadan/a8-fe/node_modules/angular.ts]
[/Users/rmadan/a8-fe/node_modules/angular.js]
[/Users/rmadan/a8-fe/node_modules/angular]
[/Users/rmadan/root-platform/node_modules/angular]
[/Users/rmadan/root-platform/node_modules/angular.ts]
[/Users/rmadan/root-platform/node_modules/angular.js]
[/Users/rmadan/root-platform/node_modules/angular]
[/Users/rmadan/root-platform/src/angular]
[/Users/rmadan/root-platform/src/angular.ts]
[/Users/rmadan/root-platform/src/angular.js]
[/Users/rmadan/root-platform/src/angular]
[/Users/rmadan/root-platform/src/angular]
[/Users/rmadan/root-platform/src/angular.ts]
[/Users/rmadan/root-platform/src/angular.js]
[/Users/rmadan/root-platform/src/angular]
 @ ../a8-fe/app.js 2:0-30
 @ ./src/app/app.component.ts
 @ ./src/app/app.module.ts
 @ ./src/main.ts
 @ multi webpack-dev-server/client?https://root.localhost:4200 ./src/main.ts

It seems like it was only looking in a8-fe/node_modules, but angular was in a8-fe/bower_components. Is this because my angular 4 root app uses npm? If I could get it to look in bower_components it might fix a lot of my problems.

I was able to solve this error by doing npm install angular on the root app.
It compiles and then the next error I get is in the console and says it can't find a dependency in my child app.

Uncaught Error: 'analytics' died in status NOT_MOUNTED: [$injector:modulerr] Failed to instantiate module analytics due to:
Error: [$injector:modulerr] Failed to instantiate module environment due to:
Error: [$injector:nomod] Module 'environment' is not available! You either misspelled the module name or forgot to load it. If registering a module ensure that you specify the dependencies as the second argument.

This is my child app's initial module file:

angular
  .module('analytics', [
    'analytics.f4',
    'environment',
    'ngActionCable',
    'ngInflection',
    'ngResource',
    'ngSanitize',
    'ui.router'
  ]);

angular.module('analytics.f4', []);

I tried doing npm install angular-environment on my root app again, but this didn't fix the problem.
I tried doing import 'angular-environment and it worked and the next dependency error showed.

(On a side note, when I run the child app alone, import complies to require and it errors because require isn't defined on a browser. And ideally, I want it to be able to run on it's own too.)

I did that for all the dependencies and it didn't error, but the html container for my child app is empty.

<div id="analytics"><div id="__single_spa_angular_1" class="ng-scope"><!-- uiView: --><div ui-view="" class="ng-scope"></div></div></div>

I'm not sure if what I did is the right way to do it. If I have to do the import, I'd rather just import from the bower_components folder.

From looking your and others comments on your article (specifically https://medium.com/@joeldenning/raquel-lira-thats-a-great-question-by-default-webpack-assumes-one-big-webpack-config-for-d31b545fe776), it seems like if my root app uses webpack, all the dependencies needed for the child apps need to go into the root app (because of the way webpack works). Or I have to use one of the two methods you mention in the link above.

Update:

I think the html is empty because ui-router isn't working correctly. I should have content on '/dashboards'. I added import './app/app.states.js'; the initial states file/router config to app.js and import './app/modules/dashboard/dashboard.states.js;', but it still doesn't go into the dashboard state.

@joeldenning
Copy link
Member

@rivamadan Thanks for the detailed description of what's going on. It sounds like there is a lot going on but that part of the problem is some confusion over node_modules vs bower_components and when you should use which one. If I understand correctly (please correct where wrong), you are using angular-cli to build both the root application and the child applications, which is using webpack underneath the hood. And webpack doesn't work with bower_components by default.

I would recommend looking into only using node_modules instead of bower_components, even for the AngularJS child application. Bower is pretty outdated now (even their website suggests switching to webpack), so it might be worth moving off of it. If the angularjs child application is not a new project and is already deep into bower, it might not be worth moving off of bower, though. If that's the case, you could try to get webpack to work with bower (see old webpack docs). But, by default, webpack does not ever look into bower_components when creating the bundle. That's why you have had to npm install all of the packages and then import them in your code (import 'angular-environment') -- none of the bower_components are being loaded into the webpack bundle.

Let me know if you have other questions or if there's something besides the bower_components/node_modules problem that you'd like me to help out with!

@joeldenning
Copy link
Member

That's a good question - the reason why I import the routes file inside of app.js is because that's the way to tell webpack to include it in the bundle. Any javascript file that is not imported either by the webpack entry file or any of the files imported by the entry file will not be included in the final bundle. This makes AngularJS code be in an interesting situation with modern bundlers -- AngularJS assumed you script tagged each file individually and it does it's own module/dependency-injection stuff. Now that bundlers are a thing, though, AngularJS' module system and dependence on global variables can sometimes feel clunky when using modern tools like webpack.

@rivamadan
Copy link

So I see that the 'route.js' file imports 'root.component.js' and then the 'root.component.js' imports './root.template.html'. Does this mean to get AngularJS to work we have to import every file so it ends up in the bundler?

If I don't use angular-cli to build the root application, could I still get child apps that use webpack/angular-cli to work?

@joeldenning
Copy link
Member

Does this mean to get AngularJS to work we have to import every file so it ends up in the bundler?

For angularjs, yes you need to import every file so it ends up in the bundler. You could look into a webpack configuration or plugin that allows you to auto-include every file in a directory into the bundle, but as far as I know you have to import every file individually to get it into the bundle.

If I don't use angular-cli to build the root application, could I still get child apps that use webpack/angular-cli to work?

Yes, you can (although it is not something I have done myself). If you eject the angular-cli child applications into their own webpack bundles, then you can use a master webpack config (or a module loader like SystemJS) for the root app to load the child applications. And that master webpack config for the root app does not need to use angular-cli.

The idea is that each child application produces a javascript bundle loaded by the root application. But the root application itself doesn't use or need a javascript framework like Angular at all -- instead it just knows how to load the child application code into the browser and then single-spa takes care of mounting/unmounting the child applications.

Let me know if that doesn't make sense - I'm happy to try and help. https://github.com/joeldenning/simple-single-spa-webpack-example is a good place to start - note that the root application does not use angular-cli but that the child applications could use angular-cli if they wanted to

@rivamadan
Copy link

rivamadan commented Nov 1, 2017

If you eject the angular-cli child applications into their own webpack bundles, then you can use a master webpack config (or a module loader like SystemJS) for the root app to load the child applications.

If I have all my child applications use webpack and my root application uses webpack, do all the child applications need to have their dependencies in the root application? From previous comments it sounds like that is the case. If that's the case, what's the point of having a webpack bundle for each child application? Sorry, I'm new to webpack so maybe that's why I'm not understanding.
If SystemJS allows child applications to have their own dependencies, do you have any examples of this?

If all dependencies need to go in the root, the child applications can't have different versions of dependencies? Although I did some searching and I think yarn allows for different versions.

Update
I was testing it out and it seems like I can import packages that are only in my child applications package.json app and not my root.

@joeldenning
Copy link
Member

Reopening since there is still discussion going on here and it's clearly a topic that a lot of people are interested in.

@joeldenning
Copy link
Member

@rivamadan I'll respond to your questions tomorrow

@joeldenning
Copy link
Member

@rivamadan apparently tomorrow meant "in four days" :)

Your questions are good ones. Child applications do not need to have all of their dependencies in the root application. They can have their own package.json and webpack config. Which means they can have their own versions of dependencies.

You can do that with both SystemJS or webpack for the root application, although the implementation details are different depending on which one you choose. If you use webpack for the root application, you could publish the child applications as npm packages who have no dependencies and whose main file is a webpack bundle with everything pre-bundled. If you use systemjs, you would do much the same except you don't have to publish the child applications to npm -- instead you can just deploy them to a web server and have SystemJS load them into the browser.

Note that neither https://github.com/joeldenning/simple-single-spa-webpack-example nor https://github.com/CanopyTax/single-spa-examples do this. Instead, they use one package.json for the root application that is shared for all child applications. Also note that #126 is tracking the work to create another example project that will show how to have multiple package.jsons (one for the root app and each of the child apps)

Does that answer your questions? Keep more questions coming -- I'm happy to help try and answer and also this is good feedback for me as I rework the docs and examples.

@rivamadan
Copy link

Yes, that answers those questions. Thanks a lot!

There are a few things I want to understand further though.
I currently have a root angular 4 with angular-cli app and a child angular 4 with angular-cli app. They both have their own dependencies in package.json and a angular-cli config file. I start the server in the root app with ng serve and everything seems to be working. If I change something in the child application, the server updates.

However, when I change something in the child's angular-cli config file it doesn't look like it does anything. It seems like the root application angular-cli config file is the only one that is being recognized. Currently this doesn't matter to me, but I might eventually have to add another child app that could have something specific in the webpack config. I was just wondering if you knew why this is happening and if this is fixed with the method you mentioned above (publish to npm). In a way, I'm wondering why is it necessary to publish the child apps to npm? It seems like it is working for the most part without publishing to npm.

I was also looking at #123. You mention:

you can also achieve independent deploys for child applications by swapping out the old child application bundle with a new one on your server.

Is this what is happening the way I'm running the app right now?

@joeldenning
Copy link
Member

I currently have a root angular 4 with angular-cli app and a child angular 4 with angular-cli app

This actually confuses me a little bit -- why does your root app need Angular at all? In general, the root application is usually very very small and just calls singleSpa.declareChildApplication and singleSpa.start (no other framework necessary). See explanation here

I'm wondering why is it necessary to publish the child apps to npm?

It's not necessary to publish the child apps to npm if you have a monorepo where the root app and all child apps live, or if you use a module loader like SystemJS. It would become necessary if you're only using webpack and have separate git repos for the root app and each child app. If everything is in one git repo, you can get away without publishing to npm, but you need to make sure that the root app is importing the child application webpack bundle (not just the source file). Otherwise your child app webpack configs are not doing anything.

Is this what is happening the way I'm running the app right now? (deployments by swapping out old child app bundle with new one on your server)

Are you talking about when you're running the app locally? My comment that you quoted was related to deployments, not for local development. I'm not sure exactly how you're running your app right now, so I'm not sure I'll be able to answer your question definitively. What I can say though is that statement you quoted was not referring to local development, but rather deployments.

@rivamadan
Copy link

rivamadan commented Nov 7, 2017

The reason I made my root application Angular is because there are a few things I want all the applications to share. I have a shared top nav bar, which includes a panel that displays certain data that should accessed in each child app. I also want one authentication system for all the child apps (gets token and puts it into local storage for child apps to access). Is there a reason not to do this for the root application?

I tried following https://github.com/CanopyTax/single-spa-examples with JSPM/SystemJS, but I ran into an issue:
If I tried to import from a different repo/sibling folder, such as SystemJS.import('../../child-app/app.js'), it didn't recognize it.


Update:
We are have decided for now to have the child applications be dependencies in the root app's package.json using git urls. For development we are using npm link.

I talked to my devOps team and they want to use SystemJS and import from URLs actually. (When I first brought this up with my manager, he thought it was overcomplicated and was against it, but I don't think he really understand the difference.) Also, the way I was doing it didn't allow AoT or upgrading to angular 5 (gave me the error "A platform with a different configuration has been created. Please destroy it first").

@joeldenning
Copy link
Member

Closing for now -- feel free to reopen with further questions or discussion

@TombolaShepless
Copy link

TombolaShepless commented Jan 11, 2018

@joeldenning I didn't want to make a new issue for this as this covers the conversation quite closely. I was wondering how you guys handle assets if host and child applications live on different servers? For example, if the host app lives on domain project.com but a child app lives on domain another-project.com then requests for images within CSS (in the child app) will fail as the browser will request them from the project.com domain. Would you handle this in the Webpack config by setting the "absolute path" for images? Sorry for the rambling, I'm just curious as we are looking to use this at work.

If you publish the host application and child applications on the same server I guess it answers itself really....

@michael-robison
Copy link

Along with @TombolaShepless, I am also having difficulty with loading child app assets. Any help would be appreciated.

@skarjalainen
Copy link

I just want to share one thing that fixed multiple webpack config chunk loading issue. Webpack uses function called "webpackJsonp" to load chunks trough window namespace. Configure each webpack to use different function name using output.jsonpFunction config.

config: { output: { jsonpFunction: 'webpackJsonp_APPNAME_HERE' } }

@joeldenning
Copy link
Member

@TombolaShepless
Apologies for the very slow response. We actually do have separate servers for the html file versus the applications themselves. You can use webpack's publicPath option to configure what server to request CSS from (and other files like code split bundles). Have you tried changing publicPath to be the another-project.com domain?

@joeldenning
Copy link
Member

@skarjalainen ooo I've noticed that before but didn't know how to fix it. Thanks!

@Shepless
Copy link

Shepless commented Jan 28, 2018

@joeldenning Thanks for replying, that makes sense. Sorry (again) if this is a silly question, but how do you handle staging/production domains in publicPath? At the moment we are using Octopus Deploy which takes a single build output and it pushes it to different environments. At CanopyTax, do you run a build for staging and then another (new build) for production so you can set publicPath using an env var or something? Hopefully my (crude) diagram below explains what I mean a bit better:

(Your setup?)

stage build ---> stage server
prod build ---> prod server

OD setup

build ---> artifacts ---> OD ---> stage server
OD (using stage artifacts) ---> prod server

An aside question - do you get any memory pressure over a long period of use with multiple child apps being mounted/unmounted? I found that Vue and VueRouter (especially) leave a lot of refs hanging around even when destroyed. Was just curious as our product is mobile first which comes with its own set of difficulties!

@joeldenning
Copy link
Member

@Shepless good questions

how do you handle staging/production domains in publicPath?

By setting the publicPath dynamically at runtime. You can do so with __webpack_require__.p = 'https://staging-env.mydomain.com/' inside of your bundle (not inside of the webpack config). Doing a separate build for each environment would also work, too, though.

An aside question - do you get any memory pressure over a long period of use with multiple child apps being mounted/unmounted? I found that Vue and VueRouter (especially) leave a lot of refs hanging around even when destroyed. Was just curious as our product is mobile first which comes with its own set of difficulties!

Memory leaks are definitely a thing in javascript and something to watch out for in all single page applications. There are maybe three parts to answering your question: the memory single-spa uses, the memory that applications use (usually framework level stuff inside of Vue, etc), and just the memory that a large single page application uses

single-spa memory usage:
Single-spa itself actually has a very light memory footprint (barring any bugs I'm unaware of). The main memory it holds onto are references to the application objects (see here and here), which are very very small. Other than that, single-spa is (afaik) memory-safe and cleans everything up as applications are mounted/unmounted/remounted.

framework memory usage in applications:
This is trickier to speak definitively about, since there are so many of them and I'm not the maintainer of them. In general, though, having multiple instances of Vue/React/Angular ideally does not cause a substantial increase in memory consumption versus one large instance. This is because whether your components are split up into separate single-spa applications or not, there are still (roughly) the same number of components.

On a more practical, real-world level, I'm sure there are some unfortunate memory leaks within React/Angular/Vue, with some being exacerbated by mounting multiple instances of the framework and then unmounting regularly. I cannot speak confidently about the specifics of all the frameworks, but I can say that afaik there aren't substantial memory leaks in React, which is what we primarily use at Canopy. I have much less experience with Vue, though.

memory usage of large single page applications:
Regardless of whether you use single-spa, other frameworks, or any js library, a single page application will inevitable use more and more memory as you write more code. To be clear, I'm not talking about memory leaks, but rather that more code means the browser has to allocate more memory to even hold that code in memory, store its variables, execute it, etc. This is something that you cannot get around except by splitting up your single page application into separate page loads. In a mobile-first application, I'm not sure how much js code is too much (where the mobile browser has trouble keeping up). Or how much css is too much. This is a broad subject well beyond the scope of single-spa itself, but definitely worth thinking about, researching, and testing if you want to get truly serious about very large single page applications being run on mobile devices.

Does that help? Let me know if you have any more questions about single-spa memory consumption

@Shepless
Copy link

Shepless commented Jan 30, 2018

@joeldenning brilliant answer bud, thank you!

Thanks for the insights on setting the publicPath dynamically. Not so keen on the __webpack_require__.p approach, feels a little on the hack side and might be prone to whimsical changes from webpack itself. Since the builds are deterministic we will probably go with separate builds (more time but it feels safer I guess).

I was wondering more about the "framework memory usage in applications" aspects, and just wondered what Canopy Tax's experience had been with this to date. From my experimentation everything LGTM (apart from Vue) but nothing beats real world experience so thought it best to ask. I guess this might be a nudge in Reacts favour but will have a play and see what the specifics are like first.

Thanks for your help - really appreciated 👍 I promise I won't revive this issue again 😆

@lukalulu
Copy link

@mbanting hi, I also encountered the same problem, can you tell me finally you solved it? How to solve it?

@mbanting
Copy link
Author

mbanting commented Oct 4, 2018

@lukalulu we are setting __webpack_public_path__ to the path where additional additional chunks can be retrieved. It was important for us to be able to define this at runtime, which is why we went this route. https://webpack.js.org/guides/public-path/#on-the-fly

@rkolec
Copy link

rkolec commented Nov 1, 2018

Hi! You guys did a great job! Could be so kind and help me in my case:

I have two applications: root and child. Both have separate git repos and are served on separate servers. Both are based on create-react-app. As they use the same versions of libraries, i.e:, react, I excluded it from the bundle of the child application. When I try load the child (libraryTarget: 'amd') application with SystemJS, I get the error:

Uncaught Error: Application 'childCraApp' died in status LOADING_SOURCE_CODE: Unexpected token <    Evaluating http://localhost:3000/react    Loading /child-cra-app/singleSpaEntry.js

In this case how common libs should be provided? Right now react left in root application bundle.

I've created repo with this example: https://github.com/rkolec/single-spa-create-react-app-demo

@Beej126
Copy link

Beej126 commented Feb 15, 2019

wanted to submit a fresh working SystemJS based sample since this took me a bit to get my head around the specifics given SystemJS and everything else has evolved enough for errors to be confusing based on the slightly dated sample code in these threads... not a dig, just the reality of how everything is moving forward.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests