news 2026/9/16 7:28:19

Docker从入门到实战:镜像、容器、部署与排障全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Docker从入门到实战:镜像、容器、部署与排障全解析

1. Docker到底是什么,为什么要学它

1.1 先搞清楚镜像、容器、仓库三个概念

很多人第一次接触 Docker 时容易被“镜像”“容器”“仓库”这几个词绕晕,我用一个最朴素的理解方式:镜像就是安装包,容器就是运行中的程序,仓库就是存放安装包的应用商店。

镜像是一个只读的模板文件,里面包含了你运行一个应用所需要的全部东西——代码、运行时、系统库、配置、环境变量。它被打包成一个标准化的单元,所以只要是装了 Docker 的机器,无论 Windows、Linux 还是 macOS,拿过来就能直接跑,不会出现“在我电脑上明明好好的,到你电脑上就挂了”这种经典事故。

容器则是镜像运行起来之后的状态。你可以同时从同一个镜像启动多个容器,互相之间隔离,就像同一套安装包装到多台电脑上运行,互不干扰。仓库负责存储和分发镜像,最常用的就是 Docker Hub,也可以自己搭私有的 Registry。

1.2 容器为什么这么轻

很多人容易把容器和虚拟机混为一谈,这俩在隔离层面的实现方式是完全不同的。

虚拟机会虚拟化出一整套硬件,然后在上面跑一个完整的操作系统,所以占用空间大、启动速度慢,通常要几十秒甚至几分钟。而容器是直接复用宿主机内核,只是通过 Linux 的 namespace 机制把进程的视图隔离出来,让它以为自己独占了一个系统;再用 cgroup 限制 CPU、内存、网络等资源的使用上限;最后通过 overlayfs 这样的联合文件系统实现分层镜像存储。

说白了,虚拟机是“硬件层面的虚拟化”,容器是“操作系统层面的虚拟化”。所以容器启动往往只需要几百毫秒,一个镜像可能只有几十兆,同一个物理机上可以跑几十个容器,这密度虚拟机完全比不了。

注意:因为容器共享宿主机内核,所以没办法在 Windows 内核上原生跑 Linux 容器。Windows Docker Desktop 的做法是借助 WSL2 或 Hyper-V 创建一个轻量级 Linux 虚拟机,然后在这个虚拟机上跑 Linux 容器,只是整个过程被 Docker 隐藏得很彻底,用起来和原生 Linux 没区别。

1.3 这套机制解决了什么问题

如果你只在本地写代码,Docker 的价值可能还没那么明显。一旦涉及团队协作、多环境部署、微服务拆分,痛点立刻出来了:

  • 开发环境、测试环境、生产环境配置不一致,每次上线都像开盲盒。
  • 新同事入职,配环境配一天,各种依赖版本冲突。
  • 微服务一拆十几个模块,光启动顺序和依赖就够喝一壶的。

Docker 把这些环境问题直接锁进镜像里,代码走到哪,环境就带到哪。这也是为什么 Docker 几乎成了后端开发、运维、测试、甚至是前端工程化绕不开的基础工具。

2. Docker环境安装与启动失败排查

2.1 Windows下安装Docker Desktop

Windows 上装 Docker 基本只有一条路线:Docker Desktop。它现在是 WSL2 优先的实现方式,我强烈建议你默认就走 WSL2,Hyper-V 方案在资源占用和磁盘性能上都不如 WSL2。

安装前先确认三件事,少了任何一个后面都可能报错:

  1. CPU 虚拟化已经开启。打开任务管理器,切到“性能”标签页,看底部“虚拟化”那项,必须是“已启用”。如果显示“已禁用”,需要进 BIOS 把 Intel VT-x 或 AMD-V 打开。
  2. Windows 功能里启用了“适用于 Linux 的 Windows 子系统”和“虚拟机平台”。可以管理员身份打开 PowerShell 执行:
    dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /restart dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /restart
  3. WSL 内核更新到 WSL2 版本,并且把默认版本设为 2:
    wsl --set-default-version 2

装完 Docker Desktop 后,界面直接可以用,命令行里敲docker version能看到 Server 信息就说明引擎起来了。

