前阵子看到有人的网站上有计数功能,感觉还挺不错的。所以也想有一个。心想着这种功能肯定有现成网上的方案。然而看了一圈,还是没有发现满意的,索性自己写了一个简单的。

先说说几个网上找的现成方案。

魔改profile view counter

这个很简单。加入这段代码就好了。坏处是自定义选项少的可怜,毕竟本来的用途是给Github的项目加个小标签,而不是用来给文章计数的。

风格方面也没什么能改的,被限制在了它的体系内,顶多改改颜色和文字。因为是直接生成了一个svg文件(这个技术倒是挺有意思)。

<img src="https://komarev.com/ghpvc/?username=<post-url>/>

ReliableCounter

ReliableCounter 选择挺多,满满的90年代风格。但还是那个问题,不是很适合自定义。

不蒜子

不蒜子其实写的很不错,而且很好用,因为它直接给你返回的是数据,至于怎么处理那就随便了,只要提取出有用的部分即可。所以可谓是相当的方便。

那么接下来的事就很简单了。拿我自己的例子来说,我用了Gatsby,本质上是一个基于React的框架,我们就按照React来理解。所有的文章都是用Markdown写的,会被MDX的插件编译成对应的格式;这部分数据可以通过 GraphQL得到;再把这部分数据注入到一个template里即可。

所以接下来只要按照不蒜子的要求,在template里面加入相应的html即可。这样对于每个post的页面,就会生成如下部分

<div>
  View: <span id="busuanzi_value_page_pv"></span>
</div>

就会得到

View: 123

这样的效果了。而具体怎么计数,完全不用去管它。

理想是美好的,然而现实很骨感,正当我美滋滋的搓着小手的时候,发现有这么几个问题。

  1. 首先试验下来不知道怎么的,每次网页访问,数据都是+2的(应该+1),让我搞了半天。直到部署了我自己写的计数器碰到另外一个问题,返回来想才明白怎么一回事:原因是Gatsby会进行server side rendering,加上之后的网页访问,让busuanzi.js 执行了两次。所以加点小技巧是可以解决的。
import { Helmet } from "react-helmet";

// avoid execution during server side rendering
if (typeof window !== `undefined`) {
  <Helmet>
  // include busuanzi.js
  </Helmet>
}

但既然木已成舟,就没有再改了。直接上!

  1. 在我探究不蒜子代码的空挡,可能是我刷新太多了,结果发现它的后端直接就崩了。。。崩。。崩了。。加上之后有那么一两天一直处于崩溃状态。想着“不能被人卡脖子”吧,写一个也不复杂,就算了。之后看情况也许可以就用回不蒜子就完事了。毕竟不用维护,能用就好嘛!

我的计数器

说了这么多,计数器的本质也就是那么一回事:每次看到一个id,找到之前的数量是多少,然后再+1即可。

既然如此,思路也很清晰:

  1. 首先整一个简单的后端,用了Rails。目的是存储得到的page_id对应的计数。
class ViewCountsController < ApplicationController

  def count_view
    record = get_record params[:page_id]
    record.ViewCount = get_view_count(record) + 1
    record.save

    render :json => {:view_count => record.ViewCount}
  end
end
  1. 其次建立一个新的React component,用来发送请求。

GetDataFromEndpoint用来得到JSON data。考虑到以后还要计算别的,建了一个简单的计数class作为parent。

function GetDataFromEndpoint(endpoint) {
  return fetch(
    endpoint, 
    {
      method: "GET",
      mode: "cors", 
      headers: {"Content-Type": "application/json"},
    }
  )
    .then(res => res.json())
    .then(
      (response) => {
        return response;
      })
    .catch(error => console.warn(error));
}

class AbstractCounter extends React.Component {

  constructor(props) {
    super(props);
    this.state = {
      display: false,
      result: {},
    }
  }

  GetPageId() {
    var url = new Url(window.location.href);
    let hostname = url.hostname.replaceAll('.', '-');
    let pathname = url.pathname;
    let normalized_path = pathname.substring(0, pathname.length).replaceAll('/', '-');

    return `${hostname}${normalized_path}`;
  }

  GetEndpoint() {
     // API URL, hide for the post, you find it yourself. :)
  }

  GetResult() {
    let endpoint = this.GetEndpoint();
    GetDataFromEndpoint(endpoint).then(
      (result) => {
        this.setState({
          display: true,
          result: result,
        });
      }
    );
  }
}
  1. 创建了一个ViewCounter和一个LikeCounter。并且美化美化。这里用LikeCounter举例。
const CounterDisplay = ({icon, number}) => {
  return (
    <div className="flex items-center">
      <div className="mr-1 counter">{icon}</div>
      <div className="mr-1">{number}</div>
    </div>
  );
}

class PostLikeCounter extends AbstractCounter {
  render() {
    const { display, result } = this.state;

    if (display) {
      var icon = <LikeIcon onClick={this.HandleLike} size={25}/>;
      if (this.state.liked) {
        icon = <AiFillHeart size={25} fill={"#BF616A"}/>;
      }

      return (
        <CounterDisplay 
          icon={icon} 
          number={result.like_count}/>
      );
    }
    return <></>;
  }
}
  1. 然后把新的component加进去即可。
...
<PostSubtitleItem>
  <PostViewCounter/>
</PostSubtitleItem>
<PostSubtitleItem>
  <PostLikeCounter />
</PostSubtitleItem>
...

几个问题

  1. 后端用的是免费的Heroku,所以第一次的响应速度很慢,但目前我也不想花钱升级服务
  2. 每次计数都需要访问数据库,流量小无所谓,但万一哪天火了呢?到时候要加点缓存
  3. 想要统计更加有意思的数据,等有空的时候可以弄一下

小结

现在你应该可以在标题下面看到我的计数器了,虽然很简陋,但是至少工作了,所以我还是挺开心的。如果有时间,打算把它升级成一个好用的服务,可以帮助更多的人。