likes
comments
collection
share

这一次我终于会 Webpack 了

作者站长头像
站长
· 阅读数 17

Webpack

入门

Loader

以 CSS 相关的 Loader 为例

  • webpack 默认只识别 JavaScript ,不支持解析 CSS 文件。要支持非 JavaScript 类型的文件,需要使用 Webpack 的 Loader 机制,
const path = require('path');

module.exports = {
  // JavaScript 执行入口文件
  entry: './main.js',
  output: {
    // 把所有依赖的模块合并输出到一个 bundle.js 文件
    filename: 'bundle.js',
    // 输出文件都放到 dist 目录下
    path: path.resolve(__dirname, './dist'),
  },
  module: {
    rules: [
      {
        // 用正则去匹配要用该 loader 转换的 CSS 文件
        test: /.css$/,
        use: ['style-loader', 'css-loader?minimize'],
      }
    ]
  }
};
  • 如上配置告诉 Webpack 在遇到以 .css 结尾的文件时先使用 css-loader 读取 CSS 文件,再交给 style-loader 把 CSS 内容注入到 JavaScript 里。 在配置 Loader 时需要注意的是:

  • use 属性的值需要是一个由 Loader 名称组成的 数组,Loader 的执行顺序是 由后到前 的;

  • 每一个 Loader 都可以通过 URL querystring 的方式传入参数(相当于 GET 请求拼接参数),例如 css-loader?minimize 中的 minimize 告诉 css-loader 要开启 CSS 压缩。或者是使用对象的形式传参也可以,使用了 options 配置项。

use: [
  'style-loader', 
  {
    loader:'css-loader',
    options:{
      minimize:true,
    }
  }
]

Plugin

以 CSS 相关的插件为例

// 先 npm 下载
const ExtractTextPlugin = require('extract-text-webpack-plugin');

// plugins 跟上面的 rules 同级
 plugins: [
    new ExtractTextPlugin({
      // 从 .js 文件中提取出来的 .css 文件的名称
      filename: `[name]_[contenthash:8].css`,
    }),
  ]
  • 默认情况下, style-loader 是将 CSS 代码作以行内样式的形式嵌入到 HTML 中,可以使用 ExtractTextPlugin 插件,它能提取出 JS 代码里的 CSS 到单独的文件。

  • 可以通过插件的 filename 属性,告诉插件输出的 CSS 文件名,通过 [name]_[contenthash:8].css 字符串模版生成,里面的 [name] 代表文件名称, [contenthash:8] 代表根据文件内容算出的8位 hash 值。

DevServer

  • 在实际开发中可能会需要以下功能:
  1. 提供 HTTP 服务而不是使用本地文件预览;
  2. 监听文件的变化并自动刷新网页,做到实时预览;
  3. 支持 Source Map,以方便调试。

实时预览

实时预览原理:DevServer 会启动一个 HTTP 服务器用于服务网页请求,同时会帮助启动 Webpack ,并接收 Webpack 发出的 文件变更信号,通过 WebSocket 协议自动刷新网页做到实时预览。

  • 更具体地,Webpack 在启动时可以开启监听模式,开启监听模式后 Webpack 会监听本地文件系统的变化,发生变化时重新构建出新的结果。Webpack 默认是关闭监听模式的,可以在启动 Webpack 时通过 webpack --watch 来开启监听模式。

  • 通过 DevServer 启动的 Webpack 会开启监听模式,当发生变化时重新执行完构建后通知 DevServer。 DevServer 会让 Webpack 在构建出的 JavaScript 代码里 注入一个代理客户端 用于控制网页,网页和 DevServer 之间通过 WebSocket 协议通信, 以方便 DevServer 主动向客户端发送命令。 DevServer 在收到来自 Webpack 的文件变化通知时通过注入的客户端控制网页刷新

  • 注意:只有 entry 本身和依赖的文件才会被 Webpack 添加到监听列表里。 而其他文件是脱离了 JavaScript 模块化系统的,所以 Webpack 不知道它的存在。

模块热替换

