介绍
我知道这个问题已经以许多不同的形式提出。但是我已经阅读了我能找到的关于这个问题的每一篇文章和文章,并且无法用我们面临的先决条件解决我的问题。因此,我想先描述我们的问题并给出我们的动机,然后再描述我们已经尝试或正在使用的不同方法,即使它们并不令人满意。
问题/动机
我们的设置可以被描述为有些复杂。我们的 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 构建应用程序,并根据应用程序部署的环境替换特定变量。
除了它产生的一些问题外,这种方法非常有效:
- 一次部署需要几分钟每次重新部署应用程序时都需要运行initContainer
。当然,这总是运行命令。为了防止它在每次容器已经运行先前指令中的命令来缓存这些 ES 模块时构建 ES 模块。 尽管如此,用于差动加载的建筑物和 Terser 等正在再次运行,这需要几分钟才能完成。 当存在水平 pod 自动缩放时,额外的 pod 将永远可用。ng build
ng build
RUN
- 容器包含代码。这更像是一项政策,但我们公司不鼓励通过部署交付代码。至少不是以非混淆形式。
由于这两个问题,我们决定将逻辑从 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 脚本 aka和所有其他资产main.js
,因此我们的代码实际上都没有运行。vendor.js
runtime.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 设置了这些?
我不确定我是否提供了足够的信息来完全理解我们的问题是什么以及我们期望什么,但如果没有,我会尽可能提供。