本次课程的完整代码在这里:https://github.com/GuoYongfeng/github-notetaker-app

0.介绍 INTRODUCTION

我们计划用最近所学的技术知识来完成一个完整且实用的功能应用,这个应用具有以下功能:

  • 通过搜索github上的用户名来检索该用户的基本信息
  • 可以检索到该用户的github上的所有代码仓库信息
  • 可以对该用户进行简单的评论
  • 通过特定路由可以访问特定用户的信息

大概是长这个样子

在看到了这个原型图之后,我们来做一件很重要的事情,使用组件化的思维来解析这个应用需求。

github-notetaker-app整体组件划分示意图

1.启动

首先我们把脚手架项目下载下来并且安装启动

$ git clone git@github.com:GuoYongfeng/webpack-dev-boilerplate.git github-notetaker-app
$ cd github-notetaker-app && npm install
$ npm run dev

查看浏览器效果

2.使用react-router跑通基本路由

下载react-router和history

$ npm install react-router history --save

简单说一下react-router和history分别是什么:

  • react-router is a complete routing library for React. react-router是一个专为react提供的完整路由库。
  • history is a JavaScript library that lets you easily manage session history anywhere JavaScript runs. history是一个js库,可以让您轻松地管理会话历史。

接下来我们开始写代码:

代码清单:app/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import { Router, browserHistory  } from 'react-router';
import routes from './routes/index.jsx';

let root = document.getElementById('app');
ReactDOM.render(
  <Router routes={routes} history={browserHistory} />, root
);

这里我们引入了react-router里面的Router,为了方便路由管理,我们新建一个管理路由的目录,并且引入路由配置,接下来在app目录下创建一个路由表。

代码清单:

$ cd app && mkdir routes
$ cd routes && touch index.jsx

配置路由,代码清单:app/routes/index.jsx

import React from 'react';
import { Route, IndexRoute } from 'react-router';
import { App, Home, About } from '../containers';

export default (
  <Route path="/" component={App}>
    <IndexRoute component={Home} />
    <Route path="about" component={About} />
  </Route>
)

这里从react-router引入了Route和IndexRoute,其中Route就是用来配置单个具体的路由,IndexRoute是用于在路由中展示默认的组件,而且一级路由中还可以嵌套二级路由。

从container中引入了App和Home这两个容器组件,当访问路由"/"的时候,渲染的是组件App和Home组成的页面。

接下来对App.jsx修改,代码清单:app/containers/App/App.jsx

import React, { Component } from 'react';
import './App.css';

class App extends Component {
  constructor(props) {
    super(props);
  }
  render() {
    return (
      <div>
        <h1> content from App Component </h1>
        {this.props.children}
      </div>
    );
  }
}

export default App;

同时,新增一个容器组件Home。

$ cd app/containers && mkdir Home About
$ cd Home && touch Home.jsx
$ cd ../About && touch About.jsx

代码清单:app/containers/Home/Home.jsx

import React, { Component } from 'react';

class Home extends Component {
  render() {
    return (
      <h2>content from Home Component</h2>
    );
  }
}

export default Home;

代码清单:app/containers/Home/Home.jsx

import React, { Component } from 'react';

class About extends Component {
  render() {
    return (
      <h2>content from About Component</h2>
    );
  }
}

export default About;

这里我们新增了组件,同时在containers下面的组件索引文件中进行更新。 代码清单:app/containers/index.js

'use strict';

export App from './App/App.jsx';
export Home from './Home/Home.jsx';
export About from './About/About.jsx';

初步完成路由的管理,我们先在命令行窗口停止服务,需要修改package.json文件,在启动webpack-dev-server的时候加上参数--history-api-fallback

因为我们这里用的是browserHistory,启动服务的时候加上history-api-fallback用于enables support for history API fallback.

好了,重新运行npm run dev,我们接下来在浏览器中查看效果.

3.新增头部搜索组件

在新增组件之前,为了快速的写出好看的页面,我们考虑把bootstrap引入使用。

下载bootstrap

$ npm install bootstrap --save

