从零入门 Vite 与 Webpack 对比

什么是构建工具

浏览器只认识 .html.css.js,这就意味着当项目中使用了其他类型文件时(比如:.ts.vue 等)需要将其转换成浏览器能识别的文件才能正常运行项目,例如以下这些场景:

  • .ts:需要安装 tsc 工具将 .ts 转化成 .js
  • .react / .vue:需要安装 react-compilervue- compiler,将 .jsx.vue 转化为 render() 函数(也就是 js代码)
  • .tsc:需要经过一系列编译 .tsc - .jsx - .js
  • .less / .scss:需要安装 less-loaderscss- loader 等一系列编译工具
  • 语法降级:babel 将一些先进的 es 语法转化为不同版本浏览器都可以接受的语法
  • 体积优化:需要对代码进行压缩(uglify.js 等)将项目压缩使其体积更小传输性能更高
  • 等等

构建工具不需要我们去手动一步一步处理这些编译过程,它会集成这些编译处理工具并完成这些流程,让开发人员只需要关注业务代码,代码经过构建工具处理后,就能给出最终的 .js

构建工具处理哪些工作

模块化开发支持

JavaScript 设计之初并没有包含模块的概念,基于越来越多的工程需要,为了使用模块化开发,JavaScript 社区涌现了多种模块标准(如 Node 端的 CommonJS)。直到2015年,发布了 ES6(ECMAScript 6.0),自此 JavaScript 语言才具备了模块这一特性(JavaScript模块),最新的浏览器开始原生支持模块功能了,这是一个好事情,浏览器能够最优化加载模块,使它比使用库更有效率,因为使用库通常需要做一些额外的客户端处理

先来看一个例子:

<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Document</title>
  </head>
  <body>
    <!-- 给script标签添加属性type为module,告诉其加载的不是一个普通的js文件,而是一个js模块 -->
    <script type="module">
		import _ from 'lodash-es'
		console.log('_', _)
	</script>
  </body>
</html>

安装 lodash

npm init -y
npm i lodash-es

浏览器打开 index.html,会有报错提示:

在这里插入图片描述

提示需要通过相对路径去引入,因为浏览器是不知道 node_modules 这个路径的,我们在项目开发中可以实现这样的引入是因为项目中的构建工具已经帮我们做了这种路径的判断处理和引入

问:既然都知道,直接引入库名的最佳实践就表示要去 node_modules 下去找这个库,那么为什么 ESModule 不将这种规范加入呢?
答:因为 CommonJS 最初只为服务端而设计,例如 Node.js 的实现中就采用了 CommonJS 标准的一部分,它可以不使用相对路径和绝对路径就能找到 node_modules,是因为服务端读取文件就是本地获取,但是如果 ESModule (也就是浏览器)也支持这种模块加载方式的话就表示浏览器需要通过网络请求来加载资源,另外每个 package 包还可能引入了很多额外的包,浏览器加载这些包都是需要网络请求的,这无疑是很大的一种性能消耗。这也就是为什么 ESModule 不敢将这种最佳实践的加载方案加入标准中,而是直接报错告诉你,我就是不支持这种引入方式。

多种模块化支持

浏览器是不支持 CommonJS 模块化代码的,所以如果在编写代码时使用 CommonJS 模块化方式引入库,比如:

const lodash = require('lodash-es')
console.log('_', lodash)

浏览器打开 index.html,依然会有错误提示:
在这里插入图片描述

提示浏览器不认识 require 这种语法,无法通过这种方式引入库,而经过构建工具的处理,实现了 CommonJS 等多种模块化的支持

语法处理代码兼容

在处理例如 .less.scss.jsx.vue 等这些浏览器无法识别的文件时,构建工具通过集成每种文件对应处理工具,可以流程化自动实现语法处理工作,同时针对不同浏览器的语法兼容问题通过 babel 语法降级工具的集成可以实现自动化代码兼容性处理

提高项目性能

经过处理后的项目经过打包处理,最终会生成可以被浏览器识别的文件类型,在整个打包过程中,构建工具可以压缩文件、代码分割、优化开发体验(例如:热更新、开发服务器解决跨域问题等)等

总结:构建工具可以让开发者不用每次编码后还要关心如何让代码在浏览器端运行,而只需要关注业务逻辑即可。

