Angular 2+ - 动态更改应用程序的基本路径

sat*_*nik 7 base-path docker kubernetes angular

介绍

我知道这个问题已经以多种不同的形式被提出。但我已经阅读了我能找到的关于此事的所有帖子和文章,但无法根据我们面临的先决条件解决我的问题。因此,我想首先描述我们的问题并给出我们的动机,然后描述我们已经尝试过或正在使用的不同方法,尽管它们并不令人满意。

问题/动机

我们的设置可以说是有些复杂。我们的 Angular 应用程序部署在多个 Kubernetes 集群上,并通过不同的路径提供服务。

集群本身位于代理后面,并且 Kubernetes 本身添加了一个名为 Ingress 的代理。因此,我们的常见设置可能如下所示:

当地的

http://本地主机: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/
Run Code Online (Sandbox Code Playgroud)

要告诉 CLI 如何设置 base-href 以及从何处提供资源,可以使用Angular CLI 的--base-href和选项。--deploy-url

不幸的是,这意味着我们需要为每个要部署应用程序的环境构建一个 Docker 容器。

此外,我们必须注意在这个预构建过程中设置其他环境变量,例如在environments[.prod].ts每个部署环境中。

目前的解决方案

我们当前的解决方案包括在部署期间构建应用程序。这是通过使用另一个 Docker 容器作为所谓的initContainer来实现的,该容器在 nginx 容器启动之前运行。

该 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/
Run Code Online (Sandbox Code Playgroud)

将此容器作为initContainer合并到 Kubernetes 部署中,允许使用正确的 base-href、deployment-url 构建应用程序,并根据此应用程序部署的环境替换特定变量。

这种方法工作得很好,除了它产生的一些问题:

  1. 一次部署需要几分钟每次重新部署应用程序时,initContainer 都需要
    运行ng build当然,这总是运行命令。ng build为了防止每次容器已经运行前面RUN指令中的命令来缓存这些模块时,它都会构建 ES 模块。
    尽管如此,差载建筑和 Terser 等仍在重新运行,需要几分钟才能完工。
    当存在水平 Pod 自动缩放时,其他 Pod 需要很长时间才能可用。
  2. 该容器包含代码。这更多的是一种政策,但我们公司不鼓励通过部署来交付代码。至少不是以非混淆的形式。

由于这两个问题,我们决定将逻辑从 Kubernetes/Docker 直接移至应用程序本身。

计划解决方案

经过一番研究,我们偶然发现了APP_BASE_HREFInjectionToken。因此,我们尝试遵循网络上的不同指南,根据应用程序部署的环境来动态设置它。具体首先要做的是:

  1. 添加一个以内容(和其他变量)中的基本路径config.json命名的文件/src/assets/config.json
{
   "basePath": "/angular-app",
   "endpoint": "https://kubernetes.local/endpoint"
}
Run Code Online (Sandbox Code Playgroud)
  1. 我们向main.ts文件中添加了代码,通过父历史记录递归地探测当前window.location.pathname以查找config.json.
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/
Run Code Online (Sandbox Code Playgroud)
  1. 里面的app.module.tstheAPP_BASE_HREF被初始化为APP_CONFIG
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/
Run Code Online (Sandbox Code Playgroud)

重要提示:我们使用这种方法而不是使用 the APP_INITIALIZER,因为当我们尝试时, the 的提供程序APP_BASE_HREF总是在 the 的提供程序之前运行APP_INITIALIZER,因此 theAPP_BASE_HREF始终是未定义的。

不幸的是,这仅在本地有效,在应用程序被代理时不起作用。我们在这里观察到的问题是,当应用程序最初由网络服务器提供服务而没有指定base-href和deploy-url时,应用程序显然会尝试从“/”(根)加载所有内容。

