Docker 前端本地开发指南(实操版)
本文只讲一件事:怎么用 Docker 把前端开发环境跑起来,并且能正常热更新。
如果你是想系统学习 Docker(生产部署、镜像仓库、K8s 等),请看完整版《Docker 开发与部署指南》;本文是它的"本地开发子集"——把日常开发真正用得上的部分提炼出来,砍掉所有延伸内容,照着做就行。
第八章是一次完整的踩坑实录(全量同步 → 首次启动路由失效 → 预生成修复),遇到怪问题可以直接跳过去看。
适用范围:yudao-ui-admin-vue3(Vite 8)/ yudao-ui-admin-uniapp(Vite 5)· Node 20 · pnpm 10 · Docker Desktop(WSL2 后端)
| 日期 | 变更 |
|---|---|
| 初版 | 基础方案:COPY 进镜像 + compose watch 热更新 |
| 2026-08-20 | 新增第八章:启动前全量同步(--build)+ pages.json 路由预生成(gen-pages);新增 Q7;uniapp 容器 CMD 改为 dev:h5:docker |
目录
- 一、💡 为什么要用 Docker 跑前端开发
- 二、🧩 核心概念速览(够用就行)
- 三、🛠️ 环境准备
- 四、📄 三个核心文件
- 五、🚀 第一次启动(初始化)
- 六、☕ 日常开发流程
- 七、🔥 热更新:正确姿势与原理(重点)
- 八、🧯 启动前全量同步与 pages.json 路由预生成(uniapp 排查实录)
- 九、🩺 常见问题排查
- 十、🖥️ Docker Desktop 图形界面日常操作
- 十一、⌨️ 快捷命令速查
- 十二、⚖️ 两个项目的配置对照(vue3 / uniapp)
- 附录:📎 常用命令速查表
一、💡 为什么要用 Docker 跑前端开发
| 痛点 | Docker 解决方案 |
|---|---|
| 团队成员 Node 版本不一致 | 镜像锁定版本,人人一样 |
| Windows 装某些 npm 包失败(如 node-sass) | 容器内是 Linux,原生兼容 |
| 新人入职装环境要半天 | docker compose up 一条命令搞定 |
| 切换项目时依赖冲突 | 每个项目独立容器,互不干扰 |
一句话:把 Node 版本、依赖、启动命令全部固化进镜像,任何人、任何机器,一条命令拉出完全一致的开发环境。
二、🧩 核心概念速览(够用就行)
本地开发只需要理解三个词:
| 概念 | 通俗理解 | 本地开发里对应什么 |
|---|---|---|
| 镜像(Image) | 应用的"安装包",只读模板 | node:20-alpine + 你的项目依赖 |
| 容器(Container) | 镜像跑起来的"活实例" | 正在跑的 vite dev server |
| 数据卷(Volume) | Docker 管理的持久化存储 | node_modules、数据库数据 |
三者关系:
Dockerfile ──build──> Image(安装包)
│
│ docker compose up
▼
Container(活实例)
│
│ 挂载
▼
Volume(持久化数据)
本地开发只需要记住:容器 = 你项目的"独立运行环境",删了可以随时重建,不影响你的源码和依赖。
三、🛠️ 环境准备
3.1 安装 Docker Desktop
Windows 上装 Docker Desktop,安装后启动即可(默认用 WSL2 后端)。
3.2 配国内镜像加速(强烈建议)
国内直接访问 Docker Hub 经常超时,先配加速器:
Docker Desktop → Settings → Docker Engine:
{
"registry-mirrors": [
"https://docker.m.daocloud.io",
"https://dockerproxy.com",
"https://docker.mirrors.ustc.edu.cn",
"https://hub-mirror.c.163.com"
]
}
Apply & Restart 生效。
3.3 确认后端地址
前端开发时请求要转发到后端(yudao-server)。两种常见情况:
| 后端在哪 | 前端配置 |
|---|---|
| 宿主机 IDE 里跑 | http://host.docker.internal:48080 |
| 局域网某台机器跑 | 直接写 IP,如 http://192.168.1.182:48080 |
这个地址通过 compose 的 environment 注入(见下文 4.2),不用改代码。
四、📄 三个核心文件
每个前端项目目录下都要有这三个文件。以 yudao-ui-admin-uniapp 为例(vue3 项目的差异见第十二章)。
4.1 Dockerfile.dev
# 仅用于本地开发:跑 pnpm dev:h5,配合 compose watch 的 sync+restart 实现热更新
FROM node:20-alpine
# git 被 husky/precommit 间接需要;corepack 启用 pnpm@10
RUN apk add --no-cache git && corepack enable
WORKDIR /app
# 只拷贝依赖描述文件,利用 docker 层缓存。
COPY package.json pnpm-lock.yaml .npmrc ./
# --ignore-scripts: 跳过 prepare 钩子(init-husky / init-baseFiles),
# 因为 scripts/ 目录此时还没拷进来。pages.json 等文件由启动时的 gen-pages 预生成(见第八章)。
RUN pnpm install --frozen-lockfile --ignore-scripts
# 源码打进镜像(compose watch 的 sync 方案需要,bind mount 在 Windows 下 inotify 不穿透)
# 注意:dev:docker 的 --build 每次启动都会按"当前宿主机源码"重新执行此层(内容校验和缓存),
# 宿主机删除的文件不会进镜像 —— 这就是"启动前同步代码"的实现,无需任何脚本(见 8.1)
COPY . .
# dev server 端口(vite.config.ts 中 VITE_APP_PORT=9000)
EXPOSE 9000
# 默认 H5 开发模式
# 必须先跑 gen-pages 预生成 src/pages.json 再启动 uni(原因见 8.3):
# uni:h5 在 Vite config 钩子里读 pages.json(once 缓存,早于 UniPages 的 configResolved 生成时机),
# 容器冷启动没有该文件时整个会话路由都会失效(所有跳转被 fallback 到首页)
CMD ["pnpm", "run", "dev:h5:docker"]
三个关键点:
pnpm install只拷了依赖描述文件 → 依赖装进镜像层,改代码不会重装;COPY . .把源码拷进镜像 → 不能删,热更新方案靠它(见第七章),同时它还承担"启动前全量同步"(见 8.1);CMD不是直接dev:h5,而是dev:h5:docker(先预生成 pages.json 再启动,见 8.4)。
4.2 docker-compose.dev.yml
命名约定(本项目):镜像名 / 容器名 / project 名统一为纯项目名,不带
-dev/-vue3-dev后缀。即 vue3 项目为yudao-ui-admin-vue3、uniapp 项目为yudao-ui-admin-uniapp。这是为避免 Docker Desktop 列表里出现多余后缀、保持命名一致而定的团队约定,两个项目务必延续。
# 镜像名 / 容器名 / project 名 统一为纯项目名(不带 -dev / -uniapp-dev 后缀)
# 启动: pnpm dev:docker (= up --watch --build:先按当前源码重建镜像再启动,宿主机删的文件不会带进容器)
# 必须用 `up --watch` 启动; Docker Desktop 直接点 Start 没有 watch 会话、不会同步文件
name: yudao-ui-admin-uniapp
services:
yudao-ui-admin-uniapp:
build:
context: .
dockerfile: Dockerfile.dev
image: yudao-ui-admin-uniapp:latest
container_name: yudao-ui-admin-uniapp
ports:
- '9000:9000'
develop:
watch:
# 源码改动:仅同步进容器,Vite 原生 HMR(保留页面状态)
- action: sync
path: ./src
target: /app/src
# pages.json 由容器内 gen-pages 生成,防止宿主机的旧文件被推送覆盖(见 8.4)
ignore:
- pages.json
- action: sync
path: ./index.html
target: /app/index.html
# 根目录配置文件改动:需重启 vite(配置无法热加载)
- action: sync+restart
path: ./pages.config.ts
target: /app/pages.config.ts
- action: sync+restart
path: ./manifest.config.ts
target: /app/manifest.config.ts
- action: sync+restart
path: ./uno.config.ts
target: /app/uno.config.ts
- action: sync+restart
path: ./vite.config.ts
target: /app/vite.config.ts
# 环境变量改动:需重启 vite
- action: sync+restart
path: ./env
target: /app/env
# 依赖改动:重建镜像
- action: rebuild
path: ./package.json
- action: rebuild
path: ./pnpm-lock.yaml
environment:
# 后端 yudao-server 地址(见 3.3)
- VITE_SERVER_TARGET=http://host.docker.internal:48080
# 容器里没有微信开发者工具,跳过 openDevTools 插件
- SKIP_OPEN_DEVTOOLS=true
extra_hosts:
# Linux 上等同 host.docker.internal 指向宿主机;Windows/Mac 自带
- 'host.docker.internal:host-gateway'
# 保持 stdin 开放 + tty,便于 Ctrl+C 优雅退出 vite
stdin_open: true
tty: true
develop.watch 三种动作的含义:
| 动作 | 触发时机 | 效果 |
|---|---|---|
sync | 源码文件(src/)变化 | 只把文件同步进容器,Vite 原生 HMR,不重启 |
sync+restart | 配置文件(vite.config.ts 等)变化 | 同步后重启 vite(配置必须重启才生效) |
rebuild | package.json / pnpm-lock.yaml 变化 | 重新构建镜像(依赖变了,要重装) |
4.3 .dockerignore
node_modules
dist
.git
docs
.image
.husky
.vscode
.github
*.log
*.local
tmp/
.playwright-cli/
private.key
private.*.key
.idea
.hbuilderx
.stylelintcache
.eslintcache
docs/.vitepress/dist
docs/.vitepress/cache
# 生成类文件:交给容器内 gen-pages 按需生成,不带旧缓存进镜像(见 8.3/8.4)
src/types
src/pages.json
src/pages-json-js
# 裁剪掉的业务分包目录:不进镜像,也避免本地残留误打包
src/pages-ai
src/pages-erp
src/pages-im
src/pages-infra
src/pages-iot
src/pages-mall
src/pages-member
src/pages-mes
src/pages-mp
src/pages-pay
src/pages-statistics
src/pages-system
src/pages-wms
*.ps1
排除无关文件,缩小构建上下文、加快构建。其中 src/pages.json 是故意排除的:它是 dev 启动时的生成物(不是源码),带旧文件进镜像反而会引发路由问题(见第八章)。
五、🚀 第一次启动(初始化)
cd yudao-ui-admin-uniapp
docker compose -f docker-compose.dev.yml up --watch --build
或(项目里已封装好脚本):
pnpm dev:docker
首次启动会做四件事:拉基础镜像 → 构建项目镜像(装依赖)→ 按当前宿主机源码执行 COPY . .(启动前全量同步,见 8.1)→ 启动容器并进入 watch 模式。首次构建需要几分钟,之后启动:源码没变则构建层全部缓存命中(秒级),有变则只重建 COPY 那一层(1~2 秒)。
启动成功后,日志依次出现:
[gen-pages] 已生成 src/pages.json(主包 9 页 + 分包 86 页) ← 路由预生成(见 8.4)
ready in xxxx ms ← vite 就绪
浏览器访问:
http://localhost:9000/admin-ui-uniapp/
注意:命令里的
--watch不能省,这是热更新的关键(见第七章);uniapp 项目的dev:docker已内置--build,实现"启动即同步最新代码"(见 8.1)。
六、☕ 日常开发流程
6.1 每天开工
cd yudao-ui-admin-uniapp
pnpm dev:docker
6.2 改代码 → 自动热更新
在 IDE 里正常改 src/ 下的代码,保存即生效:
- 改组件/页面 → Vite 原生 HMR,毫秒级,保留页面状态(不刷新页面);
- 改
vite.config.ts等配置文件 → 自动重启 vite(约 2~5 秒); - 改
package.json依赖 → 自动重建镜像(需要网络)。
6.3 收工
# 停止容器(保留镜像和数据,下次秒启)
docker compose -f docker-compose.dev.yml stop
# 或直接 Ctrl+C(up --watch 在前台跑时)
6.4 什么时候需要重建
| 场景 | 命令 |
|---|---|
| 改代码(日常) | 什么都不用做,自动热更新 |
| 隔天开工 / 上次改过文件后重新启动 | pnpm dev:docker(uniapp 版已带 --build,启动即全量同步,见 8.1) |
改 Dockerfile.dev 或依赖(package.json) | pnpm dev:docker:rebuild(--build --no-cache,完全重建) |
| 改 compose 文件 | 重新 pnpm dev:docker |
七、🔥 热更新:正确姿势与原理(重点)
7.1 症状
Windows + Docker Desktop 下,如果按网上教程用 bind mount(volumes: - .:/app)方式挂源码,改代码后浏览器不刷新,必须重启容器才生效。这是 Windows 上 Docker 跑前端最常见的坑。
7.2 根因
Docker Desktop 在 Windows 上通过 WSL2/Hyper-V 的虚拟文件共享层把宿主机目录挂进容器。这个共享层不会把宿主机侧的文件变更事件转发进容器——容器内的 Vite/chokidar 收不到"文件变了"的通知,HMR 自然不触发。
bind mount 方案:
宿主机改代码 → 文件在宿主机侧变化
│
▼
虚拟文件共享层 ⚠️ 变更事件在这里丢失
▼
容器内 Vite 收不到通知 → HMR 不触发
7.3 正确方案:COPY + compose watch 的 sync
思路:不靠 bind mount,改为「源码 COPY 进镜像 + compose watch 在宿主机侧监听文件、把改动写进容器」。文件是在容器侧写入的(本地写入),Vite 的原生文件监听能正常收到事件,原生 HMR 即可工作。
正确方案:
宿主机改代码
│ compose watch 在宿主机侧监听(Windows 原生监听,正常)
▼
sync 把新文件写进容器(容器侧写入,事件正常)
▼
Vite 原生 HMR 检测到变化 → 模块热替换(保留状态,毫秒级)
这就是第四章那份配置的来源:Dockerfile.dev 里 COPY . . 打源码,compose 里 src 用 sync。
7.4 为什么必须 up --watch,点 Start 不行
Docker Desktop 的 Start 按钮只是 docker start(拉起容器进程),不会启动 compose watch 的宿主机侧监听会话。没有 watch 会话就没有 sync,容器里跑的是镜像里的源码快照——你改的代码根本进不了容器,自然不热更新。
| 启动方式 | watch 会话 | 文件同步 | 热更新 |
|---|---|---|---|
docker compose up --watch | ✅ 有 | ✅ sync | ✅ |
| Docker Desktop 点 Start | ❌ 没有 | ❌ 无同步 | ❌ 跑的是镜像快照 |
docker compose up -d(普通后台) | ❌ 没有 | ❌ 无同步 | ❌ 跑的是镜像快照 |
7.5 为什么不需要 node_modules 命名卷
旧方案(bind mount)必须挂一个 node_modules 命名卷,因为 .:/app 会把宿主机 Windows 的 node_modules 一起挂进来——里面的 .exe 二进制在 Linux 容器里跑不了,得用命名卷把容器里的 Linux 版"盖回来"。
新方案源码是 COPY 进镜像、没有 bind mount,宿主机不会污染容器内的 node_modules,sync 也只同步 ./src,所以命名卷不需要了(还能省几百 MB 磁盘)。
7.6 终极推荐:前端本地跑 + Docker 管基础设施
如果团队不强制"前端也必须在 Docker 里",最佳体验是前端在 Windows 本地原生跑(HMR 毫秒级、无中间层),Docker 只负责 MySQL、Redis、后端:
docker compose up -d mysql redis # 启动基础设施
cd yudao-ui-admin-uniapp
nvm use 20 # 切 Node 版本
pnpm install
pnpm dev:h5 # 前端本地跑,改代码即时 HMR
| 方案 | HMR 延迟 | 页面状态 | 复杂度 |
|---|---|---|---|
| 前端本地跑 + Docker 管基础设施 | <100ms | ✅ 保留 | 低(推荐) |
| Docker 跑前端(COPY + sync) | 毫秒级 | ✅ 保留 | 中(本文方案) |
| Docker 跑前端(bind mount) | ❌ 不生效 | - | - |
如果你选择了全容器化路线(团队统一环境),第八章的「启动前全量同步 + 路由预生成」是这套方案的必要补丁,别漏。
7.7 Docker Desktop 容器列表折叠 与 docker run 方案权衡
7.7.1 折叠是 Docker Desktop 的 UI 行为,关不掉
用 docker compose up --watch 起的容器,在 Docker Desktop 的 Containers 列表里永远会按 project name 分组显示成 组名 ▸ 子容器 的两层折叠结构(即使 project 名 = 容器名,也仍有一层 group 框在外)。
这不是配置问题,是 Docker Desktop 自身的硬行为——任何 compose 管理的容器都按 project 分组,没有设置项能关掉折叠。
7.7.2 为了「一行平铺」试过 docker run,但牺牲了 HMR 速度
实测过用 docker run 直接起容器(不走 compose、无 project 分组,列表真一行平铺),源码用 bind mount 挂载 + Vite server.watch.usePolling 轮询文件变化:
docker run -d --name yudao-ui-admin-uniapp -p 9000:9000 \
--add-host host.docker.internal:host-gateway \
-e VITE_DOCKER=true \
-v "$(pwd)/src:/app/src" -v "$(pwd)/vite.config.ts:/app/vite.config.ts" \
yudao-ui-admin-uniapp:latest
// vite.config.ts:Windows bind mount 不转发 inotify,开 polling 绕开
server: {
watch: process.env.VITE_DOCKER === 'true'
? { usePolling: true, interval: 300,
ignored: ['**/node_modules/**', '**/dist/**', '**/.git/**', '**/types/**'] }
: undefined,
}
结果:HMR 能工作(polling 确实检测到了文件变化),但很慢——一个 55KB 的 .vue 文件改动后 HMR 需要 5~6 秒才刷新。瓶颈在 Windows Docker Desktop 的 9P 文件共享层:每次读文件都要跨进程(宿主→Docker→容器),I/O 开销是平台级的,调 interval 也压不下去。
7.7.3 结论:保留 compose watch,接受折叠
| 启动方式 | Docker Desktop 列表 | HMR 延迟 | 取舍 |
|---|---|---|---|
docker compose up --watch | 折叠(project group) | <1 秒 | 折叠可接受,HMR 最快 |
docker run + bind mount + polling | 一行平铺 | 5~6 秒(55KB 文件) | 为去折叠牺牲速度,不划算 |
本项目最终决定:用
docker compose up --watch(纯项目名 + compose watch),HMR 毫秒级、保留页面状态;Docker Desktop 的折叠 UI 是已知限制,接受它。不要再为了「一行平铺」改回docker run+ polling。
八、🧯 启动前全量同步与 pages.json 路由预生成(uniapp 排查实录)
本章记录两个真实问题的完整解决历程:先解决"启动前全量同步代码",同步解决后才暴露"首次启动路由失效",两者环环相扣。vue3 项目没有 pages.json 动态生成机制,不受影响。
8.1 问题一:容器残留旧文件,启动前全量同步
症状:宿主机删掉的文件(如裁剪掉的页面)在容器里残留,导致容器内构建报错;容器里的 src 是旧代码。
走过的弯路:先尝试了 entrypoint.dev.sh + rsync --delete 方案(宿主机 src 只读挂载进容器,启动时 rsync 全量对账)。能用,但引入了额外脚本、启动变慢、且与 compose watch 的 sync 机制职责重叠,后废弃。
最终方案(零脚本):dev:docker 加 --build,利用 Docker 构建缓存实现"启动前全量同步":
"dev:docker": "docker compose -f docker-compose.dev.yml up --watch --build"
原理:--build 让每次启动都先走镜像构建,其中 COPY . . 层按**内容校验和(content checksum)**缓存——
- 宿主机文件没变 → 该层缓存命中,不重新拷贝(启动秒级);
- 宿主机文件有变(新增/修改/删除)→ 该层按"当前宿主机源码"重建,宿主机删除的文件自然不会进镜像。
pnpm dev:docker = up --watch --build
│
▼
镜像构建:COPY . . 层(内容校验和缓存)← 这一步就是"启动前全量同步"
│
▼
镜像有更新 → 容器 Recreate(全新可写层)
镜像无更新 → 容器仅 restart(可写层保留)
这就是 4.1 节 Dockerfile 里那条注释的含义:不需要任何同步脚本,构建缓存本身就是同步机制。
8.2 问题二:首次启动路由失效,第二次才正常
全量同步修好后,暴露出新问题:文件有变化时首次启动,所有路由跳转失效——无论点哪里都被 fallback 到首页,只有第二次执行 dev:docker 才恢复正常。
这个现象正是 8.1 方案的"副作用"链条:
文件有变 → COPY 层重建 → 镜像更新 → 容器 Recreate(不是 restart!)
→ 可写层整体丢弃 → 上次运行生成的 src/pages.json 一并消失 → 冷启动
而"第二次正常"的原因:源码没变 → 构建层缓存全命中 → 镜像没更新 → 容器只 restart 不 recreate → 可写层保留 → 上次运行时插件已写好的 pages.json 还在。
排查方法:docker exec -it yudao-ui-admin-uniapp sh 进容器,对比冷启动后 vite 首次启动与第二次启动时 src/pages.json 的内容差异;再读 uni-app 和 uni-pages 两个插件的源码,定位到下述时序冲突。
8.3 根因:pages.json 的读取时机早于生成时机
两个插件在同一次 Vite 启动中抢同一个文件 src/pages.json:
| 角色 | 插件 | Vite 钩子 | 行为 |
|---|---|---|---|
| 读取方 | uni 官方 uni:h5 | config(早) | 读 pages.json,且 parsePagesJsonOnce = once(...) 结果永久缓存 |
| 生成方 | @uni-helper/vite-plugin-uni-pages | configResolved(晚) | 扫描页面文件,写入真实路由 |
Vite 保证所有 config 钩子先于所有 configResolved 执行。冷启动时(容器里没有 pages.json,.dockerignore 也故意排除了它):
- UniPages 插件工厂发现文件不存在,先写占位文件
{pages:[{path:""}]} uni:h5在 config 钩子读到空占位 →once()缓存固化- UniPages 到 configResolved 才写入 95 条真实路由——但缓存已固化,整个 dev 会话都用空路由
- 表现为:所有跳转 fallback 首页、返回无效
本地 Windows 直接
pnpm dev:h5不触发这个问题,是因为本地src/pages.json一直存在(上次运行留下的),uni:h5 读到的永远是完整文件——只有"全新环境首次启动"(新容器、新 clone、CI)才会踩中。
8.4 修复:启动前预生成 pages.json
思路:让 pages.json 在 uni 启动之前就已就位,uni:h5 的 config 钩子第一次读到的就是完整路由。共改 4 个文件:
| 文件 | 改动 |
|---|---|
scripts/gen-pages.mjs(新增) | 用 vite.resolveConfig 只走一遍插件生命周期(不启服务、不编译),预写 src/pages.json / src/manifest.json,并校验路由非空才放行 |
package.json | 新增 "dev:h5:docker": "node ./scripts/gen-pages.mjs && uni" |
Dockerfile.dev | CMD 改为 pnpm run dev:h5:docker(先预生成再启动) |
docker-compose.dev.yml | watch 的 src sync 加 ignore: pages.json,防止宿主机残留旧文件被推送覆盖容器内生成的 |
gen-pages.mjs 的核心逻辑(完整文件见项目 scripts/ 目录):
// 关键点 1:UNI_PLATFORM 必须在动态 import 插件之前设置
//(@uni-helper/uni-env 在模块加载时求值,ESM 静态 import 会被提升导致拿不到值)
process.env.UNI_PLATFORM ||= 'h5'
const { resolveConfig } = await import('vite')
// 关键点 2:resolveConfig 只走 config / configResolved 等钩子,
// 不启 dev server、不编译 —— 秒级完成,让 UniPages 把 pages.json 写好。
// 注意:不加载项目 vite.config.ts(configFile: false),只挂生成配置文件所需的插件,
// UniPages 的 exclude / subPackages 参数从 vite.config.ts 原样复制
await resolveConfig(
{ configFile: false, root, logLevel: 'error', plugins: [UniPlatform(), UniPages({...}), UniManifest()] },
'serve',
)
// 关键点 3:pages.json 带注释(插件用 comment-json 写入),校验时也要用 comment-json 解析;
// 路由为空直接 exit(1) 中止启动,避免带着坏配置起 dev
const { parse } = await import('comment-json')
const pagesJson = parse(fs.readFileSync(pagesPath, 'utf-8'))
if (!pagesJson.pages?.length || !pagesJson.pages[0].path) {
console.error('[gen-pages] src/pages.json 生成异常:主包路由为空')
process.exit(1)
}
8.5 验证与维护注意
验证(刻意制造"文件有变化后首次启动"场景):
[gen-pages] 已生成 src/pages.json(主包 9 页 + 分包 86 页) ← 先生成
command, mode -> serve development ← 后启动
ready in 3207ms · HTTP 200
容器 Recreate(冷启动)后首次启动路由即完整(9 + 86 = 95 条),不再需要第二次执行。
维护注意(重要):
gen-pages.mjs里 UniPages 的参数(exclude / subPackages 分包目录)是从vite.config.ts复制的。以后调整分包目录时,两处要同步改(脚本内有注释提醒,忘了改会导致预生成路由与实际构建路由不一致);- 三个环节缺一不可:
.dockerignore排除 pages.json(不带旧文件进镜像)→ CMD 先 gen-pages(冷启动前生成)→ watch ignore pages.json(防宿主机旧文件覆盖)。
九、🩺 常见问题排查
Q1:pnpm install 时报 MODULE_NOT_FOUND: create-base-files.js
原因:pnpm install 自动跑 prepare 脚本,但构建时 scripts/ 目录还没拷进镜像。
解决:加 --ignore-scripts 跳过 prepare 钩子(Dockerfile.dev 里已加)。pages.json / manifest.json 等生成文件由容器启动时的 gen-pages 预生成(见 8.4)。
Q2:构建时报 Corepack 下载 pnpm 失败 / 拉镜像超时
原因:国内网络访问 registry.npmjs.org / Docker Hub 不稳定。
解决:配镜像加速(见 3.2)。注意:改过 package.json 或 Dockerfile.dev 会触发重新构建,此时需要网络,确保加速器生效后再构建。
Q3:改代码不热更新(Windows + Docker)
先按优先级排查:
- 是不是用
up --watch启动的? 点 Start 或up -d都不会热更新(见 7.4)。 - compose 里
src是不是sync? 如果配成了sync+restart,会变成全量重启(丢状态、慢 2~5s),改成sync就是原生 HMR。 - 改的是不是
src/下的文件? 配置文件本来就要重启,不算 bug。 - 改的内容是不是"有效内容"? 测试热更新时,往
.vue文件</style>之后 append 注释是无效的——那是 SFC 语法外区域,vue 编译器会忽略,编译产物没变化,Vite 自然不会触发 HMR。要验证请在<script>里加一行真实代码。
Q4:容器里访问不到宿主机后端
解决:确认 compose 里有 extra_hosts: - "host.docker.internal:host-gateway",且 VITE_SERVER_TARGET 指向 http://host.docker.internal:48080(后端在宿主机跑时)。
Q5:HMR 有反应,但页面刷成空白 / 状态丢了
原因:改的文件无法热替换(比如改了 main.ts、路由配置、全局样式),Vite 自动降级为整页刷新,这是正常行为,不是故障。
Q6:Windows 上 nvm use 报 A version argument is required
Windows 用的是 nvm-windows,它不读 .nvmrc,必须手动传版本号:
nvm use 20
# 或从 .nvmrc 读:
nvm use (Get-Content .nvmrc)
Q7:uniapp 容器首次启动后所有路由跳转都进首页(第二次启动才正常)
这是 pages.json 生成时机与 uni 读取时机的冲突,已通过 gen-pages 预生成修复(见第八章完整分析)。如果再次出现,按顺序检查三个环节是否齐全:
Dockerfile.dev的 CMD 是不是pnpm run dev:h5:docker(而非直接dev:h5);.dockerignore是否排除了src/pages.json;- compose 的 src sync 是否带
ignore: pages.json。
进容器看启动日志,第一行应出现 [gen-pages] 已生成 src/pages.json。
十、🖥️ Docker Desktop 图形界面日常操作
10.1 查看日志
打开 Docker Desktop → Containers → 点容器 → Logs 标签页。与命令行 docker logs -f 完全等价,显示的是同一份输出。
| 操作 | 方法 |
|---|---|
| 实时跟踪 | 点右上角 ▶️ Follow log |
| 筛选关键字 | 顶部搜索框输入 |
| 清屏(不影响后台) | 🗑️ Clear |
10.2 启停容器
注意:Start/Stop 只适合"临时暂停恢复",不是日常热更新入口。 要让代码同步进容器,必须用 up --watch 启动(见 7.4)。
| 操作 | 说明 |
|---|---|
| 容器旁 ⏸️ Stop | 等价 docker stop,暂停进程,数据保留 |
| 容器旁 ▶️ Start | 等价 docker start,不带 watch,仅恢复进程 |
| 彻底删除 | docker compose down(保留镜像和数据卷) |
10.3 看磁盘占用
docker system df
清理无用资源:
docker container prune # 删停止的容器
docker image prune -a # 删未使用的镜像
docker volume prune # 删未引用的数据卷
十一、⌨️ 快捷命令速查
项目 package.json 里封装了常用命令(以 uniapp 项目为例):
| 命令 | 作用 | 等价命令 |
|---|---|---|
pnpm dev:docker | 启动开发容器(带 watch + build,启动即全量同步) | docker compose -f docker-compose.dev.yml up --watch --build |
pnpm dev:docker:stop | 停止并删除容器 | docker compose -f docker-compose.dev.yml down |
pnpm dev:docker:rebuild | 无缓存重建镜像并启动 | docker compose -f docker-compose.dev.yml up --watch --build --no-cache |
pnpm dev:docker:logs | 查看日志 | docker compose -f docker-compose.dev.yml logs -f |
pnpm dev:h5:docker | 容器内实际执行:预生成 pages.json 后启动 uni | node ./scripts/gen-pages.mjs && uni |
十二、⚖️ 两个项目的配置对照(vue3 / uniapp)
两个项目都已按本文方案配好,差异只在端口、环境变量、watch 条目和路由预生成机制:
| 项 | vue3(yudao-ui-admin-vue3) | uniapp(yudao-ui-admin-uniapp) |
|---|---|---|
| 镜像名 / 容器名 / project 名 | yudao-ui-admin-vue3 | yudao-ui-admin-uniapp |
| Docker Desktop 列表 | 折叠(project group,无法关闭,见 7.7) | 同左 |
| 端口 | 8081:80 | 9000:9000 |
| Vite 版本 | 8.0.10 | 5.2.8 |
| 后端代理变量 | VITE_PROXY_TARGET | VITE_SERVER_TARGET |
watch 的 src | sync | sync(且 ignore: pages.json,见 8.4) |
| watch 的配置文件 | vite.config.ts / build / .env | vite.config.ts / pages.config.ts / manifest.config.ts / uno.config.ts / env |
| 容器 CMD | pnpm dev(直接起 vite) | pnpm run dev:h5:docker(先 gen-pages 再起 uni,见 8.4) |
dev:docker 是否带 --build | 否(up --watch) | 是(up --watch --build,启动即全量同步,见 8.1) |
| 启动命令 | pnpm dev:docker | pnpm dev:docker |
| 访问地址 | http://localhost:8081/ | http://localhost:9000/admin-ui-uniapp/ |
两个项目结论一致:Vite 5 和 Vite 8 的 chokidar 都正常,
COPY + sync方案下原生 HMR 都能工作。第八章的"启动前全量同步 + pages.json 预生成"目前只在 uniapp 项目落地(vue3 没有动态生成 pages.json 的机制,不需要);若 vue3 项目也遇到"启动时残留旧文件"问题,把它的dev:docker加上--build即可复用同样方案。
附录:📎 常用命令速查表
# 启动(带 watch + build:uniapp 日常入口,启动即全量同步 + 路由预生成)
docker compose -f docker-compose.dev.yml up --watch --build
# 停止(保留容器/镜像/数据)
docker compose -f docker-compose.dev.yml stop
# 彻底删除容器(保留镜像和数据卷)
docker compose -f docker-compose.dev.yml down
# 无缓存重建镜像并启动(改了 Dockerfile 或依赖异常时)
docker compose -f docker-compose.dev.yml up --watch --build --no-cache
# 查看日志
docker compose -f docker-compose.dev.yml logs -f <服务名>
# 查看容器状态
docker ps
# 查看磁盘占用
docker system df
# 进入容器调试(看 pages.json 生成结果、排查路由问题)
docker exec -it yudao-ui-admin-uniapp sh
cat /app/src/pages.json | head -20
最后提醒一句:本地开发最顺手的还是"前端本地跑 + Docker 管基础设施";如果团队要求前端也容器化,用本文第四、七章的 COPY + sync 方案即可,两条路都能拿到毫秒级 HMR 和保留页面状态。