React - Webpack 开发环境重新搭建

说明

距离初次使用 react 已经有一年半左右的时间,无论是 react 全家桶(redux react-router 等)还是构建工具 webpack 等都有了版本的更新,配置使用上多少都发生了变化,这里针对当前最新的版本重新搭建做一下记录方便日后查询。

webpack 环境配置参考了 vue-cli 中的 webpack 配置,开发服务器没有用 express 而是 webpack-dev-server

重要依赖版本

node: v8.9.0npm: v5.6.0yarn: v1.3.2

- react: 16.2.0
- redux: 3.7.2
- react-router-dom: 4.2.2
- react-redux: 5.0.6
- react-router-redux: 5.0.0-alpha.9
- axios: 0.17.1
- react-hot-loader: 4.0.0-beta.12
- webpack: 3.10.0
- webpack-dev-server: 2.9.7
- babel-core: 6.26.0
- babel-preset-env: 1.6.1
- babel-plugin-transform-runtime: 6.23.0

目录结构调整

1. 外层目录、文件说明
    ├── build // webpack 构建工具配置文件夹
    ├── config // 用户自定义选项
    ├── dist // 生产编译后的文件夹,需要提交的文件
    ├── server // 开发时,本地服务器
    ├── src // 源码目录
    ├── .babelrc // babel 配置文件
    ├── .eslintignore // eslint 规则忽略文件
    ├── .eslintrc.js // eslint 规则配置文件
    ├── .gitignore // git 提交忽略文件
    ├── .postcssrc.js // postcss 配置文件
    ├── package.json
    └── yarn.lock
2. 构建工具配置文件/build | /config说明
    ├── build
    │   ├── build.js  // 生产环境启动文件 yarn run build
    │   ├── dev-server.js // 开发环境启动文件 yarn start
    │   ├── utils.js // 一些工具函数
    │   ├── webpack.base.config.js // 基础公用 webpack 配置
    │   ├── webpack.dev.config.js  // 开发环境配置
    │   └── webpack.prod.config.js  // 生产环境配置
    ├── config
    │   └── index.js // 从 webpack 中抽取的可配置的选项
3. 源码目录/src文件说明
    ├── api
    │   ├── request.js // 网络请求的封装文件
    │   └── home.js // 抽取某个页面的网络请求部分
    ├── assets // 资源文件夹 图片、音频、视频等
    ├── components // 展示型组件,按照容器组件分文件夹
    │   ├── App // App 容器组件的展示型子组件目录
    │       └── index.js // 顶层zi'zu'jian
    ├── containers // 容器组件
    │   └── App.js
    ├── modules // redux 模块,包括 reducer 部分与 action 部分
    │   ├── home // 对应某个容器组件,集中了这个容器的 reducer 和 action
    │   │   ├── actions.js
    │   │   └── reducer.js
    │   ├── reducers.js // 合并后的总的 reducer
    │   └── types-constant.js // 抽取出的 type 常量
    ├── scss // 样式文件
    │   ├── _common.scss
    │   └── home.scss
    ├── store // store 配置
    │   ├── configureStore.js
    │   └── index.js
    └── utils // 公用的工具函数
    │   └── bindActions.js
    ├── index.html // 页面模板
    ├── routes.js // 路由配置
    └── index.js // react 入口

react-webpack 开发环境搭建

命令

    "scripts": {
        "dev": "node build/dev-server.js",
        "start": "npm run dev",
        "build": "node build/build.js",
        "server": "node server/index.js"
    },
  • yarn run dev # yarn start 开发环境启动
  • yarn run build 生产环境启动
  • yarn run build --report 显示打包详情

1. 创建 /config/index.js 配置文件

