自适应服务端渲染(服务端根据客户端环境自适应地响应首屏)

1,894 阅读3分钟

前言

相信很多人看了标题都觉得一脸懵逼,我就不卖关子用人话解释一下吧:
就是在某些情况(需求)下的服务端渲染应用需要获取客户端的某些参数(window.innerWidth之类的)使服务端首屏渲染能够响应适合的内容(根据window.innerWidth做响应式网站),所以我把这种做法称作自适应服务端渲染(RSSR)(自己编的)。

这种需求还是有的,比如rem布局,需要客户端加载js获取当前窗口宽度来设置根元素的font-size,对于我来说,以前开发都是客户端渲染的单页应用,这种情况自然不会遇到。但现在用服务端渲染的项目,把首屏渲染的任务放到了服务器上,又需要拿到客户端的window.innerWidth来自适应响应首屏界面,这也反映出了服务端渲染的一个大大的缺点。

自适应服务端渲染方案

对于客户端首屏请求传参的方式我第一个想到的就是Service Worker,我们先大致了解一下Service Worker的作用和令人振奋的特性吧:

Service Worker 是浏览器在后台独立于网页运行的脚本,它打开了通向不需要网页或用户交互的功能的大门。 现在,它们已包括如推送通知和后台同步等功能。 将来,Service Worker 将会支持如定期同步或地理围栏等其他功能。 本文提到的是关于拦截和处理网络请求的特性。

Service Worker 相关特性:

  • 它是一种 JavaScript Worker,无法直接访问 DOM。 Service Worker 通过响应 postMessage 接口发送的消息来与其控制的页面通信,页面可在必要时对 DOM 执行操作。
  • Service Worker 是一种可编程网络代理,让您能够控制页面所发送网络请求的处理方式。

了解更详细内容可访问 developers.google.com/web/fundame…

通过在浏览器开出的service worker便可拦截首屏请求带上相关headers 再fetch,这样服务器就可以知道客户端的参数做相应了。

注册Service Worker

我们先在首屏文档中注册service worker:

<script>
    if ('serviceWorker' in navigator) {
      // 带上需要的客户端参数
      navigator.serviceWorker.register('/service-worker.js?window-width=' + window.innerWidth);
    }
</script>

由于service-worker.js里的this上下文(ServiceWorkerGlobalScope)没有相关window对象或详细的客户端内容,所以只能从注册时带上query参数,service-workder线程才能取到客户端参数。

也可以在register后postMessage让service-worker接收到。

然后编写service-worker.js:

// 解析service-worker地址的参数
const query = (function () {
  var search = location.search
  if (search.indexOf('?') == 0) {
    search = search.substring(1);
  }
  return search
    ? search.replace('?', '').split('&').reduce(
      (o, v) => {
        const exec = v.split('=');
        o[exec[0]] = exec[1];
        return o;
      },
      {},
    )
    : {};
})()

// 取得window-width
const windowW = query[’window-width’];

this.addEventListener('fetch', function(event)  {
  const first_path = event.request.url.split('/')[3]
  // 判断是根路径即代表当前请求的是首屏文档
  if (first_path === '') {
    var new_req = new Request(url, {
      // 自定义header
      headers:   {
        'window-width': windowW
      }
    });
    // 拦截改造header后发起请求
    return fetch(new_req).then(function() {
      ...
    })
  }
})

接下来只要在服务端拿到这个header就可以自适应服务端渲染啦!

缺陷

使用Service Worker这个方案有个致命的缺陷,它不是实时的,浏览器开出的Service Worker是一个独立线程,不能完全阻塞主线程拦截或者不能挡在主线程之前就开始拦截控制页面,所以每次都会等主线层解析首屏文档的navigator.serviceWorker.register, 才轮到service worker线程进行install、activate操作之后,才能用客户端参数拦截请求,于是就会造成永远晚一步的局面(当前刷新后看到的是上一次的内容)。

这不是Service Worker的设计缺陷,只是Service Worker本意并不是对这种需求而已。

但这是个很好的思路,说不定以后浏览器会提供这种主线程拦截器之类的呢,兴许还能发现更多Service Worker的新姿势呢。

永远保持一颗探索的心,才能迸发新鲜灵感