Webpack部分常用配置速览|小册免费学

290 阅读2分钟

1. 不同环境下的配置

有以下两种方式:

  1. 配置文件根据环境不同导出不同配置

    const path = require('path')
    const { CleanWebpackPlugin } = require('clean-webpack-plugin')
    const HtmlWebpackPlugin = require('html-webpack-plugin')
    const CopyWebpackPlugin = require('copy-webpack-plugin')
    const webpack = require('webpack')
    
    const config = {
      mode: 'none',
      entry: './src/main.js',
      output: {
        filename: 'bundle.js',
        path: path.join(__dirname, 'dist')
      },
      devtool: 'cheap-eval-module-source-map',
      devServer: {
        contentBase: '/public/',
        proxy: {
          '/api': {
            target: 'https://api.github.com',
            pathRewrite: { '^/api': '' },
            changeOrigin: true
          }
        }
      },
      module: {
        rules: [
          {
            test: /.css$/,
            use: ['style-loader', 'css-loader']
          }
        ]
      },
      plugins: [
        // 用于生成 index.html
        new HtmlWebpackPlugin({
          title: 'Webpack Tutorials',
          meta: {
            viewport: 'width=device-width'
          },
          template: './template/index.html'
        }),
        new webpack.HotModuleReplacementPlugin()
      ]
    };
    // env 通过cli传递的环境名参数 argv 运行cli过程传递的所有参数
    module.exports = (env, argv) => {
      if (env === 'production') {
        config.mode = 'production';
        config.devtool = false;
        config.plugins = [
          ...config.plugins,
          new CleanWebpackPlugin(),
          new CopyWebpackPlugin(['public'])
        ]
      }
      return config;
    }
    
  2. 一个环境对应一个配置文件

    // webpack.common.js // 基本公共配置
    // webpack.prod.js // 生产环境配置
    // webpack.dev.js // 开发环境配置
    
    // 以 webpack.prod.js为例
    const common = require('./webpack.common');
    const merge = require('webpack-merge');
    const { CleanWebpackPlugin } = require('clean-webpack-plugin');
    const CopyWebpackPlugin = require('copy-webpack-plugin');
    // 使用merge合并
    // 如果使用Object.assign会完全覆盖同名属性的值,对于值类型来说没问题,对于plugins等引用类型会被覆盖掉起不到合并效果
    module.exports = merge(common, {
      mode: 'production',
      plugins: [
        new CleanWebpackPlugin(),
        new CopyWebpackPlugin(['public'])
      ]
    })
    

2. DefinePlugin

DefinePlugin 允许创建一个在编译时可以配置的全局常量。这可能会对开发模式和发布模式的构建允许不同的行为非常有用。如果在开发构建中,而不在发布构建中执行日志记录,则可以使用全局常量来决定是否记录日志。这就是 DefinePlugin 的用处,设置它,就可以忘记开发和发布构建的规则。

为代码注入全局成员,在开发构建中或者发布构建中此插件默认启用,并往代码中注入了一个process.env.NODE_ENV,很多第三方模块通过这个成员来判断当前的运行环境。

// webpack.config.js
const webpack = require('webpack');
module.exports = {
  mode: 'none',
  entry: './src/main.js',
  output: {
    filename: 'bundle.js'
  },
  plugins: [
    new webpack.DefinePlugin({ // 此处成员的值应是符合js语法的代码片段
      API_BASE_URL: JSON.stringify('https://api.example.com')
    })
  ]
}

3. Tree Shaking

官网描述:

tree shaking 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。它依赖于 ES2015 模块语法的 静态结构 特性,例如 importexport。这个术语和概念实际上是由 ES2015 模块打包工具 rollup 普及起来的。

webpack 2 正式版本内置支持 ES2015 模块(也叫做 harmony modules)和未使用模块检测能力。新的 webpack 4 正式版本扩展了此检测能力,通过 package.json"sideEffects" 属性作为标记,向 compiler 提供提示,表明项目中的哪些文件是 "pure(纯的 ES2015 模块)",由此可以安全地删除文件中未使用的部分。

