如何处理使用代码拆分的 Docker webpack 应用程序的新版本部署?

How to handle new version deployement for Docker webpack application that use code splitting?

在 Docker、

中部署我的应用程序的新版本后

我发现我的 console 出现以下错误,破坏了我的应用程序:

Uncaught SyntaxError: Unexpected token '<'

在此屏幕截图中,缺少的源名为:10.bbfbcd9d.chunk.js,该文件的内容如下:

(this.webpackJsonp=this.webpackJsonp||[]).push([[10],{1062:function(e,t,n){"use strict";var r=n(182);n.d(t,"a",(function(){return r.a}))},1063:function(e,t,n){var ...{source:Z[De],resizeMode:"cover",style:[Y.fixed,{zIndex:-1}]})))}))}}]);
//# sourceMappingURL=10.859374a0.chunk.js.map

发生此错误是因为:

  1. 在每次发布时,我们都会构建一个新的 Docker 映像,其中 包含来自最新版本 的块
  2. 有些客户端 运行 是 过时的 版本,服务器 没有 [=66] 的分辨率=]旧块 因为 (1)

Chunks are .js file that are produced by webpack, see code splitting for more information

重新加载应用程序会将版本更新到最新版本,但它仍然会破坏所有使用过时版本的用户的应用程序。

我尝试过的一个可能的修复方法是刷新应用程序。如果请求的块在服务器上丢失,如果对 .js 文件的请求在通配符路由中结束,我将发送重新加载信号。

Wild card is serving the index.html of the web application, this for delegating routing to client-side routing in case of an user refreshing it's page

// Handles any requests that don't match the ones above
app.get('*', (req, res) => {
  // prevent old version to download a missing old chunk and force application reload
  if (req.url.slice(-3) === '.js') {
    return res.send(`window.location.reload(true)`);
  }
  return res.sendFile(join(__dirname, '../web-build/index.html'));
});

这似乎是一个糟糕的修复,尤其是在 Google Chrome 和 Android 上,我发现我的应用程序在无限循环中刷新。 (是的,这也是一个丑陋的修复!)

由于这对我的最终用户来说不是一个可靠的解决方案,我正在寻找另一种方法来在用户客户端过时时重新加载应用程序。

我的 Web 应用程序是使用 webpack 构建的,就好像它是一个 create-react-app 应用程序一样,分布式构建目录包含许多 .js 块文件。

这些是一些可能的修复方法 I got offered on webpack issue tracker,一些是由 webpack 创建者自己提供的:

相关问题

我如何实施可以防止此错误的解决方案?

如果我正确理解了这个问题,那么有几种方法可以解决这个问题,我将从最简单的到更复杂的方法列出它们:

使用以前的版本从

构建新版本

这是迄今为止最简单的方法,只需要为您的新版本更改基础映像。

考虑以下 Dockerfile 来构建应用程序的版本 2:

FROM version1

RUN ...

然后构建它:

docker build -t version2 .

然而,这种方法有一个问题——所有旧块都会在新图像中累积。它可能是可取的,也可能不是可取的,但需要考虑一些事情。

另一个问题是您无法轻松更新基本映像。

使用多阶段构建

多阶段构建允许您 运行 多个阶段并将每个阶段的结果包含到您的最终图像中。每个阶段可能会使用不同的 Docker 图像和不同的工具,例如GCC 编译一些本地库,但你的最终图像中并不需要 GCC。

为了使其适用于多阶段构建,您需要能够创建第一个图像。让我们考虑以下 Dockerfile 正是这样做的:

FROM alpine

RUN mkdir -p /app/latest && touch /app/latest/$(cat /proc/sys/kernel/random/uuid).chunk.js

它创建了一个新的 Docker 图像,其中包含一个具有随机名称的新块,并将其放入名为 latest 的目录中 - 这对于提议的方法很重要!

为了创建后续版本,我们需要一个 Dockerfile.next,如下所示:

FROM version2 AS previous
RUN rm -rf /app/previous && mv /app/latest/ /app/previous

FROM alpine

COPY --from=previous /app /app
RUN mkdir -p /app/latest && touch /app/latest/$(cat /proc/sys/kernel/random/uuid).chunk.js

在第一阶段,它通过删除 previous 版本并将 latest 移动到 previous.

来轮换版本

