chrome插件開發(fā) - github倉庫star趨勢圖

1. 前言

這天,在逛github(就是劃水)的時候,突然想看看某個倉庫的star走勢,但是在star列表中翻了半天愣是沒找到相應(yīng)的功能。于是乎,谷歌一搜,發(fā)現(xiàn)有個叫Star History的谷歌插件,然而竟然要收費。。。

于是,又接著搜索,發(fā)現(xiàn)了這個倉庫。好巧的是,這個倉庫就是那個插件的源碼。稍微瞅了下源碼,感覺我也能行?

由于之前就想學(xué)學(xué)怎么寫chrome插件,本著學(xué)習(xí)的態(tài)度和好奇心驅(qū)使(都是劃水,沒有什么不同),于是也做了一個可以查看倉庫Star趨勢的插件。效果如下:

效果圖

2. 準(zhǔn)備工作

2.1 chrome插件簡單入門

由于也是第一次寫Chrome插件,作為小白,就先搜搜大家都是怎么寫chrome插件的吧。果然,一搜一大堆。。。不過,最終還是選擇了官方文檔,畢竟是第一手資料,雖然是英文,但寫得還算通俗易懂,閱讀起來沒啥問題。

這里推薦看Getting Started,非常友好,一步步教你完成一個最簡單的修改網(wǎng)頁背景顏色的Chrome插件。跟著教程完成之后你就會發(fā)現(xiàn),原來Chrome插件就像完成一個web項目一樣。

manifest.json是項目的配置文件(類似于package.json),插件所需要的一些能力(例如Storage)就在這個文件中聲明。剩下的工作,無非就是根據(jù)Chrome插件提供的API實現(xiàn)你想要的功能即可。

我們來看下要創(chuàng)建的項目目錄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"]
    }
  ]
}

這里需要解釋一點,根據(jù)最一開始我們看到的效果圖,可以發(fā)現(xiàn)我們正在瀏覽的頁面上多了一個Star Trend按鈕。所以我們要完成的插件需要能夠往頁面注入一個按鈕,而這正是通過manifest.json中的content_scripts字段實現(xiàn)的。它允許我們往matches字段匹配的網(wǎng)頁中注入js字段中的腳本文件。

因此,上面的配置意思很簡單,就是在匹配到url是https://github.com/*的網(wǎng)頁時,注入我們dist目錄下的bundle.js文件。而bundle.js其實是我們?yōu)榱嗽陧椖恐杏蒙螮S6而采用webpack編譯得到的,源碼就是src/injected.js。接下來的工作就是在我們的src目錄下開發(fā)就行了(都是寫js,沒什么不同)。

2.2 Github API

在正式進(jìn)入開發(fā)之前,我們再來體驗下Github的API調(diào)用。官方文檔在這兒,概覽看完之后,經(jīng)過一番搜索,終于找到我們的主角Starring APi。

根據(jù)這個API,我們可以拿到某個倉庫的Star列表。仔細(xì)看文檔,能夠看到有這么一條:

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倉庫的時間。不過這里有一個問題,這個請求每次返回的個數(shù)只有30條,也就是說假如像react這樣十幾萬star的倉庫豈不是要請求3k+次。。。而且,還有另外一個重要的問題,那就是Github API對調(diào)用的頻率也有限制。。。

postman-rate-limit.png

在上面的圖片中,Response Header中告訴我們limit是60次,remaning還有59次。再發(fā)幾次請求會發(fā)現(xiàn),remaning一直在持續(xù)減少。。。在翻閱了一番文檔之后,我找到了這個。

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.

其中明確提到,它會根據(jù)ip來限制API調(diào)用的頻次。對于未授權(quán)的訪問,一小時最多60次;而授權(quán)的訪問,一小時最多5000次。所以,為了盡可能避免的訪問頻次帶來的問題,我們在請求中需要帶上access_token。有關(guān)access_token,你可以在這里申請。

3. 開工

經(jīng)過前期的一番調(diào)研,事實證明想法確實可以實現(xiàn)。我們再來簡單理下思路:

  1. 根據(jù)頁面的dom結(jié)構(gòu),找到注入Star Trend按鈕的位置(injected.js)
  2. 給Star Trend按鈕綁定點擊事件,發(fā)起獲取Star時間的請求,收集數(shù)據(jù)(fetchHistoryData.js)
  3. 根據(jù)返回的數(shù)據(jù),利用echart.js繪制趨勢圖(createChart.js)

3.1 injected.js

chrome-dom-inspect.png

利用chrome的元素審查功能,我們可以很輕松地找到要注入按鈕的位置,并給它綁定上相應(yīng)的點擊事件。

