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

