4

はじめに

私は、この質問がさまざまな形で尋ねられてきたことを知っています。しかし、私はすでにその問題について見つけたすべての投稿と記事を読みましたが、私たちが直面している前提条件で私の問題を解決できませんでした. したがって、最初に私たちの問題を説明し、動機付けをしたいと思います。その後、私たちがすでに試した、または使用しているさまざまな方法について説明します。

問題/動機

私たちのセットアップはやや複雑であると言えます。Angular アプリケーションは複数の Kubernetes クラスターにデプロイされ、さまざまなパスから提供されます。

クラスター自体はプロキシの背後にあり、Kubernetes 自体が Ingress という名前のプロキシを追加します。したがって、一般的なセットアップは次のようになります。

ローカル

http://localhost:4200/

ローカル クラスタ

https://kubernetes.local/angular-app

発達

https://ourcompany.com/proxy-dev/kubernetes/angular-app

演出

https://ourcompany.com/proxy-staging/kubernetes/angular-app

お客様

https://kubernetes.customer.com/angular-app

アプリケーションは、常に異なるベースパスを念頭に置いて提供されます。これは Kubernetes であるため、アプリは Docker コンテナーでデプロイされます。

古典的にはng build、Angular アプリをトランスパイルしてから、たとえば次のような Dockerfile を使用します。

FROM nginx:1.17.10-alpine

EXPOSE 80

