首页
/ Astro SSR 示例实战:用 @astrojs/node 适配器构建服务端渲染电商 Demo

Astro SSR 示例实战:用 @astrojs/node 适配器构建服务端渲染电商 Demo

2026-09-04 22:12:51作者:温艾琴Wonderful

本文以 Astro 官方仓库中的 examples/ssr 示例为主体,完整讲解如何用 output: 'server' + @astrojs/node 适配器搭建一个服务端渲染(SSR)站点:包括 API 路由、动态路由、Cookie 会话、表单登录(同步/渐进式两种写法)以及 Svelte 客户端岛之间的组件通信。读完本文,你能独立复制该示例、理解每个页面与服务端 API 的数据流,并掌握从本地开发到 dist/server/ 生产部署的完整命令链。

示例定位与快速开始

该示例是一个名为 "Online Store" 的小型在线商店,官方 README(examples/ssr/README.md)说明其核心目标:@astrojs/node 适配器配合 @astrojs/svelte 集成来展示服务端渲染能力。你可以直接从 Astro 仓库创建同模板的新项目:

npm create astro@latest -- --template ssr

也可以使用 StackBlitz、CodeSandbox 或 GitHub Codespaces 在线打开该示例(README 中提供了对应的入口徽章,无需本地克隆即可运行)。

该示例 examples/ssr/package.json 声明了运行前提与依赖版本,适用条件需要注意:

{
  "engines": { "node": ">=22.12.0" },
  "dependencies": {
    "@astrojs/node": "^11.1.5",
    "@astrojs/svelte": "^9.0.1",
    "astro": "^7.2.10",
    "svelte": "^5.53.5"
  }
}

即要求 Node.js 不低于 22.12.0,Astro 7.x 搭配 @astrojs/node 11.x 与 Svelte 5。

项目结构

README 给出的目录树如下,这也是官方推荐的目录组织方式:

/
├── public/
│   ├── favicon.ico
│   ├── favicon.svg
│   └── images/
├── src/
│   ├── components/
│   ├── models/
│   ├── pages/
│   │   ├── api/
│   │   └── products/
│   ├── styles/
│   └── api.ts
├── astro.config.mjs
├── package.json
└── tsconfig.json

三个关键目录的职责(README 原意 + 实际文件对应):

  • src/pages/:Astro 在此目录寻找 .astro.md 文件,每个文件按文件名暴露为一个路由;动态路由如 products/[id].astro 用于渲染单个商品页。由于本项目是 SSR 模式,src/pages/ 下的 .ts 文件同样会成为服务端 API 路由(下一节详述)。
  • src/components/:放 Astro 组件或框架组件(如 Svelte 组件),没有任何特殊约定,只是团队惯例。
  • public/:存放图片等静态资源,例如商品图 public/images/products/ 下的 cereal.jpgyogurt.jpg 等。

另有 src/models/src/api.ts 承载数据层与请求封装,属于本示例特有的业务组织,README 未展开,下面结合源码说明。

核心配置:output: 'server' + Node 适配器

examples/ssr/astro.config.mjs 全文只有十几行,却是整个示例的开关:

import node from '@astrojs/node';
import svelte from '@astrojs/svelte';
import { defineConfig } from 'astro/config';

export default defineConfig({
	output: 'server',
	adapter: node({
		mode: 'standalone',
	}),
	integrations: [svelte()],
});
  • output: 'server':让 Astro 以服务端渲染为主模式,页面按需(on demand)渲染,而不是构建期生成全部静态 HTML。
  • adapter: node({ mode: 'standalone' }):将应用打包为独立的 Node 服务端。standalone 模式会把依赖打包进产物,便于在最小化 Node 环境中直接启动。
  • integrations: [svelte()]:注册 Svelte 集成,使 .svelte 文件可作为客户端组件(island)被 client:* 指令激活。

README 中的原话是:“本项目使用 @astrojs/node 适配器配合 output: 'server' 按需渲染页面,并从 src/pages/api/ 暴露 API 路由。”

TypeScript 方面,examples/ssr/tsconfig.json 继承 astro/tsconfigs/strict 并包含 .astro/types.d.ts,保证页面 props、Astro.paramsAPIContext 等有类型提示。

命令速查

README 给出的全部命令(在项目根目录的终端执行):

Command Action
npm install Installs dependencies
npm run dev Starts local dev server at localhost:4321
npm run build Build your production site to ./dist/
npm run preview Preview your build locally, before deploying
npm run server Run the built Node server from ./dist/server/
npm run astro ... Run CLI commands like astro add, astro check
npm run astro -- --help Get help using the Astro CLI

