1. 程式人生 > >記一次改造react腳手架的過程

記一次改造react腳手架的過程

lease nts rule 加載過程 npm req ems ner comm

公司突然組織需要重新搭建一個基於node的論壇系統,前端采用react,上網找了一些腳手架,或多或少不能滿足自己的需求,最終在基於YeoManreact腳手架generator-react-webpack上搭建改造,這裏作為記錄。

代碼在這裏:github

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

簡單文件夾結構

├── 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,公共庫拆分

改造過程

拆分生產環境公共庫,生成文件hash

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`,這樣在新打包的文件不會被瀏覽器緩存策略而緩存

基本配置文件區分靜態文件目錄

{
    test: /\.(png|jpg|gif|ico|swf|xap)$/,
    loaders: [
        {
            loader: ‘file-loader‘,
            query: {
                name: ‘images/[name].[ext]‘
            }
        }
    ]
}

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

組件區分

├── bussiness
│   └── README.md
└── common
    ├── README.md
    ├── Template.js
    ├── YeomanImage.js
    └── button

主要區分業務組件和公共組件。當然你也可以不區分,引用常用的公共庫如螞蟻金服的react前端庫,進行改造。如果你需要自己寫組件的話,個人愚見還是區分一下。

加入immutable

加入這個看個人意願,加入之後必定會造成一定的學習以及開發成本,但是對redux來說,運用這個庫是再好不過的了,具體表現在數據的不可變性,即每次的數據都會是一個新的,不會在原始引用的數據上進行重新操作,以免造成數據汙染。

// 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,這個大家自行斟酌。

// 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中配置

chunkFilename: ‘chunk/[chunkhash].chunk.js‘,

這樣會生成快文件。生成塊主要用到了require.ensure或者() => import(‘xxx‘)來達到,下面我用到了一個庫react-loadable,可以配置組件加載過程中的過度頁面。

// 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

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,需FQ安裝

// 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等等,抽取出來,會顯得更加方便

// 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配置

// .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

遇到的一些坑

熱加載模板不起作用

即改變了一個視圖文件之後,並不會熱更新。主要是在client.js

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版本,升級一下即可

npm install react-hot-loader@3.0.0-beta.7

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

總結

以上只是個人的改造過程中的一些想法和實踐,並不是適用於所有人,拿出來和大家共同討論,比如認為可以建立redux文件夾,將actions/reducers/stores放在一起,比如路由可以分模塊化,比如每一個組件文件與樣式文件可以放在一起(包括視圖等等),再比如異步的action統一配置middleware處理錯誤情況等等。

記一次改造react腳手架的過程