Vite基础知识总结
本文介绍了 Vite 的一些常用功能笔记,方便后续记忆与复习。总有种写这个不如直接看文档的感觉QAQ
Vite 工作基本原理
传统基于
js
的bundler based
构建工具的慢启动以及热更新缓慢的问题。- 原因:
- 开发服务器需要将所有模块全部打包后才能在浏览器中呈现。
- 原生
js
的性能问题。
- 总结:
bundler-based
在大型项目下性能低下的原因在于,其开发服务器原理必须先打包才能使用,即便是HMR
也是离不开打包这一环节,随着项目体积或者模块体积的增大,打包这一环节会耗费大量的时间。
- 原因:
Vite 的解决方案
- 将项目的代码分为两类:
dependencies
与source code
:- 依赖:即
node_modules
文件夹下的文件,不常修改的,项目的依赖文件,vite
采用使用go
编写的esbuild
来打包这些文件,提升开发体验。 - 源码:通常包括
ts
,tsx
,scss
等文件,对这些源码的提供是通过原生的ESM
来实现的,vite
只需要转换并按需提供代码,让浏览器接管了bundler
的工作,实现了源码文件的按需加载。
- 依赖:即
Vite 中 HMR 的更新方式以及浏览器缓存解决方案
- 基于 ESM 的 HMR 解决方案:Vite 只需要将被修改的模块与其最近的 HMR 边界之间的链路失活,再次请求相应的模块文件(PS:通过
fetch
实现)。 - 使用 HTTP 请求头来实现重新加载页面的相关文件的缓存设置,使用响应头:
Etag
与 HTTP 状态码304 Not Modified
来判断源码文件是否更新,对依赖文件设置响应头Cache-Control: max-age=31536000,immutable
进行强制缓存。
- 基于 ESM 的 HMR 解决方案:Vite 只需要将被修改的模块与其最近的 HMR 边界之间的链路失活,再次请求相应的模块文件(PS:通过
尽管原生 ESM 现在得到了广泛支持,但由于嵌套导入会导致额外的网络往返,在生产环境中发布未打包的 ESM 仍然效率低下(即使使用 HTTP/2)。为了在生产环境中获得最佳的加载性能,最好还是将代码进行 tree-shaking、懒加载和 chunk 分割(以获得更好的缓存)。
了解Vite HMR基本原理
Vite HMR
是通过 动态import
和WebSocket
实现的。Vite HMR
的原理是这样的:Vite
在node
端使用chokidar
监听文件的变化,当文件发生变化时,会触发HMR
事件,并将变化的文件名和模块 ID 发送给客户端。Vite
在浏览器端使用WebSocket
与服务端建立连接,接收HMR
事件。当收到HMR
事件时,会根据文件名和模块 ID 找到对应的模块,并使用动态import
重新请求该模块的代码,返回更新回调。Vite
在浏览器端使用原生的ES Module
功能加载模块,当模块的代码更新时,会触发模块的更新函数(执行回调),实现热替换。
详细了解协商缓存
举个例子,假设客户端第一次请求一个图片文件,服务器返回 200 OK 的状态码,以及图片的内容,同时在响应头中设置了
Last-Modified: Wed, 10 Nov 2021 07:00:51 GMT
和Etag: "1234567890"
,表示该图片的最后修改时间和唯一标识。客户端会将这些信息和图片一起缓存到本地。当客户端再次请求该图片时,会在请求头中添加If-Modified-Since: Wed, 10 Nov 2021 07:00:51 GMT
和If-None-Match: "1234567890"
,表示只有当图片在这个时间之后被修改过,或者图片的标识发生变化时,才需要重新获取图片。如果服务器检查发现图片没有变化,就会返回304 Not Modified
的状态码,不会返回图片的内容,客户端就可以直接使用本地缓存的图片。如果服务器检查发现图片有变化,就会返回200 OK
的状态码,以及新的图片内容,客户端就会更新本地缓存,并显示新的图片。- 将项目的代码分为两类:
Vite 快速上手
基本命令
1
2
pnpm create vite
pnpm create vite my-vue-app --template vue
社区维护的模板awesome-vite
Vite 的特性
NPM 依赖解析和预构建
浏览器不支持类似下面的bare module imports
1
import { someMethod } from "my-dep";
为了解决这个问题Vite
会完成两个工作
pre-bundle
:Vite
会使用esbuild
对这种类似的依赖进行打包。- 重写
url
:将url
重写为/node_modules/.vite/deps/my-dep.js?v=f3sf2ebd
这种格式方便浏览器识别引用(?v=f3sf2ebd
主要用来标识模块的版本,防止应依赖缓存导致的模块无法更新)。
客户端类型
由于Vite
的默认的类型定义为Node.js
环境下的API
,要补充到客户端的应用代码环境需要添加一个d.ts
声明文件
1
/// <reference types="vite/client" />
- 资源导入 (例如:导入一个
.svg
文件) import.meta.env
上Vite
注入的环境变量的类型定义import.meta.hot
上的HMR API
类型定义
需要覆盖默认的类型定义需要像下面这样做
vite-env-override.d.ts
1
2
3
4declare module "*.svg" { const content: React.FC<React.SVGProps<SVGElement>>; export default content; }
env.d.ts
1
2/// <reference types="./vite-env-override.d.ts" /> /// <reference types="vite/client" />
JSON 导入
对json
文件使用解构赋值能有效帮助tree-shaking
1
2
3
4
// 导入整个对象
import json from "./example.json";
// 对一个根字段使用具名导入 —— 有效帮助 treeshaking!
import { field } from "./example.json";
Glob 导入
Vite
支持使用特殊的import.meta.glob
函数从文件系统导入多个模块:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15const modules = import.meta.glob("./dir/*.js"); // 转译后 // vite 生成的代码 const modules = { "./dir/foo.js": () => import("./dir/foo.js"), "./dir/bar.js": () => import("./dir/bar.js"), }; // 遍历导入后的模块 for (const path in modules) { modules[path]().then((mod) => { console.log(path, mod); }); }
匹配到的文件默认是懒加载的,通过动态导入实现,并会在构建时分离为独立的
chunk
。如果你倾向于直接引入所有的模块(例如依赖于这些模块中的副作用首先被应用),你可以传入{ eager: true }
作为第二个参数1
2
3
4
5
6
7
8
9const modules = import.meta.glob("./dir/*.js", { eager: true }); // 转译后 // vite 生成的代码 import * as __glob__0_0 from "./dir/foo.js"; import * as __glob__0_1 from "./dir/bar.js"; const modules = { "./dir/foo.js": __glob__0_0, "./dir/bar.js": __glob__0_1, };
Glob 导入形式
import.meta.glob
都支持以字符串形式导入文件,类似于 以字符串形式导入资源。1
2
3
4
5
6
7
8
9
10
11const modules = import.meta.glob("./dir/*.js", { as: "raw", eager: true, }); // 转换后 // code produced by vite(代码由 vite 输出) const modules = { "./dir/foo.js": 'export default "foo"\n', "./dir/bar.js": 'export default "bar"\n', };
多个匹配模式
1
const modules = import.meta.glob(["./dir/*.js", "./another/*.js"]);
反面匹配模式
同样也支持反面
glob
匹配模式(以!
作为前缀)。若要忽略结果中的一些文件,你可以添加“排除匹配模式”作为第一个参数:1
2
3
4
5
6
7const modules = import.meta.glob(["./dir/*.js", "!**/bar.js"]); // 转译后 // vite 生成的代码 const modules = { "./dir/foo.js": () => import("./dir/foo.js"), };
具名导入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32const modules = import.meta.glob("./dir/*.js", { import: "setup" }); // 转译后的代码 const modules = { "./dir/foo.js": () => import("./dir/foo.js").then((m) => m.setup), "./dir/bar.js": () => import("./dir/bar.js").then((m) => m.setup), }; // eager 同时存在的时候进行 tree-shaking const modules = import.meta.glob("./dir/*.js", { import: "setup", eager: true, }); // 转译后 // vite 生成的代码 import { setup as __glob__0_0 } from "./dir/foo.js"; import { setup as __glob__0_1 } from "./dir/bar.js"; const modules = { "./dir/foo.js": __glob__0_0, "./dir/bar.js": __glob__0_1, }; // 设置 import 为 default 可以加载默认导出 const modules = import.meta.glob("./dir/*.js", { import: "default", eager: true, }); // vite 生成的代码 import __glob__0_0 from "./dir/foo.js"; import __glob__0_1 from "./dir/bar.js"; const modules = { "./dir/foo.js": __glob__0_0, "./dir/bar.js": __glob__0_1, };
自定义查询
你也可以使用
query
选项来提供对导入的自定义查询,以供其他插件使用。1
2
3
4
5
6
7
8const modules = import.meta.glob("./dir/*.js", { query: { foo: "bar", bar: true }, }); // vite 生成的代码 const modules = { "./dir/foo.js": () => import("./dir/foo.js?foo=bar&bar=true"), "./dir/bar.js": () => import("./dir/bar.js?foo=bar&bar=true"), };
你还需注意,所有
import.meta.glob
的参数都必须以字面量传入。你 不 可以在其中使用变量或表达式。
动态导入
1
const module = await import(`./dir/${file}.js`);
注意变量仅代表一层深的文件名。如果 file
是 foo/bar
,导入将会失败。对于更进阶的使用详情,你可以使用 glob
导入 功能。
静态资源处理
显式 URL 引入
1
2
import workletURL from "extra-scalloped-border/worklet.js?url";
CSS.paintWorklet.addModule(workletURL);
将资源引入为字符串
1
import shaderString from "./shader.glsl?raw";
公共基础路径
公共基础路径是指你的项目在部署时的根路径,它会影响到你的静态资源的引用和加载。例如,如果你的项目是部署在
https://example.com/my-app/
下,那么你的公共基础路径就是/my-app/
。
配置 base
项或者配置启动参数 vite build --base=/my/public/path/
。
环境变量与模式
环境变量
Vite
在一个特殊的 import.meta.env
对象上暴露环境变量。这里有一些在所有情况下都可以使用的内建变量:
import.meta.env.MODE
:{string}
应用运行的模式。import.meta.env.BASE_URL
:{string}
部署应用时的基本URL
。他由base
配置项决定。import.meta.env.PROD: {boolean}
应用是否运行在生产环境。import.meta.env.DEV: {boolean}
应用是否运行在开发环境 (永远与import.meta.env.PROD
相反)。import.meta.env.SSR: {boolean}
应用是否运行在server
上。
.env
文件
Vite
使用 dotenv
从你的 环境目录 中的下列文件加载额外的环境变量:
1
2
3
4
.env # 所有情况下都会加载
.env.local # 所有情况下都会加载,但会被 git 忽略
.env.[mode] # 只在指定模式下加载
.env.[mode].local # 只在指定模式下加载,但会被 git 忽略
为了防止意外地将一些环境变量泄漏到客户端,只有以VITE\_
为前缀的变量才会暴露给经过 vite
处理的代码。例如下面这些环境变量:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
VITE_SOME_KEY = 123;
DB_PASSWORD = foobar;
console.log(import.meta.env.VITE_SOME_KEY); // 123
console.log(import.meta.env.DB_PASSWORD); // undefined
// 还可以自定义前缀
// vite.config.js
export default defineConfig({
envPrefix: ["VITE_", "DZ_"], // 只暴露以VITE_或DZ_为前缀的环境变量
});
// .env
VITE_API_URL = "https://example.com/api";
DZ_APP_TITLE = "My App";
// 客户端源码
console.log(import.meta.env.VITE_API_URL); // https://example.com/api
console.log(import.meta.env.DZ_APP_TITLE); // My App
HTML 环境变量替换
Vite
还支持在 HTML
文件中替换环境变量。import.meta.env
中的任何属性都可以通过特殊的 %ENV_NAME%
语法在 HTML
文件中使用:
1
2
<h1>Vite is running in %MODE%</h1>
<p>Using data from %VITE_API_URL%</p>
CSS 变量的导入
css.preprocessorOptions
类型: Record<string, object>
指定传递给 CSS 预处理器的选项。文件扩展名用作选项的键。每个预处理器支持的选项可以在它们各自的文档中找到:
sass/scss
- 选项。less
- 选项。styl/stylus
- 仅支持define
,可以作为对象传递。
所有预处理器选项还支持 additionalData
选项,可以用于为每个样式内容注入额外代码。请注意,如果注入的是实际的样式而不仅仅是变量时,那么这些样式将会在最终的打包产物中重复出现。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export default defineConfig({
css: {
preprocessorOptions: {
scss: {
additionalData: `$injectedColor: orange;`,
},
less: {
math: "parens-division",
},
styl: {
define: {
$specialColor: new stylus.nodes.RGBA(51, 197, 255, 1),
},
},
},
},
});
给 Vite 设置 proxy 代理转发
vite proxy
代理中的proxyoptions
的对象的配置是用来设置代理服务器的规则和行为的。proxyoptions
的对象可以包含以下的属性:
target
: 一个字符串,表示要转发请求的目标服务器的地址,必须以http://
或https://
开头,例如'http://localhost:3000'
。changeOrigin
: 一个布尔值,表示是否修改请求头中的origin
字段,使其与目标服务器的域名一致。这样可以避免一些基于域名的虚拟主机或者跨域检查的问题。默认为false
。rewrite
: 一个函数,表示是否重写请求的路径,去掉或添加一些前缀。这样可以让后端服务器正确地处理请求,而不会出现404
或者其他错误。函数的参数是请求的路径,返回值是重写后的路径,例如path => path.replace(/^\/api/, '')
。ws
: 一个布尔值,表示是否支持WebSocket
代理。默认为false
。secure
: 一个布尔值,表示是否验证目标服务器的SSL
证书。默认为true
。headers
: 一个对象,表示要添加或修改的请求头。对象的键是请求头的名称,值是请求头的内容,例如{'User-Agent': 'Mozilla/5.0'}
。followRedirects
: 一个布尔值,表示是否跟随目标服务器的重定向。默认为false
。timeout
: 一个数字,表示代理请求的超时时间,单位是毫秒。默认为0
,表示无限制。logLevel
: 一个字符串,表示代理服务器的日志级别,可以是'silent','error','warn','info','debug','verbose'
之一。默认为'silent'
,表示不输出任何日志。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
export default defineConfig({
server: {
proxy: {
// 字符串简写写法:http://localhost:5173/foo -> http://localhost:4567/foo
"/foo": "http://localhost:4567",
// 带选项写法:http://localhost:5173/api/bar -> http://jsonplaceholder.typicode.com/bar
"/api": {
target: "http://jsonplaceholder.typicode.com",
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ""),
},
// 正则表达式写法:http://localhost:5173/fallback/ -> http://jsonplaceholder.typicode.com/
"^/fallback/.*": {
target: "http://jsonplaceholder.typicode.com",
changeOrigin: true,
rewrite: (path) => path.replace(/^\/fallback/, ""),
},
// 使用 proxy 实例
"/api": {
target: "http://jsonplaceholder.typicode.com",
changeOrigin: true,
configure: (proxy, options) => {
// proxy 是 'http-proxy' 的实例
},
},
// 代理 websockets 或 socket.io 写法:ws://localhost:5173/socket.io -> ws://localhost:5174/socket.io
"/socket.io": {
target: "ws://localhost:5174",
ws: true,
},
},
},
});
请注意,如果使用了非相对的 基础路径 base
,则必须在每个 key
值前加上该 base
。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// vite.config.js
export default {
base: "/app/",
server: {
proxy: {
// 以 /app/api 开头的请求,会被代理到 http://localhost:3000/api
"/app/api": {
target: "http://localhost:3000",
changeOrigin: true,
rewrite: (path) => path.replace(/^\/app/, ""),
},
// 以 /app/ws 开头的请求,会被代理到 ws://localhost:8080/ws
"/app/ws": {
target: "ws://localhost:8080",
ws: true,
},
// 以 /app/foo 开头的请求,会被代理到 http://example.com/foo
"/app/foo": "http://example.com/foo",
// 使用正则表达式匹配请求路径,以 /app/bar 开头的请求,会被代理到 http://localhost:8000/bar
"^/app/bar": {
target: "http://localhost:8000",
changeOrigin: true,
},
},
},
};
跨域请求是浏览器自己的行为,服务器与服务器之间的请求不存在跨域行为,所谓前端开发工具设置proxy
代理来实现避免同源策略的干扰的原理就是,设置一个本地服务器,将请求发往本地服务器,本地服务器再转发给远程服务器,从而避开跨域请求。核心为:服务器与服务器之间的请求不受浏览器同源策略的限制。