然后在app/index.js中引入样式文件,代码清单:app/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import { Router, browserHistory  } from 'react-router';
import routes from './routes/index.jsx';
import 'bootstrap/dist/css/bootstrap.css';

let root = document.getElementById('app');
ReactDOM.render(
  <Router routes={routes} history={browserHistory} />, root
);

按照我们前面的原型图,我们先把头部搜索表单写好,搜索部分的搜索框和按钮部分,我们可以独立成一个搜索组件SearchGithub,代码清单:app/containers/App/App.jsx

import React from 'react';
import { SearchGithub } from '../../components'

const App = ({children, history}) => {
  return (
    <div className="main-container">
      <nav className="navbar navbar-default" role="navigation">
        <div className="col-sm-7 col-sm-offset-2" style={{marginTop: 15}}>
          <SearchGithub history={history}/>
        </div>
      </nav>
      <div className="container">
        {children}
      </div>
    </div>
  )
}

export default App

同时,我们要在components目录下新建一个SearchGithub组件

$ cd app/components && mkdir SearchGithub
$ cd SearchGithub && touch SearchGithub.jsx

封装我们的SearchGithub组件,代码清单:app/components/SearchGithub/SearchGithub.jsx

import React, { Component, PropTypes } from 'react';
import { browserHistory } from 'react-router';

class SearchGithub extends Component {
  static PropTypes = {
    history: PropTypes.object.isRequired
  }
  getRef(ref){
    this.usernameRef = ref;
  }
  handleSubmit(event){
    const username = this.usernameRef.value;
    this.usernameRef.value = '';

    const path = `/profile/${username}`;
    browserHistory.push(path)

  }
  render(){
    return (
      <div className="col-sm-12">
        <form onSubmit={() => this.handleSubmit()}>
          <div className="form-group col-sm-7">
            <input type="text" className="form-control" ref={(ref) => this.getRef(ref)} />
          </div>
          <div className="form-group col-sm-5">
            <button type="submit" className="btn btn-block btn-primary">搜索 Github</button>
          </div>
        </form>
      </div>
    )
  }
}

export default SearchGithub;

为了便于其他地方获取,我们需要更新components目录下的组件列表文件,代码清单:app/components/index.js

'use strict';

export TestDemo from './TestDemo/TestDemo.jsx';
export SearchGithub from './SearchGithub/SearchGithub.jsx';

哒哒,我们来看一下效果

4.创建展示用户信息的Profile组件

前面的头部搜索框看起来像点样子,我们接着按照原型图来进行组件拆分。拆分之前,简单完善一下Home组件,这个组件是我们进入页面看到的默认的主页部分,简单写几个字示意一下即可。代码清单:app/containers/Home/Home.jsx

import React from 'react';

export default function Home () {
  return (
    <h2 className="text-center">
      通过 Github 用户名搜索代码资料
    </h2>
  )
}

理一下思路,按照原型图,我们需要创建一个容器组件,该组件可由展示用户基本信息、用户仓库信息、用户评论信息的三个组件组成,暂且将这个容器组件命名为Profile,Profile内的三个展示组件分别命名为UserProfile、UserRepos、Notes。

接下来进行创建

$ cd app/containers && mkdir Profile
$ cd Profile && touch Profile.jsx

完善Profile组件基本代码

import React, { Component } from 'react'

class Profile extends Component {
  render(){
    return (
      <div className="row">
        <div className="col-md-4">
          UserProfile 路由的参数是:{this.props.params.username}
        </div>
        <div className="col-md-4">
          UserRepos
        </div>
        <div className="col-md-4">
          Notes
        </div>
      </div>
    )
  }
}

export default Profile

更新组件列表文件,代码清单:app/containers/index.js

'use strict';

export App from './App/App.jsx';
export Home from './Home/Home.jsx';
export Profile from './Profile/Profile.jsx';

添加一条路由,代码清单:app/routes/index.jsx

import React from 'react';
import { Route, IndexRoute } from 'react-router';
import { App, Home, Profile } from '../containers';

export default (
  <Route path="/" component={App}>
    {/* 新加了profile路由 :后面是参数params */}
    <Route path="profile/:username" component={Profile} />
    <IndexRoute component={Home} />
  </Route>
)