2.2 最头疼的启动失败:虚拟化支持检测不到

很多人安装完 Docker Desktop 一点启动,立刻报错:

Docker Desktop failed to start because virtualisation support wasn't detected.

或者:

Virtualization support not detected.

这基本是两个原因。

第一种是 BIOS 里真的没开虚拟化。这个对于品牌机和笔记本尤其常见,原因五花八门——有的是出厂默认关的,有的是装了某些虚拟化安全软件后把 VT-x 占用了。解决方法是重启按 Del/F2/F10 进 BIOS,找到Intel Virtualization TechnologySVM Mode,设为 Enabled 保存退出。

第二种是系统层面 Hyper-V 组件处于混乱状态。有时候之前装过旧版 Docker Toolbox 或别的虚拟化软件,残留配置会导致冲突。这种情况先确认 Windows 功能列表里的 Hyper-V 状态,然后管理员身份运行命令行,手动设置 Hypervisor 启动方式:

bcdedit /set hypervisorlaunchtype auto

执行完重启再开 Docker Desktop。

2.3 连接引擎失败:npipe 管道报错

另一个高频报错长这样:

failed to connect to the docker api at npipe:////./pipe/dockerDesktopLinuxEngine

这个报错的意思是 Docker CLI 想连接 Docker 引擎,但引擎根本没起来。排查思路按下面顺序来:

  1. 看系统托盘里的 Docker Desktop 图标,如果一直是启动动画或鲸鱼标是“starting”,说明引擎还在启动中,等一会儿就好。
  2. 如果一直起不来,打开 Docker Desktop 的 Troubleshoot 面板,点 Restart 一次。
  3. 如果还不行,直接重置 WSL2 相关组件。在 PowerShell 里执行wsl --shutdown,然后重新打开 Docker Desktop。
  4. 最后的大招是重置 Docker Desktop 到出厂状态:Settings -> Troubleshoot -> Clean / Purge data。这会删掉已有的容器和镜像,操作前先确认里面有没有重要数据。

2.4 Ubuntu和CentOS安装

Linux 装 Docker 不建议用发行版自带的软件源,版本太旧,很多新特性用不了。我一般用官方提供的安装脚本,一条命令搞定:

curl -fsSL https://get.docker.com | bash -s docker --mirror Aliyun

装完把当前用户加进 docker 组,省得每次 sudo:

sudo usermod -aG docker $USER newgrp docker

CentOS 7 的安装稍微麻烦一点,需要手动加仓库:

sudo yum install -y yum-utils sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo sudo yum install -y docker-ce docker-ce-cli containerd.io sudo systemctl start docker sudo systemctl enable docker

如果你的 CentOS 7 上是旧版本想升级,直接sudo yum install -y docker-ce,yum 会帮你升级到最新版,不用先卸载,但注意旧容器和镜像数据可能不兼容,升级前最好备份。

3. 镜像加速与高频命令速查

3.1 镜像下载慢,先配置registry-mirrors

学习 Docker 过程中第一个劝退点就是镜像下载慢。Docker Hub 的仓库大多部署在海外,受网络链路和高峰时段带宽影响,拉一个 Ubuntu 镜像都可能卡半天。

解决办法是给 Docker 配置镜像加速器。修改/etc/docker/daemon.json(Windows 是在 Docker Desktop 的 Settings -> Docker Engine 里改):

{ "registry-mirrors": [ "https://docker.m.daocloud.io", "https://dockerproxy.com", "https://docker.nju.edu.cn" ] }

改完保存,执行sudo systemctl restart docker

注意:这些公共加速源都是社区或机构维护的,稳定性好一阵坏一阵,哪天不给用了就换一批。加速的本质是让 Docker 从国内的缓存节点拉取公共镜像,如果你遇到某个加速源突然失效,换个源或者多配几个兜底就可以了。

另外一个实用经验是:如果某个镜像特别大,拉取总是超时,不要反复删了重拉同一整个镜像,异常中断时可以先执行docker pull重试,Docker 有分层缓存机制,已下载的分层不会重复下载。实在不行就换一台网络环境更好的机器 push 到自己的 Registry,再到目标机上 pull。

