hello webpack

不论是 react 还是 vue 脚手架,都依赖 webpack 进行构建和打包。这一篇主要是我之前在入门 webpack 4 的时候记的一点小笔记,详细内容还得参见官方文档

简单打个包

初始化项目,会生成一个 package.json

npm init
// 快速 init
// 1、默认都 yes
npm init -y
// 2、设置 init 模板
npm config set init.author.name "JacksonZhou"
npm init -y // 或者 -f

安装 webpack 和 webpack-cli

npm install webpack -D
npm install webpack-cli -D

我们随意创建一个 src 目录下的 index.js 文件,然后在 package.json 修改 scripts

"build": "webapck --mode production"
npm run build

这时会生成一个 dist 文件夹,这个里面就是 webpack 打包好的文件

创建配置文件

创建一个 webpack.config.js,执行 webpack 命令时会去执行这个配置文件。若是自定义配置文件的名字,则应该使用命令 webpack config 文件名

entry

入口,webpack 会从这里开始解析依赖。默认会按 entry 来划分 chunk

entry: './src/index.js'

output

webpack 最终构建出来的静态文件,可配置输出结果的文件名、路径等

const path = require('path') // 本质上webpack配置文件就是nodejs脚本,所以可以使用nodejs的模块
output: {
    publicPath: '', // 输出资源的网址前缀
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].js' // 原文件名
}

关于 filename 的 hash 命名中 hash、chunkhash、contenthash 的区别

  • hash是跟整个项目的构建相关,只要项目里有文件更改,整个项目构建的 hash 值都会更改,并且全部文件都共用相同的 hash 值
  • chunkhash根据不同的入口文件(Entry)进行依赖文件解析、构建对应的 chunk,生成对应的哈希值,但是 chunkhash 下,引用其他文件的文件一旦修改,也会改变其他文件的 hash 值,即使其他文件没动过。
  • contenthash保证文件内容必须要修改后,hash 值才会变化

loader

通过使用不同的 loader 来解析处理不同类型的文件。webpack 默认支持打包 js 文件,其他格式的文件需要借助对应的 loader 进行转换

换句话说,当 webpack 遇到其他文件的时候,会去配置文件的 module 选项中去找相应的规则。比如在配置文件中我们可以规定当 webpack 遇到图片文件的时候,就使用 file-loader 来帮我们进行打包文件

常见 loader

  1. babel-loader 将 es6/es7 转换成 es5(待确认)
npm i babel-loader @babel/core @babel/preset-env @babel/polyfill -D
npx babel src --out-dir lib // 测试 babel
module: {
    rules: [
      {
        // 命中 js 文件
        test: '/\.js$/',
        // 使用 babel-loader 来解析 js 文件
        loader: 'babel-loader',
        // 只命中 src 目录下的 js 文件,加快 webpack 的编译速度
        include: path.resolve(__dirname, 'src'),
        // 排除 node_modules,加快 webpack 的编译速度
        exclude: /node_modules/,
      }
    ]
},

这样还不能发挥 Babel 的作用。在项目根目录下创建一个 .babelrc 文件,添加代码:

{
  "presets": [
    "@babel/preset-env"
  ]
}
  1. 构建 CSS
npm i css-loader style-loader -D
module: {
    rules: [
      {
        test: /\.css/,
        include: [
          path.resolve(__dirname, 'public/style'),
        ],
        use: ['style-loader','css-loader']
        // loader 的执行顺序是从下到上,从右到左
      }
  ]
}
// 在index.js里引入
import '../public/style/index.css'

css-loader 负责解析 CSS 代码,主要是为了处理 CSS 中的依赖,帮我们分析出几个 css 文件之间的关系,并最终把这些 css 文件合并成一段 css,例如解析 @import 和 url() 等引用外部文件的声明;

style-loader 会将 css-loader 解析的结果转变成 JS 代码,运行时动态插入 style 标签来让 CSS 代码生效。

经由上述两个 loader 的处理后,CSS 代码会转变为 JS,和 index.js 一起打包了。

  1. 处理预处理语言
