ReactJS101 项目实战:构建基于 React + Redux + Router 的 GitHub 用户查询应用

ReactJS101 项目实战:构建基于 React + Redux + Router 的 GitHub 用户查询应用

reactjs101 kdchang/reactjs101: 是一个关于 ReactJS 的前端开发教程项目。适合对前端开发有兴趣的人,特别是想学习 ReactJS 框架的人。特点是从基础概念和示例代码入手,逐步深入到 ReactJS 的各种高级特性,具有较强的实践性和指导性。 reactjs101 项目地址: https://gitcode.com/gh_mirrors/re/reactjs101

前言

在掌握了 React 生态系统的核心概念后,如何将这些技术整合起来构建一个完整的应用是每个开发者都需要面对的挑战。本文将带领大家通过 ReactJS101 项目中的示例,使用 React + Redux + ImmutableJS + React Router 技术栈,结合第三方 API 开发一个功能完整的单页应用(SPA)。

应用功能概述

我们将构建一个具有以下功能的用户查询应用:

  1. 用户可以通过输入 ID 查询用户信息
  2. 展示查询结果包括:用户名、关注者数量、正在关注数量以及头像
  3. 提供返回首页功能
  4. 包含加载状态提示

技术选型分析

这个项目采用了现代前端开发的完整技术栈:

  1. 核心框架:React 作为视图层框架
  2. 状态管理:Redux 配合 Redux Thunk 处理异步操作
  3. 路由管理:React Router 实现前端路由
  4. 数据不可变性:ImmutableJS 确保状态不可变
  5. UI组件库:Material UI 提供美观的界面组件
  6. API调用:使用 Fetch API 进行网络请求

开发环境配置

基础依赖安装

首先需要安装项目所需的各种依赖:

# 核心依赖
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)
    配置 JavaScript 转译规则,支持 ES2015 和 React JSX 语法:
{
  "presets": ["es2015", "react"],
  "plugins": []
}
  1. ESLint 配置 (.eslintrc)
    设置代码规范检查规则,采用 Airbnb 风格指南:
{
  "extends": "airbnb",
  "rules": {
    "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }]
  },
  "env": {
    "browser": true
  }
}
  1. Webpack 配置 (webpack.config.js)
    配置模块打包规则和开发服务器:
const HtmlWebpackPlugin = require('html-webpack-plugin');

const HTMLWebpackPluginConfig = new HtmlWebpackPlugin({
  template: `${__dirname}/src/index.html`,
  filename: 'index.html',
  inject: 'body',
});

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'],
      },
    }],
  },
  devServer: {
    inline: true,
    port: 8008,
  },
  plugins: [HTMLWebpackPluginConfig],
};

应用架构设计

1. 入口文件设置

应用的入口文件 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';

// 解决 Material-UI 的触摸事件问题
injectTapEventPlugin();

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. 状态管理设计

Action 设计

定义应用的所有行为类型:

// src/constants/actionTypes.js
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';

实现异步 Action 处理用户数据获取:

// src/actions/githubActions.js
import 'whatwg-fetch';
import {
  GET_GITHUB_INITIATE,
  GET_GITHUB_SUCCESS,
  GET_GITHUB_FAIL,
  CHAGE_USER_ID,
} from '../constants/actionTypes';
import { showSpinner, hideSpinner } from './uiActions';

export const getGithub = (userId = 'torvalds') => {
  return (dispatch) => {
    dispatch({ type: GET_GITHUB_INITIATE });
    dispatch(showSpinner());
    fetch(`https://api.github.com/users/${userId}`)
      .then(response => response.json())
      .then(json => {
        dispatch({ type: GET_GITHUB_SUCCESS, payload: { data: json } });
        dispatch(hideSpinner());
      })
      .catch(response => dispatch({ type: GET_GITHUB_FAIL }));
  };
};

export const changeUserId = text => ({ 
  type: CHAGE_USER_ID, 
  payload: { userId: text } 
});
Reducer 设计

使用 ImmutableJS 定义初始状态:

// src/constants/models.js
import Immutable from 'immutable';

export const UiState = Immutable.fromJS({
  spinnerVisible: false,
});

export const GithubState = Immutable.fromJS({
  userId: '',
  data: {},
});

实现 Reducer 处理状态变化:

// src/reducers/data/githubReducers.js
import { handleActions } from 'redux-actions';
import { GithubState } from '../../constants/models';
import {
  GET_GITHUB_SUCCESS,
  CHAGE_USER_ID,
} from '../../constants/actionTypes';

const githubReducers = handleActions({ 
  GET_GITHUB_SUCCESS: (state, { payload }) => (
    state.merge({
      data: payload.data,
    })
  ),  
  CHAGE_USER_ID: (state, { payload }) => (
    state.merge({
      userId: payload.userId
    })
  ),
}, GithubState);

export default githubReducers;
Store 配置

创建 Redux 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() 
  }))
);

3. 组件设计与实现

主布局组件
// src/components/Main/Main.js
import React from 'react';
import AppBar from 'material-ui/AppBar';

const Main = ({ children }) => (
  <div>
    <AppBar
      title="用户查询应用"
      showMenuIconButton={false}
    />
    <div>{children}</div>
  </div>
);

Main.propTypes = {
  children: React.PropTypes.object,
};

export default Main;
首页组件
// src/components/HomePage/HomePage.js
import React from 'react';
import { Link } from 'react-router';
import RaisedButton from 'material-ui/RaisedButton';
import TextField from 'material-ui/TextField';

const HomePage = ({ userId, onSubmitUserId, onChangeUserId }) => (
  <div>
    <TextField
      hintText="请输入用户ID"
      onChange={onChangeUserId}
    />
    <Link to={{ pathname: '/result', query: { userId } }}>
      <RaisedButton label="查询" onClick={onSubmitUserId(userId)} primary />
    </Link>
  </div>
);

export default HomePage;
结果展示组件
// src/components/GithubBox/GithubBox.js
import React from 'react';
import { Link } from 'react-router';
import { Card, CardHeader, CardText, CardActions } from 'material-ui/Card';
import RaisedButton from 'material-ui/RaisedButton';
import ActionHome from 'material-ui/svg-icons/action/home';

const GithubBox = ({ data, userId }) => (
  <div>
    <Card>
      <CardHeader
        title={data.get('name')}
        subtitle={userId}
        avatar={data.get('avatar_url')}
      />
      <CardText>关注者: {data.get('followers')}</CardText>      
      <CardText>正在关注: {data.get('following')}</CardText>
      <CardActions>
        <Link to="/">
          <RaisedButton 
            label="返回" 
            icon={<ActionHome />}
            secondary 
          />
        </Link>
      </CardActions>
    </Card> 
  </div>
);

export default GithubBox;

4. 容器组件连接

首页容器
// src/containers/HomePageContainer.js
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) => ({
    ...stateProps,
    ...dispatchProps,
    onSubmitUserId: dispatchProps.onSubmitUserId(stateProps.userId),
  })
)(HomePage);
结果页容器
// src/containers/ResultPageContainer.js
import { connect } from 'react-redux';
import ResultPage from '../components/ResultPage';

export default connect(
  state => ({
    data: state.getIn(['github', 'data'])    
  })
)(ResultPage);

开发经验分享

  1. 异步操作处理:使用 Redux Thunk 中间件处理 API 请求,可以优雅地管理异步操作流程
  2. 不可变数据:ImmutableJS 确保状态不会被意外修改,提高应用的可预测性
  3. 组件分离:严格区分容器组件和展示组件,保持代码的清晰和可维护性
  4. UI一致性:Material UI 提供了一套设计规范的组件,确保应用界面风格统一
  5. 开发体验:配置完善的 ESLint 规则和热重载开发服务器,提升开发效率

总结

通过这个项目实战,我们完整地体验了现代前端应用的开发流程:

  1. 使用 React 构建用户界面
  2. 通过 Redux 管理应用状态
  3. 利用 React Router 处理前端路由
  4. 结合 ImmutableJS 确保数据不可变性
  5. 集成 Material UI 提供美观的界面组件
  6. 使用 Fetch API 与后端服务交互

这个项目虽然功能简单,但涵盖了现代前端开发的完整技术栈,是学习 React 生态系统的优秀实践案例。掌握了这些核心概念后,开发者可以进一步探索更复杂的应用场景,如服务器端渲染、性能优化等高级主题。

reactjs101 kdchang/reactjs101: 是一个关于 ReactJS 的前端开发教程项目。适合对前端开发有兴趣的人,特别是想学习 ReactJS 框架的人。特点是从基础概念和示例代码入手,逐步深入到 ReactJS 的各种高级特性,具有较强的实践性和指导性。 reactjs101 项目地址: https://gitcode.com/gh_mirrors/re/reactjs101

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

丁骥治

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值