Webpack5基本使用与优化【这一次彻底掌握Webpack】

620 阅读10分钟

前言

  1. Webpack 是一个前端资源加载/打包工具。它将根据模块的依赖关系进行静态分析,然后将这些模块按照指定的规则生成对应的静态资源。
  2. Webpack整体配置主要围绕以下属性
    • mode(开发模式),
    • entry(打包入口)
    • output(打包出口)
    • devServer(开发环境自动化配置)
    • devtool(调试模式)
    • resolve (解析规则)
    • module (模块)
    • plugins(插件)
    • optimization(优化策略)
  3. webpack项目的基本目录结构 webpack-cli ├─ config webpack配置文件 │ ├─ webpack.base.js // 公共配置 │ └─ webpack.deve.js // 开发模式配置 │ └─ webpack.prod.js // 生成模式配置 ├─ dist 资源打包生成文件 │ ├─ chunk.js │ └─ chunk.js.map │ └─ react.chunk.js // 分割后的react相关的chunk文件 │ └─ react.chunk.js.map // react代码映射文件 │ └─ index.html // 打包后的html模板文件 ├─ public 静态资源文件 │ ├─ index.html // html模板文件 │ └─ favicon.ico // 网站图标 ├─ src │ ├─ main.js // 打包入口文件 │ └─ App.jsx │ └─ pages │ └─ router ├─ node_modules // 依赖文件夹 ├─ .browserslistrc // 指定兼容性版本 ├─ babel.config.js //babel配置文件 ├─ postcss.config.js //postcss配置文件 ├─ .eslintrc.js // eslint配置文件 ├─ .eslintignore // eslint需要忽略的配置文件 ├─ package-lock.json ├─ package.json └─ webpack.config.js

Webpack基础用法

开发前准备

  1. yarn init 初始化项目
  2. 完成package.json文件的基础配置
    {
      "name": "webpack-demo",
      "version": "1.0.0",
      "main": "index.js",
      "license": "MIT",
      "scripts": {
        "build": "webpack"
      }
    }
    
  3. yarn add webpack webpack-cli -D 安装webpack插件

一、mode

  1. 用于指定当前构建环境
  2. 值为development production node
  3. 默认值是production
  4. development会设置环境变量process.env.NODE_ENV = 'development',并且会开启内置模块:
    • NamedChunksPlugin(当开启 HMR 的时候,显示更新包的名字)
    • NamedModulesPlugin(当开启 HMR 的时候,显示更新包的相对路径)
  5. production会设置环境变量process.env.NODE_ENV = 'production',并且会开启内置模块:
    • FlagDependencyUsagePlugin (编译时标记依赖)
    • FlagIncludedChunksPlugin (防止chunks多次加载)
    • ModuleCoucatenationPlugin (作用域提升(scope hosting))
    • NoEmitOnErrorsPlugin (在输出阶段时,遇到编译错误跳过)
    • OccurrenceOrderPlugin (按照chunk出现次数分配chunk id)
    • SideEffectsFlagPlugin (识别 package.json 或者 module.rules 的 sideEffects 标志)
    • UglifyJsPlugin (删除未引用代码,并压缩)
  6. none表示不启用任何优化选项
module.exports = {
    mode:'development', // 指定为开发模式
}

二、entry

  • 指定打包文件的入口
  • 支持单文件和多文件打包
  • 值为字符串时表示打包单个文件
  • 值为数组时表示打包多个文件到单个文件
  • 值为对象时表示打包多个文件到多个文件
  1. 单文件打包
    module.exports = {
        mode:'development',
        entry:'./src/main.js', // 指定单个入口文件
    }
    
  2. 多文件打包产出单个文件
    module.exports = {
      mode: 'development',
      entry: ['./src/main.js','./src/index.js' ], // 指定多个入口文件
    };
    
  3. 多文件打包产出多个文件
    module.exports = {
      mode: 'development',
      entry: {
        main: './src/main.js',
        index: './src/index.js',
      }, // 指定多个入口文件
    };
    