用来定义一些在 webpack 环境搭建中的可配置项,主要分为三个部分,

  1. common 通用配置
  2. development 开发环境的配置
  3. production 生产环境的配置
    const ip = require('ip').address(); // ip 模块用来获取本机 ip
    const utils = require('../build/utils'); // 自定义的一组工具函数
    const resolve = utils.resolve(__dirname, '../'); // utils.resolve() 函数处理路径拼接

    module.exports = {
       common: {
          context: resolve(''), // 定义根目录路径
          sourceCode: resolve('src'), // 源码目录路径
          // 封装的请求模块位置,用于注入请求与服务器地址
          requestModule: resolve('src/api/request.js')
       },
       development: {
          env: {NODE_ENV: JSON.stringify('development')}, // 开发环境的环境变量
          port: process.env.PORT || 8273, // 设置开发时端口号
          devServerIp: ip, // 开发时的 Ip
          basicRequestLink: `http://${ip}:3167`, // 注入的请求服务器地址
          entryPath: null, // 入口文件路径,默认为 './src/index.js'
          assetsRoot: resolve('dev'), // 开发时编译后的文件路径,不会显示
          assetsSubDirectory: 'static', // 二级资源路径
          assetsPublicPath: '/', // 编译发布的根目录
       },
       production: {
          env: {NODE_ENV: JSON.stringify('production')}, // 生产环境的环境变量
          basicRequestLink: `https://xxx.com`, // 生产时设置为最后部署的服务器地址
          entryPath: null, // 入口文件路径,默认为 './src/index.js'
          assetsRoot: resolve('dist'), // 编译后的文件路径
          assetsSubDirectory: 'static', // 二级资源路径
          assetsPublicPath: '/', // 编译发布的根目录
          productionSourceMap: false, // js sourceMap
          bundleAnalyzerReport: utils.shouldReport(), // 是否显示 report 页面,也就是各个模块的打包细节
       }
    }

以上涉及到几个定义在 /build/utils.js 中的函数

1. utils.resolve()

path.join() 的封装,保存一个基础的路径,并返回一个函数,可以拼接路径到到基础路径上

    exports.resolve = function (...basicPath) {
       return function (dir) {
          return path.join(...basicPath, dir || '');
       }
    }
2. utils.shouldReport()

解析命令行参数,判断是否可以进行 report 处理,展示模块打包细节

    exports.shouldReport = function () {
       if (process.env.npm_config_report) {
          return process.env.npm_config_report;
       } 

       return process.argv.some((item) => item === '--report');
    }

注意:通过 npm run build --report 执行 会产生 process.env.npm_config_report , 通过 yarn run build --report 则可以在 process.argv 中拿到配置。

2. 创建 webpack.base.config.js 基础通用 webpack 配置文件

    const HtmlWebpackPlugin = require('html-webpack-plugin'); // 根据模板生成 HTML
    const packageConfig = require('../package.json');
    const config = require('../config/index');
    const utils = require('./utils');
    const common = config.common;
    const current = utils.getEnvAndConf(config); // 得到当前的 NODE_ENV 环境变量和对应的配置
    const namedAssets = utils.resolve(current.conf.assetsSubDirectory); // 二级资源路径拼接函数

    module.exports = {
        context: common.context,
        entry: utils.computeEntry(config, packageConfig), // computeEntry() 根据环境返回对应的入口配置
        output: utils.computeOutput(config), // computeOutput() 根据环境返回对应的出口配置
        cache: true,
        resolve: {
            extensions: [ // 默认的扩展名,使得引用的时候可以不带相应的扩展名
                '.js', '.json', '.jsx', '.css'
            ],
            modules: ['node_modules', common.sourceCode] // 告诉 webpack 解析模块时应该搜索的目录
        },
        module: { // 配置模块处理
            rules: [
                {
                    test: /\.(js|jsx)$/,
                    loader: 'eslint-loader',
                    enforce: 'pre', // 预先进行 eslint 语法检查
                    include: common.sourceCode,
                    options: {
                        formatter: require('eslint-friendly-formatter')
                    }
                }, {
                    test: /\.(js|jsx)$/,
                    loader: 'babel-loader',
                    include: common.sourceCode 
                }, {
                    test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
                    loader: 'url-loader',
                    options: {
                        limit: 10000,
                        name: namedAssets(
                            current.env !== 'production'
                            ? 'imgs/[name].[ext]'
                            : 'imgs/[name].[hash:10].[ext]')
                    }
                }, {
                    test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
                    loader: 'url-loader',
                    options: {
                        limit: 10000,
                        name: namedAssets(
                            current.env !== 'production'
                            ? 'media/[name].[ext]'
                            : 'media/[name].[hash:10].[ext]')
                    }
                }, {
                    test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
                    loader: 'url-loader',
                    options: {
                        limit: 10000,
                        name: namedAssets(
                            current.env !== 'production'
                            ? 'fonts/[name].[ext]'
                            : 'fonts/[name].[hash:10].[ext]')
                    }
                }, {
                test: require.resolve(common.requestModule), // 将配置中的服务器地址注入到指定模块中
                loader: 'imports-loader?basicRequestLink=>' + JSON.stringify(current.conf.basicRequestLink)
             }
            ]
        },
       plugins: [
          new HtmlWebpackPlugin({ // html 插件
                template: utils.resolve(common.sourceCode)('index.html'),
                filename: 'index.html',
                inject: 'body',
                minify: false,
             xhtml: true,
                cache: false,
             // favicon: ''
            })
       ]
    }

