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 в зависимости от среды, в которой развернуто это приложение.
Этот подход работает отлично, за исключением нескольких проблем, которые он создает:
- Одно развертывание занимает несколько минут
initContainer необходимо запускать каждый раз при повторном развертывании приложения. Конечно, при этом всегда выполняется команда ng build
. Чтобы предотвратить сборку ES-модулей каждый раз, когда контейнер уже запускает команду ng build
в предыдущей директиве RUN
для их кеширования.
Тем не менее, сборка для дифференциальной загрузки и Terser et c. выполняются снова, что занимает несколько минут до завершения.
Когда есть горизонтальное автомасштабирование модулей, для доступности дополнительных модулей потребуется вечность. - Контейнер содержит код. Это скорее политика, но в нашей компании не рекомендуется доставлять код вместе с развертыванием. По крайней мере, не в обфусцированной форме.
Из-за этих двух проблем мы решили переместить logi c из Kubernetes / Docker прямо в само приложение.
Планируемое решение
После некоторого исследования мы наткнулись на APP_BASE_HREF
InjectionToken. Таким образом, мы попытались следовать различным руководствам в Интернете, чтобы динамически установить это в зависимости от среды, в которой развернуто приложение. Что конкретно было сделано первым, так это:
- добавить файл с именем
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?
Я не уверен, что я дал достаточно информации, чтобы полностью понять, в чем наша проблема и чего мы ожидаем, но если нет, я предоставлю ее, если возможно.