三、output

  • 用来指定Webpack如何输出,输出内容和输出目录等。
  • 常用的配置属性:
    • filename 输出文件名称
    • path 输出文件目录
    • publicPath 公共资源路径前缀
    • chunkFilename 开启代码分割时产生的chunk文件名称
    • assetModuleFilename 统一的静态资源文件名称
    • clean 在生成文件之前清空 output 目录
    • library 当前库向外暴露的变量名称
  • [name] chunk名称、[contenthash] 模块内容的Hash值、[contenthash:8] 模块内容的 Hash值的前八位、[ext] 文件后缀名、[query] 参数、[id] chunk的id。
    const path = require('path')
    module.exports = {
      mode: 'development',
      entry: {
        main: './src/main.js',
        index: './src/index.js',
      },
      output: {
        path: path.resolve(__dirname, './dist'), // 设置打包文件目录
        //filename:'build.js', // 单文件时可以直接固定输出文件名称~~
        filename: '[name].[contenthash:8].js', // 多文件时必须动态输出文件名称,这里的[name]可entry中的key对应
        assetModuleFilename:'static/media/[name].[contenthash:8][ext][query]', // 资源文件打包到输出文件路径下的/static中
        chunkFilename:'[name].[contenthash:8].chunk.js', // chunk文件名称
        publicPath: "", // 默认值 资源路径相对于 HTML 页面
        clean: true, // 在生成文件之前清空 output 目录
      },
    };

四、devtool

  • 此选项控制是否生成,以及如何生成 source map。
  • [inline-|hidden-|eval_][nosources-][cheap-[module-]]source-map
  • 第一段可选参数为:
    • inline (生成行内base64格式source map文件,不单独产出文件)
    • hidden (有map文件,没有sourceMappingURL)
    • eval (所有的source map文件都被eval包裹)
  • 第二段可选参数为
    • nosources (提示错误信息,没有源代码信息)
  • 第三段可选参数为
    • cheap 映射转换过的代码,只显示行
    • cheap-module 映射原代码,只显示行
  • 开发环境推荐使用cheap-module-source-map,生产环境推荐使用hidden-source-map | devtool | 构建速度 | 重新构建速度 | 生产环境 | 品质(quality) | ------------------------------ | ---- | ------ | ---- | ----------- | | (none) | +++ | +++ | yes | 打包后的代码 | | eval | +++ | +++ | no | 生成后的代码 | | cheap-eval-source-map | + | ++ | no | 转换过的代码(仅限行) | | cheap-module-eval-source-map | o | ++ | no | 原始源代码(仅限行) | | eval-source-map | -- | + | no | 原始源代码 | | cheap-source-map | + | o | no | 转换过的代码(仅限行) | | cheap-module-source-map | o | - | no | 原始源代码(仅限行) | | inline-cheap-source-map | + | o | no | 转换过的代码(仅限行) | | inline-cheap-module-source-map | o | - | no | 原始源代码(仅限行) | | source-map | -- | -- | yes | 原始源代码 | | inline-source-map | -- | -- | no | 原始源代码 | | hidden-source-map | -- | -- | yes | 原始源代码 | | nosources-source-map | -- | -- | yes | 无源代码内容
const path = require('path');
module.exports = {
  mode: 'development',
  entry: ['./src/main.js', './src/index.js'], // 指定入口文件
  output: {
    path: path.resolve(__dirname, './dist'), // 设置打包文件目录
    filename: '[name].[contenthash:8].js', // 多文件时必须动态输出文件名称,这里的[name]可entry中的key对应
    assetModuleFilename: 'static/css/[name].[contenthash:8][ext][query]', // 资源文件打包到输出文件路径下的/static中
    chunkFilename: '[name].[contenthash:8].chunk.js', // chunk文件名称
    publicPath: '', // 默认值 相对于 HTML 页面
    clean: true, // 在生成文件之前清空 output 目录
  },
  devtool: 'cheap-module-source-map', // 此选项控制是否生成,以及如何生成 source map。
};

从这里开始您可以将上面已经写好的配置属性直接合并到下文,为了减少代码体积,我不再做合并操作

五、devServer

  • 开发模式下的自动化配置。
  • 常用的配置属性:
    • hot 开启HMR热模块替换
    • open 默认打开浏览器
    • compress 开启gzig压缩
    • historyApiFallback 解决history模式下重定向问题,找不到请求路径时重定向到index.html文件
    • host 指定主机地址
    • port 指定端口号
    • proxy 请求代理转发
module.exports = {
  devServer:{
      hot:true, // 开启HMR热模块替换
      open:true, // 自动打开网页
      compress:true, // 开启gzig压缩
      historyApiFallback:true, // 解决history模式下重定向问题,找不到请求路径时重定向到index.html文件
      host:'localhost', // 指定主机地址
      port:3000, // 指定端口号
      proxy: {
        '/api': {
          target: 'http://localhost:8888', // 需要转发的目标地址
          changeOrigin:true, // 覆盖请求主机来源
          pathRewrite: { '^/api': '' }, // 重写路径
        },
      },
     client: {
       progress: true, // 浏览器中以百分比显示进度
     },
  }
};

