如何将环境变量传递给前端 Web 应用程序?

Posted

技术标签:

【中文标题】如何将环境变量传递给前端 Web 应用程序?【英文标题】:How to pass environment variables to a frontend web application? 【发布时间】:2018-07-13 17:32:16 【问题描述】:

我正在尝试将前端 Web 应用程序容器化,但在弄清楚如何传递环境变量时遇到了麻烦。该应用程序是 Angular 应用程序,因此它是 100% 客户端。

在典型的后端服务中,传递环境变量很容易,因为一切都在同一主机上运行,​​因此后端服务可以轻松选择环境变量。但是,在前端应用程序中,情况有所不同:应用程序在客户端的浏览器中运行。

我想通过环境变量配置我的应用程序,因为这使部署更加容易。所有配置都可以在docker-compose.yml 中完成,无需维护多个镜像,每个可能的环境都需要一个镜像。只有一个不可变的图像。这遵循了 12 因素应用理念,可以在 https://12factor.net/config 上找到。

我正在构建我的应用程序映像,如下所示:

FROM node:alpine as builder
COPY package.json ./
RUN npm i && mkdir /app && cp -R ./node_modules ./app
WORKDIR /app
COPY . .
RUN $(npm bin)/ng build

FROM nginx:alpine
COPY nginx/default.conf /etc/nginx/conf.d/
RUN rm -rf /usr/share/nginx/html/*
COPY --from=builder /app/dist /usr/share/nginx/html
CMD ["nginx", "-g", "daemon off;"]

app/config.ts,我有:

export const config = 
    REST_API_URL: 'http://default-url-to-my-backend-rest-api'
;

理想情况下,我想在我的docker-compose.yml 中做这样的事情:

backend:
  image: ...
frontend:
  image: my-frontend-app
  environment:
    - REST_API_URL=http://backend:8080/api

所以我相信我应该更改此 app/config.ts 以将 REST_API_URL 替换为环境变量。由于我更喜欢​​不可变的 Docker 映像(所以我不想在构建期间进行此替换),我很困惑如何在这里取得进展。我相信我应该支持在启动 nginx 代理之前在运行时更改app/config.ts。然而,这个文件被缩小和 webpack 捆绑的事实使得这更加困难。

任何想法如何解决这个问题?

【问题讨论】:

这是cross-posted to the Docker forum on Reddit。 【参考方案1】:

我对静态 HTML 文件有类似的问题,这就是我想要解决的问题:

环境变量的数量可以扩展 环境变量可以在启动时设置,而不是在构建时设置 不用担心维护变量替换的格式,以免与其他文本发生冲突

我尝试了其他答案,但似乎它们不符合上述要求。所以这就是我最终使用envsubst

Dockerfile

FROM nginx:alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY . /usr/share/nginx/html
EXPOSE 80

# awkwardly replace env variables
COPY ./replaceEnvVars.sh /
RUN chmod +x replaceEnvVars.sh
ENTRYPOINT ["./replaceEnvVars.sh"]
CMD ["nginx", "-g", "daemon off;"]

replaceEnvVars.sh

#!/bin/sh

envsubst < /usr/share/nginx/html/index.tmpl.html > /usr/share/nginx/html/index.html && nginx -g 'daemon off;' || cat /usr/share/nginx/html/index.html

index.tmpl.html

<html>
 ...
 <script>
 gtag('config', '$GA_CODE');
 </script>
 ...
 <a href="$BASE_URL/login" class="btn btn-primary btn-lg btn-block">Login</a>
</html>

docker-compose.yml

version: '3'
services:
  landing:
    build: .
    ...
    environment:
      - BASE_URL=https://dev.example.com
      - GA_CODE=UA-12345678-9
    ...

【讨论】:

【参考方案2】:

把你的环境变量放在index.html!!

相信我,我知道你来自哪里!将特定于环境的变量烘焙到我的 Angular 应用程序的构建阶段与我所学到的关于可移植性和关注点分离的一切背道而驰。

但是等等!仔细看看常见的 Angular index.html

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>mysite</title>
  <base href="/">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
  <link rel="stylesheet" href="https://assets.mysite.com/styles.3ff695c00d717f2d2a11.css">
  <script>
  env = 
    api: 'https://api.mysite.com/'
  
  </script>
</head>
<body>
  <app-root></app-root>
  <script type="text/javascript" src="https://assets.mysite.com/runtime.ec2944dd8b20ec099bf3.js"></script>
  <script type="text/javascript" src="https://assets.mysite.com/polyfills.20ab2d163684112c2aba.js"></script>
  <script type="text/javascript" src="https://assets.mysite.com/main.b55345327c564b0c305e.js"></script>
</body>
</html>

这就是全部配置!!!

它就像您用来维护 Docker 应用程序的 docker-compose.yml:

版本化的不可变资产 环境变量 应用程序绑定 环境元数据 即使是不同的捆绑包也感觉像是 docker 图像的分层,不是吗? runtime 就像您很少更改的基本映像。 polyfills 是您需要的那些未包含在您需要的基本映像中的东西。 main 是您的实际应用,每次发布都会发生很大变化。

您可以对前端应用执行与 Docker 应用相同的操作!

    构建、版本化和发布不可变资产(js 包/Docker 映像) 将部署清单发布到 staging (index.html / docker-compose.yml) 暂存测试 将部署清单发布到生产环境......引用您刚刚测试的相同资产!即刻!原子地!

怎么样?

只需将臭气熏天的/src/environments/environment.prod.ts 指向window 对象即可。

export const environment = (window as any).env;
// or be a rebel and just use window.env directly in your components

并使用环境变量他们在哪里!

将脚本添加到您的 index.html
<script>
  env =  api: 'https://api.myapp.com' 
</script>

我对这种方法感觉非常强烈,因此我创建了一个专门针对它的网站:https://immutablewebapps.org。我想你会发现还有很多其他的好处!

~~~

现在,我使用两个 AWS S3 存储桶成功完成了这项工作:一个用于版本化静态资产,一个用于 index.html(它使路由变得超级简单:为每个路径提供 index.html)。我还没有像你提议的那样运行容器。如果我要使用容器,我希望在构建和发布新资产以及发布新的index.html 之间进行清晰的分离。也许我会使用容器的环境变量从模板即时渲染index.html

如果您选择这种方法,我很想知道结果如何!

【讨论】:

谢谢,这真的很好!本质上是基于 index.html 不能是不可变的事实(文件名中不能有哈希)。缓存配置没有例外:)【参考方案3】:

我的解决方案:在运行时使用 docker 卷将特定的 js 配置文件挂载为 env.js。

我有一个用于 dev 和 prod 的 docker compose 文件。

我有 dev.env.js 和 prod.env.js。

我的 html 文件引用了 env.js。

在 docker-compose.yml 中,我将任一 env 文件卷挂载为 env.js。

例如我的开发者撰写:

web:
    image: nginx
    ports:
      - 80:80
    volumes:
      - ../frontend:/usr/share/nginx/html
      - ../frontend/dev.env.js:/usr/share/nginx/html/env.js

我的产品组成:

web:
    image: nginx
    ports:
      - 80:80
    volumes:
      - ../frontend:/usr/share/nginx/html
      - ../frontend/prod.env.js:/usr/share/nginx/html/env.js

【讨论】:

谢谢,很有用:)【参考方案4】:

我在同样的问题上苦苦挣扎,但还需要将配置值从 docker-compose 级别传递到 Angular,我觉得这并不简单。

基本上,我采取了类似的方法并提出了以下解决方案:

    我使用compose ARGS 将所需值从docker-compose.yml 传递到Dockerfile。 所以在docker-compose.yml 我有:

magicsword.core.web: build: args: - AUTH_SERVER_URL=http://$EXTERNAL_DNS_NAME_OR_IP:55888/ - GAME_SERVER_URL=http://$EXTERNAL_DNS_NAME_OR_IP:55889/ - GUI_SERVER_URL=http://$EXTERNAL_DNS_NAME_OR_IP:55890/ # =self

    现在必须在 Dockerfile 中将这些标记为变量:

ARG AUTH_SERVER_URL ARG GAME_SERVER_URL ARG GUI_SERVER_URL

    因为在构建过程中这些成为正常的环境变量,最后一步是在目标文件中进行实际替换,例如使用一些神奇的单线。我做了如下(只是一个宠物项目,所以不需要是最佳的):

RUN apt-get update && apt-get install -y gettext RUN envsubst < ./src/environments/environment.ts > ./src/environments/environment.ts.tmp && mv ./src/environments/environment.ts.tmp ./src/environments/environment.ts

替换前的environment.ts,供参考:

export const environment = production: true, GAME_SERVER_URL: "$GAME_SERVER_URL", GUI_SERVER_URL: "$GUI_SERVER_URL", AUTH_SERVER_URL: "$AUTH_SERVER_URL" ;

瞧。希望这对某人有帮助:)

【讨论】:

【参考方案5】:

我解决这个问题的方法如下:

1.在enviroment.prod.ts中设置一个唯一且可识别的字符串:

export const environment = 
  production: true,
  REST_API_URL: 'REST_API_URL_REPLACE',
;

2.创建一个entryPoint.sh,每次你对容器进行一次docker run时,都会执行这个entryPoint。

#!/bin/bash
set -xe
: "$REST_API_URL_REPLACE?Need an api url"

sed -i "s/REST_API_URL_REPLACE/$REST_API_URL_REPLACE/g" /usr/share/nginx/html/main*bundle.js

exec "$@"

如您所见,此入口点获取 'REST_API_URL_REPLACE' 参数并在 main*bundle.js 文件中将其(在本例中)替换为 var 的值。

3.在dockerfile中CMD前添加entrypoint.sh(需要执行权限):

FROM node:alpine as builder
COPY package.json ./
RUN npm i && mkdir /app && cp -R ./node_modules ./app
WORKDIR /app
COPY . .
RUN $(npm bin)/ng build --prod

FROM nginx:alpine
COPY nginx/default.conf /etc/nginx/conf.d/
RUN rm -rf /usr/share/nginx/html/*
COPY --from=builder /app/dist /usr/share/nginx/html

# Copy the EntryPoint
COPY ./entryPoint.sh /
RUN chmod +x entryPoint.sh

ENTRYPOINT ["/entryPoint.sh"]
CMD ["nginx", "-g", "daemon off;"]

4.使用 env 启动镜像或使用 docker-compose(斜线必须转义):

docker run -e REST_API_URL_REPLACE='http:\/\/backend:8080\/api'-p 80:80 image:tag

可能存在一个更好的解决方案,不需要在缩小文件中使用正则表达式,但这工作正常。

【讨论】:

干得好@daniel-caldera 很好的回答丹尼尔谢谢你。可互换的另一个入口点 shell 脚本解决方案可能是:``` shell #!/bin/bash output="$(printenv REST_API_URL)" sed -i 's|REST_API_URL_REPLACE|'$output'|g' /usr/share/nginx /html/main*bundle.js 执行“$@”``` 缓存怎么样?如果您更改环境变量,您可能希望客户端重新加载捆绑文件。通常,您将文件的哈希添加到文件名(例如main.ab4c6c83e4fa9c.js)。由于您通过插入环境变量来更改文件,因此您应该在之后更新文件名中的哈希。 您也可以使用envsubst 来代替sed。 gnu.org/software/gettext/manual/html_node/…

以上是关于如何将环境变量传递给前端 Web 应用程序?的主要内容,如果未能解决你的问题,请参考以下文章

将用户定义的环境变量传递给 tomcat

使用 Docker 将环境变量传递给 Spring Boot 应用程序不起作用

如何将c#中的变量传递给前端

将环境变量传递给 application.yml

Svelte 框架:在运行时将环境变量传递给客户端包

如何将凭证传递给 Translation API v3?