umami 不使用 cookie 、localstorage 实现了 uv 识别,符合最新的隐私规范,代码也比较精简。花了一点时间,研究了这部分的实现,还是很有意思的。

技术框架:

next.js + mysql/postgresql
整体看下来,nextjs 准备了很多的约定,比如说 api 目录在 pages/api/下面,比如说,pages/api/user.js,nextjs 框架有大量这样的约定。

个人还是不太习惯这样的框架,总有种不伦不类的感觉,一些小项目可以这样搞,大型项目,还是需要明确的代码分成,封装。

用户识别

uv 的核心是去做用户识别,一般会本地存储一个随机的id,每次页面路由变化,上报给后台。
而 umami 为了符合一系列的隐私规范,并没有这么做,没有使用 cookie或者 localStorage 。
上报路径”pages/api/collect.js”,最终会产生 session 并且通过 session 返回 token 。session 生成方法 getSession

const session_uuid = uuid(website_id, hostname, ip, userAgent);
session = await createSession(website_id, {
    session_uuid,
    hostname,
    browser,
    os,
    screen,
    language,
    country,
    device,
});

这部分代码就比较清楚了,根据网站id,域名,ip,userAgent 生成 session_uuid,然后和数据库通信创建或者使用 session。
核心是根据一些列的变量生成一个不变的 uuid,后续用户再次进入页面,根据用户的这些参数,去数据库查询这个 uuid,就实现了用户识别。

里面也有一些其他的逻辑,比如跨域,忽略本地地址等,但是不影响我们对核心逻辑的理解。

上报脚本脚本

上报脚本位置:tracker/index.js
这个脚本很短,只有短短的225实现了上报功能。

<script async defer data-website-id="914685a1-8993-4d8c-895b-929c8646e814" src="http://localhost:3000/umami.js"></script>
  • async: async 脚本会在后台加载,并在加载就绪时运行。DOM 和其他脚本不会等待它们,它们也不会等待其它的东西。async 脚本就是一个会在加载完成时执行的完全独立的脚本。
  • defer: 特性告诉浏览器不要等待脚本。相反,浏览器将继续处理 HTML,构建 DOM。脚本会“在后台”下载,然后等 DOM 构建完成后,脚本才会执行。
  • data-website-id: 网站 id

挂载

if (!window.umami) &#123;
    const umami = eventValue => trackEvent(eventValue);
    umami.trackView = trackView;
    umami.trackEvent = trackEvent;

    window.umami = umami;
  &#125;

方法都挂载到 window 上面,后续可以直接调用。

记录 pv uv

if (autoTrack && !trackingDisabled()) &#123;
    // 监听 pushState,replaceState 事件
    history.pushState = hook(history, 'pushState', handlePush);
    history.replaceState = hook(history, 'replaceState', handlePush);

    const update = () => &#123;
      console.error('update');
      if (document.readyState === 'complete') &#123;
        console.error('complete');
        trackView();

        if (cssEvents) &#123;
          addEvents(document);
          observeDocument();
        &#125;
      &#125;
    &#125;;

    document.addEventListener('readystatechange', update, true);

    update();
  &#125;

这里最终会在document.readyState === 'complete'时候,去做事件监听绑定等操作,发送第一次页面上报。
有一点不理解,为什么已经监听了readystatechange,还是又手动执行了一次update()

监听路由改变

export const hook = (_this, method, callback) => &#123;
  const orig = _this[method];
  return (...args) => &#123;
    callback.apply(null, args);
    return orig.apply(_this, args);
  &#125;;
&#125;;

history.pushState hook劫持,为了在原生方法执行前,执行callback。这样实现了对原生 history 的监听。handlePush 方法会执行上报方法 trackView。

总结

整体代码比较简单,清晰,无侵入性的实现了网站统计。