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

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345

推荐阅读更多精彩内容