小白必看,总结前端所有主流的构建工具,webpack / vite / roollup / esbuild,包含源码,建议关注+收藏
前端的构建工具有很多,随着技术的发展有些已经不是主流使用的了,请参考这篇文章,我们还是主要学习主流用的工具比如 webapck。但是在面试的时候,会问你各种工具的区别,所以还是要学习一下现在已经不常用的构建工具比较好,这样方便理解,比如 grunt 和 gulp。多学习一点总是没错的。
前言
本篇文章旨在总结前端常见的构建工具,构建工具是前端工程化中的重要的组成部分。
在实际项目中,我们初始化项目,一般是使用脚手架命令一键生成的,比如说使用 create-vue 初始化 vue 项目的时候,就会默认使用 vite 进行打包,同理使用 vue-cli 的时候就是默认使用webpack进行打包(现在vue已经不推荐使用这种方式了)
pnpm create vue@latest
使用脚手架初始化项目简单又方便,很多东西都是现成的,但是这就导致有的时候新手小白不会去认真看打包工具的官网,误以为诸如 webpack 和 vite 等打包工具只能和 vue/ react 等框架绑定使用。
所以我的建议是,可以跟着打包工具的官网,一步一步跟着来详细的学习一下,然后再看你用脚手架初始化的项目,有些东西和配置你才会豁然开朗。
还是需要强调一下,学习一个新的工具,首选的学习资料就是该工具的官网。官网的教程,系统而详细,不要一遇到问题就去百度/博客上搜,得到的结果往往是乱七八糟的。
一、准备工具
1.1 创建仓库
1.1.1 新建 gitee 仓库
我用 gitee 仓库纯粹是因为不方便科学上网,大家也可以使用 github
我的仓库地址是 learn-pack-build
1.1.2 克隆到本地
把你的项目克隆到本地,并新增 .gitignore 文件。
接下来我们就在这个项目中学习各种打包工具。
1.2 前端构建工具总结
前端的构建工具有很多,随着技术的发展有些已经不是主流使用的了,请参考这篇文章,我们还是主要学习主流用的工具比如 webapck。但是在面试的时候,会问你各种工具的区别,所以还是要学习一下现在已经不常用的构建工具比较好,这样方便理解,比如 grunt 和 gulp。多学习一点总是没错的。
我们把这些打包工具主要分为两类,一类是基于任务的打包工具(grunt 和 gulp),第二类是基于模块的打包工具(webpack、vite、esbuild、roollup、parcel、browserify),这 8 个的基础用法我们都要学习一下,才能够更好的理解。
注意,这些工具我们比较习惯称之为构建工具,而不是打包工具,因为打包工具听起来就只是一个简单的打包功能,但是构建工具有很多其他的功能,比如编译、测试、打包、优化、压缩等。还是称之为构建工具比较好。
二、基于任务的构建工具
对于基于任务的构建工具主要就了解以下两个就行,了解即可,知道它们为什么是基于任务的就行。
2.1 grunt
Grunt 是一个基于任务的构建工具,通过配置任务列表,实现前端项目的自动化构建和优化。Grunt 的任务通常是串行执行的,但是可以使用一些插件实现并行执行。
Grunt 是一个 JavaScript 任务运行器,它使用配置文件(通常是一个名为 Gruntfile.js 的文件)来定义任务。任务通常是一些插件的集合,用于执行各种操作,如文件的合并、压缩、编译等。Grunt 使用相对较多的配置,因此有些开发者可能觉得其配置较为冗长。
注意这个单词的发音【g ruang t】
我们可以跟着 grunt 的官网来学一下到底怎么用,恕我直言,我也没用过这个工具,但是跟着官网一步一步来,是可以很容易打包一个 js 文件的。
详细代码请参看项目的这个文件夹
重点是 Gruntfile.js 这个文件,在这个文件中【定义任务】—> 【注册任务】可以使用各种插件,grunt 有很多可以使用的插件,总之我们需要理解 grunt 是基于任务的构建工具。
2.2 gulp
Gulp 也是一个基于任务的构建工具,通过定义一系列任务,可以实现对前端项目的自动化构建,包括文件的合并、压缩、编译等。Gulp 原生就可以支持任务的串行、并行执行。
Gulp 则是另一个 JavaScript 任务运行器,它更加基于代码,使用流/管道(stream)来定义任务。开发者通过编写 JavaScript 代码来配置任务,这使得任务的定义更加灵活、简洁。Gulp 的流式处理方式通常被认为更加直观和易于理解。
g ao p
同样的我们跟着 gulp 的官网一步一步来实现一个简单的 demo。
详细代码请参看项目的这个文件夹
2.3 grunt 和 gulp 的区别
Grunt 和 Gulp 是 JavaScript 的构建工具,它们有很多相似之处,但也存在一些区别,主要集中在以下几个方面:
配置方式:
- Grunt 使用基于配置的方式来定义任务,通常在一个名为 Gruntfile.js 的文件中指定任务和任务的配置。这种方式需要开发者编写较多的配置代码。
- Gulp 使用基于代码的方式来定义任务,开发者可以编写 JavaScript 代码来配置任务,使用管道(streams)来处理文件。这种方式相对于 Grunt 更加直观和灵活。
API 和插件:
- Grunt 的插件系统相对成熟,拥有大量的插件可供使用,涵盖了很多常见的构建任务,例如文件合并、压缩、编译等。
- Gulp 也有丰富的插件生态,但由于其基于流的设计,使得编写自定义任务变得更加简单,因此有时候不需要借助插件也可以完成很多任务。
性能:
- Gulp 的流式处理方式使得任务可以并行执行,因此在处理大量文件时可能会比 Grunt 更快。
- Grunt 的任务通常是串行执行的,虽然可以通过一些插件实现并行执行,但默认情况下较为常见的是串行执行任务。
易用性和可读性:
- 由于 Gulp 的代码优先设计,任务的定义相对更加直观和简洁,使得代码更易读、易维护。
- Grunt 的配置方式可能会显得冗长和繁琐,对于一些开发者来说可能不太友好。
总的来说,Grunt 和 Gulp 在实现自动化构建方面都能很好地胜任,但在使用体验、性能和可读性等方面存在一些差异,开发者可以根据自己的偏好和项目需求选择合适的工具。
好了,基于任务的构建工具就了解 grunt 和 gulp 两个就行。接下来我们要学习一下重头戏,基于模块的构建工具,这个部分我也分为两组。一组是简单的,如 parcel、browserify、roollup、esbuild,一组是较复杂的 webpack 和 vite
三、简单的基于模块的构建工具
3.1 parcel
Parcel 是一个零配置的前端打包工具,可以自动识别项目中的文件,并进行相应的打包。它支持多种文件类型,支持热更新,并且具有快速的打包速度。
3.1.1 优点
- 零配置
- 运行后会启动一个开发服务器,支持热更新
- 支持自动安装依赖,webpack 开发阶段突然使用安装某个第三方依赖,必须终止开发服务器,parcel 不需要
- parcel 加载 css 等资源文件无需配置
- 打包过程时多进程同时工作的,速度快,输出文件会被压缩,样式代码会被单独提取到单个文件中
- 支持 tree-shaking
3.1.2 缺点
- 修改文件后和 webpack 一样需要重新构建,重新打包应用程序的整个 bundle,这一点 snowpack 和 vite 做了优化【snowpack 也是一种构建工具,本文没有详细说明】
注意这个零配置,给人的感觉好邪乎,好神奇,有这么好的事?不要怕,一切都是纸老虎
同样的我们跟着 parcel 官网来实现一个简单的 demo,你测试一下会发现确实很快,而且不用任何关于 parcel 的配置文件。
详细代码请参看项目的这个文件夹
我个人认为,如果你自己写一个学习的项目,不考虑性能,用这个打包是绝佳的首选,简单好上手,零配置真是强推。
但是如果在实际的项目中,我们就要考虑性能等各种方面,parcel 没有webpack 的社区活跃,功能强大。
parcel 2的官网里面显示也支持插件等,功能比1 强大很多,有兴趣可以试着学习一下(我没有兴趣~~)
3.2 browserify
Browserify 允许在前端使用类似于 Node.js 的 require 语法,将模块化的 JavaScript 文件打包成一个文件,以便在浏览器中使用,它打包后的文件可以直接在html 中引用。
模块化开发:Browserify 允许开发者使用模块化的方式组织 JavaScript 代码,使得代码更易于维护、重用和测试。通过
require()
来引入模块,可以将代码分割成多个文件,每个文件负责一个特定的功能,有助于降低代码耦合度。依赖管理:Browserify 自动处理模块之间的依赖关系,可以将各个模块打包到一个文件中,避免了手动管理依赖关系的繁琐工作。这简化了开发流程,提高了开发效率。
前端与后端代码共享:由于 Browserify 可以使用类似于 Node.js 的模块系统,在一定程度上实现了前端和后端代码的共享,使得开发者可以更容易地在前端和后端之间共享代码和逻辑,提高了代码复用性和一致性。
使用 npm 生态系统:Browserify 可以直接使用 npm 上的模块,这意味着您可以利用 npm 生态系统中丰富的第三方模块和工具来加速开发,无需额外的学习成本。
支持预处理器:除了 JavaScript 模块外,Browserify 还支持预处理器,如通过适当的插件,可以使用 CoffeeScript、TypeScript、Less 等语言,并将其编译成浏览器可执行的 JavaScript 代码。
如果你用过 webpack 等主流的构建工具,你会发现 browserify 有的功能,webpack 也有,因为 browserify 是先提出来的,所以有了webpack 之后就不咋用了。
看出来了,它的核心优势就是前端使用类似于 Node.js 的 require 语法
同样的我们跟着 browserify 的官网实现一个简单的demo,看看怎么回事。
详细代码请参看项目的这个文件夹,在这个demo中也是没有配置文件,只是单纯的使用 browserify 打包了一个 js 文件。
四、重要的基于模块的构建工具
这一章节会介绍 4 个重要的构建工具,分别是 rollup 、esbuild、webpack、vite,都是我们在开发过程中常用的,这四个构建工具的共同点有
- 都是基于模块的构建工具:都用于将多个 JavaScript 模块打包成一个或多个浏览器可执行的文件。它们可以处理不同类型的模块(例如 CommonJS、ES6 模块等)并将它们合并为一个输出文件
- 优化和压缩,都支持 tree-shaking:提供了代码优化和压缩的功能,以减少输出文件的大小并提高页面加载速度。它们可以去除未使用的代码(Tree-shaking)、进行代码压缩、合并文件等操作。
- 插件系统:Rollup、Webpack 和 Vite 都拥有丰富的插件系统,允许开发者根据需要扩展和定制构建过程。esbuild 本身并没有插件系统,但是它的速度快,可以作为其他工具的插件使用。
4.1 Rollup
Rollup 是一个 JavaScript 模块打包器,专注于打包 JavaScript 库。它能够进行 Tree-shaking,即删除未使用的代码,以减小打包后的文件体积,常用于构建库(library),特别是那些专注于 ES6 模块的库。
4.1.1 优点
- 代码效率更简洁、效率更高
- 默认支持 tree-shaking
- 常用于打包 js 库,如 vue 等,因为打包出来的代码更小,更快
4.1.2 缺点
- 加载其他类型文件 / 导入 cjs 模块/ 编译 es 新特性,都需要使用插件去完成
- 不适合开发应用,因为应用一般需要第三方模块,而第三方模块大多使用 commonjs 方式导出成员,「rollup需要用插件才能完成这个功能」
- 不支持热更新,开发效率低
JavaScript 库是啥?
JavaScript 库通常包含了一系列功能、组件或模块,用于在开发过程中提供特定的功能或解决特定的问题。vue 、react、jquery、lodash 等都属于 javascript库。就是我们使用 npm 命令去安装的都是 js 库。
在我们的项目中很少单独时候 rollup 打包,但是实际上我们常用的构建工具 vite 他的底层逻辑就是使用了 rollup, 所以还是有必要学习一下的。
可以跟着rollup 官网进行学习,rollup 支持配置文件,支持插件,但是不支持热更新。
详细代码请参看项目的这个文件夹。
如果你在自己学习一个新的技术,也可以是试着使用 rollup 进行打包。不过 rollup 不支持热更新,不如之前说的 parcel 方便。
4.2 esbuild
esbuild 是一个非常快速的 JavaScript 和 TypeScript 构建工具。它以极快的速度执行构建任务,并支持 Tree-shaking,即消除未使用的代码。esbuild 被设计成一个零配置的工具,但也提供了一些配置选项供用户进行调整。
esbuild 主打一个快速,但是不支持插件配置,也不支持热更新,他是 2019 年首次发布的,因此它相对较新。所以目前为止有些插件和 esbuild 并不兼容,这也就是 vite 的打包使用了 rollup 而没有用 esbuild 的原因,相对而言 rollup 有更活的插件 api 和基础建设。
一下内容来自 vite 官网
可以跟着esbuild 官网进行学习,esbuild 不支持插件,也不支持热更新。
详细代码请参看项目的这个文件夹。
4.2.1 为啥会快
那么问题来了,问什么esbuild会这么快?
用 Go 编写:esbuild 是用 Go 编写的,Go 是一种编译型语言,具有出色的性能和并发能力。esbuild 利用了 Go 的特性,通过并行编译和高效的资源管理,实现了快速的构建速度。
高效的算法和数据结构:esbuild 使用了一些高效的算法和数据结构来加速构建过程。例如,它使用了一种称为并行增量算法的技术,可以有效地处理多个模块并行编译,从而提高了构建的效率。
零分析的工作方式:esbuild 不会像其他工具那样对整个项目进行完整的分析,而是仅仅对被导入的模块进行编译。这种零分析的工作方式大大减少了构建过程中的计算量,从而提高了构建速度。
基于原生代码生成器:esbuild 采用了一种基于原生代码生成器的方法,可以直接生成高效的原生代码,而无需通过中间表示或额外的转换步骤。这样可以减少构建过程中的不必要的计算和内存开销,从而提高了构建的速度。
4.3 vite
Vite 是一种新型的前端构建工具,特别设计用于快速开发。Vite 支持使用原生 ES 模块作为开发环境,采用按需编译的方式,因此在开发过程中能够获得更快的冷启动速度。Vite 集成了 Vue.js,但也可以用于其他框架或库。
常见问题
vite 是我们常用的一个打包工具,有三个问题我们理解了,基本就涵盖了面试题中的问题,分别是
可以点进去看一下详细内容,有一点需要补充一下,这些内容在 vite 的官网实际上都有详细的描述,所以说官网是个好东西,建议大家都去看一遍官网,常看常新。
优点
- 利用 esm, 快速的冷启动
- 即时的热更新
- 动态导入,按需编译
缺点
- 功能不如 webpack 强大
接下来还是实现一个简单的demo,我不使用脚手架,也不用 vue 等框架,就自己写一个。
4.3.1 新建 vite 文件夹
新建 vite 文件夹后,并运行下面命令,跳转到该文件夹下面
cd vite
4.3.2 初始化项目
在 vite 文件夹下面,运行下面的命令
npm init -y
4.3.3 安装 vite
在 vite 文件夹下面,运行下面的命令
npm i vite -D
4.3.4 新建 index.html / index.js
index.html 作为入口文件,并在 index.html 中 引入 index.js
代码如下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h1>vite</h1>
<!-- 以模块的形式导入 index.js -->
<script type="module" src="./index.js"></script>
<script>
// 这里面可以得到name
console.log('outer name', name)
</script>
</body>
</html>
// index.js
export const name = 'vite'
console.log('inner name', name)
4.3.5 配置 dev 命令
在 package.json 中配置
"scripts": {
"dev": "vite"
},
4.3.6 运行项目
npm run dev
4.3.7 重点说明
在 vite 热更新的原理这篇文章,我们提到适用了 websocket 并且,在启动的时候会向客户端注入一个 js 文件。我们来看一下我们的项目中就可以看到 /@vite/client 文件。
可以自己在源代码中查看
4.3.8 增加配置
新增 vite.config.js ,可以根据官网进行各种配置,比如配置启动的端口号。
4.3.9 热更新
上面的这篇文章中提到了,vite 的热更新是使用了 websocket 原理,我们可以正好测试一下,启动项目的 demo,然后修改 html 的内容,会发现有 有些网络请求,可以自己看一下。
这部分代码在下面这个文件夹中,可以参考。
好了, vite 还是相对简单的构建工具,我们还剩一个最重要的重头戏 webpack,加油坚持就是胜利。
4.4. webpack
Webpack 是一个模块打包工具,它能够将各种资源(JavaScript、CSS、图片等)打包成一个或多个静态文件,以优化加载性能。
常见问题
webpack 是一个强大的构建工具,主要涉及到的重点问题有
- 对 webpack 的理解,webpack 有哪些功能?
- 说一下 webpack 的构建流程
- webpack 的热更新原理
- webpack 的 tree-shaking 原理
- webpack 的 loader 和 plugin 的区别,分别的实现思路
- webpack 常见的 loader 和 plugin
- 如何提高 webpack 的构建速度
- webpack 中的 module、bundle、chunk 分别是指什么
- webpack 中 hash、contenthash、chunkhash 的区别是什么
- webpack 中常用的生命周期
依次看完这 9 个问题,基本上就对 webpack 的理论知识有了一定的了解,现在我们还是跟着官网动手写一个 demo 吧。
4.4.1 新建 webpack 文件夹
新建 webpack 文件夹后,并运行下面命令,跳转到该文件夹下面
cd webpack
4.4.2 初始化项目
在 webpack 文件夹下面,运行下面的命令
npm init -y
4.4.3 安装 webpack 等
在 webpack 文件夹下面,运行下面的命令
npm i -D webpack webpack-cli webpack-dev-server html-webpack-plugin
【webpack-cli】Command Line Interface 是 Webpack 的命令行工具,用于在命令行中运行和配置 Webpack。
【webpack-dev-server】是 webpack 的开发服务器,用于开发中的热更新,自动编译等功能。
【html-webpack-plugin】是 webpack 的 html 插件,可以将打包后的 js 等资源文件挂载到 html中,进行预览、开发。
4.4.4 依次新建如下文件
都一些简单的代码,其中重要的配置是在 webpack.config.js 中,具体的可以在后面我的仓库中查看。
const HtmlWebpackPlugin = require('html-webpack-plugin')
const path = require('path')
module.exports = {
mode: 'development',
entry: {
// 配置多个入口文件,就会有多个 bundle 捆绑包,就要配置多个output
main: './main.js',
login: './login.js'
},
output: {
path: path.resolve(__dirname, 'dist'),
// 打包后的文件名,因为是多入口,所以要按照入口文件名区分
// 下面这个 name / chunkhash 都是是固定写法
// 这里面就涉及到著名的面试题 hash, chunkhash, contenthash 有什么区别,入口文件一般用 chunkhash
// 可以试下,不同的写法打包后有啥不同的效果
filename: 'bundle.[name].[hash].js',
// filename: 'bundle.[name].[chunkhash].js',
// filename: 'bundle.[name].[contenthash].js'
},
devServer: {
port: 3001
},
plugins: [
new HtmlWebpackPlugin({
// 打包后的文件名,这个文件名也可以用 hash, chunkhash, contenthash
// 但是如果用来哈希值,每次大包的名字不一样,在开发过程中的路由每次就会变不方便我们开发
filename: 'main.html',
template: 'main.html', //自己写的html文件
}),
new HtmlWebpackPlugin({
filename: 'login.html', //打包后的文件名
template: 'login.html', //自己写的html文件
}),
]
}
4.4.5 配置运行脚本
需要在 package.json 中配置开发和打包的脚本
4.4.5 运行项目
运行 npm run dev
访问 http://localhost:3001/login.html
我们在看一下 html 的内容,它把脚本加上了 defer,代表了脚本异步加载,加载之后等待 html 解析后才执行,注意 defer 和 async 的区别,请参考这篇文章。
4.4.5 hash / chunkhash / contenthash
光说不练假把式,我们还是实际是一下这三个单词的区别吧。
(1)hash
入口文件名,设置为【hash】,然后运行打包命名 npm run pack,你会发现,所有文件共用一个哈希值。
此时我们只对其中的一个 js 文件 login.js 做出改动,然后重新打包,你会发现重新生成了两个 js 文件,没有改动的 main.js 也重新打包了
所以说 hash, 是所有文件公用一个哈希值,一个文件改动,所有文件都会被重新打包,所有的文件名都会发生改变。
(2)chunkhash
我们再来看一下 chunkhash,首先把 dist 文件夹里面的内容清空,然后打包。
改变 login.js 的内容,重新打包,发现只新打包了 login
我们刚才是改动了 login.js ,现在我们改动 loginInfo.js ,login 引用了 loginInfo,【换言之,loginInfo.js 是 login.js 的依赖】再重新打包。
会发现也会重新打包,所以说,在入口文件的依赖发生改变时,也会重新打包,这是很好理解的,因为 webpack 会根据入口文件依次打包它的依赖文件整合到一个文件中,所以一个依赖发生变化,生成的资源文件肯定发生变化。
chunkhash 就很适合用来打包入口文件,这样每个入口文件以及依赖发生变化,只会重新打包发生变化的文件,而不会影响其他的入口文件。
(3)contenthash
我们修改一下项目,增加一个css 的插件,依次安装下面的插件
npm i css-loader mini-css-extract-plugin -D
【css-loader】使得能够在 js 中 import css 文件加载样式
【mini-css-extract-plugin】可以把 js 文件单独打包成一个文件,如果不适用这个插件,就会把 css的内容打包到引用它的 js 文件中。
新增 index.css ,并在 login.js 中引用
此时 webpack.config.js 的配置如下
现在我们配置的是 chunkhash,打包之后内容如下
修改 login.js ,注意修改的是js文件,重新打包,会发现 css 文件也重新打包,但是其实css 的文件没有做任何改动。
所以我们这个时候可以使用 contenthash,再来重复上面的操作。
所以说 contenthash 是只有在内容发生变化的时候,哈希值才会变化,一般用于打包 css 文件,这对于缓存很有用。
三个哈希值的对比,请参考这篇文章。
五、总结
本篇文章所有的代码都在这个仓库中,内容不是很多,所以没有特别详细的按照步骤提交,不过每个工具的代码都是独立的,有任何问题欢迎在评论区指正。
至此,基本上前端涉及到的流行的构建工具都已经自己写了一个 demo,其中 vite 和 webpack 的尤为重要,webpack 是高频面试题,所以还是需要好好看一下官网,理解一下原理。
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)