美文网首页Vue源码分析
【源码】Vite源码分析,是时候弄清楚Vite的原理了

【源码】Vite源码分析,是时候弄清楚Vite的原理了

作者: ceido | 来源:发表于2022-07-12 23:17 被阅读0次

注:本篇是组内PPT分享内容,说的部分会比较多。弄成文章后还未整理,后续整理一下。

0. 源码目录结构

https://github.com/vitejs/vite

image.png image.jpeg

1. 源码入口

1.1 运行npm run dev后发生了什么?

先准备一个vite脚手架生成的项目vite-vue3,当运行npm run dev后发生了什么?在package.json中看到运行的是vite命令。

vite命令是在哪里注册的呢,在node_modules/vite/package.json中查看bin字段。

"bin" 字段的作用是能让我们在命令窗口全局输入命令执行。可以看到 vite 命令:

  "bin": {
    "vite": "bin/vite.js"
  }

打开vite.js看到,其中主要运行的是

function start() {
  require('../dist/node/cli')
}

这里的/dist/node/cli.js是打包后的文件,可能有点长,可以配合vite打包前的源码一起阅读。

在/dist/node/cli.js中,首先使用 cac 命令工具处理用户的输入。

cli

然后命令执行的是createServer,这个createServer./chunks/dep-55830a1a.js引入,这里也是打包后的代码,比较长。

1.2 createServer

我们找到了前面大概执行的顺序后,这里回到源码,在 packages/vite/src/node/server/index.ts 里面找到createServer

export async function createServer(
    inlineConfig: InlineConfig = {}
): Promise<ViteDevServer> {
    // Vite 配置整合
    const config = await resolveConfig(inlineConfig, 'serve', 'development')
    const root = config.root
    const serverConfig = config.server

    // 创建http服务
    const httpServer = await resolveHttpServer(serverConfig, middlewares, httpsOptions)

    // 创建ws服务
    const ws = createWebSocketServer(httpServer, config, httpsOptions)

    // 创建watcher,设置代码文件监听
    const watcher = chokidar.watch(path.resolve(root), {
        ignored: [
            '**/node_modules/**',
            '**/.git/**',
            ...(Array.isArray(ignored) ? ignored : [ignored])
        ],
        ...watchOptions
    }) as FSWatcher

    // 创建server对象
    const server: ViteDevServer = {
        config,
        middlewares,
        httpServer,
        watcher,
        ws,
        moduleGraph,
        listen,
        ...
    }

    // 文件监听变动,websocket向前端通信
    watcher.on('change', async (file) => {
        ...
        handleHMRUpdate()
    })

    // 服务 middleware
    middlewares.use(...)
    
    // optimize: 预构建
    await initDepsOptimizer(config, server)
    
    // 监听端口,启动服务
    httpServer.listen = (async (port, ...args) => { ... })
    
    return server
}

可以看到 createServer 做了很多事情,上面列举主要的几个:
resolveConfig:整合配置(resolvePlugins)
注册各种中间件(indexHtml、transformMiddleware、static)
HMR:使用chokidar监听文件的修改
optimizeDeps:预构建
创建httpServer,启动服务
...

image

2. 预构建

2.1 no-bundle vs bundle

怎么理解no-bundle?不打包?


bundle no-bundle
2.2 为什么要预构建?

1、Vite是基于浏览器原生支持ESM的能力实现的,要求用户的代码模块必须是ESM模块,因此必须将commonJs的文件提前处理,转化成 ESM 模块并缓存入 node_modules/.vite

2、减少模块和请求数量。例如,lodash-es 有超过 600 个内置模块。当我们执行 import { debounce } from 'lodash-es' 时,浏览器同时发出 600 多个 HTTP 请求!尽管服务器在处理这些请求时没有问题,但大量的请求会在浏览器端造成网络拥塞,导致页面的加载速度相当慢。
通过预构建 lodash-es 成为一个模块,我们就只需要一个 HTTP 请求了!

2.3 预构建的核心流程

预构建的核心流程,包括缓存判断、依赖扫描、依赖打包和元信息保存这四个主要的步骤。

image (5).png

关于预构建的实现代码都在optimizeDeps函数当中,在仓库源码的 packages/vite/src/node/optimizer/index.ts 查看 optimizeDeps:

image

optimizeDeps首先调用loadCachedDepOptimizationMetadata获取node_modules/.vite/_metadata.json中的元信息。

image (7).png

然后调用getDepHash,这个函数是读取目录下的package-lock.json的内容,然后将文件内容进行hash得到一个hash值。

然后两个hash进行对比是否相等。
也就是说,预编译就是看node_modules的包有没有变化,如果不相等。会调用scanImports去扫,在scanImports中,得到入口文件后,对入口文件进行了解析,当然,具体的解析过程在依赖扫描阶段的 Esbuild 插件(esbuildScanPlugin)中得以实现。


