Vite源码解析(1)
工欲善其事,必先利其器---debug
第一步 用tsc
编译带有sourcemap
的vite
源码
进入源码packages/vite
目录,执行tsc -p src/node
和tsc -p src/client
,生成带有sourmap
的源码。
第二步 配置vscode
的nodejs
debug
这一步的原理就是用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
了。
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__ 复制代码
主要分成两部分,client
和node
。
主要的是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 } 复制代码
按照代码执行顺序,这个函数做了这些事:
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'> } > 复制代码
用
Connect
模块创建一个服务器用
chokidar
创建文件监听功能调用插件
configureServer
钩子根据条件创建
timeMiddleware
、corsMiddleware
、proxyMiddleware
、baseMiddleware
、launchEditorMiddleware
、viteHMRPingMiddleware
、decodeURIMiddleware
、servePublicMiddleware
、transformMiddleware
、serveRawFsMiddleware
、serveStaticMiddleware
、history
、indexHtmlMiddleware
、vite404Middleware
、errorMiddleware
中间件。
项目的所有请求
只抓主线逻辑,详细逻辑后面再述。
第一个请求
我们在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
这个函数的主要逻辑一次调用插件的resolveId
、load
、transform
钩子函数。具体的逻辑是在pluginContainer
中。经过插件的这些加工后,这个请求输出的结果是一个js
对象
{ code: "...", map: null, etag: "W/"34eb-BoYIgrU2JBleZtOHa80mtwjgWJE"", } 复制代码
第三个请求
<script type="module" src="/src/main.ts"></script> 复制代码
这个请求会经过esbuild
和importAnalysis
插件的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"; 复制代码
可以看到主要分为三种请求,js
、vue
和css
主要逻辑其实都是一样的,经过所有插件的几个钩子函数,最终返回一个包含code
、map
和etag
的对象再组装其他通用对象返回给客户端。
作者:frankhe
链接:https://juejin.cn/post/7020611846549798919