ES6 Module 依赖关系的构建是在代码编译时而非运行时。基于这项特性 Webpack 提供了 tree shaking 的功能,它可以在打包过程中帮助我们检测工程中没有被引用过的模块,这部分代码将永远无法被执行到,因此也被称为“死代码”。Webpack 会对这部分代码进行标记,并在资源压缩时将它们从最终的 bundle 中去掉。

tree shaking 只能对 ES6 Module 生效。有时我们会发现虽然只引用了某个库中的一个接口,却把整个库加载进来了,而 bundle 的体积并没有因为 tree shaking 而减小。这可能是由于该库是使用 CommonJS 的形式导出的,为了获得更好的兼容性,目前大部分的 npm 包还在使用 CommonJS 的形式。也有一些 npm 包同时提供了 ES6 Module 和 CommonJS 两种形式导出,我们应该尽可能使用 ES6 Module 形式的模块,这样 tree shaking 的效率更高。

webpack 的 Tree Shaking 的作用是可以将未被使用的 exported member 标记为 unused 同时在将其 re-export 的模块中不再 export。

module.exports = {
  mode: 'none',
  entry: './src/index.js',
  output: {
    filename: 'bundle.js'
  },
  optimization: {
    usedExports: true, // 模块只导出被使用的成员(Webpack 将识别出它认为没有被使用的代码,并在最初的打包步骤中给它做标记)
    concatenateModules: true, // 尽可能将所有模块合并输出到一个函数中,提升运行效率,减少代码体积,v3中添加的特性
    minimize: true // 告知 webpack 使用 TerserPlugin 压缩 bundle,识别未使用的导出,消除无用代码
  }
}

关于Tree Shaking还有个问题:如果使用了babel-loader会导致其失效。因为Tree Shaking的前提是使用 ES Modules组织代码,也就是交给webpack打包的代码必须是使用ESM实现的模块化,而webpack在打包时会根据配置将不同的文件交给不同的loader去处理,最后将所有loader处理后的结果打包到一起,为了转换代码中ECMAScript的新特性多数时候会选择babel-loader处理js,而babel在转换代码时会有可能将ES Modules转换为CommonJS,那Tree Shaking做优化时处理的代码就不是使用ESM实现的模块化了。

我们用下面的代码和配置进行验证:

// util.js
export const Button = () => {
  return document.createElement('button')
  console.log('dead-code') // 测试无效的代码能否被去除
}
export const Link = () => {
  return document.createElement('a')
}
export const Heading = level => {
  return document.createElement('h' + level)
}

// index.js
import { Button } from './util'
document.body.appendChild(Button())

在来看下面一段配置:

module.exports = {
  mode: 'none',
  entry: './src/index.js',
  output: {
    filename: 'bundle.js'
  },
  module: {
    rules: [
      {
        test: /.js$/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env']
          }
        }
      }
    ]
  },
  optimization: {
    // 模块只导出被使用的成员
    usedExports: true
  }
};

在终端执行webpack打包:

tree_shaking.png

从上图可知usedExports已经生效了,如果开启压缩代码配置,未被使用的导出代码依然可以被移除,所以Tree Shaking没有失效。

那这又是为什么呢?

因为,在新版本的babel-loader中已经自动关闭了ES Modules的转换插件。

首先,来看babel-loader相关源码:

babel-loader.png

Webpack v2版本开始就已经支持了ESM和动态导入。

其次,来看@babel/preset-env相关源码:

preset-env.png

可以看出shouldTransformESM: false,禁用了 ESM 的转换,所以 Webpack打包后得到的还是 ESM 的代码,那 Tree Shaking 自然就可以正常工作了。

接下来,我们强制开启这个插件来测试一下效果:

module: {
  rules: [
    {
      test: /.js$/,
      use: {
        loader: 'babel-loader',
        options: {
          presets: [
            ['@babel/preset-env', { modules: 'commonjs' }] // 此处进行修改
          ]
        }
      }
    }
  ]
}

然后,打包查看bundle.js:

开启esm转换插件.png

此时,配置的usedExports: true就不生效了,那即便开启压缩代码Tree Shaking也不会生效了。

由此可知:最新版本的babel-loader不会导致Tree Shaking失效,如果你不确定是否为版本问题,最简单的办法是把 module 设置为false(modules: false),这样可以确保 preset-env不会开启 ESM转换插件,这样就保证了Tree Shaking工作的前提。

module: {
  rules: [
    {
      test: /.js$/,
      use: {
        loader: 'babel-loader',
        options: {
          presets: [
            ['@babel/preset-env', { modules: false }] // 关闭 ESM 转换插件
          ]
        }
      }
    }
  ]
}

关闭esm转换插件.png

4. sideEffects

允许我们通过配置的方式标示代码是否有副作用,为Tree Shaking提供更大的压缩空间

副作用:在导入时会执行特殊行为的代码,而不是仅仅暴露一个 export 或多个 export。举例说明,例如 polyfill,它影响全局作用域,并且通常不提供 export。(一般用于 npm 包标记是否有副作用)

有如下示例代码:

// src/components/button.js
export default () => {
  return document.createElement('button')
  console.log('dead-code')
}

// src/components/head.js
export default level => {
  return document.createElement('h' + level)
}

// src/components/link.js
export default () => {
  return document.createElement('a')
}

// src/components/index.js
export { default as Button } from './button'
export { default as Link } from './link'
export { default as Heading } from './head'

// src/index.js
import { Button } from './components'
document.body.appendChild(Button())
// webpack.config.js
module.exports = {
  mode: 'none',
  entry: './src/index.js',
  output: {
    filename: 'bundle.js'
  },
  module: {},
  optimization: {}
}

执行 webpack 打包,然后查看dist/bundle.js会发现所有组件模块都被打包进了 bundle.js。

接下来,开启 sideEffects

// webpack.config.js
module.exports = {
  mode: 'none',
  entry: './src/index.js',
  output: {
    filename: 'bundle.js'
  },
  module: {},
  optimization: {
    sideEffects: true // 开启 webpack打包时会检查当前代码所在模块的package.json中是否有 sideEffects 标示
  }
}

package.json中增加sideEffects属性:

// package.json
{
  // ...
  "sideEffects": false // 标示 package.json 所影响的项目当中所有的代码都没有副作用
}

此时,在运行webpack打包,查看dist/bundle.js会发现没有使用到的模块就不会被打包进来了。

下面对示例做进一步修改:

增加src/extend.js src/global.css

// src/extend.js
// 为 Number 的原型添加一个扩展方法
Number.prototype.pad = function (size) {
  // 将数字转为字符串 => '8'
  let result = this + ''
  // 在数字前补指定个数的 0 => '008'
  while (result.length < size) {
    result = '0' + result
  }
  return result
}

// src/global.css
body {
  background-color: #fff;
}

修改src/index.js

// src/index.js
import { Button } from './components'

// 样式文件属于副作用模块
import './global.css'

// 副作用模块
import './extend'

console.log((3).pad(4))

document.body.appendChild(Button())

此时,依然标记所有代码均无副作用:

// package.json
{
  // ...
  "sideEffects": false // 标示 package.json 所影响的项目当中所有的代码都没有副作用
}

执行 webpack 打包,然后查看dist/bundle.js会发现引入的extend.js global.css没有被打包进 bundle.js。

如果想把扩展代码和全局样式打包进bundle.js,解决办法在sideEffects中进行配置:

// package.json
{
  // ...其他key: value
  "sideEffects": [
    "./src/extend.js",
    "*.css"
  ]
}

在此执行 webpack 打包,然后查看dist/bundle.js会发现有副作用的extend.js global.css两个模块就被打包进了 bundle.js。