六、resolve

  • Webpack的解析规则
  • 常用属性介绍
    • alias 路径别名,用来确保模块引入变得更简单
    • extensions 补全后缀名,按数组中的顺序执行解析后缀名
    • modules 模块的解析位置,默认是node_modules
    • plugins 更多插件,可以在这里注册更多插件,一般都是在option.plugins中配置插件
const path = require('path');
module.exports = {
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
    extensions: ['.js', '.jsx', '.tsx', '.vue'],
  },
}

七、module

  • 对不同类型文件的模块做出解析处理
  • loader的解析顺序是从左到右,从下到上
  • loader的执行优先级也受enforce字段影响, Pre loader > Inline loader > Normal loader > Post loader
  • 单个loader使用loader属性注册,单个或多个loader使用use注册
module.exports = {
  module: {
    rules: [
        // 处理css文件
        {
            test:/\.css$/
            loader:'style-loader', // 单个loader
            use:['style-loader','css-loader'], // 多个loader
        }
    ],
  }
}

八、loader资源处理和配置

1) css资源处理

1.处理css资源

yarn add style-loader css-loader -D 使用style-loader css-loader处理css资源

module.exports = {
  module: {
    rules: [
        // 处理css文件
        {
            test:/\.css$/
            use:['style-loader','css-loader'], // 多个loader
        }
    ],
  }
}
2.为css添加兼容性处理

yarn add style-loader css-loader postcss-loader postcss-preset-env -D

根目录新建.browserslistrc文件,用于指定兼容版本 .browserslistrc

> 0.01%
last 2 version
not dead

webpack.config.js

module.exports = {
  module: {
    rules: [
        // 处理css文件
        {
            test:/\.css$/
            use:[
                'style-loader',
                {
                    loader:'css-loader',
                    // 此处添加loader的配置信息
                    options:{
                        importLoaders:1, // 当处理css文件遇到@import导出的资源时,先使用上一级loader进行处理
                    }
                },
                {
                    loader:'postcss-loader',
                    // 此处添加loader的配置信息
                    options: {
                      postcssOptions: {
                        // 使用postcss-preset-env处理兼容性问题
                        // 在这里配置有一个弊端就是,再处理less sass等依然要写重复代码,官方推荐在根目录新建postcss.config.js文件进行配置
                        plugins: [require('postcss-preset-env')],
                      },
                    },
                }
            ], // 多个loader
        }
    ],
  }
}

上面的配置过于繁琐,接下来我们使用官方推荐的,在跟目录下新建postcss.config.js完成兼容性配置 (后续Js的兼容性处理也使用单独文件处理)

// postcss.config.js
module.exports = {
  plugins: [require('postcss-preset-env')], // 使用安装的postcss-preset-env插件
};
3.处理less和sass文件
module: {
  rules: [
    {
      test: /\.css$/,
      use: ['style-loader', { loader: 'css-loader', options: { importLoaders: 1 } }, 'postcss-loader'],
    },
    {
      test: /\.less$/,
      use: ['style-loader', { loader: 'css-loader', options: { importLoaders: 2 } }, 'postcss-loader', 'less-loader'],
    },
    {
      test: /\.s[ac]ss$/,
      use: ['style-loader', { loader: 'css-loader', options: { importLoaders: 2 } }, 'postcss-loader', 'sass-loader'],
    },
  ],
},
4. loader简写
  const setStyleLoaders = (i,loader)=> ['style-loader', { loader: 'css-loader', options: { importLoaders: i } }, 'postcss-loader',loader].filter(Boolean); // loader参数可能为undefined,过滤一下结果
    module: {
      rules: [
        {
          test: /\.css$/,
          use: setStyleLoaders(1)
        },
        {
          test: /\.less$/,
          use: setStyleLoaders(2,'less-loader')
        },
        {
          test: /\.s[ac]ss$/,
          use: setStyleLoaders(2,'sass-loader'),
        },
      ],
    },

2) 图片资源处理

  1. 在Webpack4中一般使用row-loader | file-loader | url-loader处理图片等资源
  2. Webpack5内置asset资源模块类型(asset module type)
    • asset/resource 发送一个单独的文件并导出 URL。之前通过使用 file-loader 实现。
    • asset/inline 导出一个资源的 data URI。之前通过使用 url-loader 实现。
    • asset/source 导出资源的源代码。之前通过使用 raw-loader 实现。
    • asset 在导出一个 data URI 和发送一个单独的文件之间自动选择。之前通过使用 url-loader,并且配置资源体积限制实现。
  3. 当你仍然想使用其他laoder处理资源时,可通过javascript/auto来停止Webpack的默认处理
