《从 Webpack 到 Vite 系列一》大话 Webpack

731 阅读21分钟

引用:

前端模块化开发

模块化

最早的时候,我们会通过文件划分的形式实现模块化,也就是将每个功能及其相关状态数据各自单独放到不同的 JS 文件中。 使用 JavaScript 编码在于正确地管理变量,在于给变量赋值,或者给变量赋以数值或者合并两个变量并把它们赋值给另外一个变量。

因为你的大多数代码都是在更改变量,所以这些变量将会对你的编码方式以及代码的维护产生重大的影响。

当一次只需要考虑几个变量的时候使得事情变得非常简单,JavaScript 有一个方式来帮助你实现这个目标,那就是 —— 作用域。因为作用域的存在,函数不能访问 定义在其他函数内部的变量。

image.png 这很棒。这意味着当你专注于实现一个函数的时候,你只需要专注于实现这个函数,而不需要担心其他的函数会影响到你这个函数里的变量。

不过,它也有一个缺陷,它使得==不同的函数之间共享变量变得更加困难==。

那么假如你的确想要在作用域之外共享你的变量呢?通常的做法是将它放在当前作用域之上,比如:全局作用域。这是可行的,但是会产生一些烦人的问题。

首先,你所有的 script 标签都必须放置于一个正确的顺序。那么你就必须很小心并确保这些脚本之间不会互相影响。

如果你确实不小心搞乱了顺序,那么在代码运行的时候,你的应用就会抛出异常。当函数寻找 jQuery 对象的存在 —— 也就是全局作用域之下,但是却找不到的时候,函数就会报错并停止执行。

这让代码维护变得棘手!! 移除旧的代码或者是 script 标签就像是玩赌场转盘一样。你无法预料到什么代码可能崩溃。代码之间的依赖关系变得隐蔽。任何函数都可以获取到全局作用域上的任何东西,所以你并没有办法知道哪个函数依赖于哪个 script 标签。

其次,由于你的变量都存在于全局作用域上,所有处于这个作用域之上的代码都可以改变这些变量。恶意代码可以通过更改这些变量来让你的代码做并非你本意的事情,或者非恶意的代码会不小心破坏你的变量。

理想的解决方式是,在页面中引入一个JS入口文件,其余用到的模块可以通过代码控制,按需加载进来。除了模块加载的问题以外,还需要规定模块化的规范,如今流行的则是CommonJS ES Modules

推荐阅读 前端模块化开发的价值

遇到的其他问题

从后端渲染的JSPPHP,到前端原生JavaScript,再到jQuery开发,再到目前的三大框架VueReactAngular

开发方式,也从javascript到后面的es5es6、7、8、9、10,再到typescript,包括编写CSS的预处理器lessscss

现代前端开发已经变得十分的复杂,所以我们开发过程中会遇到如下的问题:

  • 需要通过模块化的方式来开发
  • 使用一些高级的特性来加快我们的开发效率或者安全性,比如通过 ES6+、TypeScript开发脚本逻辑,通过sass、less等方式来编写 css 样式代码
  • 监听文件的变化来并且反映到浏览器上,提高开发的效率
  • JavaScript 代码需要模块化,HTML 和 CSS 这些资源文件也会面临需要被模块化的问题
  • 开发完成后我们还需要将代码进行压缩、合并以及其他相关的优化

webpack恰巧可以解决以上问题

webpack四个核心概念及工作原理

webpack有一个独立的配置文件 webpack.config. js 四个核心概念 ( 入口 、输出、 loader、 插件)

入口

entry是配置模块的入口,可以抽象成输入,webpack执行构建的第一步将从入口开始搜寻及递归解析出所有入口依赖的模块。

单个入口 string | [string]

想要一次注入多个依赖文件,并且将它们的依赖关系绘制在一个 "chunk" 中时,这种方式就很有用。

对象语法 { string | [string] } | {}

用于将关注点从环境(environment)、构建目标(build target)、运行时(runtime)中分离。然后使用专门的工具(如 webpack-merge)将它们合并起来。

