服务端区块
本页内容
有时候,一个原本静态的页面上会有某个区域需要每次请求都拿到最新数据 —— 缓存博客文章下方的评论、商品页上的库存标签。服务端区块让页面保持静态,只在有人访问时把那一块放到服务端渲染。
1. 写一个区块
区块是放在 src/islands/ 下的片段模块 —— 一个普通的 .js 或 .ts 文件(不是 .ht.js,因为区块是片段而非页面)。思路和 sitelo 其他地方一样:一个返回 HTML 的函数。
export default async function comments({ props, request }) {
const comments = await fetchComments(props.postId)
return `<ul>${comments.map((c) => `<li>${c.text}</li>`).join('')}</ul>`
}它接收 { name, props, request },并且必须返回一个 HTML 字符串。区块模块只在服务端运行 —— src/ 下未被引用的代码永远不会进入浏览器。
2. 放进页面
从 sitelo/islands 引入 island()。静态构建会发布回退 HTML;props 会被嵌进占位元素,所以请让它们保持精简且不含机密。
import { island } from 'sitelo/islands'
export default ({ params }) => `
<html>
<body>
<article>…静态内容…</article>
${island('comments', { postId: params.slug }, '<p>正在加载评论…</p>')}
<script type="module" src="/islands.js"></script>
</body>
</html>
`import { html, body, article, script } from 'javascript-to-html'
import { island } from 'sitelo/islands'
export default ({ params }) =>
html(
body(
article('…静态内容…'),
island('comments', { postId: params.slug }, '<p>正在加载评论…</p>'),
script({ type: 'module', src: '/islands.js' }),
),
)import { island } from 'sitelo/islands'
export default function Post({ params }) {
return (
<html>
<body>
<article>…静态内容…</article>
{island('comments', { postId: params.slug }, '<p>正在加载评论…</p>')}
<script type="module" src="/islands.js" />
</body>
</html>
)
}3. 加上客户端加载器
一个很小的脚本会取回每个渲染好的片段并替换进去。它走的是常规资源流水线,所以只需要一个普通的 src/islands.js 入口:
import { mountIslands } from 'sitelo/islands/client'
mountIslands()在 sitelo(dev)和 sitelo preview 中这已经能用 —— 两者都会从 src/islands/ 在 /_sitelo/islands/<name> 提供区块。preview 加载原生 .js / .mjs 模块(与 Node 宿主一致);TypeScript 区块在开发时由 Vite 支持。
生产环境
你的静态托管继续提供页面。在任何能运行服务端代码的地方挂一个小处理器 —— Node、serverless 或边缘函数 —— 它就会渲染同样的区块模块。想要包含可运行 Node 宿主以及 Netlify、Vercel 模板的完整示例,请看服务端区块示例。
// 例如 Node 服务器,或 serverless/边缘函数
import { createIslandsHandler } from 'sitelo/islands/server'
const handleIslands = createIslandsHandler({
islands: {
comments: () => import('./src/islands/comments.js'),
},
})
// Web Request → Response | null(null = 不是区块请求)
export default { fetch: (request) => handleIslands(request) }在原生 Node http 或 express 上,请改用 createIslandsNodeHandler(options) —— 选项相同,签名为 (req, res, next)。可以用 createIslandsFromDirectory 自动接入 src/islands/ 下的每个 .js / .mjs 模块。如果加载器要访问其他源或路径,请传入 mountIslands({ endpoint: 'https://api.example.com/islands' }),并与处理器的 endpoint 选项保持一致。
稳定性清单
island()/mountIslands()/createIslandsHandler/createIslandsNodeHandler—— 公开 API,视为稳定sitelo和sitelo preview都会从src/islands/提供/_sitelo/islandscreateIslandsFromDirectory—— Node 宿主与 preview 共用同一份映射(原生.js/.mjs/.cjs)- 服务端区块示例 中的宿主模板:Node 的
server.js、Netlify 函数 + rewrite、Vercel serverless + rewrite - props 保持精简且不含机密(GET 查询串)—— 这是有意为之;机密请在区块内部、在服务端获取
- props 来自客户端 —— 请校验它们,或用
SITELO_ISLANDS_SECRET对其签名
加载策略
默认情况下每个区块都会在页面加载时立刻取数据,所以有八个区块的页面会在首屏绘制期间发出八个并发请求。用 when 把那些不会立刻看到的推迟掉。
// 页面一加载就取——默认行为。
island('cart', { id }, '<p>…</p>')
// 等待空闲回调。
island('recommendations', { id }, '<p>…</p>', { when: 'idle' })
// 等到滚动进入视口。
island('comments', { postId }, '<p>正在加载评论…</p>', {
when: 'visible',
rootMargin: '400px', // 提前 400px 开始加载
})'load'(默认)—— 立即执行,与其他所有区块一起'idle'—— 在requestIdleCallback时执行(不支持时回退到定时器)'visible'—— 滚动进入视口时,通过 IntersectionObserver 触发
rootMargin 只对 'visible' 生效,默认为 '200px'。区块也会超时结束,而不会一直转圈:
mountIslands({
timeout: 5000, // 每个区块;0 表示禁用。默认 10000
rootMargin: '300px', // `when: 'visible'` 区块的默认值
})mountIslands() 会在「立即加载」的区块稳定后就完成 —— 被推迟的区块稍后自行加载,并且刻意不被等待。失败或超时的区块会保留它的回退 HTML。
props 是不可信输入
区块的 props 来自客户端。它们被嵌进页面、随请求回传,任何人都可以先改动它们:
GET /_sitelo/islands/profile?props={"userId":"someone-else"}请把区块收到的 props 完全当作查询参数看待 —— 校验它们,并且绝不要用它们去取访问者本就无权查看的数据。
当 props 会决定取用特权数据时,请对其签名。设置一个 secret,sitelo 就会在构建时为每个占位元素签名,并以 403 拒绝其他一切:
SITELO_ISLANDS_SECRET=$(openssl rand -hex 32) sitelo buildsitelo、sitelo preview 和 createIslandsHandler 读取的是同一个变量,因此开发、预览与生产保持一致。请给生产宿主配置同样的 secret。更想在代码里设置?
import { configureIslands } from 'sitelo/islands'
configureIslands({ secret: process.env.MY_SECRET })签名是对区块名称及其 props 计算的 HMAC-SHA256,因此为某个区块签发的签名无法重放到另一个区块上。签名只能证明 props 来自你的构建 —— 它并不隐藏内容,所以 props 仍然不能包含机密。没有 secret 时,props 会被原样接受,校验完全由你的区块模块负责。
值得知道的细节
- 没有部署区块端点?那就只会保留回退 HTML —— 页面会优雅降级。
- 请求是
GET,props 放在查询串里,因此响应可被缓存 —— 如果希望 CDN 短暂保留这些片段,请设置处理器的cacheControl选项。 - 加载期间占位元素带有
data-sitelo-island-state="loading"(随后是loaded或error)—— 写 CSS 时很好用。