ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

R Markdown电子书构建:bookdown长文档编排与交叉引用实战

R Markdown电子书构建:bookdown长文档编排与交叉引用实战

打开一堆技术文档站点,你会发现一个很有意思的现象:左侧目录、顶部搜索框、右上角的下载按钮、章节末尾的“上一节/下一节”,长得几乎一模一样。再看几本独立作者写的中文技术书,排版风格、图表编号方式、公式渲染效果,还是那一套。这不是巧合,这些成品大概率都出自同一套源文件构建流程——bookdown。这个标题其实说的就是这件事:别人书架上的电子书,和你手里正在折腾的电子书,用的是同一套工具链。别人能做出那种结构完整、图表可交叉引用、一份源文件同时出网页和 PDF 的书,你也能。这篇文章不是官方文档的复述,而是我把 bookdown 真正用在长文档项目里之后,关于目录结构怎么定、交叉引用为什么老报错、PDF 构建失败该从哪一步查起这些实打实的问题的整理。适合已经会写 R Markdown、准备把零散笔记升级成一本完整文档的人看,也适合被 Word 的目录和图表编号折磨过、想换条路的人参考。

1. bookdown 在整条写作流水线上到底站哪个位置

1.1 它不是编辑器,而是一层“编译与编排”

很多人第一次接触 bookdown,会下意识把它当成一个写作软件去找界面,找了半天发现它根本没有独立的编辑器,于是就觉得这东西门槛高。其实定位搞错了:bookdown 是 R 的一个扩展包,它做的事是接收一堆 Markdown 源文件,按你定义的顺序和规则,把它们编译成一本结构完整的书。编辑器可以是任何你顺手的文本编辑器,bookdown 只管“编排”和“输出”这一段。

这个分工带来一个直接好处:源文件永远是纯文本。你没有锁定在任何私有格式里,哪天想换工具,把.Rmd文件拷走就行;想用版本管理工具对比两版差异,纯文本的 diff 结果干净得能一眼看出改了哪句话,不会出现二进制文件那种“整个文件都变了”的绝望场面。

真正干活的底层其实有两层:Markdown 到中间格式的转换由通用文档转换工具负责,bookdown 在其上加了一层“书”的概念——章节合并、编号、交叉引用解析、多格式调度。理解这个分层很关键,因为后面所有报错定位都要靠它:有些错误是 Markdown 语法层面的,有些是 bookdown 的编号逻辑层面的,还有些是最终排版引擎层面的,三者混在一起排查会非常痛苦。

1.2 从 .Rmd 到成品的三个中间态

构建过程中会生成一批中间文件,很多人从来不看,出了问题就抓瞎。我建议至少知道这三个:

中间产物出现位置作用出问题时能说明什么
_main.Rmd项目根目录(可配置自动删除)所有章节按顺序合并后的单文件章节顺序不对、内容缺失,看它
_bookdown_files/项目根目录缓存、图片、临时资源目录缓存不失效、图片找不到,看它
_book/默认输出目录最终成品样式、链接、下载按钮,看它

我遇到过最典型的一次,是某章内容在网页版里完全消失,但源文件明明白白写着。把delete_merged_file临时关掉,打开合并后的_main.Rmd一看,那一章压根没被拼进去——原因是文件名前缀用了两位数没问题,但我中途改过一次文件名,rmd_files里漏写了这一项。这类“内容凭空消失”的问题,九成能在合并文件里当场定位。

1.3 什么样的项目值得上手,什么项目其实不必

bookdown 不是万能的。我把它适用和不适用的场景列一下,省得你走弯路:

适合的场景很明确——章节数超过十个、需要统一的图表公式编号、需要被反复引用和跳转、需要同时出网页版和可打印版。比如技术手册、系列教程、课程讲义、长篇调研报告、内部规范文档。这类文档的共同特征是“结构比文采重要”,正好是 bookdown 的强项。

不太适合的场景也得说清楚。单篇两三千字的文章,用 bookdown 属于杀鸡用牛刀,构建一次的等待时间都比写作时间长;强排版要求的出版物,比如需要精细控制字体、行距、页边距的印刷品,用所见即所得的排版软件或者专业排版工具更省事;团队里没有一个人愿意碰命令行和配置文件的,更现实的方案是先统一用普通文档工具把内容攒够,再考虑迁移。

