深入浅出 solid.js 源码 (二十四)—— server

363 阅读2分钟

这是我参与「掘金日新计划 · 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 服务器渲染的部分本系列文章就介绍这么多。