本文介绍了 Vite 的一些常用功能笔记,方便后续记忆与复习。总有种写这个不如直接看文档的感觉QAQ

Vite 工作基本原理

  1. 传统基于jsbundler based构建工具的慢启动以及热更新缓慢的问题。

    • 原因:
      • 开发服务器需要将所有模块全部打包后才能在浏览器中呈现。
      • 原生js的性能问题。
    • 总结:bundler-based在大型项目下性能低下的原因在于,其开发服务器原理必须先打包才能使用,即便是HMR也是离不开打包这一环节,随着项目体积或者模块体积的增大,打包这一环节会耗费大量的时间。
  2. Vite 的解决方案

    • 将项目的代码分为两类:dependenciessource code
      • 依赖:即node_modules文件夹下的文件,不常修改的,项目的依赖文件,vite采用使用go编写的esbuild来打包这些文件,提升开发体验。
      • 源码:通常包括tstsxscss等文件,对这些源码的提供是通过原生的ESM来实现的,vite只需要转换并按需提供代码,让浏览器接管了bundler的工作,实现了源码文件的按需加载。
    • Vite 中 HMR 的更新方式以及浏览器缓存解决方案

      • 基于 ESM 的 HMR 解决方案:Vite 只需要将被修改的模块与其最近的 HMR 边界之间的链路失活,再次请求相应的模块文件(PS:通过fetch实现)。
      • 使用 HTTP 请求头来实现重新加载页面的相关文件的缓存设置,使用响应头:Etag与 HTTP 状态码304 Not Modified来判断源码文件是否更新,对依赖文件设置响应头Cache-Control: max-age=31536000,immutable进行强制缓存。

    尽管原生 ESM 现在得到了广泛支持,但由于嵌套导入会导致额外的网络往返,在生产环境中发布未打包的 ESM 仍然效率低下(即使使用 HTTP/2)。为了在生产环境中获得最佳的加载性能,最好还是将代码进行 tree-shaking、懒加载和 chunk 分割(以获得更好的缓存)。

    了解Vite HMR基本原理

    Vite HMR 是通过 动态importWebSocket 实现的。Vite HMR 的原理是这样的:

    • Vitenode 端使用 chokidar 监听文件的变化,当文件发生变化时,会触发 HMR 事件,并将变化的文件名和模块 ID 发送给客户端。
    • Vite 在浏览器端使用 WebSocket 与服务端建立连接,接收 HMR 事件。当收到 HMR 事件时,会根据文件名和模块 ID 找到对应的模块,并使用动态import重新请求该模块的代码,返回更新回调。
    • Vite 在浏览器端使用原生的 ES Module 功能加载模块,当模块的代码更新时,会触发模块的更新函数(执行回调),实现热替换。
    详细了解协商缓存

    举个例子,假设客户端第一次请求一个图片文件,服务器返回 200 OK 的状态码,以及图片的内容,同时在响应头中设置了 Last-Modified: Wed, 10 Nov 2021 07:00:51 GMTEtag: "1234567890",表示该图片的最后修改时间和唯一标识。客户端会将这些信息和图片一起缓存到本地。当客户端再次请求该图片时,会在请求头中添加 If-Modified-Since: Wed, 10 Nov 2021 07:00:51 GMTIf-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-bundleVite会使用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.envVite 注入的环境变量的类型定义
  • import.meta.hot 上的 HMR API 类型定义

需要覆盖默认的类型定义需要像下面这样做

  1. vite-env-override.d.ts

    1
    2
    3
    4
    declare module "*.svg" {
      const content: React.FC<React.SVGProps<SVGElement>>;
      export default content;
    }
  2. 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 导入

  1. Vite 支持使用特殊的 import.meta.glob 函数从文件系统导入多个模块:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    const 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
    9
    const 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,
    };
  2. Glob 导入形式

    import.meta.glob 都支持以字符串形式导入文件,类似于 以字符串形式导入资源。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const 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',
    };
  3. 多个匹配模式

    1
    const modules = import.meta.glob(["./dir/*.js", "./another/*.js"]);
  4. 反面匹配模式

    同样也支持反面 glob 匹配模式(以 ! 作为前缀)。若要忽略结果中的一些文件,你可以添加“排除匹配模式”作为第一个参数:

    1
    2
    3
    4
    5
    6
    7
    const modules = import.meta.glob(["./dir/*.js", "!**/bar.js"]);
    
    // 转译后
    // vite 生成的代码
    const modules = {
      "./dir/foo.js": () => import("./dir/foo.js"),
    };
  5. 具名导入

    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
    const 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,
    };
  6. 自定义查询

    你也可以使用 query 选项来提供对导入的自定义查询,以供其他插件使用。

    1
    2
    3
    4
    5
    6
    7
    8
    const 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`);

注意变量仅代表一层深的文件名。如果 filefoo/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代理来实现避免同源策略的干扰的原理就是,设置一个本地服务器,将请求发往本地服务器,本地服务器再转发给远程服务器,从而避开跨域请求。核心为:服务器与服务器之间的请求不受浏览器同源策略的限制