Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

vite 源码解读 · 核心链路 #156

Open
hoperyy opened this issue May 28, 2022 · 0 comments
Open

vite 源码解读 · 核心链路 #156

hoperyy opened this issue May 28, 2022 · 0 comments

Comments

@hoperyy
Copy link
Owner

hoperyy commented May 28, 2022

本文发表在 https://github.com/hoperyy/blog

(0)概述

总体而言,vite 利用「中间件」和「暴露一系列生命周期的插件」,完成其主要功能。

「中间件」负责 server 端的对请求 url、文件修改、代码转译等方面的工作;「插件」通过暴露 vite 各个生命周期钩子,通过代码转译等操作,实现对 vite 流转过程的影响,从功能上它是 webpack plugin 和 loader 的结合体。

对源码的拆解是个相对繁杂的工程,我也还未对 vite 源码逐行分解,目前主要的解读方向是「主要工具」和「值得学习的技巧」。

(1)工程化

  • 限制只使用 pnpm 执行安装
    • "preinstall": "npx only-allow pnpm"

(2)cli

  • 创建命令行的工具 cac

(3)vite dev/serve

3.1 解析用户配置

  • 定义文件

    • vite/packages/vite/src/node/config.ts
  • 配置收集(resolveConfig)

    • configFile: 配置文件,默认读取根目录下的 vite.config.js 配置文件
    • envFile: 环境变量配置文件,默认读取根目录下的 .env 环境变量配置文件
    • root: 项目根目录,默认 process.cwd()
    • base: 类似于 webpack 中的 publicPath,资源的公共基础路径
    • server: 本地服务的配置,如 porthost
    • build: 构建产物配置
    • preview: 预览配置。执行 build 后,可以用 vite preview 执行预览
    • publicDir: 静态资源目录,用于放置不需要编译的静态资源,默认值是 public 目录
    • cacheDir: 缓存目录,用于存放预编译的依赖等,如 .vite/deps
    • mode: development | production,编译模式
    • define: 定义全局变量。不同的是,开发环境是定义在全局,生产环境是静态替换
    • plugins: 配置插件
    • resolve:
    • css:
    • json:
    • esbuild:
    • assetsInclude:
    • optimizeDeps: 依赖优化配置
    • ssr: ssr 相关配置
    • logLevel: 控制台日志级别,默认 info
    • clearScreen: 重复编译后,是否清除之前的日志,默认是 true
    • envDir: 加载 .env 环境变量配置文件的目录,默认是当前根目录
    • envPrefix: 环境变量的前缀,带前缀的环境变量会被注入项目
    • worker: 配置 bundle 输出类型、rollup 配置、plugins 等
  • 插件排序

    插件执行顺序: enforce: pre > normal > enforce: post

    export function sortUserPlugins(
      plugins: (Plugin | Plugin[])[] | undefined
    ): [Plugin[], Plugin[], Plugin[]] {
      const prePlugins: Plugin[] = []
      const postPlugins: Plugin[] = []
      const normalPlugins: Plugin[] = []
    
      if (plugins) {
        plugins.flat().forEach((p) => {
          if (p.enforce === 'pre') prePlugins.push(p)
          else if (p.enforce === 'post') postPlugins.push(p)
          else normalPlugins.push(p)
        })
      }
    
      return [prePlugins, normalPlugins, postPlugins]
    }
  • 处理 alias

    合并用户配置的 alias 配置。

  • 读取 env 环境变量文件

    loadEnv() 方法定义。

    依次读取 .env.{mode}.local / .env.{mode} / .env.local / .env

    默认返回一个空对象。

  • 初始化构建配置(build

    resolveBuildOptions() 方法。

  • 收尾

    • 在处理 publicDiroptimizeDeps 等后

3.2 启动本地服务 createServer

  • 定义文件

    • vite/packages/vite/src/node/server/index.ts
  • 收集 httpServer 相关配置

    • config 对象中拿到 config.server.https / config.cacheDir 等配置
    • 拿到上述配置后,用 resolveHttpsConfig() 生成 httpsConfig
  • config.server.middlewareMode 获取是否是 ssr

  • 利用 connect() 的中间件能力生成 middleware 容器

  • 如果不是 ssr 模式,启动 webSocket 服务

  • 文件监听 + 热重载

    • 利用 chokidar 对项目文件进行监听

      const watcher = chokidar.watch(path.resolve(root), {
          ignored: [
            '**/node_modules/**',
            '**/.git/**',
            ...(Array.isArray(ignored) ? ignored : [ignored])
          ],
          ignoreInitial: true,
          ignorePermissionErrors: true,
          disableGlobbing: true,
          ...watchOptions
      }) as FSWatcher
      watcher.on('change', async (file) => {
          file = normalizePath(file)
          if (file.endsWith('/package.json')) {
            return invalidatePackageData(packageCache, file)
          }
          // invalidate module graph cache on file change
          moduleGraph.onFileChange(file)
          if (serverConfig.hmr !== false) {
            try {
              await handleHMRUpdate(file, server)
            } catch (err) {
              ws.send({
                type: 'error',
                err: prepareError(err)
              })
            }
          }
        })

      handleHMRUpdate 触发编译更新文件

      handleHMRUpdate 会通过 webSocket 向客户端发送更新消息:

      • full-reload: 页面更新
      • update: 部分更新
  • 插件容器(pluginContainer): 用于执行各个插件的相关钩子

    // container.buildStart()
    if (!middlewareMode && httpServer) {
        let isOptimized = false
        // overwrite listen to init optimizer before server start
        const listen = httpServer.listen.bind(httpServer)
        httpServer.listen = (async (port: number, ...args: any[]) => {
          if (!isOptimized) {
            try {
              await container.buildStart({})
              initOptimizer()
              isOptimized = true
            } catch (e) {
              httpServer.emit('error', e)
              return
            }
          }
          return listen(port, ...args)
        }) as any
      } else {
        await container.buildStart({})
        initOptimizer()
      }

3.3 中间件

技巧

所有的中间件都返回了具名方法,便于 debug 调试

return function viteBaseMiddleware(req, res, next) { ... }

中间件列表

  • timeMiddleware
  • coresMiddleware
  • proxyMiddleware
  • baseMiddleware
  • lanchEditorMiddleware
  • viteHMRPingMiddleware
  • servePublicMiddleware
  • transformMiddleware
  • serveRawFsMiddleware
  • serveStaticMiddleware
  • spaFallbackMiddleware
  • hooks: configureServer
  • indexHtmlMiddleware
  • vite404Middleware
  • errorMiddleware

3.4 预构建依赖

  • createOptimizedDeps

(4)插件

4.1 生命周期定义

插件所有的生命周期钩子定义在 vite/packages/vite/src/node/plugin.ts

并且生命周期继承自 RollupPlugin 钩子。

4.2 生命周期钩子

  • name: 插件名称
    • rollup + vite
  • handleHotUpdate: 执行自定义 HMR(模块热替换)更新处理
    • vite
  • config: 在解析 Vite 配置前调用。可以自定义配置,会与 vite 基础配置进行合并
    • vite
  • configResolved: 在解析 Vite 配置后调用。可以读取 vite 的配置,进行一些操作
    • vite
  • configureServer: 用于配置开发服务器的钩子。最常见的用例是在内部 connect 应用程序中添加自定义中间件
    • vite
  • transformIndexHtml: 转换 index.html 的专用钩子
    • vite
  • options: 在收集 rollup 配置前,vite (本地)服务启动时调用,可以和 rollup 配置进行合并
    • rollup + vite
  • buildStart: 在 rollup 构建中,vite (本地)服务启动时调用,在这个函数中可以访问 rollup 的配置
    • rollup + vite
  • resolveId: 在解析模块时调用,可以返回一个特殊的 resolveId 来指定某个 import 语句加载特定的模块
    • rollup + vite
  • load: 在解析模块时调用,可以返回代码块来指定某个 import 语句加载特定的模块
    • rollup + vite
  • transform: 在解析模块时调用,将源代码进行转换,输出转换后的结果,类似于 webpack 的 loader
    • rollup + vite
  • buildEnd: 在 vite 本地服务关闭前,rollup 输出文件到目录前调用
    • rollup + vite
  • closeBundle: 在 vite 本地服务关闭前,rollup 输出文件到目录前调用
    • rollup + vite

4.3 内置插件

  • ensureWatch
  • metadata
  • preAlias
  • alias(@rollup/plugin-alias)
  • modulePreloadPolyfill
  • resolve
  • optimizedDeps
  • htmlInlineProxy (html)
  • css
  • esbuild
  • json
  • wasm
  • webWorker (worker)
  • asset
  • define
  • cssPost (css)
  • ssrRequireHook
  • workerImportMetaUrl
  • importMetaBlob
  • clientInjections
  • importAnalysis
  • dataUri
  • importAnalysisBuild
  • loadFallback
  • manifest
  • reporter
  • splitVendorChunks
  • terser

4.4 plugin-vue 插件详解

  • config

    config(config) {
      return {
        define: {
          __VUE_OPTIONS_API__: config.define?.__VUE_OPTIONS_API__ ?? true,
          __VUE_PROD_DEVTOOLS__: config.define?.__VUE_PROD_DEVTOOLS__ ?? false
        },
        ssr: {
          external: ['vue', '@vue/server-renderer']
        }
      }
    }

    定义了两个全局变量 __VUE_OPTIONS_API__ / __VUE_PROD_DEVTOOLS__

    设置了要为 SSR 强制外部化的依赖。

  • configResolved

    在 config 钩子执行完成后,下一个调用的是 configResolved 钩子。

    configResolved(config) {
      options = {
        ...options,
        root: config.root,
        sourceMap: config.command === 'build' ? !!config.build.sourcemap : true,
        cssDevSourcemap: config.css?.devSourcemap ?? false,
        isProduction: config.isProduction,
        devToolsEnabled:
          !!config.define!.__VUE_PROD_DEVTOOLS__ || !config.isProduction
      }
    }

    该钩子读取了 config 对象的 config.rootconfig.isProduction 配置插件内部配置项。

    另外,sourceMap 生成有一定的逻辑,本地开发环境会一直生成,生产环境会有条件判断。

  • configureServer

    configureServer(server) {
      options.devServer = server
    }

    只是将 vite 钩子中的 server 对象放到了内部配置项里。

  • buildStart

    buildStart() {
      options.compiler = options.compiler || resolveCompiler(options.root)
    }

    设置了 compiler 编译对象,优先使用用户配置的 compiler。

    内置的 compiler 生成逻辑位于: vite/packages/plugin-vue/src/compiler.ts

    export function resolveCompiler(root: string): typeof _compiler {
      // resolve from project root first, then fallback to peer dep (if any)
      const compiler =
        tryRequire('vue/compiler-sfc', root) || tryRequire('vue/compiler-sfc')
    
      if (!compiler) {
        throw new Error(
          `Failed to resolve vue/compiler-sfc.\n` +
            `@vitejs/plugin-vue requires vue (>=3.2.25) ` +
            `to be present in the dependency tree.`
        )
      }
    
      return compiler
    }

    可以看出,其用的是 compiler-sfc。

  • load

    load 钩子执行前,vite 已经启动了。

    此时用浏览器或其他方式访问 vite 在控制台提示的访问路径,如 http://localhost:3000/

    打开服务后,会触发 load 钩子执行。

    • 解析 id(也就是请求链接)

    • 过滤非 vue 请求

    • 解析目标 vue 文件(const descriptor = getDescriptor(filename, options)!

      getDescriptor 内部调用了 compiler.parse,也就是用 vue/compiler-sfc 解析出 vue 文件的 template / style / script 部分

  • transform

    在 load 返回了对应的代码片段后,进入到 transform 钩子。

    transform 做了 3 件事:

    • 转译 vue 文件
    • 转译以 vue 文件解析出的 template 模板
    • 转译以 vue 文件解析出的 style 代码

    transformMain 函数内部主要做了几件事情:

    • 解构 vue 文件的 script、template、style

    • 解析 vue 文件中的 script 代码

      内部使用 genScriptCode() 方法转译。

      会解析 scripts 中定义的变量、定义的 import,最终转译为

      import ..
      import ..
      expot default defineComponent({
          setup(__props, { expose }) {
          }
      })
    • 解析 vue 文件中的 template 代码

      内部使用 genTemplateCode() 方法转译。

      template 转译后,template 部分代码会通过 AST 解析、节点操作等,转译为:

      _createVNode($setup['input'], {
          value: $setup.todoText,
          class: 'todo-input',
          ...
      })
    • 解析 vue 文件中的 style 标签

      会解析为一段 import 'xxx.vue?vue&type=style&index=0&scoped=true&lang.less'

      接下来会触发请求,并由 transform 钩子的 transformStyle 方法处理样式的编译。

    • 解析 vue 文件中的 自定义模块 代码

    • 处理 HMR(模块热重载)的逻辑

    • 处理 ssr 的逻辑

    • 处理 sourcemap 的逻辑

    • 处理 ts 的转换,转成成 es

      内部通过 esbuild 完成 ts 到 es 的转译。

    最终,transform 会将 vue 的 template、script、style 解析为一段 es 的代码。

  • handleHotUpdate

    文件模块热重载。

    当 vue 文件修改后,会触发 handleHotUpdate 钩子。钩子内部会继续使用 compiler.parse 解析 vue 文件内容,并检测发生变更的部分。

    该钩子会返回一个变更模块的数组,交给 vite 处理。

(5)client

  • 从 server 获取消息类型
    • connected
    • update
    • custom
    • full-reload
    • prune
    • error

(6)日志

  • 分级
    • silent
    • error
    • warn
    • info
  • 技巧
    • 如何清空控制台 clearScreen

(7)打开浏览器

  • 定义在 vite/packages/vite/src/node/server/openBrowser.ts
  • 可以执行
    • js 脚本
    • 指定的浏览器路径的话(用 open 打开)
    • 未指定浏览器路径,并且是在 OS X 环境下,启用 AppleScript 脚本,尝试优先在当前页面刷新的方式打开 url

(8)参考资料

@hoperyy hoperyy changed the title vite 原理解析 vite 源码解析 May 30, 2022
@hoperyy hoperyy changed the title vite 源码解析 vite 源码解读 Jun 6, 2022
@hoperyy hoperyy changed the title vite 源码解读 vite 源码解读 · 核心链路 Jun 6, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant