您可以捐助,支持我们的公益事业。

1元 10元 50元





认证码:  验证码,看不清楚?请点击刷新验证码 必填



  求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Modeler   Code  
会员   
 
   
 
 
     
   
 订阅
  捐助
一次React+Redux实践及思考
 
来源:segmentfault 发布于 2017-4-7
   次浏览      
 

这是笔者第一次将React+Redux应用到一个较为复杂的项目中,这个项目初期遇到最大的问题是以何种粒度进行组件拆分,因为该项目没有专配的UI,所以是程序猿直接按照自己的理解进行开发,在这种情况下,笔者习惯性的先写了一个包含菜单以及常见控制项的整体项目,然后再进行拆分。笔者在本文中就是把一些迭代和自己感受到的点扯扯。水文一篇,一笑而过。

首先来看下整个项目的大概功能与用户逻辑:

可以看出整个项目的分布上,分为五个角色,然后每个角色有一个单独的入口。为了保证一定的隔离性与代码的清晰性,笔者是分为了五个模块,然后在这五个模块里面对公共组件进行封装。总体而言,不同组件中表示同一功能的代码块都应该被抽出来形成独立的组件。组件之间的通信应该从Redux的Store进行。

另外,这边还有一个考虑,就是是否需要将所有的状态都放到Redux中进行统一管理。譬如在我们有一个创建接口的弹窗,大概就是下图这个样子:

这个组件相对而言还是独立的,其中的接口状态等暂时可以认为是不需要与其他组件交互的。那么到底要不要将它的状态,或者说要不要将创建API等等逻辑函数也提出来放到ActionCreator与Reducer中,感觉有点多此一举啊,毕竟对于一个Demo而言UnitTest与Time Travel好像都不是那么必须。不过,千里之堤毁于蚁穴,为了避免未来坑多,还是从零开始都规范一点吧。具体会在下文中的表单部分进行讨论

Project Structure

项目的总体目录情况如下:

Webpack Config

对于Webpack部分的详细配置与讲解可以参考Webpack-React-Redux-Boilerplate这个。

var path = require('path');
var webpack = require('webpack');

//PostCSS plugins
var autoprefixer = require('autoprefixer');

//webpack plugins
var ProvidePlugin = require('webpack/lib/ProvidePlugin');
var DefinePlugin = require('webpack/lib/DefinePlugin');
var CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');
var HtmlWebpackPlugin = require('html-webpack-plugin');
var CopyWebpackPlugin = require('copy-webpack-plugin');
var WebpackMd5Hash = require('webpack-md5-hash');
var ExtractTextPlugin = require("extract-text-webpack-plugin");

var NODE_ENV = process.env.NODE_ENV || "develop";//获取命令行变量

//@region 可配置区域

//定义统一的Application,不同的单页面会作为不同的Application
/**
* @function 开发状态下默认会把JS文本编译为main.bundle.js,然后使用根目录下dev.html作为调试文件.
* @type {*[]}
*/
var apps = [
{
//登录与注册
id: "login",//编号
title: "登录",//HTML文件标题
entry: {
name: "login",//该应用的入口名
src: "./src/modules/login/login_container.js",//该应用对应的入口文件
},//入口文件
indexPage: "./src/modules/login/login.html",//主页文件

//optional
dev: false,//判断是否当前正在调试,默认为false
compiled: true//判斷當前是否加入编译,默认为true
},
{
//内容管理
id: "content",//编号
title: "内容管理",//HTML文件标题
entry: {
name: "content",//该应用的入口名
src: "./src/modules/content/content.js"//该应用对应的入口文件
},//入口文件
indexPage: "./src/modules/content/content.html",//主页文件

//optional
dev: true,//判断是否当前正在调试,默认为false
compiled: true//判斷當前是否加入编译,默认为true
},
{
//权限管理
id: "auth",//编号
title: "权限管理",//HTML文件标题
entry: {
name: "auth",//该应用的入口名
src: "./src/modules/auth/auth.js"//该应用对应的入口文件
},//入口文件
indexPage: "./src/modules/auth/auth.html",//主页文件

//optional
dev: false,//判断是否当前正在调试,默认为false
compiled: true//判斷當前是否加入编译,默认为true
},
{
//密钥管理
id: "key",//编号
title: "密钥管理",//HTML文件标题
entry: {
name: "key",//该应用的入口名
src: "./src/modules/key/key.js"//该应用对应的入口文件
},//入口文件
indexPage: "./src/modules/key/key.html",//主页文件

//optional
dev: false,//判断是否当前正在调试,默认为false
compiled: true//判斷當前是否加入编译,默认为true
},
{
//超级管理
id: "admin",//编号
title: "权限管理",//HTML文件标题
entry: {
name: "admin",//该应用的入口名
src: "./src/modules/admin/admin.js"//该应用对应的入口文件
},//入口文件
indexPage: "./src/modules/admin/admin.html",//主页文件

//optional
dev: false,//判断是否当前正在调试,默认为false
compiled: false//判斷當前是否加入编译,默认为true
}
];

