深入浅出 solid.js 源码 (二十三)—— SSR

401 阅读3分钟

这是我参与「掘金日新计划 · 8 月更文挑战」的第23天,点击查看活动详情

到此为止我们看的都是从 client 切入的,其中一部分逻辑是 client render 和 server render 共有的,另一部分是 client 特有的,对于服务端渲染的内容目前还未涉及,接下来就来看服务器渲染相关的内容。

服务器渲染就是在服务器上组装 html 内容,之后直接返回完整的页面,与之相反的概念是客户端渲染,客户端渲染时服务器只返回一个空的页面,在前端 js 逻辑中请求数据并组装页面。现代网站使用服务器渲染通常会伴随着同构处理,同构即浏览器端和服务器端使用同一种技术实现,这里的服务器使用 nodejs 环境,这样浏览器中运行的前端框架也可以在服务器上运行。这样浏览器渲染和服务器渲染两部分工作可以更好地结合起来,具体的过程大概是服务器先初步组装页面,填充初始数据,这样可以提升首屏加载效率,浏览器收到的是一个相当于提前已经设置过数据的页面,之后接着后面的流程处理,由于之前在服务器上面使用的是同样的框架,因此这里可以无缝连接。

同构的应用和普通应用相比需要进行一些改造,在服务器上,框架需要能够把内容渲染成普通的字符串,这样才可以通过 http 传输回来。另一部分是在客户端上,由于内容已经在服务器填充过了,这里不是重新从头开始渲染,而是把客户端数据注入,假装是从客户端渲染的,这样可以继续后面的行为。这里是相当于把服务器返回的普通字符串转为能融入到浏览器框架中的特殊结构,这个过程在 SSR 中有一个专业的词叫 hydrate。

solid 在服务器上的处理流程是从 server 入口进入的,这部分我们下一篇文章展开分析,这一节先来看一下 hydrate 的实现,hydrate 的过程取代的是 CSR 场景下的 render 过程,这里的用法也是一样的,我们来看一下 hydrate 的源码:

export const hydrate: typeof hydrateCore = (...args) => {
  enableHydration();
  return hydrateCore(...args);
};

enableHydration 就是设置了一下 hydrationEnabled 变量值,这个变量会在 createComponent 函数中使用到:

export function createComponent<T>(Comp: Component<T>, props: T): JSX.Element {
  if (hydrationEnabled) {
    if (sharedConfig.context) {
      const c = sharedConfig.context;
      setHydrateContext(nextHydrateContext());
      const r = "_SOLID_DEV_"
        ? devComponent(Comp, props || ({} as T))
        : untrack(() => Comp(props || ({} as T)));
      setHydrateContext(c);
      return r;
    }
  }
  if ("_SOLID_DEV_") return devComponent(Comp, props || ({} as T));
  return untrack(() => Comp(props || ({} as T)));
}

之前我们跳过了这部分逻辑,这里暂时不急着看,先来看 hydrateCore,它的实现依旧位于 dom-expressions 中:

export function hydrate(code, element, options = {}) {
  sharedConfig.completed = globalThis._$HY.completed;
  sharedConfig.events = globalThis._$HY.events;
  sharedConfig.load = globalThis._$HY.load;
  sharedConfig.gather = root => gatherHydratable(element, root);
  sharedConfig.registry = new Map();
  sharedConfig.context = {
    id: options.renderId || "",
    count: 0
  };
  gatherHydratable(element, options.renderId);
  const dispose = render(code, element, [...element.childNodes]);
  sharedConfig.context = null;
  return dispose;
}

这里首先就是初始化 sharedConfig,这个对象上面存储的就是服务器渲染浏览器渲染共享的内容,服务器渲染时会把一些需要传递的数据挂到全局环境下,这里通过 globalThis._$HY 和 [data-hk] 来获取这些服务器注入的数据,之后再调用 render 方法。

在 render 中会调用 createComponent,这里就有区别了,如果是 hydrate 的调用方式,这里会去处理 context,在 solid 的源码中,还可以搜到很多 sharedConfig 的引用,这些逻辑都是针对 hydrate 场景特有的,具体的处理感兴趣可以深入阅读,这里和其他同构框架实现的思路是一样的,就不做过多展开了。