工欲善其事,必先利其器---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 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
这个函数的主要逻辑一次调用插件的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
的对象再组装其他通用对象返回给客户端。