項目開源地址:GitHub Popular,GitHubTrending

關于GitHub Trending API的困惑
GitHub Popular中有個treding模塊,該模塊是GitHub的treding的手機版,在這個模塊中你可以使用只有在PC上才能使用的功能。為了開發(fā)這個treding模塊我們需要獲取GitHub的treding的API數(shù)據(jù)。不過不幸的的是GitHub并沒有開放有關trending的API,所以想調(diào)GitHub的treding的API已經(jīng)是不現(xiàn)實的了。

撥開云霧見月明
為了給GitHub Popular的treding模塊提供可靠的數(shù)據(jù)支持,我查遍了所有看似可行的方法,但都沒能達到要求。本著只要思想不滑坡,方法總比問題多態(tài)度,我打開了https://github.com/trending的頁面源碼研究了起來。

在源碼中我發(fā)現(xiàn)了能夠滿足GitHub Popular的treding模塊的所有數(shù)據(jù),但存在如下兩個問題:
- 冗余的數(shù)據(jù)太多,我們需要從這些冗余的數(shù)據(jù)中提取出
treding模塊真正需要的數(shù)據(jù)。 - 這些數(shù)據(jù)都是HTML格式的,而我們需要的是Json格式的數(shù)據(jù)。
GitHubTrending項目的開發(fā)
經(jīng)過上述的分析,我們的需求與任務也逐漸明確了,我們需要一個能為我們提供可靠的https://github.com/trending數(shù)據(jù)的模塊,暫且叫它GitHubTrending吧。這個模塊需要滿足如下需求:
- 接受一個url參數(shù),如:https://github.com/trending/。
- 能夠根據(jù)url參數(shù)返回對應的json或object數(shù)據(jù)。
為了實現(xiàn)這一需求,我們需要對請求url返回的數(shù)據(jù)進行解析,提取出我們所需要的數(shù)據(jù),下面就跟大家分享GitHubTrending的具體實現(xiàn):
數(shù)據(jù)模型TrendingRepoModel
我們需要讓GitHubTrending返回一個包含TrendingRepoModel.js的集合,TrendingRepoModel.js的代碼如下:
/**
* TrendingRepoModel
* 項目地址:https://github.com/crazycodeboy/GitHubTrending
* 博客地址:http://www.devio.org
* @flow
*/
export default class TrendingRepoModel {
constructor(fullName,url,description,language,meta,contributors,contributorsUrl) {
this.fullName = fullName;
this.url = url;
this.description = description;
this.language = language;
this.meta = meta;
this.contributors = contributors;
this.contributorsUrl = contributorsUrl;
}
}
從上面代碼中可以看出,TrendingRepoModel.js包含了https://github.com/trending/的所以數(shù)據(jù)的模型。
將HTML解析成TrendingRepoModel
我們通過TrendingUtil.js將HTML解析成包含TrendingRepoModel.js的集合。下面是TrendingUtil.js文件代碼:
/**
* TrendingUtil
* 工具類:用于將github trending html 轉換成 TrendingRepoModel
* 項目地址:https://github.com/crazycodeboy/GitHubTrending
* 博客地址:http://www.devio.org
* @flow
*/
import TrendingRepoModel from './TrendingRepoModel';
import StringUtil from './StringUtil';
export default class TrendingUtil {
static htmlToRepo(responseData) {
responseData = responseData.substring(responseData.indexOf('<li class="repo-list-item'), responseData.indexOf('</ol>')).replace(/\n/, '');
var repos = [];
var splitWithH3 = responseData.split('<h3');
splitWithH3.shift();
for (var i = 0; i < splitWithH3.length; i++) {
var repo = new TrendingRepoModel();
var html = splitWithH3[i];
this.parseRepoBaseInfo(repo, html);
var metaNoteContent = this.parseContentOfNode(html, 'repo-list-meta');
this.parseRepoMeta(repo, metaNoteContent);
this.parseRepoContributors(repo, metaNoteContent);
repos.push(repo);
}
return repos;
}
static parseContentOfNode(htmlStr, classFlag) {
var noteEnd = htmlStr.indexOf(' class="' + classFlag);
var noteStart = htmlStr.lastIndexOf('<', noteEnd) + 1;
var note = htmlStr.substring(noteStart, noteEnd);
var sliceStart = htmlStr.indexOf(classFlag) + classFlag.length + 2;
var sliceEnd = htmlStr.indexOf('</' + note + '>', sliceStart);
var content = htmlStr.substring(sliceStart, sliceEnd);
return StringUtil.trim(content);
}
static parseRepoBaseInfo(repo, htmlBaseInfo) {
var urlIndex = htmlBaseInfo.indexOf('<a href="') + '<a href="'.length;
var url = htmlBaseInfo.slice(urlIndex, htmlBaseInfo.indexOf('">', urlIndex));
repo.url = url;
repo.fullName = url.slice(1, url.length);
var description = this.parseContentOfNode(htmlBaseInfo, 'repo-list-description');
var index = description.indexOf('</g-emoji>');
if (index !== -1) {
var indexEmoji = description.indexOf('</g-emoji>');
var emoji = description.substring(description.indexOf('>') + 1, indexEmoji)
description = emoji + description.substring(indexEmoji + '</g-emoji>'.length);
}
repo.description = description;
}
static parseRepoMeta(repo, htmlMeta) {
var splitWit_n = htmlMeta.split('\n');
if (splitWit_n[0].search('stars') === -1) {
repo.language = splitWit_n[0];
}
for (var i = 0; i < splitWit_n.length; i++) {
if (splitWit_n[i].search('stars') !== -1) {
repo.meta = StringUtil.trim(splitWit_n[i]);
break;
}
}
}
static parseRepoContributors(repo, htmlContributors) {
var splitWitSemicolon = htmlContributors.split('"');
repo.contributorsUrl = splitWitSemicolon[1];
var contributors = [];
for (var i = 0; i < splitWitSemicolon.length; i++) {
var url = splitWitSemicolon[i];
if (url.search('http') !== -1) {
contributors.push(url);
}
}
repo.contributors = contributors;
}
}
上面代碼將HTML解析成一個包含TrendingRepoModel.js的集合,為了去除空行,上述代碼中用到了StringUtil.js工具類:
/**
* 字符串工具類
* 項目地址:https://github.com/crazycodeboy/GitHubTrending
* 博客地址:http://www.devio.org
* @flow
*/
export default class StringUtil {
/*
* 去掉字符串左右空格、換行
*/
static trim( text ){
if (typeof(text) == "string") {
return text.replace(/^\s*|\s*$/g, "");
}
else{
return text;
}
}
}
上述代碼用于去除字符串中左右空格與換行。
GitHubTrending封裝
經(jīng)過上述步驟之后,我們的準備工作已經(jīng)完成了,下面我們就可以通過GitHubTrending來提供數(shù)據(jù)了:
/**
* 從https://github.com/trending獲取數(shù)據(jù)
* 項目地址:https://github.com/crazycodeboy/GitHubTrending
* 博客地址:http://www.devio.org
* @flow
*/
import TrendingUtil from './TrendingUtil';
export default class GitHubTrending {
GitHubTrending(){//Singleton pattern
if (typeof GitHubTrending.instance==='object') {
return GitHubTrending.instance;
}
GitHubTrending.instance=this;
}
fetchTrending(url){
return new Promise((resolve,reject)=>{
fetch(url)
.then((response)=>response.text())
.catch((error)=>{
reject(error);
console.log(error);
}).then((responseData)=>{
try {
resolve(TrendingUtil.htmlToRepo(responseData));
} catch (e) {
reject(e);
}
}).done();
});
}
}
上述代碼接受一個url,然后通過fetchAPI獲取url返回的HTML數(shù)據(jù),最后將HTML解析成包含TrendingRepoModel.js的集合。
如何使用GitHubTrending
為了方面大家使用,我已將GitHubTrending發(fā)布到npm,大家可以通過下列步驟來使用GitHubTrending。
安裝
打開在終端中運行如下命名進行安裝:
npm i GitHubTrending --save
使用
new GitHubTrending().fetchTrending(url)
.then((data)=> {
//
}).catch((error)=> {
//
});
更多用例可參考:GitHubPopular:DataRepository.js
總結
從探索使用官方API,到自己動手去實現(xiàn)它,雖然過程比較曲折,但最終還是完成目標。經(jīng)過反復測試GitHubTrending
現(xiàn)在已經(jīng)滿足了GitHub Popular項目的需求,而且穩(wěn)定性還是不錯的,感興趣的小伙伴可以下載GitHub Popular
體驗一下。