从零使用webpack5搭建React18+Ts项目

265 阅读3分钟

最近使用最新的webpack5搭建了一个react新版本的项目,希望对大家有帮助,不足也请热心评论一起讨论

image.png

   yarn add webpack webpack-cli webpack-dev-server webpack-merge webpack-bundle-analyzer -D

build
----webpack.ananly.ts
----webpack.base.ts
----webpack.dev.ts
----webpack.prod.ts

package.json

  "scripts": {
    "dev:dev": "cross-env NODE_ENV=development BASE_ENV=development webpack serve -c build/webpack.dev.ts",
    "dev:test": "cross-env NODE_ENV=development BASE_ENV=test webpack serve -c build/webpack.dev.ts",
    "dev:pre": "cross-env NODE_ENV=development BASE_ENV=pre webpack serve -c build/webpack.dev.ts",
    "dev:prod": "cross-env NODE_ENV=development BASE_ENV=production webpack serve -c build/webpack.dev.ts",
    "build:dev": "cross-env NODE_ENV=production BASE_ENV=development webpack -c build/webpack.prod.ts",
    "build:test": "cross-env NODE_ENV=production BASE_ENV=test webpack -c build/webpack.prod.ts",
    "build:pre": "cross-env NODE_ENV=production BASE_ENV=pre webpack -c build/webpack.prod.ts",
    "build:prod": "cross-env NODE_ENV=production BASE_ENV=production webpack -c build/webpack.prod.ts",
    "build:analy": "cross-env NODE_ENV=production BASE_ENV=production webpack -c build/webpack.analy.ts"
  },

webpack.base.ts公共配置 基本五大核心结构
主要:对 ts tsx css less sass 图片 字体 媒体 json文件 等静态资源进行解析

import { Configuration, DefinePlugin } from 'webpack'
import path from 'path'
import MiniCssExtractPlugin from 'mini-css-extract-plugin'
import * as dotenv from 'dotenv'
import HtmlWebpackPlugin from 'html-webpack-plugin'
import WebpackBar from 'webpackbar'

//判断当前运行属于什么环境下
const isDev = process.env.NODE_ENV === 'development'

//开发和生产环境下css解析配置
const styleLoadersArray=[
    isDev?'style-loader':MiniCssExtractPlugin.loader,
    {
        loader:'css-loader',
        options:{
            modules:{
                localIdentName:'[path][name]__[local]--[contenthash:8]'
            }
        }
    },
    'postcss-loader'
]
// 加载配置文件
const envConfig = dotenv.config({
    path: path.resolve(__dirname, '../env/.env.' + process.env.BASE_ENV)
  })
