如何从零搭建一个react + react-router + webpack框架

导语:学习搭建react +webpack工程时,看了很多资料,学习点真的很多,几乎每一项都可以单独写一个篇幅,该文章只在怎么搭建出开发框架,很多东西没有深入,只做了一些简单的介绍,但基本会把官方文档列出来,想要深入了解的可以先自己把工程跑起来,然后认真看官方文档。

我们想要的框架需要什么样的功能:

1、使用webpack打包
3、入口JS文件可以自动注入到HTML模板中
2、使用ES6 react语法
4、可以按需提取打包公用JS
3、使用SCSS,且想要框架自动对需要加兼容的CSS属性做处理
3、单页应用,需要react+router路由
4、单页应用,需要考虑各页面的JS按需加载
5、组件热更新,加快开发速度
5、生产环境下,希望图片能自动压缩,且小于3K的自动转成baseuri格式,CSS可以单独提取出来,且JS CSS可以压缩
6、可以查看打包信息进行打包模块优化

搭建步骤

1、初始化一个npm或yarn工程

执行以下命令,创建一个webpack-react目录,并初始化

mkdir webpack-react
cd webpack-react
npm init
// 若使用yarn,则执行
yarn init

执行完成后,会在webpack-react目录下生成一个package.json文件,文件中有初始化时你填入的内容

2、使用webpack

  1. 安装webpack(使用webpack 3)
    首先需要在你的工程中安装webpack,执行命令:
npm install webpack --save-dev  
// 或
yarn add webpack --dev

注:--save-dev参数会让依赖包添加到package.json文件中的devDependencies中
devDependencies与dependencies的区别是:
在其他工程引入你的包时,添加到dependencies中的依赖,会被自动下载。而devDependencies中的依赖不会,devDependencies表示只在该工程开发环境时需要。

  1. 配制及运行webpack
    webpack有很多的配制参数,我们在运行webpack命令时,webpack会自动去项目根目录寻找webpack.config.js文件,也可指定配制文件,如运行命令
webpack --config mycofing.js

在我们的项目webpack-react目录下创建文件webpack.config.js,并写入以下内容:

module.exports = {
    // 入口文件
    entry: {
        app: './src/index.js'
    },
    output: {
        // chunkhash hash的区别:hash是所有输出文件共用一个hash,chunkhash是不同文件是不同的hash,可以用这个做缓存
        // 是入口文件的输出名字
        filename: '[name].[hash:4].bundle.js',
        // 输出绝对路径
        path: path.resolve(__dirname, 'dist'),
    }
}

在项目根目录下再创建一个src目录,用来存放我们的JS文件
在src目录下创建一个index.js,里面随便写入一些JS代码,但暂时不要使用ES6语法,因为目前整个框架还没有做对ES6语法支持的配制。
执行命令:

webpack

此时会在你的项目根目录下生成一个dist文件夹,dist文件夹中会生成一个app.js文件
我们只需要在HTML文件中引用app.js文件即可。

3、入口JS文件自动注入到HTML模板中

有时我们生成的JS入口文件的名字是变化的,如上面output参数中配制filename: [name].[hash:4].bundle.js,这样我们每次改动后重新打包,JS文件名都会变化,我们每次都得重新修改HTML。
webpack给我们提供了一个插件来帮助我们解决这个问题:HtmlWebpackPlugin

  1. 安装HtmlWebpackPlugin
npm install HtmlWebpackPlugin --save-dev
  1. 在src目录下创建一个index.html,作为html模板
<html>
  <head>
    <title>webpack配制学习</title>
  </head>
  <body>
  </body>
</html>
  1. 配制,修改webpack.config.js文件
