入门优雅快速配置webpack你需要知道的

127 阅读8分钟

webpack快速配置篇

在讲这篇文章前,如果你以前并没自己配置过webpack或者对webpack并不是太了解的,可以先去看看我的另一篇文章:Webpack与Babel基本配置

webpack基本配置

配置拆分和merge

在配置前我们可以规范一下文件,先建好一个build文件夹,下面建新建文件,另外需要提醒本篇文章基于webpack5构建:

  • paths.js:放我们经常会用到的一些文件路径
  • webpack.common.js:webpack开发环境和生产环境的公共配置
  • webpack.dev.js:webpack用于开发环境的配置
  • webpack.prod.js:webpack用于生产环境的配置

当然你也可以按照自己的想法来进行建立文件,建好文件后,我们需要到package.json添加脚本命令方便后面运行:

"scripts": {
  "test": "echo \"Error: no test specified\" && exit 1",
  "dev-without-dev-server": "webpack --config build/webpack.dev.js",
  "dev": "webpack serve --config build/webpack.dev.js",
  "build": "webpack --config build/webpack.prod.js"
},

完成这些准备工作后我们开始正式的配置。

我这里的paths.js定义了一个入口文件夹路径,一个打包文件夹:

// 常用文件夹
const path = require('path')

const srcPath = path.join(__dirname, '..', 'src')
const distPath = path.join(__dirname, '..', 'dist')

module.exports = {
  srcPath,
  distPath
}

首先在公共配置webpack.common.js中定义一下入口和一个HtmlWebpackPlugin插件:

// 公共配置
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { srcPath } = require('./paths')

module.exports = {
  // 单文件打包入口
  entry: path.join(srcPath, 'index'),
  // 插件
  plugins: [
    // 单文件
    new HtmlWebpackPlugin({
      template: path.join(srcPath, 'index.html'),
      filename: 'index.html'
    })
  ]
}

因为我们把配置文件拆分成了公共配置、开发配置和生产配置,为的就是减少代码冗余,那么我们后面写详细配置就需要用到webpack-mergemerge方法。

在**webpack.dev.js**中,我们需要定义好devServerENV

// 开发环境配置
const path = require('path')
const webpack = require('webpack')
const webpackCommonConf = require('./webpack.common.js')
const { merge } = require('webpack-merge')
const { distPath } = require('./paths')

// 合并webpackCommonConf
module.exports = merge(webpackCommonConf, {
  mode: 'development',
  devServer: {
    port: 8080,
    static: distPath, // 根目录
    open: true, // 自动打开浏览器
    hot: true,
    compress: true, // 启动gzip压缩
    // 设置代理
    // proxy: {
    //   // 将本地 /api/xxx 代理到 localhost:3000/api/xxx
    //   '/api': 'http://localhost:3000',

    //   // 将本地 /api2/xxx 代理到 localhost:3000/xxx
    //   '/api2': {
    //       target: 'http://localhost:3000',
    //       pathRewrite: {
    //           '/api2': ''
    //       }
    //   }
    // }
  },
  plugins: [
    new webpack.DefinePlugin({
      // window.ENV = 'development'
      ENV: JSON.stringify('development')
    })
  ]
})

在**webpack.prod.js**中,我们则需要加上打包出口:

const path = require('path')
const webpack = require('webpack')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const webpackCommonConf = require('./webpack.common.js')
const { merge } = require('webpack-merge')
const { srcPath, distPath } = require('./paths')

module.exports = merge(webpackCommonConf, {
  mode: 'production',
  output: {
    // 打包带上hash戳
    filename: 'bundle.[contenthash:8].js',
    path: distPath
  },
  plugins: [
    new CleanWebpackPlugin(), // 会默认清空 output文件夹
    new webpack.DefinePlugin({
      ENV: JSON.stringify('production')
    })
  ]
})

这样一个初步架构就已经搭好了。

处理ES6+文件

很多时候因为浏览器不支持高语法,我们需要把js文件都转到es5语法,这时我们就需要用到babel-loader,可以在wbpack.common.js中加上(后面配置可自行根据项目需求加在公共配置、开发配置和生产配置):