//定义非直接引用依赖
//定义第三方直接用Script引入而不需要打包的类库
//使用方式即为var $ = require("jquery")
const externals = {
jquery: "jQuery",
pageResponse: 'pageResponse'
};


/*********************************************************/
/*********************************************************/
/*下面属于静态配置部分,修改请谨慎*/
/*********************************************************/
/*********************************************************/

//开发时的入口考虑到热加载,只用数组形式,即每次只会加载一个文件
var devEntry = [
'eventsource-polyfill',
'webpack-hot-middleware/client'
];

//生产环境下考虑到方便编译成不同的文件名,所以使用数组
var proEntry = {
"vendors": "./src/vendors.js"//存放所有的公共文件
};

//定义HTML文件入口,默认的调试文件为src/index.html
var htmlPages = [];

//遍历定义好的app进行构造
apps.forEach(function (app) {

//判断是否加入编译
if (app.compiled === false) {
//如果还未开发好,就设置为false
return;
}

//添加入入口
proEntry[app.entry.name] = app.entry.src;

//构造HTML页面
htmlPages.push({
filename: app.id + ".html",
title: app.title,
// favicon: path.join(__dirname, 'assets/images/favicon.ico'),
template: 'underscore-template-loader!' + app.indexPage, //默认使用underscore
inject: false, // 使用自动插入JS脚本,
chunks: ["vendors", app.entry.name] //选定需要插入的chunk名
});

//判断是否为当前正在调试的
if (app.dev === true) {
//如果是当前正在调试的,则加入到devEntry
devEntry.push(app.entry.src);
}
});

//@endregion 可配置区域

//基本配置
var config = {
devtool: 'source-map',
//所有的出口文件,注意,所有的包括图片等本机被放置到了dist目录下,其他文件放置到static目录下
output: {
path: path.join(__dirname, 'dist'),//生成目录
filename: '[name].bundle.js',//文件名
sourceMapFilename: '[name].bundle.map'//映射名
},
//配置插件
plugins: [
// new WebpackMd5Hash(),//计算Hash插件
new webpack.optimize.OccurenceOrderPlugin(),
new webpack.DefinePlugin({
'process.env': {
//因为使用热加载,所以在开发状态下可能传入的环境变量为空
'NODE_ENV': process.env.NODE_ENV === undefined ? JSON.stringify('develop') : JSON.stringify(NODE_ENV)
// NODE_ENV: JSON.stringify('development')
},
//判断当前是否处于开发状态
__DEV__: process.env.NODE_ENV === undefined || process.env.NODE_ENV === "develop" ? JSON.stringify(true) : JSON.stringify(false)
}),

//提供者fetch Polyfill插件
new webpack.ProvidePlugin({
// 'fetch': 'imports?this=>global!exports?global.fetch!whatwg-fetch'
}),

//提取出所有的CSS代码
new ExtractTextPlugin('[name].css'),

//自动分割Vendor代码
new CommonsChunkPlugin({name: 'vendors', filename: 'vendors.bundle.js', minChunks: Infinity}),

//自动分割Chunk代码
// new CommonsChunkPlugin({
// children: true,
// async: true,
// })
],
module: {
loaders: [
{
test: /\.(js|jsx)$/,
exclude: /(libs|node_modules)/,
loaders: ["babel-loader"]
},
{
test: /\.(eot|woff|woff2|ttf|svg|png|jpe?g|gif)(\?\S*)?$/,
loader: 'url?limit=100000&name=[name].[ext]'
},
{
test: /\.vue$/,
loader: 'vue'
}
]
},
postcss: [autoprefixer({browsers: ['last 10 versions', "> 1%"]})],//使用postcss作为默认的CSS编译器
resolve: {
alias: {
libs: path.resolve(__dirname, 'libs'),
nm: path.resolve(__dirname, "node_modules"),
assets: path.resolve(__dirname, "assets"),
}
}
};

//进行脚本组装
config.externals = externals;

//自动创建HTML代码
htmlPages.forEach(function (p) {
config.plugins.push(new HtmlWebpackPlugin(p));
});

//为开发状态下添加插件
if (process.env.NODE_ENV === undefined || process.env.NODE_ENV === "develop") {

//配置SourceMap
config.devtool = 'cheap-module-eval-source-map';

config.module.loaders.push({
test: /\.(css|scss|sass)$/,
loader: "style-loader!css-loader!postcss-loader!sass?sourceMap"
});

//设置入口为调试入口
config.entry = devEntry;

//設置公共目錄名
config.output.publicPath = '/dist/'//公共目录名


//添加插件
config.plugins.push(new webpack.HotModuleReplacementPlugin());
config.plugins.push(new webpack.NoErrorsPlugin());

} else {
//如果是生产环境下
config.entry = proEntry;

//设置提取CSS文件的插件
config.module.loaders.push({
test: /\.(css|scss|sass)$/,
loader: ExtractTextPlugin.extract("style-loader", "css-loader!postcss-loader!sass?sourceMap")
});

//如果是生成环境下,将文件名加上hash
config.output.filename = '[name].bundle.js.[hash:8]';

//設置公共目錄名
config.output.publicPath = '/'//公共目录名

//添加代码压缩插件
config.plugins.push(
new webpack.optimize.UglifyJsPlugin({
compressor: {
warnings: false
}
}));

//添加MD5计算插件

//判断是否需要进行检查
if (process.env.CHECK === "true") {
config.module.loaders[0].loaders.push("eslint-loader");
}
}