除了通过重新刷新整个网页来实现实时预览,DevServer 还有一种被称作 模块热替换 的刷新技术。

  • 模块热替换能做到在不重新加载整个网页的情况下,用被更新过的模块替换老的模块,再重新执行一次来实现实时预览。

  • 模块热替换相对于默认的刷新机制能提供更快的响应和更好的开发体验。 模块热替换默认是关闭的,要开启模块热替换,只需在启动 DevServer 时带上 --hot 参数。

支持 Source Map

  • Source Map 是一种文件,用于将编译后的代码映射回原始源代码。它解决了 Web 开发中调试困难的问题。在 Web 开发中,我们通常会使用一些编译器、压缩器等工具,来将开发中的源代码转换为浏览器可以解析的代码。但是,由于转换后的代码与原始源代码存在差异,导致在调试时很难定位到问题所在,需要通过逐行调试或者添加大量的 log 来排查问题,非常繁琐。

  • 使用 Source Map,开发者可以通过在编译后的代码中嵌入原始源代码的映射关系,从而能够在浏览器中直接调试原始源代码,而不必去查看经过编译后的代码。大大提高工作效率。

  • Webpack 支持生成 Source Map,只需在启动时带上 --devtool source-map 参数。 加上参数重启 DevServer 后刷新页面,再打开 Chrome 浏览器的开发者工具,就可在 Sources 栏中看到可调试的源代码。

Webpack 常用配置项

  1. entry:入口文件,用于指定 Webpack 打包的入口文件路径。
  2. output:输出文件,用于指定 Webpack 打包后的输出目录和文件名。
  3. module:模块配置,用于配置 Webpack 如何处理不同类型的模块,例如 JavaScript、CSS、图片等。
  4. resolve:解析模块路径,用于配置 Webpack 如何解析模块路径,包括别名、扩展名等。
  5. plugins:插件配置,用于配置 Webpack 的插件,例如压缩代码、提取公共代码、生成 HTML 文件等。
  6. devServer:开发服务器,用于启动开发服务器,支持热更新、代理等功能。
  7. mode:模式配置,用于指定 Webpack 的构建模式,包括开发模式和生产模式。

通常情况下可用以下经验去判断如何配置 Webpack:

  • 想让源文件加入到构建流程中去被 Webpack 控制,配置 entry
  • 想自定义输出文件的位置和名称,配置 output
  • 想自定义寻找依赖模块时的策略,配置 resolve
  • 想自定义解析和转换文件的策略,配置 module,通常是配置 module.rules 里的 Loader。
  • 其它的大部分需求可能要通过 Plugin 去实现,配置 plugin

webpack 的优化方案

(一)缩小文件搜索范围

1. 优化 loader 配置

  • 为了尽可能少的让文件被 Loader 处理,可以通过 include 去命中只有哪些文件需要被处理。
module.exports = {
  module: {
    rules: [
      {
        // 如果项目源码中只有 js 文件就不要写成 /.jsx?$/,提升正则表达式性能
        test: /.js$/,
        // babel-loader 支持缓存转换出的结果,通过 cacheDirectory 选项开启
        use: ['babel-loader?cacheDirectory'],
        // 只对项目根目录下的 src 目录中的文件采用 babel-loader
        include: path.resolve(__dirname, 'src'),
      },
    ]
  },
};

2. 优化 resolve.modules 配置

  • resolve.modules 的默认值是 ['node_modules'],含义是先去当前目录下的 ./node_modules 目录下去找想找的模块,如果没找到就去上一级目录 ../node_modules 中找,再没有就去 ../../node_modules 中找,以此类推。

  • 当安装的第三方模块都放在项目根目录下的 ./node_modules 目录下时,没有必要按照默认的方式去一层层的寻找,可以指明存放第三方模块的绝对路径,以减少寻找,配置如下:

module.exports = {
  resolve: {
    // 使用绝对路径指明第三方模块存放的位置,以减少搜索步骤
    // 其中 __dirname 表示当前工作目录,也就是项目根目录
    modules: [path.resolve(__dirname, 'node_modules')]
  },
};

3. 优化 resolve.extensions 配置

  • 在导入语句没带文件后缀时,Webpack 会自动带上后缀后去尝试询问文件是否存在。 resolve.extensions 用于配置在尝试过程中用到的后缀列表,默认是:
extensions: ['.js', '.json']
  • 也就是说当遇到 require('./data') 这样的导入语句时,Webpack 会先去寻找 ./data.js 文件,如果该文件不存在就去寻找 ./data.json 文件,如果还是找不到就报错。

  • 这个列表越长,或者正确的后缀在越后面,就会造成尝试的次数越多,所以 resolve.extensions 的配置也会影响到构建的性能。 在配置时可以遵守以下几点,尽可能地优化性能:

    • 后缀尝试列表要尽可能的小。
    • 频率出现最高的文件后缀要优先放在最前面,以做到尽快地退出寻找过程。
    • 在源码中写导入语句时,要尽可能的带上后缀,从而可以避免寻找过程。如把 require('./data') 写成 require('./data.json')
module.exports = {
  resolve: {
    // 尽可能的减少后缀尝试的可能性
    extensions: ['js'],
  },
};

4. 优化 module.noParse 配置

  • module.noParse 配置项可以让 Webpack 忽略对部分没采用模块化的文件的递归解析处理,可以提高构建性能。 原因是一些库,如 jQuery 、ChartJS, 既庞大又没有采用模块化标准,让 Webpack 去解析这些文件又没有意义。
const path = require('path');

module.exports = {
  module: {
    // 忽略对 `react.min.js` 文件的递归解析处理(因为其没有采用模块化)
    noParse: [/react.min.js$/],
  },
};

注意被忽略掉的文件里不应该包含 import 、 require 、 define 等模块化语句,不然会导致构建出的代码中依旧包含这些模块化的语句,导致在浏览器环境下无法执行。

(二)使用 HappyPack

运行在 Node.js 之上的 Webpack 是单线程模型的,也就是说 Webpack 需要处理的任务需要一件件挨着做,不能多个事情一起做。

  • HappyPack就能让 Webpack 做到这点,它把任务分解给多个子进程去并发的执行,子进程处理完后再把结果发送给主进程。由于 JavaScript 是单线程模型,要想发挥多核 CPU 的能力,只能通过多进程去实现,而无法通过多线程实现。
const path = require('path');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
// 导入 HappyPack
const HappyPack = require('happypack');

module.exports = {
  module: {
    rules: [
      {
        test: /.js$/,
        // 把对 .js 文件的处理转交给 id 为 babel 的 HappyPack 实例
        use: ['happypack/loader?id=babel'],
        // 排除 node_modules 目录下的文件,node_modules 目录下的文件都是采用的 ES5 语法,没必要再通过 Babel 去转换
        exclude: path.resolve(__dirname, 'node_modules'),
      },
      {
        // 把对 .css 文件的处理转交给 id 为 css 的 HappyPack 实例
        test: /.css$/,
        use: ExtractTextPlugin.extract({
          use: ['happypack/loader?id=css'],
        }),
      },
    ]
  },
  plugins: [
    new HappyPack({
      // 用唯一的标识符 id 来代表当前的 HappyPack 是用来处理一类特定的文件
      id: 'babel',
      // 如何处理 .js 文件,用法和 Loader 配置中一样
      loaders: ['babel-loader?cacheDirectory'],
      // ... 其它配置项
    }),
    new HappyPack({
      id: 'css',
      // 如何处理 .css 文件,用法和 Loader 配置中一样
      loaders: ['css-loader'],
    }),
    new ExtractTextPlugin({
      filename: `[name].css`,
    }),
  ],
};
  • Webpack 的整个构建流程中,最耗时的流程可能就是 Loader 对文件的转换操作了,因为要转换的文件数据巨多,而且这些转换操作都只能一个个挨着处理。 HappyPack 的核心原理就是把这部分任务分解到多个进程去并行处理,从而减少了总的构建时间

  • 所有需要通过 Loader 处理的文件都先交给了 happypack/loader 去处理,收集到这些文件的处理权后 HappyPack 再统一分配

  • 每通过 new HappyPack() 实例化一个 HappyPack 其实就是告诉 HappyPack 核心调度器如何通过一系列 Loader 去转换一类文件,并且可以指定如何给这类转换操作分配子进程。

  • 核心调度器的逻辑代码在主进程中,也就是运行着 Webpack 的进程中,核心调度器会把一个个任务分配给当前空闲的子进程,子进程处理完毕后把结果发送给核心调度器,它们之间的数据交换是通过进程间通信 API 实现的。

  • 核心调度器收到来自子进程处理完毕的结果后会通知 Webpack 该文件处理完毕。