module: {
  rules: [
    // es6以上编译到es5
    {
      test: /\.js$/,
      use: ['babel-loader'],
      include: srcPath
    }
  ]
},

这里我们就需要安装上babel-loader相关依赖了,并且需要在项目目录新建.babelrc文件

{
  "presets": ["@babel/preset-env"],
  "plugins": []
}

注配置中相关依赖均需自行安装好,不清除的可以到文末拿到package.json安装

处理样式文件

module: {
  rules: [
    {
      test: /\.css$/,
      // loader执行顺序是: 从后往前
      // postcss-loader兼容浏览器
      // css-loader编译css 
      // style-loader添加到html头部style标签里面(不抽离)
      use: ['style-loader', 'css-loader', 'postcss-loader'],
      include: srcPath
    },
    {
      test: /\.less$/,
      // 增加'less-loader'
      use: ['style-loader', 'css-loader', 'less-loader']
    }
  ]
},

注意上面我们在编译样式时是先进行了浏览器兼容处理,我们需要额外在项目目录新建postcss.config.js文件。

module.exports = {
  plugins: [require('autoprefixer')]
}

处理图片

这里我们可以分开发环境生产环境配置,因为开发环境可以直接使用图片url更快,生产环境可以让小文件就不用加载多次,生成base64编码反而更适合,当然这里只是推荐,自行选择:

webpack.dev.js:

// 开发环境直接引入图片url
{
  test: /\.(png|jpg|jpeg|gif)$/,
  use: 'file-loader'
},

webpack.prod.js:

// img考虑base64编码
{
  test: /\.(png|jpg|jpeg|gif)$/,
  use: {
    loader: 'url-loader',
    options: {
      // 当大小小于5kb 用base64产出, 否则直接file-loader产出
      limit: 5 * 1024,
      // 打包输出目录
      outputPath: '/img1/'
    }
  }
},

webpack进阶配置

上面我们讲的都是基于单文件的打包,css、js文件也未进行抽离,接下来我们重点说到这些配置方法。

多入口配置

这时我们需要在webpack.common.js进行多入口配置:

// 公共配置
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { srcPath } = require('./paths')

module.exports = {
  // 多文件打包入口
  entry: {
    index: path.join(srcPath, 'index.js'),
    other: path.join(srcPath, 'other.js')
  },
  module: {
    rules: [
      // es6以上编译到es5
      {
        test: /\.js$/,
        use: ['babel-loader'],
        include: srcPath
      }
    ]
  },
  plugins: [
    // 多入口时 index.html
    new HtmlWebpackPlugin({
      template: path.join(srcPath, 'index.html'),
      filename: 'index.html',
      // chunks表示该页面要引用哪些 chunks 上面entry包含的js
      chunks: ['index']
    }),
    // other.html
    new HtmlWebpackPlugin({
      template: path.join(srcPath, 'other.html'),
      filename: 'other.html',
      chunks: ['other']
    })
  ]
}

抽离压缩CSS文件

我们可以思考一下,前面的css打包后并未抽离出css文件,而是用style-loader会放到html头部的style标签里,当我们项目复杂起来,这显然是不靠谱的。

这里我们就需要用到mini-css-extract-plugin插件实现了。具体操作如下:

// ...
const MixCssExtractPlugin = require('mini-css-extract-plugin')
// ...

module: {
  rules: [
    // 抽离css
    {
      test: /\.css$/,
      use: [
        MixCssExtractPlugin.loader,
        'css-loader',
        'postcss-loader'
      ]
    },
    // 抽离less
    {
      test: /\.less$/,
      use: [
        MixCssExtractPlugin.loader,
        'css-loader',
        'less-loader',
        'postcss-loader'
      ]
    }
  ]
},
plugins: [
  // 抽离css文件
  new MixCssExtractPlugin({
    filename: 'css/[name].[contenthash:8].css'
  })
]

在上面的基础上实现css文件的压缩,我们用到css-minimizer-webpack-plugin

