47

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

postman-example.png

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

postman-rate-limit.png

在上面的图片中,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. 开工

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

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

3.1 injected.js

chrome-dom-inspect.png

利用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


小石头若海
1.4k 声望1.4k 粉丝

努力不一定成功,但不努力会很轻松哦~