0. 前言
嗯,可能一進(jìn)來(lái)大部分人都會(huì)覺(jué)得,為什么還會(huì)有人重復(fù)造輪子,GitHub第三方客戶端都已經(jīng)爛大街啦。確實(shí),一開(kāi)始我自己也是這么覺(jué)得的,也問(wèn)過(guò)自己是否真的有意義再去做這樣一個(gè)項(xiàng)目。思考再三,以下原因也決定了我愿意去做一個(gè)讓自己滿意的GitHub第三方客戶端。
- 對(duì)于時(shí)常關(guān)注GitHub Trending列表的筆者來(lái)說(shuō),迫切需要一個(gè)更簡(jiǎn)單的方式隨時(shí)隨地去跟隨GitHub最新的技術(shù)潮流;
- 已有的一些GitHub小程序客戶端顏值與功能并不能滿足筆者的要求;
- 聽(tīng)說(shuō)iOS開(kāi)發(fā)沒(méi)人要了,掌握一門新的開(kāi)發(fā)技能,又何嘗不可?
- 其實(shí)也沒(méi)那么多原因,既然想做,那就去做,開(kāi)心最重要。
1. Gitter
- GitHub:https://github.com/huangjianke/Gitter,可能是目前顏值最高的GitHub小程序客戶端,歡迎star
- 數(shù)據(jù)來(lái)源:GitHub API v3
目前實(shí)現(xiàn)的功能有:
- 實(shí)時(shí)查看Trending
- 顯示用戶列表
- 倉(cāng)庫(kù)和用戶的搜索
- 倉(cāng)庫(kù):詳情展示、README.md展示、Star/Unstar、Fork、Contributors展示、查看倉(cāng)庫(kù)文件內(nèi)容
- 開(kāi)發(fā)者:Follow/Unfollow、顯示用戶的followers/following
- Issue:查看issue列表、新增issue、新增issue評(píng)論
- 分享倉(cāng)庫(kù)、開(kāi)發(fā)者
- ...
Gitter的初衷并不是想把網(wǎng)頁(yè)端所有功能照搬到小程序上,因?yàn)槟菢拥捏w驗(yàn)并不會(huì)很友好,比如說(shuō),筆者自己也不想在手機(jī)上閱讀代碼,那將會(huì)是一件很痛苦的事。
在保證用戶體驗(yàn)的前提下,讓用戶用更簡(jiǎn)單的方式得到自己想要的,這是一件有趣的事。
2. 探索篇
技術(shù)選型
第一次覺(jué)得,在茫茫前端的世界里,自己是那么渺小。
當(dāng)決定去做這個(gè)項(xiàng)目的時(shí)候,就開(kāi)始了馬不停蹄的技術(shù)選型,但擺在自己面前的選擇是那么的多,也不得不感慨,前端的世界,真的很精彩。
- 原生開(kāi)發(fā):基本上一開(kāi)始就放棄了,開(kāi)發(fā)體驗(yàn)很不友好;
- WePY:之前用這個(gè)框架已經(jīng)開(kāi)發(fā)過(guò)一個(gè)小程序,詩(shī)詞墨客,不得不說(shuō),坑是真多,用過(guò)的都知道;
- mpvue:用Vue的方式去開(kāi)發(fā)小程序,個(gè)人覺(jué)得文檔并不是很齊全,加上近期維護(hù)比較少,可能是趨于穩(wěn)定了?
- Taro:用React的方式去開(kāi)發(fā)小程序,Taro團(tuán)隊(duì)的小伙伴維護(hù)真的很勤快,也很耐心的解答大家疑問(wèn),文檔也比較齊全,開(kāi)發(fā)體驗(yàn)也很棒,還可以一鍵生成多端運(yùn)行的代碼(暫沒(méi)嘗試)
貨比三家,經(jīng)過(guò)一段時(shí)間的嘗試及踩坑,綜合自己目前的能力,最終確定了Gitter的技術(shù)選型:
Taro?+?Taro UI?+ Redux + 云開(kāi)發(fā) Node.js
頁(yè)面設(shè)計(jì)
其實(shí),作為一名Coder,曾經(jīng)一直想找個(gè)UI設(shè)計(jì)師妹子做老婆的(肯定有和我一樣想法的Coder),多搭配啊?,F(xiàn)在想想,code不是生活的全部,現(xiàn)在的我一樣很幸福。
話回正題,沒(méi)有設(shè)計(jì)師老婆頁(yè)面設(shè)計(jì)怎么辦?畢竟筆者想要的是一款高顏值的GitHub小程序。
嗯,不慌,默默的拿出了筆者沉寂已久的Photoshop和Sketch。不敢說(shuō)自己的設(shè)計(jì)能力如何,Gitter的設(shè)計(jì)至少是能讓筆者自己心情愉悅的,倘若哪位設(shè)計(jì)愛(ài)好者想對(duì)Gitter的設(shè)計(jì)進(jìn)行改良,歡迎歡迎,十二分的歡迎!
3. 開(kāi)發(fā)篇
Talk is cheap. Show me the code.
作為一篇技術(shù)性文章,怎可能少得了代碼。
在這里主要寫寫幾個(gè)踩坑點(diǎn),作為一個(gè)前端小白,相信各位讀者均是筆者的前輩,還望多多指教!
Trending
進(jìn)入開(kāi)發(fā)階段沒(méi)多久,就遇到了第一個(gè)坑。GitHub居然沒(méi)有提供Trending列表的API!!!
也沒(méi)有過(guò)多的去想GitHub為什么不提供這個(gè)API,只想著怎么去盡快填好這個(gè)坑。一開(kāi)始嘗試使用Scrapy寫一個(gè)爬蟲對(duì)網(wǎng)頁(yè)端的Trending列表信息進(jìn)行定時(shí)爬取及存儲(chǔ)供小程序端使用,但最終還是放棄了這個(gè)做法,因?yàn)楣P者并沒(méi)有服務(wù)器與已經(jīng)備案好的域名,小程序的云開(kāi)發(fā)也只支持Node.js的部署。
開(kāi)源的力量還是強(qiáng)大,最終找到了github-trending-api,稍作修改,成功部署到小程序云開(kāi)發(fā)后臺(tái),在此,感謝原作者的努力。
async function fetchRepositories({language = '',since = 'daily',} = {}) {const url = `${GITHUB_URL}/trending/${language}?since=${since}`;const data = await fetch(url);const $ = cheerio.load(await data.text());return ($('.repo-list li').get()// eslint-disable-next-line complexity.map(repo => {const $repo = $(repo);const title = $repo.find('h3').text().trim();const relativeUrl = $repo.find('h3').find('a').attr('href');const currentPeriodStarsString =$repo.find('.float-sm-right').text().trim() || /* istanbul ignore next */ '';const builtBy = $repo.find('span:contains("Built by")').parent().find('[data-hovercard-type="user"]').map((i, user) => {const altString = $(user).children('img').attr('alt');const avatarUrl = $(user).children('img').attr('src');return {username: altString? altString.slice(1): /* istanbul ignore next */ null,href: `${GITHUB_URL}${user.attribs.href}`,avatar: removeDefaultAvatarSize(avatarUrl),};}).get();const colorNode = $repo.find('.repo-language-color');const langColor = colorNode.length? colorNode.css('background-color'): null;const langNode = $repo.find('[itemprop=programmingLanguage]');const lang = langNode.length? langNode.text().trim(): /* istanbul ignore next */ null;return omitNil({author: title.split(' / ')[0],name: title.split(' / ')[1],url: `${GITHUB_URL}${relativeUrl}`,description:$repo.find('.py-1 p').text().trim() || /* istanbul ignore next */ '',language: lang,languageColor: langColor,stars: parseInt($repo.find(`[href="${relativeUrl}/stargazers"]`).text().replace(',', '') || /* istanbul ignore next */ 0,10),forks: parseInt($repo.find(`[href="${relativeUrl}/network"]`).text().replace(',', '') || /* istanbul ignore next */ 0,10),currentPeriodStars: parseInt(currentPeriodStarsString.split(' ')[0].replace(',', '') ||/* istanbul ignore next */ 0,10),builtBy,});}));}
async function fetchDevelopers({ language = '', since = 'daily' } = {}) {const data = await fetch(`${GITHUB_URL}/trending/developers/${language}?since=${since}`);const $ = cheerio.load(await data.text());return $('.explore-content li').get().map(dev => {const $dev = $(dev);const relativeUrl = $dev.find('.f3 a').attr('href');const name = getMatchString($dev.find('.f3 a span').text().trim(),/^\((.+)\)$/i);$dev.find('.f3 a span').remove();const username = $dev.find('.f3 a').text().trim();const $repo = $dev.find('.repo-snipit');return omitNil({username,name,url: `${GITHUB_URL}${relativeUrl}`,avatar: removeDefaultAvatarSize($dev.find('img').attr('src')),repo: {name: $repo.find('.repo-snipit-name span.repo').text().trim(),description:$repo.find('.repo-snipit-description').text().trim() || /* istanbul ignore next */ '',url: `${GITHUB_URL}${$repo.attr('href')}`,},});});
}
// 云函數(shù)入口函數(shù)
exports.main = async (event, context) => {const { type, language, since } = eventlet res = null;let date = new Date()if (type === 'repositories') {const cacheKey = `repositories::${language || 'nolang'}::${since ||'daily'}`;const cacheData = await db.collection('repositories').where({cacheKey: cacheKey}).orderBy('cacheDate', 'desc').get()if (cacheData.data.length !== 0 &&((date.getTime() - cacheData.data[0].cacheDate) < 1800 * 1000)) {res = JSON.parse(cacheData.data[0].content)} else {res = await fetchRepositories({ language, since });await db.collection('repositories').add({data: {cacheDate: date.getTime(),cacheKey: cacheKey,content: JSON.stringify(res)}})}} else if (type === 'developers') {const cacheKey = `developers::${language || 'nolang'}::${since || 'daily'}`;const cacheData = await db.collection('developers').where({cacheKey: cacheKey}).orderBy('cacheDate', 'desc').get()if (cacheData.data.length !== 0 &&((date.getTime() - cacheData.data[0].cacheDate) < 1800 * 1000)) {res = JSON.parse(cacheData.data[0].content)} else {res = await fetchDevelopers({ language, since });await db.collection('developers').add({data: {cacheDate: date.getTime(),cacheKey: cacheKey,content: JSON.stringify(res)}})}}return {data: res}
}
Markdown解析
嗯,這是一個(gè)大坑。
在做技術(shù)調(diào)研的時(shí)候,發(fā)現(xiàn)小程序端Markdown解析主要有以下方案:
- wxParse:作者最后一次提交已是兩年前了,經(jīng)過(guò)自己的嘗試,也確實(shí)發(fā)現(xiàn)已經(jīng)不適合如README.md的解析
- wemark:一款很優(yōu)秀的微信小程序Markdown渲染庫(kù),但經(jīng)過(guò)筆者嘗試之后,發(fā)現(xiàn)對(duì)README.md的解析并不完美
- towxml:目前發(fā)現(xiàn)是微信小程序最完美的Markdown渲染庫(kù),已經(jīng)能近乎完美的對(duì)README.md進(jìn)行解析并展示
在Markdown解析這一塊,最終采用的也是towxml,但發(fā)現(xiàn)在解析性能這一塊,目前并不是很優(yōu)秀,對(duì)一些比較大的數(shù)據(jù)解析也超出了小程序所能承受的范圍,還好貼心的作者(sbfkcel)提供了服務(wù)端的支持,在此感謝作者的努力!
const Towxml = require('towxml');
const towxml = new Towxml();// 云函數(shù)入口函數(shù)
exports.main = async (event, context) => {const { func, type, content } = eventlet resif (func === 'parse') {if (type === 'markdown') {res = await towxml.toJson(content || '', 'markdown');} else {res = await towxml.toJson(content || '', 'html');}}return {data: res}
}
import Taro, { Component } from '@tarojs/taro'
import PropTypes from 'prop-types'
import { View, Text } from '@tarojs/components'
import { AtActivityIndicator } from 'taro-ui'import './markdown.less'import Towxml from '../towxml/main'const render = new Towxml()export default class Markdown extends Component {static propTypes = {md: PropTypes.string,base: PropTypes.string}static defaultProps = {md: null,base: null}constructor(props) {super(props)this.state = {data: null,fail: false}}componentDidMount() {this.parseReadme()}parseReadme() {const { md, base } = this.propslet that = thiswx.cloud.callFunction({// 要調(diào)用的云函數(shù)名稱name: 'parse',// 傳遞給云函數(shù)的event參數(shù)data: {func: 'parse',type: 'markdown',content: md,}}).then(res => {let data = res.result.dataif (base && base.length > 0) {data = render.initData(data, {base: base, app: this.$scope})}that.setState({fail: false,data: data})}).catch(err => {console.log('cloud', err)that.setState({fail: true})})}render() {const { data, fail } = this.stateif (fail) {return (<View className='fail' onClick={this.parseReadme.bind(this)}><Text className='text'>load failed, try it again?</Text></View>)}return (<View>{data ? (<View><import src='../towxml/entry.wxml' /><template is='entry' data='{{...data}}' /></View>) : (<View className='loading'><AtActivityIndicator size={20} color='#2d8cf0' content='loading...' /></View>)}</View>)}
}
Redux
其實(shí),筆者在該項(xiàng)目中,對(duì)Redux的使用并不多。一開(kāi)始,筆者覺(jué)得所有的接口請(qǐng)求都應(yīng)該通過(guò)Redux操作,后面才發(fā)現(xiàn),并不是所有的操作都必須使用Redux,最后,在本項(xiàng)目中,只有獲取個(gè)人信息的時(shí)候使用了Redux。
// 獲取個(gè)人信息
export const getUserInfo = createApiAction(USERINFO, (params) => api.get('/user', params)) export function createApiAction(actionType, func = () => {}) {return (params = {},callback = { success: () => {}, failed: () => {} },customActionType = actionType,) => async (dispatch) => {try {dispatch({ type: `${customActionType }_request`, params });const data = await func(params);dispatch({ type: customActionType, params, payload: data });callback.success && callback.success({ payload: data })return data} catch (e) {dispatch({ type: `${customActionType }_failure`, params, payload: e })callback.failed && callback.failed({ payload: e })}}
} getUserInfo() {if (hasLogin()) {userAction.getUserInfo().then(()=>{Taro.hideLoading()Taro.stopPullDownRefresh()})} else {Taro.hideLoading()Taro.stopPullDownRefresh()}}const mapStateToProps = (state, ownProps) => {return {userInfo: state.user.userInfo}
}
export default connect(mapStateToProps)(Index) export default function user (state = INITIAL_STATE, action) {switch (action.type) {case USERINFO:return {...state,userInfo: action.payload.data}default:return state}
}
目前,筆者對(duì)Redux還是處于一知半解的狀態(tài),嗯,學(xué)習(xí)的路還很長(zhǎng)。
4. 結(jié)語(yǔ)篇
當(dāng)Gitter第一個(gè)版本通過(guò)審核的時(shí)候,心情是很激動(dòng)的,就像自己的孩子一樣,看著他一點(diǎn)一點(diǎn)的長(zhǎng)大,筆者也很享受這樣一個(gè)項(xiàng)目從無(wú)到有的過(guò)程,在此,對(duì)那些幫助過(guò)筆者的人一并表示感謝。
當(dāng)然,目前功能和體驗(yàn)上可能有些不大完善,也希望大家能提供一些寶貴的意見(jiàn),Gitter走向完美的路上希望有你!
最后,希望Gitter小程序能對(duì)你有所幫助!
總結(jié)
以上是生活随笔為你收集整理的Gitter - 高颜值GitHub小程序客户端诞生记的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。