Angular 2+ - динамически изменять базовый путь приложения - PullRequest
2 голосов
/ 07 августа 2020

Intro

Я знаю, что этот вопрос задавали в самых разных формах. Но я уже прочитал все сообщения и статьи, которые мог найти по этому поводу, и не смог решить свою проблему с предварительными условиями, с которыми мы сталкиваемся. Поэтому я хотел бы сначала описать нашу проблему и дать нашу мотивацию, а затем описать различные методы, которые мы уже пробовали или используем, даже если они неудовлетворительны.

Проблема / Мотивация

Наша установка можно охарактеризовать как несколько сложный. Наше приложение 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 и где используются ресурсы, нужно использовать параметры --base-href и --deploy-url из Angular CLI.

Это, к сожалению, означает , что нам нужно создать 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/

Включение этого контейнера в развертывание Kubernetes в качестве initContainer позволяет создавать приложение с правильными base-href, deployment-url и заменой указанных переменных c в зависимости от среды, в которой развернуто это приложение.

Этот подход работает отлично, за исключением нескольких проблем, которые он создает:

  1. Одно развертывание занимает несколько минут
    initContainer необходимо запускать каждый раз при повторном развертывании приложения. Конечно, при этом всегда выполняется команда ng build. Чтобы предотвратить сборку ES-модулей каждый раз, когда контейнер уже запускает команду ng build в предыдущей директиве RUN для их кеширования.
    Тем не менее, сборка для дифференциальной загрузки и Terser et c. выполняются снова, что занимает несколько минут до завершения.
    Когда есть горизонтальное автомасштабирование модулей, для доступности дополнительных модулей потребуется вечность.
  2. Контейнер содержит код. Это скорее политика, но в нашей компании не рекомендуется доставлять код вместе с развертыванием. По крайней мере, не в обфусцированной форме.

Из-за этих двух проблем мы решили переместить logi c из Kubernetes / Docker прямо в само приложение.

Планируемое решение

После некоторого исследования мы наткнулись на APP_BASE_HREF InjectionToken. Таким образом, мы попытались следовать различным руководствам в Интернете, чтобы динамически установить это в зависимости от среды, в которой развернуто приложение. Что конкретно было сделано первым, так это:

  1. добавить файл с именем config.json в /src/assets/config.json с базовым путем в содержимом (и других переменных)
{
   "basePath": "/angular-app",
   "endpoint": "https://kubernetes.local/endpoint"
}
Мы добавили код в файл main.ts, который рекурсивно проверяет текущий window.location.pathname через родительскую историю, чтобы найти config.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));
});
Внутри app.module.ts APP_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 скрипты, известные как main.js, vendor.js, runtime.js и все другие ресурсы, и поэтому ни один из наших кодов на самом деле не запускается. * Чтобы исправить это, мы немного адаптировали код.

Вместо того, чтобы позволять angular проверять сервер на предмет 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.

Маршрутизация, похоже, работает, но все остальные logi c, такие как загрузка языковых файлов, файлов разметки и другие ресурсы больше не работают.

Я думаю, что опция --base-href действительно устанавливает APP_BASE_HREF, но что делает опция --deploy-url, я не мог узнать.
Большинство статей и В сообщениях указано, что достаточно указать base-href, и активы тоже будут работать, но это не похоже на случай.

Вопрос

Учитывая все это, тогда мой вопрос было бы, как разработать приложение Angular, чтобы определенно иметь возможность устанавливать его base-href и deploy-url, чтобы все функции angular, такие как маршрутизация, перевод, import () et c. работают так, как если бы я установил их через Angular CLI?

Я не уверен, что я дал достаточно информации, чтобы полностью понять, в чем наша проблема и чего мы ожидаем, но если нет, я предоставлю ее, если возможно.

1 Ответ

1 голос
/ 25 августа 2020

Потребовалось некоторое время, но нам, наконец, удалось найти достаточно простое решение, охватывающее все моменты, которые мы затронули в вопросе. app в автономном режиме и копирует содержимое внутри контейнера nginx, смешанное с процессом сборки, ранее использовавшимся в контейнере инициализации.

Наше решение выглядит следующим образом. Сначала мы создаем приложение в автономном режиме и вводим некоторую узнаваемую строку, где base-href и deploy-url должны быть позже.

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

Полученные runtime*.js и index.html файлы будут содержать их внутри код и в качестве основы для загрузки ресурсов.

Затем на втором этапе файл Dockerfile для стандартного Angular приложения адаптируется из

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.

другие переменные, которые могут определяться средой c, также могут быть заменены использованием envsubst и заменой всех упоминаний этих переменных из файлов main*.js. Поэтому environments.prod.ts подготовлен следующим образом:

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

Это учитывает все основные моменты, в которых я участвовал, а именно:

  1. не использовать исходный код
  2. быстрое развертывание
  3. контейнер может обслуживаться за любым прокси (например, Nginx -Ingress / Kubernetes)
  4. переменные среды могут быть введены

Я надеюсь, что это поможет кому-то еще, особенно потому, что когда я исследовал этот топи c, я нашел десятки статей, но ни одна из них не соответствовала всем критериям и не требовала большого количества нестандартного кода, который является сложным, неэффективным или трудным в поддержке.

Редактировать:

Если кому-то интересно, я создал демонстрационный проект, где это решение можно протестировать:
https://gitlab.com/satanik-angular/base-path-problem

...