Vite源码解析(1)

979 阅读4分钟

工欲善其事,必先利其器---debug

第一步 用tsc编译带有sourcemapvite源码

进入源码packages/vite目录,执行tsc -p src/nodetsc -p src/client,生成带有sourmap的源码。

第二步 配置vscodenodejsdebug

这一步的原理就是用vscode自带的debug功能,执行vite serve 项目路径这句命令。在.vscode目录中的lanuch.json中配置

{
      "type": "pwa-node",
      "request": "launch",
      "name": "Launch Program",
      "skipFiles": [
        "<node_internals>/**"
      ],
      "args": [
        "serve",
        "/Users/apple/Documents/learn/vite/vue-ts",
        "--open"
      ],
      "program": "${workspaceFolder}/bin/vite.js"
    }
  ]

这么做的好处是可以从头开始debug

第三步 开始断点

在源文件中想要断点的任何打上断点,点击绿色箭头就可以开始debug了。

image.png

vscode debug简单使用方法

vscode debug一共分5个功能模块,变量,监视,调用堆栈,已载入脚本和断点。

变量就是当前断点所在的上下文的变量。

监视可以输入自定义的变量表达式。

调用堆栈,显示当前执行到的行从近到远的执行函数,这样可以很方便的厘清代码执行逻辑。

已载入脚本用的不多。

断点就是你所打的所有的断点,可以手动取消,还可以在任何一个断点上输入逻辑表达式,只有当这个表达式为真,才会进入断点。

我们无法debug client的代码。

整体结构

关键的源码src目录结构:

缩略目录结构

.
├── client
└── node
    ├── __tests__
    ├── optimizer
    ├── plugins
    ├── server
    │   ├── __tests__
    │   │   └── fixtures
    │   │       ├── none
    │   │       │   └── nested
    │   │       ├── pnpm
    │   │       │   └── nested
    │   │       └── yarn
    │   │           └── nested
    │   └── middlewares
    └── ssr
        └── __tests__

主要分成两部分,clientnode

主要的是node部分。

这部分又分为optimizer优化功能模块,plugins插件功能模块,server服务器模块和ssr服务端渲染功能模块。

整体运行逻辑

vite创建的vue模板作为调试项目,入口文件:bin/vite.js

关键逻辑代码

function start() {
  require('../dist/node/cli')
}
​
if (profileIndex > 0) {
  process.argv.splice(profileIndex, 1)
  const next = process.argv[profileIndex]
  if (next && !next.startsWith('-')) {
    process.argv.splice(profileIndex, 1)
  }
  const inspector = require('inspector')
  const session = (global.__vite_profile_session = new inspector.Session())
  session.connect()
  session.post('Profiler.enable', () => {
    session.post('Profiler.start', start)
  })
} else {
  start()
}

调用node/cli中的逻辑代码

// dev
cli
  .command('[root]') // default command
  .alias('serve')
  .option('--host [host]', `[string] specify hostname`)
  .option('--port <port>', `[number] specify port`)
  .option('--https', `[boolean] use TLS + HTTP/2`)
  .option('--open [path]', `[boolean | string] open browser on startup`)
  .option('--cors', `[boolean] enable CORS`)
  .option('--strictPort', `[boolean] exit if specified port is already in use`)
  .option('-m, --mode <mode>', `[string] set env mode`)
  .option(
    '--force',
    `[boolean] force the optimizer to ignore the cache and re-bundle`
  )
  .action(async (root: string, options: ServerOptions & GlobalCLIOptions) => {
    // output structure is preserved even after bundling so require()
    // is ok here
    const { createServer } = await import('./server')
    try {
      const server = await createServer({
        root,
        base: options.base,
        mode: options.mode,
        configFile: options.config,
        logLevel: options.logLevel,
        clearScreen: options.clearScreen,
        server: cleanOptions(options) as ServerOptions
      })
      await server.listen()
    } catch (e) {
      createLogger(options.logLevel).error(
        chalk.red(`error when starting dev server:\n${e.stack}`)
      )
      process.exit(1)
    }
  })