输出

可以通过配置 output 选项,告知 webpack 如何向硬盘写入编译文件。注意,即使可以存在多个 entry 起点,但只能指定一个 output 配置。

loader

module.exports = {
  //...
  module: {
    rules: [
      {
        test: /\.less/,
        use: ['style-loader','less-loader'],
      },
    ],
  },
};

loader 用于对模块的源代码进行转换。loader 可以使你在 import 或 "load(加载)" 模块时预处理文件。因此,loader 类似于其他构建工具中“任务(task)”,并提供了处理前端构建步骤的得力方式。loader 可以将文件从不同的语言(如 TypeScript)转换为 JavaScript 或将内联图像转换为 data URL。

1、loader 使用方式

① (==推荐方式==)配置loader 在 webpack.config.js 文件中指定 loader。

modules.rules 允许你在 webpack 配置中指定多个 loader,

loader 从右到左(或从下到上)地取值(evaluate)/执行(execute)

② 内联方式

可以在 import 语句或任何 与 "import" 方法同等的引用方式 中指定 loader。使用 ! 将资源中的 loader 分开。每个部分都会相对于当前目录解析。

尽可能使用 module.rules,因为这样可以减少源码中样板文件的代码量,并且可以在出错时,更快地调试和定位 loader 中的问题。

loader特性:

  • loader 支持==链式调用==。
  • loader 可以是同步的,也可以是异步的。(如果是单个处理结果,可以在 同步模式 中直接返回。如果有多个处理结果,则必须调用 this.callback()。在 异步模式 中,必须调用 this.async()来告知 loader runner 等待异步结果,它会返回 this.callback() 回调函数。随后 loader 必须返回 undefined 并且调用该回调函数。)
  • loader 运行在 Node.js 中,并且能够执行任何操作。
  • loader 可以通过 options 对象配置。
  • 除了常见的通过 package.jsonmain 来将一个 npm 模块导出为 loader,还可以在 module.rules 中使用 loader 字段直接引用一个模块。
  • 插件(plugin)可以为 loader 带来更多特性。

loader 说白了就是找一个合适的加载器处理代码,最终返回一段标准的JS代码字符串

所谓 loader 只是一个导出为函数的 JavaScript 模块。它接收上一个 loader 产生的结果或者资源文件(resource file)作为入参。也可以用多个 loader 函数组成 loader chain。compiler 需要得到最后一个 loader 产生的处理结果。这个处理结果应该是 String 或者 Buffer(被转换为一个 string)。具体的用法,可以看 Loader 官网的描述。接下来我们从源码的角度去分析,为什么可以这样做,为什么可以实现同异步钩子,内部到底是怎么实现的。那么这就是 ==loader-runner== 的作用所在。(Webpack 内部会使用 loader-runner 这个库来运行已配置的 loaders。)

2、loader工作原理(loader-runner)

(1) loader本质是导出函数的JavaScript模块
function simpleLoader(content, map, meta) {
  console.log("我是 SimpleLoader");
  return content;
}
module.exports = simpleLoader;
(2) Normal Loader 和 Pitching Loader (按照运行机制分)

① Normal Loader: Loader模块中导出的函数称为 Normal Loader

// loaders/a-loader.js
function aLoader(content, map, meta) {
  console.log("开始执行aLoader Normal Loader");
  content += "aLoader]";
  return `module.exports = '${content}'`;
}
module.exports = aLoader;

// loaders/b-loader.js
function bLoader(content, map, meta) {
  console.log("开始执行bLoader Normal Loader");
  return content + "bLoader->";
}
module.exports = bLoader;

// loaders/c-loader.js
function cLoader(content, map, meta) {
  console.log("开始执行cLoader Normal Loader");
  return content + "[cLoader->";
}
module.exports = cLoader;

// webpack.config.js
module: {
  rules: [
    {
      test: /\.txt$/i,
      use: ["a-loader", "b-loader", "c-loader"],
    },
  ],
},
resolveLoader: {
  modules: [
    path.resolve(__dirname, "node_modules"),
    path.resolve(__dirname, "loaders"),
  ],
},

