文章目录
Vite
定义
面向现代浏览器的一个更轻,更快的web应用开发工具,基于ECMAScript标准原生模块系统(ES Modules)实现。
Vite (法语意为 “快速的”,发音 /vit/
) 是一种新型前端构建工具,能够显著提升前端开发体验,它主要由两部分组成:
- 一个开发服务器,它利用 原生 ES 模块 提供了 丰富的内建功能,如速度快到惊人的 模块热更新(HMR)。
- 一套构建指令,它使用 Rollup 打包你的代码,预配置输出高度优化的静态资源用于生产。
Vite 意在提供更开箱即用的配置,同时它的 插件 API 和 JavaScript API 带来了高度的可扩展性,并完全支持类型化
由来
如果应用比较复杂,使用Webpack的开发过程相对没有那么丝滑
- Webpack Dev Server冷启动时间比较长
- Webpack HMR 热更新的反应速度比较慢
对于冷启动和vite启动的区别,vite官网是这么说的(以下摘于官网):
当冷启动开发服务器时,基于打包器的方式是在提供服务前,急切地抓取和构建你整个应用
vite通过在一开始将应用中的模块区分为依赖和源码两类,改进了开发服务器启动时间。
-
依赖:大多为纯JavaScript 并在开发时不会变动。一些较大的依赖(例如有上百个模块的组件库)处理的代价也很高。依赖也通常会以某些方式(例如 ESM 或者 CommonJS)被拆分到大量小模块中。
Vite 将会使用 esbuild 预构建依赖。Esbuild 使用 Go 编写,并且比以 JavaScript 编写的打包器预构建依赖快 10-100 倍。
-
源码:通常包含一些并非直接是 JavaScript 的文件,需要转换(例如 JSX,CSS 或者 Vue/Svelte 组件),时常会被编辑。同时,并不是所有的源码都需要同时被加载。(例如基于路由拆分的代码模块)。
Vite 以 原生 ESM 方式服务源码。这实际上是让浏览器接管了打包程序的部分工作:Vite 只需要在浏览器请求源码时进行转换并按需提供源码。根据情景动态导入的代码,即只在当前屏幕上实际使用时才会被处理。
当基于打包器启动时,编辑文件后将重新构建文件本身。显然我们不应该重新构建整个包,因为这样更新速度会随着应用体积增长而直线下降。
一些打包器的开发服务器将构建内容存入内存,这样它们只需要在文件更改时使模块图的一部分失活[1],但它也仍需要整个重新构建并重载页面。这样代价很高,并且重新加载页面会消除应用程序的当前状态,所以打包器支持了动态模块热重载(HMR):允许一个模块 “热替换” 它自己,而对页面其余部分没有影响。这大大改进了开发体验 - 然而,在实践中我们发现,即使是 HMR 更新速度也会随着应用程序规模的增长而显著下降。
在 Vite 中,HMR 是在原生 ESM 上执行的。当编辑一个文件时,Vite 只需要精确地使已编辑的模块与其最近的 HMR 边界之间的链失效(大多数时候只需要模块本身),使 HMR 更新始终快速,无论应用程序的大小。
Vite 同时利用 HTTP 头来加速整个页面的重新加载(再次让浏览器为我们做更多事情):源码模块的请求会根据 304 Not Modified
进行协商缓存,而依赖模块请求则会通过 Cache-Control: max-age=31536000,immutable
进行强缓存,因此一旦被缓存它们将不需要再次请求。
一旦你体验到 Vite 有多快,我们十分怀疑你是否愿意再忍受像曾经那样使用打包器开发。
vite创建项目
Vite官方目前提供了一个比较简单的脚手架 create-vite-app
可以使用这个脚手架快速创建一个使用vite构建的vue.js应用
npm:
创建项目
npm init vite-app
启动项目
cd
npm run dev
yarn:
创建项目
yarn create vite-app
启动项目
cd
yarn dev
npm init 或者 yarn create 是这两个包管理工具提供的新功能,其内部就是自动去安装一个create-的模块(临时),然后自动执行这个模块中的bin
你还可以通过附加的命令行选项直接指定项目名称和你想要使用的模板。例如,要构建一个 Vite + Vue 项目,运行:
# npm 6.x
npm init @vitejs/app my-vue-app --template vue
# npm 7+, 需要额外的双横线:
npm init @vitejs/app my-vue-app -- --template vue
# yarn
yarn create @vitejs/app my-vue-app --template vue
支持的模板预设包括:
vanilla
vue
vue-ts
react
react-ts
preact
preact-ts
lit-element
lit-element-ts
对比差异点
打开生成的项目过后,你会发现是一个很普通的vue.js
应用,没有太多特殊的地方。
不过相比于之前的vue-cli
构建的项目或者是基于webpack
搭建的vue.js
项目,这里开发依赖非常简单,只有vite
和@vue/compiler-sfc
vite
就是我们今天要说的主角,而@vue/compiler-sfc
就是用来编译我们项目中.vue结尾的单文件组件(SFC
),它取代的就是vue.js 2.x
时使用的vue-template-compiler
再者就是Vite只支持Vue3.0版本 (实现原理了解以后,还可以改造vite支持vue2.0)
vite速度体验
viet两个子命令:
- serve:启动一个用于开发的服务器
- build:构建整个项目(上线)
当我们执行vite serve
的时候,你会发现响应速度非常快,几乎就是秒开
可能单独体验你不会有太明显的感觉,你可以对比使用vue-cli-serive
(内部还是webpack)启动开发服务器
当我们对比使用vue-cli-service serve
的时候,你会有更明显的感觉。
因为Webpack Dev Server在启动的时候,需要先build一遍,而build的过程是需要耗费很多时间的。
这类工具的做法是将所有模块进行提前编译,打包进bundle里,换句话说,不管模块是否被执行,都要被编译和打包进bundle里,随着项目越来越大,打包后的bundle也越来越大,打包的速度自然也就越来越慢
webpack启动过程:
而vite完全是不同的,当我们执行了vite serve
时,内部直接启动了Web server
并不会先编译所有的代码文件,仅仅是启动了web server
服务
vite启动过程:
对于vite来说,它利用现代浏览器原生支持ESM特性,省略了对模块的打包,对于需要编译的文件,vite采用的是另外一种模式:即时编译也就是说只有具体去请求某个文件时才会编译这个文件。
vite第一次启动时的编译
对于vite第一次启动的时候,vite也会进行一次简短的编译。这是由于我们的main.js
引入了vue
的模块,而vue的模块是存在于node_modules
中的,而node_modules
里面的模块会有个问题就是一个模块依赖另外一个模块,而我们一般不会去动里面的东西,vite会在node_modules\.vite_opt_cache
下生成一个依赖的缓存,而这个文件第一次被创建以后,只要配置没有发生变化是不会改变的,下次启动就会很迅速。
Vite 会将预构建的依赖缓存到
node_modules/.vite
。
它根据几个源来决定是否需要重新运行预构建步骤:
package.json
中的dependencies
列表- 包管理器的 lockfile,例如
package-lock.json
,yarn.lock
,或者pnpm-lock.yaml
- 可能在
vite.config.js
相关字段中配置过的
只有当上面的一个步骤发生变化时,才需要重新运行预构建步骤。
如果出于某些原因,你想要强制 Vite 重新绑定依赖,你可以用 --force
命令行选项启动开发服务器,或者手动删除 node_modules/.vite
目录。
vite的按需请求
vite启动的时候,会将项目的根目录会作为静态文件服务器的根目录。
vite构建的项目目录:
index.html:
index.html文件请求
我们请求的html
文件就是根目录下的index.html
文件,我们可以看到请求过来的index.html
文件中的script
标签的type
属性为module
这个就是ESM
定义的一个标准,一旦这么写的时候,那么它加载的时候,它可以使用ESM
的标准,这样就可以使用import
去组织模块化,而这块不是vite
所实现的,这是浏览器自带的。这样main.js
就可以去加载一些模块文件。
NPM 依赖解析和预构建:
原生 ES 引入不支持下面这样的裸模块导入:
import { someMethod } from 'my-dep'
上面的操作将在浏览器中抛出一个错误。Vite 将在服务的所有源文件中检测此类裸模块导入,并执行以下操作:
- 预构建 他们以提升页面重载速度,并将 CommonJS / UMD 转换为 ESM 格式。预构建这一步由 esbuild 执行,这使得 Vite 的冷启动时间比任何基于 javascript 的打包程序都要快得多。
- 重写导入为合法的 URL,例如
/node_modules/.vite/my-dep.js?v=f3sf2ebd
以便浏览器能够正确导入它们。
HMR 模块热重载
同样也是模式的问题,热更新的时候,Vite只需要立即编译当前所修改的文件即可,然后传递给页面,所以响应速度非常快
而Webpack修改某个文件过后,会自动以这个文件为入口重写build一次,所有涉及到的依赖也会被重新加载一边,所以反应速度会非常慢。
打包和不打包的问题
vite的出现,引发了另外一个值得我们思考的问题:究竟还有没有必要打包应用
之前我们使用Webpack打包应用代码,使之称为一个bundle.js,主要有两个原因:
- 浏览器环境并不支持模块化
- 零散的模块文件会产生大量的HTTP请求
随着浏览器对ES标准支持的逐渐完善,第一个问题已经慢慢不存在了,大多数浏览器都是支持ES modules的
零散模块文件确实会产生大量的HTTP请求,而大量的HTTP请求在浏览器端就会并发请求资源的问题
并行请求就会产生因为域名连接数超限而被挂起等待一段时间。
在HTTP 1.1的标准下,每次请求都是单独建立TCP连接,经过完整的通讯过程,非常耗时。而且每次请求除了请求体中的内容,请求头也会包含很多数据,大量请求会耗费很多的资源。而在HTTP2.0也不复存在了,但是vite为了兼容性,还是选择了打包的方式。
实现原理
vite的核心功能:Static Server + Compiler + HMR
核心思路
- 将当前项目目录作为静态文件服务器的根目录
- 拦截部分文件请求
- 处理代码中import node_modules中的模块
- 处理vue单文件组件(SFC)的编译
- 通过WebSocket实现HMR
手写实现vite
- 删除vite项目中的vite依赖
- 创建一个文件夹,里面创建js文件模拟实现vite
-
创建js文件和package.json文件
npm init
创建cli.js文件
给webpack.json文件中增加bin配置指向cli.js
-
编写cli.js文件.(等会vite启动就使用cli.js文件里面编写实现vite的代码。)
-
安装koa依赖
npm install koa --save // 类似express
npm install koa-send --save //用于项目目录作为静态文件服务器的根目录
-
简单配置koa先让服务跑起来,并且能够访问到目录下的index.html
#!/usr/bin/env node // 文件头,给linux系统用的,意思就是用node去执行当前这个文件脚本 // 1. 将当前项目目录作为静态文件服务器的根目录 // 用koa-send 去实现将当前目录作为静态文件服务器的根目录,因为它会有一些中间件。static封装的太彻底,没法简单修改 const send = require('koa-send'); // 这里web服务器用koa去做,vite也用的koa,类似express const Koa = require('koa'); const app = new Koa(); app.use(async (ctx, next) =>{ ctx.body = 'my-vite'; // 将项目目录作为静态文件服务器的根目录,并自动请求index.html文件 // 因为有的路径为不完整的路径,所以通过send的第三个参数做一些处理 await send(ctx, ctx.path, { root: process.cwd(), index: 'index.html'});// 有可能还要额外处理相应结果 await next(); }) app.listen(3080); console.log('Server-running-@-http://localhost:3080');
-
在删除vite依赖的项目中将
my-vite
(我们编写的实现vite)文件夹作为本地npm依赖进行安装,并更改启动命令配置npm install …/my-vite
此时
node_modules/.bin
文件夹下有了一个my-vite
的命令文件
更改package.json
配置
-
测试
这里页面请求成功了,但是报错:
Uncaught TypeError: Failed to resolve module specifier “vue”. Relative references must start with either “/”, “./”, or “…/”.
这就是因为请求的这个页面它里面调用了main.js
这个文件<script type="module" src="/src/main.js"></script>
这个文件里面又import
了vue
这个文件import { createApp } from 'vue'
,原生 ES 引入不支持下面这样的裸模块导入,所以它会报错,它会找不到这个模块。我们接下来可以使用Koa提供的中间件去处理这个问题。
HTML页面请求成功
分析这个报错的原因,因为我们一般开发当中,如果使用import { createApp } from 'vue'
, 我们是知道它会去项目目录里面的node_module/vue/package.json
文件里面去找对应的 module
配置,然后在dist
里面调相应的js文件。
所以如果人工去修改这个问题的话,只需要将dist下对应文件的路径配置在Import语句里即可,比如:import { createApp } from '../node_modules/vue/dist/vue.runtime.esm-bundler.js'
,当然这是不行的,我们更希望开发者能够更友好和简单的去引入模块
-
替换代码中的特殊位置
我们可以看到vite内部对于引入vue做的操作是将引入的目录改为
@modules/vue.js
我们也来实现这样的操作,我们可以简单用正则表达式来实现。
因为send
的时候,它已经把那个文件读出来了,而且将结果放在了ctx.body
当中,我们可以输出一下ctx.body
看一下,它输出了一个ReadStream
文件读取流。我们就要把这个读取流里面的内容都拿出来。这里我们可以用一个处理函数去处理一下,然后转成字符串
app.use(async (ctx,next) => {
console.log(ctx.body)
})
// 输出结果
ReadStream {
_readableState: ReadableState {
objectMode: false,
highWaterMark: 65536,
buffer: BufferList {
head: null, tail: null, length: 0 },
length: 0,
pipes: [],
flowing: null,
ended: false,
endEmitted: false,
reading: false,
sync: true,
needReadable: false,
emittedReadable: false,
readableListening: false,
resumeScheduled: false,
errorEmitted: false,
emitClose: true,
autoDestroy: false,
destroyed: false,
errored: null,
closed: false,
closeEmitted: false,
defaultEncoding: 'utf8',
awaitDrainWriters: null,
multiAwaitDrain: false,
readingMore: false,
decoder: null,
encoding: null,
[Symbol(kPaused)]: null
},
_events: [Object: null prototype] {
end: [Function (anonymous)],
error: [Function: bound onceWrapper] {
listener: [Function (anonymous)] }
},
_eventsCount: 2,
_maxListeners: undefined,
path: 'D:\\Iprogram\\programs\\vite\\vite-myself\\index.html',
fd: null,
flags: 'r',
mode: 438,
start: undefined,
end: Infinity,
autoClose: true,
pos: undefined,
bytesRead: 0,
closed: false,
[Symbol(kFs)]: {
appendFile: [Function: appendFile],
appendFileSync: [Function: appendFileSync],
access: [Function: access],
accessSync: [Function: accessSync],
chown: [Function