Skip to content

Latest commit

 

History

History
123 lines (96 loc) · 4.78 KB

54.精读《在浏览器运行 serverRender》.md

File metadata and controls

123 lines (96 loc) · 4.78 KB

54.精读《在浏览器运行 serverRender》

在浏览器运行 serverRender

概述

  • 前端 ssr 好处
    1. 不消耗服务器资源 完美的分布式运行
    2. 前后端分离,ssr 不需要部署服务器,前端代码不需要担心质量问题引起的 OOM,可以不必注意使用同构的三方库,只考虑前端运行
    3. 不需要做后端缓存(htmlToString 存 redis),后端 ssr 缓存规模大的无法计算
    4. 相比后端 ssr,前端可以绕过复杂的权限系统,http 请求的权限问题也不需要关心
    5. 不支持后端服务的静态服务器(github pages)也能做到 ssr
  • 缺点是
    1. 客户端需要支持 service worker
    2. 第二次首屏才会生效,后端 ssr 可以做到访问前预缓存 ssr 结果(预加载?)
    3. 可能破坏前端页面状态,同一个环境偷偷执行页面逻辑,可以通过 web worker 执行 ssr 解决
    4. service worker 拦截入口 html 风险很高,代码有 bug 可能导致整个页面直出失败,需要考虑完备的回滚方案和降级方案
  • 缓存清空时机的问题前后端 ssr 都会遇到所以不做解释

整体流程

  • service worker 在浏览器发起首屏 html 请求时将其拦截,进入 ssr 缓存判断流程,如果 caches 命中,就讲相应扔给服务器,服务端完全不会收到请求,完成最高效的缓存命中

    • 第一次如果没有缓存,会同步做两件事情:

      1. 发送请求,拿到后端返回的相应数据丢给浏览器

      2. 前端代码准备好后通过 postMessage 通信给浏览器,索要 ssr 内容

      3. 实现

        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

    • 主要利用react-routerreact-loadable 完成前端 ssr
    • 根据 service worker 告诉我们的 pathname, 拿到对应的 loadable 实例,通过 loadable.preload() 预先加载 chunk,当 chunk 加载完毕,资源已经准备好了。
    • 利用 statciRouter 传递当前的 pathname,让 react-router 模拟出需要 ssr 的页面内容,通过 renderToString 拿到 ssr 的结果
if (navigator.serviceWorker) {
  navigator.serviceWorker.addEventListener("message", event => {
    if (event.data.type === "getServerRenderConent") {
      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 内容作为首屏加载

summary

  • 方案不算完善,但是给了我们另一种解决思路,需要优化后才能做生产落地