这个时候再去输入路由参数的时候,我们可以在Profile组件中通过this.props.params.username拿到了。

5.创建组件UserProfile、UserRepos、Notes

创建三个组件

$ cd app/components && mkdir UserProfile UserRepos Notes
$ cd UserProfile && touch UserProfile.jsx
$ cd ../UserRepos && touch UserRepos.jsx
$ cd ../Notes && touch Notes.jsx

现在我们先简单的给这三个组件添加示意性的逻辑

代码清单:app/components/UserProfile/UserProfile.jsx

import React, { Component } from 'react';

export default class UserRepos extends Component {
  render(){
    return (
      <div> UserRepos </div>
    )
  }
}

代码清单:app/components/UserRepos/UserRepos.jsx

import React, { Component } from 'react';

export default class UserRepos extends Component {
  render(){
    return (
      <div> UserRepos </div>
    )
  }
}

代码清单:app/components/Notes/Notes.jsx

import React, { Component } from 'react';

export default class Notes extends Component {
  render(){
    return (
      <div> Notes </div>
    )
  }
}

接下来更新一下组件维护列表,代码清单:app/components/index.js

'use strict';

export TestDemo from './TestDemo/TestDemo.jsx';
export SearchGithub from './SearchGithub/SearchGithub.jsx';
export UserProfile from './UserProfile/UserProfile.jsx';
export UserRepos from './UserRepos/UserRepos.jsx';
export Notes from './Notes/Notes.jsx';

ok,我们在Profile组件使用这三个组件,代码清单:app/containers/Profile/Profile.jsx

import React, { Component } from 'react';
import { UserProfile, UserRepos, Notes } from '../../components';

class Profile extends Component {
  render(){
    return (
      <div className="row">
        <div className="col-md-4">
          基本信息
          <UserProfile />
        </div>
        <div className="col-md-4">
          代码仓库
          <UserRepos />
        </div>
        <div className="col-md-4">
          笔记
          <Notes />
        </div>
      </div>
    )
  }
}

export default Profile

在浏览器中查看效果。

6.使用state和props传递数据

我们在Profile组件预设部分初始state,并且使用state和props将数据传递给三个子组件。

代码清单:app/containers/Profile/Profile.jsx

import React, { Component } from 'react';
import { UserProfile, UserRepos, Notes } from '../../components';

class Profile extends Component {
  state = {
    notes: ['1', '2', '3'],
    bio: {
      name: 'guoyongfeng'
    },
    repos: ['a', 'b', 'c']
  }
  render(){
    console.log(this.props);
    return (
      <div className="row">
        <div className="col-md-4">
          <UserProfile
            username={this.props.params.username}
            bio={this.state.bio} />
        </div>
        <div className="col-md-4">
          <UserRepos repos={this.state.repos} />
        </div>
        <div className="col-md-4">
          <Notes notes={this.state.notes} />
        </div>
      </div>
    )
  }
}

export default Profile

在三个子组件里面分别拿到这些数据进行展示。

代码清单:app/components/UserProfile/UserProfile.jsx

import React, { Component } from 'react';

export default class UserProfile extends Component {
  render(){
    return (
      <div>
        <p> 基本信息 </p>
        <p> 姓名: {this.props.username} </p>
        <p> 介绍:{this.props.bio.name}</p>
      </div>
    )
  }
}

代码清单:app/components/UserProfile/UserProfile.jsx

import React, { Component } from 'react';

export default class UserRepos extends Component {
  render(){
    return (
      <div>
        <p> GIT仓库 </p>
        <p> REPOS: {this.props.repos}</p>
      </div>
    )
  }
}

代码清单:app/components/UserProfile/UserProfile.jsx

import React, { Component } from 'react';

export default class Notes extends Component {
  render(){
    return (
      <div>
        <p> 评论 </p>
        <p> Notes: {this.props.notes} </p>
      </div>
    )
  }
}

7.接入真实的数据