(三)使用 ParallelUglifyPlugin

当 Webpack 有多个 JavaScript 文件需要输出和压缩时,原本会使用 UglifyJS 去一个个挨着压缩再输出, 但是 ParallelUglifyPlugin 则会开启多个子进程,把对多个文件的压缩工作分配给多个子进程去完成,每个子进程其实还是通过 UglifyJS 去压缩代码,但是变成了并行执行。 所以 ParallelUglifyPlugin 能更快的完成对多个文件的压缩工作。

const path = require('path');
const DefinePlugin = require('webpack/lib/DefinePlugin');
const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');

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

(四)开启自动刷新

  1. Webpack 开启监听模式,有两种方式:

    • 在配置文件 webpack.config.js 中设置 watch: true
    • 在执行启动 Webpack 命令时,带上 --watch 参数,完整命令是 webpack --watch
  2. 文件监听的原理

    • 在 Webpack 中监听一个文件发生变化的 原理是定时的去获取这个文件的最后编辑时间,每次都存下最新的最后编辑时间,如果发现当前获取的和最后一次保存的最后编辑时间不一致,就认为该文件发生了变化。 配置项中的 watchOptions.poll 就是用于控制定时检查的周期,具体含义是每隔多少毫秒检查一次。

    • 当发现某个文件发生了变化时,并不会立刻告诉监听者,而是先缓存起来,收集一段时间的变化后,再一次性告诉监听者。 配置项中的 watchOptions.aggregateTimeout 就是用于配置这个等待时间。 这样做的目的是因为我们在编辑代码的过程中可能会高频的输入文字导致文件变化的事件高频触发,如果每次都重新执行构建就会让构建卡死。(就是一个防抖的操作)

    • 默认情况下 Webpack 会从配置的 Entry 文件出发,递归解析出 Entry 文件所依赖的文件,把这些依赖的文件都加入到监听列表中去,不是粗暴的直接监听项目目录下的所有文件。

    • 由于保存文件的路径和最后编辑时间需要占用内存,定时检查周期检查需要占用 CPU 以及文件 I/O,所以最好减少需要监听的文件数量和降低检查频率。

    module.export = {
      watchOptions: {
        // 不监听的 node_modules 目录下的文件
        ignored: /node_modules/,
      }
    }
    

    除了忽略掉部分文件的优化外,还有如下两种方法:

    • watchOptions.aggregateTimeout 值越大性能越好,因为这能降低重新构建的频率。

    • watchOptions.poll 值越大越好,因为这能降低检查的频率。

    但两种优化方法的后果是会让你感觉到监听模式的反应和灵敏度降低了。

  3. 自动刷新浏览器

  • 监听到文件更新后的下一步是去刷新浏览器,webpack 模块负责监听文件,webpack-dev-server 模块则负责刷新浏览器
  • 在使用 webpack-dev-server 模块去启动 webpack 模块时,webpack 模块的监听模式默认会被开启。 webpack 模块会在文件发生变化时告诉 webpack-dev-server 模块。

(五)开启模块热替换(HMR)

  • 在 HMR 中,当一个模块发生变化时,Webpack 只会重新编译这个模块,并将更新后的模块发送给浏览器。然后,浏览器会使用更新后的模块替换掉原来的模块,从而实现局部更新的效果。
  • 热替换其实跟自动更新是一个道理,区别就是热替换只更换修改的模块,而自动更新是刷新一整个页面,它俩都能做到实时预览的效果。

(六)区分环境

if (process.env.NODE_ENV === 'production') {
  console.log('你正在线上环境');
} else {
  console.log('你正在使用开发环境');
}

(七)CDN 加速

