2017-01-03 23:02:09

单页应用:服务端谷歌统计

本文继续介绍基于SSR的一种首屏优化策略:服务端谷歌统计

背景

由于众所周知的原因,国内的谷歌统计不大精准,特别是会被一些广告屏蔽软件会直接屏蔽掉。
为了解决这些问题,屈屈大大介绍了一种中转策略,即客户端发送数据到我们自己的服务器,经过处理加工后再转发给谷歌服务器。有了一层中转,统计的成功率就可以大幅度提高了。
本文不仅仅介绍服务端谷歌统计,对博主而言,单页应用以及服务端渲染作为基础,才能够发挥出服务端谷歌统计的威力。

  • 优点
    • 在国内的统计成功率大幅提高
    • 减少首屏数据,提高首屏加载速度
    • 转发统计信息时可以在服务端直接记录结果
      • 例如,所有谷歌统计相关的报错直接就在服务端被我们收集到了
  • 缺点
    • 需要自行收集并处理数据
      • 相当于得自己写一个后端版的谷歌统计SDK啦

幸运的是,谷歌统计这种被大范围使用的东西,它的自定义文档非常详细,甚至自带官方中文。

demo

demo就是本博客啦,读者可以刷新一下本页面,你不会发现任何额外的请求,只有当你点击链接切换到其他路由时,才会发出一个图片请求。

这样,既不会对首屏加载有任何的影响,也保证了统计不会被广告屏蔽软件给拦截

2017-03-04更新

由于service-worker莫名其妙每次都在首屏的一秒后自己发出请求,导致加载数字很难看(虽然实际上一点也不影响实际的首屏时间),因此博主自暴自弃去除了服务端谷歌统计

准备

图片请求

想发请求,又不能用fetch,又不能用XHR怎么办?
即使可以用fetch和XHR,又不想承担被它俩被截取的风险怎么办?
图片请求可以解决这个问题。代码如下

new Image().src = "www.baidu.com/test/?data=wow"

仅仅简单的一行,浏览器就发出了一个Get请求。

实际上,客户端的谷歌统计也是用这种图片请求来发送信息的。

最小的图片响应

const EMPTY_GIF = new Buffer('R0lGODlhAQABAIABAAAAAP///yH5BAEAAAEALAAAAAABAAEAAAICTAEAOw==', 'base64')

res.setHeader('Content-Type', 'image/png')
res.setHeader('Content-Length', 43)
res.end(EMPTY_GIF)

如上是一个1x1的透明png的Buffer形式,几乎是合法图片的最小大小了。

另外,配合Content-type和Content-Length,有一种传说是有些浏览器自带1x1的透明png,在发现这两种响应头时,浏览器不会去下载,而是直接用本地的缓存。博主没有确认过,不过从技术上来说还是说得通的。

客户端收集数据

export default function (fullPath) {
  let screen = window.screen
  let params = {
    dt: document.title,
    dr: fullPath,
    ul: navigator.language || navigator.browserLanguage || '',
    sd: screen.colorDepth + '-bit',
    sr: screen.width + 'x' + screen.height,
    dpr: window.devicePixelRatio || window.webkitDevicePixelRatio || window.mozDevicePixelRatio || 1,
    dp: fullPath,
    z: +new Date()
  }

  let queryArr = []
  for (let i in params) {
    queryArr.push(i + '=' + encodeURIComponent(params[i]))
  }
  let queryString = '?' + queryArr.join('&')
  window.ga_image = new window.Image()
  window.ga_image.src = '/_.gif' + queryString
}

以vue-router为例,我们需要在afterEach这个钩子中收集信息并发送给SSR服务端

SSR服务端处理数据

添加cookie

  let clientId = req.cookies.id
  if (!clientId) {
    clientId = uuid.v4()
    res.cookie('id', clientId, {
      expires: new Date(Date.now() + expires)
    })
  }

这一段依赖express的cookie-parser,以及uuid这个库

返回图片

res.setHeader('Content-Type', 'image/gif')
res.setHeader('Content-Length', 43)
res.end(EMPTY_GIF)

先返回图片,之后的逻辑完全可以异步做,不影响客户端的性能。

校验数据

  let passParams = config.ga.required.reduce((prev, curr) => {
    if (!realQuery[curr]) {
      prev = false
    }
    return prev
  }, true)

  if (passParams === false) return
  if (config.googleTrackID === '') return
  let userAgent = req.header('user-agent')
  if (shouldBanSpider(userAgent) === true) return

这里校验参数是否合法,以及根据userAgent禁止掉爬虫

处理数据并发送

  let timeStamp = realQuery.z || Date.now()
  let form = Object.assign({}, realQuery, {
    v: config.ga.version,
    tid: config.googleTrackID,
    ds: 'web',
    z: timeStamp + clientId,
    cid: clientId,
    uip: getClientIp(req),
    ua: userAgent,
    t: 'pageview',
    an: config.title,
    dh: config.siteUrl
  })
  request.get(config.ga.api).query(form).end()

谷歌统计官方文档在这里,按照文档一个一个填入必填项以及一些选填项即可。

优化首屏访问

读者可以打开chrome devtools,刷新一下本页面,你会发现,首页没有任何ajax请求,也没有客户数据被收集的请求。
这是因为,博主对首屏做了优化,将首屏的客户数据收集全部放到了服务端异步完成,这样对首屏来说,加载速度和没有引入谷歌统计时是完全一模一样的。
这对于那些对性能很敏感的工程师非常有意义。

由于本博客的首屏是服务端渲染的,依赖于vue-server-renderer,并采用stream的方式吐出HTML,因此,可以选在写入header前就调用一次服务端谷歌统计的流程。

注意,这里需要传入一个对象,用以标志是否会在服务端谷歌统计函数中返回图片信息,因为如果默认返回的话,后续的stream妥妥地写不进response了。

但是,首屏将服务端谷歌统计的逻辑完全交给服务端的话,难点在于如何拿到页面的标题等信息,这里推荐vue-meta

    renderStream.once('data', () => {
      const { title, link, meta } = context.meta.inject()
      const titleText = title.text()
      const metaData = `${titleText}${meta.text()}${link.text()}`
      const matched = titleText.match(titleReg)
      let clientId = req.cookies.id
      if (!clientId) {
        clientId = uuid.v4()
        res.cookie('id', clientId, {
          expires: new Date(Date.now() + expires)
        })
      }
      const chunk = html.head.replace('<title></title>', metaData)
      res.write(chunk)
      sendGoogleAnalytic(req, res, next, {
        dt: matched ? matched[1] : config.title,
        dr: req.url,
        dp: req.url,
        z: +Date.now(),
        cid: clientId
      })
    })

另外,此方法需要我们额外做爬虫过滤,因为任意请求都会触发这个流程。而客户端的数据发送本身就可以过滤掉不支持JS的爬虫,因此这个优化策略是有利有弊的。

结语

这又是一个服务端渲染SPA的典型案例,演示了SSR时服务端能扮演的另一种角色。
进入2017年,前端早已不是局限于切图仔的角色了(连说这句话都显得老土),SPA和前后端分离让前端从后端手里抢来了路由,但是一些和前端联系很紧凑的工作范围,本来就和后端没多大关系,就该分给前端做,例如:

  • 本文的服务端谷歌统计
  • 定时任务,例如sitemap和rss的生成
  • 服务端判断UA以确定用户浏览器是否支持webp
  • 邮件模板

参考资料

屈屈的博客零散优化点汇总
thinkjs与服务端谷歌统计

本文链接:https://smallpath.me/post/spa-server-side-goolge-analyse

-- EOF --