我自己的一条判断标准是:如果你在写的过程中,需要反复问“这是第几章第几节、这个图在哪一页被引用了”,那就该换 bookdown 了。手工维护这种编号关系,超过二十个引用点之后,出错率是指数上升的。

2. 搭出一个能跑起来的最小骨架

2.1 目录结构与那几个不能少的文件

bookdown 项目看起来文件不少,但真正必须的就那么几个。我的建议是起步阶段越简单越好,先跑通再加装饰。一个可用的最小结构大概长这样:

my-book/ ├── index.Rmd # 必须有,全书入口,含标题、作者、前言 ├── 01-intro.Rmd # 第一章 ├── 02-basics.Rmd # 第二章 ├── 03-advanced.Rmd # 第三章 ├── _bookdown.yml # 书级配置:顺序、文件名、语言标签 ├── _output.yml # 输出配置:出哪些格式、各格式参数 └── style.css # 自定义样式(可选)

index.Rmd是硬性要求,没有它构建直接失败。这个文件不只是“首页”,它承载全书级别的元信息,写在文件开头的 YAML 区块里:

--- title: "长文档写作实战手册" author: "某开发者" date: "2024-10-01" site: bookdown::bookdown_site documentclass: ctexbook ---

这里有个新手常踩的坑:site字段。它告诉构建流程“这是一个 bookdown 站点项目”,漏了这一行,某些构建入口会把它当成单篇文档处理,结果只输出一个 HTML 文件,所有章节挤在一页里。我第一次遇到时还以为是自己配错了分章参数,查了半天才发现是这一行没写。

2.2_bookdown.yml与_output.yml的分工

这两个文件名字很像,作用完全不同,很多人会搞混。简单说:_bookdown.yml管“书本身”,_output.yml管“怎么输出”。

_bookdown.yml里我最常用的几项:

book_filename: "writing-handbook" output_dir: "_book" delete_merged_file: true new_session: yes language: ui: chapter_name: "第 " label: fig: "图 " tab: "表 " rmd_files: - index.Rmd - 01-intro.Rmd - 02-basics.Rmd - 03-advanced.Rmd

rmd_files这一项值得单独讲。不写它的时候,bookdown 会按文件名的字典序自动收集所有.Rmd文件。字典序听起来没问题,但只要你用过10-xxx.Rmd和9-xxx.Rmd混排,就会知道“10”排在“9”前面有多让人抓狂。显式列出顺序是唯一可靠的解法,而且顺带解决了“临时草稿文件被误拼进书里”的问题——没列进去的文件,永远不会出现在成品里。

new_session: yes是针对代码型文档的。默认情况下所有章节在同一个 R 会话里跑,前面章节定义的变量会污染后面章节的环境,导致“单独跑这一章没问题,整本构建就报错”这种玄学问题。开启后每章一个干净会话,代价是构建变慢,但我认为这个代价必须付。

_output.yml则定义输出格式:

bookdown::gitbook: css: style.css split_by: chapter config: toc: collapse: subsection download: ["pdf", "epub"] search: yes sharing: null bookdown::pdf_book: latex_engine: xelatex keep_tex: yes bookdown::epub_book: default

split_by: chapter决定了网页版是每一章一个 HTML 还是整本一个页面。长文档必须按章拆,否则单个页面几兆大小,浏览器打开就卡。download那一行则把 PDF 和 ePub 的下载入口挂到网页右上角——这就是你在别人书上看到的那个下载按钮的来源。

2.3 第一次构建:命令、目录与第一个报错

配置齐了,构建命令就一行:

bookdown::render_book("index.Rmd", "bookdown::gitbook")

如果你习惯点按钮,编辑器里的构建入口最终执行的也是这一句。第一次跑的时候,我建议先只出网页版,别一上来就三个格式全开。原因很实际:网页版对环境的依赖最少,只要能跑 R 就行;PDF 需要完整的 LaTeX 环境,ePub 需要额外的转换组件。三个一起开,报错信息交叠在一起,你根本分不清是配置问题还是环境问题。

构建成功后,_book/目录里应该有一个index.html,双击打开就能看到左侧目录、正文、搜索框。如果打开是空白页或者 404,先检查output_dir里到底有没有文件,再检查index.Rmd里的site字段——这两个是“构建说成功了但打不开”的常见原因。

