ReactJS101 项目实战:构建基于 React + Redux + Router 的 GitHub 用户查询应用
前言
在掌握了 React 生态系统的核心概念后,如何将这些技术整合起来构建一个完整的应用是每个开发者都需要面对的挑战。本文将带领大家通过 ReactJS101 项目中的示例,使用 React + Redux + ImmutableJS + React Router 技术栈,结合第三方 API 开发一个功能完整的单页应用(SPA)。
应用功能概述
我们将构建一个具有以下功能的用户查询应用:
- 用户可以通过输入 ID 查询用户信息
- 展示查询结果包括:用户名、关注者数量、正在关注数量以及头像
- 提供返回首页功能
- 包含加载状态提示
技术选型分析
这个项目采用了现代前端开发的完整技术栈:
- 核心框架:React 作为视图层框架
- 状态管理:Redux 配合 Redux Thunk 处理异步操作
- 路由管理:React Router 实现前端路由
- 数据不可变性:ImmutableJS 确保状态不可变
- UI组件库:Material UI 提供美观的界面组件
- 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
配置文件设置
- Babel 配置 (.babelrc)
配置 JavaScript 转译规则,支持 ES2015 和 React JSX 语法:
{
"presets": ["es2015", "react"],
"plugins": []
}
- ESLint 配置 (.eslintrc)
设置代码规范检查规则,采用 Airbnb 风格指南:
{
"extends": "airbnb",
"rules": {
"react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }]
},
"env": {
"browser": true
}
}
- 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);
开发经验分享
- 异步操作处理:使用 Redux Thunk 中间件处理 API 请求,可以优雅地管理异步操作流程
- 不可变数据:ImmutableJS 确保状态不会被意外修改,提高应用的可预测性
- 组件分离:严格区分容器组件和展示组件,保持代码的清晰和可维护性
- UI一致性:Material UI 提供了一套设计规范的组件,确保应用界面风格统一
- 开发体验:配置完善的 ESLint 规则和热重载开发服务器,提升开发效率
总结
通过这个项目实战,我们完整地体验了现代前端应用的开发流程:
- 使用 React 构建用户界面
- 通过 Redux 管理应用状态
- 利用 React Router 处理前端路由
- 结合 ImmutableJS 确保数据不可变性
- 集成 Material UI 提供美观的界面组件
- 使用 Fetch API 与后端服务交互
这个项目虽然功能简单,但涵盖了现代前端开发的完整技术栈,是学习 React 生态系统的优秀实践案例。掌握了这些核心概念后,开发者可以进一步探索更复杂的应用场景,如服务器端渲染、性能优化等高级主题。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考