43

The background

I'm using Angular CLI to build a project (with multiple apps). I want to publish the apps on separate sub-paths on my domain, like example.com/apps/app1/.

If I set the --base-href parameter to /apps/app1/ it solves any issues regarding the router, and it will load the assets (JS, CSS, and images etc) just fine.

If I use the Location service, I can use

this.location.prepareExternalUrl('/assets/data/data.json')

to resolve dynamically loaded assets (they will resolve to /apps/app1/assets/data/data.json).

So far so good. But I now want to serve the app assets through a CDN, such as cdn.example.com, while hosting the app itself on the original URL example.com/apps/app1/`. So now I build the app using:

 ng build -prod --app app1 --base-href "/apps/app1/" --deploy-url "http://cdn.example.com/app-assets/apps/app1/"

This time, I apply both the --base-href and --deploy-url parameters. It works great in that it uses the base-href to help the Router resolve the URL and it loads the js and CSS files from the CDN. It also resolves the image URL references in the CSS files using the CDN URL.


The problem

When loading images or data from the assets folder dynamically (in a service or template), I can't find a good way for it to resolve the URLs using the deploy-url configuration.

If I use the Location service, it still uses the base-href to resolve URLs, so

this.location.prepareExternalUrl('/assets/data/data.json')

will still resolve to /apps/app1/assets/data/data.json instead of http://cdn.example.com/app-assets/apps/app1/assets/data/data.json.

I would have expected it to use the deploy-url value if one is defined, especially since that would be a general solution that would work when hosting the files on the same domain and when hosting the files on an external domain.


The question

Is there a way to resolve the asset URLs considering both the base-href and the deploy-url parameters?

Ideally an official Angular function like Location.prepareExternalUrl, but if I can get the base-href and deploy-url parameters from Angular in some way, I could build my own service for it.

I would not want to define the URLs in the environment config since:

  1. It would require specific environment configs per app
  2. It creates a potential conflict with the values that are supplied when building the app.
BinaryButterfly
  • 18,137
  • 13
  • 50
  • 91
david.emilsson
  • 2,313
  • 1
  • 20
  • 24
  • 1
    I'll try or remove completly base tag or Replace < base href="/" > with < script >document.write('');< /script > – Eliseo Dec 19 '17 at 14:30
  • @Eliseo the base-href is needed for the router to work in a subfolder, and it doesn't look like it's interfering here. It's rather the apparent lack of access to the deploy-url value that's the bother. After looking at the [source](https://github.com/angular/angular/blob/bebedfed24d6fbfa492e97f071e1d1b41e411280/packages/common/src/location/path_location_strategy.ts#L73) of the `Location.prepareExternalUrl` function, it's clear that it *only* looks at the `base-href`. Removing the base tag won't help me, I'm afraid... – david.emilsson Dec 19 '17 at 15:10
  • And replace with the script? I think you must to use hashtag Strategies – Eliseo Dec 19 '17 at 17:23
  • @Eliseo hashed strategies won’t help either since everything works except getting a reliable path to the external sources when loading them in templates or services. The js and css files in the index.html file are referenced correctly (and I’m currently using an ugly hack reading the url from those script tags). – david.emilsson Dec 19 '17 at 22:04
  • did u ever find out what to do? – hammies Dec 31 '17 at 18:35
  • 2
    @JamAndJammies As far as I've gathered, it's not currently supported. As a temporary workaround, I'm looking for the main.bundle.js script tag in the document and extracting the path from there. I will replace that code in an instant as soon as we get a reasonable way to deal with this. – david.emilsson Jan 16 '18 at 10:21
  • have you tried `https://angular.io/api/common/APP_BASE_HREF` – Robert Sep 05 '18 at 21:16
  • if I understand your question correctly, can't you use `window.location.origin + /app-asset + this.location.prepareExternalUrl('/assets/data/data.json')` ? – Binbo Mar 14 '19 at 03:56
  • @Binbo The question concerns the case when the deploy-url is set to an external domain, i.e. a different origin than the site origin. In my case, the site is hosted and distributed via a CDN, while the index.html page is proxied through a server on a different url. To take advantage of the properties of the CDN, I don't want to proxy all assets but rather reference the assets directly from the CDN. – david.emilsson Mar 14 '19 at 09:06

