阅读 97

Vite源码解析(1)

工欲善其事,必先利其器---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> <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的对象再组装其他通用对象返回给客户端。


作者:frankhe
链接:https://juejin.cn/post/7020611846549798919

文章分类
后端
文章标签
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