// 处理HTML,可以将所有的入口文件注册到HTML模板中
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    // 入口文件
    entry: {
        app: './src/index.js'
    },
    output: {
        // chunkhash hash的区别:hash是所有输出文件共用一个hash,chunkhash是不同文件是不同的hash,可以用这个做缓存
        // 是入口文件的输出名字
        filename: '[name].[hash:4].bundle.js',
        // 输出绝对路径
        path: path.resolve(__dirname, 'dist'),
    },
    plugins: [
        new HtmlWebpackPlugin({
            title: 'webpack配制学习',
            // 指定打包出来的html的名字,默认是在output指定的path路径下创建一个叫index.html文件
            // filename: 'test/index.html',
            // 指定模板,也可以指定模板的loader,如handlebars来加载解析这种模板,也可以在module.loaders中指定
            // template: '!!handlebars!src/index.hbs',
            template: 'src/index.html',
            // minify: {
            //     html5: true
            // }
            // 若为true会在引入的JS后面加上?hash
            // hash: true,
            // cache: false,
        })
    ]
}
  1. 运行webpack命令后,你会发现在dist文件夹下会自动生成一个index.html文件,文件内会在body标签中自动注入一个script标签,src指向新生成的bundle.js文件

注:当我们执行了多次命令后,会发现在dist文件下生成了多个文件,为了方便查看我们最新生成的文件,可以在执行webpack命令前执行以下命令:rm -rf dist。我们可以将这些命令写入到package.json 文件的script脚本中,如:

"scripts": {
    "start": "rm -rf dist && webpack"
  }

这样我们可以直接在命令行中执行:npm start即可

4、使用react、ES6语法

在往下之前,我们再改造一下我们的启动脚本,在本地使用服务器的方式运行我们的页面。修改package.json文件:

"scripts": {
    "start": "webpack-dev-server",
    "build": "rm -rf dist && webpack"
  }

在开发环境使用webpack-dev-server,很方便,我们不用自己去启用一个node服务器。想要了解更多webpack-dev-server的配制,可以参考官方文档https://doc.webpack-china.org/guides/development/#-webpack-dev-server,这里不做过多的说明。

因为ES6及更高的JS语法如类属性等,有些浏览器是不支持的,我们需要使用 babel 做一个转换。这里用到了webpack的loader配制选项,官方推荐的loader列表:https://doc.webpack-china.org/loaders/babel-loader/

  1. 安装依赖包:
// babel
yarn add babel-loader babel-core --dev
// babel插件
yarn add babel-preset-es2015 babel-preset-react babel-plugin-transform-class-properties --dev
// react,因是项目依赖的包,放入dependencies中
yarn add react react-dom --save

注:babel有很多插件可以安装使用,具体可参考:https://babeljs.io/docs/plugins/

  1. 配制、修改webpack.config.js文件
// 处理HTML,可以将所有的入口文件注册到HTML模板中
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    // 入口文件
    entry: {
        app: './src/index.js'
    },
    output: {
        // chunkhash hash的区别:hash是所有输出文件共用一个hash,chunkhash是不同文件是不同的hash,可以用这个做缓存
        // 是入口文件的输出名字
        filename: '[name].[hash:4].bundle.js',
        // 输出绝对路径
        path: path.resolve(__dirname, 'dist'),
    },
    module: {
        rules: [
            {
                // .js 或.jsx格式的文件都会使用下面配制的loader去解析
                test: /\.jsx?$/,
                exclude: /node_modules/,
                use: [
                    // 使用babel-loader解析
                    {
                        loader: 'babel-loader',
                        options: {
                            // 支持react ES6语法
                            presets: ['react', 'es2015'],
                            plugins: [
                                // 支持calss属性
                                'transform-class-properties'
                            ]
                        }
                    }
                ]
            }
        ]
    },
    plugins: [
        new HtmlWebpackPlugin({
            title: 'webpack配制学习',
            // 指定打包出来的html的名字,默认是在output指定的path路径下创建一个叫index.html文件
            // filename: 'test/index.html',
            // 指定模板,也可以指定模板的loader,如handlebars来加载解析这种模板,也可以在module.loaders中指定
            // template: '!!handlebars!src/index.hbs',
            template: 'src/index.html',
            // minify: {
            //     html5: true
            // }
            // 若为true会在引入的JS后面加上?hash
            // hash: true,
            // cache: false,
        })
    ]
}

以上配制指明了.js或.jsx文件将会先使用babel-loader进行转换,babel-loader中指明了支持es6 react语法,以及calss的属性

  1. 在JS中使用react、ES6语法,例:
import React from 'react';

export default class Test extends React.Component {
    static propTypes = {
        name: React.PropTypes.string,
    }
    render() {
        return (
            <div>This is test page!</div>
        );
    }
}