npm i less-loader -D
module: {
    rules: [
      {
        test: /\.less$/,
        use: ExtractTextPlugin.extract({
          fallback: 'style-loader',
          use: [
            'css-loader',
            'less-loader',
          ],
        }),
      },
    ],
},
  1. 处理图片

file-loader 可以用于处理很多类型的文件,它的主要作用是直接输出文件,把构建后的文件路径返回

url-loader会将引入的图片编码,生成 dataURl 并将其打包到文件中。但是如果图片较大,编码会消耗性能。因此 url-loader 提供了一个 limit 参数,小于 limit 字节的文件会被转为 DataURl,大于 limit 的还会使用 file-loader 进行 copy

image-webpack-loader 可用于压缩图片

{
    test: '/\.(png|gif|jpe?g)$/i',
    use: [
      {
        loader: 'url-loader',
        options: {
          limit: 8192 // 单位是 Byte,一般当文件小于 8KB 时作为 DataURL 处理
        }
      }
    ]
},
{
    test: '/.*\.(gif|png|jpe?g|svg|webp)$/i',
    use: [
      {
        loader: 'file-loader',
        options: {}
      },
      {
        loader: 'image-webpack-loader',
        options: {
          mozjpeg: { // 压缩 jpeg 的配置
            progressive: true,
            quality: 65
          },
          optipng: { // 使用 imagemin-optipng 压缩 png,enable: false 为关闭
            enabled: false,
          },
          pngquant: { // 使用 imagemin-pngquant 压缩 png
            quality: '65-90',
            speed: 4
          },
          gifsicle: { // 压缩 gif 的配置
            interlaced: false,
          },
          webp: { // 开启 webp,会把 jpg 和 png 图片压缩为 webp 格式
            quality: 75
          },
        }
      },
    ]
}

使用html-withimg-loader进行处理 img 标签引入的图片

npm i html-withimg-loader --save-dev
{
    test:/\.(html|htm)$/,
    use:'html-withimg-loader'
}
  1. 打包图标字体

其他常用 loader

  • px2rem-loader。移动端 css px 自动转换为 rem 的 loader
  • postcss-loader + autoprefixer。在 css 属性上加上相应的浏览器的厂商前缀,如-webkit、-ms、-moz 等
// postcss.config.js
module.exports = {
  plugins: [
    require('autoprefixer')
  ]
}
// webpack.config.js
{
	test: /\.less$/,
	use: [
    'style-loader',
    'css-loader',
    'less-loader',
    'postcss-loader',
  ]
}
  • ts-loader。转换 ts 代码
  • thread-loader。多进程打包资源
  • raw-loader。将文件以字符串的形式导入

plugin

在 webpack 的构建流程中,plugin 用于处理更多其他的一些构建任务。比如有以下这些常见的使用场景

清除 dist 文件

npm i clean-webpack-plugin -D
const { CleanWebpackPlugin } = require("clean-webpack-plugin");

plugins: [new CleanWebpackPlugin()];

关联 HTML

  1. webpack 从入口开始构建 JS ,而通常一个 spa 项目都是从一个 html 页面出发的,这时候需要使用 script 标签引用构建好的 JS 文件。
  2. 但是问题来了,这些构建好的 JS 文件名可能会变,比如用了 hash 值作为文件名,所以我们需要通过插件来自动关联 html 文件
npm i -D html-webpack-plugin
// webpack.config.js
...
const HtmlWebpackPlugin = require('html-webpack-plugin');

plugins: [
    new HtmlWebpackPlugin({
      filename: 'index.html',  // 输出文件
      template: './public/index.html' // 模板文件
    })
]

之后打包npm run build,dist 目录就有了 index.html 了

其他 plugin

  • ProvidePlugin:设置全局变量
  • mini-css-extract-plugin:将 css 从 bundle 文件中提取成一个独立的 css 文件
  • terser-webpack-plugin:压缩 js 的插件,支持压缩 es6 代码
  • copy-webpack-plugin:将文件或者文件夹拷贝到构建的输出目录