COPY conf/nginx.conf /etc/nginx/conf.d/default.conf
RUN rm -rf /usr/share/nginx/html/*
COPY dist/ /usr/share/nginx/html/

base-href を設定する方法とアセットが提供される場所を CLI に伝えるには、Angular CLI のオプションを使用--base-hrefします。--deploy-url

残念ながら、これは、アプリケーションをデプロイするすべての環境に対して Docker コンテナを構築する必要があることを意味します。

environments[.prod].tsさらに、このビルド前のプロセスでは、たとえばfor every デプロイメント環境内など、他の環境変数を設定するように注意する必要があります。

現在のソリューション

私たちの現在のソリューションは、展開中にアプリを構築することで構成されています。これは、nginx-container が開始される前に実行される、いわゆるinitContainerとして使用される別の Docker-container を使用することで機能します。

この Dockerfile は次のようになります。

FROM node:13.10.1-alpine

# set working directory
WORKDIR /app

# add `/app/node_modules/.bin` to $PATH
ENV PATH /app/node_modules/.bin:$PATH

# install and cache app dependencies
COPY package*.json ./

RUN npm install

COPY / /app/

ENV PROTOCOL http
ENV HOST localhost
ENV CONTEXT_PATH /
ENV PORT 8080
ENV ENDPOINT localhost/endpoint
VOLUME /app/dist

# prebuild ES5 modules
RUN ng build --prod --output-path=build
CMD \
  sed -i -E "s@(endpoint: ['|\"])[^'\"]+@\1${ENDPOINT}@g" src/environments/environment.prod.ts \
  && \
  ng build \
  --prod \
  --base-href=${PROTOCOL}://${HOST}:${PORT}${CONTEXT_PATH} \
  --deploy-url=${PROTOCOL}://${HOST}:${PORT}${CONTEXT_PATH} \
  --output-path=build \
  && \
  rm -rf /app/dist/* && \
  mv -v build/* /app/dist/

このコンテナーを Kubernetes デプロイメントにinitContainerとして組み込むことで、正しい base-href、deployment-url を使用してアプリをビルドし、このアプリがデプロイされている環境に応じて特定の変数を置き換えることができます。

このアプローチは、いくつかの問題を除いて完全に機能します。


  1. 1 回のデプロイには数分かかります アプリが再デプロイされるたびにinitContainerを実行する必要があります。もちろん、これは常にng buildコマンドを実行します。コンテナーが以前のディレクティブng buildでコマンドを実行してこれらをキャッシュするたびに ES モジュールをビルドするのを防ぐため。 とはいえ、差動装填用の建屋やターサーなどは再稼働中で、完成まで数分。 水平ポッドの自動スケーリングがある場合、追加のポッドが使用可能になるまでに永遠に時間がかかります。RUN

  2. コンテナにはコードが含まれています。これはよりポリシーですが、当社では展開でコードを提供することは推奨されていません。少なくとも難読化されていない形式ではありません。

これら 2 つの問題のため、ロジックを Kubernetes/Docker から直接アプリ自体に移動することにしました。

計画された解決策

いくつかの調査の結果、APP_BASE_HREFInjectionToken にたどり着きました。したがって、Web 上のさまざまなガイドに従って、アプリがデプロイされている環境に応じてこれを動的に設定しようとしました。具体的に最初に行われたのは次のとおりです。

  1. コンテンツ (およびその他の変数) に base-path を含む inconfig.jsonという名前のファイルを追加します。/src/assets/config.json
{
   "basePath": "/angular-app",
   "endpoint": "https://kubernetes.local/endpoint"
}
  1. 親履歴を再帰的main.tsに現在を調べて.window.location.pathnameconfig.json
export type AppConfig = {
  basePath: string;
  endpoint: string;
};

export const APP_CONFIG = new InjectionToken<AppConfig>('APP_CONFIG');

export async function fetchConfig(): Promise<AppConfig> {
  const pathName = window.location.pathname.split('/');

  for (const index of pathName.slice().reverse().keys()) {

    const path = pathName.slice(0, pathName.length - index).join('/');
    const url = stripSlashes(`${window.location.origin}/${path}/assets/config.json`);
    const promise = fetch(url);
    const [response, error] = await handle(promise) as [Response, any];

    if (!error && response.ok && response.headers.get('content-type') === 'application/json') {
      return await response.json();
    }
  }
  return null;
}

fetchConfig().then((config: AppConfig) => {
  platformBrowserDynamic([{ provide: APP_CONFIG, useValue: config }])
    .bootstrapModule(AppModule)
    .catch(err => console.error('An unexpected error occured: ', err));
});
  1. の内部はapp.module.tsAPP_BASE_HREFで初期化されますAPP_CONFIG
@NgModule({
  providers: [
  {
    provide: APP_BASE_HREF,
    useFactory: (config: AppConfig) => {
      return config.basePath;
    },
    deps: [APP_CONFIG]
  }
  ]
})
export class AppModule { }

重要:を使用する代わりにこのアプローチを使用しましたAPP_INITIALIZER。これを試したとき、 のプロバイダーAPP_BASE_HREFは常に のプロバイダーの前に実行されたAPP_INITIALIZERため、APP_BASE_HREFは常に未定義でした。

残念ながら、これはローカルでのみ機能し、アプリがプロキシされている間は機能しません. ここで観察した問題は、アプリが最初に base-href と deploy-url を指定せずに Web サーバーによって提供されると、アプリは明らかに '/' (ルート) からすべてをロードしようとすることです。

しかし、これは、Angular スクリプト (別名main.jsvendor.jsruntime.jsおよびその他すべてのアセット) をそこからフェッチしようとすることも意味するため、実際にはコードは実行されません。

これを修正するために、コードを少し修正しました。

angular でサーバーをプローブする代わりにconfig.json、コードを の内部に直接配置してindex.htmlインライン化しました。
このように、base-path を見つけて、html 内のすべてのリンクを接頭辞付きのリンクに置き換えて、少なくともスクリプトとその他のアセットをロードすることができます。これは次のようになります。

<body>
  <app-root></app-root>
  <script>
    function addScriptTag(d, src) {
      const script = d.createElement('script');
      script.type = 'text/javascript';
      script.onload = function(){
        // remote script has loaded
      };
      script.src = src;
      d.getElementsByTagName('body')[0].appendChild(script);
    }

    const pathName = window.location.pathname.split('/');

    const promises = [];

    for (const index of pathName.slice().reverse().keys()) {

      const path = pathName.slice(0, pathName.length - index).join('/');
      const url = `${window.location.origin}/${path}/assets/config.json`;
      const stripped = url.replace(/([^:]\/)\/+/gi, '$1')
      promises.push(fetch(stripped));
    }

    Promise.all(promises).then(result => {
      const response = result.find(response => response.ok && response.headers.get('content-type').includes('application/json'));
      if (response) {
        response.json().then(json => {
          document.querySelector('base').setAttribute('href', json.basePath);
          for (const node of document.querySelectorAll('script[src]')) {
            addScriptTag(document, `${json.basePath}/${node.getAttribute('src')}`);
            node.remove();
            window['app-config'] = json;
          }
        });
      }
    });
  </script>
</body>

さらに、プロバイダー内のコードAPP_BASE_HREFを次のように変更する必要がありました。

useFactory: () => {
  const config: AppConfig = (window as {[key: string]: any})['app-config'];
  return config.basePath;
},
deps: []

これで、ページが読み込まれ、ソース URL がプレフィックス付きの URL に置き換えられ、スクリプトが読み込まれ、アプリが読み込まれ、APP_BASE_HREF.

ルーティングは機能しているように見えますが、言語ファイル、マークダウン ファイル、その他のアセットの読み込みなど、他のすべてのロジックは機能しなくなりました。

--base-hrefオプションは実際に設定すると思いますが、オプションAPP_BASE_HREFが何をするのか--deploy-urlわかりませんでした。
ほとんどの記事や投稿では、base-href を指定するだけで十分であり、アセットも同様に機能すると指定されていますが、そうではないようです。

質問

以上のことをすべて考慮して、私の質問は、ルーティング、翻訳、import() などのすべての角度機能がまるで私と同じように機能するように、Angular アプリを確実に base-href と deploy-url を設定できるように設計する方法です。 Angular CLI 経由でそれらを設定しましたか?

私たちの問題が何であり、何を期待するかを完全に理解するのに十分な情報を提供したかどうかはわかりませんが、そうでない場合は、可能であれば提供します.

4

1 に答える 1