5、按需提取打包公用JS

当我们项目中模块增多时,很多不同的模块中都引入了相同的模块,比如react包,此时我们需要将一些公用的模块提取出来,单独打包,此时会用到webpack的插件:CommonsChunkPlugin
CommonsChunkPlugin有多种配制方式,常用的一种是你自己判断哪些文件是公用的,配制到入口文件中,如:

    // 入口文件
    entry: {
        app: './src/index.js',
        vendor: [
            'react',
            'react-dom'
        ]
    },
    ...
    plugins: [
        // 将多个入口文件中公用的模块提取出来一个单独的文件,方便浏览器做缓存,可以有多个
        new Webpack.optimize.CommonsChunkPlugin({
            // 若什么都不配制,只配制一个公用模块的名字,则会把所有【入口文件(entry中配制的入口文件)】依赖的公用模块都提取到公用模块中
            name: ['vendor', 'manifest'],
            // ?还没明白这个参数的用法
            // names: ['lodash', 'test'],
            // filename: 'vender.[hash:4].bundle.js',
            // 如: 3,指定当有几个文件共用的模块才需要提取,当Infinity保证只打指定的文件进来
            minChunks: Infinity,
            // 指定需要提取哪些入口文件中的公用模块
            // chunks: [],
            // 公共文件的文件大小的最小值
            minSize: 1024
        }),
    ]

更多配制参考:https://doc.webpack-china.org/plugins/commons-chunk-plugin/
另外,也可以不指定公用vendor文件,而是配制minChunks参数,指定当模块重复使用大于多少时提取

6、处理图片,及使用SCSS、autoprefixer处理CSS

我们的项目决定使用scss来做CSS的预处理,且希望使用autoprefixer使框架自动对CSS属性加上指定浏览器的兼容。我们使用webpack的loader来解决这些问题。

  1. 安装需要的loader
yarn add sass-loader node-sass css-loader style-loader --dev
yarn add postcss-loader autoprefixer --dev
// url-loader处理图片
yarn add url-loader --dev

注:
sass-loader:处理scss语法
node-sass:sass-loader的依赖
css-loader:CSS模块化解析,主要可以处理CSS中的@import 和 url()
style-loader:将JS中引入的CSS文件插入到HTML文件的header的style标签中
postcss-loader:处理CSS的一个平台,有很多基于他的插件,这里主要是用来使用autoprefixer
autoprefixer:对CSS属性加上指定浏览器的兼容
url-loader: 处理图片的加载,且可以指定小于多少的图片自动转成baseURI格式。

  1. 修改、配制webpack.config.js
...
      module: {
        rules: [
          ...
            {
                // .scss或.css文件使用下面的loader
                test: /\.(scss|css)$/,
                // loader使用顺序,postcss-loader --> sass-loader  --> css-loader --> style-loader
                use: ['style-loader', 'css-loader', 'sass-loader', {
                    loader: 'postcss-loader',
                    options: {
                        plugins: [require('autoprefixer')]
                    }
                }]
            },
            {
                // 处理图片格式的文件
                test: /\.(png|jpe?g||git)$/,
                use: [{
                    loader: 'url-loader',
                    options: {
                        // 小于8192K的图片转成baseURI
                        limit: 8192
                    }
                }]
            },
          ...
        ]
      }

注:use指定处理.scss或.css文件的loader,loader的执行顺序从右向左,即先使用postcss-loader处理,再使用sass-loader,依次向左执行

  1. 添加autoprefixer需要的配制文件
    autoprefixer的配制文件有两种形式:一种是在项目根目录下创建一个.browserslistrc文件;一种是在package.json文件中添加配制。
    我们选择将配制添加到package.json文件中,如下:
{
  ...
  "browserslist": [
    "Android > 4",
    "IOS > 5"
  ],
  ...
}

注:更多browserslist的配制参考:https://github.com/ai/browserslist#config-file

7、使用react-router处理路由

在react项目中,react-router已经是一个比较成熟的路由管理工具,我们可以直接使用。这里,我们使用react-router 4.1.2版本。官方帮助文档地址:https://reacttraining.com/react-router/web/guides/philosophy

