如何处理使用代码拆分的 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
发生此错误是因为:
- 在每次发布时,我们都会构建一个新的
Docker
映像,其中 仅 包含来自最新版本 的块
- 有些客户端 运行 是 过时的 版本,服务器 没有 [=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 创建者自己提供的:
- 不要删除旧构建。 <= 我正在构建一个 Docker 图像,所以这有点挑战
- 捕获
import()
错误并重新加载。您也可以通过在某个地方修补 __webpack_load_chunk__
来全局执行此操作。 <= 我没有得到那个补丁或在哪里使用 import()
,我不是自己生产那些块,它只是一个生产特性
- 让服务器为不存在的 js 文件发送
window.location.reload(true)
,但这是一个非常奇怪的 hack。 <= 它使我的应用程序重新加载在 chrome android 循环中
- 不要为
.js
请求发送 HTML,即使它们不存在,这只会导致奇怪的错误 <= 这没有解决我的问题
相关问题
我如何实施可以防止此错误的解决方案?
如果我正确理解了这个问题,那么有几种方法可以解决这个问题,我将从最简单的到更复杂的方法列出它们:
使用以前的版本从
构建新版本
这是迄今为止最简单的方法,只需要为您的新版本更改基础映像。
考虑以下 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 存储桶需要更多工作。由于存储相对便宜,现在我们只是将资产放在那里。当我们在文件名中添加块标识符时,存储使用量不会增加那么多。
需要考虑的是重定向的实施。您会希望您的应用程序不了解其构造。我们是通过以下方式完成的:
- 请求进入
https://example.com/assets/asset-that-is-no-longer-available.js
。
- 服务器检测到请求是针对
assets
目录,但文件不存在。
- 服务器将请求 url 中的主机名替换为
assets.example.com
并重定向到该位置。
- 浏览器资产请求被重定向到可用的
https://assets.example.com/assets/asset-that-is-no-longer-available.js
。
- 应用程序正常继续。
这可以让您的主 Docker 映像没有非常不常访问的文件,并确保您的内部部署可以以更高的速度继续进行。它还消除了您的 CI 应该始终能够访问每个以前完成的部署代码的要求。
我们一直在使用 Docker 的设置中使用这种方法进行部署,并且没有发现任何客户端的问题。
在 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
发生此错误是因为:
- 在每次发布时,我们都会构建一个新的
Docker
映像,其中 仅 包含来自最新版本 的块 - 有些客户端 运行 是 过时的 版本,服务器 没有 [=66] 的分辨率=]旧块
Chunks are
.js
file that are produced bywebpack
, 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 创建者自己提供的:
- 不要删除旧构建。 <= 我正在构建一个 Docker 图像,所以这有点挑战
- 捕获
import()
错误并重新加载。您也可以通过在某个地方修补__webpack_load_chunk__
来全局执行此操作。 <= 我没有得到那个补丁或在哪里使用import()
,我不是自己生产那些块,它只是一个生产特性 - 让服务器为不存在的 js 文件发送
window.location.reload(true)
,但这是一个非常奇怪的 hack。 <= 它使我的应用程序重新加载在 chrome android 循环中
- 不要为
.js
请求发送 HTML,即使它们不存在,这只会导致奇怪的错误 <= 这没有解决我的问题
相关问题
我如何实施可以防止此错误的解决方案?
如果我正确理解了这个问题,那么有几种方法可以解决这个问题,我将从最简单的到更复杂的方法列出它们:
使用以前的版本从
构建新版本这是迄今为止最简单的方法,只需要为您的新版本更改基础映像。
考虑以下 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 存储桶需要更多工作。由于存储相对便宜,现在我们只是将资产放在那里。当我们在文件名中添加块标识符时,存储使用量不会增加那么多。
需要考虑的是重定向的实施。您会希望您的应用程序不了解其构造。我们是通过以下方式完成的:
- 请求进入
https://example.com/assets/asset-that-is-no-longer-available.js
。 - 服务器检测到请求是针对
assets
目录,但文件不存在。 - 服务器将请求 url 中的主机名替换为
assets.example.com
并重定向到该位置。 - 浏览器资产请求被重定向到可用的
https://assets.example.com/assets/asset-that-is-no-longer-available.js
。 - 应用程序正常继续。
这可以让您的主 Docker 映像没有非常不常访问的文件,并确保您的内部部署可以以更高的速度继续进行。它还消除了您的 CI 应该始终能够访问每个以前完成的部署代码的要求。
我们一直在使用 Docker 的设置中使用这种方法进行部署,并且没有发现任何客户端的问题。