一次React+Redux實踐及思考

本部分的代碼參考ConfigurableAPIServercss

這是筆者第一次將React+Redux應用到一個較爲複雜的項目中,這個項目初期遇到最大的問題是以何種粒度進行組件拆分,由於該項目沒有專配的UI,因此是程序猿直接按照本身的理解進行開發,在這種狀況下,筆者習慣性的先寫了一個包含菜單以及常見控制項的總體項目,而後再進行拆分。筆者在本文中就是把一些迭代和本身感覺到的點扯扯。水文一篇,一笑而過。html

首先來看下整個項目的大概功能與用戶邏輯:vue

能夠看出整個項目的分佈上,分爲五個角色,而後每一個角色有一個單獨的入口。爲了保證必定的隔離性與代碼的清晰性,筆者是分爲了五個模塊,而後在這五個模塊裏面對公共組件進行封裝。整體而言,不一樣組件中表示同一功能的代碼塊都應該被抽出來造成獨立的組件。組件之間的通訊應該從Redux的Store進行。node

另外,這邊還有一個考慮,就是是否須要將全部的狀態都放到Redux中進行統一管理。譬如在咱們有一個建立接口的彈窗,大概就是下圖這個樣子:react

這個組件相對而言仍是獨立的,其中的接口狀態等暫時能夠認爲是不須要與其餘組件交互的。那麼到底要不要將它的狀態,或者說要不要將建立API等等邏輯函數也提出來放到ActionCreator與Reducer中,感受有點畫蛇添足啊,畢竟對於一個Demo而言UnitTest與Time Travel好像都不是那麼必須。不過,千里之堤毀於蟻穴,爲了不將來坑多,仍是從零開始都規範一點吧。具體會在下文中的表單部分進行討論jquery

Project Structure

項目的整體目錄狀況以下:webpack

  • /src 源代碼目錄git

    • app 主界面以及通用模塊github

      • components 可重用組件web

        • .story 用於在StoryBook中進行預覽

        • api 接口方面組件

          • api.reducer.js 對於api部分組件的Reducer的封裝,詳細討論見下面

          • api_content api內容管理

            • api_content.action.js 相關的Action與ActionCreator定義

            • api_content.js 包含Component於Container定義

            • api_content.scss 樣式文件

            • api_content.reducer.js Reducer定義

      • models 模型層

        • model.js 通用請求封裝

        • api api部分的數據交互組件

      • service 常見的服務層

        • url 常見的url過濾處理

        • storage 常見的存儲服務

    • modules 獨立頁面

      • content api內容管理模塊

        • components 相關組件定義

          • api 對於api組件的從新封裝

        • container 根容器以及路由定義

        • reducers 對於所有的reducer的封裝

        • store 對於跟Store的定義

        • content.html

        • content.js

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;
相關文章
相關標籤/搜索