如果不清楚我们为什么要使用路由管理工具,可以自己试着不用react-router,自己实现当有几个页面时,根据不同的地址来切换页面内容。再使用react-router,你就能体会到方便之处了。

  1. 安装react-router
    版本4的react-router分成了好几个包,如下:
react-router

我们只需要安装react-router、react-router-dom:

yarn add react-router react-router-dom --save
  1. 使用
    react-router的官方文档(https://reacttraining.com/react-router/web/guides/philosophy)中有很详细的使用帮助,介意有时间可以自己按照文档及例子学习。
    这里需要说明一点,我们使用的版本4与之前的版本使用方式上还是有挺多不同的,比如我们项目中使用hash来做路由,我们的代码如下:
/**App.js*/
import React, { Component } from 'react'
import ReactDOM from 'react-dom'
// 这里使用react-router-dom中的HashRouter,4之前的版本引用方法是:
// import { Router, Route } from 'react-router'
// Router是根路由,由history属性来决定使用哪一种路由方式
import { HashRouter, Route, Switch } from 'react-router-dom'

// 引入页面,在src目录的module目录下创建你的页面
import Page1 from './module/Page1'
import Page2 from './module/Page2'
import NotFoundPage from './module/404'

class App extends Component {
    render() {
        return (
            <HashRouter>
                {/* Switcth组件作用是:只展示第一个匹配到的路由页面内容 */}
                <Switch>
                    <Route path="/page1" component={Page1}></Route>
                    <Route path="/page2" component={Page2}></Route>
                    <Route component={NotFoundPage}></Route>
                </Switch>
            </HashRouter>
        )
    }
}

export default App

8、页面JS按需求加载

按需加载,其实在react-router的官方文档中也叫代码分离(code-splitting)。若按照我们上面的步骤做下来,APP.js中引用的page1,page2,404这三个页面的代码都会打包到一起,这显然不是我们想要的效果。我们需要访问一个具体的页面时,只加载当前页面的JS。好在react-router 4的文档中有一个非常详细的例子(https://reacttraining.com/react-router/web/guides/code-splitting)。我们需要使用到webpack的bundle-loader来异步加载每一个页面的JS。

  1. 安装bundle-loader
yarn add bundle-loader

注:可以先学习使用bundle-loader,https://doc.webpack-china.org/loaders/bundle-loader/可以更好的帮助我们理解接下来要做的事情

  1. 改造APP.js
    1). 首先使用bundle-loader加载页面JS, 以Page1为例:
import Page1 from 'bundle-loader?lazy!./module/Page1'

注:bundle-loader后面的?lazy是该loader接收的参数,表示使用懒加载来加载Page1.js
若你看过bundle-loader的使用文档,你就会知道,此时使用lazy加载进来的Page1并不是真正的页面组件,而只是一个加载器,只有真正调用Page1时,才会去加载Page1.js,如:

Page1((file) => {
  // 我们使用的是webpack3,所以需要加default才能拿到真正的组件
  return file.default
})

2). 创建一个Bunlde.js用来统一处理bundle-loader import进来的loader,方便react-router的Route组件的componet使用

/**Bundle.js*/
import React, {
    Component
} from 'react'

class Bundle extends Component {
    state = {
        // 要加载的module
        mod: null
    }

    componentWillMount() {
        this.load(this.props)
    }

    load(props) {
        this.setState({
            mod: null
        })
        // 这里的load就是我们通过bundle-load?lazy加载进来的
        props.load((mod) => {
            this.setState({
                mod: mod.default || mod
            })
        })
    }

    render() {
        return this.state.mod ? this.props.children(this.state.mod) : null
    }
}

export default Bundle

3). APP.js中使用Bundle.js

/**App.js*/
import React, { Component } from 'react'
import ReactDOM from 'react-dom'
// 这里使用react-router-dom中的HashRouter,4之前的版本引用方法是:
// import { Router, Route } from 'react-router'
// Router是根路由,由history属性来决定使用哪一种路由方式
import { HashRouter, Route, Switch } from 'react-router-dom'
import Bundle from './Bundle'

// 引入页面
import Page1 from 'bundle-loader?lazy!./module/Page1'
import Page2 from 'bundle-loader?lazy!./module/Page2'
import NotFoundPage from 'bundle-loader?lazy!./module/404'

