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/

要告诉 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/

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

除了它产生的一些问题外,这种方法非常有效:

  1. 一次部署需要几分钟每次重新部署应用程序时都需要运行initContainer
    。当然,这总是运行命令。为了防止它在每次容器已经运行先前指令中的命令来缓存这些 ES 模块时构建 ES 模块。 尽管如此,用于差动加载的建筑物和 Terser 等正在再次运行,这需要几分钟才能完成。 当存在水平 pod 自动缩放时,额外的 pod 将永远可用。ng buildng buildRUN

  2. 容器包含代码。这更像是一项政策,但我们公司不鼓励通过部署交付代码。至少不是以非混淆形式。

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

计划的解决方案

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

  1. 在内容(和其他变量)中添加一个以基本路径命名的config.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,应用程序显然会尝试从“/”(root)加载所有内容。

但这也意味着它会尝试从那里获取 angular 脚本 aka和所有其他资产main.js,因此我们的代码实际上都没有运行。vendor.jsruntime.js

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

我们没有让角度探测服务器,config.json而是将代码直接放在里面index.html并内联它。
像这样,我们可以找到基本路径并将 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,加载脚本,加载应用程序并设置APP_BASE_HREF.

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

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

问题

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

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

4

1 回答 1

2

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

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

我们的解决方案如下所示。base-href我们首先离线构建应用程序,然后在deploy-url后面应该出现的地方注入一些可识别的字符串。

ng build --prod --base-href=http://recognisable-host-name/ --deploy-url=http://recognisable-host-name/

结果runtime*.jsindex.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/

像这样到新版本

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;'

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

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

export const environment = {
    "endpoint": "${ENDPOINT}",
    "variable": "${VARIABLE}"
}

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

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

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

编辑:

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

于 2020-08-25T05:56:38.460 回答