前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >react脚手架改造(react/react-router/redux/eslint/karam/immutable/es6/webpack/Redux DevTools)

react脚手架改造(react/react-router/redux/eslint/karam/immutable/es6/webpack/Redux DevTools)

作者头像
糊糊糊糊糊了
发布2018-05-09 16:00:05
1.7K0
发布2018-05-09 16:00:05
举报
文章被收录于专栏:糊一笑

公司突然组织需要重新搭建一个基于node的论坛系统,前端采用react,上网找了一些脚手架,或多或少不能满足自己的需求,最终在基于YeoManreact脚手架generator-react-webpack上搭建改造,这里作为记录。

代码在这里:github

另外推荐地址:react-starter-kit

简单文件夹结构

代码语言:javascript
复制
├── README.md                       # 项目README文件
├── conf                            # 配置文件夹
│   └── webpack                     # webpack配置(下面包括开发、生产、测试环境的配置)
├── karma.conf.js                   # karma测试配置文件
├── node_modules                    # 包文件夹
├── package.json                    # 包描述文件
├── src                             # 源文件夹
│   ├── actions                     # redux actions文件夹
│   ├── client.js                   # 客户端启动文件
│   ├── components                  # 项目组件(下面分为业务组件和公共组件)
│   ├── config                      # 环境配置文件夹(指明当前环境)
│   ├── containers                  # 入口容器
│   ├── exports.js                  # 常用组件的exports文件,可以忽略
│   ├── images                      # 图片
│   ├── index-release.html          # 生产环境模板文件
│   ├── index.html                  # 开发环境入口html
│   ├── reducers                    # redux reducers文件夹
│   ├── routes                      # 路由配置
│   ├── sources                     # 资源文件(可忽略)
│   ├── static                      # 静态文件(可以存放第三方库)
│   ├── stores                      # redux stores文件夹
│   ├── styles                      # 全局样式文件夹
│   └── views                       # 视图文件夹
├── test                            # 测试文件夹
│   ├── actions                     # 测试actions
│   ├── components                  # 测试组件
│   ├── config                      # 测试配置(检测环境)
│   ├── loadtests.js                # 加载测试文件
│   ├── reducers                    # 测试reducers
│   ├── sources                     # 测试资源(flux datasource)
│   └── stores                      # 测试stores
└── webpack.config.js               # webpack配置入口文件

整体应用技术

  • react
  • redux
  • react-router(4.0.0^,可以换成2x或者3x)
  • eslint
  • karma + mocha
  • immutable(可选)

在原始脚手架上新增

  • 路由(react-router)
  • 调试工具(react devTools)
  • 增加文件分类(images/fonts/media)
  • 生产配置增加文件hash,公共库拆分
  • 添加异步middleware,统一处理全局状态
  • 改造Actions/Reducers

改造过程

拆分生产环境公共库,生成文件hash

代码语言:javascript
复制
this.config = {
    cache: false,
    devtool: 'source-map',
    entry: {
        main: ['./client.js'],
        vendor: ['react', 'react-dom', 'redux', 'react-redux', 'react-router-dom',
                 'react-router-redux', 'react-css-modules', 'history']
    },
    plugins: [
        new webpack.DefinePlugin({
            'process.env.NODE_ENV': '"production"'
        }),
        new webpack.optimize.AggressiveMergingPlugin(),
        new webpack.NoEmitOnErrorsPlugin(),
        new webpack.optimize.CommonsChunkPlugin({
            name: 'vendor',
            minChunks: Infinity
        }),
        new HtmlWebpackPlugin({
            filename: path.resolve('./dist/index.html'),
            template: path.resolve('./src/index-release.html'),
            inject: 'body'
        })
    ]
};

this.config.output.filename = '[name].[chunkhash].js';