用 Webpack 实现 CDN 的接入

  1. 静态资源的导入 URL 需要变成指向 CDN 服务的绝对路径的 URL 而不是相对于 HTML 文件的 URL。
  2. 静态资源的文件名称需要带上有文件内容算出来的 Hash 值,以防止被缓存。
  3. 不同类型的资源放到不同域名的 CDN 服务上去,以防止资源的并行加载被阻塞。
  • 先看看要实现以上要求的最终 Webpack 配置:
const path = require('path');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const {WebPlugin} = require('web-webpack-plugin');

module.exports = {
  // 省略 entry 配置...
  output: {
    // 给输出的 JavaScript 文件名称加上 Hash 值
    filename: '[name]_[chunkhash:8].js',
    path: path.resolve(__dirname, './dist'),
    // 指定存放 JavaScript 文件的 CDN 目录 URL
    publicPath: '//js.cdn.com/id/',
  },
  module: {
    rules: [
      {
        // 增加对 CSS 文件的支持
        test: /\.css$/,
        // 提取出 Chunk 中的 CSS 代码到单独的文件中
        use: ExtractTextPlugin.extract({
          // 压缩 CSS 代码
          use: ['css-loader?minimize'],
          // 指定存放 CSS 中导入的资源(例如图片)的 CDN 目录 URL
          publicPath: '//img.cdn.com/id/'
        }),
      },
      {
        // 增加对 PNG 文件的支持
        test: /\.png$/,
        // 给输出的 PNG 文件名称加上 Hash 值
        use: ['file-loader?name=[name]_[hash:8].[ext]'],
      },
      // 省略其它 Loader 配置...
    ]
  },
  plugins: [
    // 使用 WebPlugin 自动生成 HTML
    new WebPlugin({
      // HTML 模版文件所在的文件路径
      template: './template.html',
      // 输出的 HTML 的文件名称
      filename: 'index.html',
      // 指定存放 CSS 文件的 CDN 目录 URL
      stylePublicPath: '//css.cdn.com/id/',
    }),
    new ExtractTextPlugin({
      // 给输出的 CSS 文件名称加上 Hash 值
      filename: `[name]_[contenthash:8].css`,
    }),
    // 省略代码压缩插件配置...
  ],
};
  • 以上代码中最核心的部分是通过 publicPath 参数设置存放静态资源的 CDN 目录 URL, 为了让不同类型的资源输出到不同的 CDN,需要分别在:
    1. output.publicPath 中设置 JavaScript 的地址。
    2. css-loader.publicPath 中设置被 CSS 导入的资源的的地址。
    3. WebPlugin.stylePublicPath 中设置 CSS 文件的地址。
    4. 设置好 publicPath 后,WebPlugin 在生成 HTML 文件和 css-loader 转换 CSS 代码时,会考虑到配置中的 publicPath,用对应的线上地址替换原来的相对地址。

(八)Tree-Shaking

Tree Shaking 正常工作的前提是交给 Webpack 的 JavaScript 代码必须是采用 ES6 模块化语法的, 因为 ES6 模块化语法是静态的(导入导出语句中的路径必须是静态的字符串,而且不能放入其它代码块中),这让 Webpack 可以简单的分析出哪些 export 的被 import 过了。 如果你采用 ES5 中的模块化,例如 module.export={...}require(x+y)if(x){require('./util')},Webpack 无法分析出哪些代码可以剔除。

  • 首先,为了把采用 ES6 模块化的代码交给 Webpack,需要配置 Babel 让其保留 ES6 模块化语句,修改 .babelrc 文件为如下:
{
  "presets": [
    [
      "env",
      {
        "modules": false
      }
    ]
  ]
}
  • 其中 "modules": false 的含义是关闭 Babel 的模块转换功能,保留原本的 ES6 模块化语法。
  • Webpack 只是指出了哪些函数用上了哪些没用上,要剔除用不上的代码还得经过 UglifyJS 去处理一遍;也可以简单的通过在启动 Webpack 时带上 --optimize-minimize 参数来实现 Tree-Shaking。

(九)提取公共代码

const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');

new CommonsChunkPlugin({
  // 从哪些 Chunk 中提取
  chunks: ['a', 'b'],
  // 提取出的公共部分形成一个新的 Chunk,这个新 Chunk 的名称
  name: 'common'
})

