静态站点在请求到来之前准备好文档。判断是否适用,关键是不同访问者是否需要服务器即时生成不同表示,而不是站点到底有多少页面。几百篇文章也可以共享一条发布流程:编辑源文件、构建 HTML 和资源、检查生成结果,再通过服务器或 CDN 对外提供这些文件。页面数量本身不是引入动态运行时的充分理由。

把文档生成放在合适边界

一篇文章对每位读者含义相同,逐次请求重新拼装通常不会增加多少价值。模板、导航、翻译关系和索引都可以提前生成。对外服务此时依赖的是发布产物,而不是作者使用的内容数据库或生成器。请求仍然需要加密、日志、缓存和必要的访问控制;静态交付只改变文档生成时机,并没有消除托管层的这些职责。

静态 HTML 完全可以包含 JavaScript 和交互控件。规模适中的公开文集可以使用客户端搜索索引,私有搜索或个性化结果则需要能执行权限检查的服务。表单、评论、支付和需要登录的写入,同样要由合适的处理端负责。应逐个识别这些边界:文章静态化,并不意味着浏览器能够成为权限判断或持久状态的权威来源。

提前生成是转移工作,而不是消灭工作。大型文集可能重建缓慢,构建依赖可能失败,内容新鲜度也受到发布流水线限制。源文件刚刚保存,并不代表读者已经看到更新。对于有时效的内容,需要观察从编辑到发布的延迟、明确暴露构建失败,并直接验证发布后的页面,而不能以编辑器保存成功代替上线证据。

坏掉的样子

常见错误是把整个仓库根目录当成网站根目录。这样即使没有导航链接,源数据、构建脚本和私有草稿也可能被直接下载。应使用专门的输出目录,其中只放准备公开的产物。从文章索引隐藏一项,与从发布文件中排除它,是两件不同的事。检查必须覆盖真实文件清单,包括复制进来的附件以及生成的客户端搜索数据。

另一个问题是请求到来时逐个覆盖文件。新 HTML 可能先引用尚未上传的脚本,旧页面也可能在清理过程中失去依赖。应先准备完整发布集,保留必要的旧资源,再使用托管平台支持的切换机制。仅仅恢复旧源码提交,不会自动恢复当时的依赖、构建产物、缓存状态或外部服务,因此回退能力必须按真实交付链验证。

静态输出同样可能存在注入问题。来自不可信输入的文字需要按所在上下文转义,直接插进 HTML 可能把普通字段变成可执行标记。文本转义也不是万能的:它不负责判断任意链接是否安全,不负责清洗有意允许的富文本,更不能直接用于 JavaScript 上下文。应审阅实际模板位置和源数据的信任边界,而不是因为没有请求时渲染就默认安全。

快速验证

将下面程序保存为 static-build.py,用 Python 3 执行 python3 static-build.py。它先在临时目录创建内容源,并把源码放在 public 目录之外,再构建一个 HTML 文件。回环服务器只提供 public 内的文件。所有关键结果都通过实际 HTTP 请求确认;服务器关闭之后,实验创建的临时文件也会移除,不会修改现有站点。

from functools import partial
from hashlib import sha256
from html import escape
from http.client import HTTPConnection
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from tempfile import TemporaryDirectory
from threading import Thread
import json
import os


class Handler(SimpleHTTPRequestHandler):
    def log_message(self, *args):
        pass


with TemporaryDirectory() as directory:
    root = Path(directory)
    public = root / 'public'
    public.mkdir()
    source = root / 'source.json'
    source.write_text(json.dumps({'title': 'Version one', 'text': 'Published content'}))

    def build():
        content = json.loads(source.read_text())
        document = f'<!doctype html><html lang="en"><head><meta charset="utf-8"><title>{escape(content["title"])}</title></head><body><h1>{escape(content["title"])}</h1><p>{escape(content["text"])}</p></body></html>'
        pending = root / 'next.html'
        pending.write_text(document)
        os.replace(pending, public / 'index.html')
        return document.encode()

    first = build()
    server = ThreadingHTTPServer(('127.0.0.1', 0), partial(Handler, directory=str(public)))
    thread = Thread(target=server.serve_forever)
    thread.start()
    try:
        def fetch(path='/'):
            connection = HTTPConnection('127.0.0.1', server.server_port, timeout=5)
            try:
                connection.request('GET', path)
                response = connection.getresponse()
                return response.status, response.read()
            finally:
                connection.close()

        source.unlink()
        assert fetch() == (200, first)
        source.write_text(json.dumps({'title': 'Version two', 'text': '<b>literal text</b>'}))
        assert fetch() == (200, first)
        second = build()
        assert fetch() == (200, second) and second != first
        assert b'&lt;b&gt;literal text&lt;/b&gt;' in second
        assert fetch('/source.json')[0] == 404
        print(json.dumps({'serves_without_source': True, 'edit_requires_rebuild': True,
                          'published_sha256': [sha256(first).hexdigest(), sha256(second).hexdigest()],
                          'source_not_published': True, 'text_escaped': True}))
    finally:
        server.shutdown()
        thread.join()
        server.server_close()

首先,程序删除可编辑的内容源,仍然能够读取已经发布的页面字节。随后重新创建有改动的源文件,但在再次执行 build 之前,HTTP 请求仍返回第一版。第二次构建后,页面摘要发生变化,响应变为第二版。作为普通文字的 <b>literal text</b> 被转义,而不是成为真正的加粗标签;访问 /source.json 则收到 404。预期四个行为标志都为真,同时打印两个不同的发布摘要。

这些断言已经在 Python 3.14.4 下运行通过。程序通过同一文件系统上的 os.replace 替换一个完整文件,因此没有展示整个多资源站点的原子发布,也没有验证掉电持久性、自动回滚或某家托管平台的部署机制。实验的重点是证明源码与发布产物之间存在明确边界,不能把单文件行为推广成未经验证的整站承诺。

实际发布仍需检查全部生成文件,覆盖代表性链接及不存在的路径,并把缓存策略和资源可用性放在一起验证。静态交付适合生成边界与产品需求相符的场景;采用之后,也应分别观察构建层和服务层的健康状态,确保源文件中的正确修改确实能够成为用户收到的正确文档。

延伸阅读

来源