为了接入真实的数据,这里我们将用到几个新的知识点:

  • firebase:是一个数据同步的云服务,帮助开发者开发具有「实时」(Real-Time)特性的应用,让我们实现真正的无后端编程,有木有很厉害,啊哈哈...
  • reactfire: 一个react的mixin库,封装了六个组件公共的方法(bindAsArray,unbind,bindAsObject...),专门用于处理React和Firebase集成的mixin方法,几行代码轻松获取数据,叼炸天...

首先下载这两个库

$ npm install --save reactfire firebase

然后在代码中引入:

import ReactFireMixin from 'reactfire';
import Firebase from 'firebase';

其中提到reactfire是一个mixin库,但是我们目前使用的是ES6语法写的,而不幸的是,ES6不支持mixin的写法,所以,我们只能想其他的办法,想知道更多请看这里.

看完这篇文章后,我觉得我们可以用decorator来实现mixin的写法,比如在代码中这样写:

function testable(target) {
  target.isTestable = true;
}

@testable
class MyTestableClass {}

console.log(MyTestableClass.isTestable) // true

我们接着使用core-decorators提供的mixin来做重用模块的叠加,首先下载:

$ npm install --save core-decorators

然后在代码中引入这样就可以使用了

import { mixin } from 'core-decorators';

@mixin(ReactFireMixin)
class Profile extends Component {
 ...
}

这还没完成,我们目前使用的是decorator,但是浏览器不支持这个写法啊,怎么办,我们让babel来编译解决这个问题吧。首先需要下载一个能解析decorator的babel插件,然后在.babelrc里面配置:

$ npm install babel-plugin-transform-decorators-legacy --save-dev

配置.babrelrc

{
  "presets": ["es2015", "stage-0", "react"],
  "plugins": ["transform-runtime", "transform-decorators-legacy"]
}

so,目前为止,我们可以在代码中使用reactfire这个mixin类库以及decorator语法。

现在贴出Profile组件完整的代码:app/containers/Profile/Profile.jsx

import React, { Component } from 'react';
import { UserProfile, UserRepos, Notes } from '../../components';
import { mixin } from 'core-decorators';
import ReactFireMixin from 'reactfire';
import Firebase from 'firebase';

@mixin(ReactFireMixin)
class Profile extends Component {
  state = {
    notes: ['1', '2', '3'],
    bio: {
      name: 'guoyongfeng'
    },
    repos: ['a', 'b', 'c']
  }
  componentDidMount(){
    // 为了读写数据,我们首先创建一个firebase数据库的引用
    this.ref = new Firebase('https://github-note-taker.firebaseio.com/');
    // 调用child来往引用地址后面追加请求,获取数据
    var childRef = this.ref.child(this.props.params.username);
    // 将获取的数据转换成数组并且赋给this.state.notes
    this.bindAsArray(childRef, 'notes');
  }
  componentWillUnMount(){
    this.unbind('notes');
  }
  render(){
    return (
      <div className="row">
        <div className="col-md-4">
          <UserProfile
            username={this.props.params.username}
            bio={this.state.bio} />
        </div>
        <div className="col-md-4">
          <UserRepos
            username={this.props.params.username}
            repos={this.state.repos} />
        </div>
        <div className="col-md-4">
          <Notes
            username={this.props.params.username}
            notes={this.state.notes} />
        </div>
      </div>
    )
  }
}

export default Profile

现在通过firebase获取的是notes的数据,我们到Notes组件来看一下传过来的是什么数据. 代码清单:app/components/Notes/Notes.jsx

import React, { Component } from 'react';

export default class Notes extends Component {
  render(){
    console.log('notes:', this.props.notes);

    return (
      <div>
        <p> 评论 </p>
      </div>
    )
  }
}

数据是长这样子的:

8.评论列表组件NoteList

评论的数据拿到了,我们创建一个展示评论数据的组件NoteList,并且在Notes组件中调用

$ cd app/components/Notes && touch NoteList.jsx

ok,我们在Notes组件中使用NoteList,并将获取的notes数据传给它;

import React, { Component } from 'react';
import NoteList from './NoteList.jsx';

export default class Notes extends Component {
  render(){
    console.log('notes:', this.props.notes);

    return (
      <div>
        <p> 对{this.props.username}评论: </p>
        <NoteList notes={this.props.notes} />
      </div>
    )
  }
}

