Reputation: 491
We are creating an MFE angular app using Module Federation WebPack 5 and ended up in an issue with the image source path. When we run the MFE alone, the image is loading(localhost:5000/assets/../angular.png
) but when we run the host/shell app, the image is not loading for MFE since MFE is running in a different port(5000) and Shell is running in a different port(4200) and the app is trying to access the image from Shell's asset folder(localhost:4200/assets/../angular.png
) when we run the shell app.
We have two options in hand:
We have used the below sample for my testing but in the below sample the angular.png file is available in both MFE1 and Shell but if we remove it from Shell, it won't work when we load the shell app.
Source Reference Code Sample (thanks to @manfredsteyer)
Any other solution in MFE to resolve this?
Upvotes: 7
Views: 6546
Reputation: 1597
I found a few solutions for this problem. First we need to understand the main issue: let's say the shell app is running on localhost:3000, and the remote app is running on localhost:3001. In the remote app you can use images in two ways:
<img src="/assets/image.png">
.img { background-image: url(/assets/image.png) }
When you deploy the remote app and run it directly (on localhost:3001) it will correctly resolve both links to image to url localhost:3001/assets/image.png, and image will show.
But when you load the remote app in the shell app (on localhost:3000), the shell app will inject the remote html and css code inside the host DOM, and all links will be resolved relatively to the shell app url, which means on localhost:3000/assets/image.png (where the image does not exist.
The solution is we have to modify the remote app code to output absolute url to images:
<img src="http://localhost:3001/assets/image.png">
.img { background-image: url(http://localhost:3001/assets/image.png) }
If you know URL where the remote app will be deployed in advance, the easiest solution is to set deployUrl
option in angular.json file (if remote app is Angular):
"build": {
"options": {
"deployUrl": "http://localhost:3001"
...
},
"configurations": {
"production": {
"deployUrl": "https://production.server.com"
...
}
}
With this option angular compiler will automatically convert all relative links to absolute (=all links to images in compiled HTML and CSS code will start with http://localhost:3001). When the remote app is loaded inside the shell app the images will be loaded from the correct locations.
This is not a good solution when we need to build a project that can be deployable on any server, because this compiled code has absolute URLs hard coded and can only be deployed on the particular server. Another dissadvantage is if our remote app uses a lot of images, because hard-coded absolute links will make html/css files larger. Maybe in that case we can optimize the deployment process and deploy all the remote app assets to the shell server, so relative links will still work.
I think currently the most elegant solution to make the remote app deployable anywhere (not having to set the deployment url in advance) is to use dynamic link resolvement in our HTML and CSS code. For that a special global javascript variable __webpack_public_path__
comes handy. This variable is automatically defined by webpack in the project compilation process, and contains absolute URL to deployed server (that we can configure statically in advance or can be resolved dynamically at run-time based on browser URL address from where we loaded the remote app (=I think it gets resolved in the remote app remoteEntry.js
, where the client-side JS code is aware of the absolute URL where it got downloaded from).
So, we can dynamically resolve absolute links to images in our remote app like this:
Angular component TS file:
@Component({
templateUrl: './page1.component.html',
})
export class Page1Component {
assetUrl = __webpack_public_path__;
}
Angular component HTML template:
<img src="{{assetUrl}}image.png" />
If we have images inside our CSS files the solution is quite more tricky. Angular is possible to dynamically inject variables in CSS at run-time using @HostBinding
decorator, but is not very convenient solution especially for injecting URL's for images:
Angular component TS file that will dynamicall handle it's CSS file:
@Component({
styleUrls: ['./page1.component.scss'],
})
export class Page1Component {
// CSS variable --imageUrl will be passed to CSS style declared in page1.component.scss.
@HostBinding("style.--imageUrl") imageUrl = `url('${__webpack_public_path__}image.png')`;
}
CSS file:
.img {
background-image: var(--imageUrl);
}
Disadvantage of this solution is that we must declare CSS variable for every different image we want to inject in the CSS. It would be much easier if we could just declare a base url path variable in TS file, and in CSS to append it to our image filename, like: background-image: url(var(--imageBaseUrl)/image.png);
. This would throw compilation error because angular compiler (or mroe specifically css-loader in webpack, used by angular compiler) wants to resolve every url()
function it finds in CSS files, and it won't understand how to resolve url(var(--imageBaseUrl)/image.png)
(it will throw syntax error). I could not find a way we could tell the compiler to ignore this cases, css-loader is always used and we cannot configure it properly.
If you use many images in CSS files there is also a more elegant solution: put all styles that use images in your "global-remote" CSS file, where you can use relative links to images. When you deploy your remote app, browser will load the globa-remote CSS from http://localhost:3001/global-remote.css, and browser will automatically resolve all relative links relatively to localhost:3001. But for this to work you cannot use "scoped" CSS files from angular (=CSS files that are attached to angular component), because scoped CSS styles are compiled in JS code together with the component and are dynamically injected at-runtime to html DOM, so relative links to image will be resolved relatively to the shell app localhost:3000, not to the remote app). You have to compile this kind of CSS file separately, and include in your remote app separately:
To compile "remote-global" CSS separately, configure it in angular.json:
"build": {
"options": {
"styles": [
{
"input": "src/app/remote-global.scss",
"bundleName": "remote-global" // Tell compiler to compile our style to remote-global.css
}
],
...
Include this CSS in our remote app main App component (app.component.html
):
<link [rel]="'stylesheet'" [href]="{{assetUrl}}remote-global.css">
Make sure also [rel]
attribute is in brackets otherwise angular compiler will try to resolve href in compile-time and will throw syntax error. Compiler must ignore this <link>
tag that must be resolved at run-time.
In app.component.ts
define assetUrl variable that will resolve to absolute url of deployed remote app, like mentioned before:
@Component({
templateUrl: './app.component.html',
})
export class AppComponent {
assetUrl = __webpack_public_path__;
}
Upvotes: 3
Reputation: 431
The problem is that the images are imported using relative paths. Because the micro frontends are always loaded inside the shell, these paths will reference the shell's assets. To fix the issue you would need to add the complete URL to the image to load it from the correct micro frontend.
If you want to load an image in the micro frontend it should look like this:
<img src="http://localhost:5000/assets/image.png" />
To adjust the base url for production or development, you could write a method in a service that you can inject into your componentes. It could look something like this:
public getImageBaseUrl(): string {
if (environment.production) {
return "http://someurl.com/assets/";
} else {
return "http://localhost:5000/assets/";
}
}
Then you can call the method inside your HTML-template:
<img [src]="getImageBaseUrl() + 'image.png'" />
Upvotes: 8