Vite相比Webpack

Webpack 就是这样一个构建工具,在浏览器支持 ES 模块之前,JavaScript 并没有提供原生机制让开发者以模块化的方式进行开发,上述构建工具需要处理的工作 Webpack 都可以帮我们完成。

但是,当我们开始构建越来越大型的应用时,需要处理的 JavaScript 代码量也呈指数级增长,因此开始遇到技术瓶颈 —— 使用 JavaScript 开发的工具通常需要很长时间才能启动开发服务器(项目跑起来),即使使用 HMR,文件修改后的效果也需要几秒钟才能在浏览器中反映出来。如此循环往复,迟钝的反馈会极大地影响开发者的开发效率和幸福感。

而 Webpack 无法通过改善自身来解决上述问题,为什么?我们从 Webpack 的原理进行说明。

Webpack 是一种多模块化支持的构建工具,例如:

const lodash = require('lodash') // commonjs 规范
import Vue from 'vue' // es6 module

// webpack允许上面的两种写法,webpack通过 AST 抽象语法分析工具,分析出所有的导入导出操作
// 通过启动的服务器最终将其转化成统一的浏览器可以识别的代码
const lodash = webpack_require('lodash')
const Vue = webpack_require('vue')

这就意味着 Webpack 需要一开始统一模块化代码(统一成 webpack_require 函数处理的模块化方式),这也就意味着需要将所有依赖文件都读取完。这也就是为什么当项目文件越来越多时,Webpack 需要读取的文件就越多,从而需要解析转化的文件也越多,打包(开发时候打包看不到因为是在内存中)过程就越长,导致启动开发服务器所需的时间也就越长。
因为需要打包构建的项目并不一定总是跑在浏览器端的项目,而运行在不同端的项目构建工具处理的模块化可能也不同,因此,Webpack 考虑更多的是兼容性

在这里插入图片描述

Vite 旨在利用生态系统中的新进展解决上述问题:浏览器开始原生支持 ES 模块。它是基于 ESModule 的,不允许 CommonJS 的代码,不需要去处理不同模块化的统一而遍历所有模块文件,而是按需加载的过程( index.html 通过esmodule 新特性可以直接去请求 main.js,再通过 main.js 中引入的模块 Vite 再通过开发服务器去按需加载这些模块 ),以原生 ES 模块方式提供源码,这实际上时让浏览器接管了部分打包程序的工作,Vite 只需要在浏览器请求源码时进行转换并按需提供源码,因此能快速启动开发服务器。

在这里插入图片描述

总结:

  • Vite 关注浏览器端的开发体验,Webpack 更多关注的是模块化兼容性
  • Vite 比 Webpack 快是因为它是先启动服务器的,而且不会一次将所有文件解析完,而 Webpack 在启动服务器之前需要先遍历解析所有文件

关于更多为何选择 Vite 构建工具的原因,官网也给出了更详情的说明

从0开始 Vite

Vite 是一种新型前端构建工具,能够显著提升前端开发体验,主要由两部分组成:

  • 一个开发服务器,它基于 原生 ES 模块 提供了 丰富的内建功能
  • 一个构建指令,它使用 Rollup 打包项目代码,并且是预配置的,可输出用于生产环境的高度优化过的静态资源

开箱即用

Vite 是开箱即用(out of box)的工具,不需要任何额外配置的情况下就可以帮你处理构建工作:

<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Vite</title>
</head>
<body>
  <script src="./main.js" type="module"></script>
</body>
</html>
// main.js
import {
    
    count} from './counter.js'
console.log('count', count)

// counter.js
export const count = 0

在没有使用构建工具的情况下如果想要直接运行 index.html 会因为 type="module" 给出跨域报错,具体原因及解决方法可以在 CommonJS,ES6 Module以及模块打包原理 一文中找到答案,在使用了插件以后打开 index.html 页面显示没有问题,这里不再赘述。

在一个 Vite 项目中,index.html 在项目最外层而不是在 public 文件夹内,是因为在开发期间 Vite 是一个服务器,这个 index.html 是该 Vite 项目的入口文件。通过解析 <script type="module" src="..."> 指向 JavaScript 源码已经 <link href> 指向的 CSS 源码。

下面在项目中引入 npm 包

npm init -y
npm i lodash-es
// main.js
import {
    
    count} from './counter.js'