const baseConfig:Configuration = {
    entry:path.join(__dirname,'../src/index.tsx'),//入口文件
    //打包出口文件
    output:{
        filename:'static/js[name][chunkhash:8].js',//每个输出js的名称
        path:path.join(__dirname,"../dist"),//打包结果输出路径
        clean:true,//webpack4需要配置clean-webpack-plugin来删除dist文件,webpack5内置了
        publicPath:"/" //打包后文件的公共前缀路径
    },
    module:{
        rules:[
            {
                test:/.(ts|tsx)$/, //匹配ts tsx文件
                exclude:/node_modules/, //不匹配 小优化
                use:["thread-loader","babel-loader"]//项目大以后开启thread-loader
            },
            {
                test:/\.css$/,//匹配css文件
                use:styleLoadersArray
            },
            {
                test: /\.less$/,//匹配less文件
                use: [
                  ...styleLoadersArray,
                  {
                    loader: 'less-loader',
                    options: {
                      lessOptions: {
                        importLoaders: 2,
                        // 可以加入modules: true,这样就不需要在less文件名加module了
                        modules: true,
                        // 如果要在less中写类型js的语法,需要加这一个配置
                        javascriptEnabled: true
                      }
                    }
                  }
                ]
              },
              {
                test: /\.(scss|sass)$/,
                use: [...styleLoadersArray, 'sass-loader']
              },
              {
                test: /\.styl$/,
                use: [...styleLoadersArray, 'stylus-loader']
              },
              {
                test:/\.(png|jpe?g|gif|svg)$/i,//匹配图片文件
                type:"asset",
                parser:{
                    dataUrlCondition: {
                        maxSize: 20 * 1024 // 小于20kb转base64
                    }
                },
                generator: {
                    filename: 'static/images/[contenthash:8][ext][query]' // 文件输出目录和命名
                }
              },
              {
                test: /.(woff2?|eot|ttf|otf)$/, // 匹配字体图标文件
                type: 'asset', // type选择asset
                parser: {
                  dataUrlCondition: {
                    maxSize: 10 * 1024 // 小于10kb转base64
                  }
                },
                generator: {
                  filename: 'static/fonts/[contenthash:8][ext][query]' // 文件输出目录和命名
                }
              },
              {
                test: /.(mp4|webm|ogg|mp3|wav|flac|aac)$/, // 匹配媒体文件
                type: 'asset', // type选择asset
                parser: {
                  dataUrlCondition: {
                    maxSize: 10 * 1024 // 小于10kb转base64
                  }
                },
                generator: {
                  filename: 'static/media/[contenthash:8][ext][query]' // 文件输出目录和命名
                }
              },
              {
                // 匹配json文件
                test: /\.json$/,
                type: 'asset/resource', // 将json文件视为文件类型
                generator: {
                  // 这里专门针对json文件的处理
                  filename: 'static/json/[name].[contenthash:8][ext][query]'
                }
              }
        ]
    },
    resolve:{
        extensions: ['.ts', '.tsx', '.js', '.jsx', '.less', '.css'],
        // 别名需要配置两个地方,这里和 tsconfig.json
        alias: {
          '@': path.join(__dirname, '../src')
        },
        modules: [path.resolve(__dirname, '../node_modules')] // 查找第三方模块只在本项目的node_modules中查找
    },
    cache:{
        type: 'filesystem' // 使用文件缓存
    },
    plugins:[
        new HtmlWebpackPlugin({
            title: 'webpack5-react-ts',
            filename: 'index.html',
            // 复制 'index.html' 文件,并自动引入打包输出的所有资源(js/css)
            template: path.join(__dirname, '../public/index.html'),
            inject: true, // 自动注入静态资源
            hash: true,
            cache: false,
            // 压缩html资源
            minify: {
              removeAttributeQuotes: true,
              collapseWhitespace: true, //去空格
              removeComments: true, // 去注释
              minifyJS: true, // 在脚本元素和事件属性中缩小JavaScript(使用UglifyJS)
              minifyCSS: true // 缩小CSS样式元素和样式属性
            },
            nodeModules: path.resolve(__dirname, '../node_modules')
          }),
          // 注入到业务
          new DefinePlugin({
            'process.env': JSON.stringify(envConfig.parsed),
            'process.env.BASE_ENV': JSON.stringify(process.env.BASE_ENV),
            'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV)
          }),
          new WebpackBar({
            color: '#85d', // 默认green,进度条颜色支持HEX
            basic: false, // 默认true,启用一个简单的日志报告器
            profile: false // 默认false,启用探查器。
          })
    ].filter(Boolean),
}

export default baseConfig

webpakc.dev.ts主要对于开发模式下webpack-dev-server主要进行配置和热更新和对选择合适的source-map

import path from 'path';
import baseConfig from "./webpack.base";
import webpack, { Configuration as WebpackConfiguration } from 'webpack';
import { merge } from 'webpack-merge';
import WebpackDevServer, {
    Configuration as WebpackDevServerConfiguration,
} from 'webpack-dev-server';
import ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin'
// 运行命令的时候重启一次打开一个tab 页很烦,所以呢优化一下
// 参考:create-react-app 的启动方式
// https://github.com/facebook/create-react-app/blob/main/packages/react-dev-utils/openChrome.applescript
// 记得关闭webpack-dev-server的配置中的自动打开 open: false 或者注释
// const openBrowser = require("./util/openBrowser");

interface Configuration extends WebpackConfiguration {
    devServer?: WebpackDevServerConfiguration;
}

const host = '127.0.0.1';
const port = '8082';
const devConfig: Configuration = merge(baseConfig, {
    mode: 'development', // 开发模式,打包更加快速,省了代码优化步骤
    /**
      开发环境推荐:eval-cheap-module-source-map
      ● 本地开发首次打包慢点没关系,因为 eval 缓存的原因, 热更新会很快
      ● 开发中,我们每行代码不会写的太长,只需要定位到行就行,所以加上 cheap
      ● 我们希望能够找到源代码的错误,而不是打包后的,所以需要加上 module
     */
    devtool: 'eval-cheap-module-source-map',
    plugins: [
        new ReactRefreshWebpackPlugin()// 添加热更新插件
    ],

})