npx webpack 开始打包,控制台输出结果:

开始执行cLoader Normal Loader
开始执行bLoader Normal Loader
开始执行aLoader Normal Loader

通过观察以上的输出结果,我们可以知道 Normal Loader 的执行顺序是从右到左以管道的形式,对数据进行处理。

image.png 现在我们知道了什么是 Normal Loader 及 Normal Loader 的执行顺序。

② Pitching Loader: Loader模块中导出的函数的pitch属性所指向的函数称之为Pitching Loader

function aLoader(content, map, meta) {
  // 省略部分代码
}
 
aLoader.pitch = function (remainingRequest, precedingRequest, data) {
  console.log("开始执行aLoader Pitching Loader");
  console.log(remainingRequest, precedingRequest, data)
};
 
module.exports = aLoader;

npx webpack 开始打包,控制台输出结果:

开始执行aLoader Pitching Loader
...
开始执行bLoader Pitching Loader
...
开始执行cLoader Pitching Loader
...
开始执行cLoader Normal Loader
开始执行bLoader Normal Loader
开始执行aLoader Normal Loader

明显对于我们的示例来说,Pitching Loader 的执行顺序是 从左到右,而 Normal Loader 的执行顺序是 从右到左

image.png

Pitching Loader 除了可以提前运行之外,还有什么作用呢?

其实当某个 Pitching Loader 返回非 undefined 值时,就会实现熔断效果。

bLoader.pitch = function (remainingRequest, precedingRequest, data) {
  console.log("开始执行bLoader Pitching Loader");
  return "bLoader Pitching Loader->";
};

npx webpack 开始打包,控制台输出结果:

开始执行aLoader Pitching Loader
开始执行bLoader Pitching Loader
开始执行aLoader Normal Loader
asset bundle.js 4.53 KiB [compared for emit] (name: main)
runtime modules 937 bytes 4 modules
...

image.png

(3) pitching Loader 应用场景

如果我们去阅读 style Loader 的源码,就会惊讶的发现: 首先,==这个 loader 的所有逻辑都是设计在pitching阶段进行执行,它的normal函数就是一个空函数。== 其次,style-loader做的事情很简单:它获得对应的样式文件内容,然后通过在页面创建style节点。将样式内容赋给style节点然后将节点加入head标签即可。

为了探究pitch函数到底有什么用?我们不妨自己去实现一个 style Loader

function StyleLoader(source){
    const script = `
       const styleEl = document.createElement('style')
       styleEl.innerHTML = ${JSON.stringify(source)}
       document.head.appendChild(styleEl)
    `;
    return script
}

用我们写的 loader 搭配官方的 css-loader 去使用

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

我们知道loader 会返回一段标准的 JavaScript 代码字符串,通过 css-loader 解析后的 source 插入到 styleEl 中,任何样式都不会生效。

这也就意味着,如果我们将 style-loader 设计为normal loader 的话,我们需要执行上一个 loader 返回的js脚本,并且获得它导出的内容才可以得到对应的样式内容。 那么此时我们需要在 style-loader 的 normal 阶段实现一系列js的方法才能执行js并读取到 css-loader 返回的样式内容,这无疑是一种非常糟糕的设计模式。

==那我们将 style-loader 设计成为 pitch loader==

function styleLoader(source) {}

// pitch阶段
styleLoader.pitch = function (remainingRequest, previousRequest, data) {
  const script = `
  import style from "!!${remainingRequest}"

    const styleEl = document.createElement('style')
    styleEl.innerHTML = style
    document.head.appendChild(styleEl)
  `;
  return script;
};


pitch阶段的 remainingRequest 表示剩余还未处理loader的绝对路径以"!"拼接(包含资源路径)的字符串。

module.exports = styleLoader
pitch阶段的remainingRequest表示剩余还未处理loader的绝对路径以"!"拼接(包含资源路径)的字符串。
现在会出现什么现象?(本文中唯一的问题) pitch函数熔断反应!!

