# Vite Client源码

## Vite

### vite介绍

vite是一种新型前端构建工具，它提供两部分功能：

1. 一个开发服务器，提供快速的模块热更新(HMR)
2. 一套构建指令，使用Rollup打包代码

vite相较于传统的构建工具(例如webpack)，最大的特点就是速度快，无论的冷启动还是热更新，速度都非常的快。

### 为什么vite那么快

vite的速度快体现在冷启动和热更新上。

#### 预构建

vite将代码分为源码和依赖两部分，源码就是用户开发的源代码，而依赖通常是引入的第三方包。

vite会对依赖执行依赖预构建，vite使用了esbuild来实现依赖预构建，并且会将结果缓存到`/node_modules/.vite/`目录下，只有一些特殊情况才会重新预构建(例如修改vite配置文件)。

预构建主要做了两件事，一是将CommonJs模块和UMD模块转换成ESM模块，二是重写裸模块地址。预构建可以加快页面的加载速度。

#### ESM

vite之所以要将CommonJs模块和UMD模块转换成ESM模块，这是因为vite自身的机制就是借助ESM来承担一部分打包工作。

首先在index.html中使用ESM，引入入口文件

```jsx
<script type="module" src="/src/main.tsx"></script>
```

浏览器会识别ESM，自动加载`main.tsx`文件，然后会将`main.tsx`中`import`的文件也加载了，这样就可以将所有依赖的文件都加载下来。

也就是说vite会通过ESM的形式提供源码，让浏览器承担了一部分打包工作，而vite只需要在这个过程中做一些处理工作，例如将非JavaScript文件内容转换成JavaScript能够理解的代码，转换导入文件的地址等。

采用这种方式很明显的一个好处是不需要vite自己去构建依赖关系，并且加载时只加载使用的依赖和源码，没有用到的依赖或源码不会被加载。

#### Http缓存

前面说过vite通过ESM加载文件，而在这个过程vite还借助了http缓存来实现加载优化。

前面说过vite会将应用中的模块分为两类：源码和依赖，对于依赖而言，基本上是不会变动的(例如你引入的组件库)，如果每个更新时都处理成本会很高，因此vite会对依赖模块使用强缓存`Cache-Control: max-age=31536000,immutable`。

对于源码而言，这部分可能是会经常改变的，因此会使用协商缓存`cache-control: no-cache`。

#### 冷启动

vite在开发环境下会启动一台node服务器来提供源码和进行其他操作，vite只会在浏览器请求源码时进行转换并按需提供源码，而对于不会使用到的源码则不会进行处理，而webpack则会将源码和依赖全部构建成bundle，因此启动时速度非常慢。