3.2 核心命令查得最多的那几张表

高频命令不需要背,多敲两遍自然就记住了。我把平时工作里最常用的整理成几组:

镜像操作

命令作用
docker pull nginx:latest拉取镜像,tag 不写默认 latest
docker images查看本地镜像列表
docker rmi nginx删除镜像
docker build -t myapp:v1 .用当前目录 Dockerfile 构建镜像
docker push myrepo/app:v1推送镜像到仓库
docker tag nginx myrepo/nginx:v1给镜像打标签

容器生命周期

命令作用
docker run -d --name nginx -p 8080:80 nginx后台运行容器
docker ps查看运行中的容器
docker ps -a查看所有容器,包含已停止的
docker start/stop/restart nginx启停容器
docker rm nginx删除容器
docker exec -it nginx bash进入容器内部
docker logs -f nginx跟随查看容器日志

数据卷与网络

docker volume create mydata docker run -d -v mydata:/data nginx docker network create mynet docker run -d --network mynet --name app nginx

3.3 端口映射和数据卷的底层逻辑

玩过几天 Docker 的人都知道-p 8080:80是把宿主机 8080 端口映射到容器 80 端口,但很多人不明白为什么需要这一步。

因为容器有自己的网络命名空间,和别人隔离的。你在宿主机上访问localhost:80时,流量进的是宿主机的网络协议栈,不是容器的。如果想让外部访问容器里跑的服务,就得显式地把宿主机的某个端口“桥接”到容器端口上。这个桥接是 iptables 规则实现的,所以如果你在宿主机上开了防火墙,忘了放行宿主机那个映射端口,外部一样访问不了。

数据卷-v解决的是另一个问题:容器默认是易失的。删除容器时,容器里所有写入的数据就一起没了。把宿主机的目录或数据卷挂载进容器,读写就会落到宿主机磁盘上,容器没了数据还在。

docker run -d --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=123456 \ -v /data/mysql:/var/lib/mysql \ mysql:8.0

这样/data/mysql目录里就是 MySQL 的物理数据文件,哪怕容器删了重建,数据也完好无损。这是生产环境使用 Docker 的底线要求:任何有状态的数据,必须落到数据卷上。

4. 真实场景部署与Compose编排

4.1 MySQL 8.0 部署:三分钟跑起来一个数据库

光说理论没感觉,直接上手部署几个真实服务。MySQL 8.0 是平时咨询量最大的一个。

docker run -d --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=root123 \ -e TZ=Asia/Shanghai \ -v /data/mysql:/var/lib/mysql \ -v /etc/localtime:/etc/localtime:ro \ mysql:8.0 \ --character-set-server=utf8mb4 \ --collation-server=utf8mb4_unicode_ci

这里几个关键点拆开讲:

-e MYSQL_ROOT_PASSWORD是容器首次初始化时创建 root 用户的密码。-v /data/mysql挂载数据目录,这是必须的。后面的--character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci是 MySQL 服务端启动参数,把默认字符集改成 utf8mb4,避免出现中文乱码问题。最后那两行command都会拼到 mysqld 启动命令后面。

跑起来之后,命令行验证一下:

docker exec -it mysql8 mysql -uroot -p

如果本机装了 MySQL 客户端,也可以直接mysql -h127.0.0.1 -P3306 -uroot -p连上去,相当于 DBA 日常操作的一个平滑迁移。

4.2 Redis 主从复制:一条命令搭建一主一从

Redis 用 Docker 跑主从,最大的好处是不用在本机装多个 Redis 进程,也不用担心端口冲突和数据目录混乱。先用 Docker 网络把两个容器打通:

docker network create redis-net

启动主节点:

docker run -d --name redis-master \ --network redis-net \ -p 6379:6379 \ redis:7 \ redis-server --requirepass 123456

启动从节点:

docker run -d --name redis-slave \ --network redis-net \ -p 6380:6379 \ redis:7 \ redis-server --masterauth 123456 --replicaof redis-master 6379

