Webpack4 高级概念 常用配置

本文记录webpack的一些常用高级概念,需要对webapck基础有所了解,本文重点记录概念涉及到的配置,其他配置有所省略,更多内容参照官方文档。

webpack中文网

webpack英文网

Tree Shaking

作用是当引入一个模块时,不会引入模块全部代码,只引入需要的部分代码,Tree Shaking只支持ES Module,不支持CommonJS模块方式。

例如只引入match.js的add方法

// match.js
export function add(a, b) {
  return a + b
}
export function minus(a, b) {
  return a - b
}

// index.js
import { add } from \'./math\'

webpack配置

// webpack.config.js 生产环境
module.exports = {
  mode: \'production\',
  devtool: \'cheap-module-source-map\',
}

// webpack.config.js 开发环境
module.exports = {
  mode: \'development\',
  devtool: \'cheap-module-eval-source-map\',
  optimization: {
    usedExports: true
  }
}

// package.json 如果某些文件不需要做Tree Shaking,需要增加如下配置
"sideEffects": false, // 没有要处理的文件
"sideEffects": ["*.css"], // 任何css文件都不做Tree Shaking

Develoment 和 Production 模式的区分打包

webpack.common.js

const path = require(\'path\')
const HtmlWebpackPlugin = require(\'html-webpack-plugin\')
const CleanWebpackPlugin = require(\'clean-webpack-plugin\')

module.exports = {
  entry: {
    main: \'./src/index.js\'
  },
  module: {
    // 相关loader配置(此处省略)
    rules: [ ... ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: \'src/index.html\'
    }), 
    new CleanWebpackPlugin([\'dist\'], {
      root: path.resolve(__dirname, \'../\')
    })
  ],
  output: {
    filename: \'[name].js\',
    path: path.resolve(__dirname, \'../dist\')
  }
}

webpack.dev.js

const webpack = require(\'webpack\')
const merge = require(\'webpack-merge\')
const commonConfig = require(\'./webpack.common.js\')

const devConfig = {
  mode: \'development\',
  devtool: \'cheap-module-eval-source-map\',
  devServer: {
    contentBase: \'./dist\',
    open: true,
    port: 8080,
    hot: true
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin()
  ],
  optimization: {
    usedExports: true
  }
}

module.exports = merge(commonConfig, devConfig)

webpack.prod.js

const merge = require(\'webpack-merge\')
const commonConfig = require(\'./webpack.common.js\')

const prodConfig = {
  mode: \'production\',
  devtool: \'cheap-module-source-map\'
}

module.exports = merge(commonConfig, prodConfig)

Webpack 和 Code Splitting

  • 代码分割,和webpack无关
  • webpack中实现代码分割,两种方式
    • 同步代码: 只需要在webpack.common.js中做optimization的配置即可
    • 异步代码(import): 异步代码,无需做任何配置,会自动进行代码分割,放置到新的文件中

同步加载

// index.js
import _ from \'lodash\'
console.log(_.join([\'a\', \'b\'], \'-\'))

// webpack.common.js
module.exports = {
  optimization: {
    splitChunks: {
      chunks: \'all\'
    }
  }
}

异步加载

注意需要借助babel插件来支持该语法

npm install --save-dev @babel/plugin-syntax-dynamic-import
// .babelrc
{
  plugins: ["@babel/plugin-syntax-dynamic-import"]
}
// index.js
function getComponent() {
  return import(/* webpackChunkName:"lodash" */ \'lodash\').then(({ default: _ }) => {
    var element = document.createElement(\'div\')
    element.innerHTML = _.join([\'a\', \'b\'], \'-\')
    return element
  })
}

getComponent().then(element => {
  document.body.appendChild(element)
})

/* webpackChunkName:"lodash" */webpack魔法注释的作用是对异步加载的lodash打包生成的文件进行重命名,即为vendors~lodash.js,不设置的时候是0.js

SplitChunksPlugin详细配置参数

详细配置项详见webpack官方文档

module.exports = {
  //...
  optimization: {
    splitChunks: {
      chunks: \'async\', // 对异步模块进行优化,结合cacheGroups生效,有效值是all、async(异步)和initial(同步)
      minSize: 30000, // 引入的模块大于30kb才做代码分割
      maxSize: 50000, // 如果分割的lodash大于50kb,则尝试进行二次分割(了解即可)
      minChunks: 2, // 当一个模块至少用了2次的时候才进行代码分割
      maxAsyncRequests: 5, // 同时只能加载五个请求,超过五个就不会做代码分割
      maxInitialRequests: 3, // 入口文件引入的库如果做代码分割最多只能分割出3个文件
      automaticNameDelimiter: \'~\', // 默认打包生成文件名的连接符
      name: true, // cacheGroups中改变的名字生效
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/, // 是node_modules中引入的模块
          priority: -10, // 优先级值越大优先级越高
          filename: \'vendors.js\' // 打包生成的文件名
        },
        default: {
          priority: -20, // 优先级值越大优先级越高
          reuseExistingChunk: true, // 如果模块已经被打包过,则忽略此模块,直接复用
          filename: \'commin.js\' // 打包生成的文件名
        }
      }
    }
  }
}

Lazy Loading 懒加载

懒加载其实就是上文 Code Splitting 中提到的异步加载模块,指的是需要的时候再去加载该模块

// index.js
async function getComponent() {
  const { default: _ } = await import(/* webpackChunkName:"lodash" */ \'lodash\')
  const element = document.createElement(\'div\')
  element.innerHTML = _.join([\'a\', \'b\'], \'-\')
  return element
}

document.addEventListener(\'click\', () =>{
  getComponent().then(element => {
    document.body.appendChild(element)
  })
})

打包分析

暂不做详细介绍(后续补充),详细查看官方文档。

webpack-analyse

官方文档

build命令需要增加 --profile 参数

// package.js
{
  "scripts": {
    "dev-build": "webpack --profile --json > stats.json --config ./build/webpack.dev.js"
  }
}

Preloading,Prefetching

  • preload chunk 会在父 chunk 加载时,以并行方式开始加载。prefetch chunk 会在父 chunk 加载结束后开始加载。
  • preload chunk 具有中等优先级,并立即下载。prefetch chunk 在浏览器闲置时下载。

官方文档

// click.js
function handleClick() {
  const element = document.createElement(\'div\')
  element.innerHTML = \'hello world\'
  document.body.appendChild(element)
}

export default handleClick
// index.js
document.addEventListener(\'click\', () =>{
  import(/* webpackPrefetch: true */ \'./click.js\').then(({default: func}) => {
    func();
  })
})

/* webpackPrefetch: true */等有空闲带宽时加载此模块

CSS 文件的代码分割 MiniCssExtractPlugin

官方文档

# 代码分割插件
npm install --save-dev mini-css-extract-plugin
# 代码压缩合并插件
npm install --save-dev optimize-css-assets-webpack-plugin

MiniCssExtractPlugin插件目前还不支持HMR support热更新,一般在线上环境做打包时使用

webpack.common.js

const path = require(\'path\');
const HtmlWebpackPlugin = require(\'html-webpack-plugin\');
const CleanWebpackPlugin = require(\'clean-webpack-plugin\');

module.exports = {
  entry: {
    main: \'./src/index.js\',
  },
  module: {
    rules: [{ 
      test: /\.js$/, 
      exclude: /node_modules/, 
      loader: \'babel-loader\',
    }, {
      test: /\.(jpg|png|gif)$/,
      use: {
        loader: \'url-loader\',
        options: {
          name: \'[name]_[hash].[ext]\',
          outputPath: \'images/\',
          limit: 10240
        }
      } 
    }, {
      test: /\.(eot|ttf|svg)$/,
      use: {
        loader: \'file-loader\'
      } 
    }]
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: \'src/index.html\'
    }), 
    new CleanWebpackPlugin([\'dist\'], {
      root: path.resolve(__dirname, \'../\')
    })
  ],
  optimization: {
    usedExports: true, // 注意这里要对css文件不做Tree Shaking,具体在package.json中配置
    splitChunks: {
      chunks: \'all\'
    }
  },
  output: {
    filename: \'[name].js\', // entry直接引入的(入口)文件走这里
    chunkFilename: \'[name].chunk.js\', // 间接引入的文件走这里
    path: path.resolve(__dirname, \'../dist\')
  }
}

webpack.dev.js

const webpack = require(\'webpack\');
const merge = require(\'webpack-merge\');
const commonConfig = require(\'./webpack.common.js\');

const devConfig = {
  mode: \'development\',
  devtool: \'cheap-module-eval-source-map\',
  devServer: {
    contentBase: \'./dist\',
    open: true,
    port: 8080,
    hot: true
  },
  module: {
    rules: [{
      test: /\.scss$/,
      use: [
        \'style-loader\', 
        {
          loader: \'css-loader\',
          options: {
            importLoaders: 2
          }
        },
        \'sass-loader\',
        \'postcss-loader\'
      ]
    }, {
      test: /\.css$/,
      use: [
        \'style-loader\',
        \'css-loader\',
        \'postcss-loader\'
      ]
    }]
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin()
  ],
}

module.exports = merge(commonConfig, devConfig);

webpack.prod.js

const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
const merge = require(\'webpack-merge\');
const commonConfig = require(\'./webpack.common.js\');

const prodConfig = {
  mode: \'production\',
  devtool: \'cheap-module-source-map\',
  module: {
    rules:[{
      test: /\.scss$/,
      use: [
        MiniCssExtractPlugin.loader, 
        {
          loader: \'css-loader\',
          options: {
            importLoaders: 2
          }
        },
        \'sass-loader\',
        \'postcss-loader\'
      ]
    }, {
      test: /\.css$/,
      use: [
        MiniCssExtractPlugin.loader,
        \'css-loader\',
        \'postcss-loader\'
      ]
    }]
  },
  optimization: {
    // 代码压缩合并插件的配置
    minimizer: [new OptimizeCSSAssetsPlugin({})]
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: \'[name].css\', // 直接引入的文件走这里
      chunkFilename: \'[name].chunk.css\' // 间接引入的文件走这里
    })
  ]
}

module.exports = merge(commonConfig, prodConfig);

package.json

// css文件不做Tree Shaking
{
  "sideEffects": [
    "*.css"
  ]
}

Webpack 与浏览器缓存

// webpack.common.js
module.exports = {
  optimization: {
    // 老版本webpack4需要runtimeChunk配置(处理没改变业务代码也会改变打包后的contenthash情况)
    runtimeChunk: {
      name: \'runtime\'
    },
    usedExports: true,
    splitChunks: {
      chunks: \'all\',
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10,
          name: \'vendors\',
        }
      }
    }
  },
  performance: false, // 性能问题不做警告
  output: {
    path: path.resolve(__dirname, \'../dist\')
  }
}

// webpack.prod.js
module.exports = {
  output: {
    // 文件名添加contenthash值
    filename: \'[name].[contenthash].js\',
    chunkFilename: \'[name].[contenthash].js\'
  }
}

Shimming 的作用

index.js

import $ from \'jquery\'
import _ from \'lodash\'
import { ui } from \'./jquery.ui\'

ui()

const dom = $(\'<div>\')
dom.html(_.join([\'a\', \'b\'], \'-\'))
$(\'body\').append(dom)

jquery.ui.js

// 在另一个模块中直接使用jquery和lodash方法
export function ui() {
  $(\'body\').css(\'background\', _join([\'red\'], \'\'))
}

webpack.common.js

const webpack = require(\'webpack\')

module.exports = {
  plugins: [
    new webpack.ProvidePlugin({
      $: \'jquery\', // 在一个模块引入可以在其他模块直接用$
      _join: [\'lodash\', \'join\'] // _.join的另一种方式,可以直接用_join
    })
  ]
}

环境变量的使用方法

在上文 Develoment 和 Production 模式的区分打包 中的区分打包可以改为使用环境变量的方式。即webpack.common.js中通过变量判断是合并开发环境还是生产环境的配置,webpack.dev.jswebpack.prod.js只写各自配置,不需要单独合并引入。

webpack.common.js

const merge = require(\'webpack-merge\')
const devConfig = require(\'./webpack.dev.js\')
const prodConfig = require(\'./webpack.prod.js\')
const commonConfig = {
  ...
}

module.exports = (env) => {
  if(env && env.production) {
    return merge(commonConfig, prodConfig);
  } else {
    return merge(commonConfig, devConfig);
  }
}

webpack.dev.js或webpack.prod.js直接导出,不用合并webpack.common.js

const devConfig = {
  ...
}

module.exports = devConfig;

package.json

注意npm run build中需要传入--env.production变量,其他都是直接引入webpack.common.js文件。

{
  "scripts": {
    "dev-build": "webpack --config ./build/webpack.common.js",
    "dev": "webpack-dev-server --config ./build/webpack.common.js",
    "build": "webpack --env.production --config ./build/webpack.common.js"
  }
}