1.使用url-loader处理资源 (仅作为演示,不推荐)

yarn add url-loader -D

module.exports = {
  module: {
   rules: [
      {
        test: /.(png|jpe?g|gif|svg|webg)$/i,
        use: [
          {
            loader: 'url-loader',
            options: {
              // 超过10kb导出文件,小于10kb直接使用base64格式添加到行内
              limit: 10 * 1024,
            }
          },
        ],
       type: 'javascript/auto'
      },
   ]
  },
}
2.使用asset处理资源 (推荐)
module.exports = {
  module: {
   rules: [
      {
        test: /.(png|jpe?g|gif|svg|webg)$/,
        type: 'asset',
        generator:{
          // 输出的资源文件名称 (同上文output.assetModuleFilename)
          filename:'static/media/[name][contenthash:8][ext][query]'
        },
        parser:{
          dataUrlCondition:{
            // 超过10kb导出文件,小于10kb直接使用base64格式添加到行内
            maxSize:10 * 1024
          }
        }
      },
   ]
  },
}

3) 字体图标资源处理

module.exports = {
  module: {
   rules: [
      {
        test: /.(ttf|woff)$/,
        type: 'asset/resource',
      }
   ]
  },
}

4) Js资源处理

  • 可以使用babel-loader解析js代码
  • 单独使用babel-loader并不会对代码进行兼容性处理
  • 使用@babel/preset-env预设,对代码做出兼容处理

yarn add babel-loader @babel/preset-env core-js -D

新建babel.config.js文件

// babel.config.js
module.exports = {
  presets: [
    [
      // 使用预设
      '@babel/preset-env',
      {
        // 按需加载
        useBuiltIns: 'usage',
        // 指定core-js版本
        corejs: 3,
      },
    ],
  ],
};

webpack.config.js

module: {
    rules: [
    {
      // js或者jsx的代码都使用babel-loader处理
      test: /.(jsx?)$/,
      exclude: /node_modules/, // 排除node_modules文件夹
      loader: 'babel-loader',
    },
  ],
},

八、plugin插件

  1. plugin是为了扩展webpack,增加自定义的构建行为
  2. 使用webpack执行更加广泛的内容,拥有更强的构建能力
  3. webpack在构建的过程中会执行一系列的钩子事件,插件所做的就是找到对应的钩子,在上面注册事件,这样在webpack构建的过程中,自定义的任务事件就会随着钩子事件的触发而执行
  4. 每一个插件都是一个构造函数,使用时需要实例化这个插件(每一个plugin都有apply方法,编译时会生成全局的compiler对象,可以在插件apply方法中调用compiler.hook上注册的方法(解析,执行,编译都会有不同的方法)来扩展webpack的构建能力) 模拟插件的注册过程
class TestPlugin{
    apply(compiler){
        // 执行emit事件的同步钩子
        compiler.hook.emit.tap('TestPlugin',(compilation)=>{
           console.log('emit tap')
        })
    }
}
module.exports = TestPlugin;

1) html-webpack-plugin使用

yarn add html-webpack-plugin -D

const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
  plugins: [
      new HtmlWebpackPlugin({
        template: path.resolve(__dirname, './public/index.html'), // html模板文件
        title: 'webpack-template', // 添加变量
      }),
    ],
}

新建plubic/index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <!-- 使用模板变量 -->
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
    <div id="app"></div>
  </body>
</html>

2) eslint-webpack-plugin

  1. yarn add eslint-webpack-plugin eslint -D
  1. 根目录新建.eslintignore
# 忽略build目录下类型为js的文件的语法检查
build/*.js
# 忽略src/assets目录下文件的语法检查
src/assets
# 忽略src/utils目录下文件的语法检查
src/utils
# 忽略public目录下文件的语法检查
public
  1. 根目录新建.eslintec.js
module.exports = {
  root: true,
  env: {
    browser: true,
    node: true,
    es2021: true,
  },
  extends: ['eslint:recommended'],
  parserOptions: {
    ecmaVersion: 'latest',
    sourceType: 'module',
  },
  rules: {
    'no-unused-vars': 'off',
  },
};
  1. 配置plugin
const EslintWebpackplugin = require('eslint-webpack-plugin')
module.exports = {
  plugins: [
    new EslintWebpackPlugin({
      context: path.resolve(__dirname, './src'), // 指定文件根目录的字符串
      exclude: 'node_modules', // 排除node_modules文件夹
    }),
  ],
}

3) copy-webpack-plugin

yarn add copy-webpack-plugin -D

const CopyWebpackPlugin = require('copy-webpack-plugin');
module.exports = {
  plugins: [
    new CopyWebpackPlugin({
      patterns: [
        {
          from: path.resolve(__dirname, './public'), // copy来源文件夹
          to: path.resolve(__dirname, './dist'), // copy目的文件夹
          globOptions: {
            ignore: ['**/index.html'], // 排除index.html文件 (已经通过html-webpack-plugin处理)
          },
        },
      ],
    }),
  ],
}