然后封装NoteList组件

import React, { Component } from 'react';

export default class NoteList extends Component {
  render(){
    let notes = this.props.notes.map((note, index) => {
      return <li className="list-group-item" key={index}>{note['.value']}</li>
    })
    return (
      <ul className="list-group">
        {notes}
      </ul>
    )
  }
}

现在,我们可以在浏览器中看到展示真实数据的Notes组件

9.为组件添加PropTypes接口约束

代码清单;app/components/Notes/Notes.jsx

import React, { Component, PropTypes } from 'react';
import NoteList from './NoteList.jsx';

export default class Notes extends Component {
  static propTypes = {
    username: PropTypes.string.isRequired,
    notes: PropTypes.array.isRequired
  }
  render(){
    console.log('notes:', this.props.notes);

    return (
      <div>
        <p> 对{this.props.username}评论: </p>
        <NoteList notes={this.props.notes} />
      </div>
    )
  }
}

代码清单;app/components/UserRepos/UserRepos.jsx

import React, { Component, PropTypes } from 'react';

export default class UserRepos extends Component {
  static propTypes = {
    username: PropTypes.string.isRequired,
    repos: PropTypes.array.isRequired
  }
  render(){
    return (
      <div>
        <p> GIT仓库 </p>
        <p> REPOS: {this.props.repos}</p>
      </div>
    )
  }
}

代码清单;app/components/UserProfile/UserProfile.jsx

import React, { Component, PropTypes } from 'react';

export default class UserProfile extends Component {
  static propTypes = {
    username: PropTypes.string.isRequired,
    bio: PropTypes.object.isRequired
  }
  render(){
    return (
      <div>
        <p> 基本信息 </p>
        <p> 姓名: {this.props.username} </p>
        <p> 介绍:{this.props.bio.name}</p>
      </div>
    )
  }
}

10.使用axios请求github API的数据

axios: Promise based HTTP client for the browser and node.js axios是一个能同时运行于浏览器端和nodejs的AJAX/HTTP方法/库

我们将用axios来请求github API的接口数据,用于组件的展示。下载:

$ npm install --save axios

首先写一个公共的工具函数来请求github上用户信息和仓库信息相关的数据。

$ cd app && mdkir util
$ cd util && touch helper.js

代码清单:app/util/helper.js

import axios from 'axios'

// axios用法很简单,请参考这里:https://github.com/mzabriskie/axios

/**
 * 传入用户名,获取用户的github上仓库信息
 * @param  {[type]} username [description]
 * @return {[type]}          [description]
 */
function getRepos(username){
  // 这里使用了 ES6 的字符串模板
  return axios.get(`https://api.github.com/users/${username}/repos`);
}

/**
 * 传入用户名,获取用户github上的基本信息
 * @param  {[type]} username [description]
 * @return {[type]}          [description]
 */
function getUserInfo(username){
  return axios.get(`https://api.github.com/users/${username}`);
}

export default function getGithubInfo(username){
  // 将请求回来的数据做了一个 merge 操作
  return axios.all([
    getRepos(username),
    getUserInfo(username)
  ])
  .then((arr) => ({
      repos: arr[0].data,
      bio: arr[1].data}
  ));
}

上面的代码中,我们使用axios发送请求到https://api.github.com,如果不太清楚通过github API获取数据的话,没关系,请到这里看一下:https://developer.github.com/v3。同时访问链接感受下:https://api.github.com/users/guoyongfeng。

现在我们在Profile组件中引入这个工具函数,并且传入用户名,查看返回的数据。

import React, { Component } from 'react';
import { UserProfile, UserRepos, Notes } from '../../components';
import { mixin } from 'core-decorators';
import ReactFireMixin from 'reactfire';
import Firebase from 'firebase';
import getGithubInfo from '../../util/helper';