// js压缩
const TerserJSPlugin = require('terser-webpack-plugin')
// css压缩
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
// ...
// 加上optimization配置
optimization: {
  // 压缩js和css
  minimizer: [new TerserJSPlugin({}), new CssMinimizerPlugin({})],
}

抽离公共代码和第三方库

一个工具文件,我们可能在多个地方重复引入使用,打包后却是把功能的工具文件重复编译到了用到它的地方,这就导致了代码冗余,当代码量大时,无疑增加了打包体积,我们需要考虑抽离出去,第三方库也不应该和我们自己写的逻辑代码打包到一起,需要打包到一个单独文件

optimization: {
  // 分割代码块
  splitChunks: {
    /**
      * initial 入口chunk, 对于异步导入的文件不处理
      * async 异步chunk, 只对异步导入文件处理
      * all 全部chunk
      */
    chunks: 'all',
    // 缓存分组
    cacheGroups: {
      // 第三方模块
      vendor: {
        name: 'vender', // chunk名称
        priority: 1, // 权限更高, 优先抽离
        test: /node_modules/,
        minSize: 0, // 大小限制 超过才抽离
        minChunks: 1 // 最少复用过次数, 超过才打包
      },
      // 公共模块
      common: {
        name: 'common',
        priority: 0,
        minSize: 0,
        minChunks: 2
      }
    }
  }
}

懒加载

也会单独创建一个chunk,打包出一个文件。

// 引入动态数据 - 懒加载
setTimeout(() => {
  import('./data.js').then(res => {
    console.log(res.default.message)
  })
}, 3000)

modulechunkbundle的区别

可能看前面的代码注释会看到很多关于moudlechunkbundle的说法,这里来解释一下区别:

  • moudle可以理解为各个源码文件,模块化,webpack中一切皆模块
  • chunk可以理解为有一个或moudle模块组成,最后打包出的一个js文件就是由一个·chunk来的,如entrysplitChunkimport()懒加载都会产生chunk
  • bundle就是最终输出的文件

webpack性能优化(重点)

优化打包构建速度--开发体验和效率

优化babel-loader

  • 使用cacheDirectory,开启缓存,代码未改变下次就可以不用打包
  • 灵活使用include|exclude
rules: [
  // es6以上编译到es5
  {
    test: /\.js$/,
    use: ['babel-loader?cacheDirectory'],
    include: srcPath
  }
]

IgnorePlugin

  • 使用第三方模块不直接引入整个模块,按需引入
  • 避免了无用模块的引入,优化打包编译速率
  • 同时优化产出代码大小
// 以moment.js语言包为例子
plugins: [
  // 忽略 moment 下的 /locale 目录
  new webpack.IgnorePlugin(/\.\/locale/, /moment/),
]
import moment from 'moment'
// 在需要的文件下手动引入 单独模块
import 'moment/locale/zh-cn'

coment.locale('zh-cn')

noParse

避免重复打包

// 忽略已经打包好的文件
module: {
  noParse: [/react\.min\.js$/],
}

happyPack

  • js单线程,开启多进程打包
  • 提高构建速度
const HappyPack = require('happypack')

module: {
  rules: [
    // js
    {
      test: /\.js$/,
      // 把对 .js 文件的处理转交给 id 为 babel 的 HappyPack 实例
      use: ['happypack/loader?id=babel'],
      include: srcPath,
      // exclude: /node_modules/
    }
  ]
},

plugins: [
  // happyPack 开启多进程打包
  new HappyPack({
      // 用唯一的标识符 id 来代表当前的 HappyPack 是用来处理一类特定的文件
      id: 'babel',
      // 如何处理 .js 文件,用法和 Loader 配置中一样
      loaders: ['babel-loader?cacheDirectory']
  }),
]

ParallelUglifyPlugin

  • 多进程压缩
  • webpack内置Uglify工具压缩js
  • happyPack同理
  • 放生产环境
  • 项目较大,打包较慢,开启多进程能提高速度
  • 项目较小,打包很快,开启多进程反而会降低速度(进程也需要开销)
const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin')

