演示仓库地址(可以翻 commit 记录):https://github.com/wangpeng1994/webpack-demo

有三个比较容易混淆的概念:bundle,chunk 和 module

先看一下官方术语 https://webpack.docschina.org/glossary

再看一下下面的理解:

首先对于“模块”(module)的概念相信大家都没有异议,它指的就是我们在编码过程中有意识的封装和组织起来的代码片段。狭义上我们首先联想到的是碎片化的 React 组件,或者是 CommonJS 模块又或者是 ES6 模块,但是对 Webpack 和 Loader 而言,广义上的模块还包括样式和图片,甚至说是不同类型的文件。

而“包”(bundle) 就是把相关代码都打包进入的最终的单个文件。如果你不想把所有的代码都放入一个包中,你可以把它们划分为多个包,也就是“块”(chunk) 中。从这个角度上看,“块”等于“包”,它们都是对代码再一层的组织和封装。如果必须要给一个区分的话,通常我们在讨论时,bundle 指的是所有模块都打包进入的单个文件,而 chunk 指的是按照某种规则的模块集合,chunk 的体积大于单个模块,同时小于(或等于)整个 bundle。

零配置 wbpack 包含:

  1. module.exports = {
  2. entry: './src/index.js',
  3. output: './dist/main.js'
  4. }

1. 核心概念 entry

第一章:基础用法 - 图1

Entry 用法分为单入口和多入口。

单入口:entry 是一个字符串,通常适合单页应用。

module.exports = {
  entry: './path/to/my/entry/file.js'
};

多入口:entry 是一个对象,通常适合多页应用。

module.exports = {
  entry: {
    app: './src/app.js',
    adminApp: './src/adminApp.js'
  }
};

2. 核心概念 output

Output 用来告诉 webpack 如何将编译后的文件输出到磁盘。

单入口配置:只需要指定 filename 和 path。

module.exports = {
  entry: './path/to/my/entry/file.js',
  output: {
    filename: 'bundle.js',
    path: __dirname + '/dist'
  }
};

多入口配置:其实对于 output 来说不管是单入口还是多入口都一视同仁,只要通过 [name] 占位即可,而不是像上面那样写死。

module.exports = {
  entry: {
    app: './src/app.js',
    search: './src/search.js'
  }
  output: {
    filename: '[name].js',
    path: __dirname + '/dist'
  }
};

3. 核心概念 loaders

webpack 开箱即用只能理解 JavaScript 和 JSON 文件,通过 Loaders 使得 webpack 可以将其他文件类型转化为有效的模块以供使用,并且添加到依赖图中。

webpack 在 require()/ import 语句中遇到指定的文件类型的路径时,会先使用指定的 loader 对其进行转换,然后再将其添加到包中。

Loaders 本身是一个函数,接受源文件作为参数,返回转换的结果。

常见的 Loaders:
第一章:基础用法 - 图2

Loaders 的用法:
test 指定匹配规则,use 指定使用的 laoder 名称。

const path = require('path');

module.exports = {
  output: {
    filename: 'my-first-webpack.bundle.js'
  },
  module: {
    rules: [
      { test: /\.txt$/, use: 'raw-loader' }
    ]
  }
};

4. 核心概念 plugins

Loaders 用于转换某些类型的模块,而 Plugins 用来增强 webpack 的功能,任何 Loaders 做不到的事情,都可以使用 Plugins 去完成,可以执行更广泛的任务,比如用于 bundle 文件的优化,资源管理和环境变量注入。

Plugins 作用于整个构建过程。

常见的 Plugins:

extracttextwebpackplugin 这个插件在webpack4中已经替换为 mini-css-extract-plugin,另外 commonsChunkPlugin 这个插件在webpack4中目前也不推荐使用了。

第一章:基础用法 - 图3

Plugins 的用法(只观察用法,先不必理解具体含义):

