news 2026/9/13 7:10:01

Haskell 入门安装实战:从 GHCup 到第一个程序

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Haskell 入门安装实战:从 GHCup 到第一个程序

1. 为什么要在 2024 年折腾 Haskell

先聊点实在的。标题叫“Haskell 入门学习(一)之安装试用”,但真正动手之前,我们得先搞清楚一个问题:你为什么想学 Haskell?

如果你是因为听说“函数式编程能改变思维方式”才来的,那这话只说对了一半。Haskell 的确会逼你用另一种方式思考问题——它不让你随手修改变量,不让你随意写副作用,类型系统严格到近乎苛刻。这种束缚一开始很难受,但熬过前两周,你会发现自己写任何代码时脑子里都会多一根弦:这个函数到底纯不纯?这个类型到底能不能表达得更精确?这种习惯一旦养成,你再回头看 Python 或 Java 代码,会本能地捕捉到很多以前看不出来的坏味道。

如果你是因为想换一门生产级语言来找机会的,Haskell 的生态实话实说不如 Java、Go 或 Rust 那么热闹。但它在编译器、金融风控、区块链、形式化验证这些领域有自己稳固的位置。GHC(Glasgow Haskell Compiler)本身就是一个极其强大的编译器,很多现代语言的特性(比如类型类、函子、单子这些概念)都能在 Haskell 里找到最纯粹的源头。哪怕你只是把 Haskell 当成“类型系统实验室”,也完全不亏。

这门语言适合谁来学?我个人的建议是:如果你已经写过至少一两年代码,对循环、变量、函数调用这些基本概念不陌生,那 Haskell 很适合作为你的“第二语言”或“思维扩充语言”。如果你完全零基础、第一次接触编程,那不建议直接上来就啃 Haskell,函数式思维叠加语法学习会很痛苦,容易劝退。

这一篇先解决一个最务实的痛点:安装、跑通编译器、写第一个能运行的程序。我踩过的各种坑,都会尽量写出来。

2. 动手前的准备工作:环境评估与工具链认识

2.1 先搞清楚 Haskell 工具链的组成

Haskell 不是“下载一个 IDE 就能跑”的语言,它有一整套工具链。很多新手死在这一步:不知道装哪个、装完发现版本对不上、编译器报错一脸懵。所以先把工具链的构成搞明白,后面安装时心里就有底了。

一个完整可用 Haskell 环境包括这几块:

  • GHC(Glasgow Haskell Compiler):核心编译器,把.hs源码编译成可执行文件。没有它,一切都免谈。
  • Cabal:Haskell 的构建和打包工具,负责下载依赖库、配置构建、生成项目。类似 Python 里的 pip + setuptools 的合体。
  • Stack:另一个构建工具,更强调可复现构建,可以锁定 GHC 版本和依赖版本。Stack 本身会调用 GHC,所以装 Stack 不代表不用装 GHC。
  • HLS(Haskell Language Server):给编辑器(VS Code、Neovim)提供补全、跳转、类型提示的语言服务协议实现。不装也能写代码,但装了体验会好很多。
  • ghcup:Haskell 工具链的版本管理工具,类似 Python 的 pyenv 或 Node 的 nvm。它能把 GHC、Cabal、HLS 全部管理起来,切换版本非常方便。

现在主流的安装方式就是通过 ghcup 来完成,不要手动去官网下载 GHC 二进制包然后自己配环境变量。手动装很容易出现路径找不到、版本冲突的问题,用 ghcup 统一管理能省掉一大半烦恼。

2.2 各操作系统的注意事项

我分别说说三平台的情况。

Windows 用户:微软官方做了一个叫 GHCup 的安装程序(原生 Windows 版),支持 PowerShell 一键脚本安装,也支持通过 MSYS2 来装。Windows 下最容易踩的坑是 PATH 路径里有空格或者中文,或者是杀毒软件拦截 ghcup 的下载过程。建议直接去 https://www.haskell.org/ghcup/ 复制官方 PowerShell 命令,用管理员权限的终端运行。Windows 下另外一个坑是 HLS 编译时可能缺 Visual Studio Build Tools,如果之后遇到“C compiler not found”之类的报错,需要先装 VS Build Tools C++ 工作负载。