这里就会使用esbuild.build去编译文件,其中esbuildDepPlugin就是打包的插件:

生成出来保存到.vite下。最后,执行writeFile,再将相关信息保存到_metadata.json

3. 核心编译流程

webpack中plugin和loader的区别?

3.1 Rollup插件机制

Rollup 的打包过程中,会定义一套完整的构建生命周期,从开始打包到产物输出,中途会经历一些标志性的阶段,并且在不同阶段会自动执行对应的插件钩子函数(Hook)。
Vite 的插件机制是基于 Rollup 来设计的。Vite 模拟了 Rollup 的插件机制,设计了一个 PluginContainer 对象来调度各个插件。
PluginContainer 的 实现 基于借鉴于 WMR 中的rollup-plugin-container.js,主要分为 2 个部分:

1、实现 Rollup 插件钩子的调度
2、实现插件钩子内部的 Context 上下文对象

PluginContainer的定义了一系列执行plugin的方法。如buildStart、resolveId、load、transform。

3.2 vite插件的接口定义

packages/vite/src/node/plugin.ts:

export interface Plugin extends RollupPlugin {
  enforce?: 'pre' | 'post'
  apply?: 'serve' | 'build' | ((config: UserConfig, env: ConfigEnv) => boolean)
  config?: (
    config: UserConfig,
    env: ConfigEnv
  ) => UserConfig | null | void | Promise<UserConfig | null | void>
  configResolved?: (config: ResolvedConfig) => void | Promise<void>
  configureServer?: ServerHook
  configurePreviewServer?: PreviewServerHook
  transformIndexHtml?: IndexHtmlTransform
  handleHotUpdate?(
    ctx: HmrContext
  ): Array<ModuleNode> | void | Promise<Array<ModuleNode> | void>
  resolveId?(
    this: PluginContext,
    source: string,
    importer: string | undefined,
    options: {
      custom?: CustomPluginOptions
      ssr?: boolean
      /**
       * @internal
       */
      scan?: boolean
    }
  ): Promise<ResolveIdResult> | ResolveIdResult
  load?(
    this: PluginContext,
    id: string,
    options?: { ssr?: boolean }
  ): Promise<LoadResult> | LoadResult
  transform?(
    this: TransformPluginContext,
    code: string,
    id: string,
    options?: { ssr?: boolean }
  ): Promise<TransformResult> | TransformResult
}

3.3 当浏览器一个JS请求到vite服务时,发生了什么?

例如:
<script type="module" src="/src/main.js"></script> 或者 import { get } from './utils';

// main transform middleware
  middlewares.use(transformMiddleware(server))

可以看到pluginContainer会执行插件中的钩子。对于不同的资源会有不同的插件去处理。
vite的内置插件:


路径解析插件(packages/vite/src/node/plugins/resolve.ts)
路径解析插件(即vite:resolve)是 Vite 中比较核心的插件,几乎所有重要的 Vite 特性都离不开这个插件的实现,诸如依赖预构建、HMR、SSR 等等。

CSS 编译插件(packages/vite/src/node/plugins/css.ts)

import分析插件(packages/vite/src/node/plugins/importAnalysis.ts)
重写import语句,如import Vue from 'vue';导入路径会重写为预构建文件夹的路径;
注入HMR客户端脚本。

Esbuild 转译插件(packages/vite/src/node/plugins/esbuild.ts)
用来进行 .js、.ts、.jsx和tsx,代替了传统的 Babel 或者 TSC 的功能,这也是 Vite 开发阶段性能强悍的一个原因。

4. HMR流程

打包工具实现热更新的思路都大同小异:主要是通过WebSocket创建浏览器和服务器的通信监听文件的改变,当文件被修改时,服务端发送消息通知客户端修改相应的代码,客户端对应不同的文件进行不同的操作的更新。

浏览器文件是几时被注入的?在importAnalysis插件中:

      if (hasHMR && !ssr) {
        debugHmr(
          `${
            isSelfAccepting
              ? `[self-accepts]`
              : acceptedUrls.size
              ? `[accepts-deps]`
              : `[detected api usage]`
          } ${prettyImporter}`
        )
        // inject hot context
        str().prepend(
          `import { createHotContext as __vite__createHotContext } from "${clientPublicPath}";` +
            `import.meta.hot = __vite__createHotContext(${JSON.stringify(
              importerModule.url
            )});`
        )
      }

5. 源码运行演示

6. 参考

Vite中文网

深入理解Vite核心原理 - 掘金

相关文章

网友评论

    本文标题:【源码】Vite源码分析,是时候弄清楚Vite的原理了

    本文链接:https://www.haomeiwen.com/subject/bxbnbrtx.html