module.exports = config;

Reducer

Redux本身的特点就是将原来的逻辑处理部分拆分到ActionCreator与Reducer中,而Reducer本身的层次关系又决定了State的结构。为了划分State中的层叠结构,笔者一开始是打算利用如下的方式:


import apiDataGridReducer from "../../../../app/components
/api/api_datagrid/api_datagrid.reducer";
import apiContentReducer from "../../../../app/components
/api/api_content/api_content.reducer";
import apiGroupReducer from "../../../../app/components
/api/api_group/api_group.reducer";
const defaultState = {

api_datagrid: {},

api_content: {},

api_group: {}

};

export default function reducer(state = defaultState, action) {

state = Object.assign({}, state, {
api_datagrid: apiDataGridReducer(state.api_datagrid, action)
});

state = Object.assign({}, state, {
api_content: apiContentReducer(state.api_content, action)
});

state = Object.assign({}, state, {
api_group: apiGroupReducer(state.api_group, action)
});

return state;

}

就是不停地将子部分的Reducer在父Reducer中进行合成,然后在模块的根reducer.js中引入父Reducer,不过这样后来感觉不太合适,譬如在内容管理员的部分,我只需要用到apiDataGridReducer,但是还不得不把其他的Reducer也引入。后来笔者改成了直接在根reducer.js中引入单个的Reducer,然后利用层叠调combineReducers方法:


rootReducer = combineReducers({
router, // redux-react-router reducer
account: combineReducers({
profile: combineReducers({
info, // reducer function
credentials // reducer function
}),
billing // reducer function
}),
// ... other combineReducers
})
});

表单

笔者一开始没有注意到表单这一点,后来做着做着发现整个项目的一个很大的组成部分就是各式各样的重复的表单

笔者建议使用redux-form,它比较好地将常见的表单操作结合到了一起,另一方面,它还能解决上文提到的一个Reducer问题,即是State的命名空间的嵌套问题。这部分的示例代码可以参考form

(1)使用npm安装redux-form

npm install --save redux-form

(2)将redux-form提供的formReducer挂载到rootReducer中

import {createStore, combineReducers} from 'redux';
import {reducer as formReducer} from 'redux-form';
const reducers = {
// ... your other reducers here ...
form: formReducer // <---- Mounted at 'form'. See note below.
}
const reducer = combineReducers(reducers);
const store = createStore(reducer);

(3)编写自定义form组件


import React, {Component} from 'react';
import {reduxForm} from 'redux-form';

class ContactForm extends Component {
render() {
const {fields: {firstName, lastName, email}, handleSubmit} = this.props;
return (
<form onSubmit={handleSubmit}>
<div>
<label>First Name</label>
<input type="text" placeholder="First Name" {...firstName}/>
</div>
<div>
<label>Last Name</label>
<input type="text" placeholder="Last Name" {...lastName}/>
</div>
<div>
<label>Email</label>
<input type="email" placeholder="Email" {...email}/>
</div>
<button type="submit">Submit</button>
</form>
);
}
}

ContactForm = reduxForm({ // <----- THIS IS THE IMPORTANT PART!
form: 'contact', // a unique name for this form
fields: ['firstName', 'lastName', 'email'] // all the fields in your form
})(ContactForm);

export default ContactForm;

 

   
次浏览       
相关文章 相关文档 相关课程



深度解析:清理烂代码
如何编写出拥抱变化的代码
重构-使代码更简洁优美
团队项目开发"编码规范"系列文章
重构-改善既有代码的设计
软件重构v2
代码整洁之道
高质量编程规范
基于HTML5客户端、Web端的应用开发
HTML 5+CSS 开发
嵌入式C高质量编程
C++高级编程
最新课程计划
信息架构建模(基于UML+EA)3-21[北京]
软件架构设计师 3-21[北京]
图数据库与知识图谱 3-25[北京]
业务架构设计 4-11[北京]
SysML和EA系统设计与建模 4-22[北京]
DoDAF规范、模型与实例 5-23[北京]

Android手机开发(一)
理解Javascript
非典型ajax实践
彻底的Ajax
javascript 使用Cookies
使用 jQuery 简化 Ajax 开发
更多...   

Struts+Spring+Hibernate
基于J2EE的Web 2.0应用开发
J2EE设计模式和性能调优
Java EE 5企业级架构设计
Java单元测试方法与技术
Java编程方法与技术

某航空公司IT部 JavaScript实践
某电视软件 HTML5和JavaScript
中航信 JavaScript高级应用开发
大庆油田 web界面Ajax开发技术
和利时 使用AJAX进行WEB应用开发
更多...