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.ts1
2
3
4declare module "*.svg" { const content: React.FC<React.SVGProps<SVGElement>>; export default content; }env.d.ts1
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 AppHTML 环境变量替换
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代理来实现避免同源策略的干扰的原理就是,设置一个本地服务器,将请求发往本地服务器,本地服务器再转发给远程服务器,从而避开跨域请求。核心为:服务器与服务器之间的请求不受浏览器同源策略的限制。










