記一次改造react腳手架的過程
公司突然組織需要重新搭建一個基於node
的論壇系統,前端采用react
,上網找了一些腳手架,或多或少不能滿足自己的需求,最終在基於YeoMan
的react
腳手架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.js
的output
中配置
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
中數據的變化,包括state
的diff
,action
的發起情況等等,更有豐富的圖表展示,還可以自定義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;
}
}
提取共有模板文件
幾乎在所有組件中,我們都需要寫到connect
,mapStateToProps
等等,抽取出來,會顯得更加方便
// 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腳手架的過程