最近,虽然在项目中用到了vite
,但是对它的特性还不是很了解,所以想扒一扒它的源码,看下为什么它突然异军突起,好多大佬都说用了它之后,再也不想用webpack
了。
我们都知道,当执行npm run dev
的时候,npm
会去项目的package.json
文件里找scripts
里找对应的dev
指令执行。
package.json
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
复制代码
执行vite
的时候,npm
会找到 ./node_modules/.bin
对应的vite
脚本(.bin
中的文件在npm
安装对应依赖的时候创建)。下面是vite
脚本。可以看到文件顶部写着 #!/user/bin/env node
,表示这是一个通过使用 Node
执行的脚本。
vite.js
#!/usr/bin/env node
const { performance } = require('perf_hooks')
if (!__dirname.includes('node_modules')) {
try {
// only available as dev dependency
require('source-map-support').install()
} catch (e) {}
}
global.__vite_start_time = performance.now()
// check debug mode first before requiring the CLI.
const debugIndex = process.argv.findIndex((arg) => /^(?:-d|--debug)$/.test(arg))
const filterIndex = process.argv.findIndex((arg) =>
/^(?:-f|--filter)$/.test(arg)
)
const profileIndex = process.argv.indexOf('--profile')
if (debugIndex > 0) {
let value = process.argv[debugIndex + 1]
if (!value || value.startsWith('-')) {
value = 'vite:*'
} else {
// support debugging multiple flags with comma-separated list
value = value
.split(',')
.map((v) => `vite:${v}`)
.join(',')
}
process.env.DEBUG = `${
process.env.DEBUG ? process.env.DEBUG + ',' : ''
}${value}`
if (filterIndex > 0) {
const filter = process.argv[filterIndex + 1]
if (filter && !filter.startsWith('-')) {
process.env.VITE_DEBUG_FILTER = filter
}
}
}
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()
}
复制代码
那么为什么,npm
在安装依赖的时候会自动创建该文件呢?我们可以找到vite
的依赖包,打开来看一下,可以看到,在vite
的package.json
里面有如下配置:
"bin": {
"vite": "bin/vite.js"
},
复制代码
当npm
读取到该配置时,会将对应的文件链接到.bin
文件目录下
接下来,我们回到vite.js
文件,我们找到下面这个方法,找到对应的执行文件。
function start() {
require('../dist/node/cli')
}
复制代码
来看一下它的核心代码:
cli.js