Webpack高级-优化章

一、缓存

  1. 缓存可以减少babel和eslint的编译次数,减少AST的构建过程,从而减少性能和编译时间开销
  2. 开启babel-loader 和eslint-webpack-plugin缓存
module:{
  rules:[
   {
      test: /.(jsx?)$/,
      exclude: /node_modules/,
      use: {
        loader: 'babel-loader',
        options: {
          cacheDirectory: true, // 开启缓存
        },
      },
    },
  ]
},
plugins:[
  new EslintWebpackPlugin({
    context: path.resolve(__dirname, './src'), // 指定文件根目录的字符串
    exclude: 'node_modules', // 排除node_modules文件夹
    cache: true, // 开启缓存
    cacheLocation: path.resolve(__dirname, './node_modules/.cache/.eslintcache'), // 缓存文件地址
  }),
]

二、使用thread-loader多线程处理

  1. 默认node中js的编译过程是单线程的,可以使用thread-loader开启多线程并发完成代码编译,完成后交给主线程处理,从而提高运行效率,减少编译时间

yarn add thread-loader -D

const threads = os.cpus().length; // cpu线程数量
module:{
  rules:[
   {
      test: /.(jsx?)$/,
      exclude: /node_modules/,
      use: [
          {
            loader: 'babel-loader',
            options: {
              cacheDirectory: true, // 开启缓存
            },
          },
          {
            loader: 'thread-loader',
            options: {
              // 产生的worker数量
              workers: threads,
              // 一个worker工作量
              workerParallelJobs: 50,
            },
          },
        ],
    },
  ]
}

三、减少babel转换代码体积@babel/plugin-transform-runtime

yarn add @babel/plugin-transform-runtime -D

  1. @babel/plugin-transform-runtime

修改babel.config.js中的配置

module.exports = {
  presets: [['@babel/preset-env']],
  plugins: [
    [
      '@babel/plugin-transform-runtime',
      {
        helpers: true, // 是否自动引入辅助函数
        corejs: 3, // core-js的版本
        useESModules: false, // 是否使用es6模块化
      },
    ],
  ],
};

四、Css代码压缩和Js代码压缩

  1. 代码压缩仅需要在生产环境配置
  2. 使用mini-css-extract-plugincss-minimization-webpack-plugin转换和压缩css代码
  3. 使用terser-webpack-plugin压缩Js代码