const HtmlWebpackPlugin = require('html-webpack-plugin'); //installed via npm
const webpack = require('webpack'); //to access built-in plugins

module.exports = {
  module: {
    rules: [
      { test: /\.txt$/, use: 'raw-loader' }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({template: './src/index.html'})
  ]
};

5. 核心概念 mode

Mode 用来指定当前的构建环境:production(默认值)、development 或 none,使得 webpack 应用对应环境下的内建优化。

Mode 的内置操作:
第一章:基础用法 - 图4

6. 解析 ECMAScript 6 和 React JSX

关于 babel:

Preset 是一系列 Plugin 的预设集合。
先执行完所有Plugin,再执行Preset。
多个Plugin,按照声明次序顺序执行。
多个Preset,按照声明次序逆序执行。

6.1 解析 ECMAScript 6

安装 babel 和相关预设:

$ npm i @babel/core @babel/preset-env babel-loader -D

使用 babel-loader,babel 的配置文件是 .babelrc
使用 @babel/preset-env 预设。

6.2 解析 React JSX

同理使用 @babel/preset-react 预设,先安装依赖:

$ npm i react react-dom @babel/preset-react -D

.babelrc

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

webpack.config.js

'user strict';

const path = require('path');


module.exports = {
  entry: {
    index: './src/search.js',
  },
  output: {
    path: path.join(__dirname, 'dist'),
    filename: '[name].js'
  },
  mode: 'production',
  module: {
    rules: [
      {
        test: /\.js$/,
        use: 'babel-loader'
      }
    ]
  }
};

7. 解析 CSS、Less 和 Sass

css-loader 用于加载 .css 文件,并且转换成 commonjs 对象;
style-loader 将样式通过 <style> 标签插入到 HTML 的 head 中;
less-loader 将 less 转换成 css。

loader 的执行顺序是从右向左的,所以匹配到 css 文件后,先经过 less-loader 处理,再由 css-loader 处理,最后由 style-loader 处理。

代码中直接 import,之后 webpack 会应用相应的 loader 进行转化。

import './search.less';
// ...
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          'style-loader',
          'css-loader'
        ]
      },
      {
        test: /\.less$/,
        use: [
          'style-loader',
          'css-loader',
          'less-loader'
        ]
      }
    ]
  }
// ...

···

8. 解析图片和字体

file-loader 用于处理文件,比如图片,也可以用于处理字体,因为本质都是文件。

安装之后,webpack loader 配置里增加 file-loader 即可:
webpack.config.js:

{
        test: /\.(png|jpg|gif|jpeg)$/,
        use: 'file-loader'
      },
      {
        test: /\.(woff|woff2|eot|ttf|otf)$/,
        use: 'file-loader'
      }

search.less:

@font-face {
  font-family: '白舟篆古印';
  src: url('./images/白舟篆古印.ttf') format('truetype');
}

.search-text {
  font-size: 20px;
  color: #f00;
  font-family: '白舟篆古印';
}

search.js:

'use strict';

import React from 'react';
import ReactDOM from 'react-dom';
import beauty from './images/beauty.jpg';
import './search.less';

function Search() {
  return (
    <div className="search-text">
      Search Text 你好啊<img src={beauty} />
    </div>
  );
}

ReactDOM.render(
  <Search />,
  document.getElementById('root')
);

除了使用 file-loader 之外,也可以使用 url-loader (内部也是使用了 file-loader)处理图片和字体,
url-loader 功能类似于 file-loader,但是在文件大小(单位 byte)低于指定的限制(默认没有限制,总是转)时,可以返回一个 DataURL(数据是base64编码格式)。

安装之后,更改 loader 配置如下,这里主要演示当图片资源小于 10KB 时直接转为 base64 字符串嵌入到代码中,不再单独输出为一个图片文件,减少了 HTTP 请求次数。

当然,乐意的话,字体文件也可以转为 base64:

{
        test: /\.(png|jpg|gif|jpeg)$/,
        use: [
          {
            loader: 'url-loader',
            options: {
              limit: 10240
            }
          }
        ]
      },
      {
        test: /\.(woff|woff2|eot|ttf|otf)$/,
        use: 'file-loader'
      }

webpack 打包后只输出了两个文件,代表图片的 DataURL 被嵌入到 search.js 文件中:
第一章:基础用法 - 图5

第一章:基础用法 - 图6

React 渲染挂载之后,最终效果就是嵌入到 HTML 中:
第一章:基础用法 - 图7

9. webpack 中的文件监听

文件监听是指发现源码发生变化时,自动重新构建出新的输出文件。

webpack 开启监听模式,有两种方式:

  • 启动 webpack 命令时,带上 —watch 参数
  • 在配置 webpack.config.js 中设置 watch: true

文件监听的原理分析:

webpack 会轮询判断文件的最后编辑时间是否变化,某个文件发生了变化后,并不会立即告诉监听者去重新编译,而是先缓存起来,等待 aggregateTimeout(集合时间)到了再去执行:

module.export = {
  // 默认 false,也就是不开启
  watch: true,
  // 只有开启监听模式时,watchOptions 才有意义
  watchOptions: {
    // 默认为空,指定不监听的文件或文件夹,支持正则匹配
    ignored: /node_modules/,
    // 监听到变化发生后会等 300ms 再去执行,默认 300ms
    aggregateTimeout: 300,
    // 判断文件是否发生变化是通过不停询问系统指定文件有没有发生变化实现的,默认 1000ms 询问一次
    poll: 1000
  }
}

主要是可以设置忽略轮询的文件夹,提高速度。
但 watch 模式有个缺点,即使自动编译,但仍需手动刷新浏览器。所以通常使用 webpack-dev-server。

10. webpack 中的热更新及原理分析

10.1 热更新:webpack-dev-server(WDS)

第一章:基础用法 - 图8

—open 当服务启动后会自动打开浏览器

webpack-dev-server 会开启一个简单的 web 服务器并能实时自动刷新(live load),通常配合内置插件 HotModuleReplacementPlugin(HMR) 使用更佳。
热模块更换(或HMR)是 webpack 提供的最有用的功能之一。 它允许在运行时更新各种模块,而无需完全刷新。
注意!如果已经开启了 hot 模式,HotModuleReplacementPlugin 会被自动添加的,免去手动引入。

webpack.config.js:

const webpack = require('webpack');
// ...
//  plugins: [
//    new webpack.HotModuleReplacementPlugin()
//  ],
  devServer: {
    contentBase: './dist',
    hot: true
  }
// ...

WDS 不刷新浏览器,不输出文件,而是放在内存中。

10.2 热更新:使用 webpack-dev-middleware(WDM)

WDM 用于将 webpack 输出的文件传输给服务器,(webpack-dev-server 内部也正是这么做的)适用于灵活的定制场景。具体看官网,以下用法不完整,只是摘录:
server.js:

const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');

const app = express();
const config = require('./webpack.config.js');
const compiler = webpack(config);

// Tell express to use the webpack-dev-middleware and use the webpack.config.js
// configuration file as a base.
app.use(webpackDevMiddleware(compiler, {
  publicPath: config.output.publicPath,
}));

// Serve the files on port 3000.
app.listen(3000, function () {
  console.log('Example app listening on port 3000!\n');
});

10.3 热更新的原理分析

第一章:基础用法 - 图9

HMR Server 通过 socket 协议使用 json 告诉 HMR Runtime 哪些文件发生了变化,然后去做更新。

11. 文件指纹策略:chunkhash、contenthash 和 hash

文件指纹是指打包后输出的文件名的后缀,用于对文件进行版本管理,结合浏览器缓存机制,对于指纹没有更改的文件,会从浏览器本地缓存中加载,从而加快了页面的整体速度。

11.1 常见的文件指纹

