The foreword 0.

Well, the first thing most people think is, why would anyone reinvent the wheel? GitHub’s third party client is already rotten. Indeed, I felt the same way myself at the beginning and asked myself whether it was really worthwhile to do such a project again. After careful consideration, the following reasons also determine that I am willing to make a satisfactory third-party GitHub client.

  • For the author who often follows GitHub Trending list, there is an urgent need for a simpler way to follow the latest technology trend of GitHub anytime and anywhere.

  • The appearance level and function of some existing GitHub applets can not meet the author’s requirements;

  • I’ve heard that iOS development is no longer needed, so why not master a new development skill?

  • In fact, there are not so many reasons, since you want to do it, then do it, happy is the most important.

1. Gitter

  • GitHub:github.com/huangjianke… , probably the most beautiful GitHub applet client yet, welcome star

  • Data source: GitHub API V3

The current functions are as follows:

  • View Trending live

  • Display a list of users

  • Warehouse and user search

  • Warehouse: Detail display, Readme. md display, Star/Unstar, Fork, Polymorphism, view warehouse file content

  • Developer: Follow/Unfollow, show users followers/following

  • Issue: View the Issue list, add an Issue, and add an Issue comment

  • Share warehouses, developers

  • .

Gitter’s intention is not to copy all the functionality of the web into the mini program, because that experience would not be very friendly. For example, I don’t want to read the code on my phone, which would be a pain.

It’s fun to make it easier for users to get what they want without sacrificing the user experience.

2. To explore

Technology selection

For the first time, I felt that I was so small in the vast front-end world.

When I decided to do this project, I started the non-stop selection of technology, but there are so many choices in front of me. I have to feel that the front-end world is really wonderful.

  • Native development: Basically abandoned from the start, the development experience was not very friendly;

  • WePY: I have already developed a small program using this framework, Poetry writer. I have to say, there are so many pits, and anyone who has used them knows it.

  • Mpvue: using the Vue way to develop small programs, I feel that the document is not very complete, coupled with the recent maintenance is less, may be tending to stability?

  • Taro: Use the React way to develop small programs. Our team members are diligent in maintenance and patient in answering your questions. We have complete documentation and excellent development experience.

After shopping around for a period of time, trying and stepping on pits, and combining my current abilities, I finally determined the technical selection of Gitter:

Taro + Taro UI + Redux + Cloud development Node.js

Page design

In fact, as a Coder, I have been looking for a UI designer girl to be my wife. Come to think of it now, code is not the whole of life. I am still very happy now.

Anyway, what’s a page design without a designer wife? After all, what I want is a GitHub applet with a high level of appearance.

Well, without panicking, I quietly took out my long-dormant Photoshop and Sketch. I dare not say how good my design ability is. Gitter’s design can at least make the author feel happy. If any design enthusiasts want to improve Gitter’s design, welcome!

3. Development

Talk is cheap. Show me the code.

As a technical article, you can’t miss code.

Here mainly write a few step pit point, as a front-end small white, I believe that all readers are the author’s predecessors, but also hope to give advice!

Trending

Not long into development, we hit our first pothole. GitHub has no Trending API!!

I didn’t think too much about why GitHub didn’t provide the API, just how to fill the hole as soon as possible. At the beginning, I tried to use Scrapy to write a crawler to regularly crawl and store Trending list information on the web side for use by the small program side, but finally I gave up this practice, because the author did not have a server and a registered domain name, and the cloud development of the small program only supports node.js deployment.

The power of open source is still strong. Finally, I found Github-Trending API, modified it slightly, and successfully deployed it to the background of small program cloud development. Here, I would like to thank the original author for his efforts.

  • Crawl Trending Repositories

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, }); })); }Copy the code
  • Crawl Trending Developers
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')}`,}}); }); }Copy the code
  • Trending lists cloud functions
// Cloud function entry function
exports.main = async (event, context) => {
  const { type, language, since } = event
  let res = null;
  let date = new Date(a)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
  }
}
Copy the code

Markdown parsing

Well, it’s a big crater.

When doing technical research, I found that Markdown analysis of small program mainly includes the following schemes:

  • WxParse: The author’s last submission was two years ago, and after his own attempts, he did find it unsuitable for parsing such as readme.md

  • Wemark: A great rendering library for Markdown, a small app on wechat, but after trying it out, READme. md doesn’t parse it perfectly

  • Towxml: currently found to be the most perfect Markdown rendering library for wechat applet, it has been able to parse and display readme. md almost perfectly

In Markdown parsing, towXML was finally adopted, but found that the parsing performance is not very good, for some relatively large data parsing is beyond the range of small programs can bear, fortunately, the thoughtful author (SBFKcel) provides the support of the server, in this thank the author’s efforts!

  • Markdown parses the cloud functions
const Towxml = require('towxml');
const towxml = new Towxml();

// Cloud function entry function
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
  }
}
Copy the code
  • Markdown. Js components
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({
      // The name of the cloud function to call
      name: 'parse'.// The event argument passed to the cloud function
      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> ) } }Copy the code

Redux

In fact, I didn’t use Redux much in this project. At first, I thought that all interface requests should go through Redux, but later I realized that not all operations must use Redux. Finally, in this project, Redux was used only for obtaining personal information.

// Get personal information
export const getUserInfo = createApiAction(USERINFO, (params) => api.get('/user', params))
Copy the code
export function createApiAction(actionType, func = () = >{{})return (
    params = {},
    callback = { success: (a)= > {}, failed: (a)= > {} },
    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 })
    }
  }
}
Copy the code
  getUserInfo() {
    if (hasLogin()) {
      userAction.getUserInfo().then((a)= >{
        Taro.hideLoading()
        Taro.stopPullDownRefresh()
      })
    } else {
      Taro.hideLoading()
      Taro.stopPullDownRefresh()
    }
  }

const mapStateToProps = (state, ownProps) = > {
  return {
    userInfo: state.user.userInfo
  }
}
export default connect(mapStateToProps)(Index)
Copy the code
export default function user (state = INITIAL_STATE, action) {
  switch (action.type) {
    case USERINFO:
      return {
        ...state,
        userInfo: action.payload.data
      }
    default:
      return state
  }
}
Copy the code

At present, I am still in a state of incomplete understanding of Redux, well, there is a long way to learn.

4. The concluding article

When the first version of Gitter passed the review, I was very excited, just like my own child. Watching him grow up gradually, THE author also enjoyed the process of such a project from scratch. Here, I would like to express my gratitude to those who helped me.

Of course, the current functions and experience may not be perfect, but I also hope you can provide some valuable advice, Gitter on the road to perfect hope to have you!

Finally, I hope Gitter can help you!