1. 前言

这天,在逛github(就是划水)的时候,突然想看看某个仓库的star走势,但是在star列表中翻了半天愣是没找到相应的功能。于是乎,谷歌一搜,发现有个叫Star History的谷歌插件,然而竟然要收费。。。

于是,又接着搜索,发现了这个仓库。好巧的是,这个仓库就是那个插件的源码。稍微瞅了下源码,感觉我也能行?

由于之前就想学学怎么写chrome插件,本着学习的态度和好奇心驱使(都是划水,没有什么不同),于是也做了一个可以查看仓库Star趋势的插件。效果如下:

chrome插件开发 - github仓库star趋势图-LMLPHP

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测试一把:

chrome插件开发 - github仓库star趋势图-LMLPHP

果然,我们顺利拿到了star仓库的时间。不过这里有一个问题,这个请求每次返回的个数只有30条,也就是说假如像react这样十几万star的仓库岂不是要请求3k+次。。。而且,还有另外一个重要的问题,那就是Github API对调用的频率也有限制。。。

chrome插件开发 - github仓库star趋势图-LMLPHP

在上面的图片中,Response Header中告诉我们limit是60次,remaning还有59次。再发几次请求会发现,remaning一直在持续减少。。。在翻阅了一番文档之后,我找到了这个

其中明确提到,它会根据ip来限制API调用的频次。对于未授权的访问,一小时最多60次;而授权的访问,一小时最多5000次。所以,为了尽可能避免的访问频次带来的问题,我们在请求中需要带上access_token。有关access_token,你可以在这里申请。

3. 开工

经过前期的一番调研,事实证明想法确实可以实现。我们再来简单理下思路:

  1. 根据页面的dom结构,找到注入Star Trend按钮的位置(injected.js)
  2. 给Star Trend按钮绑定点击事件,发起获取Star时间的请求,收集数据(fetchHistoryData.js)
  3. 根据返回的数据,利用echart.js绘制趋势图(createChart.js)

3.1 injected.js

chrome插件开发 - github仓库star趋势图-LMLPHP

利用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列表。不过,这里有几个问题需要解决:

  1. 如何获取第2页,第3页,第N页的star列表?
  2. 如何知道一个仓库有多少页star(即N是多少)?
  3. 当一个仓库的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);
  }
}

这里有两个坑,需要特别注意:

  1. 当star数只有1页时,link字段是没有的,所以这里需要判断一下;
  2. 不知道什么原因,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个方法:

  1. show:展示loading状态
  2. ready:展示图表
  3. 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. 参考

  1. chrome插件官方文档
  2. timqian/star-history
  3. Github API rate limiting
  4. Github API - starring
  5. Github API - repos

本文所有代码托管在这儿,喜欢的可以给个star

06-17 23:49