调用server模块中的createServer方法,得到一个server实例,调用实例的listen方法,启动服务器。

所以最核心的代码就是cerateServer方法。

createServer核心逻辑

createServer最终返回的是一个ViteDevServer

export interface ViteDevServer {
  /**
   * The resolved vite config object
   */
  config: ResolvedConfig
  /**
   * A connect app instance.
   * - Can be used to attach custom middlewares to the dev server.
   * - Can also be used as the handler function of a custom http server
   *   or as a middleware in any connect-style Node.js frameworks
   *
   * https://github.com/senchalabs/connect#use-middleware
   */
  middlewares: Connect.Server
  /**
   * @deprecated use `server.middlewares` instead
   */
  app: Connect.Server
  /**
   * native Node http server instance
   * will be null in middleware mode
   */
  httpServer: http.Server | null
  /**
   * chokidar watcher instance
   * https://github.com/paulmillr/chokidar#api
   */
  watcher: FSWatcher
  /**
   * web socket server with `send(payload)` method
   */
  ws: WebSocketServer
  /**
   * Rollup plugin container that can run plugin hooks on a given file
   */
  pluginContainer: PluginContainer
  /**
   * Module graph that tracks the import relationships, url to file mapping
   * and hmr state.
   */
  moduleGraph: ModuleGraph
  /**
   * Programmatically resolve, load and transform a URL and get the result
   * without going through the http request pipeline.
   */
  transformRequest(
    url: string,
    options?: TransformOptions
  ): Promise<TransformResult | null>
  /**
   * Apply vite built-in HTML transforms and any plugin HTML transforms.
   */
  transformIndexHtml(
    url: string,
    html: string,
    originalUrl?: string
  ): Promise<string>
  /**
   * Util for transforming a file with esbuild.
   * Can be useful for certain plugins.
   */
  transformWithEsbuild(
    code: string,
    filename: string,
    options?: EsbuildTransformOptions,
    inMap?: object
  ): Promise<ESBuildTransformResult>
  /**
   * Load a given URL as an instantiated module for SSR.
   */
  ssrLoadModule(url: string): Promise<Record<string, any>>
  /**
   * Fix ssr error stacktrace
   */
  ssrFixStacktrace(e: Error): void
  /**
   * Start the server.
   */
  listen(port?: number, isRestart?: boolean): Promise<ViteDevServer>
  /**
   * Stop the server.
   */
  close(): Promise<void>
  /**
   * @internal
   */
  _optimizeDepsMetadata: DepOptimizationMetadata | null
  /**
   * Deps that are externalized
   * @internal
   */
  _ssrExternals: string[] | null
  /**
   * @internal
   */
  _globImporters: Record<
    string,
    {
      module: ModuleNode
      importGlobs: {
        base: string
        pattern: string
      }[]
    }
  >
  /**
   * @internal
   */
  _isRunningOptimizer: boolean
  /**
   * @internal
   */
  _registerMissingImport: ((id: string, resolved: string) => void) | null
  /**
   * @internal
   */
  _pendingReload: Promise<void> | null
}

按照代码执行顺序,这个函数做了这些事:

  1. resolveConfig,解析用户在命令行中输入的配置,生成以下类型的config
export type ResolvedConfig = Readonly<
  Omit<
    UserConfig,
    'plugins' | 'alias' | 'dedupe' | 'assetsInclude' | 'optimizeDeps'
  > & {
    configFile: string | undefined
    configFileDependencies: string[]
    inlineConfig: InlineConfig
    root: string
    base: string
    publicDir: string
    command: 'build' | 'serve'
    mode: string
    isProduction: boolean
    env: Record<string, any>
    resolve: ResolveOptions & {
      alias: Alias[]
    }
    plugins: readonly Plugin[]
    server: ResolvedServerOptions
    build: ResolvedBuildOptions
    assetsInclude: (file: string) => boolean
    logger: Logger
    createResolver: (options?: Partial<InternalResolveOptions>) => ResolveFn
    optimizeDeps: Omit<DepOptimizationOptions, 'keepNames'>
  }
