Github查询应用 3个月前

代码工具
330
Github查询应用

前言

学了一身本领后,本章将带大家完成一个单页式应用程式(Single Page Application),整合 React + Redux + ImmutableJS + React Router 搭配 Github API 制作一个简单的 Github 使用者查询应用,实际体验一下开发 React App 的感受。

功能规划

让访客可以使用 Github ID 搜寻 Github 使用者,展示 Github 使用者名称、follower、following、avatar_url 并可以返回首页。

使用技术

  1. React
  2. Redux
  3. Redux Thunk
  4. React Router
  5. ImmutableJS
  6. Fetch
  7. Material UI
  8. Roboto Font from Google Font
  9. Github API(https://api.github.com/users/torvalds)

不过要注意的是 Github API 若没有使用 App key 的话可以呼叫 API 的次数会受限

专案成果截图

image

image

环境安装与设定

  1. 安装 Node 和 NPM

  2. 安装所需套件

$ npm install --save react react-dom redux react-redux react-router immutable redux-immutable redux-actions whatwg-fetch redux-thunk material-ui react-tap-event-plugin
$ npm install --save-dev babel-core babel-eslint babel-loader babel-preset-es2015 babel-preset-react babel-preset-stage-1 eslint eslint-config-airbnb eslint-loader eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react html-webpack-plugin webpack webpack-dev-server redux-logger

接下来我们先设定一下开发文档。

  1. 设定 Babel 的设定档: .babelrc

    {
        "presets": [
          "es2015",
          "react",
         ],
        "plugins": []
    }
    
  2. 设定 ESLint 的设定档和规则: .eslintrc

    {
      "extends": "airbnb",
      "rules": {
        "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }],
      },
      "env" :{
        "browser": true,
      }
    }
    
  3. 设定 Webpack 设定档: webpack.config.js

    // 让你可以动态插入 bundle 好的 .js 档到 .index.html
    const HtmlWebpackPlugin = require('html-webpack-plugin');
    
    const HTMLWebpackPluginConfig = new HtmlWebpackPlugin({
      template: `${__dirname}/src/index.html`,
      filename: 'index.html',
      inject: 'body',
    });
    
    // entry 为进入点,output 为进行完 eslint、babel loader 转译后的档案位置
    module.exports = {
      entry: [
        './src/index.js',
      ],
      output: {
        path: `${__dirname}/dist`,
        filename: 'index_bundle.js',
      },
      module: {
        preLoaders: [
          {
            test: /\.jsx$|\.js$/,
            loader: 'eslint-loader',
            include: `${__dirname}/src`,
            exclude: /bundle\.js$/
          }
        ],
        loaders: [{
          test: /\.js$/,
          exclude: /node_modules/,
          loader: 'babel-loader',
          query: {
            presets: ['es2015', 'react'],
          },
        }],
      },
      // 启动开发测试用 server 设定(不能用在 production)
      devServer: {
        inline: true,
        port: 8008,
      },
      plugins: [HTMLWebpackPluginConfig],
    };
    

太好了!这样我们就完成了开发环境的设定可以开始动手实作 Github Finder 应用程式了!

动手实作

  1. Setup Mockup

    HTML Markup(src/index.html):

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
        <title>GithubFinder</title>
        <link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500" rel="stylesheet">
    </head>
    <body>
        <div id="app"></div>
    </body>
    </html>
    

    设定 webpack.config.js 的进入点 src/index.js

    import React from 'react';
    import ReactDOM from 'react-dom';
    import { Provider } from 'react-redux';
    import { browserHistory, Router, Route, IndexRoute } from 'react-router';
    import injectTapEventPlugin from 'react-tap-event-plugin';
    import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
    import Main from './components/Main';
    import HomePageContainer from './containers/HomePageContainer';
    import ResultPageContainer from './containers/ResultPageContainer';
    import store from './store';
    
    // 引入 react-tap-event-plugin 避免 material-ui onTouchTap event 会遇到的问题
    // Needed for onTouchTap
    // http://stackoverflow.com/a/34015469/988941
    injectTapEventPlugin();
    
    // 用 react-redux 的 Provider 包起来将 store 传递下去,让每个 components 都可以存取到 state
    // 这边使用 browserHistory 当做 history,并使用 material-ui 的 MuiThemeProvider 包裹整个 components
    // 由于这边是简易的 App 我们设计了 Main 为母模版,其有两个子组件 HomePageContainer 和 ResultPageContainer,其中 HomePageContainer 为根位置的子组件
    ReactDOM.render(
      <Provider store={store}>
        <MuiThemeProvider>
          <Router history={browserHistory}>
            <Route path="/" component={Main}>
              <IndexRoute component={HomePageContainer} />
              <Route path="/result" component={ResultPageContainer} />
            </Route>
          </Router>
        </MuiThemeProvider>
      </Provider>,
      document.getElementById('app')
    );
    
  2. Actions

    首先先定义 actions 常数:

    export const SHOW_SPINNER = 'SHOW_SPINNER';
    export const HIDE_SPINNER = 'HIDE_SPINNER';
    export const GET_GITHUB_INITIATE = 'GET_GITHUB_INITIATE';
    export const GET_GITHUB_SUCCESS = 'GET_GITHUB_SUCCESS';
    export const GET_GITHUB_FAIL = 'GET_GITHUB_FAIL';
    export const CHAGE_USER_ID = 'CHAGE_USER_ID';
    

    现在我们来规划我们的 actions 的部份,这个范例我们使用到了 redux-thunk 来处理非同步的 action(若读者对于新的 Ajax 处理方式 fetch() 不熟悉可以先参考这个文件)。以下是 src/actions/githubActions.js 完整程式码:

    // 这边引入了 fetch 的 polyfill,考以让旧的浏览器也可以使用 fetch
    import 'whatwg-fetch';
    // 引入 actionTypes 常数
    import {
      GET_GITHUB_INITIATE,
      GET_GITHUB_SUCCESS,
      GET_GITHUB_FAIL,
      CHAGE_USER_ID,
    } from '../constants/actionTypes';
    
    // 引入 uiActions 的 action
    import {
      showSpinner,
      hideSpinner,
    } from './uiActions';
    
    // 这边是这个范例的重点,要学习我们之前尚未讲解的非同步 action 处理方式:不同于一般同步 action 直接发送 action,非同步 action 会回传一个带有 dispatch 参数的 function,里面使用了 Ajax(这里使用 fetch())进行处理
    // 一般和 API 互动的流程:INIT(开始请求/秀出 spinner)-> COMPLETE(完成请求/隐藏 spinner)-> ERROR(请求失败)
    // 这次我们虽然没有使用 redux-actions 但我们还是维持标准 Flux Standard Action 格式:{ type: '', payload: {} }
    
    export const getGithub = (userId = 'torvalds') => {
      return (dispatch) => {
        dispatch({ type: GET_GITHUB_INITIATE });
        dispatch(showSpinner());
        fetch('https://api.github.com/users/' + userId)
          .then(function(response) { return response.json() })
          .then(function(json) { 
            dispatch({ type: GET_GITHUB_SUCCESS, payload: { data: json } });
            dispatch(hideSpinner());
          })
          .catch(function(response) { dispatch({ type: GET_GITHUB_FAIL }) });
      } 
    }
    
    // 同步 actions 处理,回传 action 物件
    export const changeUserId = (text) => ({ type: CHAGE_USER_ID, payload: { userId: text } });
    

    以下是 src/actions/uiActions.js 负责处理 UI 的行为:

    import { createAction } from 'redux-actions';
    import {
      SHOW_SPINNER,
      HIDE_SPINNER,
    } from '../constants/actionTypes';
    
    // 同步 actions 处理,回传 action 物件
    export const showSpinner = () => ({ type: SHOW_SPINNER});
    export const hideSpinner = () => ({ type: HIDE_SPINNER});
    

    透过于 src/actions/index.js 将我们 actions 输出

    export * from './uiActions';
    export * from './githubActions';
    
  3. Reducers

    接下来我们要来设定一下 Reducers 和 models(initialState 格式)的设计,注意我们这个范例都是使用 ImmutableJS。以下是 src/constants/models.js

    import Immutable from 'immutable';
    
    export const UiState = Immutable.fromJS({
      spinnerVisible: false,
    });
    
    // 我们使用 userId 来暂存使用者 ID,data 存放 Ajax 取回的资料
    export const GithubState = Immutable.fromJS({
      userId: '',
      data: {},
    });
    

    以下是 src/reducers/data/githubReducers.js

    import { handleActions } from 'redux-actions';
    import { GithubState } from '../../constants/models';
    
    import {
      GET_GITHUB_INITIATE,
      GET_GITHUB_SUCCESS,
      GET_GITHUB_FAIL,
      CHAGE_USER_ID,
    } from '../../constants/actionTypes';
    
    const githubReducers = handleActions({ 
      // 当使用者按送出按钮,发出 GET_GITHUB_SUCCESS action 时将接收到的资料 merge 
      GET_GITHUB_SUCCESS: (state, { payload }) => (
        state.merge({
          data: payload.data,
        })
      ),  
      // 当使用者输入使用者 ID 会发出 CHAGE_USER_ID action 时将接收到的资料 merge 
      CHAGE_USER_ID: (state, { payload }) => (
        state.merge({
          'userId':
          payload.userId
        })
      ),
    }, GithubState);
    
    export default githubReducers;
    

    以下是 src/reducers/ui/uiReducers.js

    import { handleActions } from 'redux-actions';
    import { UiState } from '../../constants/models';
    
    import {
      SHOW_SPINNER,
      HIDE_SPINNER,
    } from '../../constants/actionTypes';
    
    // 随着 fetch 结果显示 spinner
    const uiReducers = handleActions({
      SHOW_SPINNER: (state) => (
        state.set(
          'spinnerVisible',
          true
        )
      ),
      HIDE_SPINNER: (state) => (
        state.set(
          'spinnerVisible',
          false
        )
      ),
    }, UiState);
    
    export default uiReducers;
    

    将 reduces 使用 redux-immutablecombineReducers 在一起。以下是 src/reducers/index.js

    import { combineReducers } from 'redux-immutable';
    import ui from './ui/uiReducers';// import routes from './routes';
    import github from './data/githubReducers';// import routes from './routes';
    
    const rootReducer = combineReducers({
      ui,
      github,
    });
    
    export default rootReducer;
    

    运用 redux 提供的 createStore API 把 rootReducerinitialStatemiddlewares 整合后创建出 store。以下是 src/store/configureSotore.js

    import { createStore, applyMiddleware } from 'redux';
    import reduxThunk from 'redux-thunk';
    import createLogger from 'redux-logger';
    import Immutable from 'immutable';
    import rootReducer from '../reducers';
    
    const initialState = Immutable.Map();
    
    export default createStore(
      rootReducer,
      initialState,
      applyMiddleware(reduxThunk, createLogger({ stateTransformer: state => state.toJS() }))
    );
    
  4. Build Component

    终于我们进入了 View 的细节设计,首先我们先针对母模版,也就是每个页面都会出现的 AppBar 做设计。以下是 src/components/Main/Main.js

    import React from 'react';
    // 引入 AppBar
    import AppBar from 'material-ui/AppBar';
    
    const Main = (props) => (
      <div>
        <AppBar
          title="Github Finder"
          showMenuIconButton={false}
        />
        <div>
          {props.children}
        </div>
      </div>
    );
    
    // 进行 propTypes 验证
    Main.propTypes = {
      children: React.PropTypes.object,
    };
    
    export default Main;
    

    以下是 src/components/ResultPage/HomePage.js

    import React from 'react';
    // 使用 react-router 的 Link 当做超连结,传送 userId 当作 query
    import { Link } from 'react-router';
    import RaisedButton from 'material-ui/RaisedButton';
    import TextField from 'material-ui/TextField';
    import IconButton from 'material-ui/IconButton';
    import FontIcon from 'material-ui/FontIcon';
    
    const HomePage = ({
      userId,
      onSubmitUserId,
      onChangeUserId,
    }) => (
      <div>
        <TextField
          hintText="Please Key in your Github User Id."
          onChange={onChangeUserId}
        />
        <Link to={{ 
          pathname: '/result',
          query: { userId: userId }
        }}>
          <RaisedButton label="Submit" onClick={onSubmitUserId(userId)} primary />
        </Link>
      </div>
    );
    
    export default HomePage;
    

    以下是 src/components/ResultPage/ResultPage.js,将 userId 当作 props 传给 <GithubBox />

```javascript
import React from 'react';
import GithubBox from '../../components/GithubBox';

const ResultPage = (props) => (
  <div> 
    <GithubBox data={props.data} userId={props.location.query.userId} />  
  </div>
);

export default ResultPage;
```

以下是 `src/components/GithubBox/GithubBox.js`,负责撷取的 Github 资料呈现:

```javascript
import React from 'react';
import { Link } from 'react-router';
// 引入 material-ui 的卡片式组件
import { Card, CardActions, CardHeader, CardMedia, CardTitle, CardText } from 'material-ui/Card';
// 引入 material-ui 的 RaisedButton
import RaisedButton from 'material-ui/RaisedButton';
// 引入 ActionHome icon
import ActionHome from 'material-ui/svg-icons/action/home';

const GithubBox = (props) => (
  <div>
    <Card>
      <CardHeader
        title={props.data.get('name')}
        subtitle={props.userId}
        avatar={props.data.get('avatar_url')}
      />
      <CardText>
        Followers : {props.data.get('followers')}
      </CardText>      
      <CardText>
        Following : {props.data.get('following')}
      </CardText>
      <CardActions>
        <Link to="/">
          <RaisedButton 
            label="Back" 
            icon={<ActionHome />}
            secondary={true} 
          />
        </Link>
      </CardActions>
    </Card> 
  </div>
);

export default GithubBox;
```
  1. Connect State to Component

    最后,我们要将 Container 和 Component 连接在一起(若忘记了,请先回去复习 Container 与 Presentational Components 入门!)。以下是 src/containers/HomePage/HomePage.js,负责将 userId 和使用到的事件处理方法用 props 传进 component :

    import { connect } from 'react-redux';
    import HomePage from '../../components/HomePage';
    
    import {
      getGithub,
      changeUserId,
    } from '../../actions';
    
    export default connect(
      (state) => ({
        userId: state.getIn(['github', 'userId']),
      }),
      (dispatch) => ({
        onChangeUserId: (event) => (
          dispatch(changeUserId(event.target.value))
        ),
        onSubmitUserId: (userId) => () => (
          dispatch(getGithub(userId))
        ),
      }),
      (stateProps, dispatchProps, ownProps) => {
        const { userId } = stateProps;
        const { onSubmitUserId } = dispatchProps;
        return Object.assign({}, stateProps, dispatchProps, ownProps, {
          onSubmitUserId: onSubmitUserId(userId),
        });
      }
    )(HomePage);
    

    以下是 src/containers/ResultPage/ResultPage.js

    import { connect } from 'react-redux';
    import ResultPage from '../../components/ResultPage';
    
    export default connect(
      (state) => ({
        data: state.getIn(['github', 'data'])    
      }),
      (dispatch) => ({})
    )(ResultPage);
    
  2. That's it

    若一切顺利的话,这时候你可以在终端机下 $ npm start 指令,然后在 http://localhost:8008 就可以看到你的努力成果囉!

    image

总结

本章带领读者们从零开始整合 React + Redux + ImmutableJS + React Router 搭配 Github API 制作一个简单的 Github 使用者查询应用。下一章我们将挑战进阶应用,学习 Server Side Rendering 方面的知识,并用 React + Redux + Node(Isomorphic)开发一个食谱分享网站。

延伸阅读

  1. Tutorial: build a weather app with React
  2. OpenWeatherMap
  3. Weather Icons
  4. Weather API Icons
  5. Material UI
  6. 【翻译】这个API很“迷人”——(新的Fetch API)
  7. Redux: trigger async data fetch on React view event
  8. Github API
  9. 传统 Ajax 已死,Fetch 永生
image
EchoEcho官方
无论前方如何,请不要后悔与我相遇。
1377
发布数
439
关注者
2223034
累计阅读

热门教程文档

Rust
84小节
Next
43小节
Docker
62小节
MySQL
34小节
C++
73小节