DragonRster`s Void
Banner Image
目录
针对 build.ps1 这个脚本的详细解说

单页面构建引擎 —— 把组件拼装起来,最终输出 HTML


build.ps1 就是整个静态站点生成系统里面的核心引擎。它并不会去直接生成最终的页面——而是扮演一个"装配工"的角色,把各式各样的 HTML 组件加载进来,然后进行占位符的替换工作,把动态数据注入进去,最后才输出一个完整的 .html 文件。只要把这个脚本理解透彻了,也就等于掌握了整个网站的构建原理。

1. 参数设计

脚本会去接收 5 个命名参数,把单页面构建的所有场景都覆盖到了:


param(
    [string]$PageName = "index",        # 输出文件名(不含 .html)
    [string]$Title = "DragonRS Void",   # <title> 标签内容
    [string]$ContentFile = "",          # 正文内容文件路径
    [switch]$IncludeDrafts,             # 是否包含草稿
    [string]$Lang = "zh"                # 语言:zh / en
)

这里面 $PageName 直接决定了输出的路径:index 会去输出到项目的根目录,其他的页面就会输出到 dist/(中文)以及 dist/en/(英文)[1]

2. 组件加载

脚本的第一步,就是把所有的 HTML 组件模板都给加载进来。这些模板存放于 src/components/ 目录当中,运用 Get-Content -Raw -Encoding UTF8 这个命令,把整份文件以字符串形式读取出来:


$header     → src\components\header.html
$banner     → src\components\banner.html
$sidebarLeft  → src\components\sidebar-left.html
$sidebarRight → src\components\sidebar-right.html (或 -en.html 英文版)
$footer     → src\components\footer.html

另外还拥有 6 个"子模板",是用来注入动态内容的:


$tmplGuestbookItem      → guestbook-item.html        单条留言的 HTML
$tmplGuestbookContainer → guestbook-container.html   留言滚动容器
$tmplChangelogItem      → changelog-item.html         单条更新日志
$tmplChangelogContainer → changelog-container.html    更新日志滚动容器
$tmplPostNav            → post-nav.html              上一篇/下一篇导航条

英文页面(-Lang en)会自动把 sidebar-right-en.html 加载进来,那里的标签文字都已经翻译成了英文。

此外,为了支持博客页面的独立侧边栏布局(左侧 TOC 目录 + 右侧精简栏),脚本还会额外加载 6 个博客专属模板:


$sidebarLeftBlog      → sidebar-left-blog.html      博客左栏(TOC目录,中文)
$sidebarLeftBlogEn    → sidebar-left-blog-en.html    博客左栏(TOC目录,英文)
$sidebarRightBlog     → sidebar-right-blog.html      博客右栏(搜索+文章+标签+功能)
$sidebarRightBlogEn   → sidebar-right-blog-en.html   博客右栏(英文版)
$tmplTocItem          → toc-item.html                TOC 条目模板
$tmplTocContainer     → toc-container.html           TOC 滚动容器模板

这些模板与主页的侧边栏是独立的——主页左侧依旧是留言板,右侧依旧是搜索+最新文章+更新日志+徽章。博客页面和主页"分道扬镳",互不干扰[2]

3. 留言板注入

脚本会从 data/guestbook.txt 里面把留言数据读取出来,取出最后 $MaxMessages(默认是 20)条,并且把顺序反转过来,好让最新的那条排在最前面,之后就逐行开展解析工作:


# 数据格式(管道符分隔):
name|email|content|ip|time|show_ip    # 新格式 6 字段
name|content|ip|time|show_ip            # 旧格式 5 字段(兼容)

每一条留言,都会借助 -replace '{{placeholder}}', $value 的方式,填进 guestbook-item.html 这个模板。要是用户填写了邮箱,那么名字就会变成 <a href="mailto:..."> 这样可点击的链接。所有的条目在拼接完之后,再填到 guestbook-container.html{{messages}} 占位符当中,形成一个带着滚动条的留言容器。

最后再通过替换 <!-- GUESTBOOK_MESSAGES -->,把它注入到左侧边栏里面去。

4. 更新日志注入

跟留言板相比,流程是类似的,但有两个关键的区别:

  • 数据源是 data/changelog.txt,格式为 YYYY-MM-DD|内容
  • 条目并不需要进行反转——changelog.txt 本来就是按照时间正序(旧→新)去排列的,取最后 N 条之后,得到的就是最近的内容了
  • 最大显示的条目数量是 $MaxChangelog(默认 300 条)
  • 在英文模式下,容器标题"更新日志"会被替换成"Changelog"。最终通过 <!-- CHANGELOG --> 这个标记注入到右侧边栏。

    5. 最新文章注入

    最新文章的列表并不是由 build.ps1 来生成的——它是由 generate-archive.ps1 预先构建好,并输出到 src/components/latest-posts.html(或者是 latest-posts-en.html)里面去的。build.ps1 只负责去读取它,然后再注入到右侧边栏的 <!-- LATEST_POSTS --> 占位符那个位置。

    这样一种"预生成 + 注入"的设计,避免了 build.ps1 每次都要去扫描全部的博客文件,从而提升了单页面构建的速度[3]

    6. 草稿检测

    在把内容文件读取出来以后,脚本就会去检测正文里面是不是包含了 <!-- draft: true -->

    
    $isDraft = $content -match ''
    if ($isDraft -and -not $IncludeDrafts) {
        Write-Host "Skipped draft: $PageName" -ForegroundColor Yellow
        return    # 直接退出,不生成页面
    }
    if ($isDraft -and $IncludeDrafts) {
        $PageName = "draft-$PageName"    # 输出为 draft-blog-xxx.html
    }
    

    这就意味着:

  • 正常去运行 build.cmd 的时候,草稿会被跳过,不会出现在网站上
  • 要是使用了 -IncludeDrafts 这个参数,草稿就会以 draft- 作为前缀输出成文件
  • 草稿并不会出现在归档、标签云、RSS 和搜索索引当中
  • 7. 语言切换链接

    右侧边栏里面有一个 <!-- LANG_SWITCH_URL --> 占位符。脚本会根据当前所采用的语言,把相应的切换链接计算出来:

    
    if ($Lang -eq "en") {
        # 英文页面 → 切换到中文版
        $sidebarRight = $sidebarRight -replace
            '', "/$PageName.html"
    } else {
        # 中文页面 → 切换到英文版
        $sidebarRight = $sidebarRight -replace
            '', "/en/$PageName.html"
    }
    

    语言切换现在会同时注入到所有侧边栏——主页右侧栏、博客右侧栏(中英文)都会被替换,确保无论在哪个页面,语言切换链接都指向正确的目标[4]

    8. 博客侧边栏分离与 TOC 目录生成

    这是最近一次重构中新增的功能。博客页面不再使用主页的侧边栏,而是加载一套独立的布局:

  • 左侧栏:从"留言板"变为 TOC 目录——自动从正文标题生成,点击可跳转
  • 右侧栏:保留搜索 + 最新文章,去掉更新日志和徽章,新增文章标签功能按钮(RSS订阅、GitHub源码等)
  • 侧边栏选择逻辑:

    
    # Select sidebars based on page type
    if ($PageName -like "blog-*") {
        if ($Lang -eq "en") {
            $pageLeft = $sidebarLeftBlogEn
            $pageRight = $sidebarRightBlogEn
        } else {
            $pageLeft = $sidebarLeftBlog
            $pageRight = $sidebarRightBlog
        }
    } else {
        $pageLeft = $sidebarLeft       # 主页保持原样
        $pageRight = $sidebarRight
    }
    

    TOC 目录的生成是本模块最精巧的部分。它从正文中提取所有 <font size="5" color="#ff66cc"> 标题标签(这正是博客文章中的章节标题格式),然后为每个标题自动分配锚点 ID 并构建目录列表:

    
    $headingPattern = '<font[^>]*size\s*=\s*"5"[^>]*color\s*=\s*"#ff66cc"[^>]*>\s*(.*?)\s*</font>'
    $tocMatches = [regex]::Matches($content, $headingPattern)
    

    这里有一个关键的实现技巧:从后往前处理。因为每次在标题前插入 <a name="toc-N"> 锚点时,字符串长度会发生变化,后面所有匹配的索引都会偏移。如果从前向后处理,第一个替换之后的索引就全错了。所以脚本倒序遍历匹配结果,从文档末尾向开头逐个插入锚点:

    
    for ($ti = $tocMatches.Count - 1; $ti -ge 0; $ti--) {
        $tocNum = $ti + 1                          # 从后往前,编号始终正确
        $tm = $tocMatches[$ti]
        $anchorId = "toc-$tocNum"
        $headingText = $tm.Groups[1].Value
        # 在原标题前插入锚点
        $replacement = '<a name="' + $anchorId + '"></a>' + $tm.Value
        $content = $content.Substring(0, $tm.Index)
                 + $replacement
                 + $content.Substring($tm.Index + $tm.Length)
        # TOC 条目往前插入(因为在倒序遍历)
        $tocItem = $tmplTocItem -replace '{{anchor}}', $anchorId
        $tocItem = $tocItem -replace '{{text}}', $headingText
        $tocItems = $tocItem + $tocItems            # 前插
    }
    $tocHtml = $tmplTocContainer -replace '{{items}}', $tocItems
    

    最终 TOC 通过 <!-- TOC_ITEMS --> 注入左侧栏,渲染为带锚点链接的目录列表[5]

    文章标签注入则简单得多——从内容文件的 <!-- tags: xxx --> 元数据中提取标签列表,生成指向 /tags.html#标签名 的链接,注入到博客右侧栏的 <!-- ARTICLE_TAGS --> 占位符中。

    所有的注入(最新文章、语言切换、TOC、文章标签)都同时作用于中英文博客侧边栏,确保两种语言版本的页面都获得完整的功能[6]

    9. 脚注系统(最复杂的部分)

    脚注的处理可以说是本脚本里面最精妙的一段逻辑了,它会分成三步来完成:

    第一步:提取定义。 运用正则去匹配正文当中的 [^N]: 内容 这种格式,然后把它存入哈希表 $fnDefs。正则还需要去处理跨行以及内嵌标签的情况[7]

    第二步:替换引用。 把正文里面的 [^N](后面不紧跟冒号,以此来排除定义行)替换成带着锚点的上标链接:

    
    <small><sup>
      <a href="#fn1" id="fnref1" style="color:#ffff00;">[1]</a>
    </sup></small>
    

    第三步:构建定义列表。 在文末生成一个表格,每一行都包含了脚注编号(可以链接回引用的锚点)以及对应的内容。脚注会用 size="1"(也就是最小号的字体)来显示。

    双向锚点确保了读者可以去点击 [8] 跳转到文末去查看注释,然后还可以再点击脚注编号跳回到原文当中的那个位置——全程都不需要 JavaScript[9]

    10. 文章导航(上一篇/下一篇)

    对于那些博客页面(也就是 $PageName -like "blog-*"),脚本会自动把上一篇/下一篇的导航生成出来。具体的流程是这样的:

  • 扫描博客内容目录下面的所有 content-*.html 文件
  • 从每一个文件中把 date 以及 title 的元数据提取出来
  • 将草稿过滤掉(除非使用了 -IncludeDrafts
  • 按照日期进行降序排列
  • 找到当前这篇文章在排序列表中的位置 $idx
  • $idx+1 对应的是更早的文章(也就是"上一篇"),$idx-1 对应的是更新的文章(也就是"下一篇")
  • 得到的结果会通过 post-nav.html 模板去进行渲染,把 {{prev}}{{home}}{{next}} 这三个占位符给替换掉。

    11. 页面组装与输出

    到了最后,所有的部分都会被拼接起来,形成一个完整的 HTML 文档:

    
    # 博客页面使用独立侧边栏,主页保持原样
    $page = $header                           # <html><head>...</head><body>
          + $banner                           # 顶部横幅(960×182 图片)
          + "<table width=980><tr>"
          + $pageLeft                         # 左栏(主页=留言板,博客=TOC目录)
          + "<td width=700>" + $content + $postNavHtml + "</td>"
          + $pageRight                        # 右栏(主页=搜索+最新+更新日志+徽章
          + "</tr></table>"                #       博客=搜索+最新+标签+功能)
          + $footer                           # 页脚(含最后更新时间)
          + "</body></html>"
    

    $pageLeft$pageRight 不再是固定的变量,而是根据页面类型在组装前就已选择好。这使得同一套构建流程能产出两种不同布局的页面,而无需维护两套组装代码。

    在输出之前,还有几项收尾的工作要做:

  • 资源路径修正:把 assets/ 替换为 /assets/(从而确保是从根目录去访问的)
  • 将最后更新时间注入到页脚当中:<!-- LAST_UPDATED --> → 内容文件的修改时间
  • 标题替换:<title>DragonRS Void</title> → 文章的标题
  • 最后,运用 Set-Content -Encoding UTF8 把它输出成一个带有 BOM 的 UTF-8 文件[10]

    小结

    build.ps1 是一个典型的模板引擎 + 数据注入架构。它不依赖于任何外部库,完全凭借纯 PowerShell 去实现,其核心的思路就是把 HTML 组件化,然后借助占位符的机制,把动态的数据填入到静态的模板当中去。

    在设计上,有几个比较巧妙的地方:

  • 对老格式 5 字段留言的自动兼容,这样就避免了历史数据的丢失
  • 脚注的定义采用了正则进行精确匹配,防止把正文里的方括号错认成脚注
  • 文章导航是在构建的时候计算好的,而不是在运行时,所以是零开销
  • 博客页面与主页的侧边栏"分而治之"——通过 $PageName -like "blog-*" 一个判断完成切换,TOC 目录从正文标题自动生成,文章标签从元数据提取后链接到标签云页面
  • 资源路径的全局替换,保证了 dist/ 子目录当中的页面也能正确地引用 /assets/
  • 要说唯一的一点“遗憾”,那就是 Get-Content -Raw 这个命令需要 PowerShell 3.0 或以上的版本,这就意味着构建脚本本身没办法在真正的 Windows 95 上面去运行——只不过,这恰恰体现出了复古与实用之间那种有趣的张力[11]

    [^2]: 主页左侧依旧是留言板 + 留言表单,右侧是搜索+最新文章+更新日志+88x31徽章全集。博客页面左侧变为 TOC 目录,右侧精简为搜索+最新文章+标签+功能按钮。两者的侧边栏 HTML 模板完全独立,通过 $PageName -like “blog-*” 一个条件分支完成切换。

    [^4]: 语言切换链接现在会注入到所有侧边栏变量($sidebarRight$sidebarRightBlog$sidebarRightBlogEn),确保中英文切换在主页和博客页面都能正常工作。

    [^7]: 核心脚注正则:\[\^(\d+)\]:\s*([^\n]*?)(?:</(?:p|li|td)>)?(?=\s*\[\^\d+\]:|</font>|<hr|\s*$|\n) —— 匹配脚注定义直到遇到下一个脚注定义或段落结束标签。跨行匹配和标签内嵌是两个最容易出 bug 的地方。


    [1]输出路径的判断逻辑还把语言因素考虑进去了:英文 index 输出到 dist/en/index.html,中文 index 输出到项目根目录的 index.html;其他页面按语言分别进入 dist/dist/en/

    [2]主页左侧依旧是留言板 + 留言表单,右侧是搜索+最新文章+更新日志+88x31徽章全集。博客页面左侧变为 TOC 目录,右侧精简为搜索+最新文章+标签+功能按钮。两者的侧边栏 HTML 模板完全独立,通过 $PageName -like “blog-*” 一个条件分支完成切换。

    [3]单页面构建时只替换占位符,不重新扫描博客目录。但如果新增了文章,需要先运行 generate-archive.ps1 更新 latest-posts 组件,再运行 build.ps1。

    [4]语言切换链接现在会注入到所有侧边栏变量($sidebarRight$sidebarRightBlog$sidebarRightBlogEn),确保中英文切换在主页和博客页面都能正常工作。

    [5]TOC 生成的锚点跳转同样是纯 HTML 实现——<a name=”toc-N”> 作为目标,<a href=”#toc-N”> 作为链接。这与脚注系统采用了相同的锚点跳转机制,是 HTML 3.2 时代的标准做法。

    [6]最新文章注入和语言切换注入也都同时作用于中英文博客侧边栏,确保英文读者看到的是 Latest Posts 和正确的语言切换链接,而非中文标签或错误的 URL。

    [7]核心脚注正则:\[\^(\d+)\]:\s*([^\n]*?)(?:</(?:p|li|td)>)?(?=\s*\[\^\d+\]:|</font>|<hr|\s*$|\n) —— 匹配脚注定义直到遇到下一个脚注定义或段落结束标签。跨行匹配和标签内嵌是两个最容易出 bug 的地方。

    [8]正文中提到的 [^8] 本身就是一个脚注引用——点击它可以跳到文末这条注释,再点击编号可以跳回去。这篇博文在讲解脚注系统的同时,用脚注本身做了演示。

    [9]“90s 网站”的核心理念:所有交互走纯 HTML,不依赖 JavaScript。锚点跳转(<a name> + <a href=”#name”>)是 HTML 3.2 就支持的标准特性,在 IE5.5 甚至更早的浏览器上都能完美工作。

    [10]UTF-8 BOM 是必须的——服务器和浏览器依赖它来正确识别中文编码。没有 BOM 的话,IE5.5 可能将页面当作 Latin-1 渲染,导致中文全部变成乱码。PowerShell 的 Set-Content -Encoding UTF8 默认输出带 BOM 的 UTF-8。

    [11]PowerShell 1.0 发布于 2006 年,仅支持 Windows XP/2003/Vista。Windows 95 自带的脚本环境是 DOS 批处理和 VBScript。而 Get-Content -Raw 至少需要 PowerShell 3.0(2012 年),这让”在真·Win95 上构建”成为一个有趣的伪命题。


    « 脚本详解:generate-sitemap.ps1 —— 为搜索引擎铺路 返回主页
    English

    搜索

    最新文章

    » 脚本详解:build.ps1
    » 脚本详解:gener...
    » 脚本详解:gener...
    » 脚本详解:rebui...
    » 脚本详解:gener...

    » 文章归档


    本文标签

    教程 网站建设 脚本


    功能

    » RSS 订阅
    » GitHub 源码
    » 返回顶部
    » 文章归档


    DRAGONRSTER
    CC BY-NC-SA
    © 2004-2026 DragonRster • Made with HTML • 本站支持IE5.5+
    最佳浏览分辨率:1024x768 • 最后更新于 2026年04月29日 02:05:28