5. 代码分割

5.1 多入口打包

const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  mode: 'none',
  entry: {
    index: './src/index.js',
    album: './src/album.js'
  },
  output: {
    filename: '[name].bundle.js'
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          'style-loader',
          'css-loader'
        ]
      }
    ]
  },
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      title: 'Multi Entry',
      template: './src/index.html',
      filename: 'index.html',
      chunks: ['index']
    }),
    new HtmlWebpackPlugin({
      title: 'Multi Entry',
      template: './src/album.html',
      filename: 'album.html',
      chunks: ['album']
    })
  ]
}

不同入口中很可能会有公共模块,按照以上多入口配置不同的打包结果中就可能会有相同的模块出现,所以要提取公共模块:

module.exports = {
  // 其他配置项
  optimization: {
    splitChunks: {
      // 自动提取所有公共模块到单独 bundle
      chunks: 'all'
    }
  }
}

5.2 动态导入

所有动态导入的模块会被自动提取到单独的bundle中,从而实现分包,相比于多入口方式,动态导入更加的灵活。

// 添加 webpackChunkName 注释后,动态导入的模块打包后的名称为注释中提供的名称
import(/* webpackChunkName: 'chunkName' */'模块路径').then(({ default: module }) => {
  // 执行动态导入后的逻辑
})

6. MiniCssExtractPlugin

将css从打包结果提取出来的插件(提取css到单独的文件,实现css模块的按需加载)

const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')

module.exports = {
  mode: 'none',
  entry: {
    main: './src/index.js'
  },
  output: {
    filename: '[name].bundle.js'
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          // 'style-loader', // 将样式通过 style 标签注入html
          MiniCssExtractPlugin.loader, // 使用此loader代替 style-loader
          'css-loader'
        ]
      }
    ]
  },
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      title: 'Dynamic import',
      template: './src/index.html',
      filename: 'index.html'
    }),
    new MiniCssExtractPlugin()
  ]
}

7. OptimizeCssAssetsWebpackPlugin

压缩输出的CSS文件

const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
const TerserWebpackPlugin = require('terser-webpack-plugin')

module.exports = {
  mode: 'none',
  entry: {
    main: './src/index.js'
  },
  output: {
    filename: '[name].bundle.js'
  },
  optimization: {
    minimizer: [ // 配置数组时 webpack 会认为我们要自定义使用的压缩器插件
      new TerserWebpackPlugin(), // 如果不增加此处,会导致打包出来的 js 文件不会被压缩
      new OptimizeCssAssetsWebpackPlugin()
    ]
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          // 'style-loader', // 将样式通过 style 标签注入
          MiniCssExtractPlugin.loader,
          'css-loader'
        ]
      }
    ]
  },
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      title: 'Dynamic import',
      template: './src/index.html',
      filename: 'index.html'
    }),
    new MiniCssExtractPlugin()
  ]
}

8. 输出文件名Hash

为了解决服务器静态资源缓存设置时间长短带来的问题,建议在生产模式下输出的文件名使用 Hash

webpack 的output.filename和多数插件的filename属性都支持占位符的方式来为文件名设置hash

一般支持三种:

  • filename: '[name].[hash].bundle.js'整个项目级别的hash,项目中有任何一处改动本次打包的hash都会改变

  • filename: '[name].[chunkhash].bundle.js'chunk级别的hash,打包过程中同一路的打包chunkhash都是相同的

  • filename: '[name].[contenthash].bundle.js'文件级别的hash,根据输出文件的内容生成的hash,也就是说不同的文件就会有不同的hash

    相比于前两者 contenthash 算是解决缓存问题最好的方式,因为精确定位到了文件级别。

    另外如果觉得20位的hash太长,还可以通过占位符的方式做修改[name].[contenthash:8].bundle.js

本文正在参与「掘金小册免费学啦!」活动, 点击查看活动详情

参考:Webpack中文网