1. 前言
这天,在逛 github(就是划水)的时候,突然想看看某个仓库的 star 走势,但是在 star 列表中翻了半天愣是没找到相应的功能。于是乎,谷歌一搜,发现有个叫 Star History 的谷歌插件,然而竟然要收费。。。
于是,又接着搜索,发现了这个仓库。好巧的是,这个仓库就是那个插件的源码。稍微瞅了下源码,感觉我也能行?
由于之前就想学学怎么写 chrome 插件,本着学习的态度和好奇心驱使(都是划水,没有什么不同),于是也做了一个可以查看仓库 Star 趋势的插件。效果如下:
2. 准备工作
2.1 chrome 插件简单入门
由于也是第一次写 Chrome 插件,作为小白,就先搜搜大家都是怎么写 chrome 插件的吧。果然,一搜一大堆。。。不过,最终还是选择了官方文档,毕竟是第一手资料,虽然是英文,但写得还算通俗易懂,阅读起来没啥问题。
这里推荐看 Getting Started,非常友好,一步步教你完成一个最简单的修改网页背景颜色的 Chrome 插件。跟着教程完成之后你就会发现,原来 Chrome 插件就像完成一个 web 项目一样。
manifest.json 是项目的配置文件(类似于 package.json),插件所需要的一些能力(例如 Storage)就在这个文件中声明。剩下的工作,无非就是根据 Chrome 插件提供的 API 实现你想要的功能即可。
我们来看下要创建的项目 目录
和manifest.json
配置文件:
├── README.md
├── dist
│ └── bundle.js
├── images
│ ├── trending128.png
│ ├── trending16.png
│ ├── trending32.png
│ └── trending48.png
├── manifest.json
├── package.json
├── src
│ └── injected.js
└── webpack.config.js
{
"name": "Github-Star-Trend",
"version": "1.0",
"manifest_version": 2,
"description": "Generates a star trend graph for a github repository",
"icons": {
"16": "images/trending16.png",
"32": "images/trending32.png",
"48": "images/trending48.png",
"128": "images/trending128.png"
},
"content_scripts": [
{"matches": ["https://github.com/*"],
"js": ["dist/bundle.js"]
}
]
}
这里需要解释一点,根据最一开始我们看到的效果图,可以发现我们正在浏览的页面上多了一个 Star Trend
按钮。所以我们要完成的插件需要能够往页面注入一个按钮,而这正是通过 manifest.json
中的 content_scripts
字段实现的。它允许我们往 matches
字段匹配的网页中注入 js
字段中的脚本文件。
因此,上面的配置意思很简单,就是在匹配到 url 是 https://github.com/*
的网页时,注入我们 dist 目录下的 bundle.js
文件。而 bundle.js
其实是我们为了在项目中用上 ES6 而采用 webpack 编译得到的,源码就是src/injected.js
。接下来的工作就是在我们的 src 目录下开发就行了(都是写 js,没什么不同)。
2.2 Github API
在正式进入开发之前,我们再来体验下 Github 的 API 调用。官方文档在这儿,概览看完之后,经过一番搜索,终于找到我们的主角 Starring APi。
根据这个 API,我们可以拿到某个仓库的 Star 列表。仔细看文档,能够看到有这么一条:
You can also find out when stars were created by passing the following custom media type via the Accept header:
Accept: application/vnd.github.v3.star+json
太棒了,这不正是我们所需的 star 时间吗?赶紧打开 postman 测试一把:
果然,我们顺利拿到了 star 仓库的时间。不过这里有一个问题,这个请求每次返回的个数只有 30 条,也就是说假如像 react 这样十几万 star 的仓库岂不是要请求 3k+ 次。。。而且,还有另外一个重要的问题,那就是 Github API 对调用的频率也有限制。。。
在上面的图片中,Response Header 中告诉我们 limit
是 60 次,remaning
还有 59 次。再发几次请求会发现,remaning
一直在持续减少。。。在翻阅了一番文档之后,我找到了这个。
For API requests using Basic Authentication or OAuth, you can make up to 5000 requests per hour. For unauthenticated requests, the rate limit allows for up to 60 requests per hour. Unauthenticated requests are associated with the originating IP address, and not the user making requests.
其中明确提到,它会根据 ip 来限制 API 调用的频次。对于未授权的访问,一小时最多 60 次;而授权的访问,一小时最多 5000 次。所以,为了尽可能避免的访问频次带来的问题,我们在请求中需要带上access_token
。有关access_token
,你可以在这里申请。
3. 开工
经过前期的一番调研,事实证明想法确实可以实现。我们再来简单理下思路:
- 根据页面的 dom 结构,找到注入 Star Trend 按钮的位置(injected.js)
- 给 Star Trend 按钮绑定点击事件,发起获取 Star 时间的请求,收集数据(fetchHistoryData.js)
- 根据返回的数据,利用 echart.js 绘制趋势图(createChart.js)
3.1 injected.js
利用 chrome 的元素审查功能,我们可以很轻松地找到要注入按钮的位置,并给它绑定上相应的点击事件。
/**
* star 趋势按钮点击事件
*/
function onClickStarTrend() {
// todo: 发起请求
console.log('u click star trend');
}
/**
* 创建 star 趋势按钮
*/
const createStarTrendBtn = () => {const starTrendBtn = document.createElement('button');
starTrendBtn.setAttribute('class', 'btn btn-sm');
starTrendBtn.innerHTML = `Star Trend`;
starTrendBtn.addEventListener('click', onClickStarTrend);
return starTrendBtn;
};
/**
* 注入 star 趋势按钮
*/
const injectStarTrendBtn = () => {var newNode = document.createElement('li');
newNode.appendChild(createStarTrendBtn());
var firstBtn = document.querySelector('.pagehead-actions > li');
if(firstBtn && firstBtn.parentNode) {firstBtn.parentNode.insertBefore(newNode, firstBtn);
}
};
(function run() {injectStarTrendBtn();
}());
如果你已经安装了本地的这个插件,这个时候刷新页面你会发现多了一个 Star Trend
的按钮,点击的时候会在控制台打印出 u click star trend
的字样。
3.2 fetchHistoryData.js
获取数据首先要解决的就是构造请求 url,根据文档所示,我们需要当前的仓库信息。这个倒是简单,直接上正则从当前的 location.href 中匹配出来即可:
const repoRegRet = location.href.match(/https?:\/\/github.com\/([^/]+\/[^/]+)\/?.*/);
然后是请求参数:
const requestConfig = {headers: {Accept: 'application/vnd.github.v3.star+json'}};
这样,我们就可以用 axios 发起一次请求:
const url = `https://api.github.com/repos/${repoRegRet[1]}/stargazers`;
axios.get(url, requestConfig).then(firstResponse => console.log(firstResponse));
查看 log,我们成功地获取到了一个仓库第一页的 star 列表。不过,这里有几个问题需要解决:
- 如何获取第 2 页,第 3 页,第 N 页的 star 列表?
- 如何知道一个仓库有多少页 star(即 N 是多少)?
- 当一个仓库的 star 数多到要发送几百次,甚至上千次请求时,如何决策?
第一个问题 很好解决,在上面的 url 后面,跟上?page= n 就表示请求第 n 页的 star 数据。
第二个问题 有两种解法。一种是知道该仓库有多少 star,然后除以 30(一页返回 30 条数据)就可以知道有多少页了;还有一种方法其实 API 文档已经告诉我们了,第一次请求返回的数据已经告诉我们有多少页了,只不过这个数据被放在了 response 的 headers 中。其中有一个 link 字段:
<https://api.github.com/repositories/10270250/stargazers?page=2>; rel="next", <https://api.github.com/repositories/10270250/stargazers?page=1334>; rel="last"
以上就是 link 字段的一个例子,可以看到它包含了 lastPage 的 url 地址。因此,我们可以再次用正则提取出来:
let totalPage = 1;
const linkVal = firstResponse.headers.link;
if(linkVal) {const pageRegRet = linkVal.match(/next.*?page=(\d+).*?last/);
if(pageRegRet) {totalPage = Math.min(pageRegRet[1], 1333);
}
}
这里有两个坑,需要特别注意:
- 当 star 数只有 1 页时,link 字段是没有的,所以这里需要判断一下;
- 不知道什么原因,lastPage 的值最大是 1334(即使仓库有十几万的 star),且当 page=1334 发起请求时会失败。因此,totalPage 最大也只能是 1333。
第三个问题 其实并没有完美的解决方法,通过第二个问题我们知道最多需要发 1333 次请求。姑且不论服务器是否对访问频次是否有限制,这么多的请求所需要的耗时其实也是不能接受的,那么怎么办呢?对于一个趋势图,其实我们没必要用成千上万的点来绘制,也许我们只用 10 个点(可以做成配置)来绘制就够了。因此,我们只要用均分的策略从 [1, totalPage] 中选取 10 个 page 就可以了。看代码:
// 最多 10 个请求
const URL_NUM = 10;
// 构造待请求的 urls
const urls = new Array(totalPage - 1).fill(1).slice(0, URL_NUM - 1).map((_, idx) => {
let page = idx + 2;
if(totalPage > URL_NUM) {page = Math.round(page / URL_NUM * totalPage);
}
return {page, url: `https://api.github.com/repos/${repoRegRet[1]}/stargazers?page=${page}`};
});
// 构造请求
const requests = [{page: 1, request: Promise.resolve(firstResponse)},
...urls.map(item => ({page: item.page, request: axios.get(item.url, requestConfig)}))
];
// 发起请求
Promise.all(requests.map(_ => _.request)).then(responses => console.log(responses));
到这儿,请求数据的问题基本都已经解决了。不过还有一个容易忽视的坑,那就是由于 lastPage 最大只能到 1333,所以当仓库的 star 数大于 3990 时,我们拿到的数据其实是少于该仓库真实的 star 数。因此针对这种情况,我们还需要调用这个 API 接口拿到仓库的基本信息,也就知道了这个仓库的总 star 数。
至此,我们拿到了可以构造趋势图的数据(这里就不贴构造图的数据的代码,完整代码可以点这里查看)。
3.3 createChart.js
首先,我们把 injected.js 中的 onClickStarTrend 这个坑先给填上:
let chart = createChart();
function onClickStarTrend() {chart.show();
fetchHistoryData(location.href).then(data => {chart.ready(data);
}).catch(err => {chart.fail(err);
});
}
从上面的代码中,我们可以看到 chart 需要暴露出 3 个方法:
- show:展示 loading 状态
- ready:展示图表
- fail:展示错误信息
所以代码框架可以搭成这样:
class Chart {show() {this.node = document.createElement('div');
this.node.style = ""; // 添加合适的样式
this.loadingNode = document.createElement('div');
this.loadingNode.innerHTML = ""; // 用一个 svg 动画,增加趣味性
this.node.appendChild(this.loadingNode);
document.body.appendChild(this.node);
}
ready(data) {
this.node.innerHTML = `<div id="chart"/>`;
ECharts.init(document.getElementById('chart')).setOption({
color: '#40A9FF',
title: {text: 'STAR TREND'},
xAxis: {
type: 'time',
boundaryGap: false,
splitLine: {show: false}
},
yAxis: {type: 'value'},
tooltip: {trigger: 'axis'},
series: [{
data,
type: 'line',
smooth: true,
symbol: 'none',
name: 'star count'
}]
});
}
fail(err) {this.node.innerHTML = ""; // 错误节点内容}
}
限于篇幅,这里就不贴详细的 dom 节点代码,完整版可以看这里。而对于 echarts 的配置和使用,也可以参考官网上的例子。
4. 完结
整个插件的制作过程,到这儿基本上就已经完了。其他的还有网络请求异常(例如由于访问频次被限制)和设置 AccessToken
没有详细介绍,不过这些都是错误处理的步骤,大体上不影响插件的使用。如果想了解更多的,也可以直接看源码。
回过头再来看,这次划水也算有所收获,既体验了一把 chrome 插件开发,也学到了 Github API 的调用。虽然用到的都只是一些冰山一角,不过也算是开了个头,为以后的骚操作打下基础。
5. 参考
- chrome 插件官方文档
- timqian/star-history
- Github API rate limiting
- Github API – starring
- Github API – repos
本文所有代码托管在这儿,喜欢的可以给个 star。