@mixin(ReactFireMixin)
class Profile extends Component {
  state = {
    notes: ['1', '2', '3'],
    bio: {
      name: 'guoyongfeng'
    },
    repos: ['a', 'b', 'c']
  }
  componentDidMount(){
    // 为了读写数据,我们首先创建一个firebase数据库的引用
    this.ref = new Firebase('https://github-note-taker.firebaseio.com/');
    // 调用child来往引用地址后面追加请求,获取数据
    var childRef = this.ref.child(this.props.params.username);
    // 将获取的数据转换成数组并且赋给this.state.notes
    this.bindAsArray(childRef, 'notes');

    getGithubInfo( this.props.params.username )
      .then( ( data ) => {
        // 测试一下传入用户名后返回的数据
        console.log( data );
        // 更新state数据
        this.setState({
          bio: data.bio,
          repos: data.repos
        })
      });
  }
  componentWillUnMount(){
    this.unbind('notes');
  }
  render(){
    return (
      <div className="row">
        <div className="col-md-4">
          <UserProfile
            username={this.props.params.username}
            bio={this.state.bio} />
        </div>
        <div className="col-md-4">
          <UserRepos
            username={this.props.params.username}
            repos={this.state.repos} />
        </div>
        <div className="col-md-4">
          <Notes
            username={this.props.params.username}
            notes={this.state.notes} />
        </div>
      </div>
    )
  }
}

export default Profile

这里运行的时候会有个报错,以为之前在UserRepos组件中有这样一行代码:

<p> REPOS: {this.props.repos}</p>

repos是一个对象数组,直接展示会报错,暂时先去掉这行代码。

然后看浏览器返回的数据。

11.将数据传入组件进行展示

从第10步我们已经妥妥的把数据取回来了,那么接下来的事情将变得轻松,我们这需要将数据传入组件,修改组件的UI进行展示即可。甚至,你完全可以做个和github官网一样漂亮的首页。let's do it....

首先,来完善一下UserRepos组件吧 代码清单:app/components/UserRepos/UserRepos.jsx

import React, { Component, PropTypes } from 'react';

export default class UserRepos extends Component {
  static propTypes = {
    username: PropTypes.string.isRequired,
    repos: PropTypes.array.isRequired
  }
  render(){
    console.log('repos:', this.props.repos);

    let repos = this.props.repos.map( (repo, index ) => {
      return (
        <li className="list-group-item" key={index}>
          {repo.html_url && <h4><a href={repo.html_url}>{repo.name}</a></h4>}
          {repo.description && <p>{repo.description}</p>}
        </li>
      )
    });

    return <div>
      <h3> 用户的 Git 仓库 </h3>
      <ul className="list-group">
        {repos}
      </ul>
    </div>
  }
}

然后,完善一下UserProfile组件,代码清单:app/components/UserProfile/UserProfile.jsx

import React, { Component, PropTypes } from 'react';

export default class UserProfile extends Component {
  static propTypes = {
    username: PropTypes.string.isRequired,
    bio: PropTypes.object.isRequired
  }
  render(){
    let bio = this.props.bio;
    return (
      <div>
        <h3> 用户信息 </h3>
          {bio.avatar_url && <li className="list-group-item"> <img src={bio.avatar_url} className="img-rounded img-responsive"/></li>}
          {bio.name && <li className="list-group-item">Name: {bio.name}</li>}
          {bio.login && <li className="list-group-item">Username: {bio.login}</li>}
          {bio.email && <li className="list-group-item">Email: {bio.email}</li>}
          {bio.location && <li className="list-group-item">Location: {bio.location}</li>}
          {bio.company && <li className="list-group-item">Company: {bio.company}</li>}
          {bio.followers && <li className="list-group-item">Followers: {bio.followers}</li>}
          {bio.following && <li className="list-group-item">Following: {bio.following}</li>}
          {bio.public_repos && <li className="list-group-item">Public Repos: {bio.public_repos}</li>}
          {bio.blog && <li className="list-group-item">Blog: <a href={bio.blog}> {bio.blog}</a></li>}
      </div>
    )
  }
}

查看一下效果吧

结语

完整的示例代码在这里:https://github.com/GuoYongfeng/github-notetaker-app

本次课程内容比较丰富,请按课程步骤执行操作,遇到问题请请issue:https://github.com/GuoYongfeng/github-notetaker-app/issues