【问题标题】:How to handle new version deployement for Docker webpack application that use code splitting?如何处理使用代码拆分的 Docker webpack 应用程序的新版本部署?
【发布时间】:2020-08-12 05:41:08
【问题描述】:

在 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. 一些客户端正在运行过时版本,而服务器不会解决旧块,因为( 1)

块是由webpack 生成的.js 文件,请参阅more information 的代码拆分

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

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

通配符服务于 Web 应用程序的 index.html,用于在用户刷新页面时将路由委托给客户端路由

// 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'));
});

这似乎是一个糟糕的修复,尤其是在 Android 版 Google Chrome 上,我看到我的应用程序在无限循环中刷新。 (是的,这也是一个丑陋的解决方案!)

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

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

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

  • 不要删除旧版本。
  • 捕获import() 错误并重新加载。您也可以通过在某处修补 __webpack_load_chunk__ 来全局执行此操作。 import(),我不是自己制作这些块,它只是一个生产功能
  • 让服务器为不存在的 js 文件发送 window.location.reload(true),但这是一个非常奇怪的 hack。
  • 不要为 .js 请求发送 HTML,即使它们不存在,这只会导致奇怪的错误

相关问题

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

【问题讨论】:

  • 这似乎是客户端错误。能否提供相关的客户端代码?
  • 嗯,在我看来,这信息太少了。刷新站点后语法错误消失?当您在第一次加载网站之前清除浏览器缓存时,是否也会发生这种情况?
  • 我已经更新了问题、标题并添加了尽可能多的信息。我没有添加复制品,我看不出它会有什么帮助,而且这是不可能的。 问题标题也可能是如何处理 docker 中为过时客户端提供的 webpack 应用程序

标签: javascript docker webpack create-react-app webpack-splitchunks


【解决方案1】:

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

使用旧版本构建新版本

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

考虑以下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 中阅读有关多阶段构建的更多信息。

【讨论】:

  • 非常感谢那些我没有想到的解决方案。对于第一个,这似乎是一个很好的解决方案,但正如您所说,如果基础图像需要一些改进,它也会带来一些其他问题,所以我不认为这个解决方案是可能的最终解决方案。第二种方法在某种程度上更合规,但我不明白当你从previousnode 时会发生什么。 .
  • 我不明白其他解决方案将如何提供帮助。到目前为止,我已经决定将build/static/js 目录存储在 git 上,并保留我的旧块的踪迹。这听起来不像是一个优雅的解决方案。我想知道哪一个是您的第二个选项和我的最佳选项。无论如何,非常感谢这个解决方案,我仍然会等到提供其他方法
  • 你能澄清什么不清楚,所以我可以尝试改进答案吗?这个想法是您必须将以前版本的内容复制到新图像中,然后用新版本覆盖它。 FROM version1 AS previous 子句使之前的版本在构建期间可用,因此您可以将其全部或部分复制到新版本中。
  • 或者,如果它适用于你的情况,你可以在 CDN 后面运行你的后端,这也可以解决这个问题。
  • 目前尚未确认是否会在 CDN 后面提供服务。关于您的第一个问题,我不知道如何阅读 Jib 以帮助创建 jib 方法。
【解决方案2】:

一个简单的解决方案是DISABLE cachingindex.html

Cache-Control: no-store

【讨论】:

  • res.header('Cache-Control', 'no-store').sendFile(...)我猜你对express不是很熟悉
【解决方案3】:

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

当请求命中例如assets 文件夹并且找不到.js 文件时,我们会发出重定向到“救援”环境。这是一个简单的 AWS Cloudfront 分发,由 AWS S3 存储桶支持。在构建最前沿的环境后,我们会将所有新资产推送到该 S3 存储桶。

如果用户使用的是最新版本的应用程序,他们只会轻松地使用最新版本。一旦应用程序更新服务器端,或者用户没有使用最新版本,所有资产都通过“备份域”提供。由于前沿发出重定向*而不是提供 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 进行部署的设置中使用这种方法,并且没有发现任何客户端的问题。

【讨论】:

  • 优秀的答案。谢谢!
猜你喜欢
  • 2018-01-11
  • 2016-09-30
  • 2018-04-28
  • 1970-01-01
  • 2016-12-28
  • 2020-08-06
  • 1970-01-01
  • 2018-05-12
  • 2020-09-26
相关资源
最近更新 更多