/**
 * star趨勢按鈕點擊事件
 */
function onClickStarTrend() {
  // todo: 發(fā)起請求
  console.log('u click star trend');
}

/**
 * 創(chuàng)建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();
}());

如果你已經(jīng)安裝了本地的這個插件,這個時候刷新頁面你會發(fā)現(xiàn)多了一個Star Trend的按鈕,點擊的時候會在控制臺打印出u click star trend的字樣。

3.2 fetchHistoryData.js

獲取數(shù)據(jù)首先要解決的就是構(gòu)造請求url,根據(jù)文檔所示,我們需要當(dāng)前的倉庫信息。這個倒是簡單,直接上正則從當(dāng)前的location.href中匹配出來即可:

const repoRegRet = location.href.match(/https?:\/\/github.com\/([^/]+\/[^/]+)\/?.*/);

然后是請求參數(shù):

const requestConfig = {headers: {Accept: 'application/vnd.github.v3.star+json'}};

這樣,我們就可以用axios發(fā)起一次請求:

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. 當(dāng)一個倉庫的star數(shù)多到要發(fā)送幾百次,甚至上千次請求時,如何決策?

第一個問題很好解決,在上面的url后面,跟上?page=n就表示請求第n頁的star數(shù)據(jù)。

第二個問題有兩種解法。一種是知道該倉庫有多少star,然后除以30(一頁返回30條數(shù)據(jù))就可以知道有多少頁了;還有一種方法其實API文檔已經(jīng)告訴我們了,第一次請求返回的數(shù)據(jù)已經(jīng)告訴我們有多少頁了,只不過這個數(shù)據(jù)被放在了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. 當(dāng)star數(shù)只有1頁時,link字段是沒有的,所以這里需要判斷一下;
  2. 不知道什么原因,lastPage的值最大是1334(即使倉庫有十幾萬的star),且當(dāng)page=1334發(fā)起請求時會失敗。因此,totalPage最大也只能是1333。

第三個問題其實并沒有完美的解決方法,通過第二個問題我們知道最多需要發(fā)1333次請求。姑且不論服務(wù)器是否對訪問頻次是否有限制,這么多的請求所需要的耗時其實也是不能接受的,那么怎么辦呢?對于一個趨勢圖,其實我們沒必要用成千上萬的點來繪制,也許我們只用10個點(可以做成配置)來繪制就夠了。因此,我們只要用均分的策略從[1, totalPage]中選取10個page就可以了??创a:

// 最多10個請求
const URL_NUM = 10;

// 構(gòu)造待請求的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}`};
});

// 構(gòu)造請求
const requests = [
  {page: 1, request: Promise.resolve(firstResponse)},
  ...urls.map(item => ({page: item.page, request: axios.get(item.url, requestConfig)}))
];

// 發(fā)起請求
Promise.all(requests.map(_ => _.request)).then(responses => console.log(responses));

到這兒,請求數(shù)據(jù)的問題基本都已經(jīng)解決了。不過還有一個容易忽視的坑,那就是由于lastPage最大只能到1333,所以當(dāng)倉庫的star數(shù)大于3990時,我們拿到的數(shù)據(jù)其實是少于該倉庫真實的star數(shù)。因此針對這種情況,我們還需要調(diào)用這個API接口拿到倉庫的基本信息,也就知道了這個倉庫的總star數(shù)。

至此,我們拿到了可以構(gòu)造趨勢圖的數(shù)據(jù)(這里就不貼構(gòu)造圖的數(shù)據(jù)的代碼,完整代碼可以點這里查看)。

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狀態(tài)
  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 = "";               // 錯誤節(jié)點內(nèi)容
  }
}

限于篇幅,這里就不貼詳細(xì)的dom節(jié)點代碼,完整版可以看這里。而對于echarts的配置和使用,也可以參考官網(wǎng)上的例子

4. 完結(jié)

整個插件的制作過程,到這兒基本上就已經(jīng)完了。其他的還有網(wǎng)絡(luò)請求異常(例如由于訪問頻次被限制)和設(shè)置AccessToken沒有詳細(xì)介紹,不過這些都是錯誤處理的步驟,大體上不影響插件的使用。如果想了解更多的,也可以直接看源碼

回過頭再來看,這次劃水也算有所收獲,既體驗了一把chrome插件開發(fā),也學(xué)到了Github API的調(diào)用。雖然用到的都只是一些冰山一角,不過也算是開了個頭,為以后的騷操作打下基礎(chǔ)。

5. 參考

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

本文所有代碼托管在這兒,喜歡的可以給個star

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容