嗯,可能一进来大部分人都会觉得,为什么还会有人重复造轮子,GitHub 第三方客户端都已经烂大街啦。确实,一开始我自己也是这么觉得的,也问过自己是否真的有意义再去做这样一个项目。思考再三,以下原因也决定了我愿意去做一个让自己满意的 GitHub 第三方客户端。
对于时常关注 GitHub Trending 列表的笔者来说,迫切需要一个更简单的方式随时随地去跟随 GitHub 最新的技术潮流;
已有的一些 GitHub 小程序客户端颜值与功能并不能满足笔者的要求;
听说 iOS 开发没人要了,掌握一门新的开发技能,又何尝不可?
其实也没那么多原因,既然想做,那就去做,开心最重要。
GitHub:https://github.com/huangjianke/Gitter,可能是目前颜值最高的 GitHub 小程序客户端,欢迎 star
数据来源:GitHub API v3
目前实现的功能有:
实时查看 Trending
显示用户列表
仓库和用户的搜索
仓库:详情展示、README.md 展示、Star/Unstar、Fork、Contributors 展示、查看仓库文件内容
开发者:Follow/Unfollow、显示用户的 followers/following
Issue:查看 issue 列表、新增 issue、新增 issue 评论
分享仓库、开发者
...
Gitter 的初衷并不是想把网页端所有功能照搬到小程序上,因为那样的体验并不会很友好,比如说,笔者自己也不想在手机上阅读代码,那将会是一件很痛苦的事。
在保证用户体验的前提下,让用户用更简单的方式得到自己想要的,这是一件有趣的事。
第一次觉得,在茫茫前端的世界里,自己是那么渺小。
当决定去做这个项目的时候,就开始了马不停蹄的技术选型,但摆在自己面前的选择是那么的多,也不得不感慨,前端的世界,真的很精彩。
- WePY:之前用这个框架已经开发过一个小程序,诗词墨客,不得不说,坑是真多,用过的都知道;
- mpvue:用 Vue 的方式去开发小程序,个人觉得文档并不是很齐全,加上近期维护比较少,可能是趋于稳定了?
- Taro:用 React 的方式去开发小程序,Taro 团队的小伙伴维护真的很勤快,也很耐心的解答大家疑问,文档也比较齐全,开发体验也很棒,还可以一键生成多端运行的代码(暂没尝试)
货比三家,经过一段时间的尝试及踩坑,综合自己目前的能力,最终确定了 Gitter 的技术选型:
Taro + Taro UI + Redux + 云开发 Node.js
其实,作为一名 Coder,曾经一直想找个 UI 设计师妹子做老婆的(肯定有和我一样想法的 Coder),多搭配啊。现在想想,code 不是生活的全部,现在的我一样很幸福。
话回正题,没有设计师老婆页面设计怎么办?毕竟笔者想要的是一款高颜值的 GitHub 小程序。
嗯,不慌,默默的拿出了笔者沉寂已久的 Photoshop 和 Sketch。不敢说自己的设计能力如何,Gitter 的设计至少是能让笔者自己心情愉悦的,倘若哪位设计爱好者想对 Gitter 的设计进行改良,欢迎欢迎,十二分的欢迎!
Talk is cheap. Show me the code.
作为一篇技术性文章,怎可能少得了代码。
在这里主要写写几个踩坑点,作为一个前端小白,相信各位读者均是笔者的前辈,还望多多指教!
进入开发阶段没多久,就遇到了第一个坑。GitHub 居然没有提供 Trending 列表的 API !!!
也没有过多的去想 GitHub 为什么不提供这个 API,只想着怎么去尽快填好这个坑。一开始尝试使用Scrapy写一个爬虫对网页端的 Trending 列表信息进行定时爬取及存储供小程序端使用,但最终还是放弃了这个做法,因为笔者并没有服务器与已经备案好的域名,小程序的云开发也只支持 Node.js 的部署。
开源的力量还是强大,最终找到了github-trending-api,稍作修改,成功部署到小程序云开发后台,在此,感谢原作者的努力。
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')}`,
},
});
});
}
// 云函数入口函数
exports.main = async (event, context) => {
const { type, language, since } = event
let 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 解析主要有以下方案:
- wxParse:作者最后一次提交已是两年前了,经过自己的尝试,也确实发现已经不适合如 README.md 的解析
- wemark:一款很优秀的微信小程序 Markdown 渲染库,但经过笔者尝试之后,发现对 README.md 的解析并不完美
- towxml:目前发现是微信小程序最完美的 Markdown 渲染库,已经能近乎完美的对 README.md 进行解析并展示
在 Markdown 解析这一块,最终采用的也是 towxml,但发现在解析性能这一块,目前并不是很优秀,对一些比较大的数据解析也超出了小程序所能承受的范围,还好贴心的作者(sbfkcel)提供了服务端的支持,在此感谢作者的努力!
const Towxml = require('towxml');
const towxml = new Towxml();
// 云函数入口函数
exports.main = async (event, context) => {
const { func, type, content } = event
let res
if (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.props
let that = this
wx.cloud.callFunction({
// 要调用的云函数名称
name: 'parse',
// 传递给云函数的 event 参数
data: {
func: 'parse',
type: 'markdown',
content: md,
}
}).then(res => {
let data = res.result.data
if (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.state
if (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 的使用并不多。一开始,笔者觉得所有的接口请求都应该通过 Redux 操作,后面才发现,并不是所有的操作都必须使用 Redux,最后,在本项目中,只有获取个人信息的时候使用了 Redux。
// 获取个人信息
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
}
}
目前,笔者对 Redux 还是处于一知半解的状态,嗯,学习的路还很长。
当 Gitter 第一个版本通过审核的时候,心情是很激动的,就像自己的孩子一样,看着他一点一点的长大,笔者也很享受这样一个项目从无到有的过程,在此,对那些帮助过笔者的人一并表示感谢。
当然,目前功能和体验上可能有些不大完善,也希望大家能提供一些宝贵的意见,Gitter 走向完美的路上希望有你!
最后,希望 Gitter 小程序能对你有所帮助!
1
Everyxin 2019-01-29 10:54:53 +08:00
支持一下,前后端+UI 一个人不容易
用了平时下班写吗? |
2
lovejoy 2019-01-29 11:17:47 +08:00
token 需要什么权限?
|
3
stebest 2019-01-29 11:25:39 +08:00
我记得 gitter 这个名字不已经是基于 github 的聊天工具么? https://gitter.im/
|
4
ivechan 2019-01-29 11:29:57 +08:00
同 #3, gitter 不是个聊天工具嘛。
|
5
trait 2019-01-29 11:32:03 +08:00
说明一下权限比较好,腾讯平台无法信任
|
6
huangjianke11 OP @lovejoy 权限在创建的时候可以自由选择,推荐都选上吧
|
7
huangjianke11 OP @trait 权限这一块,用户的信息就是保存在小程序本地,只用于 api 认证,没任何其他操作,可看下登录那块的实现
|
8
huangjianke11 OP @Everyxin 感谢支持
|
9
huangjianke11 OP @stebest 我个人觉得,不同平台,而且功能不一样,是不矛盾的
|
10
z0ne 2019-01-29 12:50:18 +08:00 1
楼主你好,看到这个贴,想起了我的曾经,在此有个小建议哈,针对文章文案的:应该尽量精简,比如问中出现的这么多代码,也许我们开发者只是想让大家(同行)看到自己的不易或者分享自己的研究,但是大部分群体(普通用户)是没有那么多精力和兴趣去看这些的。
所以可以进行简单的介绍+预览截图+小程序码。 个人的小建议哈 |
12
MonoLogueChi 2019-01-29 12:58:17 +08:00 via Android
gitter 不是那个炒鸡烂的 IM 吗
|
13
stebest 2019-01-29 13:01:29 +08:00
@MonoLogueChi 除了往慢一点,不知道你说的哪里烂
|
14
stebest 2019-01-29 13:02:08 +08:00
@huangjianke11 功能比较相似
|
15
huangdayu 2019-01-29 13:06:29 +08:00 via Android
支持
|
16
luguhu 2019-01-29 13:07:26 +08:00 via Android
还以为重新造了 gitter 轮子。
|
17
loading 2019-01-29 13:20:29 +08:00 via Android
名字很尴尬。
|
18
stebest 2019-01-29 13:32:38 +08:00
@huangjianke11
嗯,不错你这个做在小程序里很不错的,名字要不改一下避免混淆 |
19
ety001 2019-01-29 13:39:23 +08:00
我还以为是把 github 家的那个 IM 集成进了微信小程序。。。。。
正如楼上所说,名字很尴尬。。。 |
20
huangjianke11 OP @stebest 这个暂时可能不会改吧,如果后面有需要可能会,谢谢
|
21
gaoyoubo 2019-01-29 13:55:36 +08:00
挺好的, 我已经在使用了。
|
22
dd0754 2019-01-29 14:23:13 +08:00 via iPhone
支持
|
23
d5 2019-01-29 16:53:11 +08:00
很美丽!支持一个
|
24
hanshijun 2019-01-29 17:17:20 +08:00
挺好看的,赞一个,虽然没有在手机上浏览 github 的习惯
|
25
hlwjia 2019-01-29 17:20:12 +08:00
一直在用 wepy,看到楼主那么捧 Taro,决定一试!
|
26
huangjianke11 OP @gaoyoubo 感谢支持
|
27
huangjianke11 OP @hanshijun 我个人的初衷是,手机上可以完成一些简单的操作,比如说上班路上关注 Trending 啊,Follow,Star 操作之类的,阅读代码之类的肯定还是电脑端体验更好。后面考虑加一些有特色的功能,敬请期待
|
28
huangjianke11 OP @hlwjia 目前我感觉 Taro 是最佳的,滴滴最近也开源了一款,可以关注下
|
29
huangjianke11 OP @d5 感谢支持
|
30
workwonder 2019-01-29 20:18:55 +08:00 via Android
trending-first 的思路不错
|
31
huangjianke11 OP @workwonder 毕竟手机上能做的很有限,初衷就是,把可做的功能尽量做到体验最好
|
32
ivank 2019-01-29 20:24:09 +08:00
优秀啊
|
33
kayanouriko 2019-01-29 20:31:40 +08:00
数据加载很快,颜值很赞
就是 issue 评论带小程序码推广,第一次用小程序回复别人 issue 的时候吓一跳,图片还特别大。 第三方客户端都没带这样推广的。 |
34
huangjianke11 OP @kayanouriko 抱歉,新版会考虑加一个设置功能,让用户自由选择,再次抱歉
|
35
uxstone 2019-01-29 21:23:42 +08:00
小程序原生开发没有说的那么烂
|
36
huangjianke11 OP @uxstone 可能吧,我个人觉得开发体验并不是很好
|
37
chotow 2019-01-30 08:02:58 +08:00 via iPhone
用户信息页背景好看,不过开启了下拉刷新后,一拉中间就断层了 😂
还没看代码,想知道那个背景是怎么做的? |
38
JinChen 2019-01-30 08:08:40 +08:00 via iPhone
标题里不是号称亮点是颜值吗?为什么帖子里只有一个小程序码,都没看到颜值高在哪
|
39
huangjianke11 OP @chotow 目前是导航栏设置与背景图片一样的颜色,隐藏导航栏的话需要自定义一个导航栏,适配可能会有些问题,暂时没想到有更好的方案,您有的话还望指教下😂
|
40
laolinn 2019-01-30 11:54:20 +08:00
这个界面我很喜欢,楼主打算什么时候弄个 IOS 端的哈哈哈哈哈
|
41
hushiwei 2019-01-30 12:16:34 +08:00
挺好的,赞一个!
|
42
huangjianke11 OP @laolinn iOS 端目前没计划,目前优秀的客户端应该也很多,我就是觉得小程序端缺少一个自己比较喜欢的,然后就打算自己开发一个
|
43
huangjianke11 OP @hushiwei 感谢支持
|
44
chotow 2019-01-30 14:00:20 +08:00
@huangjianke11 #39 我指 enablePullDownRefresh 这个配置,不是 navigationStyle,哈哈哈哈。
|
45
huangjianke11 OP @chotow 刷新操作感觉还是需要的😂
|
46
balaWgc 2019-01-30 15:26:07 +08:00
读 python 代码支持度好像不太好
|
47
elespec 2019-01-30 15:51:56 +08:00
gitter 是一个 在线 im
|
48
Mephisto233 2019-01-30 16:32:17 +08:00
很好看,赞一个👍
|
49
huangjianke11 OP @Mephisto233 感谢支持
|
50
huangjianke11 OP @elespec 嗯,不同平台,作用也不一样,个人觉得是可以的
|
51
huangjianke11 OP @balaWgc 感谢反馈
|
52
huangjianke11 OP @JinChen 可以体验下先
|
53
huangjianke11 OP @ivank 谢谢
|
54
huangjianke11 OP 春节后会有新版,敬请期待
|