const devServer = new WebpackDevServer(
    {
        host, // 地址
        port, // 端口
        open: false, // 是否自动打开,关闭
        setupExitSignals: true, // 允许在 SIGINT 和 SIGTERM 信号时关闭开发服务器和退出进程。
        compress: false, // gzip压缩,开发环境不开启,提升热更新速度
        hot: true, // 开启热更新,后面会讲react模块热替换具体配置
        historyApiFallback: true, // 解决history路由404问题
        static: {
            directory: path.join(__dirname, '../public'), // 托管静态资源public文件夹
        },
        headers: { 'Access-Control-Allow-Origin': '*' },
    },
    webpack(devConfig),
);
devServer.start().then(() => {
    // 启动界面
    console.log(`http://${host}:${port}`);
});
export default devConfig;

webpack.prod.ts 生产环境下主要进行代码的多线程压缩 公共代码分割提取

import path from 'path'
import { Configuration } from 'webpack'
import { merge } from 'webpack-merge'
import CopyPlugin from 'copy-webpack-plugin'
import CssMinimizerPlugin from 'css-minimizer-webpack-plugin'
import TerserPlugin from 'terser-webpack-plugin'
import CompressionPlugin from 'compression-webpack-plugin'
import baseConfig from './webpack.base'
const globAll = require('glob-all')
const { PurgeCSSPlugin } = require('purgecss-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')

const prodConfig: Configuration = merge(baseConfig, {
  mode: 'production', // 生产模式,会开启tree-shaking和压缩代码,以及其他优化
  /**
   * 打包环境推荐:none(就是不配置devtool选项了,不是配置devtool: 'none')
   * ● none话调试只能看到编译后的代码,也不会泄露源代码,打包速度也会比较快。
   * ● 只是不方便线上排查问题, 但一般都可以根据报错信息在本地环境很快找出问题所在。
   */
  plugins: [
    new CopyPlugin({
      patterns: [
        {
          from: path.resolve(__dirname, '../public'), // 复制public下文件
          to: path.resolve(__dirname, '../dist'), // 复制到dist目录中
          filter: source => !source.includes('index.html') // 忽略index.html
        }
      ]
    }),
    new MiniCssExtractPlugin({
      filename: 'static/css/[name][contenthash:8].css' // 抽离css的输出目录和名称
    }),
    // 打包时生成gzip文件
    new CompressionPlugin({
      test: /\.(js|css)$/, // 只生成css,js压缩文件
      filename: '[path][base].gz', // 文件命名
      algorithm: 'gzip', // 压缩格式,默认是gzip
      threshold: 10240, // 只有大小大于该值的资源会被处理。默认值是 10k
      minRatio: 0.8 // 压缩率,默认值是 0.8
    }),
    new PurgeCSSPlugin({
      paths: globAll.sync(
        [`${path.join(__dirname, '../src')}/**/*`, path.join(__dirname, '../public/index.html')],
        {
          nodir: true
        }
      ),
      // 用 only 来指定 purgecss-webpack-plugin 的入口
      // https://github.com/FullHuman/purgecss/tree/main/packages/purgecss-webpack-plugin
      only: ['dist'],
      safelist: {
        standard: [/^ant-/] // 过滤以ant-开头的类名,哪怕没用到也不删除
      }
    })
  ],
  optimization: {
    // splitChunks: {
    //   chunks: "all",
    // },
    runtimeChunk: {
      name: 'mainifels'
    },
    minimize: true,
    minimizer: [
      new CssMinimizerPlugin(), // 压缩css
      new TerserPlugin({
        parallel: true, // 开启多线程压缩
        terserOptions: {
          compress: {
            pure_funcs: ['console.log'] // 删除console.log
          }
        }
      })
    ],
    splitChunks: {
      // 分隔代码
      cacheGroups: {
        vendors: {
          // 提取node_modules代码
          test: /node_modules/, // 只匹配node_modules里面的模块
          name: 'vendors', // 提取文件命名为vendors,js后缀和chunkhash会自动加
          minChunks: 1, // 只要使用一次就提取出来
          chunks: 'initial', // 只提取初始化就能获取到的模块,不管异步的
          minSize: 0, // 提取代码体积大于0就提取出来
          priority: 1 // 提取优先级为1
        },
        commons: {
          // 提取页面公共代码
          name: 'commons', // 提取文件命名为commons
          minChunks: 2, // 只要使用两次就提取出来
          chunks: 'initial', // 只提取初始化就能获取到的模块,不管异步的
          minSize: 0 // 提取代码体积大于0就提取出来
        }
      }
    }
  },
  performance: {
    hints: false,
    maxAssetSize: 4000000, // 整数类型(以字节为单位)
    maxEntrypointSize: 5000000 // 整数类型(以字节为单位)
  }
})

export default prodConfig

先这样