const path = require('path');
const os = require('os');
// 用于压缩Css代码插件
const CssMinimizerWebpackPlugin = require('css-minimizer-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
// 用于压缩Js代码插件

// 将上文中使用的style-loader替换为MiniCssExtractPlugin.loader
const setStyleLoaders = (i, loader) => [MiniCssExtractPlugin.loader, { loader: 'css-loader', options: { importLoaders: i } }, 'postcss-loader', loader].filter(Boolean);
const threads = os.cpus().length;

module.exports = {
  optimization: {
      minimize:true, // 告诉webpack使用自己的插件压缩bundle
      minimizer: [
        new CssMinimizerWebpackPlugin(),
        new TerserWebpackPlugin({
          parallel: threads, // 开启多线程压缩
        }),
      ],
   },
   plugins:[
     new MiniCssExtractPlugin({
      filename: '[name].[contenthash:10].css', // 指定文件名称
    }),
   ]
}

五、单文件按需引入和代码分割

  1. 在开发单页面应用时,在使用import做静态引入时,代码分割尤为重要
  2. 使用splitChunks可以根据chunk大小进行分割,在代码提交和加载请求次数内均衡选择
optimization: {
  splitChunks: {
    chunks: 'all', // 代码分割优化仅选择initial(初始块),async(按需块),all(所有块)
    minSize: 30000, // chunk被分割时的最小单位 默认值:30kb
    minChunks: 1, // 一个模块被引用的次数达到多少时分割
    maxAsyncRequests: 5, // 按需加载文件时 并行请求的最大数目。默认为5。
    maxInitialRequests: 3, // 加载入口文件时 并行请求的最大数目。默认为3。
    // 符合以上条件的chunk会进入cacheGroups做最后更细致的优化判断
    // 后续搭建react-cli时就可以在这里将react react-dom react-router-dom相关的配置分割成一个chunk
    cacheGroups: {
      react: {
        test: /[\\/]node_modules[\\/]react(.*)?[\\/]/, // 表示引入库存在于node_modules相关的react库
        priority: 20, // 数字越大优先级越高
        filename: 'react.chunk.js',
      },
      vendors: {
        test: /[\/]node_modules[\/]/, // 表示引入库存在于node_modules
        priority: -10, // 数字越大优先级越高
        filename: 'vendors.chunk.js', // 设置代码分割后的文件名
      },
    },
  },
},

测试代码分割

yarn add react react-dom

在打包入口文件src/index.js中添加react相关库,测试代码分割

// main.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import './style.css';

终端执行 yarn build 打包后的结果
image.png

六、关闭打包分析

module.exports = {
  performance: false, // 关闭打包分析
}

七、清理控制台无用输出 (美化)

yarn add friendly-errors-webpack-plugin -D

// 隐藏控制台无用打印信息
const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin');
module.exports = {
  stats: 'errors-only', // 控制台只显示错误信息
  plugins: [
    new WebpackBarPlugin({
      color: '#85d3d1', // 默认green,进度条颜色支持HEX
      basic: false, // 默认true,启用一个简单的日志报告器
      profile: false, // 默认false,启用探查器。
    }),
    new FriendlyErrorsWebpackPlugin({
      // 成功的时候输出
      compilationSuccessInfo: {
        messages: [`Your application is running here: localhost:4000`],
      },
      // 是否每次都清空控制台
      clearConsole: true,
    }),
  ],
}

八、控制台美化,添加构建进度条 (美化)

yarn add webpackbar -D

// 进度条美化
const WebpackBarPlugin = require('webpackbar');
module.exports = {
  plugins: [
    new WebpackBarPlugin({
      color: '#85d3d1', // 默认green,进度条颜色支持HEX
      basic: false, // 默认true,启用一个简单的日志报告器
      profile: false, // 默认false,启用探查器。
    }),
  ],
}

Webpack高级-搭建react-cli

  1. 新建文件夹 react-cli
  2. 初始化配置 yarn init
  3. package.json添加启动命令 (使用cross-env 添加环境变量) - 点击前往cross-env
  4. yarn add cross-env -D
{
  "name": "react-cli",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "dev":"npm run serve",
    "serve":"cross-env NODE_ENV=development webpack server --config ./config/webpack.deve.js",
    "build":"cross-env NODE_ENV=production webpack --config ./config/webpack.prod.js"
  }
}
  1. 安装webpack相关依赖
  2. yarn add webpack webpack-cli webpack-dev-server -D
  3. 安装style相关依赖
  4. yarn add style-loader css-loader postcss-loader sass-loader node-sass -D
  5. 安装Js、babel等相关依赖
  6. yarn add babel-loader thread-loader -D
  7. 安装插件plugin
  8. yarn add css-minimizer-webpack-plugin mini-css-extract-plugin terser-webpack-plugin copy-webpack-plugin eslint-webpack-plugin eslint html-webpack-plugin webpackbar friendly-errors-webpack-plugin -D
  9. 安装react相关依赖
  10. yarn add react react-dom react-router-dom -S
  11. 安装react提供的预设和热更新插件
  12. yarn add babel-preset-react-app eslint-config-react-app @pmmmwh/react-refresh-webpack-plugin react-refresh -D
  1. 跟目录新建config文件夹 并添加webpack.deve.js和webpack.prod.js和webpack.base.js文件
  2. 跟目录新建src/main.js文件 (这是我们的入口文件)
  3. 跟目录新建public/index.html和favicon.ico图标,点击在线制作ico图标,然后将制作好的图表名称改为favicon.ico
  4. 跟目录新建.browserslistrc文件,新建babel.config.js,新建postcss.config.js,新建.eslintrc.js,新建.eslintignore

1. .browserslistrc文件

> 0.1%
last 2 version
not dead

2. babel.config.js

const isDevelopment = process.env.NODE_ENV === 'development';
module.exports = {
  presets: ['react-app'], // 使用react提供的babel预设
  // 只有在开发模式下使用react的热更新插件
  plugins: [isDevelopment && require.resolve('react-refresh/babel')].filter(Boolean),
};

3. postcss.config.js

module.exports = {
  plugins: [require('postcss-preset-env')],
};

4. .eslintrc.js 注:eslint配置详情