macOS 用户:同样用 ghcup 官方脚本。但 macOS 用户要注意两点:一是 Apple Silicon(M 系列芯片)下载的是aarch64版本的 GHC,ghcup 会自动检测,不用手动干预;二是 macOS 自带的 Xcode Command Line Tools 必须装,否则后续编译代码时连接器会报错。如果遇到clang: error: linker command failed,大概率就是 Command Line Tools 没装完整,运行xcode-select --install补一下。

Linux 用户:最省心。需要系统里有build-essentialcurllibffi-devlibgmp-devzlib1g-dev这些依赖。Debian/Ubuntu 系直接apt install build-essential curl libffi-dev libgmp-dev libgmp10 zlib1g-dev就行。CentOS/RHEL 系用自己的 yum/dnf 同步安装。

我自己的经验是:有条件的话,尽量在 Linux 或 macOS 上学习 Haskell。不是说 Windows 不行,而是很多开源教程、代码示例默认在 Unix 环境下测试,Windows 偶尔会碰到路径分隔符、命令行参数解析不一致的小问题,排查起来比较消耗热情。学习阶段,减少环境摩擦就是提高学习效率。

3. 安装全过程:一步步跑通 GHCup + GHC

3.1 用 ghcup 安装:最小可行步骤

先给一个能在 99% 情况下降落的最小步骤清单。以 Linux/macOS 为例:

# 1. 下载并安装 ghcup(官方脚本) curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh # 2. 脚本执行过程中会问是否自动添加 PATH、是否安装 HLS 等 # 新手建议全部选 yes(直接回车),这样后续不用再手动配置 # 3. 让 shell 加载新环境变量 source ~/.bashrc # 如果你用 zsh,就 source ~/.zshrc # 4. 确认 ghcup 安装成功 ghcup --version # 5. 安装推荐版本的 GHC(ghcup 会自动选择推荐版本) ghcup install ghc recommended # 6. 安装 cabal ghcup install cabal recommended # 7. 设置默认版本(这条很关键,否则 shell 找不到 ghc 命令) ghcup set ghc recommended ghcup set cabal recommended # 8. 验证 ghc --version cabal --version

如果一切顺利,你会看到类似这样的输出:

The Glorious Glasgow Haskell Compilation System, version 9.6.5 cabal-install version 3.10.1.0 compiled using version 3.10.1.0 of the Cabal library

3.2 Windows 下的安装细节

Windows 下用官方 PowerShell 脚本:

# PowerShell 以管理员身份运行 Set-ExecutionPolicy Bypass -Scope Process -Force [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072 Invoke-Command -ScriptBlock ([ScriptBlock]::Create((Invoke-WebRequest https://get-ghcup.haskell.org -UseBasicParsing)))

Windows 安装过程会多几个交互选项:

  • 是否安装 MSYS2(建议选 yes,它是 Windows 下的 Unix 工具环境,后续很多包的编译依赖它)。
  • 是否安装 HLS(建议选 yes,编辑器补全要靠它)。
  • 是否将 ghcup 的目录加到 PATH(必须选 yes)。

装完重新打开终端,命令行输入ghc --version测试。

3.3 等下载时你需要知道的事

ghcup 下载 GHC 时实际上是在下载一个预编译的压缩包,体积大概在 200~300MB 之间,具体取决于平台。如果你在内网或网络环境一般,可以用镜像加速,比如设置 ghcup 的下载源。在~/.ghcup/config.yaml里可以配置url-source,但新手不需要折腾这个,默认源一般都能跑完。

下载慢还有另一个物理原因:GHC 本身是个大编译器,包含了很多架构相关的东西。第一次安装等个几分钟很正常,别以为卡住了终止线程。

3.4 非 ghcup 的备选:Stack 一键安装

如果你不想记那么多命令,Stack 也提供了一键安装脚本。Stack 在装完自己之后,第一次运行stack setup时也会自动下载匹配版本的 GHC 并内部管理,对新手来说反而更“无脑”一点。

