本周精读内容是 《在浏览器运行 serverRender》。

这里是效果页，先睹为快：[client-ssr](https://ascoders.github.io/client-ssr/)。

## 1 引言

在服务端 ssr 成为常识的今天，前端 ssr 是我最近的新尝试，效果可以点击上面链接查看。说说前端 ssr 有哪些好处：

1.  不消耗服务器资源。对，不消耗服务器资源，完美的分布式运行！对于百万 UV 甚至更高的页面，服务器成本减少了几十万或者上百万。
2.  前后端分离，首先 ssr 不需要部署服务器，其次前端代码也不需要担心质量问题导致的内存泄露了，同时可以不必时刻注意使用同构的三方库，只需要考虑前端可运行！
3.  不需要后端缓存服务，对于千人千面的复杂页面，对后端 ssr 来说缓存规模庞大的无法计算。
4.  相比后端 ssr，在前端可以绕过复杂的权限系统，同时 http 请求的权限问题也无需关心。
5.  因为第一点，对于不支持后端服务的 github pages 也能做到 ssr。

相对的，缺点是：

1.  需要客户端支持 serviceWorker。
2.  第二次首屏才会生效。后端 ssr 可以做到访问前预缓存 ssr 结果。
3.  可能破坏前端页面状态，因为在同一个环境偷偷执行了一些页面逻辑。不过这个缺点可以通过 web worker 执行 ssr 解决，还在调研中。
4.  service worker 拦截入口 html 风险很高，一旦代码有故障可能导致严重后果，需要提前考虑完备的回滚方案。

像缓存清空时机等问题，前后端 ssr 都会遇到，所以不列在优缺点中。

## 2 精读

本篇精读分享的是前端 ssr 方案具体实现步骤。

我们先了解整体流程：

![](https://img.alicdn.com/imgextra/i4/O1CN01hOmtLc1VaRlyRsTRm_!!6000000002669-2-tps-1616-1598.png)

### service worker 拦截首页

service worker 可以在浏览器尝试请求首屏 html 之前的时机拦截，此时如果 caches 命中，直接将 response 扔给浏览器，那么服务端将完全不会收到请求，完成了最高效的缓存命中。

当然第一次没有缓存，所以在没有命中缓存时，会同步的做两件事：

1.  发送请求，拿到后端返回的 response，扔给浏览器。这是最普通的请求逻辑。
2.  当前端代码 ready 后，postMessage 给浏览器，索要 ssr 内容。

附上代码片段：

```javascript
self.addEventListener("fetch", event => {
  if (
    event.request.mode === "navigate" &&
    event.request.method === "GET" &&
    event.request.headers.get("accept").includes("text/html")
  ) {
    event.respondWith(
      caches.open(SSR_BUNDLE_VERSION).then(cache => {
        return cache.match(event.request).then(response => {
          // 命中缓存，直接返回结果。
          if (response) {
            return response;
          }
          return fetch(event.request).then(response => {
            const newResponse = response.clone();
            return newResponse
              .text()
              .then(text => {
                // 通知浏览器，执行 ssr 并且返回内容。
                self.clients.matchAll().then(clients => {
                  if (!clients || !clients.length) {
                    return;
                  }
                  clients.forEach(client => {
                    client.postMessage({
                      type: "getServerRenderContent",
                      pathname: new URL(event.request.url, location).pathname
                    });
                  });
                });

                return response;
              })
              .catch(err => response);
          });
        });
      })
    );
  }
});
```

当然还需要一个监听，用来拿浏览器的 ssr 内容，并缓存到 caches 中，比较简单就省略了。

### 浏览器执行 ssr

监听就不说了，主要是如何利用 `react-router` 与 `react-loadable` 完成前端 ssr。

首先根据 service worker 告诉我们的 `pathname`，拿到对应 `loadable` 的实例，并通过 `loadable.preload()` 预先加载 chunk，当 chunk 加载完毕时，资源已经准备好了。

我们利用给 `StaticRouter` 传递当前的 `pathname`，让 `react-router` 模拟出需要 ssr 的页面内容，通过 `renderToString` 拿到 ssr 的结果。

附上代码片段：

```tsx
if (navigator.serviceWorker) {
  navigator.serviceWorker.addEventListener("message", event => {
    if (event.data.type === "getServerRenderContent") {
      const baseHrefRegex = new RegExp(
        escapeRegExp("${projectConfig.baseHref}"),
        "g"
      );
      const matchRouterPath = event.data.pathname.replace(baseHrefRegex, "");
      const loadableMap = pageLoadableMap.get(
        matchRouterPath === "/" ? "/" : trimEnd(matchRouterPath, "/")
      );
      if (loadableMap) {
        loadableMap.preload().then(() => {
          const ssrResult = renderToString(
            <StaticRouter location={event.data.pathname} context={{}}>
              <App />
            </StaticRouter>
          );

          if (navigator.serviceWorker.controller) {
            navigator.serviceWorker.controller.postMessage({
              type: "serverRenderContent",
              pathname: event.data.pathname,
              content: ssrResult
            });
          }
        });
      }
    }
  });
}
```

这里需要优化，利用 web worker 执行 ssr 才可以用于生产环境。

最后，等待用户的下一次刷新，service worker 会帮我们把 ssr 内容作为首屏给用户一个惊喜的。

## 3 总结

同样这次只是抛砖引玉，希望大家能提出建议一起帮助我们完善这个方案。

此方案正式用在生产环境后，会再写一篇文章介绍实践过程。

## 4 更多讨论

> 讨论地址是：[精读《在浏览器运行 serverRender》 · Issue #80 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/80)

**如果你想参与讨论，请[点击这里](https://github.com/dt-fe/weekly)，每周都有新的主题，周末或周一发布。**