>
  1. Connect模块创建一个服务器
  2. chokidar创建文件监听功能
  3. 调用插件configureServer钩子
  4. 根据条件创建timeMiddlewarecorsMiddlewareproxyMiddlewarebaseMiddlewarelaunchEditorMiddlewareviteHMRPingMiddlewaredecodeURIMiddlewareservePublicMiddlewaretransformMiddlewareserveRawFsMiddlewareserveStaticMiddlewarehistoryindexHtmlMiddlewarevite404MiddlewareerrorMiddleware中间件。

项目的所有请求

只抓主线逻辑,详细逻辑后面再述。

第一个请求

我们在server/index.ts中加入自己的一个中间件,捕获每一个请求来debug

middlewares.use((req, res, next) => {
  next()    
})

如果req.url === '/' 会被history中间件重写为/index.html。之后会被indexHtmlMiddleware中间件捕获,然后做一些处理之后返回项目的入口文件index.html

返回过来的源文件内容:

<!DOCTYPE html>
<html lang="en">
<head>
<script type="module" src="/@vite/client"></script>
  <meta charset="UTF-8">
  <link rel="icon" href="/favicon.ico" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta name="referrer" content="no-referrer" />
  <title>Vite App</title>
</head>
<body>
  <div id="app">hello1</div>
  <script type="module" src="/src/main.ts"></script>
</body>
</html>
<script type="module" src="/@vite/client"></script>

可以看到项目源码中多了一行,这一行是在indexHtmlMiddleware中间件中插入的,发起第二个请求

第二个请求

这个请求会被当作js文件请求来处理。

在这里,会看到一段最重要的逻辑,就是这个请求会经过所有的plugins处理。调用的堆栈是index.ts => transformMiddleware =>transform.ts => transformRequest.ts

这个函数的主要逻辑一次调用插件的resolveIdloadtransform钩子函数。具体的逻辑是在pluginContainer中。经过插件的这些加工后,这个请求输出的结果是一个js对象

{
  code: "...",
  map: null,
  etag: "W/"34eb-BoYIgrU2JBleZtOHa80mtwjgWJE"",
}

第三个请求

<script type="module" src="/src/main.ts"></script>

这个请求会经过esbuildimportAnalysis插件的transform输出