注意 server 脚本的实际内容(examples/ssr/package.json 第 14 行)是 node dist/server/entry.mjs——这正是 @astrojs/nodestandalone 模式下产物的入口约定:构建后直接用一个 Node 进程启动,不需要额外框架进程。

服务端 API 路由:src/pages/api/

SSR 模式下 src/pages/ 中任何 .ts 文件都可以通过导出 HTTP 方法函数(GET/POST…)成为服务端 API 端点。本示例有三个:

GET /api/products

examples/ssr/src/pages/api/products.ts

import { products } from '../../models/db';

export function GET() {
	return new Response(JSON.stringify(products));
}

直接从内存数据模型返回全部商品 JSON。

GET /api/products/[id](动态路由 + 错误处理)

examples/ssr/src/pages/api/products/[id].ts

import type { APIContext } from 'astro';
import { productMap } from '../../../models/db';

export function GET({ params }: APIContext) {
	const id = Number(params.id);
	if (productMap.has(id)) {
		const product = productMap.get(id);
		return new Response(JSON.stringify(product));
	} else {
		return new Response(null, {
			status: 400,
			statusText: 'Not found',
		});
	}
}

要点:通过 APIContextparams 拿到 URL 中的 [id] 动态段;未命中时用 Web Response 构造 400 响应。

GET/POST /api/cart(Cookie 会话 + 内存存储)

examples/ssr/src/pages/api/cart.ts

import { userCartItems } from '../../models/session';

export function GET({ cookies }: APIContext) {
	let userId = cookies.get('user-id')?.value;
	if (!userId || !userCartItems.has(userId)) {
		return Response.json({ items: [] });
	}
	let items = userCartItems.get(userId);
	return Response.json({ items: Array.from(items.values()) });
}

export async function POST({ cookies, request }: APIContext) {
	const item = await request.json();
	let userId = cookies.get('user-id')?.value;
	if (!userCartItems.has(userId)) {
		userCartItems.set(userId, new Map());
	}
	let cart = userCartItems.get(userId);
	if (cart.has(item.id)) {
		cart.get(item.id).count++;
	} else {
		cart.set(item.id, { id: item.id, name: item.name, count: 1 });
	}
	return Response.json({ ok: true });
}

会话来源是 user-id Cookie,存储是 examples/ssr/src/models/session.ts 里的一行 Map——源码注释明确写着 // Normally this would be in a database.,即这是演示用途的进程内会话,生产环境应替换为真实数据库/缓存。

数据模型侧,examples/ssr/src/models/db.tsexamples/ssr/src/models/db.json(4 个商品:Cereal、Yogurt、Rolled Oats、Muffins,含 idnamepriceimage 字段)转为数组 + Map 双索引,分别供列表与按 ID 查询使用。

SSR 页面:在服务端发起请求

SSR 页面的核心能力是在服务端(前端拿到 HTML 之前)发起请求。本示例把这类请求统一封装在 examples/ssr/src/api.ts。其中 getJson 封装(第 20–37 行)值得逐行看:

async function getJson<T>(incomingReq: Request, endpoint: string): Promise<T> {
	const origin = new URL(incomingReq.url).origin;
	const response = await fetch(`${origin}${endpoint}`, {
		credentials: 'same-origin',
		headers: incomingReq.headers,
	});
	// ... 错误处理略
	return response.json() as Promise<T>;
}

三个关键设计:

  1. 基于 Astro.request 的 origin 拼接:服务端 fetch 没有浏览器默认 origin,因此从入站请求 URL 提取 origin,再拼出 /api/... 的绝对地址;
  2. 透传入站请求头(含 Cookie)headers: incomingReq.headers 让服务端子请求能带上浏览器 Cookie,从而读到 user-id 会话——这是“页面 SSR 时即可拿到用户购物车”的关键;
  3. 对 Network 错误的归一化DOMException/TypeError(典型如 fetch 连接失败)被转成带端点名与 statusText 的业务错误。

在此基础上导出 getProductsgetProductgetUsergetCart 四个服务端取数函数。

首页:列表 SSR

examples/ssr/src/pages/index.astro 的 frontmatter 只有两行业务代码:

const products = await getProducts(Astro.request);

页面在服务端即完成取数,再把 products 传给 ProductListing 组件渲染商品卡片(图片、名称、价格),标题通过命名 slot(slot="title")注入。浏览器拿到的就是完整 HTML,不依赖 JS 即可看到商品列表。

动态路由商品页:products/[id].astro

examples/ssr/src/pages/products/[id].astro

const id = Number(Astro.params.id);
const product = await getProduct(Astro.request, id);

Astro.params.id 提供 URL 中的动态段,服务端取数后渲染 <title>{product.name} | Online Store</title> 与商品图。页面上的“加入购物车”按钮则是唯一的客户端岛:

