服务端区块

有时候,一个原本静态的页面上会有某个区域需要每次请求都拿到最新数据 —— 缓存博客文章下方的评论、商品页上的库存标签。服务端区块让页面保持静态,只在有人访问时把那一块放到服务端渲染。

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 { 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' }),
    ),
  )

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 选项保持一致。

稳定性清单

加载策略

默认情况下每个区块都会在页面加载时立刻取数据,所以有八个区块的页面会在首屏绘制期间发出八个并发请求。用 when 把那些不会立刻看到的推迟掉。

// 页面一加载就取——默认行为。
island('cart', { id }, '<p>…</p>')

// 等待空闲回调。
island('recommendations', { id }, '<p>…</p>', { when: 'idle' })

// 等到滚动进入视口。
island('comments', { postId }, '<p>正在加载评论…</p>', {
  when: 'visible',
  rootMargin: '400px',   // 提前 400px 开始加载
})

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 build

sitelositelo previewcreateIslandsHandler 读取的是同一个变量,因此开发、预览与生产保持一致。请给生产宿主配置同样的 secret。更想在代码里设置?

import { configureIslands } from 'sitelo/islands'

configureIslands({ secret: process.env.MY_SECRET })

签名是对区块名称及其 props 计算的 HMAC-SHA256,因此为某个区块签发的签名无法重放到另一个区块上。签名只能证明 props 来自你的构建 —— 它并不隐藏内容,所以 props 仍然不能包含机密。没有 secret 时,props 会被原样接受,校验完全由你的区块模块负责。

值得知道的细节