The code for this section is referred to ConfigurableAPIServer

It was the first time for the author to apply React+Redux to a relatively complex project. The biggest problem encountered in the initial stage of the project was how to split components at what granularity. Since the project did not have a customized UI, the programmer directly developed according to his own understanding. It is customary to write an overall project with menus and common controls, and then break it down. In this article, I’m just taking a few iterations and pulling them together. Hydrology, laughed it off.

First, let’s take a look at the general functions and user logic of the whole project:

You can see the distribution of the whole project, divided into five roles, and each role has a separate entrance. In order to ensure some isolation and clarity of code, the author divided it into five modules, and then encapsulated the common components in these five modules. In general, blocks of code representing the same function in different components should be pulled out to form separate components. Communication between components should take place from Redux’s Store.

Another consideration is whether all states need to be managed centrally in Redux. For example, we have a create interface popover that looks something like this:

This component is relatively independent, and its interface state and so on can be considered not to interact with other components for the time being. So whether to put its state or create API and other logical functions into ActionCreator and Reducer is a little unnecessary. After all, UnitTest and Time Travel seem not so necessary for a Demo. However, a thousand miles of dam is destroyed in the nest, in order to avoid the future pit more, or start from scratch are standard a little bit. The details are discussed in the forms section below

Project Structure

The overall project catalog is as follows:

  • / SRC Source directory

    • App main interface and general modules

      • Components Reusable components

        • .story is used to preview in StoryBook

        • API interface aspect component

          • Api.reducer. Js See the following for detailed discussion on reducer packages of some API components

          • Api_content API content management

            • Api_content.action.js related action and ActionCreator definitions

            • Api_content.js contains the Component in the Container definition

            • Api_content.scss style file

            • Api_content. Reducer. Js reducer for definition

      • Models model layer

        • Model.js common request encapsulation

        • API Data interaction component of the API

      • Service A common service layer

        • Url Indicates common URL filtering

        • Storage Common storage services

    • Modules standalone page

      • Content API Content management module

        • Components Related component definitions

          • API Repackaging of API components

        • Container Defines the root container and route

        • Reducers Encapsulate all reducers

        • Store is the definition of store

        • content.html

        • content.js

Webpack Config

See webpack-react-redux-Boilerplate for detailed configuration and explanation of Webpack.

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;Copy the code

Reducer

The characteristic of Redux itself is to split the original logical processing part into ActionCreator and Reducer, while Reducer’s hierarchical relationship determines the State structure. In order to divide the hierarchical structure in State, the author intended to use the following methods at the beginning:

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; }Copy the code

This is to continuously synthesize the Reducer of sub-parts into the parent Reducer, and then introduce the parent Reducer into the root Reducer. However, this is not appropriate later. For example, in the content manager part, I only need to use apiDataGridReducer. However, other Reducer had to be introduced as well. Later, the author changed to directly introduce a single reducer in the root reducer. Js, and then call the combineReducers method by cascading:


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

The form

I didn’t notice forms at first, but as I went along, I realized that a big part of the project was all sorts of repetitive forms

The author suggests using Redux-form, which combines common form operations together well. On the other hand, it can also solve the Reducer problem mentioned above, namely the nesting of State namespace. For example code in this section, refer to the form

(1) Install redux-Form using NPM


npm install --save redux-form
Copy the code

(2) Mount the formReducer provided by redux-form into 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);" store="createStore(reducer);Copy the code

(3) Write custom form components

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

class ContactForm extends Component {
  render() {
    const {fields: {firstName, lastName, email}, handleSubmit} = this.props;
    return (
      
        
       
First Name
Last Name
Email
Submit ); } } ContactForm = reduxForm({ // <----- this="" is="" the="" important="" part! ="" form:="" 'contact',="" a="" unique="" for="" form="" fields:="" ['firstname',="" 'lastname',="" 'email']="" all="" fields="" in="" your="" })(contactform); ="" export="" default contactform; <="" code="">Copy the code