上边我们说到过,如果发生熔断效果那么此时会立马掉头执行normal loader,因为style-loader是第一个执行的过程,相当于没有走到 css-loader 就直接返回内容给 webpack 编译了。 那我们改怎么办?

==我们可以在 style-loader 的 pitch 阶段通过 require 语句引入 css-loader== 处理文件后返回的js脚本,得到导出的结果。然后重新组装逻辑返回给webpack即可。

这样做的好处是,之前我们在normal阶段需要处理的执行css-loader返回的js语句完全不需要自己实现js执行的逻辑。完全交给webpack去执行了!

我们在 normal loader 阶段棘手的关于 css-loader 返回值是一个js脚本的问题通过 import 语句我们交给了webpack去编译。

让我们先看一下打包后js代码:

image.png

import style from "!!${remainingRequest}"
  1. webpack会将本次 import style from "!!${remainingRequest}" 重新编译称为另一个 module
  2. webpack会分析这个 module 中的依赖语句进行递归编译。
  3. 返回的脚本阶段获得 css-loader 返回的js脚本并执行它,获取到它的导出内容。
(5) 真实 Pitching Loader 应用场景总结

通过上述的style-loader的例子,当我们希望将左侧的loader并联使用的时候使用pitch方式无疑是最佳的设计方式。

通过 pitch loaderimport someThing from !!${remainingRequest}剩余loader,从而实现上一个loader的返回值是js脚本,将脚本交给webpack去编译执行,这就是pitch loader的实际应用场景。

简单来说,==如果在loader开发中你的需要依赖loader其他loader,但此时上一个loader的normal函数返回的并不是处理后的资源文件内容而是一段js脚本,那么将你的loader逻辑设计在pitch阶段无疑是一种更好的方式==。

(6) Loader 是如何被运行的?
  • options获取loader信息

  • createLoaderObject 创建loader对象

// loader-runner/lib/LoaderRunner.js
function createLoaderObject(loader) {
   var obj = {
     path: null,
     query: null, 
     fragment: null,
     options: null, 
     ident: null,
     normal: null, 
     pitch: null,
     raw: null, 
     data: null,
     pitchExecuted: false,
     normalExecuted: false
   };
   // 省略部分代码
   obj.request = loader;
   if(Object.preventExtensions) {
    Object.preventExtensions(obj);
   }
   return obj;
}
  • 初始化 LoaderContext

  • iteratePitchingLoaders 迭代执行 pitching loaders (由左到右)

runSyncOrAsync 函数的回调函数内部,会根据当前 loader 对象 pitch 函数的返回值是否为 undefined 来执行不同的处理逻辑。如果 pitch 函数返回了非 undefined 的值,则会出现熔断。即跳过后续的执行流程,开始执行上一个 loader 对象上的 normal loader 函数。

  • iterateNormalLoaders 迭代执行 normal loaders(由右到左)

以上就是Webpack Loader 的本质、Normal Loader 和 Pitching Loader 的定义和使用及 Loader 是如何被运行的

以下整理了一些常见的Loader,感兴趣的可以去阅读他们具体实现方式

  • style-loader: 将css添加到DOM的内联样式标签style里
  • css-loader :允许将css文件通过require的方式引入,并返回css代码
  • less-loader: 处理less
  • sass-loader: 处理sass
  • file-loader: 分发文件到output目录并返回相对路径
  • url-loader: 和file-loader类似,但是当文件小于设定的limit时可以返回一个Data Url
  • html-minify-loader: 压缩HTML
  • babel-loader :用babel来转换ES6文件到ES

Plugin

1. 概念

Plugin(Plug-in)是一种计算机应用程序,它和主应用程序互相交互,以提供特定的功能

是一种遵循一定规范的应用程序接口编写出来的程序,只能运行在程序规定的系统下,因为其需要调用原纯净系统提供的函数库或者数据