基础 webpack 配置中涉及到了几个 /build/utils.js 中的函数

1. utils.getEnvAndConf(config)

获取当前的 env.NODE_ENV 环境以及对应的配置,需要传入参数 config 就是在 /config/index.js 中配置的

    exports.getEnvAndConf = function (config) {
       const env = process.env.NODE_ENV;
       const conf = config[env];

       return {env, conf};
    }
2. utils.computeEntry(config, packageConfig)

根据当前环境,返回相应的入口文件配置,开发环境配置 HMR

    exports.computeEntry = function (config, packageConfig) {
       // 先解析出,当前的环境,和对应的配置
       const {env, conf} = exports.getEnvAndConf(config);
       let entry = {};

       if(env === 'production') {
          // 生产环境
          entry.app = conf.entryPath || './src/index.js';
       } else if (env === 'development') {
          // 开发环境
          const {port, devServerIp, entryPath} = conf;
          entry.app = [
             `webpack-dev-server/client?http://${devServerIp}:${port}`,
             'webpack/hot/only-dev-server',
             entryPath || './src/index.js'
          ];
       }
       // 将项目模块与导入的模块分离,也就是 package.json 中的 dependencies 部分
       entry.vendor = Object.keys(packageConfig.dependencies);

       return entry;
    }
3. utils.computeOutput(config)

根据当前环境,返回相应的出口文件配置,生产环境添加 hash

    exports.computeOutput = function (config) {
       // 先解析出,当前的环境,和对应的配置
       const {env, conf} = exports.getEnvAndConf(config);
       const filename = path.join(
          conf.assetsSubDirectory,
          env !== 'production' ? 'js/[name].bundle.js' : 'js/[name].[chunkhash:10].bundle.js'
       );
       const chunkFilename = env !== 'production'
          ? '[id].js'
          : '[id].[chunkhash:10].js';

       const output = {
          path: conf.assetsRoot,
          publicPath: conf.assetsPublicPath,
          filename,
          chunkFilename
       };

       return output;
    }

3. 创建 webpack.dev.config.js 开发环境 webpack 配置

主要是添加 开发环境的 plugin 插件

const webpack = require('webpack');
const merge = require('webpack-merge'); // 用来合并 webpack 配置
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin'); // 优化错误提示插件
const CleanWebpackPlugin = require('clean-webpack-plugin'); // 构建前清空目录插件
const baseWebpackConfig = require('./webpack.base.config');
const utils = require('./utils');
const config = require('../config/index');
const common = config.common;
const current = utils.getEnvAndConf(config);