Hash

和整个项目的构建相关,只要项目里有文件更改,整个项目构建的 hash 值都会更改,并且全部共用相同的 hash 值。

如下所示, JS 和 CSS 都采用 hash 的话:
第一章:基础用法 - 图10

只要修改了这些源文件任意一处,下次编译时的总 hash 就会改变,而其它没有实质更改的文件因为使用了该 hash,所以指纹也会改变,这有些浪费。

Chunkhash

和 webpack 打包的 chunk 有关,根据不同的 entry 入口进行依赖文件解析、构建对应的 chunk,生成对应的 hash 值。

chunkhash 是基于给定块的全部内容生成的,所以即使改的是一个块的 CSS 文件,也会导致该块对应的整体 chunkhash 改变。但好在另一个 入口的 chunkhash 不会改变。

JS 文件一般采用 chunkhash 的指纹方式。通常在生产环境里把一些公共库和程序入口文件区分开,使用不同的 entry,从而单独打包构建,达到修改一个 entry 的 JS 文件,不会引起另一个 entry 的 JS 文件指纹发生改变,用户的浏览器缓存依旧可以发生作用。

Contenthash

只根据文件内容来定义 hash 指纹,文件内容不变,则 contenthash 不变。
CSS 文件通常采用 Contenthash,而不适用 chunkhash,以免 JS 改变也导致 CSS 的文件指纹发生改变。

11.2 文件指纹的设置

JS 的文件指纹设置

设置 output 的 filename,使用 [chunkhash]。

注意 webpack 的 chunkhash 是没办法在热更新(HMR)一起使用的。

CSS 的文件指纹设置

设置 MiniCssExtractPlugin 的 filename,使用 [contenthash]。

使用了 style-loader 和 css-loader,最终 style-loader 会把 CSS 内容放在 head 的 <style> 标签中,因此没有独立的 CSS 文件。
为了配合设置文件指纹,使用 MiniCssExtractPlugin 来将 css-loader 的输出抽取成单独的 CSS 文件,以便此时有机会设置文件指纹,由于该插件和 style-loader 功能互斥,所以不要再指定 style-loader 了。

图片/字体的文件指纹设置

设置 file-loader (或 url-loader)的 name,使用 [hash] 或 [contenthash],都只会根据图片本身的信息来计算 hash 值,注意图片的 hash 和 css/js 资源的 hash 概念不一样,此 hash 并非每次 webpack 项目构建的那个总 hash,这里的 hash 是由 file-loader 根据文件内容计算出来的,所以图片的 hash 是由图片内容决定的。

第一章:基础用法 - 图11

文件的指纹设置演示如下:

'user strict';

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

module.exports = {
  entry: {
    index: './src/index.js',
    search: './src/search.js'
  },
  output: {
    path: path.join(__dirname, 'dist'),
    filename: '[name]_[chunkhash:8].js',
  },
  mode: 'production',
  module: {
    rules: [
      {
        test: /\.js$/,
        use: 'babel-loader'
      },
      {
        test: /\.css$/,
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader'
        ]
      },
      {
        test: /\.less$/,
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader',
          'less-loader'
        ]
      },
      {
        test: /\.(png|jpg|gif|jpeg)$/,
        use: [
          {
            loader: 'url-loader',
            options: {
              limit: 10240, // 小于 10KB 会转为 base64 URIs,否则内部会自动调用 file-loader 
              name: '[name]_[hash:8].[ext]'
            }
          }
        ]
      },
      {
        test: /\.(woff|woff2|eot|ttf|otf)$/,
        use: [
          {
            loader: 'file-loader',
            options: {
              name: '[name]_[hash:8].[ext]'
            }
          }
        ]
      }
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({
    // 和 Output 中一样 [name] 是指 chunk 名字,即对应 entry 的 key
      filename: '[name]_[contenthash:8].css' 
    })
  ]
};