主要在entry上做了文章,将公共库分离成vendor,同时配合CommonsChunkPlugin进行代码抽离。最后将output的文件名加上chunkhash`,这样在新打包的文件不会被浏览器缓存策略而缓存

基本配置文件区分静态文件目录

代码语言:javascript
复制
{
    test: /\.(png|jpg|gif|ico|swf|xap)$/,
    loaders: [
        {
            loader: 'file-loader',
            query: {
                name: 'images/[name].[ext]'
            }
        }
    ]
}

主要使用query配置,区分不同文件目录。fonts/media相同道理配置即可

组件区分

代码语言:javascript
复制
├── bussiness
│   └── README.md
└── common
    ├── README.md
    ├── Template.js
    ├── YeomanImage.js
    └── button

主要区分业务组件和公共组件。当然你也可以不区分,引用常用的公共库如蚂蚁金服的react前端库,进行改造。如果你需要自己写组件的话,个人愚见还是区分一下。

加入immutable

加入这个看个人意愿,加入之后必定会造成一定的学习以及开发成本,但是对redux来说,运用这个库是再好不过的了,具体表现在数据的不可变性,即每次的数据都会是一个新的,不会在原始引用的数据上进行重新操作,以免造成数据污染。

代码语言:javascript
复制
// reducers/items.js
const initialState = fromJS({
    items: [
        {
            "forum_name": "武汉大学",
            "user_level": 12,
            "user_exp": 5301,
            "id": 30996,
            "is_like": 1,
            "favo_type": 2
        },
        // ...
    ]
});

function reducer(state = initialState, action) {
    switch (action.type) {
        case GET_ITEMS:
            return state;
        default:
            return state;

    }
}

// views/Home.js
render() {
    const list = items.get('items');
    // ...
    {
      list.map((l, index) => {
        return (
            <tr key={ `list${index}` }>
                <td>{ l.get('forum_name') }</td>
                <td>{ l.get('user_level') }</td>
                <td>{ l.get('user_exp') }</td>
                <td>{ l.get('is_like') === 0 ? '是' : '否' }</td>
                <td>{ l.get('favo_type') }</td>
            </tr>
          );
        })
    }
}

如果不清楚immutable,可以自行百度、谷歌。

使用路由,拆分views文件夹

加入react-router,脚手架中是没有生成路由的(可能有吧,只是楼主没有找到?)。于是加入配合react最紧密的react-router,官网的react-router已经到了4.x.x版本了,真是快呀。于是去了解了一下,这里只是做了基本的应用,如果用得不顺手,随时可以会退到2.x.x或者3.x.x,这个大家自行斟酌。

代码语言:javascript
复制
// routes/index.js
const routes = (
    <Switch>
        <Route exact path="/" component={ App } />
        <Route exact path="/home" component={ Home } />
        <Route exact path="/about" component={ About } />
        <Route exact path="/contact" component={ Contact } />
    </Switch>
);

// views
└── views
    ├── About.js
    ├── Contact.js
    ├── Home.js
    ├── README.md
    └── app

定义路由,加入exact代表所有路由唯一,即/about不会匹配到/,我的理解就是,不是子集路由。

组件分块加载

即用到该组件的时候才会加载组件,主要是在Base.jsoutput中配置

代码语言:javascript
复制
chunkFilename: 'chunk/[chunkhash].chunk.js',

这样会生成快文件。生成块主要用到了require.ensure或者() => import('xxx')来达到,下面我用到了一个库react-loadable,可以配置组件加载过程中的过度页面。

代码语言:javascript
复制
// async.template.js
import Loadable from 'react-loadable';
import MyLoadingComponent from './Loading';

const asyncTempalte = (loaderFunc) => {
    return Loadable({
        loader: loaderFunc,
        loading: MyLoadingComponent
    });
};

export default asyncTempalte;

// 在index.js中可以这样引用
const App = asyncTemplate(() => import("views/app/App"));
const Home = asyncTemplate(() => import("views/Home"));
const About = asyncTemplate(() => import("views/About"));
const Contact = asyncTemplate(() => import("views/Contact"));

store中配置router的reducers

代码语言:javascript
复制
import { createStore, combineReducers, applyMiddleware, compose } from 'redux';
import { routerReducer, routerMiddleware } from 'react-router-redux';
import config from 'config';
import reducers from '../reducers';

function reduxStore(history, initialState) {

    // Build the middleware for intercepting and dispatching navigation actions
    const rMiddleware = routerMiddleware(history);
    const middlewares = [rMiddleware];

    // ...

    const createStoreWithMiddleware = composeEnhancers(applyMiddleware(...middlewares));
    const store = createStoreWithMiddleware(createStore)(
        combineReducers({
            ...reducers,
            // 配置router reducers
            router: routerReducer
        }),
        initialState
    );

    if (module.hot) {
        // Enable Webpack hot module replacement for reducers
        module.hot.accept('../reducers', () => {
            // We need to require for hot reloading to work properly.
            const nextReducer = require('../reducers');  // eslint-disable-line global-require

            store.replaceReducer(nextReducer);
        });
    }

    return store;
}

export default reduxStore;

配合Redux DevTools展示store中数据的变化

配合Redux DevTools可以实时监控到store中数据的变化,包括statediffaction的发起情况等等,更有丰富的图表展示,还可以自定义actions,然后自行dispath。首先去谷歌安装插件Redux DevTools,需访问外国网站安装

代码语言:javascript
复制
// stores/index.js
import { createStore, combineReducers, applyMiddleware, compose } from 'redux';
// 可以另外加入redux-logger一起使用
import { createLogger } from 'redux-logger';

const loggerMiddleware = createLogger({ collapsed: true });
// 主要是这个函数
let composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;

function reduxStore(history, initialState) {
    // ...

    // 开发环境开启日志
    if (config.appEnv === 'dev') {
        middlewares.unshift(loggerMiddleware);
        composeEnhancers = composeEnhancers ? composeEnhancers : compose;
    } else {
        composeEnhancers = compose;
    }
}

提取共有模板文件

几乎在所有组件中,我们都需要写到connectmapStateToProps等等,抽取出来,会显得更加方便

代码语言:javascript
复制
// components/common/Template.js
import React, { Component } from 'react';
import { is, fromJS } from 'immutable';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import actions from 'actions/';

const Main = MyComponent => {
    class IndexTemplate extends Component {

        constructor(props, context) {
            super(props, context);
        }

        shouldComponentUpdate(nextProps, nextState) {
            return !is(fromJS(this.props), fromJS(nextProps))
                || !is(fromJS(this.state), fromJS(nextState));
        }

        render() {
            return <MyComponent { ...this.props } />;
        }
    }

    IndexTemplate.displayName = 'IndexTemplate';
    IndexTemplate.defaultProps = {};

    function mapStateToProps(state) {
        const { items } = state;
        return {
            items
        };
    }

    function mapDispatchToProps(dispatch) {
        return { actions: bindActionCreators(actions, dispatch) };
    }

    return withRouter(connect(mapStateToProps, mapDispatchToProps)(IndexTemplate));
};

export default Main;

eslint改造

楼主用的webstorm,所以首先开启eslint功能,其他IDE请大家自行百度。

具体路径:File -> Settings -> Languages & Frameworks -> Javascript -> Code Quality Tools -> Eslint,在右侧按钮开启即可。

添加.eslintignore和添加.eslintrc配置

代码语言:javascript
复制
// .eslintignore
node_modules/
dist/
src/static/
src/images/

// .eslintrc
{
    "parser": "babel-eslint",
    "env": {
        "browser": true,
        "node": true,
        "mocha": true
    },
    "extends": "airbnb",
    "rules": {
        "comma-dangle": ["off"],
        "import/extensions": 0,
        "no-unused-vars": ["warn"],
        "object-curly-spacing": ["off"],
        "padded-blocks": ["off"],
        "react/jsx-closing-bracket-location": ["off"],
        "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }],
        "react/jsx-space-before-closing": ["off"],
        "react/prefer-stateless-function": ["off"],
        "react/jsx-indent": ["error", 4],
        "react/jsx-curly-spacing": ["off"],
        "react/jsx-indent-props": ["error", 4],
        "no-underscore-dangle": [ "off"],
        "import/no-unresolved": ["error", {
            "ignore": [
                "config",
                "components/",
                "stores/",
                "actions/",
                "sources/",
                "styles/",
                "images/",
                "containers"
            ]
        }],
        "import/no-extraneous-dependencies": ["off", {
            "devDependencies": true,
            "optionalDependencies": false,
            "peerDependencies": false
        }],
        "indent": ["error", 4, {
            "SwitchCase": 1,
            "VariableDeclarator": 1,
            "outerIIFEBody": 1,
            "FunctionDeclaration": {
                "parameters": 1,
                "body": 1
            },
            "FunctionExpression": {
                "parameters": 1,
                "body": 1
            }
        }],
        "arrow-body-style": ["warn", "as-needed"],
        "max-len": ["warn", {
            "ignoreUrls": true,
            "ignoreStrings": true
        }],
        "no-script-url": ["warn"],
        "quote-props": ["warn", "as-needed"],
        "arrow-parens": ["error", "as-needed"]
    }
}

上面贴的是我个人的配置,如果不习惯,可以自己改造。

运行完成后,你可能会得到这样的截图,如果有error,编译将不能通过。

你可能会用到下面的地址:

eslint-rules

eslint-plugin-react

prop-types

改造Actions

actions文件夹中,新增了便捷创建action的方法,变化为:

代码语言:javascript
复制
// actions/getItem.js
// 之前在actions目录下会存在这个文件夹,现在已经融合成item.js
// 之前可能是这样的:
function action(parameter) {
    return { type: ADD_ITEM, parameter };
}

// actions/items.js
// 现在是这样的
const getItems = createAction(GET_ITEMS);

增加异步Actions支持,并配置全局状态

middlewares/apiMiddleware.js中使用axios进行接口请求,支持GET/POST,另支持jsonp方式,只需要如下调用即可:

代码语言:javascript
复制
// actions/items.js
const getAsyncItem = params => {
    return createAsyncAction(
        GET_ASYNC_ITEM_REQUEST,
        {
            [CALL_API]: {
                url: 'sug',
                method: 'get',
                type: 'jsonp',
                success: GET_ASYNC_ITEM_SUCCESS,
                params
            }
        }
    );
};

// views/About.js
this.props.actions.getAsyncItem({
    code: 'utf-8',
    q: '图书'
});

接口调用会在store中添加数据,之后可以这样调用:

代码语言:javascript
复制
// views/About.js
const { items } = this.props;
const { books } = items.get('books');

添加异步中间件,更新全局状态:

代码语言:javascript
复制
// middlewares/apiMiddleware.js
const apiMiddleware = ({ getStore }) => next => action => {
    // ...
    axios(url, p).then(response => {
        if (response.status >= 200 && response.status < 300) {
            return response;
        }
        // ...
        return throwError(response, response.statusText);
    })
    .then(res => {
        // ...
    })
    .then(data => {
        next(createAction(UPDATE_G_PROPERTY, LOAD_STATUS.COMPLETE)());
        return next(createAction(success, data)());
    });
    // ...
}

// 更新全局状态
next(createAction(UPDATE_G_PROPERTY, LOAD_STATUS.STATUS_ERROR)());

// reducers/g.js
/* eslint quote-props: 0 */

import { fromJS } from 'immutable';
import { handleActions } from 'redux-actions';
import { LOAD_STATUS } from '../middlewares/const';
import { UPDATE_G_PROPERTY } from '../actions/const';

const initialState = fromJS({
    loadStatus: LOAD_STATUS.REQUEST
});

const gReducers = handleActions({
    /**
     * 改变全局加载状态
     * @param state
     * @returns {*}
     */
    [UPDATE_G_PROPERTY](state, action) {
        return state.set('loadStatus', action.payload);
    }
}, initialState);

module.exports = gReducers;

全局状态现在只有接口loadStatus的状态,如果需要其他的,可以自行添加。

改造reducers的处理

引入了redux-actions库,其中对reducers的处理进行了很好的封装。而不是单调的使用switch/case来进行匹配,中间运用到了扁平化reducers以及我之前在深入redux中间件一文中的reduce函数。如果有兴趣,可以自行去看redux-actions的源码。

代码语言:javascript
复制
// reducers/g.js
// 之前可能是这样的
const gReducers = (state = initialState, action) {
    case UPDATE_G_PROPERTY: 
        return state.set('loadStatus', action.payload);
    default:
        return state;
}

// 改造之后是这样的
const gReducers = handleActions({
    [UPDATE_G_PROPERTY](state, action) {
        return state.set('loadStatus', action.payload);
    }
}, initialState);

记录自行发起的日志

主要是调整了stores/index.js中的日志中间件的位置,具体如下:

代码语言:javascript
复制
// 之前是这样的
middlewares.unshift(loggerMiddleware);

// after
middlewares.push(loggerMiddleware);

这样调整只要是在console控制台中的日志打印,如果是使用正常的actions发起的是可以正常记录的,但是类似如此的代码是记录不到的:

代码语言:javascript
复制
dispatch(action)

因为middlewares其实是层层嵌套的,因此action也会层层往下面传,大致的图是这样的:

代码语言:javascript
复制
middleware1 -> middleware2 -> ... -> middleware(n) -> action -> middleware(n) -> ... -> middleware2 -> middleware1

你可以点击我的图书,将会得到这样的记录:

遇到的一些坑

热加载模板不起作用

即改变了一个视图文件之后,并不会热更新。主要是在client.js

代码语言:javascript
复制
import React from 'react';
import ReactDOM from 'react-dom';
import { AppContainer } from 'react-hot-loader';
import App from 'containers/App';

ReactDOM.render(
    <AppContainer>
        <App />
    </AppContainer>,
    document.getElementById('app')
);

if (module.hot) {
    // module.hot.accept('./containers/App', () => {
        const NextApp = require('./containers/App').default; // eslint-disable-line global-require

        ReactDOM.render(
            <AppContainer>
                <NextApp />
            </AppContainer>,
            document.getElementById('app')
        );
    // });
}

需要注释掉module.hot.accept这一行代码,如果不注释,会报<Provider></Provider>不能被热加载的一些错误。具体原因暂不清楚。

React-hot-loader的wranning警告

之前为3.0.0-beta.6版本,升级一下即可

代码语言:javascript
复制
npm install react-hot-loader@3.0.0-beta.7

另外忽略一些想不起来的BUG

总结

以上只是个人的改造过程中的一些想法和实践,并不是适用于所有人,拿出来和大家共同讨论,比如认为可以建立redux文件夹,将actions/reducers/stores放在一起,比如路由可以分模块化,比如每一个组件文件与样式文件可以放在一起(包括视图等等),再比如异步的action统一配置middleware处理错误情况等等。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 简单文件夹结构
  • 整体应用技术
  • 在原始脚手架上新增
  • 改造过程
    • 拆分生产环境公共库,生成文件hash
      • 基本配置文件区分静态文件目录
        • 组件区分
          • 加入immutable
            • 使用路由,拆分views文件夹
              • 组件分块加载
            • store中配置router的reducers
              • 配合Redux DevTools展示store中数据的变化
                • 提取共有模板文件
                  • eslint改造
                    • 添加.eslintignore和添加.eslintrc配置
                  • 改造Actions
                    • 增加异步Actions支持,并配置全局状态
                      • 改造reducers的处理
                        • 记录自行发起的日志
                        • 遇到的一些坑
                          • 热加载模板不起作用
                            • React-hot-loader的wranning警告
                              • 另外忽略一些想不起来的BUG
                                • 总结
                                相关产品与服务
                                消息队列 TDMQ
                                消息队列 TDMQ (Tencent Distributed Message Queue)是腾讯基于 Apache Pulsar 自研的一个云原生消息中间件系列,其中包含兼容Pulsar、RabbitMQ、RocketMQ 等协议的消息队列子产品,得益于其底层计算与存储分离的架构,TDMQ 具备良好的弹性伸缩以及故障恢复能力。
                                领券
                                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档