![https://cn.vitejs.dev/assets/bundler.37740380.png](https://cn.vitejs.dev/assets/bundler.37740380.png)

![https://cn.vitejs.dev/assets/esm.3070012d.png](https://cn.vitejs.dev/assets/esm.3070012d.png)

#### 热更新

在 Vite 中，HMR 是在原生 ESM 上执行的。当编辑一个文件时，Vite会将被编辑的模块重新导入(`import()`)，并且还借助http缓存优化模块加载策略，依赖使用了强缓存不会被重新加载，而源码也只会对更改过的文件进行重新加载，使得无论应用大小如何，HMR 始终能保持快速更新。

## Vite client原理分析

### vite组成

vite由client和node两部分组成，分别是客户端和服务器端。

### 热更新

vite的热更新是依靠websocket来实现的。vite其实可以看做是一台静态资源服务器，它会监听项目源码文件的改动，如果有文件改动了，则通过ws通知客户端执行对应的回调，回调中会通过`import()`+时间缀的方式重新加载改动后的代码文件。

```jsx
/* /src/client/client.ts */
// 创建websocket连接
const socketProtocol =
  __HMR_PROTOCOL__ || (location.protocol === 'https:' ? 'wss' : 'ws')
const socketHost = `${__HMR_HOSTNAME__ || location.hostname}:${__HMR_PORT__}`
const socket = new WebSocket(`${socketProtocol}://${socketHost}`, 'vite-hmr')
// 监听服务器消息，从而执行相应的回调
socket.addEventListener('message', async ({ data }) => {
  handleMessage(JSON.parse(data))
})
/* data的数据格式： {
  "type": "update",
  "updates": [
    {
      "type": "js-update",
      "timestamp": 1650103650496,
      "path": "/src/App.tsx",
      "acceptedPath": "/src/App.tsx"
    }
  ]
}
*/
```

type有以下几种值

* connected 初次连接时
* update 更新
* custom 自定义事件
* full-reload 全更新，即刷新页面，浏览器当前所在的html文件被更改时执行
* prune 更新后有模块不再被导入，则清除副作用
* error 出现错误时执行

我们通常更关心update的处理。

```jsx
async function handleMessage(payload: HMRPayload) {
  // ...
  switch (payload.type) {
    case 'update':
      notifyListeners('vite:beforeUpdate', payload)
      /*
      如果这是第一次更新，并且已经存在error overlay，则
      表示打开的页面存在现有服务器编译错误，并且整个
      模块脚本无法加载（因为其中一个模块导入为 500）。
      在这种情况下，正常更新将不起作用，需要完全重新加载。
      */
      if (isFirstUpdate && hasErrorOverlay()) {
        window.location.reload()
        return
      } else {
        clearErrorOverlay()  // 清除错误覆盖，就是出现错误时的提示界面
        isFirstUpdate = false
      }
      payload.updates.forEach((update) => {
        // update type只有两种：css-update和js-update
        // 其中css-update只针对被link标签引入的css
        if (update.type === 'js-update') {
          queueUpdate(fetchUpdate(update))
        } else {
          const { path, timestamp } = update
          const searchUrl = cleanUrl(path)
          // 不能使用`[href*=]`，因为href可能使用了相对路径
          // 这里需要使用link.href来获取完整路径
          const el = Array.from(
            document.querySelectorAll<HTMLLinkElement>('link')
          ).find((e) => cleanUrl(e.href).includes(searchUrl))
          if (el) {
            const newPath = `${base}${searchUrl.slice(1)}${
              searchUrl.includes('?') ? '&' : '?'
            }t=${timestamp}`
            el.href = new URL(newPath, el.href).href
          }
          console.log(`[vite] css hot updated: ${searchUrl}`)
        }
      })
      break
  }
```

css-update只针对通过link引入的css文件，加入是通过import导入css文件，那么仍然是通过js-update来更新。

可以看到vite拿到了update信息后是先执行了`fetchUpdate`函数，接着又执行了`queueUpdate`函数，接下来我们看一下这两个函数做了什么。

```jsx
async function fetchUpdate({ path, acceptedPath, timestamp }: Update) {
  // hotModulesMap保存了所有被加载的模块信息
  // mod：{ id: 文件地址, callbacks: [{ deps: 依赖文件路径数组, fn: 定义的回调函数 }] }
  const mod = hotModulesMap.get(path)
  if (!mod) {
    // In a code-splitting project,
    // it is common that the hot-updating module is not loaded yet.
    // https://github.com/vitejs/vite/issues/721
    return
  }

  // 模块信息
  const moduleMap = new Map()
  // 是否是自身更新
  const isSelfUpdate = path === acceptedPath

  // 借助Set的去重特性，确保每个依赖只被导入一次
  const modulesToUpdate = new Set<string>()
  if (isSelfUpdate) {
    // 如果是自身更新，则只需要将自身path添加即可
    modulesToUpdate.add(path)
  } else {
    // 依赖更新时
    for (const { deps } of mod.callbacks) {
      deps.forEach((dep) => {
        if (acceptedPath === dep) {
          modulesToUpdate.add(dep)
        }
      })
    }
  }

  // 在重新导入模块之前找出对应的回调
  const qualifiedCallbacks = mod.callbacks.filter(({ deps }) => {
    return deps.some((dep) => modulesToUpdate.has(dep))
  })

  await Promise.all(
    Array.from(modulesToUpdate).map(async (dep) => {
      const disposer = disposeMap.get(dep)
      if (disposer) await disposer(dataMap.get(dep))
      const [path, query] = dep.split(`?`)
      try {
        const newMod = await import(
          /* @vite-ignore */
          base +
            path.slice(1) +
            `?import&t=${timestamp}${query ? `&${query}` : ''}`
        )
        moduleMap.set(dep, newMod)
      } catch (e) {
        warnFailedFetch(e, dep)
      }
    })
  )

  return () => {
    // 执行回调
    for (const { deps, fn } of qualifiedCallbacks) {
      fn(deps.map((dep) => moduleMap.get(dep)))
    }
    const loggedPath = isSelfUpdate ? path : `${acceptedPath} via ${path}`
    console.log(`[vite] hot updated: ${loggedPath}`)
  }
}
```

```jsx
/**
 使用队列来确保以与发送加载时相同的顺序来执行fn
 */
async function queueUpdate(p) {
    queued.push(p);
    if (!pending) {
        pending = true;
        await Promise.resolve();
        pending = false;
        const loading = [...queued];
        queued = [];
        (await Promise.all(loading)).forEach((fn) => fn && fn());
    }
}
```

`fetchUpdate`借助`import()`实现了模块的重新导入，而`queueUpdate`则是调用了回调函数来更新渲染，这里使用了队列结构，因为http响应模块文件的顺序不一定就是模块的请求顺序，所以需要用队列确保以与发送加载时相同的顺序来重新渲染。

接下来我们看看浏览器通过ESM请求模块时，vite对模块文件做了什么处理。

我们创建一个vue文件，并导入一些模块

```jsx
import { ref } from "vue";
import { test } from "../utils/common";
```

刷新重新加载，可以在看到浏览器请求得到的文件是这样的：

```jsx
import {createHotContext as __vite__createHotContext} from "/@vite/client";
import.meta.hot = __vite__createHotContext("/src/components/HelloWorld.vue");
import {ref} from "/node_modules/.vite/deps/vue.js?v=4ffd74a8";
import {test} from "/src/utils/common.js";

// ...

function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
    return (_openBlock(),
    _createElementBlock(_Fragment, null, [_createElementVNode("h1", null, _toDisplayString($props.msg), 1 /* TEXT */
    ), _hoisted_1, _hoisted_2, _createElementVNode("button", {
        type: "button",
        onClick: _cache[0] || (_cache[0] = $event=>($setup.count++))
    }, "count is: " + _toDisplayString($setup.count), 1 /* TEXT */
    ), _hoisted_3], 64 /* STABLE_FRAGMENT */
    ))
}
_sfc_main.__hmrId = "469af010"
typeof __VUE_HMR_RUNTIME__ !== 'undefined' && __VUE_HMR_RUNTIME__.createRecord(_sfc_main.__hmrId, _sfc_main)
import.meta.hot.accept(({default: updated, _rerender_only})=>{
    if (_rerender_only) {
        __VUE_HMR_RUNTIME__.rerender(updated.__hmrId, updated.render)
    } else {
        __VUE_HMR_RUNTIME__.reload(updated.__hmrId, updated)
    }
}
)
```

可以看出

* vite服务器会对导入的路径进行转换
* vite在每个模块中都添加了`createHotContext`函数，并且将自身模块路径作为参数传入执行。
* vite通过`import.meta.hot.accept`导入了渲染该模块的方法

既然使用到了`createHotContext`函数，那我们就来看一下这个函数的定义。

```jsx
const hotModulesMap = new Map();
const disposeMap = new Map();
const pruneMap = new Map();
const dataMap = new Map();
const customListenersMap = new Map();
const ctxToListenersMap = new Map();
function createHotContext(ownerPath) {
    if (!dataMap.has(ownerPath)) {
        dataMap.set(ownerPath, {});
    }
    // 当文件更新时，创建一个新的上下文
    // 清除旧的回调
    const mod = hotModulesMap.get(ownerPath);
    if (mod) {
        mod.callbacks = [];
    }
    // 清除旧的自定义事件监听器
    const staleListeners = ctxToListenersMap.get(ownerPath);
    if (staleListeners) {
        for (const [event, staleFns] of staleListeners) {
            const listeners = customListenersMap.get(event);
            if (listeners) {
                customListenersMap.set(event, listeners.filter((l) => !staleFns.includes(l)));
            }
        }
    }
    const newListeners = new Map();
    ctxToListenersMap.set(ownerPath, newListeners);
    function acceptDeps(deps, callback = () => { }) {
        const mod = hotModulesMap.get(ownerPath) || {
            id: ownerPath,
            callbacks: []
        };
        mod.callbacks.push({
            deps,
            fn: callback
        });
        hotModulesMap.set(ownerPath, mod);
    }
    const hot = {
        get data() {
            return dataMap.get(ownerPath);
        },
        accept(deps, callback) {
            if (typeof deps === 'function' || !deps) {
                // self-accept: hot.accept(() => {})
                console.log('deps',deps);
                acceptDeps([ownerPath], ([mod]) => deps && deps(mod));
            }
            else if (typeof deps === 'string') {
                // explicit deps
                acceptDeps([deps], ([mod]) => callback && callback(mod));
            }
            else if (Array.isArray(deps)) {
                acceptDeps(deps, callback);
            }
            else {
                throw new Error(`invalid hot.accept() usage.`);
            }
        },
        acceptDeps() {
            throw new Error(`hot.acceptDeps() is deprecated. ` +
                `Use hot.accept() with the same signature instead.`);
        },
        dispose(cb) {
            disposeMap.set(ownerPath, cb);
        },
        // @ts-expect-error untyped
        prune(cb) {
            pruneMap.set(ownerPath, cb);
        },
        // TODO
        // eslint-disable-next-line @typescript-eslint/no-empty-function
        decline() { },
        invalidate() {
            // TODO should tell the server to re-perform hmr propagation
            // from this module as root
            location.reload();
        },
        // 监听自定义事件
        on(event, cb) {
            const addToMap = (map) => {
                const existing = map.get(event) || [];
                existing.push(cb);
                map.set(event, existing);
            };
            addToMap(customListenersMap);
            addToMap(newListeners);
        },
        // 发服务器发送自定义事件
        send(event, data) {
            messageBuffer.push(JSON.stringify({ type: 'custom', event, data }));
            sendMessageBuffer();
        }
    };
    return hot;
}
```

可以看到之前在`fetchUpdate`中用到的`hotModulesMap`是在这里定义的。`createHotContext`函数返回了一个hot对象，事实上这个就是模块中使用到的`import.meta.hot`对象，上面挂载了一些方法，其中比较重要的是`accept`方法，它的作用是当模块更新时执行对应的回调，在vite服务器返回模块文件时其实就会执行这个方法，回调函数就是重新渲染该模块的方法，因此当前面执行重新`import()`后，会调用模块的回调函数来渲染更新，此外accept也被暴露给开发者使用，开发者可以通过`import.meta.hot.accept()`来传入自定义回调函数。

综上，vite的热更新流程大致流程如下

1. vite服务器和浏览器建立websocket连接
2. vite服务器在返回模块文件时会创建热更新上下文，并将该模块的渲染方法传入到回调中
3. 服务器监听文件改动，如果有文件更新则将模块文件path、更新type等信息发给浏览器
4. 浏览器接收到后重新`import()`该模块文件，并执行重新渲染的回调方法


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://1425816423.gitbook.io/my-knowledge-base/yuan-ma-jie-xi/vite-yuan-ma-jie-xi/vite-client-yuan-ma.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
