Next.js 团队在其官方示例中,就多环境的开发问题,有一个示例为 With Docker - Multiple Deployment Environments,其核心是通过在一个 Makefile 来进行管理的。
基于这个启发,本案例使用 Makefile 作为一个总控,实现了我日常工作流程的自动化,同时设计了一个 一致的标签 来便于版本验证。
本文就 设计思路 和 不同上下文之间传递参数的途径 进行了详细的阐述。
本文的英文版本:dev.io
代码仓库:with-makefile-docker-automation
系 PragmaticFrontEnd 原创内容
痛点和思考
当管理多个环境的部署时,很容易加载错误的配置。
为了更轻松地进行 版本验证,应该在构建阶段向所有 输出 注入一个 一致的标签。
一个 标签 应该显示:
- 公共版本标签
- 用于构建的环境文件名称
- 构建时的 git 提交哈希码
该标签应以 一致 的方式被记录在记录:
- 在终端输出中打印,构建 Docker 镜像时
- 在后端日志文件中打印,一旦启动节点服务
- 在浏览器控制台或一个页面上显示,一旦 HTML 页面被获取
预期效果
当前项目的 package.json 如下所示:
"name": "x-app",
"version": "1.0.9",
构建阶段后
package.json中的版本字段将自动增加。
"version": "1.1.0",
- Terminal 会输出:
# docker image
x-app-production v1.1.0 xxx Less than a second ago 152MB
部署后
- 服务器端
// 在 Node 服务器中记录
▲ Next.js 14.2.3
- Local: http://localhost:3000
- Network: http://0.0.0.0:3000
✓ Starting...
🚀 version: v1.1.0-75a44a7-production
✓ Ready in 799ms
- 前端
// 浏览器控制台 / 页面上
Release: v1.1.0-75a44a7-production
通过查看这些信息,我们可以自信地宣布:x-app 项目 v1.1.0 生产环境 已经准备就绪 🎉
如何使用
克隆项目并安装依赖项
# 选择您喜欢的软件包管理器,我们在这里使用 pnpm
pnpm install
- 在 .env.development、.env.staging、.env.production 文件中输入要在每个环境中使用的值。
通过在项目的根路径上, 运行 make 命令来检查所有可用命令:
make
- 对于非 Unix-based 操作系统,您可能需要安装 make 工具 Make Windows 版本
用于生产部署
# 构建 Docker 镜像
make build
# 将 Docker 镜像推送到注册表
# 如果在 shell 环境中有 DOCKER_ACCOUNT,只需运行 `make push`
make push DOCKER_ACCOUNT=<YOUR_DOCKER_ACCOUNT>
# 提交更改到 Git
make commit
# 顺序运行上述命令的快捷方式
make all
用于灰度测试环境部署
make <command> NODE_ENV=staging
用于本地部署
make <command> NODE_ENV=development
用于本地开发
make dev
工作原理
Make a Demo
根据 维基百科 的描述:
- Make 是一个构建自动化工具,用于
构建可执行程序,同时也是一个依赖跟踪构建实用程序。 - 于 1976 年在贝尔实验室创建。至今仍广泛使用。
- 由
Makefile控制,其中指定了如何生成目标程序。
其语法如下所示:
target: prerequisites
<TAB> recipe
- target:
命令名称或需要构建的文件/目录。以下翻译为目标或者命令。 - prerequisites: 依赖项,即在目标构建之前需要构建的
其他目标或文件。 - receipt:
以<TAB>开头,由一系列任意数量的shell 命令组成。
让我们动手尝试一个 hello world 示例:
首先,在一个目录下创建一个 Makefile 文件:
touch Makefile
其次,将一个 echo 示例写入文件中:
project=x-app
# syntax
# target:
# <TAB> recipe
echo1:
@echo "hello"
echo2:
@echo "world"
start: echo1 echo2
@echo "start..."
@echo "$(project)"
- 注意:默认情况下,一个
receipt必须以一个 <TAB> 开始,而不是空格,否则会出现错误:*** missing separator. Stop. - 以
@字符开头是告诉 make,不要将原始规则代码打印到控制台上。
最后,打开一个终端并键入 make start,输出应该是
hello
world
start...
x-app
到目前为止,我们已经获得了足够的知识来继续前进。
以下是一些关于 make 和 Makefile 的教程::
- Learn Makefiles With the tastiest examples
- Using Make & Makefiles to Automate your Frontend Workflow
上下文及其变量
在继续定义 make 命令之前,我们应该意识到工作流程涉及三个运行时上下文:
- Shell 上下文
- Docker 构建上下文
- Node 运行时上下文
Shell 上下文
在打开终端窗口时,变量可以最初在 .zshrc 或 .bashrc 中定义和公开。有关 shell 配置,请参阅 how-do-zsh-configuration-files-work 以获取更多详细信息。
make 作为一个 shell 脚本运行,它可以读取和写入现有的 shell 变量,也可以为上下文创建新的成员。
Shell 上下文暴露给:
- npm 脚本
- docker-compose.yml
- docker .env
- 项目 .env
Docker 构建上下文
此上下文与 Docker 构建生命周期一起创建和销毁,它与 shell 上下文隔离。
- 参数通过 shell 上下文中的
--build-arg传递:
# title="./Makefile"
@docker compose -f docker/$(NODE_ENV)/docker-compose.yml build \
--build-arg GIT_COMMIT=$(GIT_COMMIT) \
--build-arg TAG=$(TAG) \
--build-arg ENV=$(NODE_ENV) \
--build-arg DOCKER_CONTAINER_PORT=$(DOCKER_CONTAINER_PORT)
- 在其自己的上下文中通过
ARG接受:
# title="./Dockerfile"
ARG GIT_COMMIT \
TAG \
ENV \
DOCKER_CONTAINER_PORT
Node 运行时上下文
此上下文随 next build 进程创建和销毁,它从 Dockerfile 中调用:
# title="./Dockerfile"
...
elif [ -f package-lock.json ]; then npm run build; \
...
这将触发构建 npm 脚本:
// title="./package.json"
"scripts" : {
"build": "cross-env NEXT_PUBLIC_VERSION=$TAG NEXT_PUBLIC_GIT_COMMIT_ID=$GIT_COMMIT NEXT_PUBLIC_ENV_FILE=$ENV next build",
}
cross-env包在这里用于设置变量到 Node 运行时,而不用担心操作系统设置变量方式不同的问题。- 如以上设计的
一致的标签,我们在向 Node 运行时传递三个关键变量。 NEXT_PUBLIC前缀是 Next.js 中的一种约定,允许node在构建过程中将变量传递给前端,即代表是是公开的。
图形化阐述
以变量 A 为例,它在 shell 配置中定义为 "0",然后由 Makefile 更新为 "1",并保持为 "1" 以供后续使用:
- 在 docker-compose.yml 和 docker env 中读取和使用
- 通过
--build-arg和ARG传递到 docker 构建上下文 - 传递到 Node 运行时并暴露给公共访问
设计 Make 命令
通过上面的知识,定义 Makefile 中的目标 / 命令很简单:
- 定义变量和辅助目标
# 定义暴露的变量
export NODE_ENV := production
export APP_NAME := $(shell npm pkg get name | xargs)# 从 package.json 配置中获取项目名称
export TAG :=# 最新版本标签
export DOCKER_HOST_PORT :=3000
export DOCKER_CONTAINER_PORT :=3000
# 内部变量
DOCKER_ACCOUNT :=$(DOCKER_ACCOUNT)# 将镜像推送到注册表时的 Docker 账号名称
GIT_COMMIT :=$(shell git rev-parse --short HEAD)# 获取最新提交的哈希码
DOCKER_IMAGE :=# 最新的 Docker 镜像
RECEIPT :=# 用于 echo
# 通过触发 npm 脚本 `version:update` 更新项目版本
version-update:
@sh -c "npx cross-env NODE_ENV=${NODE_ENV} npm run version:update || (echo 'Version update failed!' && exit 1)"
# 获取最新的变量
variable-update:
$(eval TAG := v$(shell npm pkg get version | xargs))
$(eval DOCKER_IMAGE :=$(APP_NAME)-$(NODE_ENV):$(TAG))
- 组合核心目标命令
# 一键发布:构建新镜像、推送到注册表、提交更改
# 依赖于 build、push 和 commit
all: build push commit
# 构建最新的 Docker 镜像
# 依赖于 version-update 和 variable-update
build: version-update variable-update
@docker compose -f docker/$(NODE_ENV)/docker-compose.yml build \
--build-arg GIT_COMMIT=$(GIT_COMMIT) ...
# 标记并推送最新镜像到 Docker 注册表
push: variable-update
@docker tag $(DOCKER_IMAGE) $(DOCKER_ACCOUNT)/$(DOCKER_IMAGE)
@docker push $(DOCKER_ACCOUNT)/$(DOCKER_IMAGE)
@docker image rm $(DOCKER_ACCOUNT)/$(DOCKER_IMAGE)
# 在本地运行最新的 Docker 镜像
run: variable-update
@docker container run -it -p $(DOCKER_HOST_PORT):$(DOCKER_CONTAINER_PORT) $(DOCKER_IMAGE)
# 启动开发模式
dev: export NODE_ENV = development
dev:
# 如果已启用 pnpm,则只需 `@pnpm install`
@corepack enable pnpm && pnpm install
@pnpm run dev
如上所示,辅助目标被重用于我的核心目标。Makefile 真的可以让它变得干净简洁!
在 Node 和前端显示
接下来,让我们在服务器和前端显示一致的标签。
- 为 Node 进程环境添加类型定义
// title="typings/index.d.ts"
namespace NodeJS {
interface ProcessEnv {
// for version control
NEXT_PUBLIC_VERSION: string;
NEXT_PUBLIC_GIT_COMMIT_ID: string;
NEXT_PUBLIC_ENV_FILE: string;
// from env file
NEXT_PUBLIC_BASE_URL: string;
}
}
- 包含类型定义
// title="tsconfig"
"include": [
...,
"typings/*.d.ts"
],
要在 Node 服务器启动的时候输入 log,Next.js 提供了一种方法,可以使用 Instrumentation:
- 启用 Next.js 的
Instrumentation功能
// title="next.config.mjs"
const nextConfig = {
experimental: {
instrumentationHook: true,
},
...
}
- 当服务器启动时注册辅助函数
// title="instrumentation.ts"
export async function register() {
logProjectVersion();
}
要在浏览器上显示,只需在页面上调用 logProjectVersion 函数即可。
至此,我们已经完成最繁琐的部分!
是时候部署和验证了
这一部分留给你来完成,选择你喜欢的部署 Docker 镜像的方式。检查是否能够获得与上面的 预期效果 类似的输出。
Cheers !