module.exports = merge(baseWebpackConfig, {
    devtool: '#cheap-module-eval-source-map',
    module: {
        rules: [
            {
                test: /\.(scss|sass|css)$/, // 样式文件处理
                include: common.sourceCode,
                use: utils.computeStyleLoader(false, ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader'])
            }
        ]
    },
    plugins: [
        new CleanWebpackPlugin(['dev'], {root: common.context}),
        // 使用模块的路径,而不是数字标识符作为ID,避免解析顺序引起的 hash 变化
        new webpack.NamedModulesPlugin(),
        // 热模块替换插件
        new webpack.HotModuleReplacementPlugin(),
        // 定义process.env.NODE_ENV环境变量
        new webpack.DefinePlugin({'process.env.NODE_ENV': current.conf.env.NODE_ENV}),
        // 将引入的第三方库拆分出来
        new webpack.optimize.CommonsChunkPlugin({name: 'vendor'}),
        // 将运行时代码拆分出来,配合其他插件避免每次打包 hash 都变化
        new webpack.optimize.CommonsChunkPlugin({name: 'runtime'}),
        // 编译出现错误时,跳过输出阶段,确保输出资源不会包含错误
        new webpack.NoEmitOnErrorsPlugin(),
        new FriendlyErrorsPlugin()
    ]
})

用到了一个 /build/utils.js 中的函数

1. utils.computeStyleLoader()

抽取出的,方便配置 样式的 loader

    // 如果有了 less-laoder 页可以在这里配置
    exports.styleLoadersOptions = {
       dev: {
          'sass-loader': {
             outputStyle: 'expanded',
             sourceMapContents: true,
             sourceMap: true
          }
       },
       prod: {
          'sass-loader': {outputStyle: 'expanded'}
       }
    }

    exports.computeStyleLoader = function (isProduction, loaders) {
       const optionsMap = exports.styleLoadersOptions[isProduction ? 'prod' : 'dev'];
       const defaultOptions = isProduction ? {} : {sourceMap: true};

       return loaders.map(loader => {
          const options = optionsMap[loader] || defaultOptions;

          return {loader, options};
       })
    }

4. 创建 dev-server.js 开发环境启动文件

  1. 设置环境变量 env.NODE_ENV
  2. 配置 webpack-dev-server
  3. 开启一个服务器,打开一个页面
    const config = require('../config/index');
    const devConfig = config.development;

    // 设置环境变量
    if (!process.env.NODE_ENV) {
       process.env.NODE_ENV = JSON.parse(devConfig.env.NODE_ENV)
    }

    const webpack = require('webpack');
    const WebpackDevServer = require('webpack-dev-server');
    const opn = require('opn'); // 一个 node 插件,在浏览器中打开指定链接
    const chalk = require('chalk'); // 命令行输出美化插件
    const webpackConfig = require('./webpack.dev.config');
    const utils = require('./utils');

    const common = config.common;
    const resolve = utils.resolve(common.context);

    // 配置 webpack-dev-server 
    const devServerOptions = {
       contentBase: resolve('dev'),
       publicPath: devConfig.assetsPublicPath,
       historyApiFallback: true,
       clientLogLevel: 'none',
       hot: true,
       inline: true,
       compress: true,
       openPage: 'index.html',
       stats: {
            colors: true,
            errors: true,
            warnings: true,
            modules: false,
            chunks: false
        }
    };

    // 开启服务
    const compiler = webpack(webpackConfig);
    const server = new WebpackDevServer(compiler, devServerOptions);
    const {port, devServerIp, basicRequestLink} = devConfig;

    server.listen(port, devServerIp, () => {
       const link = `http://${devServerIp}:${port}`;
       console.log(chalk.cyan(`Starting server on ${link}`));
       console.log(chalk.cyan(`development data server on ${basicRequestLink}`));

        // 成功后打开指定链接
       opn(link).then(() => {
          console.log(chalk.cyan('success open ...'));
       }).catch(err => {
          console.log(chalk.red(err));
       })
    })

5. 创建 webpack.prod.config.js 生产环境配置

主要配置 plugin 插件

    const webpack = require('webpack');
    const merge = require('webpack-merge');
    const ExtractTextPlugin = require('extract-text-webpack-plugin'); // 分离出样式文件插件
    const CleanWebpackPlugin = require('clean-webpack-plugin');
    const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin'); // 优化 css 插件
    const baseWebpackConfig = require('./webpack.base.config');
    const utils = require('./utils');
    const config = require('../config/index');
    const common = config.common;
    const current = utils.getEnvAndConf(config);

    let reportPlugin = [];

    if (current.conf.bundleAnalyzerReport) {
        // 如果可以显示 report 则添加如下插件
        const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

        reportPlugin.push(new BundleAnalyzerPlugin());
    }

    module.exports = merge(baseWebpackConfig, {
        devtool: current.conf.productionSourceMap ? '#source-map' : false,
        module: {
            rules: [
                {
                    test: /\.(scss|sass|css)$/, // 样式文件 loader 配置,并分离输出
                    include: common.sourceCode,
                    use: ExtractTextPlugin.extract({
                        fallback: 'style-loader',
                        use: utils.computeStyleLoader(true, ['css-loader', 'postcss-loader', 'sass-loader'])
                    })
                }
            ]
        },
        plugins: [
            new CleanWebpackPlugin(['dist'], {root: common.context}),
            // 根据模块的相对路径生成一个四位数的hash作为模块id, 避免解析顺序引起的 hash 变化
            new webpack.HashedModuleIdsPlugin(),
            // 作用域提升插件
            new webpack.optimize.ModuleConcatenationPlugin(),
            // 设置生产环境变量
            new webpack.DefinePlugin({'process.env.NODE_ENV': current.conf.env.NODE_ENV}),
            // 配合输出 css
            new ExtractTextPlugin({
                filename: utils.resolve(current.conf.assetsSubDirectory)('css/[name].[contenthash:10].css'),
                disable: false,
                allChunks: true
            }),
            // 优化合并输出的css
            new OptimizeCSSPlugin({cssProcessorOptions: {safe: true}}),
            // 压缩 js
            new webpack.optimize.UglifyJsPlugin({
                compress: {
                    warnings: false,
                    'drop_debugger': true,
                    'drop_console': true
                },
                comments: false,
                'space_colon': false
            }),
            // 拆分模块
            new webpack.optimize.CommonsChunkPlugin({name: 'vendor'}),
            new webpack.optimize.CommonsChunkPlugin({name: 'runtime'}),
            ...reportPlugin
        ] 
    });

6. 创建 build.js 生产环境启动文件

    const config = require('../config/index');
    const prodConfig = config.production;

    // 设置环境变量
    process.env.NODE_ENV = JSON.parse(prodConfig.env.NODE_ENV)

    const webpack = require('webpack');
    const ora = require('ora'); // 一个命令行 loading 插件
    const chalk = require('chalk'); // 命令行输出美化
    const webpackConfig = require('./webpack.prod.config');

    // loading
    const spinner = ora('building for production...');
    spinner.start();

    webpack(webpackConfig, function(err, stats) {
        spinner.stop();
        if (err) {
            throw err
        }
        process
            .stdout
            .write(stats.toString({colors: true, modules: false, children: false, chunks: false, chunkModules: false}) + '\n\n')

        if (stats.hasErrors()) {
            console.log(chalk.red('  Build failed with errors.\n'))
            process.exit(1)
        }

        console.log(chalk.cyan('  Build complete.\n'))
        console.log(chalk.yellow('  Tip: built files are meant to be served over an HTTP server.\n' + '  Opening index.html over file:// won\'t work.\n'))

    })

一些插件的配置文件

1. babel 的配置文件 .babelrc

babel 相关插件

  • babel-core: ^6.26.0
  • babel-eslint: ^8.1.2
  • babel-loader: ^7.1.2
  • babel-plugin-transform-decorators-legacy: ^1.3.4
  • babel-plugin-transform-runtime: ^6.23.0
  • babel-preset-env: ^1.6.1
  • babel-preset-react: ^6.24.1
  • babel-preset-stage-2: ^6.24.1
  • babel-register: ^6.26.0
    {
       "presets": [
          ["env", {
             "modules": false,
             "targets": {
                "node": "current",
                "browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
             }
          }],
          "stage-2",
          "react"
       ],
       "plugins": [
          "transform-runtime", "transform-decorators-legacy", "react-hot-loader/babel"
       ]
    }

2. eslint 配置文件 .eslintrc.js

详情查看源码地址

3. postcss 配置文件 .postcssrc.js

需要安装插件 sudo yarn add autoprefixer 更多插件查看

    module.exports = {
        "plugins": {
            // to edit target browsers: use "browserslist" field in package.json
            "autoprefixer": {}
        }
    }

4. react-hot-loader@4.0.0-beta.12 使用

与版本 3 的使用有很大区别

1. 在 .babelrc 中配置插件
 "plugins": ["react-hot-loader/babel"]
2. webpack 插件
    plugins: [
        new webpack.HotModuleReplacementPlugin()
    ]
3. react 入口文件中 /src/index.js
    import React from 'react'
    import { render } from 'react-dom'
    import App from './App' // 引入组件,在这个组件中进行热更新

    const root = document.createElement('div')
    document.body.appendChild(root)

    render(<App />, root)
4. App 组件中
    import React from 'react'
    import { hot } from 'react-hot-loader'
    import Counter from './Counter'

    const App = () => (
      <h1>
        Hello, world.<br />
        <Counter />
      </h1>
    )

    // 使用 react-hot-loader 提供的 hot 方法代替以前的 AppContainer 组件,更加方便
    export default hot(module)(App)
5. 配合 redux react-router

查询源码

Logo

前往低代码交流专区

更多推荐