(十)按需加载

  • Webpack 内置了强大的分割代码的功能去实现按需加载,实现起来非常简单。

举个例子,现在需要做这样一个进行了按需加载优化的网页:网页首次加载时只加载 main.js 文件,网页会展示一个按钮,main.js 文件中只包含监听按钮事件和加载按需加载的代码。当按钮被点击时才去加载被分割出去的 show.js 文件,加载成功后再执行 show.js 里的函数。

  • 其中 main.js 文件内容如下:
window.document.getElementById('btn').addEventListener('click', function () {
  // 当按钮被点击后才去加载 show.js 文件,文件加载成功后执行文件导出的函数
  import(/* webpackChunkName: "show" */ './show').then((show) => {
    show('Webpack');
  })
});
  • show.js 文件内容如下:
module.exports = function (content) {
  window.alert('Hello ' + content);
};
  • 代码中最关键的一句是 import(/* webpackChunkName: "show" */ './show'),Webpack 内置了对 import(*) 语句的支持,当 Webpack 遇到了类似的语句时会这样处理:

    1. 以 ./show.js 为入口新生成一个 Chunk;
    2. 当代码执行到 import 所在语句时才会去加载由 Chunk 对应生成的文件。
    3. import 返回一个 Promise,当文件加载成功时可以在 Promise 的 then 方法中获取到 show.js 导出的内容。

/* webpackChunkName: "show" */ 的含义是为动态生成的 Chunk 赋予一个名称,以方便我们追踪和调试代码。 如果不指定动态生成的 Chunk 的名称,默认名称将会是 [id].js

  • 为了正确的输出在 /* webpackChunkName: "show" */ 中配置的 ChunkName,还需要配置下 Webpack,配置如下:
module.exports = {
  // JS 执行入口文件
  entry: {
    main: './main.js',
  },
  output: {
    // 为从 entry 中配置生成的 Chunk 配置输出文件的名称
    filename: '[name].js',
    // 为动态加载的 Chunk 配置输出文件的名称
    chunkFilename: '[name].js',
  }
};
  • 其中最关键的一行是 chunkFilename: '[name].js',,它专门指定动态生成的 Chunk 在输出时的文件名称。 如果没有这行,分割出的代码的文件名称将会是 [id].js

(十一)使用 Prepack

  • 在保持运行结果一致的情况下,改变源代码的运行逻辑,输出性能更高的 JavaScript 代码。实际上 Prepack 就是一个部分求值器,编译代码时提前将计算结果放到编译后的代码中,而不是在代码运行时才去求值。

(十二)开启 Scope Hoisting

  • Scope Hoisting 的实现原理:分析出模块之间的依赖关系,尽可能的把打散的模块合并到一个函数中去,但前提是不能造成代码冗余。 因此只有那些被引用了一次的模块才能被合并。
  • 代码体积更小,因为函数申明语句会产生大量代码;代码在运行时由于创建的函数作用域更少了,内存开销也随之变小。

Webpack 的原理与流程

基本概念

  1. Entry:入口,Webpack 执行构建的第一步将从 Entry 开始,可抽象成输入。
  2. Module:模块,在 Webpack 里一切皆模块,一个模块对应着一个文件。Webpack 会从配置的 Entry 开始递归找出所有依赖的模块。
  3. Chunk:代码块,一个 Chunk 由多个模块组合而成,用于代码合并与分割。
  4. Loader:模块转换器,用于把模块原内容按照需求转换成新内容。
  5. Plugin:扩展插件,在 Webpack 构建流程中的特定时机会广播出对应的事件,插件可以监听这些事件的发生,在特定时机做对应的事情。

流程概括

Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:

  1. 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的 参数
  2. 开始编译:用上一步得到的参数 初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法 开始执行编译;
  3. 确定入口:根据配置中的 entry 找出所有的入口文件;
  4. 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再 递归 本步骤直到所有入口依赖的文件都经过了本步骤的处理;
  5. 完成模块编译:在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系;
  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会;
  7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。
  • 在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果。

流程细节