12. HTML、CSS 和 JavaScript 代码压缩

12.1 JS 文件的压缩

内置了 terser-webpack-plugin,生产环境下会自动开启。

12.2 CSS 文件的压缩

使用 optimize-css-assets-webpack-plugin,同时使用 cssnano 作为指定的预处理。

12.3 HTML 文件的压缩

修改 html-webpack-plugin,设置压缩参数。创建 html 文件离不开这个插件,它能自动引入外部的打包资源,尤其文件名加了 hash 指纹后自动化很重要。

minify 在 webpack 的 production 模式下默认就是开启状态,
默认参数是:

{
  collapseWhitespace: true,
  removeComments: true,
  removeRedundantAttributes: true,
  removeScriptTypeAttributes: true,
  removeStyleLinkTypeAttributes: true,
  useShortDoctype: true
}

但我们也可以配置自己的对象参数,只是需要注意 To use custom html-minifier options pass an object to minify instead. This object will not be merged with the defaults above.
意思是自己传对象参数时,html-webpack-plugin 提供的默认值就全被放弃了,其实 minify 这个功能主要还是 html-minifier-terser 负责的,站在这更底一层的角度看,html-minifier-terser 本身也是提供很多配置项及其默认值,只是大部分都是 false。所以当决定自己配置 html-webpack-plugin 的压缩参数时,就去看 html-minifier-terser 即可。

演示中配置了两个入口 JS 文件,以及两次 HtmlWebpackPlugin(后面学习多页面打包时再深究)。

代码压缩演示如下:

'user strict';

const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: {
    index: './src/index.js',
    search: './src/search.js'
  },
  output: {
    path: path.join(__dirname, 'dist'),
    filename: '[name]_[chunkhash:8].js',
  },
  mode: 'production',
  module: {
    rules: [
      {
        test: /\.js$/,
        use: 'babel-loader'
      },
      {
        test: /\.css$/,
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader'
        ]
      },
      {
        test: /\.less$/,
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader',
          'less-loader'
        ]
      },
      {
        test: /\.(png|jpg|gif|jpeg)$/,
        use: [
          {
            loader: 'url-loader',
            options: {
              limit: 10240, // 小于 10KB 会转为 base64 URIs,否则内部会自动调用 file-loader 
              name: '[name]_[hash:8].[ext]'
            }
          }
        ]
      },
      {
        test: /\.(woff|woff2|eot|ttf|otf)$/,
        use: [
          {
            loader: 'file-loader',
            options: {
              name: '[name]_[hash:8].[ext]'
            }
          }
        ]
      }
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name]_[contenthash:8].css'
    }),
    new OptimizeCSSAssetsPlugin(), // 会在构建时默认寻找 .css 文件并使用 cssnamo 压缩(可配置)
    new HtmlWebpackPlugin({
      filename: 'index.html',
      template: path.join(__dirname, 'src/index.html'),
      chunks: ['index'], // HTML 中只引入感兴趣的 chunk
      minify: {
        collapseWhitespace: true,
        preserveLineBreaks: false,
        minifyCSS: true, // 压缩源 html 文件中的内联 css
        minifyJS: true // 压缩源 html 文件中的内联 js
      }
    }),
    new HtmlWebpackPlugin({
      filename: 'search.html',
      template: path.join(__dirname, 'src/search.html'),
      chunks: ['search'],
      minify: {
        collapseWhitespace: true,
        preserveLineBreaks: false,
        minifyCSS: true,
        minifyJS: true
      }
    })
  ]
};

第一章:基础用法 - 图12


温馨提示:

  1. 本文省略了一些 npm 装包过程,自行安装即可;
  2. 官方文档优先看英文的,其次 webpack.docschina.org 的时效性看起来也有一定保证,而 https://www.webpackjs.com 这个我和英文文档也对比了下,很多描述都并非一一对应,我不相信采用了意译,应该是落后于官方英文文档了。