这里最容易踩的坑是:从节点的replicaof后面必须写主节点的“容器名”而不是127.0.0.1。因为在 Docker 自定义网络里,容器之间是通过容器名进行 DNS 解析的,如果你写127.0.0.1,从节点连的是它自己,这叫自欺欺人。

验证主从状态:

docker exec -it redis-slave redis-cli -a 123456 info replication

能看到role:slavemaster_link_status:up基本就成了。这套玩法还能继续扩展,热词里有人提到“Redis 生产环境部署”,我的建议是生产至少三主三从集群,用官方的 redis-cli --cluster 命令或者直接上云厂商的托管版,别自己折腾太简单的拓扑。

4.3 GitLab为例:docker-compose 一键部署全家桶

如果部署的服务多了,单个docker run命令不仅长,还容易漏参数。这时候用 Docker Compose 把编排配置全部写进一个 YAML 文件里,所有环境变量、端口映射、数据卷一目了然,一条命令完成整套服务的启停。

以 GitLab 为例,新建一个docker-compose.yml

version: "3.8" services: gitlab: image: gitlab/gitlab-ce:latest container_name: gitlab restart: always hostname: gitlab.example.com ports: - "8083:80" - "8022:22" - "8443:443" volumes: - /data/gitlab/config:/etc/gitlab - /data/gitlab/logs:/var/log/gitlab - /data/gitlab/data:/var/opt/gitlab shm_size: "256m"

启动:

docker compose up -d

查看状态:

docker compose ps

GitLab 是出了名的吃内存,官方推荐至少 4G,实测下来低于 2G 很容易启动失败或者运行期间 OOM。所以生产环境部署前先free -h看一下可用内存。

Compose 在数据卷上有个细节容易搞混:/data/gitlab/config:/etc/gitlab这种是“路径挂载”,宿主机路径必须提前建好;而如果写的是gitlab_config:/etc/gitlab这种,就是卷挂载,由 Docker 管理存储目录。路径挂载适合你想直接备份文件的情况,卷挂载更省心但文件位置不好找。这里用的是路径挂载,方便做备份和迁移。

4.4 开源项目的Compose模板直接抄

不只是 GitLab,现在绝大多数开源项目都默认提供了 docker-compose 模板。我记得第一次搭建 Dify 这类应用的时候,下载源代码后,进入主项目目录的 docker 文件夹里,按照文档操作:

cp .env.example .env docker compose up -d

一两分钟整个服务栈就起来了。里面的依赖服务,包括数据库、缓存、向量数据库、网关,全都由 compose 编排好了,这种项目天生就是为 Docker 设计的。

如果你要部署的是本地知识库、自动化工具这类需要依赖管理的项目,Docker 还有一个好处:依赖全部固化在镜像里,不会因为你手动在宿主机装依赖导致环境越搞越乱。我之前为了省事,在宿主机上直接装依赖跑服务,半年后各种版本冲突到崩溃,后来全部收敛进容器,一了百了。

5. 镜像构建与打包部署

5.1 用Dockerfile构建你自己的镜像

别人提供现成镜像可以直接拉,但业务系统就得自己写 Dockerfile 构建了。一个 Java Spring Boot 应用最简的 Dockerfile 长这样:

FROM openjdk:11-jre-slim WORKDIR /app COPY target/app.jar app.jar EXPOSE 8080 CMD ["java", "-jar", "app.jar"]

构建:

docker build -t myapp:v1 .

注意.是构建上下文目录,Docker 会把整个目录发给守护进程作为上下文。所以一定要在项目里加.dockerignore,把target/.git/node_modules/这些大目录排除掉,否则每次构建都慢得想砸电脑。

5.2 多阶段构建,镜像瘦身利器

如果你用 Java,基础的openjdk镜像动辄几百兆,推送到私有仓库、拉取到生产服务器都费劲。这时候多用多阶段构建。

需求是:先用一个包含完整编译工具链的镜像来编译项目,再把编译产物拷贝到精简运行时镜像里。以 Go 应用为例:

FROM golang:1.21 AS builder WORKDIR /src COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 GOOS=linux go build -o /app/app main.go FROM alpine:3.18 WORKDIR / COPY --from=builder /app/app . EXPOSE 8080 CMD ["./app"]

最终镜像只有十几兆。当年我第一次看到镜像体积从 800MB 降到 97MB 的时候,确实感觉打开了新世界。多阶段构建还能顺手解决编译环境与运行环境不一致的问题,因为最终镜像里只保留了运行必需的东西,攻击面小很多。

5.3 IDEA里直接打包镜像

日常开发中,IDE 里点点就能打包镜像是最省事的。IDEA 装 Docker 插件后,Dockerfile 右键就能 Build。

更主流的方式是结合 Maven 插件,比如 Google 的 Jib:

<plugin> <groupId>com.google.cloud.tools</groupId> <artifactId>jib-maven-plugin</artifactId> <version>3.4.0</version> <configuration> <to> <image>registry.example.com/myapp:${project.version}</image> </to> </configuration> </plugin>

执行:

mvn compile jib:build

它会自动推导基础镜像,把项目打成镜像并推送到远程仓库,全程不需要你写 Dockerfile。微服务项目里,十几个模块用这种方式批量打包太爽了。不过需要注意 Jib 默认会从远端的 Google Container Registry 拉基础镜像,在国内网络环境下建议配置jib.from.image为镜像加速器可访问的镜像地址。

5.4 国产化场景下的Docker

前面聊的多是 x86 架构,但热词里有“龙芯 docker”“人大金仓数据库 docker”,说明国产化环境里 Docker 的需求在起来。

龙芯的 LoongArch 架构跟 x86 完全不同,直接拉 x86 的镜像跑肯定起不来。目前的方案主要有两类:一是用支持 LoongArch 的镜像,随着生态完善,越来越多的官方仓库开始提供 loong64 版本;二是靠 QEMU 之类的模拟层跑 x86 镜像,但性能损失很明显,不建议用在线上。国产数据库如人大金仓、达梦等也都陆续发布了官方 Docker 镜像,很多厂家已经默认把 Docker 作为交付方式了,运维体验确实在变好。

6. 常见问题与排查技巧实录

6.1 权限报错:docker: permission denied

刚在 Linux 上装完 Docker 的用户,敲命令大概率会遇到:

docker: permission denied while trying to connect to the Docker daemon socket

原因是 Docker CLI 和引擎通信时需要访问/var/run/docker.sock,只有 root 用户和 docker 组的成员才有权限。解决办法就是把当前用户加进 docker 组,重新登录:

sudo usermod -aG docker $USER sudo reboot

注意:加入 docker 组就等于拿到了 root 级别的权限,因为 docker 组用户可以控制宿主机上的任意容器。生产服务器上别随便给人加,这点安全洁癖得有。

6.2 镜像拉取异常:unexpected EOF

还有一类高频报错是:

unexpected EOF

这个基本都是镜像下载过程中网络中断或者连接被重置导致的。Docker 守护进程在拉取大型镜像时,和远端仓库的连接保持时间较长,一旦链路不稳就会断。解决方案按顺序尝试:

  1. 直接重试几次docker pull ...,有时候就是波动。
  2. 换一个镜像加速器,多配置几个 mirror,一个失败会自动切换。
  3. 把镜像分割成多个小的,在 Dockerfile 里用多阶段构建减少单层体积。
  4. 实在不行,在稳定的机器上先构建好镜像,导出成 tar 再传输到目标机器。

6.3 常见问题速查表

问题现象可能原因排查命令 / 解决方法
容器启动后立刻退出,看不到任何日志前台进程结束docker logs 容器名看最后几行日志
端口映射不生效防火墙拦截检查宿主机防火墙,确认放行映射端口
容器内部时间不对时区未设置run 时加-e TZ=Asia/Shanghai
docker命令需要sudo才能用用户不在docker组usermod -aG docker $USER
容器数据删没了没挂数据卷确认是否需要挂载-v
镜像越来越多,磁盘满了无自动清理docker system prune -a
容器之间互相访问不通没在同一网络docker network create mynet,run 时加--network mynet
Docker Desktop连不上引擎WSL2异常wsl --shutdown后重启 Docker Desktop