import {createApp} from "/node_modules/.vite/vue.js?v=43098e9d";
import App from "/src/App.vue";
import test1 from "/src/components/test1.vue";
import test11 from "/src/components/test11.vue";
import {createRouter, createWebHashHistory} from "/node_modules/.vite/vue-router.js?v=43098e9d";
import "/src/assets/css.css";
const router = createRouter({
    history: createWebHashHistory(),
    routes: [{
        path: "/test1",
        component: test1,
        children: [{
            path: "test11",
            component: test11
        }]
    }]
});
const app = createApp(App);
app.use(router);
app.mount("#app");
​
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi9Vc2Vycy9hcHBsZS9Eb2N1bWVudHMvbGVhcm4vdml0ZS92dWUtdHMvc3JjL21haW4udHMiXSwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHsgY3JlYXRlQXBwIH0gZnJvbSAndnVlJ1xuaW1wb3J0IEFwcCBmcm9tICcuL0FwcC52dWUnXG5pbXBvcnQgdGVzdDEgZnJvbSAnLi9jb21wb25lbnRzL3Rlc3QxLnZ1ZSdcbmltcG9ydCB0ZXN0MTEgZnJvbSAnLi9jb21wb25lbnRzL3Rlc3QxMS52dWUnXG5pbXBvcnQge2NyZWF0ZVJvdXRlcixjcmVhdGVXZWJIYXNoSGlzdG9yeX0gZnJvbSBcInZ1ZS1yb3V0ZXJcIlxuaW1wb3J0ICcuL2Fzc2V0cy9jc3MuY3NzJ1xuLy8gaW1wb3J0ICcuL2Fzc2V0cy9zY3NzLnNjc3MnXG4vLyBpbXBvcnQgYXhpb3MgZnJvbSAnYXhpb3MnXG4vLyBheGlvcy5nZXQoJy90ZXN0JykudGhlbihyZXMgPT4ge1xuLy8gICBjb25zb2xlLmxvZyhyZXMpO1xuLy8gfSkuY2F0Y2goZXJyID0+IHtcbi8vICAgY29uc29sZS5sb2coZXJyKTtcbi8vIH0pXG4vLyAvLyBDcmVhdGUgYSBuZXcgc3RvcmUgaW5zdGFuY2UuXG4vLyBjb25zdCBzdG9yZSA9IGNyZWF0ZVN0b3JlKHtcbi8vICAgc3RhdGUgKCkge1xuLy8gICAgIHJldHVybiB7XG4vLyAgICAgICBjb3VudDogMFxuLy8gICAgIH1cbi8vICAgfSxcbi8vICAgbXV0YXRpb25zOiB7XG4vLyAgICAgaW5jcmVtZW50IChzdGF0ZTogYW55KSB7XG4vLyAgICAgICBzdGF0ZS5jb3VudCsrXG4vLyAgICAgfVxuLy8gICB9XG4vLyB9KVxuLy8gaW1wb3J0IHsgY3JlYXRlU3RvcmUgfSBmcm9tICd2dWV4J1xuY29uc3Qgcm91dGVyID0gY3JlYXRlUm91dGVyKHtcbiAgaGlzdG9yeTogY3JlYXRlV2ViSGFzaEhpc3RvcnkoKSxcbiAgcm91dGVzOiBbXG4gICAge1xuICAgICAgcGF0aDogJy90ZXN0MScsXG4gICAgICBjb21wb25lbnQ6IHRlc3QxLFxuICAgICAgY2hpbGRyZW46IFtcbiAgICAgICAge1xuICAgICAgICAgIHBhdGg6ICd0ZXN0MTEnLFxuICAgICAgICAgIGNvbXBvbmVudDogdGVzdDExXG4gICAgICAgIH1cbiAgICAgIF1cbiAgICB9XG4gIF1cbn0pXG5jb25zdCBhcHAgPSBjcmVhdGVBcHAoQXBwKVxuLy8gYXBwLnVzZShzdG9yZSlcbmFwcC51c2Uocm91dGVyKVxuYXBwLm1vdW50KCcjYXBwJykiXSwibWFwcGluZ3MiOiJBQUFBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQXNCQSxNQUFNLFNBQVMsYUFBYTtBQUFBLEVBQzFCLFNBQVM7QUFBQSxFQUNULFFBQVE7QUFBQSxJQUNOO0FBQUEsTUFDRSxNQUFNO0FBQUEsTUFDTixXQUFXO0FBQUEsTUFDWCxVQUFVO0FBQUEsUUFDUjtBQUFBLFVBQ0UsTUFBTTtBQUFBLFVBQ04sV0FBVztBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFNckIsTUFBTSxNQUFNLFVBQVU7QUFFdEIsSUFBSSxJQUFJO0FBQ1IsSUFBSSxNQUFNOyIsIm5hbWVzIjpbXX0=

插件主要做了两件事,第一件是把typescript文件编译成js文件,第二件是把import的路径重写。

于是这个文件又会产生以下请求

import {createApp} from "/node_modules/.vite/vue.js?v=43098e9d";
import App from "/src/App.vue";
import test1 from "/src/components/test1.vue";
import test11 from "/src/components/test11.vue";
import {createRouter, createWebHashHistory} from "/node_modules/.vite/vue-router.js?v=43098e9d";
import "/src/assets/css.css";

可以看到主要分为三种请求,jsvuecss

主要逻辑其实都是一样的,经过所有插件的几个钩子函数,最终返回一个包含codemapetag的对象再组装其他通用对象返回给客户端。