umami 源码分析


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) {
    const umami = eventValue => trackEvent(eventValue);
    umami.trackView = trackView;
    umami.trackEvent = trackEvent;

    window.umami = umami;
  }

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

记录 pv uv

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

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

        if (cssEvents) {
          addEvents(document);
          observeDocument();
        }
      }
    };

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

    update();
  }

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

监听路由改变

export const hook = (_this, method, callback) => {
  const orig = _this[method];
  return (...args) => {
    callback.apply(null, args);
    return orig.apply(_this, args);
  };
};

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

总结

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


 上一篇
战争与和平 战争与和平
战争:项目紧急或复杂和平:项目不急或简单 战争资源:战争是资源的消耗战,工作强度往往超过 996,往往我们是极度的缺乏资源,又需要赢下这场战争。在团结内部的同时,也要尽力去争取一切可以争取的资源,比如说,水果零食,加班能不能尽量争取,道义放
2022-06-26
下一篇 
小程序中的滚动穿透 小程序中的滚动穿透
在小程序开发中,弹出层滚动穿透是个比较棘手的问题。如下图,蓝色部分滚动,底部也跟着一起滚动,就是滚动穿透。 常见的处理方法比如,禁止滚动,并不生效。而给主体加 overflow: hidden 又会导致主体滚动条高度为 0,需要关闭时候记录
2022-04-18
  目录