# Linux/macOS curl -sSL https://get.haskellstack.org/ | sh # 创建并运行一个项目 stack new my-project cd my-project stack setup stack build stack exec my-project-exe

Stack 的优势是项目级隔离:每个项目锁定各自使用的 GHC 版本,团队协作时不会因为个人环境差异导致构建失败。我个人的经验是:如果你准备认真学 Haskell、后面可能会参与开源项目,那 ghcup + cabal 的组合更接近社区的主流玩法;如果你只是想快速跑个 demo、不想关心版本细节,Stack 更省心。两条路殊途同归,选一条深入即可。

4. 编辑器选型与 HLS 配置:写第一行代码之前

4.1 VS Code:新手最稳妥的选择

老老实实说,如果让我推荐一个最适合新手的 Haskell 编辑器,我还是会提名 VS Code。原因很简单:HLS 在 VS Code 上集成得最成熟,插件安装向导在官方文档里写得也很直白。

插件名是Haskell,开发者是 haskell.org 官方组织。在 VS Code 扩展商店里搜“Haskell”就会出现,直接安装。装完后它会自动定位你系统里的 HLS。如果你按前面的步骤通过 ghcup 安装了 HLS,那基本上零配置就能用。

打开一个.hs文件,你会看到:

  • 输入时实时显示类型签名(这是 Haskell 开发最爽的一个体验)。
  • 错误信息直接标红波浪线。
  • 支持跳转定义、查看所有引用。
  • 自动补全模块导出/导入。

4.2 Neovim / Emacs 等其他选择

用 Neovim 的话,LSP 配置方式是通过内置的vim.lsp客户端连接 HLS。这里给一个最小的配置片段(如果你用 lazy.nvim 插件管理器):

return { { "neovim/nvim-lspconfig", opts = { servers = { haskell = { settings = { haskell = { formattingProvider = "ormolu", }, }, }, }, }, }, }

用 Emacs 的话,通过lsp-modeeglot连接 HLS,配合haskell-mode使用。具体配置会复杂一点,有兴趣的读者可以在官方 HLS 文档里找到 Emacs 的配置示例。

4.3 HLS 不工作的排查思路

HLS 启动失败的典型症状:VS Code 右下角提示 “Haskell language server is not running”。这时候按顺序排查:

  1. 终端跑一下haskell-language-server --version,看命令是否存在。
  2. 如果命令不存在,回 ghcup 跑一次ghcup install hls recommendedghcup set hls recommended
  3. 如果版本匹配也没问题但还不行,看 HLS 的日志文件。实际工作中,最常见的两个原因是 GHC 版本与 HLS 版本不匹配,或者项目路径里有中文/空格导致 LSP 无法正常解析。

强烈建议(来自踩坑):新手期项目路径,一律只用英文、不要带空格。

5. 第一个 Haskell 程序:从 GHCi 到编译运行

5.1 GHCi:交互式环境试水

GHCi 是 Haskell 的交互式解释器,类似 Python 的 REPL。在终端里输入ghci就能进入。先玩几个最基本的表达式:

Prelude> 1 + 2 3 Prelude> "Hello, " ++ "World" "Hello, World" Prelude> :t (+) (+) :: Num a => a -> a -> a

这里特别注意一下:t命令——它用来查看表达式的类型。Haskell 里一切皆有类型,这是理解这门语言的钥匙。比如(+) :: Num a => a -> a -> a读作:(+)的类型是“对于所有Num类型的a,接受一个a和一个a,返回一个a”。这种读法要尽快适应。

一个新手最容易用错的命令是:q,很多人想退出 GHCi 时会输exitquit,全都不识别。正确姿势是:quit:q。记不住就死死记住这一条,真的能避免很多无谓的卡壳。

5.2 编写并运行一个真正的程序文件

GHCi 适合临时试东西,但真正写程序还是要建.hs文件。我们写一个经典的程序,顺便展示 Haskell 的核心特性:

-- hello.hs module Main where -- 带有类型签名的函数定义 greet :: String -> String greet name = "Hello, " ++ name ++ "!" main :: IO () main = do putStrLn "What's your name?" name <- getLine putStrLn (greet name)