plugins: [
  // 使用 ParallelUglifyPlugin 并行压缩输出的 JS 代码
  new ParallelUglifyPlugin({
    // 传递给 UglifyJS 的参数
    // (还是使用 UglifyJS 压缩,只不过帮助开启了多进程)
    uglifyJS: {
      output: {
        beautify: false, // 最紧凑的输出
        comments: false, // 删除所有的注释
      },
      compress: {
        // 删除所有的 `console` 语句,可以兼容ie浏览器
        drop_console: true,
        // 内嵌定义了但是只用到一次的变量
        collapse_vars: true,
        // 提取出出现多次但是没有定义成变量去引用的静态值
        reduce_vars: true,
      }
    }
  })
]

自动刷新

devServer默认有,一般不用配置

watch: true, // 开启监听,默认为 false
watchOptions: {
  ignored: /node_modules/, // 忽略哪些
  // 监听到变化发生后会等300ms再去执行动作,防止文件更新太快导致重新编译频率太高
  // 默认为 300ms
  aggregateTimeout: 300,
  // 判断文件是否发生变化是通过不停的去询问系统指定文件有没有变化实现的
  // 默认每隔1000毫秒询问一次
  poll: 1000
}

热更新

  • 自动刷新:整个网页全部刷新。速度较慢,状态会丢失
  • 热更新:新代码生效,网页不刷新,状态不丢失
  • 但是需要自行配置监听模块
if (module.hot) {
  // 监听模块
  module.hot.accept(['./math'], () => {
    // 不会丢失状态
    const sumRes = sum(10, 30)
    console.log('sumRes in hot', sumRes)
  })
}

DllPlugin

  • 一般框架体积大,构建慢,但是较稳定,不常升级版本
  • 同一版本只构建一次即可,不用每次都重新构建
  • webpack内置DllPlugin支持,打包出dll
  • 通过DllReferencePlugin 使用dll文件

优化产出代码 -- 产品性能

  • 小图片通过base64编码
  • bundlehash
  • 懒加载
  • 提取公共代码
  • IngorePlugin按需引入需要的代码模块
  • 使用CDN加速
  • Scope Hosting

这里单独说说Scope Hosting,它可以处理:

  1. 优化代码,代码体积更小
  2. 创建函数作用域更少
  3. 代码可读性更好
// 开启方式
const ModuleConcatenationPlugin = require('webpack/lib/optimize/ModuleConcatenationPlugin')

resolve: {
  // 针对npm中第三方模块优先采用 jsnext:main中指向的es6模块化语法文件
  mianFields: ['jsnext:main', 'brower', 'main']
},
plugins: [
  new ModuleConcatenationPlugin()
]

使用production模式,也会有很多自带优化:

  • 会自动压缩js代码
  • vue、react框架等会自动删除调试代码(如开发环境的warning)
  • 会启动Tree-Shaking(用到的模块才打包,没用的模块不打包,但是只有ES6 module才能让其生效commonjs模块就不行)

ES6 ModuleCommonjs区别

  • ES6 Module静态引入,编译时引入
  • Commonjs动态引入,执行时引入
  • ES6 Module可以静态分析,实现Tree-Shaking

ES6 Module导出和引入

export const sum = (a, b) => {
  return a + b
}

export const muti = (a, b) => {
  return a * b
}

// 对象导出
// export {
//   sum,
//   muti
// }
import { sum } from './util'

const sumRes = sum(10, 20)

Commonjs导出和引入

const sum = (a, b) => {
  return a + b
}

const muti = (a, b) => {
  return a * b
}

module.exports.sum = sum
module.exports.muti = muti
const util = require('./util')

const sumRes = util.sum(10, 20)

Babel

环境搭建和基本配置

presets: 作为babel插件的组合

plugins: 当presets不能满足需求,需要额外插件时使用

babel-polyfill

Polyfill补丁 babel单独不能解析的语法

core-js和regenerator兼容语法es6 7

babel-polyfill即是两者的集合,7.4后被弃用,推荐直接使用core-js和regenerator

只解析语法,不解析模块

babel-ployfill文件较大,只用一部分功能,按需引入

问题

  • 会污染全局环境

babel-runtime