5 Answers5

7

To access --deploy-url value at application runtime, create deploy-url.ts with:

export const DEPLOY_URL = new InjectionToken<string>('deployUrl');

And use this snippet in your main.ts file:

const deployUrl = (function() {
  const scripts = document.getElementsByTagName('script');
  const index = scripts.length - 1;
  const mainScript = scripts[index];
  return mainScript.src.replace(/main.*?\.js$/, '');
})();

const DEPLOY_URL_PROVIDER = {
  provide: DEPLOY_URL,
  useValue: deployUrl,
};

platformBrowserDynamic([DEPLOY_URL_PROVIDER])
  .bootstrapModule(AppModule)
  .catch(err => console.error(err));

The idea is to get the url of currently executed Javascript file, which is main.js (or main.hash.js if outputHashing is enabled) and strip filename from it. Then in your services inject --deploy-url value with @Inject(DEPLOY_URL) deployUrl: string as a constructor parameter.

  • 2
    Dirty but works, this issue is well known and tracked in https://github.com/angular/angular-cli/issues/6666 – soyuka Oct 07 '19 at 09:30
4

Here is how I solve this "BS"-problem. When your app is for e.g. hosted under www.domain.de/hehe.

  1. in angular.json add "deployUrl": "./",
  2. in src/index.html inside <head> add/update <base href="./">
  3. throughout your app you will use relative paths for assets and whatnot: <img src="./assets/img/pictureOfMyCrazyWife.png">
  4. just run ng build or ng build --prod conveniently.

with those simple steps you have a general setup. So it will work no matter if its www.domain.de/hehe, www.domain.de/hehe2, www.domain.de/ or www.domain.de/lol

Andre Elrico
  • 10,956
  • 6
  • 50
  • 69
  • 1
    this solution seems really reasonable. 1) Is there any caveat? 2) Why is it not the default way used by the angular community? – jeromerg Feb 01 '21 at 21:21
  • 1
    This did not work for me. I'm trying to deploy the app in a `www.domain.com/directory` and that works when I set the base-href to `directory`, but then images don't load. Not sure what's going on. – Tony Brasunas Jun 30 '21 at 06:09
0

I ran into this exact same situation, and as you outlined in The Background, building with both the base-href and deploy-url set does work in that we're able to serve up our css and js files from a CDN while hosting the application on another server.

As a solution to dynamically loaded assets in our templates, we wrote an API that delivers environment variables for this purpose which delivers the appropriate URLs needed per deployment.

DevMike
  • 1,630
  • 2
  • 19
  • 33
0

Angular 14.2+ (2022)

You can use NgOptimizedImage directive to load images from CDN URL. The most basic set-up using environments files with different URL for different environments is below:

// app.module.ts
import { NgOptimizedImage } from '@angular/common';
...
providers: [{
    provide: IMAGE_LOADER,
    useValue: (config: ImageLoaderConfig) => {
      return `${environment.imageURL}${config.src}`;
    },
}],
...

// component.html
<img ngSrc="assets/images/image.svg" width="100" height="100"/>
Valera Tumash
  • 628
  • 7
  • 16
  • While it helps in _applying_ the URL to an image, this solution doesn't solve the root issue, which is to get access to the deployment URL dynamically instead of hardcoding it in the environment files. – david.emilsson Mar 15 '23 at 10:01
-2

Here is how I have done for few projects.

Set the APP_BASE_HREF using a dynamic value set by server E.g. Cookie and SPA reads it on init. getBaseUrl function simply read a cookie value.

{ provide: APP_BASE_HREF, useFactory: () => getBaseUrl() }

Set the deployUrl as part of your CI process. You need to set the BUILD and VERSION dynamically. This can be done by using a simple shell script.

https://my-cloud.cloudfront.net/my-app/{BUILD}/{VERSION}/
fernando
  • 707
  • 8
  • 24