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列表。仔细看文档,能够看到有这么一条:
Accept: application/vnd.github.v3.star+json
太棒了,这不正是我们所需的star时间吗?赶紧打开postman测试一把:
果然,我们顺利拿到了star仓库的时间。不过这里有一个问题,这个请求每次返回的个数只有30条,也就是说假如像react这样十几万star的仓库岂不是要请求3k+次。。。而且,还有另外一个重要的问题,那就是Github API对调用的频率也有限制。。。
在上面的图片中,Response Header中告诉我们limit
是60次,remaning
还有59次。再发几次请求会发现,remaning
一直在持续减少。。。在翻阅了一番文档之后,我找到了这个。
其中明确提到,它会根据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的调用。虽然用到的都只是一些冰山一角,不过也算是开了个头,为以后的骚操作打下基础。