webpack中的plugin也是如此,plugin赋予其各种灵活的功能,例如打包优化、资源管理、环境变量注入等,它们会运行在 webpack 的不同阶段(钩子 / 生命周期),贯穿了webpack整个编译周期

主要用于扩展webpack的功能,在webpack运行的生命周期中会广播出很多事件,plugin可以监听这些事件,通过webpack提供的Api改变输出结果,它是在plugins中配置,每一项都是 plugin 的实例,参数都是通过构造函数传入.

2. 配置方式

const HtmlWebpackPlugin = require('html-webpack-plugin'); // 通过 npm 安装
const webpack = require('webpack'); // 访问内置的插件
module.exports = {
  ...
  plugins: [
    new webpack.ProgressPlugin(),
    new HtmlWebpackPlugin({ template: './src/index.html' }),
  ],
};

3. 工作原理

其本质是一个具有apply方法javascript对象

Function.prototype.apply()

The apply() method calls a function with a given this value, and arguments provided as an array (or an array-like object).

apply 方法会被 webpack compiler 调用,并且在整个编译生命周期都可以访问 compiler 对象

const pluginName = 'ConsoleLogOnBuildWebpackPlugin';

class ConsoleLogOnBuildWebpackPlugin {
  apply(compiler) {
    compiler.hooks.run.tap(pluginName, (compilation) => {
      console.log('webpack 构建过程开始!');
    });
  }
}

module.exports = ConsoleLogOnBuildWebpackPlugin;

关于整个编译生命周期钩子,有如下:

  • entry-option :初始化 option
  • run
  • compile: 真正开始的编译,在创建 compilation 对象之前
  • compilation :生成好了 compilation 对象
  • make 从 entry 开始递归分析依赖,准备对每个模块进行 build
  • after-compile: 编译 build 过程结束
  • emit :在将内存中 assets 内容写到磁盘文件夹之前
  • after-emit :在将内存中 assets 内容写到磁盘文件夹之后
  • done: 完成所有的编译过程
  • failed: 编译失败的时候

4. 一些常见的 Plugin

  • HtmlWebpackPlugin :在打包结束后,⾃动生成⼀个 html ⽂文件,并把打包生成的 js 模块引⼊到该 html
  • DefinePlugin :允许在编译时(compile time)配置的全局常量
  • HotModuleReplacementPlugin: 启用模块热替换(Enable Hot Module Replacement - HMR)

webpack 模块(Modules)

1. 使用示例

  • ES2015 import 语句
  • CommonJS require() 语句
  • AMD definerequire 语句
  • css/sass/less 文件中的 @import 语句
  • stylesheet url(...) 或者 HTML <img src=...> 文件中的图片链接。

2. 模块是如何解析的 (Module Resolution)

resolver 是一个帮助寻找模块绝对路径的库。 一个模块可以作为另一个模块的依赖模块,然后被后者引用,如下:

import foo from 'path/to/module';
// 或者
require('path/to/module');

所依赖的模块可以是来自应用程序的代码或第三方库。 resolver 帮助 webpack 从每个 require/import 语句中,找到需要引入到 bundle 中的模块代码。 当打包模块时,webpack 使用 enhanced-resolve 来解析文件路径。

3. 模块解析规则

使用 enhanced-resolve,webpack 能解析三种文件路径:

  • 绝对路径
import '/home/me/file';
import 'C:\\Users\\me\\file';

由于已经获得文件的绝对路径,因此不需要再做进一步解析。

  • 相对路径
import '../src/file1';
import './file2';

在这种情况下,使用 importrequire 的资源文件所处的目录,被认为是上下文目录。在 import/require 中给定的相对路径,会拼接此上下文路径,来生成模块的绝对路径。

  • 模块路径
import 'module';
import 'module/lib/file';

resolve.modules 中指定的所有目录中检索模块。 你可以通过配置别名的方式来替换初始模块路径,具体请参照 resolve.alias 配置选项。

Hot Module Replacement + webpack-dev-server

1. webpack-dev-server

为我们提供了一个基本的 web server,并且具有 live reloading(实时重新加载) 功能

