动态网站每次访问都要查数据库、跑模板、生成 HTML 返回——流量小的时候没感觉,访问量一上来服务器就吃不消,还得时刻担心数据库被注入。静态站点生成器(SSG, Static Site Generator)换了个思路:把"查数据库 + 跑模板"这件事提前到"构建时"做,生成一堆纯 HTML 文件扔到 CDN 上,用户访问时直接返回静态文件,无需服务器运算。本篇带你从零理解 SSG 的工作原理。
一、SSG 的核心三要素:模板、数据、输出
所有 SSG 工具(Hugo、Hexo、Jekyll、Eleventy)的内核都由三部分组成:模板文件(决定页面长什么样)、数据源(决定页面填什么内容)、输出目录(生成的 HTML 放哪)。构建时,生成器读取数据,套进模板,把渲染结果写成 HTML 文件。这跟动态网站的"模板引擎"本质一样,区别只在于"渲染时机"——动态网站是请求时渲染,SSG 是构建时渲染。
// 一个极简 SSG 的核心流程
const fs = require('fs');
const path = require('path');
const ejs = require('ejs');
class StaticSiteGenerator {
constructor(config) {
this.templateDir = config.templateDir; // 模板目录
this.contentDir = config.contentDir; // 数据目录(Markdown/JSON)
this.outputDir = config.outputDir; // 输出目录
}
// 1. 读取所有内容文件
readContents() {
const files = fs.readdirSync(this.contentDir).filter(f => f.endsWith('.md'));
return files.map(file => {
const raw = fs.readFileSync(path.join(this.contentDir, file), 'utf8');
const { meta, body } = this.parseFrontMatter(raw); // 解析标题、日期等元信息
return { ...meta, body, slug: file.replace('.md', '') };
});
}
// 2. 用模板渲染每个页面
build() {
const contents = this.readContents();
const template = fs.readFileSync(path.join(this.templateDir, 'post.ejs'), 'utf8');
contents.forEach(post => {
const html = ejs.render(template, { post, site: { title: '尧图建站' } });
const outPath = path.join(this.outputDir, post.slug + '.html');
fs.writeFileSync(outPath, html);
console.log('生成:' + outPath);
});
// 额外生成列表页、首页
this.buildIndex(contents);
}
}
new StaticSiteGenerator({
templateDir: './templates',
contentDir: './content',
outputDir: './dist'
}).build();
这段代码不到 40 行,却涵盖了 SSG 的全部核心逻辑。尧图曾用类似的最简 SSG 给一个小型文档站点做构建,500 篇文档 2 秒生成完毕,部署到 CDN 后首屏加载 200ms 以内。SSG 的魅力就在于此——把运行时成本压缩到零,所有复杂度都留给了构建时。
二、模板继承与布局复用
真实站点的页面有大量重复结构(头部导航、侧边栏、页脚),如果每个模板都复制一遍,改一次页脚要改几十个文件。SSG 借鉴动态框架的"模板继承"机制:定义一个基础布局(layout),子模板只填"内容区",公共部分由基础布局统一管理。修改页脚只需改基础布局一处。
<!-- base.ejs 基础布局 -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title><%= page.title %> | <%= site.title %></title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<header><%= site.nav %></header>
<main>
<%- body %> <!-- 子模板内容插入这里 -->
</main>
<footer><%= site.footer %></footer>
</body>
</html>
<!-- post.ejs 文章模板(继承 base) -->
<% layout('base') %>
<article class="post">
<h1><%= post.title %></h1>
<time><%= post.date %></time>
<%- post.body %>
</article>
除了模板继承,SSG 还提供"局部模板"(partial/partials/include)机制,把可复用的小块(评论组件、分页器、文章卡片)拆成独立文件,在任意模板里 <% include('pagination') %> 引入。尧图在做企业官网时,把"案例卡片""服务卡片""团队介绍卡片"都拆成 partial,30 多个页面共用一套组件库,改样式只动一处。
三、增量构建与缓存机制
站点规模一大,"全量构建"就慢得难以忍受——1000 篇文章的全量构建可能要 30 秒,每写一篇都等 30 秒谁也受不了。增量构建的思路是:只重新渲染"内容有变化的页面",没变的直接用上次结果。判断变化靠"文件指纹"(hash 值)或"修改时间",只有指纹变了才重新渲染。
// 增量构建:只重新生成有变化的页面
class IncrementalBuilder extends StaticSiteGenerator {
constructor(config) {
super(config);
this.cacheFile = path.join(this.outputDir, '.cache.json');
this.cache = this.loadCache(); // { 'post-123.md': 'abc123hash' }
}
build() {
const contents = this.readContents();
let changed = 0;
contents.forEach(post => {
const hash = this.md5(post.body + JSON.stringify(post.meta));
if (this.cache[post.slug] === hash) {
return; // 内容没变,跳过
}
// 内容变了,重新渲染
const html = this.renderTemplate('post.ejs', { post });
fs.writeFileSync(path.join(this.outputDir, post.slug + '.html'), html);
this.cache[post.slug] = hash;
changed++;
});
this.saveCache();
console.log(`本次构建:${changed} 个页面更新,${contents.length - changed} 个跳过`);
// 列表页、首页依赖所有文章,任何一篇变都要重建
if (changed > 0) {
this.buildIndex(contents);
this.buildArchive(contents);
}
}
}
增量构建有个难点:列表页、标签页这类"聚合页"依赖多个内容文件,任意一篇变更都要重建。进阶方案是建立"依赖图"——记录每个输出页依赖哪些输入文件,某文件变更时只重建依赖它的页面。Hugo 用内容哈希实现增量,1000 篇文章的增量构建只要 2 秒。尧图实践:开发时用 --watch 模式监听文件变更,写完一篇保存即秒级预览,体验和动态网站几乎无差。
SSG 的本质是"用构建时间换运行时间"——把本该在服务器上做的事提前做完,换来极致的访问速度和安全性。理解了模板、数据、输出三要素,掌握了模板继承与增量构建,你就能用任何 SSG 工具快速搭建出高性能站点。后续几篇我们将动手实践 Markdown 转静态站、前端性能优化、无后端方案与 Serverless 部署,把轻量化站点开发的全套技能打通。