// 处理bundle-loader?lazy加载进来的模块的方法,供Route的componet属性使用
function lazyLoad(mod) {
    return (props) => (
        <Bundle load={mod}>
            {(Mod) => <Mod {...props} />}
        </Bundle>
    )
}

class App extends Component {
    render() {
        return (
            <HashRouter>
                {/* Switcth组件作用是:只展示第一个匹配到的路由页面内容 */}
                <Switch>
                    <Route path="/page1" component={lazyLoad(Page1)}></Route>
                    <Route path="/page2" component={lazyLoad(Page2)}></Route>
                    <Route component={lazyLoad(NotFoundPage)}></Route>
                </Switch>
            </HashRouter>
        )
    }
}

export default App

4). 修改入口文件index.js

/**index.js*/
import React from 'react'
import ReactDOM from 'react-dom'
// import './styles/index.scss'

import App from './App'

ReactDOM.render(
    <App />,
    document.getElementById('root')
)

9、组件热更新

以上我们的工程基本搭建完成,而为了提高我们的开发速度,组件热更新肯定少不了。目前我们修改工程中的JS文件,浏览器中打开的页面会自动全局刷新,而组件热更新的意思是,不刷新整个页面,只更新修改的组件对应的DOM。
我们需要用到react-hot-loader,同时webpack-dev-server也要开户热更新的功能。

  1. 安装react-hot-loader
yarn add react-hot-loader --save

注:同样,你可以不使用react-hot-loader,先尝试webpack官方文档中的原生热更新的例子来加深理解:https://doc.webpack-china.org/guides/hot-module-replacement/
react-hot-loader是webpack推荐的react模块热更新组件,更多的使用文档可以参考:https://github.com/gaearon/react-hot-loader/tree/master/docs

  1. 改造我们的代码
    1). webpack.config.js文件修改内容:
      ...
      entry: {
        // 1) 
        app: ['react-hot-loader/patch', './src/index.js'],
        vendor: [
            'react',
            'react-dom',
            'react-hot-loader',
            'react-router-dom'
        ]
      }
    ...
    devServer: {
        // 2) 启用HMR
        hot: true
    }
    ...
    moudle: {
      rules: [
        ...
        {
                test: /\.jsx?$/,
                exclude: /node_modules/,
                use: [
                    // 使用babel-loader解析
                    {
                        loader: 'babel-loader',
                        options: {
                            presets: ['react', 'es2015'],
                            plugins: [
                                // 3) react-hot-loader for HMR
                                'react-hot-loader/babel',
                                'transform-class-properties'
                            ]
                        }
                    }
                ]
            },
        ...
      ]
    }
    ...
    plugins: [
      // 4) 启用HMR
        new Webpack.HotModuleReplacementPlugin(),
    ]

2). 修改index.js

import React from 'react'
import ReactDOM from 'react-dom'
// 使用react-hot-loader包裹所有的组件
import {
    AppContainer
} from 'react-hot-loader'
// import './styles/index.scss'

import App from './App'

const render = (Component) => {
    ReactDOM.render(
        <AppContainer>
            <Component />
        </AppContainer>,
        document.getElementById('root')
    )
}
render(App);

// 用于监听react模块的热更新
if (module.hot) {
    module.hot.accept('./App', () => {
        render(require('./App').default)
    })
}

10、生产环境图片、CSS、js处理、以及生成打包信息文件

生产环境想要对图片、CSS、js打包时进行压缩,且图片小于某个指定的大小时可以自动转换成baseUri格式。
生产打包时,我们可以有专门的生产webpack的配制文件与开发环境的区分开,可以使用如下的:

/** webpack.deploy.js */
const Webpack = require('webpack');
const path = require('path');
// HTML文件模板解析插件
const HtmlWebpackPlugin = require('html-webpack-plugin');
// 提取CSS文件到单独的文件
const ExtractTextPlugin = require('extract-text-webpack-plugin');
// 提取打包信息
const StatsPlugin = require('stats-webpack-plugin');
const env = process.env.NODE_ENV === 'product' ? 'product' : 'test'
const publicPaths = {
    test: '',
    product: '/'
};