这个程序展示了四个关键概念:

  • 类型签名greet :: String -> String表示greet接收一个字符串,返回一个字符串。
  • 纯函数与 IO 的分界greet是纯函数,它不读取任何外部状态,同样的输入必然得到同样的输出。而main的类型是IO (),表示这是一个会产生 IO 副作用(读用户输入、打印到屏幕)的动作。
  • do语法块:虽然 Haskell 本身是纯函数式语言,但处理 IO 时必须依赖do语法块,这背后是 Monad 的概念(现在先不用深究)。
  • <-绑定name <- getLine表示执行getLine这个 IO 动作,并把结果“取出”绑定给name<-并不是简单的赋值,它是从“执行动作”到“拿到纯值”的桥梁。

运行方式有三种:

# 方式一:用 runhaskell 直接运行(不编译,适合快速测试) runhaskell hello.hs # 方式二:交给 ghc 临时编译并运行 ghc -o hello hello.hs ./hello # 方式三:在 GHCi 里加载 ghci hello.hs *Main> main

我用ghc -o hello hello.hs编译时特意加了个-o参数来指定输出文件名,否则默认会生成一个叫hello(Linux/macOS)或hello.exe(Windows)的可执行文件。编译完成后目录里会多出hellohello.hihello.o两个中间文件,删掉不影响程序,只影响重新编译的效率。第一次看到这些文件不用慌,它们就是界面文件(.hi)和对象文件(.o)。

5.3 消灭第一个编译错误的心法

如果你是完全的新手,下面这种报错几乎一定会遇到:

hello.hs:8:13: error: • Couldn't match expected type ‘String’ with actual type ‘IO String’ • In a stmt of a 'do' block: name <- getLine

这句话翻译过来是:getLine的类型是IO String,你把它绑定给name,但后续使用greet name时要求name是纯的String。解决办法的关键在于理解:<-已经把IO String里头的String提出来了,所以这里的报错实际不是发生在绑定那行,而是发生在其他地方。

这类错误排查时有个心法:看报错位置,更要看报错类型。Haskell 的类型错误信息通常比大多数语言精确很多。它告诉你的“期望类型(expected type)”和“实际类型(actual type)”差异,往往是定位根因的钥匙。不要在没理解类型是什么之前就去网上抄答案,那会让你养成瞎猜的习惯。

6. 新建项目:用 Cabal 搭建标准工程结构

6.1 为什么建议从第一课就用 Cabal 项目

很多入门教程会让新手直接单文件跑,这样确实简单,但到了第二、第三课开始引入依赖库时,单文件模式马上就不够用了。Cabal 是 Haskell 官方的“标准项目管理工具”,学习曲线前期稍微陡一点,但早用早享受。

创建一个新项目:

cabal init --interactive # 交互式初始化 cabal init --non-interactive # 也可以用非交互模式,但会生成一堆默认值

交互式初始化会问几个问题:

  • 项目名称:输入一个合法的包名,不能有大写字母和空格。
  • 版本号:默认 0.1.0.0 就行。
  • 许可证:选一个,比如 BSD-3-Clause,或者直接选 AllRightsReserved(学习项目无所谓)。
  • 是否生成测试套件:选 yes,方便以后练习写测试。
  • 源目录:默认src目录,保持默认。

生成之后,项目结构长这样:

my-project/ ├── src/ │ └── Main.hs ├── my-project.cabal ├── cabal.project ├── CHANGELOG.md ├── LICENSE └── Setup.hs

6.2 在 Cabal 里配置依赖

打开.cabal文件,找到executable那一节,你会看到:

executable my-project main-is: Main.hs build-depends: base ^>=4.17 hs-source-dirs: src default-language: Haskell2010

build-depends是核心,它声明了这个可执行文件的依赖包。例子里的base ^>=4.17表示“使用 base 库的 4.17.x 系列”。base 库是 GHC 自带的,包含 Prelude(预加载的标准库),每个 Haskell 程序都隐式依赖它。

如果我们需要 JSON 解析库aeson,就把它加进去:

build-depends: base ^>=4.17, aeson ^>=2.1