module.exports = {
  root: true,
  env: {
    browser: true,
    node: true,
    es2021: true,
  },
  extends: ['react-app', 'eslint:recommended'],
  parserOptions: {
    ecmaVersion: 'latest',
    sourceType: 'module',
    babelOptions: {
      presets: [['babel-preset-react-app', false], 'babel-preset-react-app/prod'],
    },
  },
  rules: {
    'no-unused-vars': 'off',
  },
};

5. eslintignore

# 忽略build目录下类型为js的文件的语法检查
build/*.js
# 忽略src/assets目录下文件的语法检查
src/assets
# 忽略src/utils目录下文件的语法检查
src/utils
# 忽略public目录下文件的语法检查
public

6. webpack.base.js

const path = require('path');
const os = require('os');

const HtmlWebpackplugin = require('html-webpack-plugin');
const CssMinimizerWebpackPlugin = require('css-minimizer-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const TerserWebpackPlugin = require('terser-webpack-plugin');
const { DefinePlugin } = require('webpack');
const threads = os.cpus().length;
// 是否是生成环境
const isProduction = process.env.NODE_ENV === 'production';
const resolvePath = _path => path.resolve(__dirname, _path);

// 生产环境使用MiniCssExtractPlugin.loader 开发环境使用style-loader
const setStyleLoaders = (i, loader) => [isProduction ? MiniCssExtractPlugin.loader : 'style-loader', { loader: 'css-loader', options: { importLoaders: i } }, 'postcss-loader', loader].filter(Boolean); // loader参数可能为undefined,过滤一下结果

const baseConfig = {
  entry: resolvePath('../src/main.js'), // 指定入口文件
  output: {
    path: isProduction ? undefined : resolvePath('../dist'), // 设置打包文件目录
    filename: 'static/js/[name].[contenthash:8].js', // 多文件时必须动态输出文件名称,这里的[name]可entry中的key对应
    assetModuleFilename: 'static/media/[name].[contenthash:8][ext][query]', // 资源文件打包到输出文件路径下的/static中
    chunkFilename: 'static/js/[name].[contenthash:8].chunk.js', // chunk文件名称
    publicPath: '', // 默认值 相对于 HTML 页面
    clean: true, // 在生成文件之前清空 output 目录
  },
  resolve: {
    alias: {
      '@': resolvePath('../src'),
    },
    extensions: ['.js', '.jsx', '.tsx'],
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: setStyleLoaders(1),
      },
      {
        test: /\.less$/,
        use: setStyleLoaders(2, 'less-loader'),
      },
      {
        test: /\.s[ac]ss$/,
        use: setStyleLoaders(2, 'sass-loader'),
      },
      {
        test: /.(png|jpe?g|gif|svg|webg)$/,
        type: 'asset',
        parser: {
          dataUrlCondition: {
            // 超过10kb导出文件,小于10kb直接使用base64格式添加到行内
            maxSize: 10 * 1024,
          },
        },
      },
      {
        test: /.(ttf|woff)$/,
        type: 'asset/resource',
      },
      {
        test: /.(jsx?)$/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'babel-loader',
            options: {
              cacheDirectory: true, // 开启缓存
            },
          },
          {
            loader: 'thread-loader',
            options: {
              // 产生的worker数量
              workers: threads,
              // 一个worker工作量
              workerParallelJobs: 50,
            },
          },
        ],
      },
    ],
  },
  optimization: {
    // 只在生产环境开启压缩优化
    minimize: isProduction, // 告诉webpack使用自己的插件压缩bundle
    minimizer: [
      new CssMinimizerWebpackPlugin(),
      new TerserWebpackPlugin({
        parallel: threads, // 开启多线程压缩
      }),
    ], // 插件列表
    splitChunks: {
      chunks: 'all', // 代码分割优化仅选择initial(初始块),async(按需块),all(所有块)
      minSize: 30000, // chunk被分割时的最小单位 默认值:30kb
      minChunks: 1, // 一个模块被引用的次数达到多少时分割
      maxAsyncRequests: 5, // 按需加载文件时 并行请求的最大数目。默认为5。
      maxInitialRequests: 3, // 加载入口文件时 并行请求的最大数目。默认为3。
      // 符合以上条件的chunk会进入cacheGroups做最后更细致的优化判断
      // 后续搭建react-cli时就可以在这里将react react-dom react-router-dom相关的配置分割成一个chunk
      cacheGroups: {
        react: {
          test: /[\\/]node_modules[\\/]react(.*)?[\\/]/, // 表示引入库存在于node_modules相关的react库
          priority: 20, // 数字越大优先级越高 (-10大于-20)
          filename: 'static/js/react.chunk.js',
        },
        vendors: {
          test: /[\/]node_modules[\/]/, // 表示引入库存在于node_modules
          priority: -10, // 数字越大优先级越高
          filename: 'static/js/vendors.chunk.js', // 设置代码分割后的文件名
        },
      },
    },
    runtimeChunk: {
      name: entrypoint => `runtime-${entrypoint.name}.chunk`,
    },
  },
  plugins: [
    new HtmlWebpackplugin({
      template: resolvePath('../public/index.html'), // html模板文件
      title: 'react-cli', // 添加变量
    }),
    new DefinePlugin({
      BASE_URL: '"/"',
    }),
  ],
  performance: false, // 关闭打包分析
};
module.exports = {
  baseConfig,
  resolvePath,
};

7. webpack.deve.js

  • webpack开发环境配置
const EslintWebpackPlugin = require('eslint-webpack-plugin');
const { merge } = require('webpack-merge');
const { resolvePath, baseConfig } = require('./webpack.base');
// react热更新插件
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
// 进度条美化
const WebpackBarPlugin = require('webpackbar');
// 隐藏控制台无用打印信息
const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin');

// 开发环境配置
module.exports = merge(baseConfig, {
  mode: 'development',
  stats: 'errors-only', // 控制台只显示错误信息
  devtool: 'cheap-module-source-map', // 此选项控制是否生成,以及如何生成 source map。
  devServer: {
    hot: true, // 开启HMR热模块替换
    open: true, // 自动打开网页
    compress: true, // 开启gzig压缩
    historyApiFallback: true, // 解决history模式下重定向问题,找不到请求路径时重定向到index.html文件
    host: 'localhost', // 指定主机地址
    port: 4000, // 指定端口号
    proxy: {
      '/api': {
        target: 'http://localhost:8888', // 需要转发的目标地址
        changeOrigin: true, // 覆盖请求主机来源
        pathRewrite: { '^/api': '' }, // 重写路径
      },
    },
    client: {
      progress: true, // 浏览器中以百分比显示进度
    },
  },
  plugins: [
    new EslintWebpackPlugin({
      context: resolvePath('../src'), // 指定文件根目录的字符串
      exclude: 'node_modules', // 排除node_modules文件夹
      cache: true, // 开启缓存
      cacheLocation: resolvePath('../node_modules/.cache/.eslintcache'), // 缓存文件地址
    }),
    new ReactRefreshWebpackPlugin(), // react热更新插件
    new WebpackBarPlugin({
      color: '#85d3d1', // 默认green,进度条颜色支持HEX
      basic: false, // 默认true,启用一个简单的日志报告器
      profile: false, // 默认false,启用探查器。
    }),
    new FriendlyErrorsWebpackPlugin({
      // 成功的时候输出
      compilationSuccessInfo: {
        messages: [`Your application is running here: localhost:4000`],
      },
      // 是否每次都清空控制台
      clearConsole: true,
    }),
  ],
});

8. webpack.prod.js

  • webpack生产环境配置
const CopyWebpackPlugin = require('copy-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

const { merge } = require('webpack-merge');
const { resolvePath, baseConfig } = require('./webpack.base');

// 生产环境配置
module.exports = merge(baseConfig, {
  mode: 'production',
  devtool: 'source-map', // 此选项控制是否生成,以及如何生成 source map。
  plugins: [
    new MiniCssExtractPlugin({
      filename: 'static/css/[name].[contenthash:10].css',
    }),
    new CopyWebpackPlugin({
      patterns: [
        {
          from: resolvePath('../public'), // copy来源文件夹
          to: resolvePath('../dist'), // copy目的文件夹
          globOptions: {
            ignore: ['**/index.html'], // 排除index.html文件 (已经通过html-webpack-plugin处理)
          },
        },
      ],
    }),
  ],
});

9. src/main.js

  • 入口文件
import React from 'react';
import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';

const root = createRoot(document.getElementById('app'));
root.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>
);

10. src/App.js

  • 入口文件
import React, { lazy, Suspense } from 'react';
import { Link, Routes, Route } from 'react-router-dom';
// 使用lazy动态加载组件
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const App = () => {
  return (
    <>
      <Link to="./home">home</Link>
      <Link to="./about">about</Link>
      <img src={require('./assets/hero.jpg')} width={500} height={300}></img>
      <Suspense fallback={<>加载中...</>}>
        <Routes>
          <Route path="/home" element={<Home></Home>}></Route>
          <Route path="/about" element={<About></About>}></Route>
        </Routes>
      </Suspense>
    </>
  );
};
export default App;