<!DOCTYPE html><html lang="zh" data-theme="dark"><head><meta charset="utf-8"><meta name="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Hexo 源代码分析【2】:「generate」的奇幻漂流 · Cytrogen 的个人博客</title><meta name="description" content="本文以 hexo generate 命令为主线,深入剖析 Hexo 源码,讲述一篇 Markdown 文章从原材料加工、渲染、生成页面蓝图到最终写入硬盘的完整生命周期故事。"><link rel="icon" href="../favicon.png"><link rel="canonical" href="https://cytrogen.icu/posts/d601.html"><link rel="webmention" href="https://webmention.io/cytrogen.icu/webmention"><link rel="me" href="https://m.otter.homes/@Cytrogen"><link rel="me" href="https://github.com/cytrogen"><meta name="fediverse:creator" content="@Cytrogen@m.otter.homes"><link rel="preload" href="../fonts/opensans-regular-latin.woff2" as="font" type="font/woff2" crossorigin="anonymous"><style>@font-face {
font-family: 'Open Sans';
src: url('../fonts/opensans-regular-latin.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
size-adjust: 107%;
ascent-override: 97%;
descent-override: 25%;
line-gap-override: 0%;
}
</style><script>(function() {
try {
// 优先级:用户选择 > 系统偏好 > 默认浅色
const saved = localStorage.getItem('theme');
const theme = saved ||
(window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
document.documentElement.setAttribute('data-theme', theme);
document.documentElement.style.colorScheme = theme;
} catch (error) {
// 失败时使用默认主题,不阻塞渲染
document.documentElement.setAttribute('data-theme', 'light');
}
})();
</script><link rel="stylesheet" href="../css/ares.css"><script data-netlify-skip-bundle="true">(function() {
document.addEventListener('DOMContentLoaded', function() {
const theme = document.documentElement.getAttribute('data-theme');
const pageWrapper = document.getElementById('page-wrapper');
if (pageWrapper && theme) {
pageWrapper.setAttribute('data-theme', theme);
}
});
})();
</script><!-- hexo injector head_end start -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.12.0/dist/katex.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/hexo-math@4.0.0/dist/style.css">
<!-- hexo injector head_end end --><meta name="generator" content="Hexo 8.1.1"><link rel="alternate" href="atom.xml" title="Cytrogen 的个人博客" type="application/atom+xml">
</head><body><div id="page-wrapper"><a class="skip-link" href="#main-content">跳到主要内容</a><div class="wrap"><header><a class="logo-link" href="../index.html"><img src="../favicon.png" alt="logo"></a><div class="h-card visually-hidden"><img class="u-photo" src="https://cytrogen.icu/favicon.png" alt="Cytrogen"><a class="p-name u-url u-uid" href="https://cytrogen.icu">Cytrogen</a><p class="p-note">Cytrogen 的个人博客,Cytrogen's Blog</p><a class="u-url" rel="me noopener" target="_blank" href="https://m.otter.homes/@Cytrogen">Mastodon</a><a class="u-url" rel="me noopener" target="_blank" href="https://github.com/cytrogen">GitHub</a></div><nav class="site-nav"><div class="nav-main"><div class="nav-primary"><ul class="nav-list hidden-mobile"><li class="nav-item"><a class="nav-link" href="../index.html">首页</a></li></ul><div class="nav-tools"><div class="language-menu"><button class="language-toggle" type="button"><svg class="icon icon-globe" width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" focusable="false"><path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8zm7.5-6.923c-.67.204-1.335.82-1.887 1.855A7.97 7.97 0 0 0 5.145 4H7.5V1.077zM4.09 4a9.267 9.267 0 0 1 .64-1.539 6.7 6.7 0 0 1 .597-.933A7.025 7.025 0 0 0 2.255 4H4.09zm-.582 3.5c.03-.877.138-1.718.312-2.5H1.674a6.958 6.958 0 0 0-.656 2.5h2.49zM4.847 5a12.5 12.5 0 0 0-.338 2.5H7.5V5H4.847zM8.5 5v2.5h2.99a12.495 12.495 0 0 0-.337-2.5H8.5zM4.51 8.5a12.5 12.5 0 0 0 .337 2.5H7.5V8.5H4.51zm3.99 0V11h2.653c.187-.765.306-1.608.338-2.5H8.5zM5.145 12c.138.386.295.744.468 1.068.552 1.035 1.218 1.65 1.887 1.855V12H5.145zm.182 2.472a6.696 6.696 0 0 1-.597-.933A9.268 9.268 0 0 1 4.09 12H2.255a7.024 7.024 0 0 0 3.072 2.472zM3.82 11a13.652 13.652 0 0 1-.312-2.5h-2.49c.062.89.291 1.733.656 2.5H3.82zm6.853 3.472A7.024 7.024 0 0 0 13.745 12H11.91a9.27 9.27 0 0 1-.64 1.539 6.688 6.688 0 0 1-.597.933zM8.5 12v2.923c.67-.204 1.335-.82 1.887-1.855A7.97 7.97 0 0 0 10.855 12H8.5zm3.68-1h2.146c.365-.767.594-1.61.656-2.5h-2.49a13.65 13.65 0 0 1-.312 2.5zm2.802-3.5a6.959 6.959 0 0 0-.656-2.5H12.18c.174.782.282 1.623.312 2.5h2.49zM11.27 2.461c.247.464.462.98.64 1.539h1.835a7.024 7.024 0 0 0-3.072-2.472c.218.284.418.598.597.933zM10.855 4a7.966 7.966 0 0 0-.468-1.068C9.835 1.897 9.17 1.282 8.5 1.077V4h2.355z"></path></svg><span>中文</span></button><div class="language-dropdown"></div></div></div><div class="nav-controls"><div class="more-menu hidden-mobile"><button class="more-toggle" type="button"><span>更多</span><svg class="icon icon-chevron-down" width="12" height="12" viewBox="0 0 12 12" fill="currentColor" aria-hidden="true" focusable="false"><path d="M6 8.825c-.2 0-.4-.1-.5-.2l-3.3-3.3c-.3-.3-.3-.8 0-1.1s.8-.3 1.1 0l2.7 2.7 2.7-2.7c.3-.3.8-.3 1.1 0s.3.8 0 1.1l-3.3 3.3c-.1.1-.3.2-.5.2z"></path></svg></button><div class="more-dropdown"><ul class="dropdown-list"><li class="dropdown-item"><a class="nav-link" href="../archives/index.html">归档</a></li><li class="dropdown-item"><a class="nav-link" href="../categories/index.html">分类</a></li><li class="dropdown-item"><a class="nav-link" href="../tags/index.html">标签</a></li><li class="dropdown-item"><a class="nav-link" href="../about/index.html">关于</a></li><li class="dropdown-item"><a class="nav-link" href="../sitemap/index.html">领地地图</a></li></ul></div></div><div class="theme-switcher"><button class="theme-toggle" type="button" role="switch" aria-pressed="false" aria-label="切换主题"><div class="theme-icon moon-icon"><svg class="icon icon-moon" width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" focusable="false"><path d="M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278z"></path></svg></div><div class="theme-icon sun-icon"><svg class="icon icon-sun" width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" focusable="false"><path d="M8 11a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0 1a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0zm0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13zm8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5zM3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8zm10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0zm-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0zm9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707zM4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708z"></path></svg></div></button></div><details class="mobile-menu-details hidden-desktop"><summary class="hamburger-menu" aria-label="nav.menu"><svg class="icon icon-bars" width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" focusable="false"><path d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5z"></path></svg><span class="menu-text">nav.menu</span></summary><div class="mobile-menu-dropdown"><ul class="mobile-nav-list"><li class="mobile-nav-item"><a class="mobile-nav-link" href="../index.html">首页</a></li><li class="mobile-nav-item"><a class="mobile-nav-link" href="../archives/index.html">归档</a></li><li class="mobile-nav-item"><a class="mobile-nav-link" href="../categories/index.html">分类</a></li><li class="mobile-nav-item"><a class="mobile-nav-link" href="../tags/index.html">标签</a></li><li class="mobile-nav-item"><a class="mobile-nav-link" href="../about/index.html">关于</a></li><li class="mobile-nav-item"><a class="mobile-nav-link" href="../sitemap/index.html">领地地图</a></li></ul></div></details></div></div></div></nav></header><main class="container" id="main-content" tabindex="-1"><div class="post"><article class="post-block h-entry"><div class="post-meta p-author h-card visually-hidden"><img class="author-avatar u-photo" src="../favicon.png" alt="Cytrogen"><span class="p-name">Cytrogen</span><a class="u-url" href="https://cytrogen.icu">https://cytrogen.icu</a></div><a class="post-permalink u-url u-uid visually-hidden" href="https://cytrogen.icu/posts/d601.html">永久链接</a><div class="p-summary visually-hidden"><p>使用 Hexo,痛骂 Hexo,理解 Hexo,成为 Hexo。</p>
<p>这篇文章是用来记录我阅读 Hexo 源代码的过程和分析。</p></div><div class="visually-hidden"><a class="p-category" href="../categories/%E4%BB%A3%E7%A0%81%E8%A7%A3%E8%AF%BB/">代码解读</a><a class="p-category" href="../tags/Hexo/">Hexo</a><a class="p-category" href="../tags/JavaScript/">JavaScript</a></div><h1 class="post-title p-name">Hexo 源代码分析【2】:「generate」的奇幻漂流</h1><div class="post-info"><time class="post-date dt-published" datetime="2025-07-24T04:00:00.000Z">7/24/2025</time><time class="dt-updated visually-hidden" datetime="2026-02-09T17:16:54.713Z"></time></div><div class="post-content e-content"><html><head></head><body><p>使用 Hexo,痛骂 Hexo,理解 Hexo,成为 Hexo。</p>
<p>这篇文章是用来记录我阅读 Hexo 源代码的过程和分析。</p>
<span id="more"></span>
<div class="danger">
<p>其实这篇文章去年暑假就在筹备写,但是当时的思维被局限在了线性的代码导览。</p>
<p>看过 <a href="/posts/5d57.html">上一篇文章</a> 的朋友应该能明显感觉出,一个小小的 <code>index.js</code> 都能 <s>水</s> 写那么多内容,很多还是占篇幅的代码。实际上根本不需要把代码一次又一次贴出来,只要贴出来最核心的几行代码就好了,难道真的会有人去跟着我的文章去一个一个对 Hexo 的源代码吗 2333。重点还得是如何用文字描述内容、让读者更聚焦。</p>
<p>那么我又为什么没有继续写第二篇了呢?自然不是因为我又鸽了 <s>(当然可能性不是零)</s>,而是我当时计划写 Hexo 的 <code>extend</code> 目录,即扩展(官方文档 <code>/api</code> 路由下就能在左侧边栏看到蓝蓝的 <em>扩展</em> 二字,它包含了控制台、部署器、过滤器等 Hexo 很核心的内容。而这就是问题所在,它并不是一个线性的代码、和第一篇的入口文件 <code>index.js</code> 不一样。<code>extend</code> 目录下的模块那么多,逐一分析过去非常耗时并且容易失去焦点,我那样写又是折磨自己还折磨读者,花费那么多精力根本不讨好。</p>
<p>问题又来了:我为什么又跑来写这个第二篇,并且在开头加了这么长一个警告块?</p>
<p>答案很简单,因为我在睡梦中受高人指点。高人曰:「不要做代码的导游,要做故事的讲述者。」</p>
<p>我顿时恍然大悟,之前那种线性分析方法,就像是拿着一本字典,一页一页地给读者讲定义、枯燥且乏味。而一个优秀的技术分享,应该像一部引人入胜的侦探小说、有一条清晰的主线。巴拉巴拉,这个这个,那个那个。开玩笑的。</p>
<p>所以这篇文章的风格将会和上一篇不同。我们不再按部就班地解刨 <code>extend</code> 目录下的每一个文件,而是会以一个核心的用户行为为线索,将这些分散的扩展模块串联起来,讲述一个它们如何协同工作的完整故事。这意味着本文会更侧重于流程、数据流和模块间的交互,而不是孤立地分析某个函数的实现。</p>
<blockquote>
<p>实际上写完后感觉风格没变多少,晕。</p>
</blockquote>
</div>
<p>在 <a href="/posts/5d57.html">上一篇文章</a> 中,我们了解了 Hexo 源代码中的入口文件,并且在没有讲太多细节的情况下过了一遍 Hexo 运行的流程:</p>
<figure class="highlight js"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Hexo</span> <span class="keyword">extends</span> <span class="title class_ inherited__">events_1.EventEmitter</span> {</span><br><span class="line"> <span class="title function_">constructor</span>(<span class="params">base = process.cwd(), args = {}</span>) { ... }</span><br><span class="line"> <span class="title function_">_bindLocals</span>(<span class="params"></span>) { ... }</span><br><span class="line"> <span class="title function_">init</span>(<span class="params"></span>) { ... }</span><br><span class="line"> <span class="title function_">call</span>(<span class="params">name, args, callback</span>) { ... }</span><br><span class="line"> <span class="title function_">model</span>(<span class="params">name, schema</span>) { ... }</span><br><span class="line"> <span class="title function_">resolvePlugin</span>(<span class="params">name, basedir</span>) { ... }</span><br><span class="line"> <span class="title function_">loadPlugin</span>(<span class="params">path, callback</span>) { ... }</span><br><span class="line"> <span class="title function_">_showDrafts</span>(<span class="params"></span>) { ... }</span><br><span class="line"> <span class="title function_">load</span>(<span class="params">callback</span>) { ... }</span><br><span class="line"> <span class="title function_">watch</span>(<span class="params">callback</span>) { ... }</span><br><span class="line"> <span class="title function_">unwatch</span>(<span class="params"></span>) { ... }</span><br><span class="line"> <span class="title function_">_generateLocals</span>(<span class="params"></span>) { ... }</span><br><span class="line"> <span class="title function_">_runGenerators</span>(<span class="params"></span>) { ... }</span><br><span class="line"> <span class="title function_">_routerRefresh</span>(<span class="params">runningGenerators, useCache</span>) { ... }</span><br><span class="line"> <span class="title function_">_generate</span>(<span class="params">options = {}</span>) { ... }</span><br><span class="line"> <span class="title function_">exit</span>(<span class="params">err</span>) { ... }</span><br><span class="line"> <span class="title function_">execFilter</span>(<span class="params">type, data, options</span>) { ... }</span><br><span class="line"> <span class="title function_">execFilterSync</span>(<span class="params">type, data, options</span>) { ... }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>简单来说是:</p>
<ol>
<li>初始化了各种目录路径、设置环境变量、初始化各种扩展,设置配置、日志、渲染器、路由等。</li>
<li><code>_bindLocals</code> 方法将数据库中的数据绑定到 <code>locals</code> 对象上。</li>
<li><code>init</code> 方法初始化 <code>Hexo</code>,加载插件和配置。</li>
<li><code>call</code> 方法调用控制台命令。</li>
<li><code>model</code> 方法创建或获取数据库模型。</li>
<li><code>resolvePlugin</code> 和 <code>loadPlugin</code> 方法用于解析和加载插件。</li>
<li><code>load</code> 和 <code>watch</code> 方法加载数据并处理源文件,<code>watch</code> 方法还会设置文件监听。</li>
<li><code>_generate</code> 方法生成静态文件。</li>
<li><code>exit</code> 方法退出程序,执行清理工作。</li>
</ol>
<p>但一个静态的类如何响应我们的命令,并将一堆散乱的 Markdown 文件变成一个精美的网站的呢?</p>
<p>熟悉 Hexo 的朋友都知道,我们最常用的命令之一 —— <code>hexo generate</code> —— 就是用来做这个的,而这篇文章要剥下 <code>hexo generate</code> 的皮,看看 Hexo 的核心是如何运作的。</p>
<h1 id="1-解析命令"><a class="markdownIt-Anchor" href="#1-解析命令"></a> 1. 解析命令</h1>
<p>无奖竞猜:当我们在终端敲下 <code>hexo generate</code> 时,Hexo 中第一个被激活的部件是什么?</p>
<p>答案是 <code>Console</code> 扩展。</p>
<p>我们可以把它想象成 Hexo 的「总机」或者「前台」。它的核心职责就是:接收你输入的命令,然后把它转接给正确的内部处理函数。这个过程分为两步:<strong>注册</strong> 和 <strong>调用</strong></p>
<p>一个命令能被调用,前提是它得先被「注册在案」。这个注册过程发生在 Hexo 的初始化阶段,也就是 <code>hexo.init()</code> 方法中。记性很好的朋友应该可以瞬间想起这行关键代码:</p>
<figure class="highlight javascript"><figcaption><span>/hexo/index.JS</span></figcaption><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">require</span>(<span class="string">'../plugins/console'</span>)(<span class="variable language_">this</span>);</span><br></pre></td></tr></tbody></table></figure>
<p>这行代码会加载 Hexo 内置的所有控制台命令。对于 <code>generate</code> 命令,它会执行 <code>/plugins/console/index.js</code> 文件中的代码,该文件负责注册包括 <code>generate</code> 在内的多个核心命令。我们来看 <code>generate</code> 的注册部分:</p>
<figure class="highlight javascript"><figcaption><span>/plugins/console/index.JS</span></figcaption><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable language_">console</span>.<span class="title function_">register</span>(<span class="string">'generate'</span>, <span class="string">'Generate static files.'</span>, {</span><br><span class="line"> <span class="attr">options</span>: [</span><br><span class="line"> { <span class="attr">name</span>: <span class="string">'-d, --deploy'</span>, <span class="attr">desc</span>: <span class="string">'Deploy after generated'</span> },</span><br><span class="line"> { <span class="attr">name</span>: <span class="string">'-f, --force'</span>, <span class="attr">desc</span>: <span class="string">'Force regenerate'</span> },</span><br><span class="line"> { <span class="attr">name</span>: <span class="string">'-w, --watch'</span>, <span class="attr">desc</span>: <span class="string">'Watch file changes'</span> },</span><br><span class="line"> { <span class="attr">name</span>: <span class="string">'-b, --bail'</span>, <span class="attr">desc</span>: <span class="string">'Raise an error if any unhandled exception is thrown during generation'</span> },</span><br><span class="line"> { <span class="attr">name</span>: <span class="string">'-c, --concurrency'</span>, <span class="attr">desc</span>: <span class="string">'Maximum number of files to be generated in parallel. Default is infinity'</span> }</span><br><span class="line"> ]</span><br><span class="line">}, <span class="built_in">require</span>(<span class="string">'./generate'</span>));</span><br></pre></td></tr></tbody></table></figure>
<p><code>register</code> 方法在这里接收了四个参数:</p>
<ol>
<li>
<p><code>name</code>:命令名称</p>
</li>
<li>
<p><code>desc</code>:命令描述</p>
</li>
<li>
<p><code>options</code>:定义了所有 <code>hexo g</code> 支持的命令行参数,比如我们 <code>-d</code>(生成完成后部署)和 <code>-w</code>(监视文件变动)</p>
</li>
<li>
<p><code>fn</code>:命令的执行函数。这里通过 <code>require('./generate')</code> 加载了同目录下的 <code>generate.js</code> 文件,该文件导出的 <code>generateConsole</code> 函数就是 <code>generate</code> 命令的真正入口:</p>
<figure class="highlight javascript"><figcaption><span>/plugins/console/generate.JS</span></figcaption><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">generateConsole</span>(<span class="params">args = {}</span>) {</span><br><span class="line"> <span class="keyword">const</span> generator = <span class="keyword">new</span> <span class="title class_">Generater</span>(<span class="variable language_">this</span>, args); <span class="comment">// 这个类之后会提起</span></span><br><span class="line"> <span class="keyword">if</span> (generator.<span class="property">watch</span>) {</span><br><span class="line"> <span class="keyword">return</span> generator.<span class="title function_">execWatch</span>();</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="title function_">load</span>().<span class="title function_">then</span>(<span class="function">() =></span> generator.<span class="title function_">firstGenerate</span>()).<span class="title function_">then</span>(<span class="function">() =></span> {</span><br><span class="line"> <span class="keyword">if</span> (generator.<span class="property">deploy</span>) {</span><br><span class="line"> <span class="keyword">return</span> generator.<span class="title function_">execDeploy</span>();</span><br><span class="line"> }</span><br><span class="line"> });</span><br><span class="line">}</span><br><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = generateConsole;</span><br></pre></td></tr></tbody></table></figure>
</li>
</ol>
<p>这样一来,<code>generate</code> 命令、它的描述、它的所有参数,以及它的执行函数,就通过这几行代码被清晰地关联起来,并存放在 <code>hexo.extend.console</code> 这个登记簿内。</p>
<p>注册完成后,当 <code>hexo-cli</code> 工具解析到你的 <code>generate</code> 命令时,它就会调用我们在 <code>Hexo</code> 类中看到的方法:<code>hexo.call('generate', args)</code>。</p>
<blockquote>
<p>也就是这一段:</p>
<figure class="highlight javascript"><figcaption><span>/hexo/index.JS</span></figcaption><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="title function_">call</span>(<span class="params">name, args, callback</span>) {</span><br><span class="line"> <span class="keyword">if</span> (!callback && <span class="keyword">typeof</span> args === <span class="string">'function'</span>) {</span><br><span class="line"> callback = args;</span><br><span class="line"> args = {};</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">const</span> c = <span class="variable language_">this</span>.<span class="property">extend</span>.<span class="property">console</span>.<span class="title function_">get</span>(name); <span class="comment">// 1. 从登记簿里查找命令</span></span><br><span class="line"> <span class="keyword">if</span> (c)</span><br><span class="line"> <span class="keyword">return</span> <span class="title class_">Reflect</span>.<span class="title function_">apply</span>(c, <span class="variable language_">this</span>, [args]).<span class="title function_">asCallback</span>(callback); <span class="comment">// 2. 执行找到的函数</span></span><br><span class="line"> <span class="keyword">return</span> bluebird_1.<span class="property">default</span>.<span class="title function_">reject</span>(<span class="keyword">new</span> <span class="title class_">Error</span>(<span class="string">`Console \`<span class="subst">${name}</span>\` has not been registered yet!`</span>));</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
</blockquote>
<p>就这样,通过注册和调用这两步简单的操作,Hexo 的 <code>Console</code> 扩展干净利落地完成了它的使命…… 可喜可贺、可喜可贺。</p>
<p>现在 <code>generateConsole</code> 函数接过了指挥棒,而它要做的第一件事,就是调用 <code>this.load()</code>。</p>
<h1 id="2-原材料加工"><a class="markdownIt-Anchor" href="#2-原材料加工"></a> 2. 原材料加工</h1>
<p>我们来看 <code>this.load</code> 方法的核心:</p>
<figure class="highlight javascript"><figcaption><span>/hexo/index.JS</span></figcaption><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="title function_">load</span>(<span class="params">callback</span>) {</span><br><span class="line"> <span class="keyword">return</span> (<span class="number">0</span>, load_database_1.<span class="property">default</span>)(<span class="variable language_">this</span>).<span class="title function_">then</span>(<span class="function">() =></span> { <span class="comment">// 确保数据库已加载</span></span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">log</span>.<span class="title function_">info</span>(<span class="string">'Start processing'</span>);</span><br><span class="line"> <span class="keyword">return</span> bluebird_1.<span class="property">default</span>.<span class="title function_">all</span>([</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">source</span>.<span class="title function_">process</span>(), <span class="comment">// 我们的主角</span></span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">theme</span>.<span class="title function_">process</span>()</span><br><span class="line"> ]);</span><br><span class="line"> }).<span class="title function_">then</span>(<span class="function">() =></span> {</span><br><span class="line"> <span class="title function_">mergeCtxThemeConfig</span>(<span class="variable language_">this</span>);</span><br><span class="line"> <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="title function_">_generate</span>({ <span class="attr">cache</span>: <span class="literal">false</span> });</span><br><span class="line"> }).<span class="title function_">asCallback</span>(callback);</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p><code>this.load()</code> 的任务很明确:将 <code>source</code> 文件夹里所有零散的文件,加工成结构化、可供后续使用的内存数据。</p>
<p><code>this.source.process()</code> 是故事的起点。那么 <code>this.source</code> 是什么?它的 <code>process()</code> 方法又是什么?</p>
<h2 id="21-box-类"><a class="markdownIt-Anchor" href="#21-box-类"></a> 2.1. <code>Box</code> 类</h2>
<p>在 <code>Hexo</code> 类的结构函数中,<code>this.source</code> 被实例化:</p>
<figure class="highlight javascript"><figcaption><span>/hexo/index.JS</span></figcaption><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> source_1 = <span class="title function_">__importDefault</span>(<span class="built_in">require</span>(<span class="string">"./source"</span>));</span><br><span class="line"></span><br><span class="line"><span class="variable language_">this</span>.<span class="property">source</span> = <span class="keyword">new</span> source_1.<span class="title function_">default</span>(<span class="variable language_">this</span>);</span><br></pre></td></tr></tbody></table></figure>
<p>而 <code>Source</code> 类的定义出奇地简单:</p>
<figure class="highlight javascript"><figcaption><span>/hexo/source.JS</span></figcaption><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">"use strict"</span>;</span><br><span class="line"><span class="keyword">var</span> __importDefault = (<span class="variable language_">this</span> && <span class="variable language_">this</span>.<span class="property">__importDefault</span>) || <span class="keyword">function</span> (<span class="params">mod</span>) {</span><br><span class="line"> <span class="keyword">return</span> (mod && mod.<span class="property">__esModule</span>) ? mod : { <span class="string">"default"</span>: mod };</span><br><span class="line">};</span><br><span class="line"><span class="keyword">const</span> box_1 = <span class="title function_">__importDefault</span>(<span class="built_in">require</span>(<span class="string">"../box"</span>));</span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Source</span> <span class="keyword">extends</span> <span class="title class_ inherited__">box_1.default</span> {</span><br><span class="line"> <span class="title function_">constructor</span>(<span class="params">ctx</span>) {</span><br><span class="line"> <span class="variable language_">super</span>(ctx, ctx.<span class="property">source_dir</span>);</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">processors</span> = ctx.<span class="property">extend</span>.<span class="property">processor</span>.<span class="title function_">list</span>();</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = <span class="title class_">Source</span>;</span><br><span class="line"><span class="comment">//# sourceMappingURL=source.js.map</span></span><br></pre></td></tr></tbody></table></figure>
<p>原因也很简单:<code>Source</code> 类继承自 <code>Box</code> 类。记性很好的朋友此时就要一拍脑袋了:<a href="/posts/5d57#%E6%BA%90%E6%96%87%E4%BB%B6%E7%AE%A1%E7%90%86">上一篇文章</a> 提到过。</p>
<p>当我们调用 <code>this.source.process()</code> 时,我们实际上是在调用 <code>Box</code> 类中定义的 <code>process</code> 方法:</p>
<figure class="highlight javascript"><figcaption><span>/box/index.JS</span></figcaption><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="title function_">process</span>(<span class="params">callback</span>) {</span><br><span class="line"> <span class="keyword">const</span> { base, <span class="title class_">Cache</span>, <span class="attr">context</span>: ctx } = <span class="variable language_">this</span>;</span><br><span class="line"> <span class="keyword">return</span> (<span class="number">0</span>, hexo_fs_1.<span class="property">stat</span>)(base).<span class="title function_">then</span>(<span class="function"><span class="params">stats</span> =></span> {</span><br><span class="line"> <span class="keyword">if</span> (!stats.<span class="title function_">isDirectory</span>())</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> <span class="comment">// Check existing files in cache</span></span><br><span class="line"> <span class="keyword">const</span> relativeBase = <span class="title function_">escapeBackslash</span>(base.<span class="title function_">substring</span>(ctx.<span class="property">base_dir</span>.<span class="property">length</span>));</span><br><span class="line"> <span class="keyword">const</span> cacheFiles = <span class="title class_">Cache</span>.<span class="title function_">filter</span>(<span class="function"><span class="params">item</span> =></span> item.<span class="property">_id</span>.<span class="title function_">startsWith</span>(relativeBase)).<span class="title function_">map</span>(<span class="function"><span class="params">item</span> =></span> item.<span class="property">_id</span>.<span class="title function_">substring</span>(relativeBase.<span class="property">length</span>));</span><br><span class="line"> <span class="comment">// Handle deleted files</span></span><br><span class="line"> <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="title function_">_readDir</span>(base)</span><br><span class="line"> .<span class="title function_">then</span>(<span class="function">(<span class="params">files</span>) =></span> cacheFiles.<span class="title function_">filter</span>(<span class="function">(<span class="params">path</span>) =></span> !files.<span class="title function_">includes</span>(path)))</span><br><span class="line"> .<span class="title function_">map</span>(<span class="function">(<span class="params">path</span>) =></span> <span class="variable language_">this</span>.<span class="title function_">_processFile</span>(file_1.<span class="property">default</span>.<span class="property">TYPE_DELETE</span>, path));</span><br><span class="line"> }).<span class="title function_">catch</span>(<span class="function"><span class="params">err</span> =></span> {</span><br><span class="line"> <span class="keyword">if</span> (err && err.<span class="property">code</span> !== <span class="string">'ENOENT'</span>)</span><br><span class="line"> <span class="keyword">throw</span> err;</span><br><span class="line"> }).<span class="title function_">asCallback</span>(callback);</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p><code>Box</code> 类是 Hexo 通用的文件处理引擎。它的 <code>process</code> 方法负责扫描指定目录(这里的话扫描的是 <code>source/</code>)、与缓存对比、找出被删除/新增/修改的文件,然后为每个文件调用 <code>_processFile</code> 方法:</p>
<figure class="highlight javascript"><figcaption><span>/box/index.JS</span></figcaption><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br></pre></td><td class="code"><pre><span class="line"><span class="title function_">_processFile</span>(<span class="params">type, path</span>) {</span><br><span class="line"> <span class="keyword">if</span> (<span class="variable language_">this</span>.<span class="property">_processingFiles</span>[path]) {</span><br><span class="line"> <span class="keyword">return</span> bluebird_1.<span class="property">default</span>.<span class="title function_">resolve</span>();</span><br><span class="line"> }</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">_processingFiles</span>[path] = <span class="literal">true</span>;</span><br><span class="line"> <span class="keyword">const</span> { base, <span class="title class_">File</span>, <span class="attr">context</span>: ctx } = <span class="variable language_">this</span>;</span><br><span class="line"> <span class="variable language_">this</span>.<span class="title function_">emit</span>(<span class="string">'processBefore'</span>, {</span><br><span class="line"> type,</span><br><span class="line"> path</span><br><span class="line"> });</span><br><span class="line"> <span class="keyword">return</span> bluebird_1.<span class="property">default</span>.<span class="title function_">reduce</span>(<span class="variable language_">this</span>.<span class="property">processors</span>, <span class="function">(<span class="params">count, processor</span>) =></span> {</span><br><span class="line"> <span class="keyword">const</span> params = processor.<span class="property">pattern</span>.<span class="title function_">match</span>(path);</span><br><span class="line"> <span class="keyword">if</span> (!params)</span><br><span class="line"> <span class="keyword">return</span> count;</span><br><span class="line"> <span class="keyword">const</span> file = <span class="keyword">new</span> <span class="title class_">File</span>({</span><br><span class="line"> <span class="comment">// source is used for filesystem path, keep backslashes on Windows</span></span><br><span class="line"> <span class="attr">source</span>: (<span class="number">0</span>, path_1.<span class="property">join</span>)(base, path),</span><br><span class="line"> <span class="comment">// path is used for URL path, replace backslashes on Windows</span></span><br><span class="line"> <span class="attr">path</span>: <span class="title function_">escapeBackslash</span>(path),</span><br><span class="line"> params,</span><br><span class="line"> type</span><br><span class="line"> });</span><br><span class="line"> <span class="keyword">return</span> <span class="title class_">Reflect</span>.<span class="title function_">apply</span>(bluebird_1.<span class="property">default</span>.<span class="title function_">method</span>(processor.<span class="property">process</span>), ctx, [file])</span><br><span class="line"> .<span class="title function_">thenReturn</span>(count + <span class="number">1</span>);</span><br><span class="line"> }, <span class="number">0</span>).<span class="title function_">then</span>(<span class="function"><span class="params">count</span> =></span> {</span><br><span class="line"> <span class="keyword">if</span> (count) {</span><br><span class="line"> ctx.<span class="property">log</span>.<span class="title function_">debug</span>(<span class="string">'Processed: %s'</span>, (<span class="number">0</span>, picocolors_1.<span class="property">magenta</span>)(path));</span><br><span class="line"> }</span><br><span class="line"> <span class="variable language_">this</span>.<span class="title function_">emit</span>(<span class="string">'processAfter'</span>, {</span><br><span class="line"> type,</span><br><span class="line"> path</span><br><span class="line"> });</span><br><span class="line"> }).<span class="title function_">catch</span>(<span class="function"><span class="params">err</span> =></span> {</span><br><span class="line"> ctx.<span class="property">log</span>.<span class="title function_">error</span>({ err }, <span class="string">'Process failed: %s'</span>, (<span class="number">0</span>, picocolors_1.<span class="property">magenta</span>)(path));</span><br><span class="line"> }).<span class="title function_">finally</span>(<span class="function">() =></span> {</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">_processingFiles</span>[path] = <span class="literal">false</span>;</span><br><span class="line"> }).<span class="title function_">thenReturn</span>(path);</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p><code>_processFile</code> 则是处理单个文件的核心,它会遍历一个名为 <code>this.processors</code> 的数组,用数组中的每个 <code>processor</code> 的 <code>pattern</code> 与文件路径进行匹配。如果匹配成功,就执行该 <code>processor</code> 的 <code>process</code> 方法。</p>
<p>那么,<code>this.processors</code> 这个关键的数组是从哪里来的呢?这引出了 <code>Processor</code> 的注册机制。</p>
<h2 id="22-processor-扩展"><a class="markdownIt-Anchor" href="#22-processor-扩展"></a> 2.2. <code>Processor</code> 扩展</h2>
<p><code>Processor</code> 扩展遵循一个清晰的注册与加载流程。</p>
<p>首先是注册,在 <code>hexo.init()</code> 阶段,Hexo 会加载 <code>/plugins/processor/index.js</code> 文件,该文件负责注册所有内置的 <code>Processor</code>:</p>
<figure class="highlight javascript"><figcaption><span>/plugins/processor/index.JS</span></figcaption><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">"use strict"</span>;</span><br><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = <span class="function">(<span class="params">ctx</span>) =></span> {</span><br><span class="line"> <span class="comment">// 1. 从 Hexo 上下文中获取 processor 扩展的注册器实例</span></span><br><span class="line"> <span class="keyword">const</span> { processor } = ctx.<span class="property">extend</span>;</span><br><span class="line"> <span class="comment">// 2. 定义一个内部的 register 辅助函数</span></span><br><span class="line"> <span class="keyword">function</span> <span class="title function_">register</span>(<span class="params">name</span>) {</span><br><span class="line"> <span class="comment">// 2.1. 加载指定 processor 的定义文件</span></span><br><span class="line"> <span class="keyword">const</span> obj = <span class="built_in">require</span>(<span class="string">`./<span class="subst">${name}</span>`</span>)(ctx);</span><br><span class="line"> <span class="comment">// 2.2. 调用注册器实例的 register 方法</span></span><br><span class="line"> <span class="comment">// 将 processor 的 pattern 和 process 函数正式注册到 Hexo 的扩展管理器中</span></span><br><span class="line"> processor.<span class="title function_">register</span>(obj.<span class="property">pattern</span>, obj.<span class="property">process</span>);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 3. 为每一个内置的 processor 调用 register 函数</span></span><br><span class="line"> <span class="title function_">register</span>(<span class="string">'asset'</span>); <span class="comment">// 静态资源处理器</span></span><br><span class="line"> <span class="title function_">register</span>(<span class="string">'data'</span>); <span class="comment">// _data 目录处理器</span></span><br><span class="line"> <span class="title function_">register</span>(<span class="string">'post'</span>); <span class="comment">// 文章处理器</span></span><br><span class="line">};</span><br><span class="line"><span class="comment">//# sourceMappingURL=index.js.map</span></span><br></pre></td></tr></tbody></table></figure>
<p>接着是加载。当 <code>new source_1.default(this)</code> 执行时,<code>Source</code> 类的构造函数会从 <code>Processor</code> 扩展的注册器中,获取所有已注册的 <code>Processor</code> 列表,并赋值给自身的 <code>this.processors</code> 属性。</p>
<figure class="highlight javascript"><figcaption><span>/hexo/source.JS</span></figcaption><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Source</span> <span class="keyword">extends</span> <span class="title class_ inherited__">box_1.default</span> {</span><br><span class="line"> <span class="title function_">constructor</span>(<span class="params">ctx</span>) {</span><br><span class="line"> <span class="variable language_">super</span>(ctx, ctx.<span class="property">source_dir</span>);</span><br><span class="line"> <span class="comment">// 从 ctx.extend.processor.list() 获取完整的 processor 列表</span></span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">processors</span> = ctx.<span class="property">extend</span>.<span class="property">processor</span>.<span class="title function_">list</span>();</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>至此,调用链完全闭合:</p>
<ol>
<li><code>init</code> 阶段,<code>/plugins/processor/index.js</code> 将 <code>post</code> 等处理器注册到 <code>hexo.extend.processor</code>。</li>
<li><code>Source</code> 实例将 <code>hexo.extend.processor</code> 的完整列表复制到自身的 <code>this.processors</code> 数组中。</li>
<li><code>source.process()</code> 调用 <code>_processFile</code>,<code>_processFile</code> 遍历 <code>this.processors</code> 数组,从而找到了 <code>post</code> 处理器来处理匹配的文件。</li>
</ol>
<h2 id="23-post-处理器"><a class="markdownIt-Anchor" href="#23-post-处理器"></a> 2.3. <code>post</code> 处理器</h2>
<p>现在我们以 <code>_posts/hello-world.md</code> 为例,看看 <code>post</code> 处理器是如何工作的。</p>
<h4 id="231-资格审查"><a class="markdownIt-Anchor" href="#231-资格审查"></a> 2.3.1. 资格审查</h4>
<p>当 <code>hello-world.md</code> 开始要被 <code>Box</code> 引擎处理时,它首先会遇到 <code>post</code> 的资格审查。</p>
<p><code>post</code> 会通过其 <code>pattern</code> 函数来决定是否处理一个文件。这个函数非常智能,它不仅仅是匹配路径,更是进行了一系列的检查:</p>
<figure class="highlight javascript"><figcaption><span>/plugins/processor/post.JS</span></figcaption><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = <span class="function">(<span class="params">ctx</span>) =></span> {</span><br><span class="line"> <span class="keyword">return</span> {</span><br><span class="line"> <span class="attr">pattern</span>: <span class="keyword">new</span> hexo_util_1.<span class="title class_">Pattern</span>(<span class="function"><span class="params">path</span> =></span> {</span><br><span class="line"> <span class="comment">// 1. 临时的文件直接拒绝</span></span><br><span class="line"> <span class="keyword">if</span> ((<span class="number">0</span>, common_1.<span class="property">isTmpFile</span>)(path))</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> <span class="comment">// 2. 文件必须来自 _posts/ 或者 /_drafts 目录</span></span><br><span class="line"> <span class="keyword">let</span> result;</span><br><span class="line"> <span class="keyword">if</span> (path.<span class="title function_">startsWith</span>(postDir)) {</span><br><span class="line"> result = {</span><br><span class="line"> <span class="attr">published</span>: <span class="literal">true</span>,</span><br><span class="line"> <span class="attr">path</span>: path.<span class="title function_">substring</span>(postDir.<span class="property">length</span>)</span><br><span class="line"> };</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> (path.<span class="title function_">startsWith</span>(draftDir)) {</span><br><span class="line"> result = {</span><br><span class="line"> <span class="attr">published</span>: <span class="literal">false</span>,</span><br><span class="line"> <span class="attr">path</span>: path.<span class="title function_">substring</span>(draftDir.<span class="property">length</span>)</span><br><span class="line"> };</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 3. 隐藏的文件直接拒绝</span></span><br><span class="line"> <span class="keyword">if</span> (!result || (<span class="number">0</span>, common_1.<span class="property">isHiddenFile</span>)(result.<span class="property">path</span>))</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> <span class="comment">// 4. 判断其可渲染性(ctx.render 会在下一章提起)</span></span><br><span class="line"> result.<span class="property">renderable</span> = ctx.<span class="property">render</span>.<span class="title function_">isRenderable</span>(path) && !(<span class="number">0</span>, common_1.<span class="property">isMatch</span>)(path, ctx.<span class="property">config</span>.<span class="property">skip_render</span>);</span><br><span class="line"> <span class="comment">// 5. 处理文章资源文件夹</span></span><br><span class="line"> <span class="comment">// 如果开启了 post_asset_folder,那么只有和默认文章扩展名相同的文件才被认为是可渲染的文章</span></span><br><span class="line"> <span class="keyword">if</span> (result.<span class="property">renderable</span> && ctx.<span class="property">config</span>.<span class="property">post_asset_folder</span>) {</span><br><span class="line"> result.<span class="property">renderable</span> = ((<span class="number">0</span>, path_1.<span class="property">extname</span>)(ctx.<span class="property">config</span>.<span class="property">new_post_name</span>) === (<span class="number">0</span>, path_1.<span class="property">extname</span>)(path));</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> result;</span><br><span class="line"> }),</span><br><span class="line"> };</span><br><span class="line">};</span><br></pre></td></tr></tbody></table></figure>
<p>这个函数返回的 <code>result</code> 对象,会被 <code>Box</code> 附加到 <code>File</code> 实例的 <code>params</code> 属性上,供 <code>process</code> 函数使用。</p>
<h4 id="232-分流与处理"><a class="markdownIt-Anchor" href="#232-分流与处理"></a> 2.3.2. 分流与处理</h4>
<p><code>post</code> 处理器的 <code>process</code> 函数会读取 <code>file.params.renderable</code> 的值,决定下一步操作:</p>
<figure class="highlight javascript"><figcaption><span>/plugins/processor/post.JS</span></figcaption><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = <span class="function">(<span class="params">ctx</span>) =></span> {</span><br><span class="line"> <span class="keyword">return</span> {</span><br><span class="line"> <span class="attr">process</span>: <span class="keyword">function</span> <span class="title function_">postProcessor</span>(<span class="params">file</span>) {</span><br><span class="line"> <span class="comment">// 如果可渲染,就作为文章处理</span></span><br><span class="line"> <span class="keyword">if</span> (file.<span class="property">params</span>.<span class="property">renderable</span>) {</span><br><span class="line"> <span class="keyword">return</span> <span class="title function_">processPost</span>(ctx, file);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 否则,如果开启了 post_asset_folder,则作为文章的静态资源处理</span></span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> (ctx.<span class="property">config</span>.<span class="property">post_asset_folder</span>) {</span><br><span class="line"> <span class="keyword">return</span> <span class="title function_">processAsset</span>(ctx, file);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<h4 id="233-精加工与入库"><a class="markdownIt-Anchor" href="#233-精加工与入库"></a> 2.3.3. 精加工与入库</h4>
<p>当 <code>process</code> 函数决定一个文件是 <strong>可渲染的</strong> 文章时,它就会把 <code>file</code> 对象交给 <code>processPost</code> 函数。这个函数使整个 <code>Processor</code> 中最复杂的部分,它的任务是 <strong>读取文件、解析并融合所有元数据,最后将结构化的文章数据存入数据库</strong>。</p>
<p>整个过程可以分解为以下几个关键步骤:</p>
<ol>
<li>
<p>准备与前置处理</p>
<p>函数首先会进行一些准备工作,并处理几种简单的文件状态。</p>
<figure class="highlight javascript"><figcaption><span>/plugins/processor/post.JS</span></figcaption><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">processPost</span>(<span class="params">ctx, file</span>) {</span><br><span class="line"> <span class="keyword">const</span> <span class="title class_">Post</span> = ctx.<span class="title function_">model</span>(<span class="string">'Post'</span>);</span><br><span class="line"> <span class="keyword">const</span> { path } = file.<span class="property">params</span>;</span><br><span class="line"> <span class="keyword">const</span> doc = <span class="title class_">Post</span>.<span class="title function_">findOne</span>({ <span class="attr">source</span>: file.<span class="property">path</span> }); <span class="comment">// 1. 检查数据中是否已存在该文章</span></span><br><span class="line"> <span class="keyword">const</span> { config } = ctx;</span><br><span class="line"> <span class="keyword">const</span> { <span class="attr">timezone</span>: timezoneCfg, updated_option, use_slug_as_post_title } = config;</span><br><span class="line"> <span class="keyword">let</span> categories, tags;</span><br><span class="line"> <span class="comment">// 2. 根据文件类型进行处理</span></span><br><span class="line"> <span class="keyword">if</span> (file.<span class="property">type</span> === <span class="string">'skip'</span> && doc) {</span><br><span class="line"> <span class="keyword">return</span>; <span class="comment">// 文件未修改,直接跳过</span></span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (file.<span class="property">type</span> === <span class="string">'delete'</span>) {</span><br><span class="line"> <span class="keyword">if</span> (doc) {</span><br><span class="line"> <span class="keyword">return</span> doc.<span class="title function_">remove</span>(); <span class="comment">// 文件已删除,从数据库移除</span></span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br></pre></td></tr></tbody></table></figure>
</li>
<li>
<p>异步读取与初步解析</p>
<p>对于 <code>create</code> 或 <code>update</code> 状态的文件,处理正式开始。Hexo 会并行地执行两个异步操作:读取文件内容和获取文件系统状态。</p>
<figure class="highlight javascript"><figcaption><span>/plugins/processor/post.JS</span></figcaption><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">return</span> bluebird_1.<span class="property">default</span>.<span class="title function_">all</span>([</span><br><span class="line"> file.<span class="title function_">stat</span>(), <span class="comment">// 获取文件系统信息,例如创建、修改时间</span></span><br><span class="line"> file.<span class="title function_">read</span>() <span class="comment">// 读取文件内容</span></span><br><span class="line">]).<span class="title function_">spread</span>(<span class="function">(<span class="params">stats, content</span>) =></span> {</span><br><span class="line"> <span class="comment">// 1. 解析 Front-matter</span></span><br><span class="line"> <span class="keyword">const</span> data = (<span class="number">0</span>, hexo_front_matter_1.<span class="property">parse</span>)(content);</span><br><span class="line"> <span class="comment">// 2. 解析文件名</span></span><br><span class="line"> <span class="keyword">const</span> info = <span class="title function_">parseFilename</span>(config.<span class="property">new_post_name</span>, path);</span><br></pre></td></tr></tbody></table></figure>
<p><code>hexo_front_matter_1.parse(content)</code> 会将文件头部的 YAML Front-matter(<code>---</code> 开头)解析成一个 JavaScript 对象 <code>data</code>。文件的正文内容会存放在 <code>data._content</code> 属性中:</p>
<figure class="highlight javascript"><figcaption><span>front_matter.JS</span></figcaption><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">parse</span>(<span class="params">str, options</span>) {</span><br><span class="line"> <span class="keyword">if</span> (<span class="keyword">typeof</span> str !== <span class="string">'string'</span>)</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">TypeError</span>(<span class="string">'str is required!'</span>);</span><br><span class="line"> <span class="keyword">const</span> splitData = <span class="title function_">split</span>(str);</span><br><span class="line"> <span class="keyword">const</span> raw = splitData.<span class="property">data</span>;</span><br><span class="line"> <span class="keyword">if</span> (!raw)</span><br><span class="line"> <span class="keyword">return</span> { <span class="attr">_content</span>: str };</span><br><span class="line"> <span class="keyword">let</span> data;</span><br><span class="line"> <span class="keyword">if</span> (splitData.<span class="property">separator</span>.<span class="title function_">startsWith</span>(<span class="string">';'</span>)) {</span><br><span class="line"> data = <span class="title function_">parseJSON</span>(raw);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span> {</span><br><span class="line"> data = <span class="title function_">parseYAML</span>(raw, options);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (!data)</span><br><span class="line"> <span class="keyword">return</span> { <span class="attr">_content</span>: str };</span><br><span class="line"> <span class="comment">// Convert timezone</span></span><br><span class="line"> <span class="title class_">Object</span>.<span class="title function_">keys</span>(data).<span class="title function_">forEach</span>(<span class="function"><span class="params">key</span> =></span> {</span><br><span class="line"> <span class="keyword">const</span> item = data[key];</span><br><span class="line"> <span class="keyword">if</span> (item <span class="keyword">instanceof</span> <span class="title class_">Date</span>) {</span><br><span class="line"> data[key] = <span class="keyword">new</span> <span class="title class_">Date</span>(item.<span class="title function_">getTime</span>() + (item.<span class="title function_">getTimezoneOffset</span>() * <span class="number">60</span> * <span class="number">1000</span>));</span><br><span class="line"> }</span><br><span class="line"> });</span><br><span class="line"> data.<span class="property">_content</span> = splitData.<span class="property">content</span>;</span><br><span class="line"> <span class="keyword">return</span> data;</span><br><span class="line">}</span><br><span class="line"><span class="built_in">exports</span>.<span class="property">parse</span> = parse;</span><br></pre></td></tr></tbody></table></figure>
<p><code>parseFilename</code> 函数则会根据我们在 <code>_config.yml</code> 中配置的 <code>new_post_name</code> 格式,从文件名中尝试提取 <code>year</code>、<code>month</code>、<code>day</code>、<code>title</code> 等信息,存放在 <code>info</code> 对象中:</p>
<figure class="highlight javascript"><figcaption><span>/plugins/processor/post.JS</span></figcaption><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">parseFilename</span>(<span class="params">config, path</span>) {</span><br><span class="line"> config = config.<span class="title function_">substring</span>(<span class="number">0</span>, config.<span class="property">length</span> - (<span class="number">0</span>, path_1.<span class="property">extname</span>)(config).<span class="property">length</span>);</span><br><span class="line"> path = path.<span class="title function_">substring</span>(<span class="number">0</span>, path.<span class="property">length</span> - (<span class="number">0</span>, path_1.<span class="property">extname</span>)(path).<span class="property">length</span>);</span><br><span class="line"> <span class="keyword">if</span> (!permalink || permalink.<span class="property">rule</span> !== config) {</span><br><span class="line"> permalink = <span class="keyword">new</span> hexo_util_1.<span class="title class_">Permalink</span>(config, {</span><br><span class="line"> <span class="attr">segments</span>: {</span><br><span class="line"> <span class="attr">year</span>: <span class="regexp">/(\d{4})/</span>,</span><br><span class="line"> <span class="attr">month</span>: <span class="regexp">/(\d{2})/</span>,</span><br><span class="line"> <span class="attr">day</span>: <span class="regexp">/(\d{2})/</span>,</span><br><span class="line"> <span class="attr">i_month</span>: <span class="regexp">/(\d{1,2})/</span>,</span><br><span class="line"> <span class="attr">i_day</span>: <span class="regexp">/(\d{1,2})/</span>,</span><br><span class="line"> <span class="attr">hash</span>: <span class="regexp">/([0-9a-f]{12})/</span></span><br><span class="line"> }</span><br><span class="line"> });</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">const</span> data = permalink.<span class="title function_">parse</span>(path);</span><br><span class="line"> <span class="keyword">if</span> (data) {</span><br><span class="line"> <span class="keyword">if</span> (data.<span class="property">title</span> !== <span class="literal">undefined</span>) {</span><br><span class="line"> <span class="keyword">return</span> data;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> <span class="title class_">Object</span>.<span class="title function_">assign</span>(data, {</span><br><span class="line"> <span class="attr">title</span>: (<span class="number">0</span>, hexo_util_1.<span class="property">slugize</span>)(path)</span><br><span class="line"> });</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> {</span><br><span class="line"> <span class="attr">title</span>: (<span class="number">0</span>, hexo_util_1.<span class="property">slugize</span>)(path)</span><br><span class="line"> };</span><br><span class="line">} </span><br></pre></td></tr></tbody></table></figure>
</li>
<li>
<p>数据融合与标准化</p>
<p>这是 <code>processPost</code> 最核心的部分。它会将上一步得到的 <code>data</code> 和 <code>info</code>,以及 <code>stats</code> 这三个来源的数据进行智能融合,并进行标准化处理,最终形成一篇完整、规范的文章数据。</p>
<p>这个融合过程遵循明确的优先级:<strong>Front-matter > 文件名 > 文件系统信息</strong>。</p>
<p>元数据确定逻辑:</p>
<ul>
<li><code>slug</code> 和 <code>source</code>:直接从文件名解析结果或文件路径中获取</li>
<li><code>title</code>:优先使用 Front-matter 中的 <code>title</code>。如果没有,并且 <code>use_slug_as_post_title</code> 配置为 <code>true</code>,则会使用从文件名中解析出的 <code>slug</code> 作为标题</li>
<li><code>date</code>:
<ol>
<li>优先使用 Front-matter 中的 <code>date</code></li>
<li>其次尝试从文件名中解析(<code>info.year</code> 等)</li>
<li>最后使用文件的创建时间 <code>stats.birthtime</code></li>
</ol>
</li>
<li><code>updated</code>:
<ol>
<li>优先使用 Front-matter 里的</li>
<li>其次根据 <code>_config.yml</code> 的 <code>updated_options</code> 配置,可能使用 <code>date</code> 的值或者留空</li>
<li>最后默认使用文件的修改时间 <code>stats.mtime</code></li>
</ol>
</li>
<li><code>tags</code> 和 <code>categories</code>:函数会进行标准化处理,确保它们最终都是数组格式,并处理了 <code>tag</code>(单数)-> <code>tags</code>(复数)这种别名情况</li>
</ul>
<figure class="highlight javascript"><figcaption><span>/plugins/processor/post.JS</span></figcaption><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> keys = <span class="title class_">Object</span>.<span class="title function_">keys</span>(info);</span><br><span class="line">data.<span class="property">source</span> = file.<span class="property">path</span>;</span><br><span class="line">data.<span class="property">raw</span> = content;</span><br><span class="line">data.<span class="property">slug</span> = info.<span class="property">title</span>;</span><br><span class="line"><span class="keyword">if</span> (file.<span class="property">params</span>.<span class="property">published</span>) {</span><br><span class="line"> <span class="keyword">if</span> (!<span class="title class_">Object</span>.<span class="property"><span class="keyword">prototype</span></span>.<span class="property">hasOwnProperty</span>.<span class="title function_">call</span>(data, <span class="string">'published'</span>))</span><br><span class="line"> data.<span class="property">published</span> = <span class="literal">true</span>;</span><br><span class="line">}</span><br><span class="line"><span class="keyword">else</span> {</span><br><span class="line"> data.<span class="property">published</span> = <span class="literal">false</span>;</span><br><span class="line">}</span><br><span class="line"><span class="keyword">for</span> (<span class="keyword">let</span> i = <span class="number">0</span>, len = keys.<span class="property">length</span>; i < len; i++) {</span><br><span class="line"> <span class="keyword">const</span> key = keys[i];</span><br><span class="line"> <span class="keyword">if</span> (!preservedKeys[key])</span><br><span class="line"> data[key] = info[key];</span><br><span class="line">}</span><br><span class="line"><span class="comment">// use `slug` as `title` of post when `title` is not specified.</span></span><br><span class="line"><span class="comment">// https://github.com/hexojs/hexo/issues/5372</span></span><br><span class="line"><span class="keyword">if</span> (use_slug_as_post_title && !(<span class="string">'title'</span> <span class="keyword">in</span> data)) {</span><br><span class="line"> data.<span class="property">title</span> = info.<span class="property">title</span>;</span><br><span class="line">}</span><br><span class="line"><span class="keyword">if</span> (data.<span class="property">date</span>) {</span><br><span class="line"> data.<span class="property">date</span> = (<span class="number">0</span>, common_1.<span class="property">toDate</span>)(data.<span class="property">date</span>);</span><br><span class="line">}</span><br><span class="line"><span class="keyword">else</span> <span class="keyword">if</span> (info && info.<span class="property">year</span> && (info.<span class="property">month</span> || info.<span class="property">i_month</span>) && (info.<span class="property">day</span> || info.<span class="property">i_day</span>)) {</span><br><span class="line"> data.<span class="property">date</span> = <span class="keyword">new</span> <span class="title class_">Date</span>(info.<span class="property">year</span>, <span class="built_in">parseInt</span>(info.<span class="property">month</span> || info.<span class="property">i_month</span>, <span class="number">10</span>) - <span class="number">1</span>, <span class="built_in">parseInt</span>(info.<span class="property">day</span> || info.<span class="property">i_day</span>, <span class="number">10</span>));</span><br><span class="line">}</span><br><span class="line"><span class="keyword">if</span> (data.<span class="property">date</span>) {</span><br><span class="line"> <span class="keyword">if</span> (timezoneCfg)</span><br><span class="line"> data.<span class="property">date</span> = (<span class="number">0</span>, common_1.<span class="property">timezone</span>)(data.<span class="property">date</span>, timezoneCfg);</span><br><span class="line">}</span><br><span class="line"><span class="keyword">else</span> {</span><br><span class="line"> data.<span class="property">date</span> = stats.<span class="property">birthtime</span>;</span><br><span class="line">}</span><br><span class="line">data.<span class="property">updated</span> = (<span class="number">0</span>, common_1.<span class="property">toDate</span>)(data.<span class="property">updated</span>);</span><br><span class="line"><span class="keyword">if</span> (data.<span class="property">updated</span>) {</span><br><span class="line"> <span class="keyword">if</span> (timezoneCfg)</span><br><span class="line"> data.<span class="property">updated</span> = (<span class="number">0</span>, common_1.<span class="property">timezone</span>)(data.<span class="property">updated</span>, timezoneCfg);</span><br><span class="line">}</span><br><span class="line"><span class="keyword">else</span> <span class="keyword">if</span> (updated_option === <span class="string">'date'</span>) {</span><br><span class="line"> data.<span class="property">updated</span> = data.<span class="property">date</span>;</span><br><span class="line">}</span><br><span class="line"><span class="keyword">else</span> <span class="keyword">if</span> (updated_option === <span class="string">'empty'</span>) {</span><br><span class="line"> data.<span class="property">updated</span> = <span class="literal">undefined</span>;</span><br><span class="line">}</span><br><span class="line"><span class="keyword">else</span> {</span><br><span class="line"> data.<span class="property">updated</span> = stats.<span class="property">mtime</span>;</span><br><span class="line">}</span><br><span class="line"><span class="keyword">if</span> (data.<span class="property">category</span> && !data.<span class="property">categories</span>) {</span><br><span class="line"> data.<span class="property">categories</span> = data.<span class="property">category</span>;</span><br><span class="line"> data.<span class="property">category</span> = <span class="literal">undefined</span>;</span><br><span class="line">}</span><br><span class="line"><span class="keyword">if</span> (data.<span class="property">tag</span> && !data.<span class="property">tags</span>) {</span><br><span class="line"> data.<span class="property">tags</span> = data.<span class="property">tag</span>;</span><br><span class="line"> data.<span class="property">tag</span> = <span class="literal">undefined</span>;</span><br><span class="line">}</span><br><span class="line">categories = data.<span class="property">categories</span> || [];</span><br><span class="line">tags = data.<span class="property">tags</span> || [];</span><br><span class="line"><span class="keyword">if</span> (!<span class="title class_">Array</span>.<span class="title function_">isArray</span>(categories))</span><br><span class="line"> categories = [categories];</span><br><span class="line"><span class="keyword">if</span> (!<span class="title class_">Array</span>.<span class="title function_">isArray</span>(tags))</span><br><span class="line"> tags = [tags];</span><br><span class="line"><span class="keyword">if</span> (data.<span class="property">photo</span> && !data.<span class="property">photos</span>) {</span><br><span class="line"> data.<span class="property">photos</span> = data.<span class="property">photo</span>;</span><br><span class="line"> data.<span class="property">photo</span> = <span class="literal">undefined</span>;</span><br><span class="line">}</span><br><span class="line"><span class="keyword">if</span> (data.<span class="property">photos</span> && !<span class="title class_">Array</span>.<span class="title function_">isArray</span>(data.<span class="property">photos</span>)) {</span><br><span class="line"> data.<span class="property">photos</span> = [data.<span class="property">photos</span>];</span><br><span class="line">}</span><br><span class="line"><span class="keyword">if</span> (data.<span class="property">permalink</span>) {</span><br><span class="line"> data.<span class="property">__permalink</span> = data.<span class="property">permalink</span>;</span><br><span class="line"> data.<span class="property">permalink</span> = <span class="literal">undefined</span>;</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
</li>
<li>
<p>数据库持久化</p>
<p>当所有数据都准备就绪后,便进入了最终的数据库操作阶段。</p>
<figure class="highlight javascript"><figcaption><span>/plugins/processor/post.JS</span></figcaption><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"> <span class="keyword">if</span> (doc) { <span class="comment">// 如果在步骤一中找到了记录</span></span><br><span class="line"> <span class="keyword">if</span> (file.<span class="property">type</span> !== <span class="string">'update'</span>) {</span><br><span class="line"> ctx.<span class="property">log</span>.<span class="title function_">warn</span>(<span class="string">`Trying to "create" <span class="subst">${(<span class="number">0</span>, picocolors_1.magenta)(file.path)}</span>, but the file already exists!`</span>);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 文件已存在,执行更新操作</span></span><br><span class="line"> <span class="keyword">return</span> doc.<span class="title function_">replace</span>(data);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 文件是新增的,执行插入操作</span></span><br><span class="line"> <span class="keyword">return</span> <span class="title class_">Post</span>.<span class="title function_">insert</span>(data);</span><br><span class="line">})</span><br></pre></td></tr></tbody></table></figure>
</li>
<li>
<p>建立关联</p>
<p>数据入库后,工作还没有完全结束。</p>
<p><code>insert/replace</code> 操作会返回一个 Promise,其结果是处理后的数据库文档 <code>doc</code>。接下来的 <code>.then()</code> 中,Hexo 会完成最后一步:建立模型之间的关联。</p>
<figure class="highlight javascript"><figcaption><span>/plugins/processor/post.JS</span></figcaption><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">.<span class="title function_">then</span>(<span class="function"><span class="params">doc</span> =></span> bluebird_1.<span class="property">default</span>.<span class="title function_">all</span>([</span><br><span class="line"> doc.<span class="title function_">setCategories</span>(categories), <span class="comment">// 将文章与分类模型关联</span></span><br><span class="line"> doc.<span class="title function_">setTags</span>(tags), <span class="comment">// 将文章与标签模型关联</span></span><br><span class="line"> <span class="title function_">scanAssetDir</span>(ctx, doc) <span class="comment">// 如果开启,处理文章的资源文件夹</span></span><br><span class="line"> ]));</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p><code>doc.setCategories</code> 和 <code>doc.setTags</code> 方法会处理 <code>categories</code> 和 <code>tags</code> 数组,在 <code>Category</code>、<code>Tag</code> 以及中间关联表中创建或更新记录。这使得我们之后可以轻松地通过 <code>post.tags</code> 或 <code>category.posts</code> 来查询关联数据。</p>
</li>
</ol>
<p>至此,<code>processPost</code> 的全部工作完成。一篇原始的 Markdown 文件,经过这一系列精密的加工,已经变成了一条结构完整、关系清晰的数据库记录。</p>
<h1 id="3-从-markdown-到-html"><a class="markdownIt-Anchor" href="#3-从-markdown-到-html"></a> 3. 从 Markdown 到 HTML</h1>
<p>上一章,我们看到 <code>post</code> 处理器将 Markdown 源文件加工成了包含 <code>_content</code>(原始 Markdown 文本)的数据库记录。但此时,文章的 <code>content</code> 属性仍然是空的。从 <code>_content</code> 到 <code>content</code>(渲染后的 HTML),是整个生成过程中最关键的「炼金」步骤。</p>
<p>这个转换不是在 <code>source.process()</code> 阶段发生的,而是在稍后的一个特殊时机,由 <code>Filter</code> 机制驱动。</p>
<h2 id="31-before_generate-过滤器"><a class="markdownIt-Anchor" href="#31-before_generate-过滤器"></a> 3.1. <code>before_generate</code> 过滤器</h2>
<p>在 <code>_generate</code> 方法的核心逻辑中,<code>_runGenerators</code> 执行之前,有一个关键的前置步骤:</p>
<figure class="highlight javascript"><figcaption><span>/hexo/index.JS</span></figcaption><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="title function_">_generate</span>(<span class="params">options = {}</span>) {</span><br><span class="line"> <span class="keyword">if</span> (<span class="variable language_">this</span>.<span class="property">_isGenerating</span>)</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> <span class="keyword">const</span> useCache = options.<span class="property">cache</span>;</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">_isGenerating</span> = <span class="literal">true</span>;</span><br><span class="line"> <span class="variable language_">this</span>.<span class="title function_">emit</span>(<span class="string">'generateBefore'</span>);</span><br><span class="line"> <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="title function_">execFilter</span>(<span class="string">'before_generate'</span>, <span class="literal">null</span>, { <span class="attr">context</span>: <span class="variable language_">this</span> }) <span class="comment">// 在这里</span></span><br><span class="line"> .<span class="title function_">then</span>(<span class="function">() =></span> <span class="variable language_">this</span>.<span class="title function_">_routerRefresh</span>(<span class="variable language_">this</span>.<span class="title function_">_runGenerators</span>(), useCache)).<span class="title function_">then</span>(<span class="function">() =></span> {</span><br><span class="line"> <span class="variable language_">this</span>.<span class="title function_">emit</span>(<span class="string">'generateAfter'</span>);</span><br><span class="line"> <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="title function_">execFilter</span>(<span class="string">'after_generate'</span>, <span class="literal">null</span>, { <span class="attr">context</span>: <span class="variable language_">this</span> });</span><br><span class="line"> }).<span class="title function_">finally</span>(<span class="function">() =></span> {</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">_isGenerating</span> = <span class="literal">false</span>;</span><br><span class="line"> });</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p><code>execFilter</code> 方法会执行所有注册在 <code>before_generate</code> 这个钩子上的函数。Hexo 默认在这里注册了一个名为 <code>renderPostFilter</code> 的过滤器,它的任务就是:确保所有文章在生成页面蓝图之前,都已经被渲染成 HTML。</p>
<p>这个过滤器的注册和定义如下:</p>
<figure class="highlight javascript"><figcaption><span>/plugins/filter/before_generate/index.JS</span></figcaption><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">"use strict"</span>;</span><br><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = <span class="function">(<span class="params">ctx</span>) =></span> {</span><br><span class="line"> <span class="keyword">const</span> { filter } = ctx.<span class="property">extend</span>;</span><br><span class="line"> <span class="comment">// 注册 render_post.js 到 before_generate 钩子</span></span><br><span class="line"> filter.<span class="title function_">register</span>(<span class="string">'before_generate'</span>, <span class="built_in">require</span>(<span class="string">'./render_post'</span>));</span><br><span class="line">};</span><br><span class="line"><span class="comment">//# sourceMappingURL=index.js.map</span></span><br></pre></td></tr></tbody></table></figure>
<figure class="highlight javascript"><figcaption><span>/plugins/filter/before_generate/render_post.JS</span></figcaption><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">"use strict"</span>;</span><br><span class="line"><span class="keyword">var</span> __importDefault = (<span class="variable language_">this</span> && <span class="variable language_">this</span>.<span class="property">__importDefault</span>) || <span class="keyword">function</span> (<span class="params">mod</span>) {</span><br><span class="line"> <span class="keyword">return</span> (mod && mod.<span class="property">__esModule</span>) ? mod : { <span class="string">"default"</span>: mod };</span><br><span class="line">};</span><br><span class="line"><span class="keyword">const</span> bluebird_1 = <span class="title function_">__importDefault</span>(<span class="built_in">require</span>(<span class="string">"bluebird"</span>));</span><br><span class="line"><span class="keyword">function</span> <span class="title function_">renderPostFilter</span>(<span class="params"></span>) {</span><br><span class="line"> <span class="comment">// 定义一个处理指定模型的函数</span></span><br><span class="line"> <span class="keyword">const</span> <span class="title function_">renderPosts</span> = model => {</span><br><span class="line"> <span class="comment">// 1. 找出所有 content 属性为 null 的文章/页面</span></span><br><span class="line"> <span class="keyword">const</span> posts = model.<span class="title function_">toArray</span>().<span class="title function_">filter</span>(<span class="function"><span class="params">post</span> =></span> post.<span class="property">content</span> == <span class="literal">null</span>);</span><br><span class="line"> <span class="comment">// 2. 遍历这些未渲染的条目</span></span><br><span class="line"> <span class="keyword">return</span> bluebird_1.<span class="property">default</span>.<span class="title function_">map</span>(posts, <span class="function">(<span class="params">post</span>) =></span> {</span><br><span class="line"> <span class="comment">// 2.1. 将原始 Markdown 内容赋值给 content</span></span><br><span class="line"> post.<span class="property">content</span> = post.<span class="property">_content</span>;</span><br><span class="line"> <span class="comment">// 2.2. 调用核心的渲染方法</span></span><br><span class="line"> <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="property">post</span>.<span class="title function_">render</span>(post.<span class="property">full_source</span>, post).<span class="title function_">then</span>(<span class="function">() =></span> post.<span class="title function_">save</span>());</span><br><span class="line"> });</span><br><span class="line"> };</span><br><span class="line"> <span class="comment">// 并行处理 Post 和 Page 两个模型</span></span><br><span class="line"> <span class="keyword">return</span> bluebird_1.<span class="property">default</span>.<span class="title function_">all</span>([</span><br><span class="line"> <span class="title function_">renderPosts</span>(<span class="variable language_">this</span>.<span class="title function_">model</span>(<span class="string">'Post'</span>)),</span><br><span class="line"> <span class="title function_">renderPosts</span>(<span class="variable language_">this</span>.<span class="title function_">model</span>(<span class="string">'Page'</span>))</span><br><span class="line"> ]);</span><br><span class="line">}</span><br><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = renderPostFilter;</span><br><span class="line"><span class="comment">//# sourceMappingURL=render_post.js.map</span></span><br></pre></td></tr></tbody></table></figure>
<h2 id="32-postrender"><a class="markdownIt-Anchor" href="#32-postrender"></a> 3.2. <code>post.render</code></h2>
<p><code>this.post.render</code> 是整个内容转换过程的核心。它确保了 Markdown 在转换为 HTML 的过程中,内嵌的特殊标签(例如 Swig 模版标签)不会被破坏,并且能被正确地执行。</p>
<p>那么 <code>this.post.render</code> 从哪里来的?回到 Hexo 实例:</p>
<figure class="highlight javascript"><figcaption><span>/hexo/index.JS</span></figcaption><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> post_1 = <span class="title function_">__importDefault</span>(<span class="built_in">require</span>(<span class="string">"./post"</span>));</span><br><span class="line"><span class="variable language_">this</span>.<span class="property">post</span> = <span class="keyword">new</span> post_1.<span class="title function_">default</span>(<span class="variable language_">this</span>);</span><br></pre></td></tr></tbody></table></figure>
<p>我们再来深入 <code>Post</code> 类的 <code>render</code> 方法:</p>
<figure class="highlight javascript"><figcaption><span>/hexo/post.JS</span></figcaption><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br></pre></td><td class="code"><pre><span class="line"><span class="title function_">render</span>(<span class="params">source, data = {}, callback</span>) {</span><br><span class="line"> <span class="keyword">const</span> ctx = <span class="variable language_">this</span>.<span class="property">context</span>;</span><br><span class="line"> <span class="keyword">const</span> { config } = ctx;</span><br><span class="line"> <span class="keyword">const</span> { tag } = ctx.<span class="property">extend</span>;</span><br><span class="line"> <span class="comment">// 文件读取和非文章类型文件的处理逻辑</span></span><br><span class="line"> <span class="keyword">const</span> ext = data.<span class="property">engine</span> || (source ? (<span class="number">0</span>, path_1.<span class="property">extname</span>)(source) : <span class="string">''</span>);</span><br><span class="line"> <span class="keyword">let</span> promise;</span><br><span class="line"> <span class="keyword">if</span> (data.<span class="property">content</span> != <span class="literal">null</span>) {</span><br><span class="line"> promise = bluebird_1.<span class="property">default</span>.<span class="title function_">resolve</span>(data.<span class="property">content</span>);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> (source) {</span><br><span class="line"> promise = (<span class="number">0</span>, hexo_fs_1.<span class="property">readFile</span>)(source);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">return</span> bluebird_1.<span class="property">default</span>.<span class="title function_">reject</span>(<span class="keyword">new</span> <span class="title class_">Error</span>(<span class="string">'No input file or string!'</span>)).<span class="title function_">asCallback</span>(callback);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">const</span> isPost = !data.<span class="property">source</span> || [<span class="string">'html'</span>, <span class="string">'htm'</span>].<span class="title function_">includes</span>(ctx.<span class="property">render</span>.<span class="title function_">getOutput</span>(data.<span class="property">source</span>));</span><br><span class="line"> <span class="keyword">if</span> (!isPost) {</span><br><span class="line"> <span class="keyword">return</span> promise.<span class="title function_">then</span>(<span class="function"><span class="params">content</span> =></span> {</span><br><span class="line"> data.<span class="property">content</span> = content;</span><br><span class="line"> ctx.<span class="property">log</span>.<span class="title function_">debug</span>(<span class="string">'Rendering file: %s'</span>, (<span class="number">0</span>, picocolors_1.<span class="property">magenta</span>)(source));</span><br><span class="line"> <span class="keyword">return</span> ctx.<span class="property">render</span>.<span class="title function_">render</span>({</span><br><span class="line"> <span class="attr">text</span>: data.<span class="property">content</span>,</span><br><span class="line"> <span class="attr">path</span>: source,</span><br><span class="line"> <span class="attr">engine</span>: data.<span class="property">engine</span>,</span><br><span class="line"> <span class="attr">toString</span>: <span class="literal">true</span></span><br><span class="line"> });</span><br><span class="line"> }).<span class="title function_">then</span>(<span class="function"><span class="params">content</span> =></span> {</span><br><span class="line"> data.<span class="property">content</span> = content;</span><br><span class="line"> <span class="keyword">return</span> data;</span><br><span class="line"> }).<span class="title function_">asCallback</span>(callback);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">let</span> disableNunjucks = ext && ctx.<span class="property">render</span>.<span class="property">renderer</span>.<span class="title function_">get</span>(ext) && !!ctx.<span class="property">render</span>.<span class="property">renderer</span>.<span class="title function_">get</span>(ext).<span class="property">disableNunjucks</span>;</span><br><span class="line"> <span class="keyword">if</span> (<span class="keyword">typeof</span> data.<span class="property">disableNunjucks</span> === <span class="string">'boolean'</span>)</span><br><span class="line"> disableNunjucks = data.<span class="property">disableNunjucks</span>;</span><br><span class="line"> <span class="keyword">const</span> cacheObj = <span class="keyword">new</span> <span class="title class_">PostRenderEscape</span>(); <span class="comment">// 1. 创建转义工具实例</span></span><br><span class="line"> <span class="keyword">return</span> promise.<span class="title function_">then</span>(<span class="function"><span class="params">content</span> =></span> { <span class="comment">// promise 在之前已读取文件内容</span></span><br><span class="line"> data.<span class="property">content</span> = content;</span><br><span class="line"> <span class="comment">// 2. 执行 before_post_render 过滤器</span></span><br><span class="line"> <span class="keyword">return</span> ctx.<span class="title function_">execFilter</span>(<span class="string">'before_post_render'</span>, data, { <span class="attr">context</span>: ctx });</span><br><span class="line"> }).<span class="title function_">then</span>(<span class="function">() =></span> {</span><br><span class="line"> <span class="comment">// 3. 转义代码和 Swig 标签</span></span><br><span class="line"> data.<span class="property">content</span> = cacheObj.<span class="title function_">escapeCodeBlocks</span>(data.<span class="property">content</span>);</span><br><span class="line"> <span class="keyword">if</span> (disableNunjucks === <span class="literal">false</span>) {</span><br><span class="line"> data.<span class="property">content</span> = cacheObj.<span class="title function_">escapeAllSwigTags</span>(data.<span class="property">content</span>);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">const</span> options = data.<span class="property">markdown</span> || {};</span><br><span class="line"> <span class="keyword">if</span> (!config.<span class="property">syntax_highlighter</span>)</span><br><span class="line"> options.<span class="property">highlight</span> = <span class="literal">null</span>;</span><br><span class="line"> ctx.<span class="property">log</span>.<span class="title function_">debug</span>(<span class="string">'Rendering post: %s'</span>, (<span class="number">0</span>, picocolors_1.<span class="property">magenta</span>)(source));</span><br><span class="line"> <span class="comment">// 4. 调用核心渲染器</span></span><br><span class="line"> <span class="keyword">return</span> ctx.<span class="property">render</span>.<span class="title function_">render</span>({</span><br><span class="line"> <span class="attr">text</span>: data.<span class="property">content</span>,</span><br><span class="line"> <span class="attr">path</span>: source,</span><br><span class="line"> <span class="attr">engine</span>: data.<span class="property">engine</span>,</span><br><span class="line"> <span class="attr">toString</span>: <span class="literal">true</span>,</span><br><span class="line"> <span class="title function_">onRenderEnd</span>(<span class="params">content</span>) { <span class="comment">// 5. 回调</span></span><br><span class="line"> <span class="comment">// 5.1. 恢复 Swig 标签</span></span><br><span class="line"> data.<span class="property">content</span> = cacheObj.<span class="title function_">restoreAllSwigTags</span>(content);</span><br><span class="line"> <span class="keyword">if</span> (disableNunjucks)</span><br><span class="line"> <span class="keyword">return</span> data.<span class="property">content</span>;</span><br><span class="line"> <span class="comment">// 5.2. 执行 Swig 标签</span></span><br><span class="line"> <span class="keyword">return</span> tag.<span class="title function_">render</span>(data.<span class="property">content</span>, data);</span><br><span class="line"> }</span><br><span class="line"> }, options);</span><br><span class="line"> }).<span class="title function_">then</span>(<span class="function"><span class="params">content</span> =></span> {</span><br><span class="line"> <span class="comment">// 6. 恢复代码块</span></span><br><span class="line"> data.<span class="property">content</span> = cacheObj.<span class="title function_">restoreCodeBlocks</span>(content);</span><br><span class="line"> <span class="comment">// 7. 执行 after_post_render 过滤器</span></span><br><span class="line"> <span class="keyword">return</span> ctx.<span class="title function_">execFilter</span>(<span class="string">'after_post_render'</span>, data, { <span class="attr">context</span>: ctx });</span><br><span class="line"> }).<span class="title function_">asCallback</span>(callback);</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>这个过程可以分解为七个步骤:</p>
<ol>
<li>实例化 <code>PostRenderEscape</code> 对象,专门用来「保护」和「恢复」特殊标签</li>
<li>执行一轮 <code>before_post_render</code> 过滤器,允许插件对原始 Markdown 内容进行修改</li>
<li>保护性转义
<ul>
<li><code>escapeCodeBlocks</code> 和 <code>escapeAllSwigTags</code> 方法会用正则表达式查找所有的代码块和 Nunjucks / Swig 标签(例如 <code>{% post_link %}</code> 或 <code>{{ post.title }}</code>)</li>
<li>它会将这些找出的内容从字符串中「挖」出来,存入 <code>cacheObj.stored</code> 数组,然后在原文中留下一个无害的占位符</li>
<li>这么做的理由是为了让下一步的 Markdown 渲染器知道,这一块是 Nunjucks 语法,不要把它当做普通文本并转义</li>
</ul>
</li>
<li>调用 <code>ctx.render.render()</code>。它会根据文件扩展名(<code>.md</code>)找到对应的渲染器插件,然后调用其 <code>render</code> 方法,将已经被保护起来的 Markdown 文本转换成 HTML。此时生成的 HTML 中依然包含着占位符</li>
<li>Markdown 渲染器完成工作后,<code>onRenderEnd</code> 回调函数会立刻执行,开始逆向工程
<ul>
<li><code>restoreAllSwigTags</code> 方法被调用。它会查找 HTML 中的所有占位符,并用之前存储在 <code>cacheObj.stored</code> 数组中的原始标签替换它们。现在我们得到了一个混合了 HTML 和 Nunjucks 标签的字符串</li>
<li><code>tag.render()</code> 被调用、用于处理这个混合字符串,执行其中的 <code>{& post_link &}</code> 等标签,并将其替换为最终的 HTML 片段</li>
</ul>
</li>
<li>经过 Nunjucks 渲染后,<code>restoreCodeBlocks</code> 被调用,以同样的方式将代码块占位符恢复成原始的代码块 HTML</li>
<li>所有渲染和模版执行都已完成。这最后一轮 <code>after_post_render</code> 过滤器让插件有机会对最终生成的 HTML 内容进行处理,例如添加 <code>target="_blank"</code>、图片懒加载等</li>
</ol>
<h2 id="33-render-类"><a class="markdownIt-Anchor" href="#33-render-类"></a> 3.3. <code>Render</code> 类</h2>
<p><code>ctx.render</code> 是 <code>Render</code> 类的一个实例,它扮演着渲染任务「总调度员」的角色。它不关心具体如何渲染,只负责找到正确的渲染器并把任务交给它。</p>
<figure class="highlight javascript"><figcaption><span>/hexo/render.JS</span></figcaption><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Render</span> {</span><br><span class="line"> <span class="title function_">render</span>(<span class="params">data, options, callback</span>) {</span><br><span class="line"> <span class="comment">// 参数处理和文件读取</span></span><br><span class="line"> <span class="keyword">if</span> (!callback && <span class="keyword">typeof</span> options === <span class="string">'function'</span>) {</span><br><span class="line"> callback = options;</span><br><span class="line"> options = {};</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">const</span> ctx = <span class="variable language_">this</span>.<span class="property">context</span>;</span><br><span class="line"> <span class="keyword">let</span> ext = <span class="string">''</span>;</span><br><span class="line"> <span class="keyword">let</span> promise;</span><br><span class="line"> <span class="keyword">if</span> (!data)</span><br><span class="line"> <span class="keyword">return</span> bluebird_1.<span class="property">default</span>.<span class="title function_">reject</span>(<span class="keyword">new</span> <span class="title class_">TypeError</span>(<span class="string">'No input file or string!'</span>));</span><br><span class="line"> <span class="keyword">if</span> (data.<span class="property">text</span> != <span class="literal">null</span>) {</span><br><span class="line"> promise = bluebird_1.<span class="property">default</span>.<span class="title function_">resolve</span>(data.<span class="property">text</span>);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> (!data.<span class="property">path</span>) {</span><br><span class="line"> <span class="keyword">return</span> bluebird_1.<span class="property">default</span>.<span class="title function_">reject</span>(<span class="keyword">new</span> <span class="title class_">TypeError</span>(<span class="string">'No input file or string!'</span>));</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span> {</span><br><span class="line"> promise = (<span class="number">0</span>, hexo_fs_1.<span class="property">readFile</span>)(data.<span class="property">path</span>);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> promise.<span class="title function_">then</span>(<span class="function"><span class="params">text</span> =></span> {</span><br><span class="line"> data.<span class="property">text</span> = text;</span><br><span class="line"> <span class="comment">// 1. 获取文件扩展名</span></span><br><span class="line"> ext = data.<span class="property">engine</span> || <span class="title function_">getExtname</span>(data.<span class="property">path</span>);</span><br><span class="line"> <span class="keyword">if</span> (!ext || !<span class="variable language_">this</span>.<span class="title function_">isRenderable</span>(ext))</span><br><span class="line"> <span class="keyword">return</span> text;</span><br><span class="line"> <span class="comment">// 2. 根据扩展名查找对应的渲染器</span></span><br><span class="line"> <span class="keyword">const</span> renderer = <span class="variable language_">this</span>.<span class="title function_">getRenderer</span>(ext);</span><br><span class="line"> <span class="comment">// 3. 执行渲染器</span></span><br><span class="line"> <span class="keyword">return</span> <span class="title class_">Reflect</span>.<span class="title function_">apply</span>(renderer, ctx, [data, options]);</span><br><span class="line"> }).<span class="title function_">then</span>(<span class="function"><span class="params">result</span> =></span> {</span><br><span class="line"> result = <span class="title function_">toString</span>(result, data);</span><br><span class="line"> <span class="comment">// 4. 检查并执行 onRenderEnd 回调</span></span><br><span class="line"> <span class="keyword">if</span> (data.<span class="property">onRenderEnd</span>) {</span><br><span class="line"> <span class="keyword">return</span> data.<span class="title function_">onRenderEnd</span>(result);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> result;</span><br><span class="line"> }).<span class="title function_">then</span>(<span class="function"><span class="params">result</span> =></span> {</span><br><span class="line"> <span class="comment">// 5. 执行 after_render 过滤器</span></span><br><span class="line"> <span class="keyword">const</span> output = <span class="variable language_">this</span>.<span class="title function_">getOutput</span>(ext) || ext;</span><br><span class="line"> <span class="keyword">return</span> ctx.<span class="title function_">execFilter</span>(<span class="string">`after_render:<span class="subst">${output}</span>`</span>, result, {</span><br><span class="line"> <span class="attr">context</span>: ctx,</span><br><span class="line"> <span class="attr">args</span>: [data]</span><br><span class="line"> });</span><br><span class="line"> }).<span class="title function_">asCallback</span>(callback);</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = <span class="title class_">Render</span>;</span><br><span class="line"><span class="comment">//# sourceMappingURL=render.js.map</span></span><br></pre></td></tr></tbody></table></figure>
<p>也就是说,核心渲染是由具体的渲染器插件完成的。</p>
<h4 id="331-hexo-renderer-marked"><a class="markdownIt-Anchor" href="#331-hexo-renderer-marked"></a> 3.3.1. <code>hexo-renderer-marked</code></h4>
<p><code>hexo-renderer-marked</code> 是 Hexo 默认的 Markdown 渲染器,也是 <code>Renderer</code> 类调度的完美范例。它本身不是一个简单的函数,而是一个集配置、扩展和定制于一体的小系统。</p>
<p><a target="_blank" rel="noopener" href="https://github.com/hexojs/hexo-renderer-marked"><img src="https://gh-card.dev/repos/hexojs/hexo-renderer-marked.svg" alt="hexojs/hexo-renderer-marked - GitHub"></a></p>
<figure class="highlight javascript"><figcaption><span>renderer.JS</span></figcaption><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = <span class="keyword">function</span>(<span class="params">data, options</span>) {</span><br><span class="line"> <span class="keyword">const</span> { post_asset_folder, <span class="attr">marked</span>: markedCfg, source_dir } = <span class="variable language_">this</span>.<span class="property">config</span>;</span><br><span class="line"> <span class="keyword">const</span> { prependRoot, postAsset, dompurify } = markedCfg;</span><br><span class="line"> <span class="keyword">const</span> { path, text } = data;</span><br><span class="line"></span><br><span class="line"> marked.<span class="property">defaults</span>.<span class="property">extensions</span> = <span class="literal">null</span>;</span><br><span class="line"> marked.<span class="property">defaults</span>.<span class="property">tokenizer</span> = <span class="literal">null</span>;</span><br><span class="line"> marked.<span class="property">defaults</span>.<span class="property">renderer</span> = <span class="literal">null</span>;</span><br><span class="line"> marked.<span class="property">defaults</span>.<span class="property">hooks</span> = <span class="literal">null</span>;</span><br><span class="line"> marked.<span class="property">defaults</span>.<span class="property">walkTokens</span> = <span class="literal">null</span>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// exec filter to extend marked</span></span><br><span class="line"> <span class="variable language_">this</span>.<span class="title function_">execFilterSync</span>(<span class="string">'marked:use'</span>, marked.<span class="property">use</span>, { <span class="attr">context</span>: <span class="variable language_">this</span> });</span><br><span class="line"></span><br><span class="line"> <span class="comment">// exec filter to extend renderer</span></span><br><span class="line"> <span class="variable language_">this</span>.<span class="title function_">execFilterSync</span>(<span class="string">'marked:renderer'</span>, renderer, { <span class="attr">context</span>: <span class="variable language_">this</span> });</span><br><span class="line"></span><br><span class="line"> <span class="comment">// exec filter to extend tokenizer</span></span><br><span class="line"> <span class="variable language_">this</span>.<span class="title function_">execFilterSync</span>(<span class="string">'marked:tokenizer'</span>, tokenizer, { <span class="attr">context</span>: <span class="variable language_">this</span> });</span><br><span class="line"></span><br><span class="line"> <span class="keyword">const</span> extensions = [];</span><br><span class="line"> <span class="variable language_">this</span>.<span class="title function_">execFilterSync</span>(<span class="string">'marked:extensions'</span>, extensions, { <span class="attr">context</span>: <span class="variable language_">this</span> });</span><br><span class="line"> marked.<span class="title function_">use</span>({ extensions });</span><br><span class="line"></span><br><span class="line"> <span class="keyword">let</span> postPath = <span class="string">''</span>;</span><br><span class="line"> <span class="keyword">if</span> (path && post_asset_folder && prependRoot && postAsset) {</span><br><span class="line"> <span class="keyword">const</span> <span class="title class_">Post</span> = <span class="variable language_">this</span>.<span class="title function_">model</span>(<span class="string">'Post'</span>);</span><br><span class="line"> <span class="comment">// Windows compatibility, Post.findOne() requires forward slash</span></span><br><span class="line"> <span class="keyword">const</span> source = path.<span class="title function_">substring</span>(<span class="variable language_">this</span>.<span class="property">source_dir</span>.<span class="property">length</span>).<span class="title function_">replace</span>(<span class="regexp">/\\/g</span>, <span class="string">'/'</span>);</span><br><span class="line"> <span class="keyword">const</span> post = <span class="title class_">Post</span>.<span class="title function_">findOne</span>({ source });</span><br><span class="line"> <span class="keyword">if</span> (post) {</span><br><span class="line"> <span class="keyword">const</span> { <span class="attr">source</span>: postSource } = post;</span><br><span class="line"> postPath = <span class="title function_">join</span>(source_dir, <span class="title function_">dirname</span>(postSource), <span class="title function_">basename</span>(postSource, <span class="title function_">extname</span>(postSource)));</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">let</span> sanitizer = <span class="keyword">function</span>(<span class="params">html</span>) { <span class="keyword">return</span> html; };</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (dompurify) {</span><br><span class="line"> <span class="keyword">if</span> (createDOMPurify === <span class="literal">undefined</span> && <span class="variable constant_">JSDOM</span> === <span class="literal">undefined</span>) {</span><br><span class="line"> createDOMPurify = <span class="built_in">require</span>(<span class="string">'dompurify'</span>);</span><br><span class="line"> <span class="variable constant_">JSDOM</span> = <span class="built_in">require</span>(<span class="string">'jsdom'</span>).<span class="property">JSDOM</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">const</span> <span class="variable language_">window</span> = <span class="keyword">new</span> <span class="title function_">JSDOM</span>(<span class="string">''</span>).<span class="property">window</span>;</span><br><span class="line"> <span class="keyword">const</span> <span class="title class_">DOMPurify</span> = <span class="title function_">createDOMPurify</span>(<span class="variable language_">window</span>);</span><br><span class="line"> <span class="keyword">let</span> param = {};</span><br><span class="line"> <span class="keyword">if</span> (dompurify !== <span class="literal">true</span>) {</span><br><span class="line"> param = dompurify;</span><br><span class="line"> }</span><br><span class="line"> sanitizer = <span class="keyword">function</span>(<span class="params">html</span>) { <span class="keyword">return</span> <span class="title class_">DOMPurify</span>.<span class="title function_">sanitize</span>(html, param); };</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> marked.<span class="title function_">use</span>({</span><br><span class="line"> renderer,</span><br><span class="line"> tokenizer</span><br><span class="line"> });</span><br><span class="line"> <span class="keyword">return</span> <span class="title function_">sanitizer</span>(marked.<span class="title function_">parse</span>(text, <span class="title class_">Object</span>.<span class="title function_">assign</span>({</span><br><span class="line"> <span class="comment">// headerIds was removed in marked v8.0.0, but we still need it</span></span><br><span class="line"> <span class="attr">headerIds</span>: <span class="literal">true</span></span><br><span class="line"> }, markedCfg, options, { postPath, <span class="attr">hexo</span>: <span class="variable language_">this</span>, <span class="attr">_headingId</span>: {} })));</span><br><span class="line">};</span><br></pre></td></tr></tbody></table></figure>
<blockquote>
<p>该渲染器用了 <code>marked</code> 库。</p>
<p><a target="_blank" rel="noopener" href="https://github.com/markedjs/marked"><img src="https://gh-card.dev/repos/markedjs/marked.svg" alt="markedjs/marked - GitHub"></a></p>
</blockquote>
<p>它的入口函数接收从 <code>Renderer</code> 类传来的 <code>data</code> 和 <code>options</code>,然后执行以下步骤:</p>
<ol>
<li>
<p>为了保证每次渲染的纯粹性,它首先会清空 <code>marked</code> 库的默认扩展。然后,它立刻通过 <code>this.execFilterSync</code> 触发三个专属于 <code>marked</code> 的过滤器:</p>
<ul>
<li><code>marked:use</code></li>
<li><code>marked:renderer</code></li>
<li><code>marked:tokenizer</code></li>
</ul>
<p>允许其他插件来进一步修改和扩展 <code>marked</code> 的行为,体现了 Hexo 插件系统的层次性。</p>
</li>
<li>
<p>定义一个 <code>renderer</code> 对象,重写 <code>marked</code> 库的默认渲染行为,以实现 Hexo 的特有功能:</p>
<ul>
<li><code>heading</code>:为标题标签自动生成 <code>id</code> 属性,用于页面内锚点跳转,并添加一个可点击的「链接」图标。因此我们能在 Hexo 文章标题旁看到那个小链接图标</li>
<li><code>link</code>:根据 <code>_config.yml</code> 中的 <code>external_link</code> 配置、自动为外部链接添加 <code>target="_blank"</code> 和 <code>rel="noopener"</code> 等属性</li>
<li><code>image</code>:智能处理图片路径,可以根据配置自动在图片路径前加上根目录(<code>root</code>),并处理与文章关联的资源文件夹中的图片</li>
<li><code>paragraph</code>:增加了一个小功能,可以识别特定的语法(<code>Term<br>: Definition</code>)并将其转换为定义列表(<code><dl></code>)</li>
</ul>
<figure class="highlight javascript"><figcaption><span>renderer.JS</span></figcaption><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> renderer = {</span><br><span class="line"> <span class="comment">// Add id attribute to headings</span></span><br><span class="line"> <span class="title function_">heading</span>(<span class="params">{ tokens, depth: level }</span>) {</span><br><span class="line"> <span class="keyword">let</span> text = <span class="variable language_">this</span>.<span class="property">parser</span>.<span class="title function_">parseInline</span>(tokens);</span><br><span class="line"> <span class="keyword">const</span> { anchorAlias, headerIds, modifyAnchors, _headingId } = <span class="variable language_">this</span>.<span class="property">options</span>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (!headerIds) {</span><br><span class="line"> <span class="keyword">return</span> <span class="string">`<h<span class="subst">${level}</span>><span class="subst">${text}</span></h<span class="subst">${level}</span>>`</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">const</span> transformOption = modifyAnchors;</span><br><span class="line"> <span class="keyword">let</span> id = <span class="title function_">anchorId</span>(text, transformOption);</span><br><span class="line"> <span class="keyword">const</span> headingId = _headingId;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">const</span> anchorAliasOpt = anchorAlias && text.<span class="title function_">startsWith</span>(<span class="string">'<a href="#'</span>);</span><br><span class="line"> <span class="keyword">if</span> (anchorAliasOpt) {</span><br><span class="line"> <span class="keyword">const</span> customAnchor = text.<span class="title function_">match</span>(rATag)[<span class="number">1</span>];</span><br><span class="line"> id = <span class="title function_">anchorId</span>(customAnchor, transformOption);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Add a number after id if repeated</span></span><br><span class="line"> <span class="keyword">if</span> (headingId[id]) {</span><br><span class="line"> id += <span class="string">`-<span class="subst">${headingId[id]++}</span>`</span>;</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> headingId[id] = <span class="number">1</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (anchorAliasOpt) {</span><br><span class="line"> text = text.<span class="title function_">replace</span>(rATag, <span class="function">(<span class="params">str, alias</span>) =></span> {</span><br><span class="line"> <span class="keyword">return</span> str.<span class="title function_">replace</span>(alias, id);</span><br><span class="line"> });</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// add headerlink</span></span><br><span class="line"> <span class="keyword">return</span> <span class="string">`<h<span class="subst">${level}</span> id="<span class="subst">${id}</span>"><a href="#<span class="subst">${id}</span>" class="headerlink" title="<span class="subst">${stripHTML(text)}</span>"></a><span class="subst">${text}</span></h<span class="subst">${level}</span>>`</span>;</span><br><span class="line"> },</span><br><span class="line"></span><br><span class="line"> <span class="title function_">link</span>(<span class="params">{ tokens, href, title }</span>) {</span><br><span class="line"> <span class="keyword">const</span> text = <span class="variable language_">this</span>.<span class="property">parser</span>.<span class="title function_">parseInline</span>(tokens);</span><br><span class="line"> <span class="keyword">const</span> { external_link, sanitizeUrl, hexo, mangle } = <span class="variable language_">this</span>.<span class="property">options</span>;</span><br><span class="line"> <span class="keyword">const</span> { <span class="attr">url</span>: urlCfg } = hexo.<span class="property">config</span>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (sanitizeUrl) {</span><br><span class="line"> <span class="keyword">if</span> (href.<span class="title function_">startsWith</span>(<span class="string">'javascript:'</span>) || href.<span class="title function_">startsWith</span>(<span class="string">'vbscript:'</span>) || href.<span class="title function_">startsWith</span>(<span class="string">'data:'</span>)) {</span><br><span class="line"> href = <span class="string">''</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (mangle) {</span><br><span class="line"> <span class="keyword">if</span> (href.<span class="title function_">startsWith</span>(<span class="string">'mailto:'</span>)) {</span><br><span class="line"> <span class="keyword">const</span> email = href.<span class="title function_">substring</span>(<span class="number">7</span>);</span><br><span class="line"> <span class="keyword">const</span> mangledEmail = <span class="title function_">mangleEmail</span>(email);</span><br><span class="line"></span><br><span class="line"> href = <span class="string">`mailto:<span class="subst">${mangledEmail}</span>`</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">let</span> out = <span class="string">'<a href="'</span>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> out += <span class="title function_">encodeURL</span>(href);</span><br><span class="line"> } <span class="keyword">catch</span> (e) {</span><br><span class="line"> out += href;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> out += <span class="string">'"'</span>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (title) {</span><br><span class="line"> out += <span class="string">` title="<span class="subst">${<span class="built_in">escape</span>(title)}</span>"`</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (external_link) {</span><br><span class="line"> <span class="keyword">const</span> target = <span class="string">' target="_blank"'</span>;</span><br><span class="line"> <span class="keyword">const</span> noopener = <span class="string">' rel="noopener"'</span>;</span><br><span class="line"> <span class="keyword">const</span> nofollowTag = <span class="string">' rel="noopener external nofollow noreferrer"'</span>;</span><br><span class="line"> <span class="keyword">if</span> (<span class="title function_">isExternalLink</span>(href, urlCfg, external_link.<span class="property">exclude</span>)) {</span><br><span class="line"> <span class="keyword">if</span> (external_link.<span class="property">enable</span> && external_link.<span class="property">nofollow</span>) {</span><br><span class="line"> out += target + nofollowTag;</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (external_link.<span class="property">enable</span>) {</span><br><span class="line"> out += target + noopener;</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (external_link.<span class="property">nofollow</span>) {</span><br><span class="line"> out += nofollowTag;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> out += <span class="string">`><span class="subst">${text}</span></a>`</span>;</span><br><span class="line"> <span class="keyword">return</span> out;</span><br><span class="line"> },</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Support Basic Description Lists</span></span><br><span class="line"> <span class="title function_">paragraph</span>(<span class="params">{ tokens }</span>) {</span><br><span class="line"> <span class="keyword">const</span> text = <span class="variable language_">this</span>.<span class="property">parser</span>.<span class="title function_">parseInline</span>(tokens);</span><br><span class="line"> <span class="keyword">const</span> { descriptionLists = <span class="literal">true</span> } = <span class="variable language_">this</span>.<span class="property">options</span>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (descriptionLists && text.<span class="title function_">includes</span>(<span class="string">'<br>:'</span>)) {</span><br><span class="line"> <span class="keyword">if</span> (rDlSyntax.<span class="title function_">test</span>(text)) {</span><br><span class="line"> <span class="keyword">return</span> text.<span class="title function_">replace</span>(rDlSyntax, <span class="string">'<dl><dt>$1</dt><dd>$2</dd></dl>'</span>);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> <span class="string">`<p><span class="subst">${text}</span></p>\n`</span>;</span><br><span class="line"> },</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Prepend root to image path</span></span><br><span class="line"> <span class="title function_">image</span>(<span class="params">{ href, title, text }</span>) {</span><br><span class="line"> <span class="keyword">const</span> { options } = <span class="variable language_">this</span>;</span><br><span class="line"> <span class="keyword">const</span> { hexo } = options;</span><br><span class="line"> <span class="keyword">const</span> { relative_link } = hexo.<span class="property">config</span>;</span><br><span class="line"> <span class="keyword">const</span> { lazyload, figcaption, prependRoot, postPath } = options;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (!<span class="regexp">/^(#|\/\/|http(s)?:)/</span>.<span class="title function_">test</span>(href) && !relative_link && prependRoot) {</span><br><span class="line"> <span class="keyword">if</span> (!href.<span class="title function_">startsWith</span>(<span class="string">'/'</span>) && !href.<span class="title function_">startsWith</span>(<span class="string">'\\'</span>) && postPath) {</span><br><span class="line"> <span class="keyword">const</span> <span class="title class_">PostAsset</span> = hexo.<span class="title function_">model</span>(<span class="string">'PostAsset'</span>);</span><br><span class="line"> <span class="comment">// findById requires forward slash</span></span><br><span class="line"> <span class="keyword">const</span> asset = <span class="title class_">PostAsset</span>.<span class="title function_">findById</span>(<span class="title function_">join</span>(postPath, href.<span class="title function_">replace</span>(<span class="regexp">/\\/g</span>, <span class="string">'/'</span>)));</span><br><span class="line"> <span class="comment">// asset.path is backward slash in Windows</span></span><br><span class="line"> <span class="keyword">if</span> (asset) href = asset.<span class="property">path</span>.<span class="title function_">replace</span>(<span class="regexp">/\\/g</span>, <span class="string">'/'</span>);</span><br><span class="line"> }</span><br><span class="line"> href = url_for.<span class="title function_">call</span>(hexo, href);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">let</span> out = <span class="string">`<img src="<span class="subst">${encodeURL(href)}</span>"`</span>;</span><br><span class="line"> <span class="keyword">if</span> (text) out += <span class="string">` alt="<span class="subst">${<span class="built_in">escape</span>(text)}</span>"`</span>;</span><br><span class="line"> <span class="keyword">if</span> (title) out += <span class="string">` title="<span class="subst">${<span class="built_in">escape</span>(title)}</span>"`</span>;</span><br><span class="line"> <span class="keyword">if</span> (lazyload) out += <span class="string">' loading="lazy"'</span>;</span><br><span class="line"></span><br><span class="line"> out += <span class="string">'>'</span>;</span><br><span class="line"> <span class="keyword">if</span> (figcaption && text) {</span><br><span class="line"> <span class="keyword">return</span> <span class="string">`<figure><span class="subst">${out}</span><figcaption aria-hidden="true"><span class="subst">${text}</span></figcaption></figure>`</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> out;</span><br><span class="line"> }</span><br><span class="line">};</span><br></pre></td></tr></tbody></table></figure>
</li>
<li>
<p>定制 <code>tokenizer</code>,用于影响 <code>marked</code> 如何解析原始的 Markdown 文本</p>
<figure class="highlight javascript"><figcaption><span>renderer.JS</span></figcaption><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> tokenizer = {</span><br><span class="line"> <span class="comment">// Support autolink option</span></span><br><span class="line"> <span class="title function_">url</span>(<span class="params">src</span>) {</span><br><span class="line"> <span class="keyword">const</span> { autolink } = <span class="variable language_">this</span>.<span class="property">options</span>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (!autolink) <span class="keyword">return</span>;</span><br><span class="line"> <span class="comment">// return false to use original url tokenizer</span></span><br><span class="line"> <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"> },</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Override smartypants</span></span><br><span class="line"> <span class="title function_">inlineText</span>(<span class="params">src</span>) {</span><br><span class="line"> <span class="keyword">const</span> { options, rules } = <span class="variable language_">this</span>;</span><br><span class="line"> <span class="keyword">const</span> { quotes, <span class="attr">smartypants</span>: isSmarty } = options;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// https://github.com/markedjs/marked/blob/b6773fca412c339e0cedd56b63f9fa1583cfd372/src/Tokenizer.js#L643-L658</span></span><br><span class="line"> <span class="keyword">const</span> cap = rules.<span class="property">inline</span>.<span class="property">text</span>.<span class="title function_">exec</span>(src);</span><br><span class="line"> <span class="keyword">if</span> (cap) {</span><br><span class="line"> <span class="keyword">let</span> text;</span><br><span class="line"> <span class="keyword">if</span> (<span class="variable language_">this</span>.<span class="property">lexer</span>.<span class="property">state</span>.<span class="property">inRawBlock</span> || <span class="variable language_">this</span>.<span class="property">rules</span>.<span class="property">inline</span>.<span class="property">url</span>.<span class="title function_">exec</span>(src)) {</span><br><span class="line"> text = cap[<span class="number">0</span>];</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> text = <span class="built_in">escape</span>(isSmarty ? <span class="title function_">smartypants</span>(cap[<span class="number">0</span>], quotes) : cap[<span class="number">0</span>]);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> {</span><br><span class="line"> <span class="attr">type</span>: <span class="string">'text'</span>,</span><br><span class="line"> <span class="attr">raw</span>: cap[<span class="number">0</span>],</span><br><span class="line"> text</span><br><span class="line"> };</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">};</span><br></pre></td></tr></tbody></table></figure>
<ul>
<li><code>inlineText</code> 主要是实现了 <code>smartypants</code> 功能,可以将普通的直引号自动转换成更美观的弯引号,以及将多个连字符转换成破折号</li>
</ul>
<figure class="highlight javascript"><figcaption><span>renderer.JS</span></figcaption><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title function_">smartypants</span> = (<span class="params">str, quotes</span>) => {</span><br><span class="line"> <span class="keyword">const</span> [openDbl, closeDbl, openSgl, closeSgl] = <span class="keyword">typeof</span> quotes === <span class="string">'string'</span> && quotes.<span class="property">length</span> === <span class="number">4</span></span><br><span class="line"> ? quotes</span><br><span class="line"> : [<span class="string">'\u201c'</span>, <span class="string">'\u201d'</span>, <span class="string">'\u2018'</span>, <span class="string">'\u2019'</span>];</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> str</span><br><span class="line"> <span class="comment">// em-dashes</span></span><br><span class="line"> .<span class="title function_">replace</span>(<span class="regexp">/---/g</span>, <span class="string">'\u2014'</span>)</span><br><span class="line"> <span class="comment">// en-dashes</span></span><br><span class="line"> .<span class="title function_">replace</span>(<span class="regexp">/--/g</span>, <span class="string">'\u2013'</span>)</span><br><span class="line"> <span class="comment">// opening singles</span></span><br><span class="line"> .<span class="title function_">replace</span>(<span class="regexp">/(^|[-\u2014/([{"\s])'/g</span>, <span class="string">'$1'</span> + openSgl)</span><br><span class="line"> <span class="comment">// closing singles & apostrophes</span></span><br><span class="line"> .<span class="title function_">replace</span>(<span class="regexp">/'/g</span>, closeSgl)</span><br><span class="line"> <span class="comment">// opening doubles</span></span><br><span class="line"> .<span class="title function_">replace</span>(<span class="regexp">/(^|[-\u2014/([{\u2018\s])"/g</span>, <span class="string">'$1'</span> + openDbl)</span><br><span class="line"> <span class="comment">// closing doubles</span></span><br><span class="line"> .<span class="title function_">replace</span>(<span class="regexp">/"/g</span>, closeDbl)</span><br><span class="line"> <span class="comment">// ellipses</span></span><br><span class="line"> .<span class="title function_">replace</span>(<span class="regexp">/\.{3}/g</span>, <span class="string">'\u2026'</span>);</span><br><span class="line">};</span><br></pre></td></tr></tbody></table></figure>
</li>
<li>
<p>在准备好所有定制的 <code>renderer</code> 和 <code>tokenizer</code> 之后,插件通过 <code>marked.use()</code> 将它们应用到 <code>marked</code> 实例上。最后调用 <code>marked.parse()</code>,传入原始文本和所有配置,得到最终的 HTML</p>
<ul>
<li>如果用户开启了 <code>dompurify</code> 选项,它还会在返回结果前对 HTML 进行一次安全过滤,防止恶意的脚本注入</li>
</ul>
</li>
</ol>
<h1 id="4-页面的生成"><a class="markdownIt-Anchor" href="#4-页面的生成"></a> 4. 页面的生成</h1>
<p>还记得第二章最开始出现的那段代码吗?</p>
<figure class="highlight javascript"><figcaption><span>/hexo/index.JS</span></figcaption><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="title function_">load</span>(<span class="params">callback</span>) {</span><br><span class="line"> <span class="keyword">return</span> (<span class="number">0</span>, load_database_1.<span class="property">default</span>)(<span class="variable language_">this</span>).<span class="title function_">then</span>(<span class="function">() =></span> {</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">log</span>.<span class="title function_">info</span>(<span class="string">'Start processing'</span>);</span><br><span class="line"> <span class="keyword">return</span> bluebird_1.<span class="property">default</span>.<span class="title function_">all</span>([</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">source</span>.<span class="title function_">process</span>(),</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">theme</span>.<span class="title function_">process</span>()</span><br><span class="line"> ]);</span><br><span class="line"> }).<span class="title function_">then</span>(<span class="function">() =></span> {</span><br><span class="line"> <span class="title function_">mergeCtxThemeConfig</span>(<span class="variable language_">this</span>);</span><br><span class="line"> <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="title function_">_generate</span>({ <span class="attr">cache</span>: <span class="literal">false</span> });</span><br><span class="line"> }).<span class="title function_">asCallback</span>(callback);</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p><code>hexo.load()</code> 方法成功地将所有 <code>source</code> 目录下的源文件处理完毕,并将结构化的数据存入了内存数据库(<code>this.database</code>)。也就是说,我们拥有了所有文章、独立页面、分类和标签的原子数据。但是,一个完整的网站还包含许多由这些原子数据聚合而成的页面,例如:首页的文章列表、归档页、分类列表页等。这些页面在 <code>source</code> 目录中并没有对应的源文件。</p>
<p>那么这些「无源之页」是如何被创建的呢?答案就在 <code>Generator</code> 扩展中。上一章节讲到了 <code>this.source.process()</code>,本章节则重点探讨之后的 <code>_generate()</code> 方法,看 Hexo 是如何调用所有 <code>Generator</code> 插件,并生产出网站所有页面的「蓝图」。</p>
<blockquote>
<p>眼尖的朋友应该发现了,我跳过了 <code>this.theme.process()</code>。搭建过个人博客的朋友都知道主题这一大山有多么重要,而这段代码就是调用了主题的处理方法。不过本文并不注重于主题,而是注重于「Markdown 文件如何变成网页」这条主线。之后的文章就会讲一讲主题(挖坑)<s>(说实话 Hexo 开发主题也是一堆坑)</s>。</p>
</blockquote>
<h2 id="41-_rungenerators-方法解析"><a class="markdownIt-Anchor" href="#41-_rungenerators-方法解析"></a> 4.1. <code>_runGenerators</code> 方法解析</h2>
<p><code>Generator</code> 的调用发生在 <code>_generate</code> 方法内部。<code>_generate</code> 会调用 <code>_runGenerators</code>,并将该方法的返回值 —— 一个包含了所有页面蓝图的数组 —— 传递给下一阶段的 <code>_routerRefresh</code>。</p>
<figure class="highlight javascript"><figcaption><span>/hexo/index.JS</span></figcaption><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="title function_">_generate</span>(<span class="params">options = {}</span>) {</span><br><span class="line"> <span class="keyword">if</span> (<span class="variable language_">this</span>.<span class="property">_isGenerating</span>)</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> <span class="keyword">const</span> useCache = options.<span class="property">cache</span>;</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">_isGenerating</span> = <span class="literal">true</span>;</span><br><span class="line"> <span class="variable language_">this</span>.<span class="title function_">emit</span>(<span class="string">'generateBefore'</span>);</span><br><span class="line"> <span class="comment">// Run before_generate filters</span></span><br><span class="line"> <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="title function_">execFilter</span>(<span class="string">'before_generate'</span>, <span class="literal">null</span>, { <span class="attr">context</span>: <span class="variable language_">this</span> })</span><br><span class="line"> .<span class="title function_">then</span>(<span class="function">() =></span> <span class="variable language_">this</span>.<span class="title function_">_routerRefresh</span>(<span class="variable language_">this</span>.<span class="title function_">_runGenerators</span>(), useCache)).<span class="title function_">then</span>(<span class="function">() =></span> {</span><br><span class="line"> <span class="variable language_">this</span>.<span class="title function_">emit</span>(<span class="string">'generateAfter'</span>);</span><br><span class="line"> <span class="comment">// Run after_generate filters</span></span><br><span class="line"> <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="title function_">execFilter</span>(<span class="string">'after_generate'</span>, <span class="literal">null</span>, { <span class="attr">context</span>: <span class="variable language_">this</span> });</span><br><span class="line"> }).<span class="title function_">finally</span>(<span class="function">() =></span> {</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">_isGenerating</span> = <span class="literal">false</span>;</span><br><span class="line"> });</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p><code>_runGenerators</code> 的职责非常专一:执行所有已注册的 <code>Generator</code> 插件,并收集它们的返回结果。</p>
<figure class="highlight javascript"><figcaption><span>/hexo/index.JS</span></figcaption><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="title function_">_runGenerators</span>(<span class="params"></span>) {</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">locals</span>.<span class="title function_">invalidate</span>();</span><br><span class="line"> <span class="comment">// 1. 准备 Generator 的输入数据(site 对象)</span></span><br><span class="line"> <span class="keyword">const</span> siteLocals = <span class="variable language_">this</span>.<span class="property">locals</span>.<span class="title function_">toObject</span>();</span><br><span class="line"> <span class="comment">// 2. 获取所有已注册的 Generator 函数列表</span></span><br><span class="line"> <span class="keyword">const</span> generators = <span class="variable language_">this</span>.<span class="property">extend</span>.<span class="property">generator</span>.<span class="title function_">list</span>();</span><br><span class="line"> <span class="keyword">const</span> { log } = <span class="variable language_">this</span>;</span><br><span class="line"> <span class="comment">// 3. 遍历并执行所有的 Generator</span></span><br><span class="line"> <span class="keyword">return</span> bluebird_1.<span class="property">default</span>.<span class="title function_">map</span>(<span class="title class_">Object</span>.<span class="title function_">keys</span>(generators), <span class="function"><span class="params">key</span> =></span> {</span><br><span class="line"> <span class="keyword">const</span> generator = generators[key];</span><br><span class="line"> log.<span class="title function_">debug</span>(<span class="string">'Generator: %s'</span>, (<span class="number">0</span>, picocolors_1.<span class="property">magenta</span>)(key));</span><br><span class="line"> <span class="comment">// 4. 执行单个 Generator 函数</span></span><br><span class="line"> <span class="keyword">return</span> <span class="title class_">Reflect</span>.<span class="title function_">apply</span>(generator, <span class="variable language_">this</span>, [siteLocals]);</span><br><span class="line"> }).<span class="title function_">reduce</span>(<span class="function">(<span class="params">result, data</span>) =></span> {</span><br><span class="line"> <span class="comment">// 5. 将所有返回结果合并成一个数组</span></span><br><span class="line"> <span class="keyword">return</span> data ? result.<span class="title function_">concat</span>(data) : result;</span><br><span class="line"> }, []);</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>这里的 <code>this.locals</code> 是 <code>Hexo</code> 实例的一个属性。<a href="/posts/5d57#%E6%9C%AC%E5%9C%B0%E6%95%B0%E6%8D%AE%E7%BB%91%E5%AE%9A">第一篇文章</a> 中,它被我们分析过的 <code>_bindLocals</code> 方法初始化。<code>_bindLocals</code> 将数据库查询函数与 <code>posts</code>, <code>pages</code>, <code>tags</code>, <code>categories</code> 等键名绑定。</p>
<p>而 <code>this.locals.toObject()</code> 方法的作用,就是执行所有这些绑定的函数,从数据库中查询出最新的数据,并组装成一个巨大的 <code>site</code> 对象。这个对象包含了:</p>
<ul>
<li><code>site.posts</code>:一个 <code>Warehouse</code> 模型集合,包含了所有文章</li>
<li><code>site.pages</code>:包含了所有独立页面</li>
<li><code>site.categories</code> 和 <code>site.tags</code>:分别包含了所有分类和标签</li>
<li><code>site.data</code>:包含了 <code>source/_data</code> 目录下的所有数据</li>
</ul>
<p>这个 <code>site</code> 对象,就是所有 <code>Generator</code> 赖以生存的「食材库」。</p>
<blockquote>
<p>看过 <a target="_blank" rel="noopener" href="https://hexo.io/zh-cn/api/locals">Hexo 官方文档</a> 的朋友或许会回忆起,<code>site</code> 对象实际上就是用于模板渲染的局部变量。</p>
</blockquote>
<h2 id="42-页面蓝图-path-layout-data"><a class="markdownIt-Anchor" href="#42-页面蓝图-path-layout-data"></a> 4.2. 页面蓝图 <code>{ path, layout, data }</code></h2>
<p><code>Generator</code> 的核心产出是一种标准化的数据结构,我们可以称之为「页面蓝图」。它不是最终的 HTML,而是描述了如何生成一个页面的指令。</p>
<p>每个蓝图对象通常包含三个核心属性:</p>
<ul>
<li><code>path</code>: <code>String</code> - 页面最终生成的文件路径,例如 <code>archives/index.html</code></li>
<li><code>layout</code>: <code>String | Array</code> - 渲染这个页面所使用的主题布局文件。例如,<code>'archive'</code> 会对应到主题的 <code>layout/archive.ejs</code> 文件。可以是一个数组,Hexo 会依次查找并使用第一个找到的布局</li>
<li><code>data</code>: <code>Object</code> - 渲染布局时需要的数据</li>
</ul>
<blockquote>
<p>示例:</p>
<ul>
<li>
<p>一篇文章的蓝图:</p>
<figure class="highlight json"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">{</span></span><br><span class="line"> <span class="attr">"path"</span><span class="punctuation">:</span> <span class="string">"2025/07/23/hello-world/index.html"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"layout"</span><span class="punctuation">:</span> <span class="string">"post"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"data"</span><span class="punctuation">:</span> <span class="punctuation">{</span> <span class="attr">"post"</span><span class="punctuation">:</span> <span class="punctuation">{</span> ...文章数据 <span class="punctuation">}</span> <span class="punctuation">}</span></span><br><span class="line"><span class="punctuation">}</span></span><br></pre></td></tr></tbody></table></figure>
</li>
<li>
<p>归档页的蓝图:</p>
<figure class="highlight json"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">{</span></span><br><span class="line"> <span class="attr">"path"</span><span class="punctuation">:</span> <span class="string">"archives/index.html"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"layout"</span><span class="punctuation">:</span> <span class="string">"archive"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"data"</span><span class="punctuation">:</span> <span class="punctuation">{</span> <span class="attr">"posts"</span><span class="punctuation">:</span> site.posts<span class="punctuation">,</span> <span class="attr">"page"</span><span class="punctuation">:</span> <span class="punctuation">{</span> <span class="attr">"current"</span><span class="punctuation">:</span> <span class="number">1</span><span class="punctuation">,</span> ... <span class="punctuation">}</span> <span class="punctuation">}</span></span><br><span class="line"><span class="punctuation">}</span></span><br></pre></td></tr></tbody></table></figure>
</li>
</ul>
</blockquote>
<h2 id="43-generator-的来源"><a class="markdownIt-Anchor" href="#43-generator-的来源"></a> 4.3. <code>Generator</code> 的来源</h2>
<p>Hexo 默认注册了几个核心的 <code>Generator</code> 来生成网站的基础部分:</p>
<ul>
<li><code>post</code> & <code>page</code>:这两个 <code>Generator</code> 负责处理「有源文件」的页面。它们遍历 <code>site.posts</code> 和 <code>site.pages</code> 集合,为每篇文章和每个独立页面生成对应的页面蓝图</li>
<li><code>archive</code>, <code>category</code>, <code>tag</code>:这三个 <code>Generator</code> 负责处理「无源之页」。它们分别遍历 <code>site.posts</code>, <code>site.categories</code>, <code>site.tags</code> 集合,生成归档页、所有分类页和所有标签页的蓝图</li>
</ul>
<p>我们可以深入一个具体的 <code>Generator</code>,来了解它是如何利用 <code>site</code> 对象创建页面蓝图的。我们以 <code>archive</code> 生成器为例。</p>
<p>不过在分析之前,有一个关键点需要明确:并非所有核心的生成功能都内置在 Hexo 的主代码库中。Hexo 遵循「保持核心精简」的设计哲学,许多基础功能都是通过 <strong>默认插件</strong> 提供的。</p>
<p>当我们 <code>hexo init</code> 一个新项目时,<code>package.json</code> 会自动包含 <code>hexo-generator-archive</code>, <code>hexo-generator-category</code>, <code>hexo-generator-tag</code> 等插件。</p>
<p>这意味着:</p>
<ul>
<li><code>post</code> & <code>page</code> 生成器位于 Hexo 核心代码库(<code>/plugins/generator/</code>)中,负责处理有源文件的页面</li>
<li><code>archive</code>、<code>category</code>、<code>tag</code> 等生成器,虽然是默认安装,但它们是独立的 npm 包。它们负责处理无源页面</li>
</ul>
<p><code>archive</code> 生成器的核心代码可以分解为三个主要步骤:</p>
<ol>
<li>
<p>生成主归档页</p>
<p><code>Generator</code> 首先会处理根归档页,也就是 <code>/archives/</code> 目录。</p>
<figure class="highlight javascript"><figcaption><span>hexo-generator-archive/lib/generator.JS</span></figcaption><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">'use strict'</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> pagination = <span class="built_in">require</span>(<span class="string">'hexo-pagination'</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="title function_">fmtNum</span> = num => num.<span class="title function_">toString</span>().<span class="title function_">padStart</span>(<span class="number">2</span>, <span class="string">'0'</span>);</span><br><span class="line"></span><br><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = <span class="keyword">function</span>(<span class="params">locals</span>) {</span><br><span class="line"> <span class="keyword">const</span> { config } = <span class="variable language_">this</span>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">let</span> archiveDir = config.<span class="property">archive_dir</span>;</span><br><span class="line"> <span class="keyword">const</span> paginationDir = config.<span class="property">pagination_dir</span> || <span class="string">'page'</span>;</span><br><span class="line"> <span class="comment">// 1. 获取所有文章,并根据配置排序</span></span><br><span class="line"> <span class="keyword">const</span> allPosts = locals.<span class="property">posts</span>.<span class="title function_">sort</span>(config.<span class="property">archive_generator</span>.<span class="property">order_by</span> || <span class="string">'-date'</span>);</span><br><span class="line"> <span class="keyword">const</span> perPage = config.<span class="property">archive_generator</span>.<span class="property">per_page</span>;</span><br><span class="line"> <span class="keyword">const</span> result = [];</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (!allPosts.<span class="property">length</span>) <span class="keyword">return</span>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (archiveDir[archiveDir.<span class="property">length</span> - <span class="number">1</span>] !== <span class="string">'/'</span>) archiveDir += <span class="string">'/'</span>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 2. 定义一个通用的页面蓝图生成函数 </span></span><br><span class="line"> <span class="keyword">function</span> <span class="title function_">generate</span>(<span class="params">path, posts, options = {}</span>) {</span><br><span class="line"> options.<span class="property">archive</span> = <span class="literal">true</span>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 3. 调用 hexo-pagination 工具生成分页数据</span></span><br><span class="line"> result.<span class="title function_">push</span>(...<span class="title function_">pagination</span>(path, posts, {</span><br><span class="line"> perPage,</span><br><span class="line"> <span class="attr">layout</span>: [<span class="string">'archive'</span>, <span class="string">'index'</span>], <span class="comment">// 指定布局</span></span><br><span class="line"> <span class="attr">format</span>: paginationDir + <span class="string">'/%d/'</span>, <span class="comment">// 分页路径格式</span></span><br><span class="line"> <span class="attr">data</span>: options</span><br><span class="line"> }));</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 4. 为根归档页调用 generate 函数</span></span><br><span class="line"> <span class="title function_">generate</span>(archiveDir, allPosts);</span><br></pre></td></tr></tbody></table></figure>
<p><a target="_blank" rel="noopener" href="https://github.com/hexojs/hexo-pagination"><img src="https://gh-card.dev/repos/hexojs/hexo-pagination.svg" alt="hexojs/hexo-pagination - GitHub"></a></p>
</li>
<li>
<p>按日期对文章进行分组</p>
<p>如果用户在 <code>_config.yml</code> 中开启了 <code>yearly</code> 或 <code>monthly</code> 归档,<code>Generator</code> 接下来会执行一个精巧的数据预处理步骤:将所有文章按年、月、日进行分组。</p>
<figure class="highlight javascript"><figcaption><span>hexo-generator-archive/lib/generator.JS</span></figcaption><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (!config.<span class="property">archive_generator</span>.<span class="property">yearly</span>) <span class="keyword">return</span> result; <span class="comment">// 如果没开启,直接返回</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> posts = {};</span><br><span class="line"></span><br><span class="line"><span class="comment">// 遍历所有文章</span></span><br><span class="line">allPosts.<span class="title function_">forEach</span>(<span class="function"><span class="params">post</span> =></span> {</span><br><span class="line"> <span class="keyword">const</span> date = post.<span class="property">date</span>;</span><br><span class="line"> <span class="keyword">const</span> year = date.<span class="title function_">year</span>();</span><br><span class="line"> <span class="keyword">const</span> month = date.<span class="title function_">month</span>() + <span class="number">1</span>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 创建一个嵌套结构来存储文章</span></span><br><span class="line"> <span class="keyword">if</span> (!<span class="title class_">Object</span>.<span class="property"><span class="keyword">prototype</span></span>.<span class="property">hasOwnProperty</span>.<span class="title function_">call</span>(posts, year)) {</span><br><span class="line"> posts[year] = [</span><br><span class="line"> [],</span><br><span class="line"> [],</span><br><span class="line"> [],</span><br><span class="line"> [],</span><br><span class="line"> [],</span><br><span class="line"> [],</span><br><span class="line"> [],</span><br><span class="line"> [],</span><br><span class="line"> [],</span><br><span class="line"> [],</span><br><span class="line"> [],</span><br><span class="line"> [],</span><br><span class="line"> []</span><br><span class="line"> ];</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> posts[year][<span class="number">0</span>].<span class="title function_">push</span>(post); <span class="comment">// 按年分组</span></span><br><span class="line"> posts[year][month].<span class="title function_">push</span>(post); <span class="comment">// 按月分组</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (config.<span class="property">archive_generator</span>.<span class="property">daily</span>) {</span><br><span class="line"> <span class="keyword">const</span> day = date.<span class="title function_">date</span>(); <span class="comment">// 按日分组</span></span><br><span class="line"> <span class="keyword">if</span> (!<span class="title class_">Object</span>.<span class="property"><span class="keyword">prototype</span></span>.<span class="property">hasOwnProperty</span>.<span class="title function_">call</span>(posts[year][month], <span class="string">'day'</span>)) {</span><br><span class="line"> posts[year][month].<span class="property">day</span> = {};</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> (posts[year][month].<span class="property">day</span>[day] || (posts[year][month].<span class="property">day</span>[day] = [])).<span class="title function_">push</span>(post);</span><br><span class="line"> }</span><br><span class="line">});</span><br></pre></td></tr></tbody></table></figure>
<p>这段代码执行完毕后,会得到一个类似 <code>posts[2025][7]</code> 的数据结构,其中存储了 2025 年 7 月的所有文章。</p>
</li>
<li>
<p>生成年、月、日归档页</p>
<p>最后,代码会遍历上一步中分组好的 <code>posts</code> 对象,为每个时间单位生成对应的归档页面。</p>
</li>
</ol>
<figure class="highlight javascript"><figcaption><span>hexo-generator-archive/lib/generator.JS</span></figcaption><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br></pre></td><td class="code"><pre><span class="line"> <span class="keyword">const</span> { <span class="title class_">Query</span> } = <span class="variable language_">this</span>.<span class="title function_">model</span>(<span class="string">'Post'</span>);</span><br><span class="line"> <span class="keyword">const</span> years = <span class="title class_">Object</span>.<span class="title function_">keys</span>(posts);</span><br><span class="line"> <span class="keyword">let</span> year, data, month, monthData, url;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 遍历年份</span></span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">let</span> i = <span class="number">0</span>, len = years.<span class="property">length</span>; i < len; i++) {</span><br><span class="line"> year = +years[i];</span><br><span class="line"> data = posts[year];</span><br><span class="line"> url = archiveDir + year + <span class="string">'/'</span>;</span><br><span class="line"> <span class="keyword">if</span> (!data[<span class="number">0</span>].<span class="property">length</span>) <span class="keyword">continue</span>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 为该年份生成归档页,例如 /archives/2025/</span></span><br><span class="line"> <span class="title function_">generate</span>(url, <span class="keyword">new</span> <span class="title class_">Query</span>(data[<span class="number">0</span>]), { year });</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (!config.<span class="property">archive_generator</span>.<span class="property">monthly</span> && !config.<span class="property">archive_generator</span>.<span class="property">daily</span>) <span class="keyword">continue</span>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 遍历月份</span></span><br><span class="line"> <span class="keyword">for</span> (month = <span class="number">1</span>; month <= <span class="number">12</span>; month++) {</span><br><span class="line"> monthData = data[month];</span><br><span class="line"> <span class="keyword">if</span> (!monthData.<span class="property">length</span>) <span class="keyword">continue</span>;</span><br><span class="line"> <span class="keyword">if</span> (config.<span class="property">archive_generator</span>.<span class="property">monthly</span>) {</span><br><span class="line"> <span class="comment">// 为该月份生成归档页,例如 /archives/2025/07/</span></span><br><span class="line"> <span class="title function_">generate</span>(url + <span class="title function_">fmtNum</span>(month) + <span class="string">'/'</span>, <span class="keyword">new</span> <span class="title class_">Query</span>(monthData), {</span><br><span class="line"> year,</span><br><span class="line"> month</span><br><span class="line"> });</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (!config.<span class="property">archive_generator</span>.<span class="property">daily</span>) <span class="keyword">continue</span>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 同样逻辑处理按日归档</span></span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">let</span> day = <span class="number">1</span>; day <= <span class="number">31</span>; day++) {</span><br><span class="line"> <span class="keyword">const</span> dayData = monthData.<span class="property">day</span>[day];</span><br><span class="line"> <span class="keyword">if</span> (!dayData || !dayData.<span class="property">length</span>) <span class="keyword">continue</span>;</span><br><span class="line"> <span class="title function_">generate</span>(url + <span class="title function_">fmtNum</span>(month) + <span class="string">'/'</span> + <span class="title function_">fmtNum</span>(day) + <span class="string">'/'</span>, <span class="keyword">new</span> <span class="title class_">Query</span>(dayData), {</span><br><span class="line"> year,</span><br><span class="line"> month,</span><br><span class="line"> day</span><br><span class="line"> });</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> result;</span><br><span class="line">};</span><br></pre></td></tr></tbody></table></figure>
<p>通过嵌套循环,<code>Generator</code> 复用了第一步中定义的 <code>generate</code> 辅助函数,为所有存在文章的年、月、日都创建了对应的分页归档页面蓝图。</p>
<p>最终,这个 <code>Generator</code> 返回一个巨大的 <code>result</code> 数组,其中包含了主归档页、以及所有年、月、日归档页的完整页面蓝图。</p>
<h1 id="5-路由的建立"><a class="markdownIt-Anchor" href="#5-路由的建立"></a> 5. 路由的建立</h1>
<p><code>_runGenerators</code> 方法为我们产出了一份包含了网站所有页面生成指令的「蓝图清单」。但这些蓝图还只是数据,系统需要一个机制来管理它们,并将它们与最终的文件路径关联起来。</p>
<p>这个机制就是 Hexo 的 <code>Router</code>。</p>
<h2 id="51-router-类内存中的-public-目录"><a class="markdownIt-Anchor" href="#51-router-类内存中的-public-目录"></a> 5.1. <code>Router</code> 类:内存中的 <code>public</code> 目录</h2>
<p>在分析 <code>_routerRefresh</code> 之前,我们必须先理解它的工作对象:<code>this.route</code>。这个属性是 <code>Router</code> 类的一个实例。</p>
<p>需要明确的是,Hexo 的 <code>Router</code> <strong>不是</strong> 一个网络服务器中的路由(用于匹配 URL 请求),而是一个 <strong>内存中的键值对集合</strong>,可以理解为一个虚拟的文件系统。</p>
<ul>
<li>键:将要生成的文件路径,例如 <code>archives/index.html</code></li>
<li>值:该路径对应的文件内容</li>
</ul>
<p><code>Router</code> 类(位于 <code>/lib/hexo/router.js</code>)提供了一系列方法来管理这个集合,核心方法包括 <code>list()</code>、<code>get(path)</code>、<code>set(path, data)</code> 和 <code>remove(path)</code>。</p>
<figure class="highlight javascript"><figcaption><span>/hexo/router.JS</span></figcaption><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Router</span> <span class="keyword">extends</span> <span class="title class_ inherited__">events_1.EventEmitter</span> {</span><br><span class="line"> <span class="title function_">constructor</span>(<span class="params"></span>) {</span><br><span class="line"> <span class="variable language_">super</span>();</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">routes</span> = {}; <span class="comment">// 核心数据结构</span></span><br><span class="line"> }</span><br><span class="line"> <span class="title function_">list</span>(<span class="params"></span>) {}</span><br><span class="line"> <span class="title function_">format</span>(<span class="params">path</span>) {}</span><br><span class="line"> <span class="title function_">get</span>(<span class="params">path</span>) {}</span><br><span class="line"> <span class="title function_">isModified</span>(<span class="params">path</span>) {}</span><br><span class="line"> <span class="title function_">set</span>(<span class="params">path, data</span>) {}</span><br><span class="line"> <span class="title function_">remove</span>(<span class="params">path</span>) {}</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p><code>_routerRefresh</code> 的核心任务便是清空并重新填充这个 <code>this.routes</code> 对象。</p>
<h2 id="52-_routerrefresh"><a class="markdownIt-Anchor" href="#52-_routerrefresh"></a> 5.2. <code>_routerRefresh</code></h2>
<p><code>_routerRefresh</code> 方法接收 <code>_runGenerators</code> 返回的蓝图数组作为输入、遍历这个数组、为每一个蓝图在 <code>this.route</code> 中创建一条对应的记录。</p>
<figure class="highlight javascript"><figcaption><span>/hexo/index.JS</span></figcaption><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line"><span class="title function_">_routerRefresh</span>(<span class="params">runningGenerators, useCache</span>) {</span><br><span class="line"> <span class="keyword">const</span> { route } = <span class="variable language_">this</span>;</span><br><span class="line"> <span class="keyword">const</span> routeList = route.<span class="title function_">list</span>(); <span class="comment">// 1. 获取旧的路由列表,用于后续对比</span></span><br><span class="line"> <span class="keyword">const</span> <span class="title class_">Locals</span> = <span class="variable language_">this</span>.<span class="title function_">_generateLocals</span>(); <span class="comment">// 2. 准备一个 Locals 类的构造器</span></span><br><span class="line"> <span class="title class_">Locals</span>.<span class="property"><span class="keyword">prototype</span></span>.<span class="property">cache</span> = useCache;</span><br><span class="line"> <span class="comment">// 3. 遍历所有由 Generator 生成的页面蓝图</span></span><br><span class="line"> <span class="keyword">return</span> runningGenerators.<span class="title function_">map</span>(<span class="function">(<span class="params">generatorResult</span>) =></span> {</span><br><span class="line"> <span class="keyword">if</span> (<span class="keyword">typeof</span> generatorResult !== <span class="string">'object'</span> || generatorResult.<span class="property">path</span> == <span class="literal">null</span>)</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">undefined</span>;</span><br><span class="line"> <span class="keyword">const</span> path = route.<span class="title function_">format</span>(generatorResult.<span class="property">path</span>);</span><br><span class="line"> <span class="keyword">const</span> { data, layout } = generatorResult;</span><br><span class="line"> <span class="comment">// 4. 根据蓝图是否有 layout 属性,进行分流处理</span></span><br><span class="line"> <span class="keyword">if</span> (!layout) {</span><br><span class="line"> <span class="comment">// 4.1. 没有 layout:视为静态资源</span></span><br><span class="line"> route.<span class="title function_">set</span>(path, data);</span><br><span class="line"> <span class="keyword">return</span> path;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 4.2. 有 layout:视为待渲染页面</span></span><br><span class="line"> <span class="comment">// 这里的 createLoadThemeRoute 是用于将主题的布局文件和页面的具体数据打包成一个待执行的渲染任务、通过 route.set 存入路由系统的</span></span><br><span class="line"> <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="title function_">execFilter</span>(<span class="string">'template_locals'</span>, <span class="keyword">new</span> <span class="title class_">Locals</span>(path, data), { <span class="attr">context</span>: <span class="variable language_">this</span> })</span><br><span class="line"> .<span class="title function_">then</span>(<span class="function"><span class="params">locals</span> =></span> { route.<span class="title function_">set</span>(path, <span class="title function_">createLoadThemeRoute</span>(generatorResult, locals, <span class="variable language_">this</span>)); })</span><br><span class="line"> .<span class="title function_">thenReturn</span>(path);</span><br><span class="line"> }).<span class="title function_">then</span>(<span class="function"><span class="params">newRouteList</span> =></span> {</span><br><span class="line"> <span class="comment">// 5. 刷新操作:移除所有过时的路由</span></span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">let</span> i = <span class="number">0</span>, len = routeList.<span class="property">length</span>; i < len; i++) {</span><br><span class="line"> <span class="keyword">const</span> item = routeList[i];</span><br><span class="line"> <span class="keyword">if</span> (!newRouteList.<span class="title function_">includes</span>(item)) {</span><br><span class="line"> route.<span class="title function_">remove</span>(item);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> });</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<blockquote>
<p><code>_generateLocals</code> 方法见 <a href="/posts/5d57#%E7%94%9F%E6%88%90%E6%9C%AC%E5%9C%B0%E7%8E%AF%E5%A2%83">这里</a>。</p>
</blockquote>
<p>这个方法的逻辑非常清晰,但要真正理解,我们需要回到 <code>Router</code> 类。</p>
<h2 id="53-router-类深度解析"><a class="markdownIt-Anchor" href="#53-router-类深度解析"></a> 5.3. <code>Router</code> 类深度解析</h2>
<ol>
<li>
<p><code>set</code> 方法负责将内容存入路由。它的实现非常巧妙,能够区分处理不同类型的数据。</p>
<figure class="highlight javascript"><figcaption><span>/hexo/router.JS</span></figcaption><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line"><span class="title function_">set</span>(<span class="params">path, data</span>) {</span><br><span class="line"> <span class="keyword">if</span> (<span class="keyword">typeof</span> path !== <span class="string">'string'</span>)</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">TypeError</span>(<span class="string">'path must be a string!'</span>);</span><br><span class="line"> <span class="keyword">if</span> (data == <span class="literal">null</span>)</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">TypeError</span>(<span class="string">'data is required!'</span>);</span><br><span class="line"> <span class="keyword">let</span> obj;</span><br><span class="line"> <span class="keyword">if</span> (<span class="keyword">typeof</span> data === <span class="string">'object'</span> && data.<span class="property">data</span> != <span class="literal">null</span>) {</span><br><span class="line"> obj = data;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span> {</span><br><span class="line"> obj = {</span><br><span class="line"> data,</span><br><span class="line"> <span class="attr">modified</span>: <span class="literal">true</span></span><br><span class="line"> };</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 检查 data 是否为函数</span></span><br><span class="line"> <span class="keyword">if</span> (<span class="keyword">typeof</span> obj.<span class="property">data</span> === <span class="string">'function'</span>) {</span><br><span class="line"> <span class="comment">// 使用 bluebird 将其包装成一个标准的 Promise-returning 函数</span></span><br><span class="line"> <span class="keyword">if</span> (obj.<span class="property">data</span>.<span class="property">length</span>) { <span class="comment">// 有回调参数的函数</span></span><br><span class="line"> obj.<span class="property">data</span> = bluebird_1.<span class="property">default</span>.<span class="title function_">promisify</span>(obj.<span class="property">data</span>);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span> { <span class="comment">// 无参数的函数</span></span><br><span class="line"> obj.<span class="property">data</span> = bluebird_1.<span class="property">default</span>.<span class="title function_">method</span>(obj.<span class="property">data</span>);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> path = <span class="variable language_">this</span>.<span class="title function_">format</span>(path); <span class="comment">// 格式化路径</span></span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">routes</span>[path] = {</span><br><span class="line"> <span class="attr">data</span>: obj.<span class="property">data</span>,</span><br><span class="line"> <span class="attr">modified</span>: obj.<span class="property">modified</span> == <span class="literal">null</span> ? <span class="literal">true</span> : obj.<span class="property">modified</span></span><br><span class="line"> };</span><br><span class="line"> <span class="variable language_">this</span>.<span class="title function_">emit</span>(<span class="string">'update'</span>, path);</span><br><span class="line"> <span class="keyword">return</span> <span class="variable language_">this</span>;</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p><code>set</code> 方法的逻辑揭示了路由内容的两种形态:</p>
<ol>
<li><strong>成品(静态资源)</strong>:如果传入的 <code>data</code> 是 Buffer 或字符串(比如一张图片的内容),它会被直接存入 <code>this.routes</code></li>
<li><strong>半成品(待渲染页面)</strong>:如果传入的 <code>data</code> 是一个函数(由 <code>createLoadThemeRoute</code> 创建的那个),<code>set</code> 方法会用 <code>bluebird</code> 将其包装成一个标准的异步函数。<strong>存入路由的,是这个被包装后的函数本身</strong></li>
</ol>
<p>这种设计就是 Hexo 高性能的 <strong>延迟执行(Lazy Evaluation)</strong> 策略的核心。它避免了在生成路由时就渲染所有页面,而是将渲染工作推迟到最后一刻。</p>
</li>
<li>
<p>如果说 <code>set</code> 方法是把「速冻料理包」(待执行的渲染函数)放入冰箱,那么 <code>get</code> 方法就是从冰箱里取出料理包并准备加热。</p>
<p><code>get</code> 方法本身很简单,但它返回的东西却大有文章:</p>
<figure class="highlight javascript"><figcaption><span>/hexo/router.JS</span></figcaption><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="title function_">get</span>(<span class="params">path</span>) {</span><br><span class="line"> <span class="keyword">if</span> (<span class="keyword">typeof</span> path !== <span class="string">'string'</span>)</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">TypeError</span>(<span class="string">'path must be a string!'</span>);</span><br><span class="line"> <span class="keyword">const</span> data = <span class="variable language_">this</span>.<span class="property">routes</span>[<span class="variable language_">this</span>.<span class="title function_">format</span>(path)];</span><br><span class="line"> <span class="keyword">if</span> (data == <span class="literal">null</span>)</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> <span class="comment">// 返回一个 RouteStream 实例</span></span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">RouteStream</span>(data);</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>它并不直接返回值,而是将从 <code>this.routes</code> 中取出的数据(无论是 Buffer 还是那个待执行的函数)传递给 <code>RouteStream</code> 类的构造函数,并返回一个 <code>RouteStream</code> 实例。</p>
<p><code>RouteStream</code> 是一个 Node.JS 的可读流(Readable Stream)。它的魔法藏在 <code>_read()</code> 方法中:</p>
<figure class="highlight javascript"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">RouteStream</span> <span class="keyword">extends</span> <span class="title class_ inherited__">Readable</span> {</span><br><span class="line"> <span class="title function_">constructor</span>(<span class="params">data</span>) {</span><br><span class="line"> <span class="variable language_">super</span>({ <span class="attr">objectMode</span>: <span class="literal">true</span> });</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">_data</span> = data.<span class="property">data</span>;</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">_ended</span> = <span class="literal">false</span>;</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">modified</span> = data.<span class="property">modified</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="title function_">_toBuffer</span>(<span class="params">data</span>) {</span><br><span class="line"> <span class="keyword">if</span> (data <span class="keyword">instanceof</span> <span class="title class_">Buffer</span>) {</span><br><span class="line"> <span class="keyword">return</span> data;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (<span class="keyword">typeof</span> data === <span class="string">'object'</span>) {</span><br><span class="line"> data = <span class="title class_">JSON</span>.<span class="title function_">stringify</span>(data);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (<span class="keyword">typeof</span> data === <span class="string">'string'</span>) {</span><br><span class="line"> <span class="keyword">return</span> <span class="title class_">Buffer</span>.<span class="title function_">from</span>(data);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="title function_">_read</span>(<span class="params"></span>) {</span><br><span class="line"> <span class="keyword">const</span> data = <span class="variable language_">this</span>.<span class="property">_data</span>;</span><br><span class="line"> <span class="comment">// 情况一:内容是“成品”(Buffer、String 等)</span></span><br><span class="line"> <span class="keyword">if</span> (<span class="keyword">typeof</span> data !== <span class="string">'function'</span>) {</span><br><span class="line"> <span class="keyword">const</span> bufferData = <span class="variable language_">this</span>.<span class="title function_">_toBuffer</span>(data);</span><br><span class="line"> <span class="keyword">if</span> (bufferData) {</span><br><span class="line"> <span class="variable language_">this</span>.<span class="title function_">push</span>(bufferData); <span class="comment">// 直接将数据推入流中</span></span><br><span class="line"> }</span><br><span class="line"> <span class="variable language_">this</span>.<span class="title function_">push</span>(<span class="literal">null</span>); <span class="comment">// 结束流</span></span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 情况二:内容是“半成品”(待执行函数)</span></span><br><span class="line"> <span class="keyword">if</span> (<span class="variable language_">this</span>.<span class="property">_ended</span>)</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">_ended</span> = <span class="literal">true</span>;</span><br><span class="line"> <span class="comment">// 执行函数</span></span><br><span class="line"> <span class="title function_">data</span>().<span class="title function_">then</span>(<span class="function"><span class="params">data</span> =></span> {</span><br><span class="line"> <span class="keyword">if</span> (data <span class="keyword">instanceof</span> stream_1.<span class="property">default</span> && data.<span class="property">readable</span>) {</span><br><span class="line"> data.<span class="title function_">on</span>(<span class="string">'data'</span>, <span class="function"><span class="params">d</span> =></span> {</span><br><span class="line"> <span class="variable language_">this</span>.<span class="title function_">push</span>(d);</span><br><span class="line"> });</span><br><span class="line"> data.<span class="title function_">on</span>(<span class="string">'end'</span>, <span class="function">() =></span> {</span><br><span class="line"> <span class="variable language_">this</span>.<span class="title function_">push</span>(<span class="literal">null</span>);</span><br><span class="line"> });</span><br><span class="line"> data.<span class="title function_">on</span>(<span class="string">'error'</span>, <span class="function"><span class="params">err</span> =></span> {</span><br><span class="line"> <span class="variable language_">this</span>.<span class="title function_">emit</span>(<span class="string">'error'</span>, err);</span><br><span class="line"> });</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">const</span> bufferData = <span class="variable language_">this</span>.<span class="title function_">_toBuffer</span>(data);</span><br><span class="line"> <span class="keyword">if</span> (bufferData) {</span><br><span class="line"> <span class="variable language_">this</span>.<span class="title function_">push</span>(bufferData); <span class="comment">// 将渲染后的 HTML 推入流中</span></span><br><span class="line"> }</span><br><span class="line"> <span class="variable language_">this</span>.<span class="title function_">push</span>(<span class="literal">null</span>); <span class="comment">// 结束流</span></span><br><span class="line"> }</span><br><span class="line"> }).<span class="title function_">catch</span>(<span class="function"><span class="params">err</span> =></span> {</span><br><span class="line"> <span class="variable language_">this</span>.<span class="title function_">emit</span>(<span class="string">'error'</span>, err);</span><br><span class="line"> <span class="variable language_">this</span>.<span class="title function_">push</span>(<span class="literal">null</span>);</span><br><span class="line"> });</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>之后我们讲到「文件写入」时,模块会从这个流中读取数据,<code>_read()</code> 方法会被触发。它会检查 <code>this._data</code>:</p>
<ul>
<li>如果是普通数据,就直接推入流</li>
<li>如果是一个函数,它就会执行这个函数(<code>data()</code>)。渲染过程在这一刻才真正发生。函数执行完毕后(<code>.then()</code>),<code>_read</code> 会将返回的 HTML 结果推入流中,供文件写入模块消费</li>
</ul>
</li>
</ol>
<h1 id="6-从内存到硬盘"><a class="markdownIt-Anchor" href="#6-从内存到硬盘"></a> 6. 从内存到硬盘</h1>
<p>经过前面四个阶段的精密运作,Hexo 已经在内存中构建了一个完整的虚拟网站(<code>this.route</code>)。现在,我们来到了这趟旅程的终点站:将这个虚拟世界实体化,写入到硬盘的 <code>public</code> 文件夹中。</p>
<p>这个最终的执行任务,由 <code>generate</code> 命令的总指挥 —— <code>Generater</code> 类 —— 来完成。</p>
<h2 id="61-怎么又是你-generator-类"><a class="markdownIt-Anchor" href="#61-怎么又是你-generator-类"></a> 6.1. 怎么又是你 <code>Generator</code> 类</h2>
<p>回顾第一章,<code>generateConsole</code> 函数在最开始就实例化了 <code>Generater</code> 类:</p>
<figure class="highlight javascript"><figcaption><span>/plugins/console/generate.JS</span></figcaption><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">generateConsole</span>(<span class="params">args = {}</span>) {</span><br><span class="line"> <span class="keyword">const</span> generator = <span class="keyword">new</span> <span class="title class_">Generater</span>(<span class="variable language_">this</span>, args); <span class="comment">// 我在这儿</span></span><br><span class="line"> <span class="keyword">if</span> (generator.<span class="property">watch</span>) {</span><br><span class="line"> <span class="keyword">return</span> generator.<span class="title function_">execWatch</span>();</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="title function_">load</span>().<span class="title function_">then</span>(<span class="function">() =></span> generator.<span class="title function_">firstGenerate</span>()).<span class="title function_">then</span>(<span class="function">() =></span> {</span><br><span class="line"> <span class="keyword">if</span> (generator.<span class="property">deploy</span>) {</span><br><span class="line"> <span class="keyword">return</span> generator.<span class="title function_">execDeploy</span>();</span><br><span class="line"> }</span><br><span class="line"> });</span><br><span class="line">}</span><br><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = generateConsole;</span><br></pre></td></tr></tbody></table></figure>
<p><code>generator.firstGenerate()</code> 就是文件写入流程的入口。这个方法负责比对文件差异、创建任务队列,并最终调用 <code>writeFile</code> 和 <code>deleteFile</code> 来同步 <code>public</code> 目录。</p>
<h2 id="62-firstgenerate"><a class="markdownIt-Anchor" href="#62-firstgenerate"></a> 6.2. <code>firstGenerate</code></h2>
<p><code>firstGenerate</code> 方法做的第一件大事,不是写入,而是 <strong>比对</strong>,以确定一份精准的「施工清单」。</p>
<figure class="highlight javascript"><figcaption><span>/plugins/console/generate.JS</span></figcaption><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line"><span class="title function_">firstGenerate</span>(<span class="params"></span>) {</span><br><span class="line"> <span class="keyword">const</span> { concurrency } = <span class="variable language_">this</span>;</span><br><span class="line"> <span class="keyword">const</span> { route, log } = <span class="variable language_">this</span>.<span class="property">context</span>;</span><br><span class="line"> <span class="keyword">const</span> publicDir = <span class="variable language_">this</span>.<span class="property">context</span>.<span class="property">public_dir</span>;</span><br><span class="line"> <span class="keyword">const</span> <span class="title class_">Cache</span> = <span class="variable language_">this</span>.<span class="property">context</span>.<span class="title function_">model</span>(<span class="string">'Cache'</span>);</span><br><span class="line"> <span class="keyword">const</span> interval = (<span class="number">0</span>, pretty_hrtime_1.<span class="property">default</span>)(process.<span class="title function_">hrtime</span>(<span class="variable language_">this</span>.<span class="property">start</span>));</span><br><span class="line"> log.<span class="title function_">info</span>(<span class="string">'Files loaded in %s'</span>, (<span class="number">0</span>, picocolors_1.<span class="property">cyan</span>)(interval));</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">start</span> = process.<span class="title function_">hrtime</span>();</span><br><span class="line"> <span class="comment">// 确保 public 文件夹存在</span></span><br><span class="line"> <span class="keyword">return</span> (<span class="number">0</span>, hexo_fs_1.<span class="property">stat</span>)(publicDir).<span class="title function_">then</span>(<span class="function"><span class="params">stats</span> =></span> {</span><br><span class="line"> <span class="keyword">if</span> (!stats.<span class="title function_">isDirectory</span>()) {</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">Error</span>(<span class="string">`<span class="subst">${(<span class="number">0</span>, picocolors_1.magenta)((<span class="number">0</span>, tildify_1.<span class="keyword">default</span>)(publicDir))}</span> is not a directory`</span>);</span><br><span class="line"> }</span><br><span class="line"> }).<span class="title function_">catch</span>(<span class="function"><span class="params">err</span> =></span> {</span><br><span class="line"> <span class="keyword">if</span> (err && err.<span class="property">code</span> === <span class="string">'ENOENT'</span>) {</span><br><span class="line"> <span class="keyword">return</span> (<span class="number">0</span>, hexo_fs_1.<span class="property">mkdirs</span>)(publicDir);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">throw</span> err;</span><br><span class="line"> }).<span class="title function_">then</span>(<span class="function">() =></span> {</span><br><span class="line"> <span class="keyword">const</span> <span class="title function_">task</span> = (<span class="params">fn, path</span>) => <span class="function">() =></span> fn.<span class="title function_">call</span>(<span class="variable language_">this</span>, path);</span><br><span class="line"> <span class="keyword">const</span> <span class="title function_">doTask</span> = fn => <span class="title function_">fn</span>();</span><br><span class="line"> <span class="keyword">const</span> routeList = route.<span class="title function_">list</span>(); <span class="comment">// 1. 获取本次需要生成的文件清单</span></span><br><span class="line"> <span class="keyword">const</span> publicFiles = <span class="title class_">Cache</span>.<span class="title function_">filter</span>(<span class="function"><span class="params">item</span> =></span> item.<span class="property">_id</span>.<span class="title function_">startsWith</span>(<span class="string">'public/'</span>)).<span class="title function_">map</span>(<span class="function"><span class="params">item</span> =></span> item.<span class="property">_id</span>.<span class="title function_">substring</span>(<span class="number">7</span>)); <span class="comment">// 2. 获取上次已生成的文件清单</span></span><br><span class="line"> <span class="comment">// 3. 生成任务队列</span></span><br><span class="line"> <span class="keyword">const</span> tasks = publicFiles.<span class="title function_">filter</span>(<span class="function"><span class="params">path</span> =></span> !routeList.<span class="title function_">includes</span>(path))</span><br><span class="line"> .<span class="title function_">map</span>(<span class="function"><span class="params">path</span> =></span> <span class="title function_">task</span>(<span class="variable language_">this</span>.<span class="property">deleteFile</span>, path)) <span class="comment">// 需要删除的文件</span></span><br><span class="line"> .<span class="title function_">concat</span>(routeList.<span class="title function_">map</span>(<span class="function"><span class="params">path</span> =></span> <span class="title function_">task</span>(<span class="variable language_">this</span>.<span class="property">generateFile</span>, path))); <span class="comment">// 需要生成/更新的文件</span></span><br><span class="line"> <span class="comment">// 4. 并发执行所有任务</span></span><br><span class="line"> <span class="keyword">return</span> bluebird_1.<span class="property">default</span>.<span class="title function_">all</span>(bluebird_1.<span class="property">default</span>.<span class="title function_">map</span>(tasks, doTask, { <span class="attr">concurrency</span>: <span class="built_in">parseFloat</span>(concurrency || <span class="string">'Infinity'</span>) }));</span><br><span class="line"> }).<span class="title function_">then</span>(<span class="function"><span class="params">result</span> =></span> {</span><br><span class="line"> <span class="keyword">const</span> interval = (<span class="number">0</span>, pretty_hrtime_1.<span class="property">default</span>)(process.<span class="title function_">hrtime</span>(<span class="variable language_">this</span>.<span class="property">start</span>));</span><br><span class="line"> <span class="keyword">const</span> count = result.<span class="title function_">filter</span>(<span class="title class_">Boolean</span>).<span class="property">length</span>;</span><br><span class="line"> log.<span class="title function_">info</span>(<span class="string">'%d files generated in %s'</span>, count.<span class="title function_">toString</span>(), (<span class="number">0</span>, picocolors_1.<span class="property">cyan</span>)(interval));</span><br><span class="line"> });</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>这里的逻辑非常清晰:</p>
<ol>
<li>获取新蓝图:从 <code>this.context.route.list()</code> 获取我们在第四章构建的内存虚拟网站的文件列表</li>
<li>获取旧清单:从 <code>Cache</code> 模型中读取上次生成时写入 <code>public/</code> 的所有文件记录</li>
<li>计算差异并生成任务:
<ul>
<li>如果一个文件在「旧清单」里,但不在「新蓝图」里,就为它创建一个 <code>deleteFile</code> 任务</li>
<li>所有「新蓝图」里的文件,都为它们创建一个 <code>generateFile</code> 任务</li>
</ul>
</li>
<li>并发执行:使用 <code>bluebird.map</code> 并发处理所有任务,以提高生成效率</li>
</ol>
<h2 id="63-触发延迟执行"><a class="markdownIt-Anchor" href="#63-触发延迟执行"></a> 6.3. 触发延迟执行</h2>
<p><code>writeFile</code> 方法是整个流程中最关键的「临门一脚」,它负责触发第四章中我们埋下的「延迟执行」机制。</p>
<figure class="highlight javascript"><figcaption><span>/plugins/console/generate.JS</span></figcaption><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="title function_">writeFile</span>(<span class="params">path, force</span>) {</span><br><span class="line"> <span class="keyword">const</span> { route, log } = <span class="variable language_">this</span>.<span class="property">context</span>;</span><br><span class="line"> <span class="keyword">const</span> publicDir = <span class="variable language_">this</span>.<span class="property">context</span>.<span class="property">public_dir</span>;</span><br><span class="line"> <span class="keyword">const</span> <span class="title class_">Cache</span> = <span class="variable language_">this</span>.<span class="property">context</span>.<span class="title function_">model</span>(<span class="string">'Cache'</span>);</span><br><span class="line"> <span class="comment">// 1. 从路由中获取内容,返回的是一个 RouteStream 实例</span></span><br><span class="line"> <span class="keyword">const</span> dataStream = <span class="variable language_">this</span>.<span class="title function_">wrapDataStream</span>(route.<span class="title function_">get</span>(path));</span><br><span class="line"> <span class="keyword">const</span> buffers = [];</span><br><span class="line"> <span class="keyword">const</span> hasher = (<span class="number">0</span>, hexo_util_1.<span class="property">createSha1Hash</span>)();</span><br><span class="line"> <span class="keyword">const</span> finishedPromise = <span class="keyword">new</span> bluebird_1.<span class="title function_">default</span>(<span class="function">(<span class="params">resolve, reject</span>) =></span> {</span><br><span class="line"> dataStream.<span class="title function_">once</span>(<span class="string">'error'</span>, reject);</span><br><span class="line"> dataStream.<span class="title function_">once</span>(<span class="string">'end'</span>, resolve);</span><br><span class="line"> });</span><br><span class="line"> <span class="comment">// 2. 消费这个流</span></span><br><span class="line"> dataStream.<span class="title function_">on</span>(<span class="string">'data'</span>, <span class="function"><span class="params">chunk</span> =></span> {</span><br><span class="line"> buffers.<span class="title function_">push</span>(chunk);</span><br><span class="line"> hasher.<span class="title function_">update</span>(chunk);</span><br><span class="line"> });</span><br></pre></td></tr></tbody></table></figure>
<p>当我们开始监听 <code>data</code> 事件,也就是开始从流中「拉取」数据时,<code>RouteStream</code> 内部的 <code>_read()</code> 方法就会被触发。</p>
<p>如果这个路由的内容是一个「待执行的渲染函数」,这个函数在此时此刻才会被执行,渲染出最终的 HTML,然后将结果推入流中,被这里的 <code>data</code> 事件监听器接收。</p>
<h2 id="64-哈希值对比"><a class="markdownIt-Anchor" href="#64-哈希值对比"></a> 6.4. 哈希值对比</h2>
<p>在 <code>writeFile</code> 方法的后半部分,还隐藏着 Hexo 再次生成时速度飞快的秘密 —— 增量生成。</p>
<figure class="highlight javascript"><figcaption><span>/plugins/console/generate.JS</span></figcaption><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"> <span class="keyword">return</span> finishedPromise.<span class="title function_">then</span>(<span class="function">() =></span> {</span><br><span class="line"> <span class="keyword">const</span> dest = (<span class="number">0</span>, path_1.<span class="property">join</span>)(publicDir, path);</span><br><span class="line"> <span class="keyword">const</span> cacheId = <span class="string">`public/<span class="subst">${path}</span>`</span>;</span><br><span class="line"> <span class="keyword">const</span> cache = <span class="title class_">Cache</span>.<span class="title function_">findById</span>(cacheId); <span class="comment">// 1. 查找上一次的缓存</span></span><br><span class="line"> <span class="keyword">const</span> hash = hasher.<span class="title function_">digest</span>(<span class="string">'hex'</span>); <span class="comment">// 2. 拿到本次内容的哈希值</span></span><br><span class="line"> <span class="comment">// 3. 如果哈希值未变,且不是强制生成,则跳过</span></span><br><span class="line"> <span class="keyword">if</span> (!force && cache && cache.<span class="property">hash</span> === hash) {</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 4. 哈希值变了,才执行真正的写入操作</span></span><br><span class="line"> <span class="keyword">return</span> <span class="title class_">Cache</span>.<span class="title function_">save</span>({</span><br><span class="line"> <span class="attr">_id</span>: cacheId,</span><br><span class="line"> hash</span><br><span class="line"> }).<span class="title function_">then</span>(<span class="function">() =></span></span><br><span class="line"> (<span class="number">0</span>, hexo_fs_1.<span class="property">writeFile</span>)(dest, <span class="title class_">Buffer</span>.<span class="title function_">concat</span>(buffers))).<span class="title function_">then</span>(<span class="function">() =></span> {</span><br><span class="line"> log.<span class="title function_">info</span>(<span class="string">'Generated: %s'</span>, (<span class="number">0</span>, picocolors_1.<span class="property">magenta</span>)(path));</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line"> });</span><br><span class="line"> });</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure>
<p>在将文件内容写入磁盘之前,<code>writeFile</code> 会计算出内容的 SHA1 哈希值,并与缓存中的旧哈希值进行对比。如果两者相同,意味着文件内容没有变化,Hexo 就会聪明地跳过这次文件写入操作,从而极大地提升了二次生成的效率。</p>
<h1 id="7-总结"><a class="markdownIt-Anchor" href="#7-总结"></a> 7. 总结</h1>
<p>现在,让我们回到最初的起点,以 <code>_posts/hello-world.md</code> 的视角,快速回顾它在 <code>hexo generate</code> 命令下经历的完整旅程:</p>
<ol>
<li>我们在终端敲下回车,<code>Console</code> 扩展接收到 <code>generate</code> 命令</li>
<li><code>Box</code> 引擎扫描 <code>source</code> 目录,发现了 <code>hello-world.md</code>。<code>post</code> 处理器接管了它,解析其文件名和 Front-matter,将标题、日期等元数据连同原始 Markdown 内容(<code>_content</code>)一起,存入了数据库的 <code>Post</code> 模型中</li>
<li>在生成页面之前,<code>before_generate</code> 过滤器被触发。<code>renderPostFilter</code> 找到了数据库中 <code>content</code> 字段尚为空的 <code>hello-world.md</code>,并调用 <code>post.render</code> 对其进行「炼金」:
<ul>
<li><code>post.render</code> 暂时将文中的 Nunjucks 标签(如果有的话)替换为占位符</li>
<li><code>ctx.render</code> 调度 <code>hexo-renderer-marked</code> 将 Markdown 转换为 HTML</li>
<li>在 <code>onRenderEnd</code> 回调中,<code>post.render</code> 恢复 Nunjucks 标签并立刻执行它们。最终,一篇完整的 HTML 被存入数据库记录的 content 字段</li>
</ul>
</li>
<li><code>_runGenerators</code> 开始执行。<code>post</code> 生成器遍历数据库,找到了我们已渲染好的 <code>hello-world.md</code>,为它创建了一张包含最终路径、主题布局(<code>layout: 'post'</code>)和所有页面数据的「页面蓝图」</li>
<li><code>_routerRefresh</code> 接收到这张蓝图。它没有立即渲染整个页面,而是创建了一个「待办任务」—— 一个包含了「使用 <code>post</code> 布局」和「填充 <code>hello-world.md</code> 内容」指令的函数,并将这个任务存入了内存中的 <code>Router</code> 里,键名为最终的文件路径 <code>posts/hello-world/index.html</code></li>
<li><code>Generater</code> 类的 <code>firstGenerate</code> 方法开始工作。它对比 <code>Router</code> 和 <code>public</code> 目录的缓存,确定 <code>posts/hello-world/index.html</code> 是一个需要生成的文件,并为其创建了一个 <code>writeFile</code> 任务</li>
<li>当 <code>writeFile</code> 任务执行时,它向 <code>Router</code> 请求 <code>posts/hello-world/index.html</code> 的内容。<code>RouteStream</code> 机制被触发,直到这一刻,第五步中创建的那个「待办任务」才被真正执行。它读取主题的 <code>post</code> 布局文件,将 <code>hello-world.md</code> 的 <code>content</code>(HTML)填充进去,生成了包含页头、页脚的完整页面 HTML</li>
<li>最终的 HTML 内容通过流被写入 <code>public/posts/hello-world/index.html</code> 文件。<code>hello-world.md</code> 的旅程至此结束,它已成为网站上一个可被访问的真实页面</li>
</ol>
</body></html></div></article></div></main><footer><div class="paginator"><a class="prev" href="9781.html">上一篇</a><a class="next" href="4148.html">下一篇</a></div><!-- Webmention 显示区域--><div class="webmention-section webmention-empty" data-page-url="posts/d601.html" data-full-url="https://cytrogen.icu/posts/d601.html" data-mode="static">
<h3 class="webmention-title">Webmentions (<span class="webmention-count">0</span>)</h3>
<div class="webmention-list"></div>
<span>暂无 Webmentions</span>
</div><div class="copyright"><p class="footer-links"><a href="../friends/index.html">友链</a><span class="footer-separator"> ·</span><a href="../links/index.html">邻邦</a><span class="footer-separator"> ·</span><a href="../contact/index.html">联络</a><span class="footer-separator"> ·</span><a href="../colophon/index.html">营造记</a><span class="footer-separator"> ·</span><a href="../atom.xml">RSS订阅</a></p><p>© 2025 - 2026 <a href="https://cytrogen.icu">Cytrogen</a>, powered by <a href="https://hexo.io/" target="_blank">Hexo</a> and <a href="https://github.com/cytrogen/hexo-theme-ares" target="_blank">hexo-theme-ares</a>.</p><p><a href="https://blogscn.fun" target="_blank" rel="noopener">BLOGS·CN</a></p></div></footer></div></div><a class="back-to-top" href="#top" aria-label="返回顶部"><svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M3.293 9.707a1 1 0 010-1.414L9.586 2a2 2 0 012.828 0l6.293 6.293a1 1 0 01-1.414 1.414L11 3.414V17a1 1 0 11-2 0V3.414L2.707 9.707a1 1 0 01-1.414 0z"></path></svg></a><script>document.addEventListener('DOMContentLoaded', function() {
const codeBlocks = document.querySelectorAll('figure.highlight');
codeBlocks.forEach(block => {
let caption = block.querySelector('figcaption');
if (!caption) {
caption = document.createElement('figcaption');
block.insertBefore(caption, block.firstChild);
}
const info = document.createElement('div');
info.className = 'info';
const filename = caption.querySelector('span');
if (filename) {
filename.className = 'filename';
info.appendChild(filename);
}
const lang = block.className.split(' ')[1];
if (lang) {
const langSpan = document.createElement('span');
langSpan.className = 'lang-name';
langSpan.textContent = lang;
info.appendChild(langSpan);
}
const sourceLink = caption.querySelector('a');
if (sourceLink) {
sourceLink.className = 'source-link';
info.appendChild(sourceLink);
}
const actions = document.createElement('div');
actions.className = 'actions';
const codeHeight = block.scrollHeight;
const threshold = 300;
if (codeHeight > threshold) {
block.classList.add('folded');
const toggleBtn = document.createElement('button');
toggleBtn.textContent = '展开';
toggleBtn.addEventListener('click', () => {
block.classList.toggle('folded');
toggleBtn.textContent = block.classList.contains('folded') ? '展开' : '折叠';
});
actions.appendChild(toggleBtn);
}
const copyBtn = document.createElement('button');
copyBtn.textContent = '复制';
copyBtn.addEventListener('click', async () => {
const codeLines = block.querySelectorAll('.code .line');
const code = Array.from(codeLines)
.map(line => line.textContent)
.join('\n')
.replace(/\n\n/g, '\n');
try {
await navigator.clipboard.writeText(code);
copyBtn.textContent = '已复制';
copyBtn.classList.add('copied');
setTimeout(() => {
copyBtn.textContent = '复制';
copyBtn.classList.remove('copied');
}, 3000);
} catch (err) {
console.error('复制失败:', err);
copyBtn.textContent = '复制失败';
setTimeout(() => {
copyBtn.textContent = '复制';
}, 3000);
}
});
actions.appendChild(copyBtn);
caption.innerHTML = '';
caption.appendChild(info);
caption.appendChild(actions);
const markedLines = block.getAttribute('data-marked-lines');
if (markedLines) {
const lines = markedLines.split(',');
lines.forEach(range => {
if (range.includes('-')) {
const [start, end] = range.split('-').map(Number);
for (let i = start; i <= end; i++) {
const line = block.querySelector(`.line-${i}`);
if (line) line.classList.add('marked');
}
} else {
const line = block.querySelector(`.line-${range}`);
if (line) line.classList.add('marked');
}
});
}
});
});</script><script async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js" id="MathJax-script"></script><script>(function() {
document.addEventListener('DOMContentLoaded', function() {
const themeToggle = document.querySelector('.theme-toggle');
if (!themeToggle) return;
const getCurrentTheme = () => {
return document.documentElement.getAttribute('data-theme') || 'light';
};
const updateUI = (theme) => {
const isDark = theme === 'dark';
themeToggle.setAttribute('aria-pressed', isDark.toString());
};
const setTheme = (theme) => {
document.documentElement.setAttribute('data-theme', theme);
document.documentElement.style.colorScheme = theme;
const pageWrapper = document.getElementById('page-wrapper');
if (pageWrapper) {
pageWrapper.setAttribute('data-theme', theme);
}
// Find and remove the temporary anti-flicker style tag if it exists.
// This ensures the main stylesheet takes full control after the initial load.
const antiFlickerStyle = document.getElementById('anti-flicker-style');
if (antiFlickerStyle) {
antiFlickerStyle.remove();
}
localStorage.setItem('theme', theme);
updateUI(theme);
};
const toggleTheme = () => {
const current = getCurrentTheme();
const newTheme = current === 'light' ? 'dark' : 'light';
setTheme(newTheme);
};
updateUI(getCurrentTheme());
themeToggle.addEventListener('click', toggleTheme);
if (window.matchMedia) {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
mediaQuery.addEventListener('change', function(e) {
if (!localStorage.getItem('theme')) {
const theme = e.matches ? 'dark' : 'light';
setTheme(theme);
}
});
}
});
})();
</script><script src="../js/details-toggle.js" defer></script><script>(function() {
document.addEventListener('DOMContentLoaded', function() {
const backToTopBtn = document.querySelector('.back-to-top');
if (!backToTopBtn) return;
const toggleButtonVisibility = () => {
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
const shouldShow = scrollTop > 200;
if (shouldShow) {
backToTopBtn.classList.add('is-visible');
} else {
backToTopBtn.classList.remove('is-visible');
}
};
let ticking = false;
const handleScroll = () => {
if (!ticking) {
requestAnimationFrame(() => {
toggleButtonVisibility();
ticking = false;
});
ticking = true;
}
};
const scrollToTop = (event) => {
event.preventDefault();
window.scrollTo({
top: 0,
behavior: 'smooth'
});
};
window.addEventListener('scroll', handleScroll);
backToTopBtn.addEventListener('click', scrollToTop);
toggleButtonVisibility();
});
})();</script></body></html>