这是我参与「掘金日新计划 · 8 月更文挑战」的第24天,点击查看活动详情
这一节来看服务器上运行的 solid.js,在 solid 中提供了一个 isServer 变量,对于一些只能在浏览器或服务器执行的逻辑我们可以使用 isServer 做一个判断,这样方便处理。在源码中可以看到 isServer 实际上是一个常量,在 client 导出的代码里就是 false,在 server 中就是 true,这件事情是在编译前就确定好的,因此在 solid 编译的过程中会直接根据环境移除不必要的代码逻辑。
接下来就进入入口了,server 与 client 的区别体现在两部分,一部分是编译结果不同,另一部分是执行逻辑不同。
我们先来看编译结果,同样来看之前的计数器逻辑,在 server 的环境中编译的结果是这样的:
import { render, createComponent, ssr, ssrHydrationKey, escape } from 'solid-js/web';
import { createSignal } from 'solid-js';
const _tmpl$ = ["<div", "><!--#-->", "<!--/--><!--#-->", "<!--/--><div>+</div><div>reset</div><div>-</div></div>"];
function Count(props) {
const [num, setNum] = createSignal(0);
return ssr(_tmpl$, ssrHydrationKey(), escape(props.name), escape(num()));
}
render(() => createComponent(Count, {}), document.getElementById("app"), {
name: 333
});
这里的组件会返回 ssr 函数的调用,这时实际执行的逻辑,位于 dom-expressions 的 server 文件中:
export function ssr(t, ...nodes) {
if (nodes.length) {
let result = "";
for (let i = 0; i < t.length; i++) {
result += t[i];
const node = nodes[i];
if (node !== undefined) result += resolveSSRNode(node);
}
t = result;
}
return { t };
}
可以看到这里就是一个字符串拼接,它的第一个参数是模板,后面的参数是内容,这里要做的就是把内容插入到模板中,具体插入的是什么,这个是在编译阶段就处理好了,这里详细的逻辑可以查看 solid babel 编译器的 ssr 部分,在此不做详细展开了,这里来看一下本例中涉及到的函数。
第一个是 ssrHydrationKey,这个东西上一节见过,就是 data-hk,这里为 html 标签添加了一个 data-hk 属性,在 html 中 data-xxx 属性是可以获取的,在上一节中我们在 hydrate 阶段会去取这个 data-hk 的内容,这个内容就是此时设置的,他的值为 ${[hydrate.id](<http://hydrate.id/>)}${hydrate.count++} 这里的 hydrate 变量即为 sharedConfig.context,因此只要 sharedConfig.context 可以在前后端共享,通过这里的 data-hk 标记,就可以识别出对应的每个组件的渲染位置,以此实现 hydrate 逻辑。
另外后面的就是 escape,这个非常好理解,就是我们常见的 escapeHTML 操作,为了防止 XSS 攻击,我们想渲染在页面上的内容都需要处理一下,具体实现没什么特别的地方,这里主要是看它传递的值。经过前面的介绍我们已经知道 ssr 的流程,这一步在服务器上渲染的只是初始值内容,因此这里 num 执行的也是其初始值,这就涉及到 createSignal 的执行逻辑了。
上面提到服务器上使用的执行逻辑是不同的,浏览器中我们需要响应用户行为,因此 createSignal 需要实现响应式系统能力。而服务器上,我们只是需要一个初始值,后面的事情一律不关心,因此我们可以看到 server 下面的 createSignal 实际上只是一个空函数:
export function createSignal<T>(
value: T,
options?: { equals?: false | ((prev: T, next: T) => boolean); name?: string }
): [get: () => T, set: (v: (T extends Function ? never : T) | ((prev: T) => T)) => T] {
return [
() => value as T,
v => {
return (value = typeof v === "function" ? (v as (prev: T) => T)(value) : v);
}
];
}
reactive 文件中的其他逻辑也都差不多,在 server 环境中都只是保留一个空函数占位置。另外编译结果中有关事件处理的部分也全部被移除了,原因也是一样的,服务器上只是组装字符串并不需要响应用户行为,自然没有这些内容,剩下都需要在浏览器中 hydrate 后由浏览器来处理了。
最后就是这里的调用,服务器上也不会去调用 render,通常是调用 renderToString:
export function renderToString(code, options = {}) {
let scripts = "";
sharedConfig.context = {
id: options.renderId || "",
count: 0,
suspense: {},
assets: [],
nonce: options.nonce,
writeResource(id, p, error) {
if (error) return (scripts += `_$HY.set("${id}", ${serializeError(p)});`);
scripts += `_$HY.set("${id}", ${devalue(p)});`;
}
};
let html = injectAssets(sharedConfig.context.assets, resolveSSRNode(escape(code())));
if (scripts.length) html = injectScripts(html, scripts, options.nonce);
return html;
}
都是一些简单的字符串处理和注入逻辑,详细阅读就不展开介绍了,除此之外还有 renderToStream 等其他的渲染方式,限于篇幅这里也不专门去阅读了,如果使用到或者对这里的实现感兴趣可以阅读 renderToStream 函数的实现,有关 solid 服务器渲染的部分本系列文章就介绍这么多。