然后在终端跑:

cabal build

Cabal 会自动解析依赖、下载库、编译,这个过程可能需要一些时间,因为会拉取传递依赖。

6.3 一个小坑:镜像源与依赖下载

在国内网络环境下,Hackage(Haskell 的中央包仓库)有时下载很慢,这会让cabal build变成漫长的折磨。此时可以在~/.cabal/config文件里配置镜像。查找文件里的repository hackage.haskell.org段落,修改如下:

repository hackage.haskell.org url: https://HackageMirrorURL secure: True

镜像地址的完整写法以你实际能访问到的为准。这类个人经验供参考,万一真的访问不动官方源,搜索引擎搜“Hackage 镜像”很快就有答案。我自己实测下来,国内网络环境下使用镜像后,cabal build的速度从经常超时变成了几分钟内稳定跑完。

7. 新手最容易踩的五个坑

7.1 路径含中文或空格导致 HLS 不工作

这个问题在前面提过一嘴,但值得单独放到坑名单里。VS Code 打开项目路径如果带了中文,HLS 的连接偶尔会有路径解析错误,GHCi 加载模块也可能出问题。解决方案只有一个:项目目录用纯英文、小写字母、不用空格。我的 macOS 用户名是中文拼音,所以吃过好多次这个亏,后来索性在主目录下建了一个~/haskell专门放实验项目。

7.2 GHC 版本与包版本不匹配

cabal install some-packagePackage has never been built或者cannot meet PVP之类的问题,大多数是因为包的版本与 GHC 版本冲突。Cabal 的依赖解析机制非常严格,它要求所有包版本满足约束条件。新手这时候别硬装(比如用--force-reinstalls),那是大人物的武器,不是新手的工具。

正确的处理是让 Cabal 自主选择一套可行的依赖集合:

cabal update cabal build --constraint "some-package == x.y.z"

或者直接cabal build,让 Cabal 告诉你哪个版本冲突了,再按提示调整。Haskell 的 PVP 规则(a.b.c.d版本号中,首位增加意味着 API 破坏性变化)在初期会保护你,但前提是你别用--force去破坏它。

7.3 被pureIO搞晕

看到purereturn时,很多 Haskell 新人会误以为它们是流程控制的关键字(比如退出函数)。实际上在 Haskell 里,return只是一个把纯值“包”进 Monad 的普通函数:

main :: IO () main = do let x = 5 return x -- 这一行的结果直接被丢弃了,程序没有输出

我一开始学的时候写过这种代码,怎么运行都没输出,怀疑了半天人生。后来才理解:return不是“返回并结束”,而是构造一个值为xIO Int动作。如果想要打印,得print x或者putStrLn (show x)return当成一种“包装动作”而非“提前退出”,对理解 Haskell 的 IO 模型至关重要。

7.4 死磕惰性求值的顺序陷阱

Haskell 默认惰性求值(lazy evaluation),这意味着表达式默认不立即计算,而是在需要时按需计算。这给 IO 的顺序带来一个微妙的陷阱:

main :: IO () main = do putStrLn "Step 1" let x = expensiveComputation putStrLn "Step 2" print x

这个程序输出顺序是:

Step 1 Step 2 (然后才执行 expensiveComputation 并打印结果)

因为let x = ...绑定了一个“未求值的表达式”,而不是“算好的值”。x只有在print x需要它的值时才被真正求值。这个特性在纯计算里是优化神技,但在新手期容易造成困惑。

7.5 分组混乱:布局规则引发的缩进报错

Haskell 用缩进(layout)来代替花括号表示代码块。这意味着:

main :: IO () main = do putStrLn "Hello" name <- getLine putStrLn ("Hi, " ++ name)

do后的每个语句必须对齐在同一列的缩进位置。如果某个语句多缩进四格,GHC 会认为它是一个嵌套代码块的开始,立刻报parse error。这个问题我在前两周几乎每隔几个文件就犯一次。排查方法很简单:看报错行和上一行是否同一缩进级别,以及do关键字后是否忘了换行

8. 从安装到能用:这份 checklist 请收好