module.exports = {
    entry: {
        app: ['./src/index.js'],
        vendor: [
            'react',
            'react-dom',
            'react-hot-loader',
            'react-router-dom',
            'react-weui'
        ]
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].js',
        // 这里使用chunkhash的好处是,当chunk中只有一个变化时,重新打包只会修改变化的chunk文件名的hash值
        chunkFilename: '[id].[chunkhash:4].js',
        publicPath: publicPaths[env]
    },
    // devtool: 'source-map',
    module: {
        rules: [{
            test: /\.jsx?$/,
            include: [path.resolve(__dirname, "src/module")],
            use: ['bundle-loader?lazy'] // src/module下的文件都使用动态加载
        }, {
            test: /\.jsx?$/,
            exclude: /node_modules/,
            use: [
                // 使用babel-loader解析
                {
                    loader: 'babel-loader',
                    options: {
                        presets: ['react', 'es2015'],
                        plugins: [
                            'transform-class-properties'
                        ]
                    }
                }
            ]
        }, {
            test: /\.(scss|css)$/,
            use: ExtractTextPlugin.extract({
                fallback: 'style-loader',
                use: ['css-loader', 'sass-loader', {
                    loader: 'postcss-loader',
                    options: {
                        plugins: [require('autoprefixer')]
                    }
                }]
            })
        }, {
            test: /\.(png|jpe?g||git)$/,
            use: [{
                loader: 'url-loader',
                options: {
                    limit: 8192
                }
            }, {
                loader: 'image-webpack-loader',
                options: {
                    progressive: true,
                    nterlaced: false,
                    pngquant: {
                        quality: '65-90',
                        speed: 4
                    }
                }
            }]
        }]
    },
    plugins: [
        // 提取公用的JS vendor
        new Webpack.optimize.CommonsChunkPlugin({
            name: ['vendor', 'manifest'],
            minChunks: Infinity,
        }),

        new HtmlWebpackPlugin({
            title: 'webpack react study',
            template: 'src/index.html'
        }),

        new ExtractTextPlugin('app.css'),

        new Webpack.optimize.UglifyJsPlugin({
            // 生成源文件,方便调试
            // sourceMap: true
        }),

        // 更多参数参考http://webpack.github.io/docs/node.js-api.html#stats-tojson
        new StatsPlugin('webpack.stats.json', {
            // the source code of modules
            source: false,
            // built modules information
            modules: true
        }),

        new Webpack.DefinePlugin({
            'process.env.NODE_ENV': process.env.NODE_ENV || 'test'
        })

    ]
}

项目中需要安装如下包:

// ExtractTextPlugin提取CSS单独打包
yarn add ExtractTextPlugin --dev
// image-webpack-loader处理图片
yarn add image-webpack-loader --dev
// stats-webpack-plugin输出打包信息
yarn add stats-webpack-plugin --dev

特别说明: image-webpack-loader处理图片若报错,本地需要全局安装libpng,安装方式:brew install libpng

stats-webpack-plugin配制中指定输出webpack.stats.json文件,该文件可以导入https://chrisbateman.github.io/webpack-visualizer/中查看项目打包时每一个模块的大小,可以给我们打包优化做参考

导入到visualizer平台上的stats文件效果如下:

打包信息文件分析

推荐阅读更多精彩内容

  • 无意中看到zhangwnag大佬分享的webpack教程感觉受益匪浅,特此分享以备自己日后查看,也希望更多的人看到...
    小小字符阅读 5,433评论 6 31
  • GitChat技术杂谈 前言 本文较长,为了节省你的阅读时间,在文前列写作思路如下: 什么是 webpack,它要...
    萧玄辞阅读 9,214评论 10 106
  • 从V1迁移到V2由于使用的是webpack版本是2.2.1,所以针对原文做了一些修改。针对webpack2的修改部...
    yzc123446阅读 334评论 0 1
  • 前两部分我们已经完成了博客页面的展示和后台页面的展示: React技术栈+Express+Mongodb实现个人博...
    SamDing阅读 3,101评论 1 11
  • 版权声明:本文为博主原创文章,未经博主允许不得转载。 webpack介绍和使用 一、webpack介绍 1、由来 ...
    it筱竹阅读 4,840评论 3 17