可以分为三个阶段

  1. 初始化阶段启动构建,读取与合并配置参数,加载 Plugin,实例化 Compiler。
  • 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数。 这个过程中还会执行配置文件中的插件实例化语句 new Plugin()。
  • 实例化 Compiler:用上一步得到的参数初始化 Compiler 实例,Compiler 负责文件监听和启动编译。Compiler 实例中包含了完整的 Webpack 配置,全局只有一个 Compiler 实例。
  • 加载插件:依次调用插件的 apply 方法,让插件可以监听后续的所有事件节点。同时给插件传入 compiler 实例的引用,以方便插件通过 compiler 调用 Webpack 提供的 API。
  • environment:开始应用 Node.js 风格的文件系统到 compiler 对象,以方便后续的文件寻找和读取。
  • entry-option:读取配置的 Entrys,为每个 Entry 实例化一个对应的 EntryPlugin,为后面该 Entry 的递归解析工作做准备。
  • after-plugins 调用完所有内置的和配置的插件的 apply 方法。
  • after-resolvers:根据配置初始化完 resolver,resolver 负责在文件系统中寻找指定路径的文件。
  1. 编译阶段从 Entry 发出,针对每个 Module 串行调用对应的 Loader 去翻译文件内容,再找到该 Module 依赖的 Module,递归地进行编译处理。
  • run:启动一次新的编译。
  • watch-run:和 run 类似,区别在于它是 在监听模式下启动的编译,在这个事件中可以获取到是哪些文件发生了变化导致重新启动一次新的编译。
  • compile:该事件是为了告诉插件一次新的编译将要启动,同时会给插件带上 compiler 对象。
  • compilation:当 Webpack 以开发模式运行时,每当检测到文件变化,一次新的 Compilation 将被创建。一个 Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。Compilation 对象也提供了很多事件回调供插件做扩展。
  • make:一个新的 Compilation 创建完毕,即将从 Entry 开始读取文件,根据文件类型和配置的 Loader 对文件进行编译,编译完后再找出该文件依赖的文件,递归的编译和解析。
  • after-compile:一次 Compilation 执行完成。
  • invalid:当遇到文件不存在、文件编译错误等异常时会触发该事件,该事件不会导致 Webpack 退出。

编译阶段最重要的事件是 compilation,因为在 compilation 阶段调用了 Loader 完成了每个模块的转换操作,在 compilation 阶段又包括很多小的事件:

  • build-module:使用对应的 Loader 去转换一个模块。
  • normal-module-loader:在用 Loader 对一个模块转换完后,使用 acorn 解析转换后的内容,输出对应的抽象语法树(AST),以方便 Webpack 后面对代码的分析。
  • program:从配置的入口模块开始,分析其 AST,当遇到 require 等导入其它模块语句时,便将其加入到依赖的模块列表,同时对新找出的依赖模块递归分析,最终搞清所有模块的依赖关系。
  • seal:所有模块及其依赖的模块都通过 Loader 转换完成后,根据依赖关系开始生成 Chunk。
  1. 输出阶段
  • should-emit:所有需要输出的文件已经生成好,询问插件哪些文件需要输出,哪些不需要。
  • emit:确定好要输出哪些文件后,执行文件输出,可以在这里获取和修改输出内容。
  • after-emit:文件输出完毕。
  • done:成功完成一次完整的编译和输出流程。
  • failed:如果在编译和输出流程中遇到异常导致 Webpack 退出时,就会直接跳转到本步骤,插件可以在本事件中获取到具体的错误原因。

在输出阶段已经得到了各个模块经过转换后的结果和其依赖关系,并且把相关模块组合在一起形成一个个 Chunk。 在输出阶段会根据 Chunk 的类型,使用对应的模版生成最终要要输出的文件内容。

webpack 与 vite 的区别

  • 最核心的区别:

    • Webpack是一次性构建所有资源,包括JS、CSS、图片等等,然后将它们打包到一个或多个bundle中。
    • Vite则是在需要使用某个资源时,才会去构建该资源,它会将这个资源单独编译和构建成一个模块,然后在浏览器中直接加载这个已经构建好的模块。

    参考文档:深入浅出 Webpack