6.4 一个检查优先级的经验

排障的时候我有一套固定的优先级,这套思路是踩了多少次坑总结出来的:先看容器状态,再看日志,最后猜配置。

很多人一遇到问题就翻配置文件,其实 90% 的问题都会在日志里告诉你答案。docker logs -f 容器名永远是你排查的第一站。日志没信息再考虑看环境变量、网络配置这些。

之前有个同事 MySQL 连不上,折腾半天防火墙端口映射,最后发现是启动时忘了设置 root 密码的环境变量,MySQL 容器初始化完连不上,日志里清清楚楚写着。所以,先日志,后配置,这个顺序错不了。

写在后面的一些心得

我自己从最早用docker run一条命令跑各种中间件,到后来整个微服务环境全用 Compose 编排,再到自己写 Dockerfile、搭私有 Registry、搞 CI/CD 自动打包,一步步走过来。最大的体会是:Docker 入门不难,但它背后那一套“一切皆镜像、环境即代码”的思想,才是真正值钱的东西。学命令只是表象,理解镜像的不可变性、容器的易失性、数据卷的持久性,再回过头看那些部署问题,思路会清晰很多。如果你刚开始学,不用贪多,按这篇的顺序,先把环境装好,把 MySQL、Redis 两个容器跑起来,再试着写一个最简单项目的 Dockerfile,整个链路走通一次,后面再深入就有底气了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 7:27:54

光伏储能微电网Simulink仿真与异步电机控制实践

1. 项目概述&#xff1a;光伏储能微电网的Simulink仿真实践在新能源并网领域&#xff0c;光伏储能微电网系统正成为解决分布式能源消纳问题的关键技术方案。这个Simulink仿真模型完整复现了基于异步电机的三相并网系统&#xff0c;包含光伏阵列、储能单元、异步电机和电网交互等…

作者头像 李华
网站建设 2026/9/16 7:27:43

Agent技能工程化:TypeScript + Nx + semantic-release 实战方法论

1. 项目概述&#xff1a;这不是一个“技能库”&#xff0c;而是一套可落地的智能体能力工程化方法论“agent-skills”这个名称乍看像一个泛泛而谈的术语&#xff0c;但结合它在 GitHub、Nx monorepo 生态和 TypeScript 工程实践中的真实使用场景&#xff0c;它根本不是指“AI a…

作者头像 李华
网站建设 2026/9/16 7:25:41

Docker部署SRS流媒体服务器实战:从RTMP到WebRTC全链路配置与踩坑记录

我刚用 Docker 把 SRS 流媒体服务器从头到尾部署了一遍&#xff0c;整个流程比想象中顺畅太多。今天把完整过程写出来&#xff0c;包括我踩过的坑、调过的参数、验证过的推拉流路径&#xff0c;一次性讲清楚。不管你是想自建直播服务、做视频监控平台、还是搞 WebRTC 低延迟通话…

作者头像 李华
网站建设 2026/9/16 7:25:02

基于Flask+Vue3的法律文档智能检索系统设计与实现

1. 项目背景与核心价值去年在帮一家律所处理案例检索需求时&#xff0c;我深刻体会到传统文档管理的痛点——他们积累的2000多份判决书和司法解释&#xff0c;就像被锁在铁柜里的宝藏。律师们要么靠记忆模糊定位&#xff0c;要么用Windows搜索碰运气。这促使我动手搭建了这个支…

作者头像 李华
网站建设 2026/9/16 7:24:51

配电网电压无功分布式优化与设备损耗MATLAB实现

1. 项目背景与核心挑战在配电网运行中&#xff0c;电压无功优化是维持系统稳定、降低网损的关键手段。传统集中式优化方法虽然理论成熟&#xff0c;但在面对分布式电源高渗透率的现代配电网时&#xff0c;暴露出计算复杂度高、通信负担重等明显短板。更棘手的是&#xff0c;频繁…

作者头像 李华
网站建设 2026/9/16 7:24:38

macOS LaTeX环境搭建:MacTeX+Sublime Text+Skim配置完全指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华