import _ from 'lodash-es'
console.log('count', count)
console.log('lodash', _)

依然使用查看运行打开 index.html,浏览器报错提示:

在这里插入图片描述
报错原因在文章开头的例子中已经说过了,就是浏览器可不会主动去 node_modules 中加载资源,所以提示你要使用绝对路径或相对路径,下面通过引入 Vite 构建工具来解决这个问题:

// package.json
scripts: {
    
    
  'dev': 'vite'
}
npm i vite -D
npm run dev

在这里插入图片描述

浏览器打开 http://127.0.0.1:5173/

在这里插入图片描述

那 Vite 是如何找到 lodash 的呢?我们先来看一下网络请求,看 main.js 到底是从哪里加载到的 lodash 这个库:

在这里插入图片描述
在这里插入图片描述

可以看出,Vite 在处理的过程中,对非绝对路径或相对路径的引用,进行了补全路径的操作,而为什么可以去 /node_modules/.vite/deps 路径下呢?另外,Vite 是基于 ESModule 的,但是我们不能限制别人的库是采用的哪种模块化导出方式,那么当遇到 CommonJS 的包时会怎么办呢?

这就是 Vite 执行所谓的“依赖预构建”,这一步由 esbuild虽然 esbuild 快得惊人,但仍有一些重要功能例如代码分割和CSS处理等还在持续开发中,所以 Vite 打包还是会交给 Rollup 去做,因为它更灵活和成熟,但是不排除未来 esbuild 功能稳定后将其作为生产构建器的可能)执行,这也使得 Vite 的启动时间比任何基于 JavaScript 的打包器都要快得多,这个过程有两个目的:

  • 开发阶段 Vite 的开发服务器会将所有代码都视为原生 ES 模块,因此在执行 CommonJS 依赖时会执行智能导入分析,将其转换为 ESModule,然后放到当前目录下的 node_modules/.vite/deps 目录中
  • Vite 将有许多内部模块的 ESModule 依赖关系转换为单个模块,不管这个包本身有多少个 import,最终都会被 Vite 集成为一个或几个文件的形式,以提高后续浏览器网络请求的页面加载性能

针对上面第二点,通过下面这个例子进行说明,在引入 lodash-es (ESModule 规范的 lodash 包)模块依赖预构建和不依赖预构建不同情况下的网络请求:

依赖预构建:

在这里插入图片描述

不依赖预构建(这也是原生 ESModule 不敢支持 node_modules 的原因):

在这里插入图片描述

是否排除依赖预构建某个包可以通过 vite.config.js 中配置。关于更多 vite.config.js 的配置可以查看 Vite 基本配置及原理 这一篇。

Vite 开发服务器搭建原理

Vite 使用 Koa 或者 express 这种后端服务框架搭建了一个开发服务器,当我们执行 npm run dev 命令去启动这个开发服务器时,会提示我们访问 http://127.0.0.1:5173/ 这个服务地址打开项目,这就是我们的本地开发服务器,服务器启动后 node 服务器会去读取项目配置文件 vite.config.js

服务器端的路由对应的其实就是我们在浏览器端的请求地址,当我们请求 .html.js.vue 这些不同后缀名的文件时,Vite 这个开发服务器可以针对我们请求的路径对文件进行操作然后返回给前端,也就是浏览器。

而对于浏览器而言,任何后缀的文件都只是字符串而已,它只通过文件的 content-type 来判断使用哪种方式来读取这个文件,当访问的是 .html 文件时,设置 content-type: text/html 浏览器自然就会打开这个页面,这也就是为什么有时候我们打开一个下载链接时,浏览器会自动执行下载操作,也是因为后端返回给浏览器时设置了这个 content-type 值。

为什么 Vite 能让浏览器解析 .vue 文件就很好理解了,当请求 .vue 文件时,Vite 开发服务器会对里面的文件内容进行一个解析编译将其转化成 js 内容,并且在返回给浏览器时,设置了这个文件的 content-type: text/javascript,所以浏览器在访问这个 .vue 文件时,即使后缀名是 .vue,但其实浏览器不会看后缀名到底是什么,它只会按照 content-type 指定的类型也就是 JavaScript 脚本来执行它。

猜你喜欢

转载自blog.csdn.net/weixin_43443341/article/details/126941824