webpack + react + react-router + redux最佳实践

构建环境

node 6.10.3, webpack 3.6.0, react 15.6.1, react-router 2.8.1
兼容IE9及以上主流浏览器,各类双核浏览器极速模式。

写在前面

每个人都掌握形形色色的前端框架,无论技术流行与否,各自都用的非常舒爽。但是时间长了,会发现有很多小问题即不影响大局,却又如鲠在喉。作为一个前端技术人员,实在无法忍受,于是重构的想法应运而生。
重构又带来很多业务代码不兼容等问题,所以本文所写一篇webpack1.x升级至3.x的经历。

项目介绍

公司之前的项目是基于webpack 1.8.5, react15.3.2, react-router2.8.1动态路由的react框架。是我认识唯一一个从事前端超过十年的大牛构建于16年初的项目,也是我前端初窥门径的奠基石。
因为包管理的紊乱,webpack配置管理紊乱,构建年代久远无法兼容一些新的功能等关系。终于忍痛对其进行了静默升级。

webpack配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
let ExtractTextPlugin = require('extract-text-webpack-plugin') //样式分离插件
module.exports = function (options) { // 通过配置分离开发,测试,生产打包。
return {
devtool: options.devTool, // 配置生成SourceMaps,选择合适的选项
entry: __dirname + '/config/react.app.jsx', // 唯一入口文件
output: {
path: __dirname + '/build/', // 打包后的文件存放的地方
filename: options.bundleHash ? 'assets/bundle-[hash].js' : 'bundle.js', //bundle命名是否混淆
chunkFilename: 'assets/chunk-[id]-[chunkhash].js',//webpack code splitting chunk命名
publicPath: options.publicPath // 打包公共路径
},
module: {// 各类loaders配置
loaders: [{
test: /\.(scss)$/,
loader: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: [
{loader: 'css-loader', options: {module: true}},
'postcss-loader',
'sass-loader'
]
})
}]
},
plugins: options.plugins, //webpack plugins 分离,方便管理
devServer: {
historyApiFallback: true, // spa单页不跳转
inline: true, //热更
proxy: { //代理
'/api': {
target: options.apiUrl,
changeOrigin: true
}
}
}
}
}

说明:没有贴loader全部loader,只贴了一个scss分离样式的loader,根据自己的需要配置相应的loader。

dev-options配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var util = require('./webpack.util') //工具包
var pluginsConfig = require('./webpack.plugins.config') //插件管理包
var config = require('config') //node config管理相关配置

module.exports = [
require('../webpack.config.js')({
devTool: 'eval-source-map',
dropConsole: false,
publicPath: 'http://' + util.getIp + ':' + config.devPort + '/assets/',
bundleHash: false,
plugins: pluginsConfig.getDevPlugins(),
apiUrl: config.apiUrl
})
]

util工具包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var os = require('os')

function getIp () {
var interfaces = os.networkInterfaces()
for (var k in interfaces) {
for (var k2 in interfaces[k]) {
var address = interfaces[k][k2]
if (address.family === 'IPv4' && !address.internal) {
return address.address
}
}
}
return '127.0.0.1'
}

module.exports = {
getIp: getIp()
}

webpack.plugins.config配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
let webpack = require('webpack')
let path = require('path')
let StatsPlugin = require('stats-webpack-plugin') //打包状态管理插件
let ExtractTextPlugin = require('extract-text-webpack-plugin') //样式分离插件
let OpenBrowserPlugin = require('open-browser-webpack-plugin') //自动弹出浏览器插件
let HtmlWebpackPlugin = require('html-webpack-plugin') //生成html插件
let OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin') //css压缩插件
let util = require('./webpack.util')
let getDevPlugins = function () { //开发模式插件管理
return [
new ExtractTextPlugin('main.css'),
new OpenBrowserPlugin({url: 'http://' + util.getIp + ':3000/'})
]
}

let getProdPlugins = function () { //生产模式插件管理
return [
new webpack.optimize.UglifyJsPlugin({// 压缩代码
compress: {
warnings: false,
drop_console: true
},
except: ['$super', '$', 'exports', 'require']// 排除关键字
}),
new StatsPlugin(path.join('stats.json'), {
chunkModules: true,
exclude: [
/node_modules[\\/]react(-router)?[\\/]/,
/node_modules[\\/]items-store[\\/]/
]
}),
new ExtractTextPlugin('assets/[name]-[contenthash].css',{allChunks: true}),
new OptimizeCssAssetsPlugin({
cssProcessor: require('cssnano'),
cssProcessorOptions: { discardComments: {removeAll: true } },
canPrint: true
}),
new HtmlWebpackPlugin({
filename: 'build.html',
template: path.join(__dirname, 'template.html'),
minify:false,
inject: 'body',
title: "****",
})
]
}


module.exports = {
getDevPlugins,
getProdPlugins,
}

react react-router配置

webpack打包入口即reactapp入口代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React from 'react'
import { Provider } from 'react-redux'
import ReactDom from 'react-dom'
import { Router, browserHistory } from 'react-router'
import store from '../app/redux/store'
import BaseApplication from './react.base.config/BaseApplication.jsx' //react组件基础容器

const rootRoute = { //根路由
name: 'app',
component: BaseApplication, //基础容器
childRoutes: [{ //子路由
childRoutes: [
require('../app/routers')
]
}]
}

ReactDom.render((
<Provider store={store}>
<Router history={browserHistory} routes={rootRoute} />
</Provider>
), document.getElementById('react'))

react组件基础容器代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import React from 'react'
import { connect } from 'react-redux'
import actions from '../../app/redux/actions'
import { dispatch } from '../../app/redux/store'
require('./BaseApplication.css')

class Application extends React.Component {
constructor (props) {
super(props)
dispatch(actions.setVars('landingUrl', window.location.href))
}

render () {
return (
<div className={'noSelect'}>
{this.props.children}
</div>
)
}
}

const mapStateToProps = (state) => {
return {}
}

export default connect(mapStateToProps)(Application)

根据实际需要来做基础容器(可以写一些通用的方法)。也可以是一个空容器,即用来加载子路由组件即可。

react-router子路由配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module.exports = {
path: 'user',
childRoutes: [
require('./content'),
{
path: 'login',
getComponent (nextState, cb) {
require.ensure([], (require) => {
cb(null, require('./Login.jsx').default)
})
}
}
]
}

webpack会根据require.ensure来进行代码分割,把请求到的代码划分为一个chunk文件。这里webpack1.x和2.x与3.x是有区别的。webpack1.x和2.x不需要添加

cb(null, require(‘./Login.jsx’).default)
.default

而3.x必须添加.default如不添加则会找不到组件且不报错。这个坑处理了很久。

github地址