2. HMR 基本使用

启用 HMR 很容易,且在大多数情况下不需要任何配置。

...
devServer: {
  static: './dist',
  hot: true,
},
...
new webpack.HotModuleReplacementPlugin({
    // Options...
});

3. HMR 作用

模块热替换(HMR - hot module replacement)功能会在应用程序运行过程中,替换、添加或删除 模块,而无需重新加载整个页面。主要是通过以下几种方式,来显著加快开发速度:

  • 保留在完全重新加载页面期间丢失的应用程序状态。
  • 只更新变更内容,以节省宝贵的开发时间。
  • 在源代码中 CSS/JS 产生修改时,会立刻在浏览器中进行更新,这几乎相当于在浏览器 devtools 直接更改样式。

4. HMR + webpack-dev-server 工作原理

Webpack HMR.png

  1. 第一步,在 webpack 的 watch 模式下,文件系统中某一个文件发生修改,webpack 监听到文件变化,根据配置文件对模块重新编译打包,并将打包后的代码通过简单的 JavaScript 对象保存在内存中。
  2. 第二步是 webpack-dev-server 和 webpack 之间的接口交互,而在这一步,主要是 dev-server 的中间件 webpack-dev-middleware 和 webpack 之间的交互,webpack-dev-middleware 调用 webpack 暴露的 API对代码变化进行监控,并且告诉 webpack,将代码打包到内存中。
  3. 第三步是 webpack-dev-server 对文件变化的一个监控,这一步不同于第一步,并不是监控代码变化重新打包。当我们在配置文件中配置了devServer.watchContentBase 为 true 的时候,Server 会监听这些配置文件夹中静态文件的变化,变化后会通知浏览器端对应用进行 live reload。注意,这儿是浏览器刷新,和 HMR 是两个概念。
  4. 第四步也是 webpack-dev-server 代码的工作,该步骤主要是通过 sockjs(webpack-dev-server 的依赖)在浏览器端和服务端之间建立一个 websocket 长连接,将 webpack 编译打包的各个阶段的状态信息告知浏览器端,同时也包括第三步中 Server 监听静态文件变化的信息。浏览器端根据这些 socket 消息进行不同的操作。当然服务端传递的最主要信息还是新模块的 hash 值,后面的步骤根据这一 hash 值来进行模块热替换。
  5. webpack-dev-server/client 端并不能够请求更新的代码,也不会执行热更模块操作,而把这些工作又交回给了 webpack,webpack/hot/dev-server 的工作就是根据 webpack-dev-server/client 传给它的信息以及 dev-server 的配置决定是刷新浏览器呢还是进行模块热更新。当然如果仅仅是刷新浏览器,也就没有后面那些步骤了。
  6. HotModuleReplacement.runtime 是客户端 HMR 的中枢,它接收到上一步传递给他的新模块的 hash 值。
  7. 它通过 JsonpMainTemplate.runtime 向 server 端发送 Ajax 请求,服务端返回一个 json,该 json 包含了所有要更新的模块的 hash 值,获取到更新列表后,该模块再次通过 jsonp 请求,获取到最新的模块代码。
  8. 在该步骤决定 HMR 成功与否的关键步骤,HotModulePlugin 将会对新旧模块进行对比,决定是否更新模块,在决定更新模块后,检查模块之间的依赖关系,更新模块的同时更新模块间的依赖引用。
  9. 最后一步,当 HMR 失败后,回退到 live reload 操作,也就是进行浏览器刷新来获取最新打包代码。

webpack 工作流程

image.png

1. 初始化流程

从配置文件和 Shell 语句中读取与合并参数,并初始化需要使用的插件和配置插件等执行环境所需要的参数

(1)初始化参数(从配置文件和shell语句读取)

(2)开始编译 (从上一步得到参数初始化 Compiler 对象,加载插件,执行对象的 run 方法开始编译)

2. 编译构建流程