const cac = (name = "") => new CAC(name);
const cli = cac('vite');
复制代码
在这个文件执行的时候,会先new
一个CAC
对象,这个对象定义了很多方法,在这里不一一赘述了,有兴趣的可以直接查看vite
的源码。创建了cli
对象后,可以看下如下代码,这些是vite
内置的一些指令。我们着重看一下action
这个方法。
// dev
cli
.command('[root]', 'start dev server') // default command
.alias('serve') // the command is called 'serve' in Vite's API
.alias('dev') // alias to align with the script name
.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('--force', `[boolean] force the optimizer to ignore the cache and re-bundle`)
.action(async (root, options) => {
// output structure is preserved even after bundling so require()
// is ok here
const { createServer } = await Promise.resolve().then(function () { return require('./chunks/dep-8f5c9290.js'); }).then(function (n) { return n.index$1; });
try {
const server = await createServer({
root,
base: options.base,
mode: options.mode,
configFile: options.config,
logLevel: options.logLevel,
clearScreen: options.clearScreen,
server: cleanOptions(options)
});
if (!server.httpServer) {
throw new Error('HTTP server not available');
}
await server.listen();
const info = server.config.logger.info;
info(index.colors.cyan(`\n vite v${require('vite/package.json').version}`) +
index.colors.green(` dev server running at:\n`), {
clear: !server.config.logger.hasWarned
});
server.printUrls();
// @ts-ignore
if (global.__vite_start_time) {
// @ts-ignore
const startupDuration = perf_hooks.performance.now() - global.__vite_start_time;
info(`\n ${index.colors.cyan(`ready in ${Math.ceil(startupDuration)}ms.`)}\n`);
}
}
catch (e) {
index.createLogger(options.logLevel).error(index.colors.red(`error when starting dev server:\n${e.stack}`), { error: e });
process.exit(1);
}
});
复制代码
当执行命令的时候,会调用action
的回调函数。可以看到有创建一个server
对象。这个创建方法代码有点多,这里只展示一些主要的代码以及主体流程,在后续的文章里面再一一分开阐述。
async function createServer(inlineConfig = {}) {
// 获取本地项目根路径和本地服务器相关配置
const config = await resolveConfig(inlineConfig, 'serve', 'development');
const { root, server: serverConfig } = config;
const httpsOptions = await resolveHttpsConfig(config.server.https, config.cacheDir);
let { middlewareMode } = serverConfig;
if (middlewareMode === true) {
middlewareMode = 'ssr';
}
const middlewares = connect();
// 创建http服务
const httpServer = middlewareMode
? null
: await resolveHttpServer(serverConfig, middlewares, httpsOptions);
// 创建 WebSocket 服务器
const ws = createWebSocketServer(httpServer, config, httpsOptions);
const { ignored = [], ...watchOptions } = serverConfig.watch || {};
// 通过 chokidar 监听文件
const watcher = chokidar.watch(path__default.resolve(root), {
ignored: [
'**/node_modules/**',
'**/.git/**',
...(Array.isArray(ignored) ? ignored : [ignored])
],
ignoreInitial: true,
ignorePermissionErrors: true,
disableGlobbing: true,
...watchOptions
});
// 创建Vite 的 ModuleGraph 实例
const moduleGraph = new ModuleGraph((url, ssr) => container.resolveId(url, undefined, { ssr }));
// 创建插件容器,是一个对象,对象的属性是 vite 支持的 rollup 的钩子函数
// 比如 options、resolveId、load、transform
const container = await createPluginContainer(config, moduleGraph, watcher);
const closeHttpServer = createServerCloseFn(httpServer);
// eslint-disable-next-line prefer-const
let exitProcess;
// 声明 server 对象
const server = {
config,// 包含命令行传入的配置 和 配置文件的配置
middlewares,
get app() {
config.logger.warn(`ViteDevServer.app is deprecated. Use ViteDevServer.middlewares instead.`);
return middlewares;
},
httpServer,
watcher,
pluginContainer: container,
ws,
moduleGraph,
ssrTransform,
transformWithEsbuild,
transformRequest(url, options) {
return transformRequest(url, server, options);
},
transformIndexHtml: null,
async ssrLoadModule(url, opts) {
if (!server._ssrExternals) {
let knownImports = [];
const optimizedDeps = server._optimizedDeps;
if (optimizedDeps) {
await optimizedDeps.scanProcessing;
knownImports = [
...Object.keys(optimizedDeps.metadata.optimized),
...Object.keys(optimizedDeps.metadata.discovered)
];
}
server._ssrExternals = resolveSSRExternal(config, knownImports);
}
return ssrLoadModule(url, server, undefined, undefined, opts === null || opts === void 0 ? void 0 : opts.fixStacktrace);
},
ssrFixStacktrace(e) {
if (e.stack) {
const stacktrace = ssrRewriteStacktrace(e.stack, moduleGraph);
rebindErrorStacktrace(e, stacktrace);
}
},
ssrRewriteStacktrace(stack) {
return ssrRewriteStacktrace(stack, moduleGraph);
},
listen(port, isRestart) {
return startServer(server, port, isRestart);
},
async close() {
process.off('SIGTERM', exitProcess);
if (!middlewareMode && process.env.CI !== 'true') {
process.stdin.off('end', exitProcess);
}
await Promise.all([
watcher.close(),
ws.close(),
container.close(),
closeHttpServer()
]);
},
printUrls() {
if (httpServer) {
printCommonServerUrls(httpServer, config.server, config);
}
else {
throw new Error('cannot print server URLs in middleware mode.');
}
},
async restart(forceOptimize) {
if (!server._restartPromise) {
server._forceOptimizeOnRestart = !!forceOptimize;
server._restartPromise = restartServer(server).finally(() => {
server._restartPromise = null;
server._forceOptimizeOnRestart = false;
});
}
return server._restartPromise;
},
_optimizedDeps: null,
_ssrExternals: null,
_globImporters: Object.create(null),
_restartPromise: null,
_forceOptimizeOnRestart: false,
_pendingRequests: new Map()
};
server.transformIndexHtml = createDevHtmlTransformFn(server);
exitProcess = async () => {
try {
await server.close();
}
finally {
process.exit(0);
}
};
process.once('SIGTERM', exitProcess);
if (!middlewareMode && process.env.CI !== 'true') {
process.stdin.on('end', exitProcess);
}
const { packageCache } = config;
const setPackageData = packageCache.set.bind(packageCache);
packageCache.set = (id, pkg) => {
if (id.endsWith('.json')) {
watcher.add(id);
}
return setPackageData(id, pkg);
};
// 被监听文件发生变化时触发
watcher.on('change', async (file) => {
file = normalizePath$3(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)
});
}
}
});
// 添加文件时触发
watcher.on('add', (file) => {
handleFileAddUnlink(normalizePath$3(file), server);
});
watcher.on('unlink', (file) => {
handleFileAddUnlink(normalizePath$3(file), server, true);
});
if (!middlewareMode && httpServer) {
httpServer.once('listening', () => {
// update actual port since this may be different from initial value
serverConfig.port = httpServer.address().port;
});
}
// apply server configuration hooks from plugins
// 执行插件中的 configureServer 钩子函数
// configureServer:https://vitejs.cn/guide/api-plugin.html#configureserver
const postHooks = [];
for (const plugin of config.plugins) {
if (plugin.configureServer) {
// configureServer 可以注册前置中间件,就是在内部中间件之前执行;也可以注册后置中间件
// 如果configureServer 返回一个函数,这个函数内部就是注册后置中间件,并将这些函数收集到 postHooks 中
postHooks.push(await plugin.configureServer(server));
}
}
// Internal middlewares ------------------------------------------------------
// request timer
if (process.env.DEBUG) {
middlewares.use(timeMiddleware(root));
}
// cors (enabled by default)
const { cors } = serverConfig;
if (cors !== false) {
middlewares.use(corsMiddleware(typeof cors === 'boolean' ? {} : cors));
}
// proxy
const { proxy } = serverConfig;
if (proxy) {
middlewares.use(proxyMiddleware(httpServer, proxy, config));
}
// base
// 注册中间件
if (config.base !== '/') {
middlewares.use(baseMiddleware(server));
}
// open in editor support
middlewares.use('/__open-in-editor', launchEditorMiddleware());
// hmr reconnect ping
// Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...`
middlewares.use('/__vite_ping', function viteHMRPingMiddleware(_, res) {
res.end('pong');
});
// serve static files under /public
// this applies before the transform middleware so that these files are served
// as-is without transforms.
if (config.publicDir) {
middlewares.use(servePublicMiddleware(config.publicDir, config.server.headers));
}
// main transform middleware
// 主要转换中间件
middlewares.use(transformMiddleware(server));
// serve static files
middlewares.use(serveRawFsMiddleware(server));
middlewares.use(serveStaticMiddleware(root, server));
// spa fallback
// 如果请求路径是 /结尾,则将路径修改为 /index.html ??
if (!middlewareMode || middlewareMode === 'html') {
middlewares.use(spaFallbackMiddleware(root));
}
// run post config hooks
// This is applied before the html middleware so that user middleware can
// serve custom content instead of index.html.
// 调用用户定义的后置中间件
postHooks.forEach((fn) => fn && fn());
if (!middlewareMode || middlewareMode === 'html') {
// 如果请求的url是 html 则调用插件中所有的 transformIndexHtml 钩子函数,转换html,并将转换后的 html 代码发送给客户端
// transform index.html
middlewares.use(indexHtmlMiddleware(server));
// handle 404s
// Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...`
middlewares.use(function vite404Middleware(_, res) {
res.statusCode = 404;
res.end();
});
}
// error handler
middlewares.use(errorMiddleware(server, !!middlewareMode));
const initOptimizer = () => {
if (!config.optimizeDeps.disabled) {
server._optimizedDeps = createOptimizedDeps(server);
}
};
if (!middlewareMode && httpServer) {
let isOptimized = false;
// overwrite listen to init optimizer before server start
// 重写 httpServer.listen,在服务器启动前预构建
const listen = httpServer.listen.bind(httpServer);
httpServer.listen = (async (port, ...args) => {
if (!isOptimized) {
try {
await container.buildStart({});
initOptimizer();
isOptimized = true;
}
catch (e) {
httpServer.emit('error', e);
return;
}
}
return listen(port, ...args);
});
}
else {
await container.buildStart({});
initOptimizer();
}
return server;
}
复制代码
总体来说,createServer
函数的大体流程如下
- 获取
config
配置 - 创建 http 服务器
httpServer
?? - 创建 WebSocket 服务器
ws
?? - 通过 chokidar 创建监听器
watcher
- 创建一个兼容rollup钩子函数的对象
container
- 创建实例
moduleGraph
?? - 声明
server
- 注册
watcher
- 执行插件中的
configureServer
钩子函数(注册用户定义的前置中间件),并收集用户定义的后置中间件 - 注册中间件
- 注册用户定义的后置中间件
- 注册转换
html
文件的中间件和未找到文件的404中间件 - 重写
httpServer.listen
大致内容就是这些啦,还有超多不懂的地方,欢迎大佬来指导哦!!
最后,给自己留个作业吧!欢迎大家和我一起来探讨呀!