我个人的习惯是:每加一个新章节,就先构建一次网页版确认它能进目录,再往下写内容。增量验证的成本,远低于写完五章之后一次性排查。

3. 交叉引用和自动编号:长文档真正的门槛

3.1 标签命名规则与冲突排查

bookdown 最值钱的功能就是这个,也是最容易出问题的功能。核心机制是:你给图、表、公式、章节打一个标签,写正文时用\@ref(标签)引用,构建时自动替换成正确的编号。改了章节顺序、插了新图,编号全部自动更新,不用手工核对。

标签的写法有一套约定,前缀区分类型:

引用对象定义方式引用写法
章节# 方法设计 {#method}\@ref(method)
图代码块标签fig-scatter\@ref(fig:fig-scatter)
表代码块标签tab-summary\@ref(tab:tab-summary)
公式(\#eq:linear)\@ref(eq:linear)
定理类(\#thm:main)\@ref(thm:main)

这个表格我建议直接存进备忘录。因为这里的命名规则有一处反直觉:图的引用前缀是fig:,但标签本身通常也以fig-开头,于是引用写出来是\@ref(fig:fig-scatter),看着像重复了一遍。这不是笔误,是设计如此。第一次写的人几乎百分百会写成\@ref(fig-scatter),然后得到一个原样输出的红字或者干脆是问号。

冲突问题更隐蔽。两个章节用了同一个标签,bookdown 通常不会明确报错,而是其中一个引用默默指向了错误位置。排查方法是搜索源文件里的{#,把标签列表拉出来人工扫一遍。我现在养成的习惯是标签统一加章节前缀,比如第三章的图用fig-ch3-scatter,从命名上就杜绝跨章撞车。

3.2 图、表、公式三类引用的具体写法差异

三类对象的写法差异比想象中大,逐个说。

图最简单,标签直接挂在代码块上,图注写在fig.cap里:

```{r fig-ch3-scatter, fig.cap="不同参数下的分布对比", out.width="80%"} plot(x, y) ```

正文里写如图 \@ref(fig:fig-ch3-scatter) 所示。注意fig.cap不能省,没有图注的图在 bookdown 里不参与编号,引用会失效。这是最常见的“引用不生效”原因,没有之一。

表格稍微绕一点,因为表格的 caption 不在代码块选项里,而在生成表格的函数里:

```{r tab-ch3-summary} knitr::kable(head(df), caption = "参数对照表") ```

引用写如表 \@ref(tab:tab-ch3-summary) 所示。这里如果用了别的表格生成工具,caption 参数名可能不一样,需要查对应文档,但原理一致:必须有一个能被识别为表格标题的东西存在。

公式要在数学环境里埋标签:

\begin{equation} y = \beta_0 + \beta_1 x + \varepsilon (\#eq:linear) \end{equation}

引用写见式 \@ref(eq:linear)。这里有个细节:标签必须写在公式环境内部、单独一行,写在\begin{equation}前面是无效的。我见过有人在公式外写标签,构建不报错但引用全是问号,查了半天。

3.3 网页版和 PDF 版编号不一致时怎么处理

这是个真实存在的一致性陷阱。网页版按章节分页,PDF 版是连续排版,两者的编号规则在大多数情况下一致,但有两种情况会出现偏差。

第一种是附录和正文混排。网页版的附录可能被当成独立部分重新起编号,PDF 版则是延续正文编号。如果你在正文里引用了附录里的表,两种格式下显示的数字可能对不上。处理办法是在_bookdown.yml里明确设置language.label的各类前缀,让两边的计数起点一致,同时在附录开头手动重置计数器。

第二种是未编号章节。{-}这种不带编号的章节标题,在网页版里不会计入章节序号,但它里面的图和表仍然参与全书编号,这就导致读者按目录找不到对应位置。我的做法是:凡是需要被引用的内容,一律放在带编号的章节里;纯前言、致谢这类不放引用内容。

排查这类问题最有效的手段是keep_tex: yes,保留生成的.tex中间文件,直接搜\label{}和\ref{},看编号到底是怎么分配的。这比在成品里瞎猜快得多。

4. 一份源文件,喂饱网页、PDF 和电子书三种成品

4.1 网页版的阅读体验调优

网页版是 bookdown 用得最多的输出,也是默认皮肤最“眼熟”的那个。默认配置能用,但想让它读起来舒服,有几个地方值得动。

split_by我一般设成chapter。设成section会拆得太碎,读者翻页翻到手酸;不拆则单页太大。章节级拆分是比较舒服的粒度。

搜索功能默认开启,但要注意它会索引代码块内容,如果你的书里代码很多,搜一个常见函数名会得到上百条结果。可以通过样式或者配置把纯代码块排除出索引范围,具体做法是在代码块上加不参与索引的标记。

导航栏里的下载按钮,download: ["pdf", "epub"]这一行只是声明“我有这些格式”,实际上需要你先把这几个文件构建出来放到输出目录里,按钮点了才有东西可下。我第一次配的时候只写了这一行没构建 PDF,点下载直接 404,还以为是配置写错了。

自定义样式通过css: style.css挂进去。常见需求无非几样:正文字号调大一点、行高放宽、代码块横向滚动而不是折行、表格宽度自适应。这些在 CSS 里各写几行就够,不需要大动干戈。但要注意别去改那些由构建流程动态生成的类名,那些在不同版本之间会变,改了这版好使下版就崩。

4.2 PDF 输出的环境依赖与典型报错

PDF 是三个格式里最容易出事的一个,因为它依赖一整套排版环境。第一次构建报错的概率相当高,但错误类型其实很集中。

最常见的报错是找不到某个宏包。这类错误的排查顺序是:先看报错信息里的宏包名,确认环境里装了没有;没装就补装;装了还报错,检查是不是用了多个不同版本的环境导致路径混乱。我踩过一次坑,机器上先后装过两套排版环境,构建时总是调用到旧的那一套,报错信息完全对不上实际情况,清掉一套之后立刻正常。

第二个高频问题是中文字体。用默认配置出中文 PDF,要么报字体缺失,要么出来一堆方框。解法是明确指定文档类为中文文档类并切换排版引擎:

bookdown::pdf_book: latex_engine: xelatex documentclass: ctexbook keep_tex: yes

xelatex这一项不能省,默认引擎处理中文字体有问题。documentclass换成ctexbook之后,中文字体、行距、段落缩进都有了一套合理默认值,省掉大量手工设置。

第三个问题是长表格跨页断裂。表格太长超过一页,默认环境直接截断,后面的内容凭空消失且不报错。解法是把表格渲染成支持跨页的格式,在代码块里指定longtable = TRUE之类的选项。这个坑特别阴险,因为构建过程一路绿灯,只有你自己翻到那一页才会发现内容少了。

keep_tex: yes这个配置我强烈建议常开。报错信息里的行号指的是.tex文件的行号,不保留中间文件的话,你拿着一个行号却不知道去哪里看,排查等于从零开始。

4.3 ePub 的那些专属坑

ePub 是三个格式里最容易被忽略的,但如果你真打算让读者用阅读器看,它值得单独调。

第一个坑是图片路径。ePub 打包时会把资源内嵌,相对路径和绝对路径的处理规则跟网页版不完全一样。用绝对路径引用的图片,网页版正常显示,ePub 里就是裂图。统一用相对于项目根目录的路径最稳。

第二个坑是代码块。ePub 阅读器对横向滚动支持很差,长代码行要么被截断,要么强制折行导致缩进全乱。我的处理办法是在代码块上设一个较窄的out.width,然后手工控制源码的行宽,一般控制在八十字符以内。这个限制倒逼我把代码写得更紧凑,算是意外收获。

第三个坑是样式。ePub 支持的 CSS 子集比网页小得多,很多在网页版好看的效果在阅读器里直接失效。心态上要接受这一点:ePub 版本的目标是“能读”,不是“好看”。把精力放在段落层级、标题层级、图片尺寸这些基础项上,花哨效果留给网页版。

构建 ePub 之后一定要用至少两个不同的阅读器打开验证,因为不同阅读器的渲染差异比浏览器之间的差异还大。我用过的一个阅读器会吃掉所有段首缩进,另一个则会把加粗渲染成斜体,这些都得提前发现。

5. 让代码跑进书里:可复现文档的取舍

5.1 代码块选项的组合用法

bookdown 能执行代码并把结果嵌进成品,这是它区别于纯 Markdown 工具的核心能力。但“能执行”和“该执行”是两回事,代码块选项用得好不好,直接决定构建体验。

最常用的几个选项我按使用频率排一下:

选项作用我的使用建议
eval是否执行代码示例代码设 FALSE,真实结果设 TRUE
echo是否显示源码教学文档 TRUE,结果展示 FALSE
results输出处理方式默认markup,文本类结果用asis更干净
fig.cap图注需要引用的图必须写
cache缓存结果耗时超过十秒的块才开

有个组合值得单独提:echo = FALSE, results = 'asis'。这个组合适合“我只想展示结果表格,不想让读者看到生成表格的代码”的场景。但要注意results='asis'之后,输出内容会被当成原始 Markdown 直接插进文档,如果输出里含有 Markdown 特殊字符,会打乱整段结构。我第一次用的时候,输出里有个#开头的字符串,直接把后面一段变成了标题,排版乱得一塌糊涂。

5.2 缓存机制与“结果对不上”的排查

缓存是个双刃剑。开了之后构建飞快,但一旦结果和预期不符,排查成本极高,因为你不确定问题出在代码本身还是缓存没更新。

缓存失效的触发条件需要记清楚:代码块内容变了、块标签变了、缓存目录被删了,这三种情况会重新执行。不触发的情况里最坑的是——代码块内容没变,但它依赖的上游变量变了。比如 A 块定义了参数,B 块用这个参数算结果,你改了 A 块,B 块的缓存不会自动失效,成品里还是旧结果。这类问题在数字型文档里特别致命,因为你不会逐页核对每个数字。

我的做法是:只要改了任何影响计算的代码,就手工清一次缓存目录。缓存目录默认在_bookdown_files/下面,整个删掉重来最保险。构建时间翻倍,但换来的是结果可信。

另一个常见现象是“单独渲染这一章结果正确,整本构建结果不对”。这几乎必然是会话污染——前面章节定义了同名变量。解法就是前面提到的new_session: yes,开启后每章独立会话。它的代价是缓存命中率下降,两者需要权衡,但对正确性要求高的项目,我建议无条件开启。

5.3 输出体积失控与清理策略

长文档加上代码执行,输出体积很容易失控。我遇到过一次,一本三十章的手册,网页版目录里塞了将近两百兆的图片,读者打开都费劲。

原因有两类,处理方式不同。一类是图片分辨率过高,截图直接原图塞进去,一张就好几兆。处理方式是统一压到合适宽度,网页版一般一千二百像素宽足够。另一类是重复生成的中间图,每次构建都往缓存目录里堆一份,从来不清理。定期清空_bookdown_files/就能解决。

还有一个容易被忽略的点:delete_merged_file。默认会保留合并后的_main.Rmd,这个文件内容是全书的完整副本,如果有几张大图嵌在里面,它自己就能占几十兆。设成true让它在构建成功后自动删掉,需要排查问题时再临时关掉。

6. 几个让我停下来排查半天的真实状况

6.1 中文路径与文件编码

这个问题不新鲜,但它依然值得写在最前面,因为一旦踩上,报错信息完全指不到问题所在。

症状是构建过程一切正常,但生成的 PDF 里中文全是乱码或者方框,网页版反而正常。原因是排版环节对文件路径里的非 ASCII 字符处理有问题。解法只有一个:项目路径全用英文和数字,不要有空格、不要有中文。这条建议听着老套,但我见过太多人把项目建在“桌面/我的文档/某本书”这种路径下面,然后花几个小时怀疑字体配置。

编码问题则表现在内容层面。所有源文件统一用 UTF-8 保存,这是唯一推荐的做法。混合编码的项目会出现“某几章的中文正常,另外几章乱码”的诡异现象,因为不同编辑器保存时用了不同编码。批量检查的方法是用命令行工具扫一遍所有.Rmd文件的编码,不一致的统一转换。

6.2 章节顺序和目录对不上

前面提过字典序排序的坑,这里说完整的排查链路,因为它涉及好几个可能的原因,容易在一棵树上吊死。

第一步,看_bookdown.yml里有没有写rmd_files。写了就以它为准,检查列表里每一项的文件名是否和实际文件完全一致,注意大小写和空格。第二步,如果没有写,检查文件名前缀是否用了不一致的位数,1-和01-混用必然乱序。第三步,检查输出目录里的合并文件,直接看拼接顺序是不是你期望的。第四步,如果合并文件顺序对,但网页版目录顺序错,那就是输出配置里的目录生成逻辑问题,看toc相关配置。

这四步走下来基本能覆盖所有情况。我自己的经验是,能写rmd_files就别依赖自动收集,显式声明一次性解决所有排序问题,代价只是加新章节时多改一行配置。

6.3 引用编号重复或者跳号的定位过程

这类问题最让人崩溃,因为成品看起来“差不多对”,只有逐页核对才能发现某个编号重复了或者跳过了。

我的排查顺序是这样的:先保留.tex中间文件,在里面搜\label{把所有标签列出来,排序后肉眼扫重复项——重复的标签会挨在一起,一眼就能看到。如果标签没有重复,再搜计数器重置的位置,看是不是某个章节或附录里不小心重置了计数器。如果计数器也正常,那问题可能在交叉引用的解析顺序上,检查是不是有引用写在了被引用对象之前且跨越了章节边界。

有一类特殊情况值得单独说:在代码块内部生成的内容里包含了\@ref()。这种引用在代码执行阶段会被当成普通字符串,不会参与编号解析,最终原样输出。如果确实需要在动态生成的内容里做引用,得用专门的文本生成函数,而不是直接拼字符串。这个问题我第一次遇到时,查了整整一个下午才想明白是执行时机的问题。

7. 协作、版本管理与把成品交出去

7.1 只提交源文件,不提交构建产物

多人协作时,第一条纪律就是明确哪些文件进版本库,哪些不进。我的原则很简单:源文件、配置文件、必要的图片资源进;构建产物、缓存目录、合并文件一律不进。

理由有两层。技术层面,构建产物每次构建都会变,进版本库会让提交历史全是噪音,真正的内容改动淹没在几百条“更新输出”的提交里。协作层面,不同人的环境可能产出略有差异的成品,如果都提交上去,合并冲突处理起来极其麻烦。

忽略规则里至少要有这几项:输出目录、缓存目录、合并文件、以及各种临时转换产物。写一份清晰的忽略清单,比事后解释“为什么不要提交这个”省事得多。

7.2 自动构建的通用思路

手工构建适合写作阶段,要频繁看效果。但如果是团队协作或者定期发布,自动构建更靠谱——每次内容更新后自动跑一遍,生成三种格式,检查有没有报错。

思路是:把构建命令写进一个脚本,交给自动化流程按计划或者按提交触发。脚本里要做的检查包括构建是否成功、输出目录里三种格式是不是都生成了、生成文件的体积是否在合理范围内(突然变大通常意味着混进了大图或者重复内容)。构建失败时要有明确的失败标记,不能静默跳过。

这里有个容易忽略的细节:自动构建环境里的依赖必须显式声明。本地能跑通不代表自动环境能跑通,因为本地可能装了一堆你早就忘了的包,而自动环境是干净的。我吃过一次亏,本地一切正常,自动构建连续失败,最后发现是一个只在某个章节用到的小包没写进依赖清单。

7.3 我自己的几个习惯

写到这里,分享几个用久了养成的习惯,都是踩坑换来的。

第一个:每章写完后单独构建一次,不要攒着。攒到第十章一次性构建,报错信息里全是后面章节的问题,前面章节埋的雷要到最后才炸。

第二个:图表标签统一加章节前缀。前面说过,这能杜绝跨章撞车,而且搜索的时候特别方便——想找第三章的图,搜fig-ch3就行。

第三个:配置文件里的每一项都写注释。YAML 支持注释,几个月后回来改配置时,你会感谢当时写注释的自己。特别是那些“不知道为什么这么写但删了会崩”的项。

第四个:成品一定要用不同设备打开验证。网页版在宽屏和手机上完全是两个样子,PDF 在不同阅读器里的字体渲染也有差异。切图、表格溢出这类问题,只有在窄屏上才会暴露。

最后一个,也是我认为最重要的:别一开始就追求完美配置。我见过太多人卡在样式调优上一周没写一个字。先用最朴素的默认配置把内容写出来,构建能跑通就行,等到内容成型了再回头调样式。内容和结构是骨架,样式是衣服,骨架没长好,衣服再漂亮也没用。真正决定一本电子书价值的,永远是里面的内容有没有把一件事讲透,bookdown 只是把讲透之后的东西排得整齐一点。

返回列表