结尾之前,把整篇的核心步骤整理成清单,方便你照着走。这是我几次在干净机器上装 Haskell 总结出来的,按顺序执行基本不会出错。

  1. 安装前:确认系统依赖(Linux 装 build-essential,macOS 装 Command Line Tools,Windows 装 VS Build Tools)。
  2. 安装 ghcup:运行官方脚本,交互选项全部选 yes。
  3. 安装 GHC 和 Cabalghcup install ghc recommended,然后cabal update
  4. 安装 HLSghcup install hls recommended,装完验证haskell-language-server --version
  5. 配置编辑器:VS Code 装 Haskell 插件,打开一个空的.hs文件,测试补全是否生效。
  6. 跑交互式测试:在 GHCi 里输入几个表达式,确认类型查询:t好用。
  7. 写第一个文件:复制上文hello.hs,在项目目录用runhaskell hello.hs直接跑一版。
  8. 用 Cabal 建项目cabal init --non-interactivecabal build,跑一遍标准构建流程。
  9. 安全检查:确认所有项目路径里没有中文和空格。

9. 下一步学什么

安装试用只是第一步。经过这一篇,你已经能够运行表达式、编译程序、构建 Cabal 项目了。这已经超过了 60% 只是在网上看过教程的人。

接下来的学习路径,我的建议是按这个顺序推进:

  1. 类型系统基础:代数数据类型、模式匹配、类型类。这是 Haskell 最强大的部分,也是最容易理解“为什么会有函数式编程”的起点。
  2. 纯函数与递归思维:忘掉 for 循环,用迭代器和递归解决问题。这个过程很痛苦,但价值极高。
  3. 函子、应用函子、单子:这三个概念被誉为 Haskell 的“三大件”。别急着一口气全搞懂,先在代码里用起来,用熟之后回头再看理论。
  4. 常用库的实践text(高效字符串)、containers(Map/Set/Sequence)、aeson(JSON 解析)、http-conduit(HTTP 请求)、opticslens(复杂记录访问)。
  5. 真实项目练手:写一个命令行工具、写一个小型 web 服务(用 servant 或 scotty),用真实需求逼自己查 API 文档。

每学会一个新概念,就回到 GHCi 里亲手验证一遍,这是我在 Haskell 学习路上最高效的方法。别怕写错,编译器就是你的导师——它会用精确的类型错误告诉你哪里想歪了。

写到这里,我想起自己刚装完 Haskell 时盯着ghci提示符发呆的场景:满屏英文,一个程序都写不出来,前面的空白仿佛在嘲讽我。但在跑了几天:t之后,那种“一个类型能被语言自己推出来”的感觉真的会上瘾。希望这一篇搭建环境的小文章,能帮你跨过最枯燥的第一道门槛,早点体验到这种乐趣。

现在,打开终端,输入ghci,亲手敲一行map (+1) [1,2,3],看看结果是什么。这是你 Haskell 之旅的第一个指令,玩得开心。

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

MCP协议实战:从零构建AI Agent工具链

先聊个最近都绕不开的场景。你手上有一个大模型应用&#xff0c;希望它能像真正的助手一样去查资料、读文件、调接口&#xff0c;而不再只是“对话框里聊天”。这时候你就需要做 AI Agent 开发&#xff0c;而 Agent 一旦要干活&#xff0c;第一个要解决的就是工具链怎么接。过去…

作者头像 李华
网站建设 2026/9/13 7:08:31

Chrome侧边栏投屏替代QtScrcpy的技术演进

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

作者头像 李华
网站建设 2026/9/13 7:07:19

如何为 GitHub 账户添加 passkey 并用附近设备完成登录

如何为 GitHub 账户添加 passkey 并用附近设备完成登录 【免费下载链接】docs The open-source repo for docs.github.com 项目地址: https://gitcode.com/GitHub_Trending/do/docs 这篇文章面向想要摆脱密码登录的 GitHub 账户使用者&#xff1a;先为自己的账户注册一个…

作者头像 李华
网站建设 2026/9/13 7:07:18

运算放大器设计实战:虚短虚断、11种经典电路与稳定性分析

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

作者头像 李华