Webpack

coderljw 2024-10-13 Webpack
  • Webpack
大约 8 分钟

# 1. Webpack 配置

  • webpack.config.js

    // Node 内置路径模块
    const { resolve } = require('path')
    // 生成 HTML
    const HtmlWebpackPlugin = require('html-webpack-plugin')
    // 提取 CSS 成单独文件
    const MiniCssExtractPlugin = require('mini-css-extract-plugin')
    // 压缩 CSS
    const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
    
    const isProduction = process.env.NODE_ENV === 'production'
    
    const commonCssLoader = [
      // style-loader 自带 HMR
      isProduction ? MiniCssExtractPlugin.loader : 'style-loader',
      'css-loader',
      {
        loader: 'postcss-loader',
        options: {
          ident: 'postcss',
          plugins: () => [require('postcss-preset-env')],
        },
      },
    ]
    
    module.exports = {
      // 模式:development(开发)、production(生产)
      mode: 'production',
    
      // 入口:要打包的文件
      entry: resolve(__dirname, './src/index.js'),
    
      // 出口
      output: {
        // 资源引用路径前缀
        publicPath: isProduction ? './' : '/',
        // 打包后的文件夹路径
        path: resolve(__dirname, 'build'),
        // 打包后文件的名字
        filename: 'js/[contenthash:10].js',
        // 非入口 chunk 名称(会影响文件命名缓存,可使用 optimization 中 runtimeChunk 配置可解决)
        chunkFilename: 'js/[name]_chunk.js',
      },
    
      // 插件
      plugins: [
        new HtmlWebpackPlugin({
          // 要在内存中生成的文件
          template: './src/index.html',
          // 内存中生成文件的名字
          filename: 'index.html',
          // 压缩HTML
          minify: {
            // 移除空格
            collapseWhitespace: true,
            // 移除注释
            removeComments: true,
          },
        }),
        new MiniCssExtractPlugin({
          filename: 'css/built.[contenthash:10].css',
        }),
        new OptimizeCssAssetsWebpackPlugin(),
      ],
    
      // loader规则配置(多个loader倒序使用use)
      module: {
        rules: [
          // eslint
          {
            test: /\.js$/,
            loader: 'eslint-loader',
            // 优先执行
            enforce: 'pre',
            exclude: /node_modules/,
            options: {
              // 自动修复
              fix: true,
            },
          },
          // oneOf:只匹配一个 loader(不能有多个loader处理相同类型文件)
          {
            oneOf: [
              // babel
              {
                test: /\.js$/,
                loader: 'babel-loader',
                exclude: /node_modules/,
                options: {
                  presets: [
                    '@babel/preset-env',
                    {
                      // 按需加载
                      useBuiltIns: 'usage',
                      // 指定 core-js 版本
                      corejs: {
                        version: 3,
                      },
                      // 指定兼容版本
                      targets: {
                        chrome: '60',
                        firefox: '60',
                        ie: '9',
                        safari: '10',
                        edge: '17',
                      },
                    },
                  ],
                  // 开启 babel 缓存
                  cacheDirectory: true,
                },
              },
              // css
              {
                test: /\.css$/i,
                use: commonCssLoader,
              },
              // less
              {
                test: /\.less$/i,
                use: [...commonCssLoader, 'less-loader'],
              },
              // sass、scss
              {
                test: /\.s[ac]ss$/i,
                use: [...commonCssLoader, 'sass-loader'],
              },
              // 图片
              {
                test: /\.(png|jpe?g|gif)$/i,
                loader: 'url-loader',
                options: {
                  // 图片小于 limit,打包成 base64 格式,大于 limit 生成 hash 名称的图片
                  limit: 10 * 1024,
                  // 10 位 hash 名称图片
                  name: '[hash:10].[ext]',
                  // 输出路径
                  outputPath: 'assets',
                  // true:ESM、false:CJS、默认为 true
                  esModule: false,
                },
              },
              // html图片
              {
                test: /\.html$/i,
                loader: 'html-loader',
              },
              // 其他资源
              {
                // 排除 html|css|js|less|sass|jpe?g|png|gif 资源
                exclude: /\.(html|css|js|less|sass|jpe?g|png|gif)$/i,
                loader: 'file-loader',
                options: {
                  name: '[hash:10].[ext]',
                  // 输出路径
                  outputPath: 'media',
                },
              },
            ],
          },
        ],
      },
    
      // 开发配置
      devServer: {
        // 启动路径(打包出口路径)
        contentBase: resolve(_dirname, 'build'),
        // 开启 HMR
        hot: true,
        // 启动 gzip 压缩
        compress: true,
        // 服务器外部可访问
        host: '0.0.0.0',
        // 启动端口
        port: 7777,
        // 自动打开浏览器
        open: true,
        // 指定打开页面
        openPage: '/home',
        // 可防止热更新失效
        disableHostCheck: true,
        // 不显示启动日志
        clientLogLevel: 'none',
        // 除了基本启动信息,其余都不显示
        quiet: true,
        // 不全屏提示错误
        overlay: false,
        // 代理
        proxy: {
          // 要代理的请求路径
          '/agency': {
            // 要跨域的域名(后端接口地址)
            target: 'http://www.coderljw.ga:4396',
            // 是否开启跨域
            changeOrigin: true,
            // 将 /agency 替换成 ''(删除 /agency)
            pathRewrite: {
              '^/agency': '',
            },
          },
        },
      },
    
      // 解析模块规则
      resolve: {
        // 别名
        alias: {
          '@': resolve(__dirname, 'src'),
        },
        // 省略后缀
        extensions: ['.js', 'json', 'jsx'],
      },
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
  • package.json

    // postcss
    "browserslist": {
      "development": [
        "last 1 chrome version",
        "last 1 firefox version",
        "last 1 safari version"
      ],
      "production": [
        ">0.2%",
        "not dead",
        "not op_mini all"
      ]
    }
    // eslint
    "eslintConfig": {
      // 继承 airbnb-base
      "extends": "airbnb-base",
      "env": {
        // 支持浏览器全局变量
        "browser": true
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22

# 2. 提取 CSS 成单独文件

const MiniCssExtractPlugin = require('mini-css-extract-plugin')

module.exports = {
  module: {
    rules: [
      // css
      {
        test: /\.css$/,
        use: [MiniCssExtractPlugin.loader, 'css-loader'],
      },
    ],
  },

  plugins: [
    new MiniCssExtractPlugin({
      filename: 'css/built.css',
    }),
  ],
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 3. CSS 兼容性处理

  • webpack.config.js

    module.exports = {
      module: {
        rules: [
          {
            test: /\.css$/,
            use: [
              MiniCssExtractPlugin.loader,
              'css-loader',
              {
                loader: 'postcss-loader',
                options: {
                  ident: 'postcss',
                  plugins: () => [require('postcss-preset-env')],
                },
              },
            ],
          },
        ],
      },
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
  • package.json

    "browserslist": {
      "development": [
        "last 1 chrome version",
        "last 1 firefox version",
        "last 1 safari version"
      ],
      "production": [
        ">0.2%",
        "not dead",
        "not op_mini all"
      ]
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12

# 4. 压缩 CSS

const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')

module.exports = {
  plugins: [new OptimizeCssAssetsWebpackPlugin()],
}
1
2
3
4
5

# 5. eslint 语法检查

  • webpack.config.js

    // eslint-config-airbnb(React)
    // eslint-config-airbnb-base(not React)
    
    module.exports = {
      module: {
        rules: [
          {
            test: /\.js$/,
            loader: 'eslint-loader',
            // 优先执行
            enforce: 'pre',
            exclude: /node_modules/,
            options: {
              fix: true,
            },
          },
        ],
      },
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
  • package.json

    "eslintConfig": {
      // 继承 airbnb-base
      "extends": "airbnb-base",
      "env": {
        // 支持浏览器全局变量
        "browser": true
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8

# 6. HMR 热模块替换

  • 作用:重新打包变换的模块,提升构建速度。

    1. 样式文件:style-loader 内部实现了 HMR 功能。
    2. js 文件:默认不能使用 HMR 功能。
    3. html 文件:单页面只有一个 html,不需要实现 HMR 功能。
module.exports = {
  // 开启 HMR 功能会导致html热更新失效,在 entry 引入 html 文件即可解决
  entry: [
    resolve(__dirname, './src/index.js'),
    resolve(__dirname, './src/index.html'),
  ],

  devServer: {
    // 开启 HMR
    hot: true,
  },
}
1
2
3
4
5
6
7
8
9
10
11
12

# 7. source-map

  • 作用:提供源代码与构建后代码的映射,方便追踪调试。

  • [inline- | hidden- | eval-][nosources-][cheap- [module- ]]source-map

  • 外部:会单独生成.map 文件;内联:嵌套在 js 文件中。内联构建速度相对更快。

    1. source-map外部:提示错误信息与源代码错误位置。
    2. inline-source-map内联(只生成一个内联 source-map):与 source-map 一致。
    3. hidden-source-map外部:提示错误信息与构建后代码错误位置,无源代码错误位置。
    4. eval-source-map内联(每一个文件都生成对应的 source-map):与 source-map 一致。
    5. nosources-source-map外部:提示错误信息,无源代码和构建后代码信息。
    6. cheap-source-map外部:与 source-map 一致,但错误位置只精确到行,不包含 loader 的 source-map。
    7. cheap-module-source-map外部:与 cheap-source-map 一致,但包含 loader 的 source-map,module 会将 loader 的 sourcemap 简化为精确到行。
  • 开发环境:速度快,好调试。

    • 速度(eval > inline > cheap > ...)。
    1. eval-cheap-source-map
    2. eval-source-map
    • 调试。
    1. source-map
    2. cheap-module-source-map
    3. cheap-source-map
    • 综上。
    1. eval-source-map
    2. eval-cheap-module-source-map
  • 生产环境:隐藏源代码?好调试?因内联会增大打包体积,生产环境不考虑内联。

    1. source-map
    2. cheap-source-map
module.exports = {
  devtool: 'source-map',
}
1
2
3

# 8. 缓存

  • babel 缓存:cacheDirectory: true

  • HardSourceWebpackPlugin:为模块提供中间缓存,缓存默认的存放路径是: node_modules/.cache/hard-source。首次构建时间没有太大变化,但是第二次开始,构建时间大约可以节约 80%。

  • 文件资源缓存:打包文件名称加 hash(推荐使用 contenthash)。

    1. hash:根据 webpack 构建时生成的唯一 hash 值。
    2. chunkhash:根据 chunk 生成 hash 值,同一个 chunk 的 hash 值一致。
    3. contenthash:根据文件内容生成 hash 值。

# 9. tree-sharking

  • 作用:去除无用的代码,减小代码体积。

  • 开启条件:1. 使用 ESM、2. 生产环境。

  • webpack 版本差异,可能会将 css 文件去除。可在 package.json 中配置副作用文件:"sideEffects: [ "*.css", "*.less" ]"

# 10. code split

  • 作用:代码分割,按需加载。

    多入口:应用于多页面。

    module.exports = {
      // 多入口
      entry: {
        index: resolve(__dirname, './src/index.js'),
        home: resolve(__dirname, './src/home.js'),
      },
    
      // 出口
      output: {
        // 打包后的文件夹路径
        path: resolve(__dirname, 'build'),
        // 打包后文件的名字
        filename: 'js/[name].[contenthash:10].js',
      },
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15

    optimization。

    module.exports = {
      // 单入口
      entry: resolve(__dirname, './src/index.js'),
    
      /*
      // 多入口
      entry: {
        index: resolve(__dirname, './src/index.js'),
        home: resolve(__dirname, './src/home.js'),
      },
      */
    
      // 出口
      output: {
        // 打包后的文件夹路径
        path: resolve(__dirname, 'build'),
        // 打包后文件的名字
        filename: 'js/[contenthash:10].js',
      },
      // 1. 将 node_modules 分别打包
      // 2. 多入口使用相同文件时,只打包一个 chunk
      optimization: {
        splitChunks: {
          chunks: 'all', // 选择哪些 chunk 进行优化
          minSize: 30000, // 生成 chunk 的最小体积
          minChunks: 1, // 拆分前必须共享模块的最小 chunks 数
          maxAsyncRequests: 30, // 限制异步模块最大并行数(import())
          maxInitialRequests: 30, // 限制入口拆分数量
          automaticNameDelimiter: '~', // 文件名称连接符
          cacheGroups: {
            react: {
              name: 'react', // chunk 名称
              test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/, // 匹配条件
              priority: 10, // 优先级
            },
            vendors: {
              name: 'vendors',
              test: /[\\/]node_modules[\\/]/,
              priority: -10,
            },
            commons: {
              name: 'commons',
              minChunks: 2,
              priority: -20,
            },
          },
        },
        // 将引用模块的 hash 单独打包(解决出口 chunkFilename 配置文件命名缓存失效)
        runtimeChunk: {
          name: entrypoint => `runtime-${entrypoint.name}`,
        },
      },
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53

    import 动态导入。

    const home = await import(/* webpackChunkName: 'home' */ './home')
    
    1

# 12. 懒加载和预加载

  • 懒加载:使用时加载。

    const home = await import(/* webpackChunkName: 'home' */ './home')
    
    1
  • 预加载:等其他资源加载完后加载,使用时再从缓存中读取。

    const home = await import(
      /* webpackChunkName: 'home', webpackPrefetch: true */ './home'
    )
    
    1
    2
    3

# 13. PWA

  • 作用:渐进式网络开发应用程序(离线可访问)。

  • webpack.config.js

    const WorkboxWebpackPlugin = require('workbox-webpack-plugin')
    
    module.exports = {
      plugins: [
        new WorkboxWebpackPlugin.GenerateSW({
          // 清除旧 SW
          clientsClaim: true,
          // 快速启动
          skipWaiting: true,
        }),
      ],
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
  • index.js

    if ('serviceWorker' in navigator)
      window.addEventListener('load', () =>
        navigator.serviceworker.register('/service-worker.js')
      )
    
    1
    2
    3
    4

# 14. 多进程打包

  • 作用:提升打包耗时程序的速度,主要针对 babel loader 优化。

    1. 进程启动大概为 600ms,进程通信也有开销。
    2. 只有工作耗时长的程序,才需要多进程打包。
module.exports = {
  module: {
    rules: [
      // babel
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: [
          'thread-loader',
          {
            loader: 'babel-loader',
            options: {
              presets: [
                '@babel/preset-env',
                {
                  // 按需加载
                  useBuiltIns: 'usage',
                  // 指定 core-js 版本
                  corejs: {
                    version: 3,
                  },
                  // 指定兼容版本
                  targets: {
                    chrome: '60',
                    firefox: '60',
                    ie: '9',
                    safari: '10',
                    edge: '17',
                  },
                },
              ],
              // 开启 babel 缓存
              cacheDirectory: true,
            },
          },
        ],
      },
    ],
  },
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

# 15. externals

  • 作用:指定一些第三方库不进行打包,自行配置 CDN 资源。
module.exports = {
  externals: {
    vue: 'Vue',
  },
}
1
2
3
4
5

# 16. dll

  • 作用:指定一些第三方库单独打包。

    webpack.config.js

    const webpack = require('webpack')
    const AddAssetHtmlWebpackPlugin = require('add-assets-html-webpack-plugin')
    
    module.exports = {
      mode: 'production',
    
      entry: resolve(__dirname, './src/index.js'),
    
      output: {
        filename: 'js/[contenthash:10].js',
        path: resolve(__dirname, 'build'),
      },
    
      plugins: [
        // 通知 webpack 不打包的库,同时更改引入名称
        new webpack.DllReferencePlugin({
          manifest: resolve(__dirname, 'dll/manifest.json'),
        }),
        // 将文件打包输出,并在 html 自动引入
        new AddAssetHtmlWebpackPlugin({
          filepath: resolve(__dirname, 'dll/vue.js'),
        }),
      ],
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24

    webpack.dll.js

    const webpack = require('webpack')
    
    module.exports = {
      mode: 'production',
    
      entry: {
        // [name]: 包名
        vue: 'Vue',
      },
    
      output: {
        filename: '[name].js',
        path: resolve(__dirname, 'dll'),
        // 打包后向外暴露的名称
        library: '[name]_[hash]',
      },
    
      plugins: [
        // 打包成 manifest.json 文件,提供映射信息
        new webpack.DllPlugin({
          // 映射库暴露的名称
          name: '[name]_[hash]',
          // 输出文件路径
          path: resolve(__dirname, 'dll/manifest.json'),
        }),
      ],
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27

# 17. 批量导出文件、组件等

  • CJS 与 ESM 不能混用,webpack 打包会报引用错误。
const files = require.context('.', false, /.js$/)
const hooks = {}
files.keys().forEach(key => {
  if (key === './index.js') return
  Object.assign(hooks, {
    [key.replace(/^\.\/(.*?)\.js$/, '$1')]: files(key).default,
  })
})

// CJS导出(webpack5 以下,无 tree-sharking 哟!)
module.exports = hooks
// 使用
const { useMatrix } = require('./hooks')

// ESM导出
export default hooks
// 使用
import hooks from './hooks'
const { useMatrix } = hooks
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
以父之名
周杰伦.mp3