为什么需要框架?从白物集看 Astro 的选择
上周我们用纯 HTML + CSS 做了一个个人主页,代码整洁、页面美观。但你有没有想过——如果网站有几十个页面、需要从数据库读取数据、还要处理用户登录和搜索,纯手写 HTML 还能撑住吗?
答案是撑不住。这就是框架存在的理由。
纯手写的三条死线
先看三个具体问题,每个都是真实项目中踩过的坑。
问题一:重复代码爆炸。 每个页面都有导航栏、页脚、<head> 标签。纯 HTML 的做法是复制粘贴——导航栏改一个链接,所有页面都得手动改一遍。10个页面勉强能管,100个页面直接崩。
问题二:数据怎么塞进 HTML? 个人主页的内容是写死的。但一个内容网站的文章、标题、摘要都来自数据库。纯 HTML 没法在页面里写 fetch 然后把数据填进 <div>——你只能在浏览器端用 JavaScript 做,意味着白屏、闪烁、SEO 等于零。
问题三:构建和部署靠手动。 没有压缩、没有代码分割、没有热更新。改一行 CSS 要手动刷新浏览器。
白物集在第一阶段也经历过这些问题。早期原型用静态 HTML 配合客户端 JS 渲染,遇到的就是这三条死线。后来迁移到了 Astro。
Astro 做了什么
Astro 是一个静态站点生成器(SSG)+ 服务端渲染(SSR)框架。它不是「又一个 React」,而是换了一条思路。
白物集的首页 src/pages/index.astro 是典型的 Astro 页面:
---
import BaseLayout from '../layouts/BaseLayout.astro';
import { getArticles } from '../lib/api.js';
import { AI_CATEGORIES, APPLE_CATEGORIES, TECH_CATEGORIES } from '../constants';
const { articles: all } = await getArticles({ limit: 100 });
const aiArticles = published
.filter((a) => AI_CATEGORIES.includes(a.category))
.sort((a, b) => new Date(b.published_at) - new Date(a.published_at))
.slice(0, 3);
---
<BaseLayout title="首页">
<section>
<!-- 模板里直接写 HTML,数据已经是渲染好的 -->
{aiArticles.map(a => (
<a href={`/articles/${a.id}`}>{a.title}</a>
))}
</section>
</BaseLayout>
注意上面 --- 之间的部分。这段代码在服务器上运行,从 API 获取数据,把结果填入模板,然后返回完整的 HTML。浏览器收到的不再是一个空壳加一堆 JS 脚本——它直接看到了文章标题。
这是 Astro 最核心的设计:零 JS 默认输出。页面模板中的 {#each} 和 {if} 在构建时或请求时就已经计算完毕,不会把循环逻辑打包成 JS 发给浏览器。
组件化:解决重复代码
Astro 的组件系统解决了导航栏和页脚重复的问题。
白物集的 BaseLayout.astro 定义了所有页面的公共结构:
---
import NavBar from '../components/NavBar.astro';
---
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{title} · 白物集</title>
</head>
<body>
<NavBar />
<main class="wrapper">
<slot />
</main>
<footer>...</footer>
</body>
</html>
每个页面只需要写内容区的代码,<slot /> 就是 Astro 的插槽机制——页面内容自动填入这个位置。导航栏、页脚、SEO 标签全部由布局组件统一管理。
改导航栏链接?只改 NavBar.astro 一个文件,所有页面自动生效。
SSR API 客户端:白物集的双层缓存
说到数据,白物集没有在前端直接调 Supabase——中间隔了一层 Express API + Astro SSR 缓存层。
看一下 src/lib/api.js 的核心逻辑:
const cache = new Map();
const TTL = {
list: 60_000,
detail: 120_000,
};
function cached(key, ttl, fetcher) {
const now = Date.now();
const entry = cache.get(key);
if (entry && (now - entry.time) < ttl) {
return entry.data;
}
return fetcher().then(data => {
cache.set(key, { data, time: now });
return data;
});
}
export async function getArticles(opts = {}) {
const p = new URLSearchParams({ limit: String(opts.limit || 50) });
const cacheKey = '/articles?' + p.toString();
return cached(cacheKey, TTL.list, () => fetch('http://127.0.0.1:3001/api/v1' + cacheKey));
}
这个设计回答了「框架能做什么」这个问题:框架让你有能力在服务器端加一层缓存。
纯静态 HTML 做不到——每篇文章一更新,你得重新生成所有 HTML。纯客户端 JS 也做不到——每个用户打开页面都要发一次 API 请求。
Astro SSR 配合进程内缓存:60 秒内相同请求直接走内存,下一页访问零延迟。
Astro 的 SSR 模式:一次迁移带来的改变
白物集最初是纯静态站点(SSG),每次发文章要重新构建、部署、重启。后来切换到 @astrojs/node 的 SSR 模式。
配置 astro.config.mjs 里就几行:
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';
export default defineConfig({
output: 'server',
adapter: node({ mode: 'standalone' }),
});
output: 'server' 告诉 Astro:别在构建时生成所有 HTML 了,在服务器上运行时按需渲染。文章发布后,API 里有了新数据,下一个请求就已经能看到,不需要重新构建。
但 SSR 的陷阱也随之而来。白物集踩过一个典型坑:首页按 b.id - a.id(数据库自增 ID)排序,旧文章 ID=100~144 远大于新 API 文章 ID=40,新文章被 .slice(0, 3) 切掉,在首页消失了。排查发现是排序键没用 published_at。修复后改成了:
.sort((a, b) => new Date(b.published_at) - new Date(a.published_at))
这种问题在纯静态 HTML 时不会出现——因为所有页面都是手动维护的。但用了框架就要理解它的数据流。框架帮你解决了重复劳动,代价是你必须搞清楚数据从哪里来、经过什么变换、最终怎么出现在页面上。
那为什么不直接用 React/Vue?
很多人觉得「学框架 = 学 React 或 Vue」。Astro 的不同在于:它不强制你用任何 UI 框架。
白物集的导航栏、文章列表、频道页全部是纯 .astro 文件——没有 JSX,没有 useState,没有虚拟 DOM。Astro 组件就是 HTML 模板加服务器端逻辑。只有在需要客户端交互的地方(比如主题切换按钮),才写一个轻量的 <script> 标签。
这意味着:你可以先学 Astro 的组件语法(和 HTML 几乎一样),而不是一上来就被 hooks、响应式、状态管理搞得头晕。
总结
框架不是「给高手用的工具」——恰恰相反,框架是为了让你不需要重复造轮子、不需要手动拼接每一块砖。
Astro 的选择对白物集意味着: - 导航栏、页脚写一次,全局复用(组件化) - 文章数据从 API 读取,服务器端渲染成 HTML,用户打开直接看到内容(SSR) - 60 秒内相同请求零延迟(进程缓存) - 发布文章不需要重新构建(SSR 模式)
下一篇我们打开编辑器,用 Astro 的组件语法写第一个页面——从 Layout 到 NavBar 到手把手搭一个频道页,你会看到框架到底省了多少代码。