介绍我最近开始写的一个有意思的项目

2020-06-12

引言

最近我在写一款网站的访客流量统计系统,它的功能具体就是,能够得出在过去的一段时间内,总共有多少名独立访客(Unique Visitor)到访过这个网站,它要求系统能够识别出老顾客,并且把新顾客纳入到统计内,这个基本上就是Cookie的一个应用之一,不过我们用的是Window.localStorage这个浏览器提供的Web API来实现,当然也是同样的效.虽说不难,可是在实现的过程中,也就是在实践的过程中,我觉得我确实有了很多的收获.

基本情况介绍

这个系统面向的用户群体呢是网站的站长,尤其是想得知自己的网站的真实访客数量统计信息的站长,使用的方式大概是,先在自己的网站的网页的head标签里面嵌入两段JavaScript脚本,一段设置系统的后台服务器的地址,另一段脚本是远程加载的,功能是周期性地向后端服务器发送被称为「心跳」的心跳信息,这样服务器就知道这个客户端是在线的,同时呢,在访客第一次访问的时候,这一段脚本还负责为这个访客生成一个我们系统内部用作统计和数据分析使用,也就是为了区分访客和访客所使用的,一个UUID,也就是说我们的系统是通过为每一个第一次到来的访客分配一个UUID来实现独立访客的统计功能.

后端的系统呢,收到了前端发来的心跳信息之后,就会把心跳信息存入数据库,然后我们的数据分析代码就会从数据库中取出过去一定时期内的记录,比如说过去5分钟的记录,看过去5分钟发来的心跳当中,有多少个不一样的UUID,这样就得出了访客的数量,其实原理也是很简单的.

编程语言呢,主要用的是TypeScript,自动构建工具用的是Gulp,也用到了Browserify和与之相关的一些插件来把Node.js代码打包成浏览器能运行的JavaScript代码,用TypeScript的原因是TypeScript是默认就是模块化的,并且实现了类型系统,有助于我们把程序写得模块化,也有助于今后的扩展,事实上,我可以很自豪的说,如今,这个项目的模块化还是很不错的!请看图

架构图

架构图

从设计的角度来说,抽象地看,整个系统其实可以看做是由三个「分区」或者「分支」(Division)组成的,注意这里的「分支」(Division)并不是指Git仓库中的分支(Branch),不过,这样按照「分支」来划分整个系统,其实你也会发现是非常自然的,即使是从软件开发的角度上.各个分支的功能呢,顾名思义,其实也是很容易理解.首先,同样也是按照我个人开发的顺序,Collector Division即「收集器分支」,收集器是收集什么的收集器呢?自然就是一开始我说过的「识别码」(Identity)和「心跳」(Heartbeat),当然这里并不是什么真的识别码也不是生物意义上的心跳,这里的识别码和心跳都只是我们的代码在服务器和客户端生成的数据,收集器分支,其实就是收集访客信息的分支,就是这么简单,收集到了就完事,数据就躺在内存中,等着别人来.除此之外没有额外的功能.我们是尽可能简单地去实现每个模块,力使之专一和易于理解.

在收集器分支中,有浏览器部分的功能单元(功能单元也就是小一些的方块,它们不一定是一个class,只是我们抽象地这样画),还有服务器部分的功能单元,浏览器部分的功能单元负责定时地向服务器发送心跳,并且如果是访客第一次访问的话,还有先像服务器申请「标识码」,再向服务器发送心跳,然后服务器端的功能单元就是负责签发、接收和验证这些识别码和心跳.

第二个分支叫做Storage Division,也即「存储分支」,也就是负责存储的系统分支,存储分支的核心功能单元其实就是图中的那个DataManagementSystem,即数据管理系统,这个所谓的「数据管理系统」是做什么的呢?其实就是管理数据的,具体怎么管理呢?具体地,数据管理系统会定时地从收集器分支的收集器功能单元,也就是那些叫做xxxDataCollector的功能单元,收集数据,但是数据管理系统不会直接和这些DataCollector打交道,而是通过一些个称为DataSource的中间人来和这些Collector打交道,DataSource在DataManagementSystem和DataCollector之间建立了一层稳定的抽象,使得数据管理系统能够运行得更加稳定,而数据管理系统收集到数据之后,就会把数据存储到MongoDB数据库中.

到了Analytics分支,就是分析数据的分支,首先是那些FeatureX,其实就是要根据原始数据计算统计信息的功能单元,比如说从「心跳」信息日志计算出访客数量,就是其中一个Feature的职能,我们要计算更多统计量,只需添加实现更多Features就可以了,当然这些FeatureX自然也是不会也不应该直接和数据库打交道,因为我们已经有了DataManagementSystem这个功能单元,我们委托DataManagementSystem从数据库中取出数据就行了,这样子看起来更加干净了一些,然后,计算出数据之后,我们可能想一次性取得多个统计量的数据,那么久交给Aggregator,它把多个统计量合成到一个对象上,然后让OnlinesInfoServer在面临请求的时候返回聚合后的数据.

