前阵子看到有人的网站上有计数功能,感觉还挺不错的。所以也想有一个。心想着这种功能肯定有现成网上的方案。然而看了一圈,还是没有发现满意的,索性自己写了一个简单的。
先说说几个网上找的现成方案。
魔改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
这样的效果了。而具体怎么计数,完全不用去管它。
理想是美好的,然而现实很骨感,正当我美滋滋的搓着小手的时候,发现有这么几个问题。
- 首先试验下来不知道怎么的,每次网页访问,数据都是+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>
}
但既然木已成舟,就没有再改了。直接上!
- 在我探究不蒜子代码的空挡,可能是我刷新太多了,结果发现它的后端直接就崩了。。。崩。。崩了。。加上之后有那么一两天一直处于崩溃状态。想着“不能被人卡脖子”吧,写一个也不复杂,就算了。之后看情况也许可以就用回不蒜子就完事了。毕竟不用维护,能用就好嘛!
我的计数器
说了这么多,计数器的本质也就是那么一回事:每次看到一个id,找到之前的数量是多少,然后再+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
- 其次建立一个新的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,
});
}
);
}
}
- 创建了一个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 <></>;
}
}
- 然后把新的component加进去即可。
...
<PostSubtitleItem>
<PostViewCounter/>
</PostSubtitleItem>
<PostSubtitleItem>
<PostLikeCounter />
</PostSubtitleItem>
...
几个问题
- 后端用的是免费的Heroku,所以第一次的响应速度很慢,但目前我也不想花钱升级服务
- 每次计数都需要访问数据库,流量小无所谓,但万一哪天火了呢?到时候要加点缓存
- 想要统计更加有意思的数据,等有空的时候可以弄一下
小结
现在你应该可以在标题下面看到我的计数器了,虽然很简陋,但是至少工作了,所以我还是挺开心的。如果有时间,打算把它升级成一个好用的服务,可以帮助更多的人。