mode

开启不同的环境,webpack 4 会根据这个参数做不同的处理和优化,具体处理和优化可以参见 https://webpack.js.org/configuration/mode/#mode-development

resolve

该配置用于扩展模块的解析路径,我们可以通过它配置一些 alias

module.exports = {
  ...
  resolve: {
    alias: {
        // Support React Native Web
        // https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/
        'react-native': 'react-native-web',
        'src': path.resolve(__dirname, '../src'),
      },
  },
};

optimization

在构建后期负责压缩和优化代码,常见的代码分割现在是通过这个配置来控制。optimization.splitChunks 默认是不用设置的。如果 modeproduction,那 Webpack 4 就会开启 Code Splitting,不过只会对按需加载的代码做分割。如果我们需要配置初始加载的代码也加入到代码分割中,可以设置 splitChunks.chunks'all'

Webpack 4 的 Code Splitting 最大的特点就是配置简单,和基于内置规则自动拆分。内置的代码切分的规则是这样的:

  • 新 bundle 被两个及以上模块引用,或者来自 node_modules
  • 新 bundle 大于 30kb (压缩之前)
  • 异步加载并发加载的 bundle 数不能大于 5 个
  • 初始加载的 bundle 数不能大于 3 个

简单的说,Webpack 会把代码中的公共模块自动抽出来,变成一个包,前提是这个包大于 30kb,不然 Webpack 是不会抽出公共代码的,因为增加一次请求的成本是不能忽视的。

一般用默认的配置就行了。如果有特殊的需求,也可以通过下面的 optimization.splitChunks API 定制。

splitChunks

用法示例

chunks: 'all',
name: true,
cacheGroups: {
  // 缓存组,会继承和覆盖splitChunks的配置
  default: {
    // 模块缓存规则,设置为false,默认缓存组将禁用
    minChunks: 2, // 模块被引用>=2次,拆分至vendors公共模块
    priority: -20, // 优先级
    reuseExistingChunk: true, // 默认使用已有的模块
  },
  vendors: {
    test: /[\\/]node_modules[\\/]/, // 表示默认拆分node_modules中的模块
    priority: -10,
  },
  antd: {
    name: 'antd', // 单独将 antd 拆包
    priority: 15, // 权重需大于其它缓存组
    test: /[\/]node_modules[\/](antd|@ant-design)[\/]/,
  },
  bizcharts: {
    name: 'bizcharts', // 单独将 bizcharts 拆包
    priority: 10,
    test: /[\/]node_modules[\/]bizcharts[\/]/,
  },
  antv: {
    name: 'antv', // 单独将 antv 拆包
    priority: 12,
    test: /[\/]node_modules[\/]@antv[\/]/,
  },
},

sourceMap

sourceMap 其实就是源码和打包文件的映射关系,可以帮助我们快速定位一些代码问题。webpack 4 默认会开启,关键配置为 devtool,下面是它的一些配置对比:

webpack-sourcemap-type.png

  • inline。直接会将 .map 文件直接打包到对应的 js 中去,从而加快相应的速度。使用这个我们会发现,打包出来的文件没有 .map 文件了,而是以 base64 的形式放入了打包的文件中了。
  • cheap。map 文件只会帮你定为到具体的 某一行,并不会把代码定位到 具体的 某一行 某一列,从而加快速度;cheap 还有一个作用,就是这个选项只使针对业务代码,也就是说只能定位到业务代码里面的错误,并不能定位到我们引用的第三方文件(比如说 loader,第三方模块)的错误。
  • module。不仅会帮我们定位 自己的业务代码中的错误,还会同时帮我们定位第三方模块的错误。
  • eval。使用 eval 包裹模块代码,并且存在 //@sourceURL,这个是打包速度最快,性能最好的的一种方式,但是有的时候,对于代码比较复杂的情况,它提示出来的错误可能不够全面。

建议配置

开发环境:cheap-module-eval-source-map

生产环境不建议开启,如果需要定位错误,可以尝试用cheap-module-source-map