但这也意味着它尝试从那里获取角度脚本(又称为main.jsvendor.jsruntime.js以及所有其他资产,因此我们的代码实际上都没有运行。

为了解决这个问题,我们稍微调整了代码。

我们没有让 Angular 探测服务器,而是config.json直接将代码放在里面index.html并内联它。
像这样,我们可以找到基本路径,并将 html 中的所有链接替换为前缀链接,以至少加载脚本和其他资源。这看起来如下:

{
   "basePath": "/angular-app",
   "endpoint": "https://kubernetes.local/endpoint"
}
Run Code Online (Sandbox Code Playgroud)

此外,我们必须调整提供程序内部的代码,APP_BASE_HREF如下所示:

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));
});
Run Code Online (Sandbox Code Playgroud)

现在发生的事情是,它加载页面,将源 URL 替换为前缀 URL,加载脚本,加载应用程序并设置APP_BASE_HREF.

路由似乎可以工作,但所有其他逻辑(例如加载语言文件、降价文件和其他资产)都不再工作。

我认为该--base-href选项实际上设置了APP_BASE_HREF但该--deploy-url选项的作用我无法找到。
大多数文章和帖子都指出,指定 base-href 就足够了,资产也可以工作,但情况似乎并非如此。

问题

考虑到所有这些,我的问题是如何设计一个 Angular 应用程序,使其绝对能够设置其 base-href 和 deploy-url,以便所有 Angular 功能(如路由、翻译、导入()等)都像我一样工作是通过 Angular CLI 设置的吗?

我不确定我是否提供了足够的信息来完全理解我们的问题是什么以及我们的期望,但如果没有,我会尽可能提供。

sat*_*nik 3

我们花了一些时间,但最终设法找到一个足够简单的解决方案来涵盖我们在问题中提出的所有要点。

该解决方案与最初的方法非常接近,即离线构建 Angular 应用程序并将内容复制到nginx容器内,并与之前在 init 容器中使用的构建过程混合在一起。

我们的解决方案如下所示。我们首先离线构建应用程序,并在 thebase-href和 thedeploy-url应该在后面的位置注入一些可识别的字符串。

ng build --prod --base-href=http://recognisable-host-name/ --deploy-url=http://recognisable-host-name/
Run Code Online (Sandbox Code Playgroud)

生成的runtime*.js文件index.html将在代码中包含这些内容,并作为要加载的资源的基础。

然后在第二阶段,标准 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/
Run Code Online (Sandbox Code Playgroud)

像这样的新版本

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/

ENV CONTEXT_PATH http://localhost:80
ENV ENDPOINT http://localhost/endpoint
ENV VARIABLE foobar

CMD \
  mainFiles=$(ls /usr/share/nginx/html/main*.js) \
  && \
  for f in ${mainFiles}; do \
    envsubst '${ENDPOINT},${VARIABLE}' < "$f" > "${f}.tmp" && mv "${f}.tmp" "$f"; \
  done \
  && \
  runtimeFiles=$(grep -lr "recognisable-host-name" /usr/share/nginx/html) \
  && \
  for f in ${runtimeFiles}; do sed -i -E "s@http://recognisable-host-name/?@${CONTEXT_PATH}@g" "$f"; done \
  && \
  nginx -g 'daemon off;'
Run Code Online (Sandbox Code Playgroud)

首先,我们传递我们想要进行的替换。首先CONTEXT_PATH是完整的基本路径,它将替换http://recognisable-host-name我们之前注入的字符串,首先找到包含该字符串的所有文件,然后用命令替换它sed

envsubst也可以通过利用和替换文件中所有提到的这些变量来替换可能特定于环境的其他变量main*.js。因此,environments.prod.ts准备如下:

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/
Run Code Online (Sandbox Code Playgroud)

这照顾了我比赛的所有要点,即:

  1. 请勿分享原始代码
  2. 快速部署
  3. 容器可以在任何代理后面提供服务(例如 Nginx-Ingress/Kubernetes)
  4. 可以注入环境变量

我希望它对其他人有帮助,特别是因为当我研究这个主题时,我发现了几十篇文章,但没有一篇满足所有标准,或者需要大量复杂、低效或难以维护的自定义代码。

编辑:

如果有人感兴趣,我建立了一个演示项目,可以在其中测试此解决方案: https:
//gitlab.com/satanik-angular/base-path-problem