在开发过程中,HeartbeatsDataCollector和IdentitiesLogsDataCollector其实可以抽象为DataCollector,而IdentitiesLogsDataSource和HeartbeatsLogsDataSource其实可以抽象为DataSource,我们假如说需要更多类型的数据,只需继承也就是实现DataCollector和DataSource的子类就可以了,而DataSource到DataManagementSystem的绑定其实是非常简单的,而在浏览器上Heartbeats和Identities亦不需要是个Class,最简单的,或许仅仅是个事件处理程序就可以了,所以,更多类型的数据的收集是容易实现的.

如何部署

如果你打算为你的网站部署这套流量统计系统的话,那么我建议你能够拿出一个子域名来作为这套系统的API的专用域名,比如说你的博客的根域名是yoursite.com,那么建议你拿出services.yoursite.com,甚至onlineservices.services.yoursite.com来作为这个API的专用域名,而不是监听某个特殊端口例如yoursite.com:xxxx这样的特殊的地.好了,现在我们确定这个系统要被部署在services.yoursite.com,监听的是80端口和443端口,那么首先我们准备这段内容

<script>window.onlineServicesServerAPIEndPoint="https://services.yoursite.com"</script>
<script src="https://cdn.jsdelivr.net/gh/explorebeyondthestars/onlineServices/frontend/dist/bundle.js" defer></script>

并把它作为head标签的子成员,如图

然后我们SSH到services.yoursite.com这台机器,开始部署这个系统(原谅我暂时还没想好这个项目的名字),首先拉取整个项目的最新版,Git仓库的master分支就是稳定分支,而testing分支是测试分支,拉取master分支就行了:

git clone https://github.com/explorebeyondthestars/onlineServices.git

然后cd进项目目录,安装以下全局依赖

  • npm
  • typescript
  • gulp
  • mongodb
  • caddy

还有记得手动启动mongodb和caddy,我们暂时还找不到有效的方式用代码启动之,以后会有,安装好全局依赖之后

npm install

按照项目依赖,都安装好之后,编译

tsc main.ts

编译好之后,在运行之前,我们要配置一下服务器名

export ONLINE_SERVICES_SERVER_NAME="services.yoursite.com"

这是为了待会启动时caddy可以自动为这个域名申请证书,从而实现HTTPS,然后就可以启动啦,确保caddy有权限监听80和443端口,

node main.js

如果这台服务器的80和443端口已经被NginX或者Apache占用了的话,这个问题我以后会解决,观察一下日志,如果没有什么出错提示就算是成功啦哈哈!

然后在浏览器打开

https://services.yoursite.com/onlinesInfo

这个地址应该可以看到提示啦

{
  "onlinesStats": {
    "last24HoursOnlines": 8,
    "last12HoursOnlines": 8,
    "last1HourOnlines": 1,
    "last30MinutesOnlines": 1,
    "last10MinutesOnlines": 1,
    "last5MinutesOnlines": 1,
    "last1MinuteOnlines": 1
  }
}

看到这样一段JSON输出说明部署成功啦.

意义和感想

说实话,如果想要专业的Web Analytics功能,大可以去选择Google Analytics或者Matomo之类的,它们做得其实都相当专业,但是,自己实现一个,也能体会到很多乐趣,也能学习到很多知识,相比去学习人家开发好了的现成的东西,也没那么枯.现成的项目例如Matomo,掺杂了太多的功能,因为它要满足太多人的需要,所以不利于学习和了解,自己从头开始做一个,反倒是更有助于理解整个系统的工作方.而且,虽然我不反对PHP是世界上最好的语言,但是,我总是觉得,npm install 似乎比在一台没有PHP的机器上开始配置PHP和Apache更加容易一些,Node.js写起来也更加舒适.

在截至目前的开发过程中,我觉得我对Node.js的熟练度增加了,对Promise和Stream的理解也更加自然了,顺带还解决了GitHub访问不畅的问题,也学习了一点点MongoDB的使用和MongoDB的Node.js的API的调用,我觉得,今后的项目可能我都要优先考虑MongoDB了,因为我觉得MongoDB真的是简单又强大,而且我还觉得对象Object相比关系Relation更加有表现.还有Gulp,今后我应该会在这个项目中更广泛地应用Gulp,提高整个项目的自动化程度.

交流与分享nodejsdesignpatterntrafficanalytics

展示一下我自己做的友链自助提交系统!

从感知机到神经网络