<AddToCart client:idle id={id} name={product.name} />

client:idle 表示浏览器空闲时再水合该 Svelte 组件,SSR 首屏保持纯 HTML。

购物车页:Cookie 守卫 + 服务端重定向

examples/ssr/src/pages/cart.astro 展示了 SSR 独有的服务端访问控制写法:

if (!Astro.cookies.get('user-id')) {
	return Astro.redirect('/');
}
const cart = await getCart(Astro.request);

未登录(无 user-id Cookie)的请求在服务端就被 Astro.redirect('/') 挡回首页,用户永远不会看到购物车页的未授权 HTML;已登录则用 getCart 服务端取数并渲染成表格。

登录:同一流程的两种实现

本示例最有教学价值的部分是 login 功能提供了经典表单 POST渐进式(AJAX)登录两套端点,登录成功后都写入同一个 user-id Cookie(maxAge: 2592000,即 30 天)。

1) 经典表单 POST:login.form.ts

examples/ssr/src/pages/login.form.ts 导出 POST 处理器,读取表单提交后:

cookies.set('user-id', '1', { path: '/', maxAge: 2592000 });
return new Response(null, { status: 301, headers: { Location: '/' } });

设置 Cookie 后以 301 重定向回首页。页面侧 login.astro 中对应的表单是 <form action="/login.form" method="POST">——完全无 JS 也能走通登录。

2) 渐进式登录:login.form.async.ts

examples/ssr/src/pages/login.async.ts 则是 JSON 接口:

export const POST: APIRoute = ({ cookies }: APIContext) => {
	cookies.set('user-id', '1', { path: '/', maxAge: 2592000 });
	return Response.json({ ok: true, user: 1 });
};

login.astro 内联的 is:inline 脚本监听表单 submite.preventDefault()fetch('/login.form.async', { method: 'POST', body: JSON.stringify(data) }),成功后提示 3 秒跳转。两种写法对照,正好覆盖了“表单端点(form endpoint)”与“REST 风格端点”两种服务端交互范式。

客户端岛与跨组件通信

除了 AddToCart,导航栏 Header.astro 也演示了“SSR 数据 + 客户端岛”的组合:

const cart = await getCart(Astro.request);
const cartCount = cart.items.reduce((sum, item) => sum + item.count, 0);
<!-- ... -->
<Cart client:idle count={cartCount} />

首屏的购物车数量在服务端算好并写入 <Cart> 岛的初始 prop,避免客户端二次请求。两个 Svelte 岛之间的联动则通过浏览器自定义事件解耦:

  • AddToCart.svelte:点击按钮后调用 api.ts 第 55–69 行的 addToUserCart(id, name)(客户端 fetch POST 到 /api/cart,带 credentials: 'same-origin' 与 JSON body),随后 window.dispatchEvent(new CustomEvent('add-to-cart', { detail: id }))
  • Cart.svelte:用 <svelte:window onadd-to-cart={onAddToCart}/> 监听该事件,本地 count++ 更新徽标,并记录已加入的 id 集合。

也就是说:加购请求由客户端岛直接打到服务端 API,UI 更新走 window 自定义事件广播,两岛之间没有任何直接依赖——这是 island 架构下典型的“事件总线”式解耦写法。

其余通用组件 Container.astro(可替换标签名的布局容器)与 TextDecorationSkip.astro(逐词下划线的装饰标题)属于演示样式能力的配角组件,与 SSR 机制无关,可按需忽略。

适用前提与限制

结合源码可以归纳使用该示例时的注意项:

  1. Node 版本engines 要求 >=22.12.0
  2. 会话是进程内存userCartItemsMap,重启进程即清空、多实例部署不共享,源码注释已声明这是示例做法;
  3. 登录是假认证:两个登录端点都不校验密码,直接写死 user-id: '1',只用于演示 Cookie 写入与重定向,不可照搬到生产;
  4. standalone 产物npm run build 后用 npm run server(即 node dist/server/entry.mjs)启动,是 @astrojs/node standalone 模式的标准部署形态。

延伸学习

示例 README 建议读者带着本示例去阅读 Astro 官方文档中 output: 'server'@astrojs/node 适配器与 Svelte 集成的章节,并可以按 examples/ssr/package.json 中的 npm run astro -- --help 探索 CLI 能力。若想对照静态站点写法,可参考同仓库的 examples/minimalexamples/blog 示例;若关心测试,仓库根级维护了 Vitest 与 Playwright 两套体系(如 packages/astro/e2e/server-islands.test.ts 覆盖了服务端岛屿的水合行为),可作为 SSR 行为的验证参考。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
889
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
980
502
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384