从 Entry 发出,针对每个 Module 串行调用对应的 Loader 去翻译文件内容,再找到该 Module 依赖的 Module,递归地进行编译处理

(1)确定入口 (根据配置的 entry 找出所有入口文件)

(2)编译模板 (从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块递归处理,直到入口文件的依赖都经过处理结束)

3.输出流程

对编译后的 Module 组合成 Chunk,把 Chunk 转换成文件,输出到文件系统

(1)输出资源 (根据依赖关系,组成 Chunk ,将其转化成单一文件输出)

(2)输出完成 (根据配置确定输出的路径和文件名,把内容写入到文件系统)

流行框架中webpack的应用

为了方便开发,它们都默认集成了webpack 常用的一些 loader 和 plugin

1. vue-cli

是基于nodejs+webpack封装的命令行工具,vue-cli 里面包含了webpack, 并且配置好了基本的webpack打包规则,原本需要自己配置webpack的相关配置,被cli简化了。并且按照vue的用户习惯整理了一套构建和目录规范。这样,你只要按照vue-cli的配置规则来,就可以满足很多繁琐的webpack+plugin配置

vue-cli-service serve 命令会启动一个开发服务器 (基于 webpack-dev-server) 并附带开箱即用的模块热重载 (Hot-Module-Replacement)。

2. create-react-app

npm run eject

会在根目录会生成 config 和 scripts 两个目录,并且 react-script 将不复存在,所有 react-script 的依赖都将注入到项目中。这就意味着复杂的 webpack 配置将交由我们自己管理

3. umijs

umi webpack

plugins:[
  ...
  HotModuleReplacementPlugin {
    options: {},
    multiStep: undefined,
    fullBuildTimeout: 200,
    requestTimeout: 10000
  }
]

与webpack类似的工具

在前端领域中,并非只有webpack这一款优秀的模块打包工具,还有其他类似的工具,例如Rollup、Parcel、snowpack,以及最近风头正盛的 Vite。

1. RollUp

Rollup 是一款 ES Modules 打包器,从作用上来看,Rollup 与 Webpack 非常类似。不过相比于 Webpack,Rollup 要小巧的多 现在很多我们熟知的库都都使用它进行打包,比如:Vue、React 和 three.js 等。

优点:

  • 代码效率更简洁、效率更高
  • 默认支持 Tree-shaking

缺点:

  • 加载其他类型的资源文件或者支持导入 CommonJS 模块,又或是编译 ES 新特性,这些额外的需求 Rollup 需要使用插件去完成
  • rollup 并不适合开发应用使用,因为需要使用第三方模块,而目前第三方模块大多数使用 CommonJs 方式导出成员,并且 rollup 不支持 HMR,使开发效率降低

2. Parcel

Parcel官网的定义就是极速零配置Web应用打包工具,它利用多核处理提供了极快的速度,并且不需要任何配置。

优点很多缺点也实在让人头痛:Parcel Vs Webpack

3. Snowpack

Snowpack,是一种闪电般快速的前端构建工具,专为现代Web设计,较复杂的打包工具(如Webpack或Parcel)的替代方案,利用JavaScript的本机模块系统,避免不必要的工作并保持流畅的开发体验

探讨不需要打包的构建工具 Snowpack

4. Vite

终于到我们的明星登场了!🔥🔥🔥

基于esbuild与Rollup,依靠浏览器自身ESM编译功能, 实现极致开发体验的新一代构建工具!

优点:

  • 快!快!非常快!!
  • 默认支持 Tree-shaking
  • 基于ESM急速热更新,无需打包编译
  • 基于esbuild的依赖预处理,比Webpack等node编写的编译器快几个数量级
  • 兼容Rollup庞大的插件机制,插件开发更简洁
  • 不与Vue绑定,支持React等其他框架,独立的构建工具
  • 内置SSR支持
  • 天然支持TS

缺点:

  • Vue仍为第一优先支持,量身定做的编译插件,对React的支持不如Vue强大
  • 生产环境集成Rollup打包,与开发环境最终执行的代码不一致
  • 目前市场上实践较少