美文网首页
如何从零搭建一个react + react-router + w

如何从零搭建一个react + react-router + w

作者: 荏苒乐淘淘 | 来源:发表于2017-08-03 13:51 被阅读0次

    导语:学习搭建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文件效果如下:

    打包信息文件分析

    相关文章

      网友评论

          本文标题:如何从零搭建一个react + react-router + w

          本文链接:https://www.haomeiwen.com/subject/ncrelxtx.html