在第二阶段,它复制第一阶段剩下的所有版本,创建一个新版本并将其放入latest

使用方法如下:

docker build -t image:1 -f Dockerfile .

>> /app/latest/99cfc0e6-3773-40a0-82d4-8c8643cc243b.chunk.js

docker build -t image:2 --build-arg PREVIOUS_VERSION=1 -f Dockerfile.next .

>> /app/previous/99cfc0e6-3773-40a0-82d4-8c8643cc243b.chunk.js
>> /app/latest/2adf34c3-c50c-446b-9e85-29fb32011463.chunk.js

docker build -t image:3 --build-arg PREVIOUS_VERSION=2 -f Dockerfile.next 

>> /app/previous/2adf34c3-c50c-446b-9e85-29fb32011463.chunk.js
>> /app/latest/2e1f8aea-36bb-4b9a-ba48-db88c175cd6b.chunk.js

docker build -t image:4 --build-arg PREVIOUS_VERSION=3 -f Dockerfile.next 

>> /app/previous/2e1f8aea-36bb-4b9a-ba48-db88c175cd6b.chunk.js
>> /app/latest/851dbbf2-1126-4a44-a734-d5e20ce05d86.chunk.js

注意块是如何从 latest 移动到 previous 的。

此解决方案要求您的服务器能够发现不同目录中的静态文件,但这可能会使本地开发复杂化,认为此逻辑可能是基于环境的条件。

或者,您可以在容器启动时将所有文件复制到一个目录中。这可以在 Docker 本身的 ENTRYPOINT 脚本中或在您的服务器代码中完成 - 这完全取决于您,取决于哪个更方便。

另外这个例子只看一个版本,但是可以通过更复杂的旋转脚本扩展到多个版本。例如,要保留 3 个最新版本,您可以这样做:

RUN rm -rf /app/version-0; \
    [ -d /app/version-1 ] && mv /app/version-1 /app/version-0; \
    [ -d /app/version-2 ] && mv /app/version-2 /app/version-1; \
    mv /app/latest /app/version-2; 

或者可以使用 Docker ARG 参数化要保留的版本数。

您可以在 official documentation 中阅读有关多阶段构建的更多信息。

一个简单的解决方案是 DISABLE caching of index.html

Cache-Control: no-store

我们在生产中使用的一种方法是让两个不同的环境为您的 .js 资产提供服务。首先,我们有最前沿的一个:这个只知道最近构建的版本。所有请求都针对此环境。

当请求到达 assets 文件夹但找不到 .js 文件时,我们将重定向到 "rescue" 环境。这是一个简单的 AWS Cloudfront 发行版,由 AWS S3 存储桶提供支持。在构建前沿环境后,我们将所有新资产推送到该 S3 存储桶。

如果用户使用的是最新版本的应用程序,他们将轻松地只使用前沿技术。一旦应用程序更新服务器端,或者用户没有使用最新版本,所有资产都将通过 "backup domain" 提供。由于前沿问题是重定向*而不是服务 404,因此用户在这里不会遇到问题(除了必须将请求重做到不同的位置)。

此设置确保即使是非常老的客户端也可以继续运行。我们已经看到 Googlebot 之前仍然可以请求超过 1000 个部署的资产的案例!

很大很大的缺点:修剪 S3 存储桶需要更多工作。由于存储相对便宜,现在我们只是将资产放在那里。当我们在文件名中添加块标识符时,存储使用量不会增加那么多。

需要考虑的是重定向的实施。您会希望您的应用程序不了解其构造。我们是通过以下方式完成的:

  1. 请求进入https://example.com/assets/asset-that-is-no-longer-available.js
  2. 服务器检测到请求是针对 assets 目录,但文件不存在。
  3. 服务器将请求 url 中的主机名替换为 assets.example.com 并重定向到该位置。
  4. 浏览器资产请求被重定向到可用的 https://assets.example.com/assets/asset-that-is-no-longer-available.js
  5. 应用程序正常继续。

这可以让您的主 Docker 映像没有非常不常访问的文件,并确保您的内部部署可以以更高的速度继续进行。它还消除了您的 CI 应该始终能够访问每个以前完成的部署代码的要求。

我们一直在使用 Docker 的设置中使用这种方法进行部署,并且没有发现任何客户端的问题。