前言
随着Vue3的逐渐普及以及Vite的逐渐成熟,我们有必要来了解一下关于
vite
的本地构建原理。
对于
webpack
打包的核心流程是通过分析JS文件中引用关系,通过递归得到
整个项目的依赖关系
,并且对于非
JS
类型的资源,通过调用对应的
loader
将其打包编译生成
JS
代码,最后再启动开发服务器。
了解到
webpack
的耗时主要花费在打包上,
Vite
选择跳过打包,直接以
以
原生 ESM
方式提供源码
,这样岂不是可以非常快!
与webpack对比
在
Vite
官网有两张对比图能够非常直观的对比两者的区别。
这张图代表的是基于打包器的构建方式(webpack就是其中之一), 它在启动服务之前,需要从入口开始扫描整个项目的依赖关系,然后基于依赖关系构建整个应用生成bundle,最后才会启动开发服务器。 这就是这类构建方式为什么慢的原因,并且整个构建时间会随着项目的变大变的越来越长!
这张图代表的是基于
ES Module
的构建方式(比如:Vite),这张图是不是能够很直观说明为什么
Vite
会非常快,因为
它上来就直接启动开发服务器,然后在浏览器请求源码时进行转换并按需提供源码。根据情景动态导入代码,即只在当前页面上实际使用时才会被处理。
也就是它不需要扫描整个项目并且打包,不打包的话那它是如何让浏览器拿到分散在项目中的各个模块呢?
这一切都要得益于浏览器支持ESM的模块化方案,当浏览器识别到模块内的 ESM 方式导入的模块时,会自动去帮我们查找对应的内容
这就是为什么
vite
项目的模版文件中的
script
标签需要加上
type=module
,而
webpack
项目不需要。
<script type="module" src="/src/main.ts"></script>
vite快的原因
其实上面已经能够说明
vite
为什么会比
webpack
快了,但还有另外一个点在上图中并没有表现出来。
Vite
会在一开始将项目中的所有模块分为
源码
和
依赖
两类
- 源码 指的是我们自己写的代码,这类代码可能需要转换(例如 JSX,CSS 或者 Vue/Svelte 组件),并且时常会被编辑。Vite 会以 原生 ESM 方式提供源码,同时并不是所有的源码都需要同时被加载(例如基于路由拆分的代码模块)。
- 依赖 大多为在开发时不会变动的纯 JavaScript。一些较大的依赖(例如有上百个模块的组件库)处理的代价也很高。依赖也通常会存在多种模块化格式(例如 ESM 或者 CommonJS)。Vite 将会使用 esbuild预构建依赖。esbuild 使用 Go 编写,并且比以 JavaScript 编写的打包器预构建依赖快 10-100 倍。
总结来说就是:基于ESM模块化方案 + 预构建
使用预构建的原因
Vite
使用依赖预构建的原因主要有以下两点:
- 兼容CommonJS与UMD :在开发阶段中,Vite 的开发服务器将所有代码视为原生 ES 模块。因此,Vite 必须先将以 CommonJS 或 UMD 形式提供的依赖项转换为 ES 模块。
- 性能 :为了提高后续页面的加载性能,Vite将那些具有许多内部模块的 ESM 依赖项转换为单个模块。
可以来看个例子:
我们引入
lodash-es
工具包中的
debounce
方法,此时它理想状态应该是只发出一个请求
import { debounce } from 'lodash-es'
事实也是这样
但这是预构建的功劳,如果我们对
lodash-es
关闭预构建呢?
vite
配置文件加上如下代码,再来试试:
// vite.config.js
optimizeDeps: {
exclude: ['lodash-es']
}
可以看到,此时发起了600多个请求,这是因为
lodash-es
有超过 600 个内置模块!
vite通过将
lodash-es
预构建成单个模块,只需要发起一个HTTP请求!可以很大程度地提高加载性能
基本原理
跟着
debug
来一步一步看
vite
本地是如何工作的
首先从
package.json
出发,找到项目启动命令:
可以看到,
dev
对应的命令直接就是
vite
,然后我们再找到
node_modules
下面的
vite
下面的
bin
文件夹下面的
vite.js
文件,这就是
vite
运行的入口文件。
这里有一个
start
方法,从这打上断点开始慢慢往下走,就能够知道整个运行的基本原理
从上面我们知道,
vite
首先是会启动一个本地服务,基于该服务对文件的请求进行处理返回
接着往下走,我们可以看到有一个处理url的方法,此时运行栈里面的
address
变量也能够看到是
127.0.0.1:5173
,这就是我们等会要访问的本地服务,当然现在浏览器还什么也看不到,因为还没开始处理
/
路由,该路由需要返回一个html文件,也就是我们的模版文件(项目基于Vue3)
继续往下走,就可以看到有一个
applyHtmlTransforms
方法用来处理
html
文件并返回,可以看到当前请求的原始路径是
/
,返回的文件是项目根目录下的
index.html
文件
里面有一个脚本文件
<script type="module" src="/src/main.ts"></script>
,接下来就该请求并处理入口文件
main.ts
了
main.ts
文件如下:
import { createApp } from 'vue'
// import './style.css'
import { debounce } from 'lodash-es'
console.log('--lodash--', debounce)
import App from './App.vue'
createApp(App).mount('#app')
经过处理之后变成了:
它其实也没做啥处理,只是把依赖的引用路径处理成了
预构建
下的路径
(.vite/deps/)
,把
源码
的引用路径处理成了绝对路径。
?这里是不是会好奇,浏览器不是不能识别处理
vue
文件吗,这个不需要处理吗?(接着往下看!)
来看看此时浏览器中的加载顺序是怎样的吧:
整个文件的加载顺序是不是都对上了,注意看这个
App.vue
文件,虽然是
.vue
结尾,但文件类型依然是一个
JavaScript
文件
App.vue
经过编译后文件类型已经转成JS了!
App.vue
文件内容如下:
<script setup lang="ts">
import { ref } from 'vue'
const userName = ref('前端南玖')
</script>
<template>
<div class="user_name">{{ userName }}</div>
</template>
<style scoped>
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.vue:hover {
filter: drop-shadow(0 0 2em #42b883aa);
}
</style>
编译后:
再接着往下走,看下style被编译成了什么内容:
最后整个页面就可以渲染出来了!
总结
vite
整体思路:启动一个 connect 服务器拦截由浏览器请求 ESM的请求。通过请求的路径找到目录下对应的文件做一下编译最终以 ESM的格式返回给浏览器。
对于
node_modules
下面的依赖,
vite
会使用
esbuild
进行预构建,主要是为了兼容CommonJS与UMD,以及提高性能。
这样完整走一遍,是不是对
Vite
的理解又更深一步了,它实际上就是“走一步看一步”,不像
webpack
上来就扫描整个项目进行打包编译,所以
vite
的构建速度会比较快!
了解完
vite
工作原理,我们是不是可以来实现一个简易的
vite
工具!