Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

基于 Docker 的 SPA 运行时变量方案 #28

Open
ryancui92 opened this issue Jan 4, 2023 · 0 comments
Open

基于 Docker 的 SPA 运行时变量方案 #28

ryancui92 opened this issue Jan 4, 2023 · 0 comments

Comments

@ryancui92
Copy link
Owner

背景

SPA 与 Docker

一个 SPA 项目的产物通常来说是一份简单的 dist 文件夹,里面包括了诸如 index.html, js, css, images 等各种静态资源。然后丢到一个 static web server (e.g. nginx, apache) 上就能对外提供服务了。

后来,我们通过 Docker 来对这份 dist 做一个包装,用一个 nginx 的 base image 把它包裹起来,就变成通过 docker run 命令就能随时对外提供服务了。

FROM nginx:stable-alpine
COPY /dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

用这样一个简单的 Dockerfile 来打包一个前端镜像,并对外提供服务

docker run --rm -d -p 80:80 <image_name>

环境变量

无论是使用 webpack 还是 vite, 都会在对应的打包方案上看到环境变量,无论是使用 process.env.* 还是 import.env.*, 其实都是在 npm build 的时候来做编译期的不同环境切换。

因此很多时候,前端打包会出现下面几种说法,打测试的包、打生产的包。也会产生不同的 npm scripts 和很多 .env 文件。

# 有很多不同的打包命令
npm run build:dev
npm run build:prod

Build once

12 Factors 里,描述了 Code, Build, Config, Deploy 之间的关系。具体到 SPA, 我们发现按照上面所说的编译期变量方案:

  1. 无法做到 build once, deploy anytime. 不同的 env 需要不同的 build
  2. Config 没有在 deploy 时使用,而是被固化到了 build 里,各种 .env 的变量其实都写到了具体的 js/css 中

因此我们希望有一个方案可以做到:

  1. Build once. SPA 的构建(具体而言就是 npm run build...)应该只有一次。当发布正式时,能够直接使用当前的这一份 Build.
  2. 运行时变量支持。可以在运行时改变 SPA 的行为。
  3. 结合 Docker, 通过环境变量来实现第 2 点的运行时变量(方便后续使用 K8s/Helm Chart 进行部署)

方案

搜索了一番,根据参考第一篇文章,基本思路如下:

  1. 用 npm run build 构建一次 SPA
  2. 用 Dockerfile nginx based image 构建一个 image, 入口调整为一个 shell 脚本
  3. 在运行时,执行 shell 脚本,对 container 内的 dist 静态文件做一些操作或直接生成一个 js 文件被 index.html 引用,然后再启动 nginx
  4. 代码中需要依赖运行时的逻辑,通过 window.__env 进行判断

构建与 Docker build

这部分不需要多解释。注意这里的 index.html 会默认在所有 bundled 的 js 前,引入一个 config.js 文件,里面定义了各种需要的运行时变量。

<div id="app"></div>
<script src="/config.js"></script>
<!-- built files will be auto injected -->
window.__env = {
  PUBLIC_PATH: '/',
  ABC: '111'
}

在本地开发的时候,新增一个 public/config.js 文件即可,内容可以是定义一个空对象。

window.__env = {}

这样就可以在代码里直接使用 window.__env.ABC 来进行逻辑判断了。

动态生成 config.js

使用 Dockerfile 构建镜像时,使用一个 shell 脚本作为启动命令

# 前面省略...
ENTRYPOINT ["/bin/sh", "/start-container.sh"]

然后在脚本里动态生成基于环境变量的 js 文件

#!/bin/bash

echo "window.__env = { PUBLIC_PATH: '${PUBLIC_PATH}' }" > /usr/share/nginx/html/config.js

nginx -g 'daemon off;'

像这样就能根据外部的环境变量把 container 里的文件覆盖了。

publicPath

如果使用 webpack 的话,有自带的 dynamic public path 功能,所有 async chunk 的 publicPath 都能通过 __webpack_public_path__ 这个变量控制。(用过 qiankun 的应该都很熟悉了...)

所以只需要在你的 entry 最开始默认 import 一个 public-path.ts, 然后在这个文件内部给这个变量赋值一个运行时变量(比如上面的 window.__env)即可。

// main.ts
import 'public-path.ts'
// 后面省略...
// public-path.ts
// @ts-ignore
__webpack_public_path__ = window.__env.PUBLIC_PATH ?? '/'

这里有两个坑要注意。

第一,基于 css-loader/url-loader 的 publicPath 并不会运行时动态,需要给 url-loader 添加一个 postTransformPublicPath 选项。根据参考第 2 点的回答尝试,发现拼接出来的 publicPath 会多了一个 /, 要做一个去重处理才能 work. (也有可能是我的 publicPath 写成了 / 换成 '' 应该会没问题吧?没试过)

{
  publicPath: '/',
  postTransformPublicPath: (p) => `__webpack_public_path__ + (${p}.startsWith('/') ? ${p}.slice(1) : ${p})`,
}

第二,html entry 引用的 bundle 并不会动态 publicPath, 因为已经写死在 script src 里了,因此可以通过一个 sed 命令来直接把 index.html 的 publicPath 改了,注意要有一个 common 的 asset prefix

PARSED_PUBLIC_PATH=$(echo ${PUBLIC_PATH} | sed 's/\//\\\//g') # 这里要转义一下,不然 sed 会有问题
sed -i "s/\/static/${PARSED_PUBLIC_PATH}static/g" /usr/share/nginx/html/index.html

思考

本质上,这是一种对 bundled 产物的魔改,只是通过一些 shell 手段把这个 bundle time 从 compile delay 到了 runtime. 如果能清晰把握 bundled 的细节和结果,你也可以说这种 shell script 也是 bundle 的一种 extension 罢了(笑

另一种方案

除了通过在运行时把静态文件魔改掉之外,其实也可以将 static file 变成 dynamic 即可。比如上文提到的 index.html 可以通过一个 Node 服务器来 serve 动态的内容,这样也能做到相应的效果。

参考

  1. 为 Single Page App 提供运行时环境变量
  2. Webpack url-loader dynamic public path
@ryancui92 ryancui92 added Draft and removed Draft labels Jan 4, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant