项目地址
island最近很火, 从deno的fresh框架到兼容多组件的astro
首先他们都是基于ssr的,普通的ssr是页面级注水, island是组件粒度的注水,今天神三元写了个demo帮我们理解island都干了什么
Object.entries(manifest.routes).forEach(([path, mod]) => {
router.get(path, async (ctx, next) => {
function renderFn(data: any) {
ISLAND_PROPS = [];
ENCOUNTER_ISLANDS = [];
const bodyHtml = renderToString(createElement(mod.default, data));
// 渲染模版
// 因为拦截了React.createElement所以将组件信息存储到了上面两个数组里面
// 并且将island加上了一个attribute
const importmap = {
imports: {
react: "https://esm.sh/react@18.2.0",
"react-dom": "https://esm.sh/react-dom@18.2.0",
},
};
const hydrateScript = [
`import { revive } from "/revive";`,
// 这里加载了所有island的组件的代码, 不打包版本的
ENCOUNTER_ISLANDS.map((island) => {
return `import ${island.id.split("/").pop()} from "${island.id}";`;
}).join(""),
// 将组件注水时用到的信息挂在在window上
`window.ISLANDS = {${ENCOUNTER_ISLANDS.map(
({ id }) => `"${id}": ${id.split("/").pop()}`
).join(",")}};`,
`window.ISLAND_PROPS = [${ISLAND_PROPS.map((props) =>
JSON.stringify(props)
)}];`,
// 在这个recvive方法里完成注水,
`revive();`,
].join("");
const template = (head: string, body: string) => {
return `<!DOCTYPE html>
<html lang="en">
<head>
${head}
<script type="importmap">${JSON.stringify(importmap)}</script>
</head>
<body>
${body}
<script type="module">${hydrateScript}</script>
</body>
</html>`;
};
return template("", bodyHtml);
}
ctx.body = await mod.handler(ctx.request, {
render: renderFn,
});
});
});
看一下这个revive
export function revive() {
const islands = document.querySelectorAll("[__island]");
for (let i = 0; i < islands.length; i++) {
const island = islands[i];
// 获取到所有带__island 属性的标签,这些标签是注水的组件
// 然后从window上读取组件进行注水
const [id, index] = island.getAttribute("__island")!.split(":");
hydrate(
// @ts-ignore
createElement(window.ISLANDS[id], window.ISLAND_PROPS[index]),
island
);
}
}
拦截的react方法
React.createElement = (type: ElementType, props: any, ...children: any[]) => {
let matchedIsland: Island | undefined;
if (
typeof type === "function" &&
(matchedIsland = ISLANDS.find(({ component }) => component === type))
) {
ISLAND_PROPS.push(props);
ENCOUNTER_ISLANDS.push(matchedIsland);
// 这样所有的 props 都被记录了
// __island属性用来匹配注水节点
return originCreateElement(
`div`,
{
__island: `${matchedIsland.id}:${ISLAND_PROPS.length - 1}`,
},
originCreateElement(type, props, ...children)
);
}
return originCreateElement(type, props, ...children);
};
流程就是生成模版的同时生成注水数据->挂载数据->选择注水节点->用保存的数据注水
Ps: 因为是demo比较简单,没有注水优先级等更多的功能,