1 Vite搭建项目
Vite搭建项目很简单,2步。
1 Vite先全局安装vite:
npm i -g vite
2 使用Vite命令创建项目:
npm create vite@latest
创建项目后。终端输入npm run dev就会出现一系列信息
包括Vite版本信息,本机以及内网地址
2 Debugger调试Vite源码
查看Vite项目的package.json文件
当我们在终端输入Vite。其实最终指向的就是bin目录下的vite.js文件
现在问题就是我们如何调试vite.js文件呢。打开vite.js文件。我们发现第一行有一句代码
#!/usr/bin/env node
这行代码就是告诉系统用node来执行这js文件。只需要在代码后加入 --inspect-brk。如下
#!/usr/bin/env node --inspect-brk
然后重新在终端输入npm run dev。
浏览器中打开箭头中地址。
此时我们发现有一个node的logo,点击logo后我们就可以调试了
3 源码阅读
进入Vite.js。vite.js代码不是很多。最重要的是调用了start方法
function start() {
// 导入了import('../dist/node/cli.js')
return import('../dist/node/cli.js')
}
if (profileIndex > 0) {
...
} else {
start()
}
可以看到start方法直接引入了cli.js。
cli.js首先获取用户在终端输入的参数以及平台的参数,CPU架构
const processArgs = process.argv;
const platformInfo = `${process.platform}-${process.arch} node-${process.version}`;
后面我们会发现一大段下面这样的代码:
cli
.commond(...)
.option(...)
.action(callback)
这里主要使用了一个第三方npm包,cac 我们全局安装Vite后,我们在终端输入 vite -h会出现很多提示:
而这就是CAC的作用。与它类似还有commander,vue-cli使用的就是commander。
Vite指令的原理
前面我们通过在终端输入npm run dev启动vite项目,本质运行的还是vite指令。 在cli.js 中有这样一句代码:
cli
.command('[root]', 'start dev server') // default command 默认的指令 即vite
....
.action(async (root, options) => {...})
一个command对应一个action。输入npm run dev即vite那么将会运行上面这个action。
.action(async (root, options) => {
// output structure is preserved even after bundling so require()
// is ok here
const { createServer } = await import('./chunks/dep-557f29e6.js').then(function (n) { return n.C; });
try {
// 创建服务端
const server = await createServer({
root,
base: options.base,
mode: options.mode,
configFile: options.config,
logLevel: options.logLevel,
clearScreen: options.clearScreen,
optimizeDeps: { force: options.force },
server: cleanOptions(options)
});
if (!server.httpServer) {
throw new Error('HTTP server not available');
}
// 监听服务端
await server.listen();
const info = server.config.logger.info;
// 在终端打印vite版本信息
// @ts-ignore
const viteStartTime = global.__vite_start_time ?? false;
const startupDurationString = viteStartTime
? picocolors.exports.dim(`ready in ${picocolors.exports.reset(picocolors.exports.bold(Math.ceil(performance.now() - viteStartTime)))} ms`)
: '';
info(`\n ${picocolors.exports.green(`${picocolors.exports.bold('VITE')} v${VERSION}`)} ${startupDurationString}\n`, { clear: !server.config.logger.hasWarned });
// 在终端打印本地和内外地址
server.printUrls();
}
npm run dev对应的action逻辑主要是创建,启动,监听服务,打印版本,地址信息
1 创建服务端
创建服务端是通过createServer方法,该方法在./chunks/dep-557f29e6.js中,方法实现如下:
async function createServer(inlineConfig = {}) {
const config = await resolveConfig(inlineConfig, 'serve', 'development');
const { root, server: serverConfig } = config;
const httpsOptions = await resolveHttpsConfig(config.server.https);
const { middlewareMode } = serverConfig;
const resolvedWatchOptions = resolveChokidarOptions({
disableGlobbing: true,
...serverConfig.watch
});
// 关键方法
const middlewares = connect();
const httpServer = middlewareMode
? null
: await resolveHttpServer(serverConfig, middlewares, httpsOptions);
// 创建websocket
const ws = createWebSocketServer(httpServer, config, httpsOptions);
if (httpServer) {
// 监听客户端error事件
setClientErrorHandler(httpServer, config.logger);
}
// chokidar是一个第三方包。用来监视文件变化
const watcher = chokidar.watch(path$n.resolve(root), resolvedWatchOptions);
// 创建模块图纸,记录每个模块之间的依赖关系
const moduleGraph = new ModuleGraph((url, ssr) => container.resolveId(url, undefined, { ssr }));
const container = await createPluginContainer(config, moduleGraph, watcher);
const closeHttpServer = createServerCloseFn(httpServer);
let exitProcess;
const server = {
config,
middlewares,
httpServer, // httpServer是server的一个属性
watcher,
pluginContainer: container,
ws,
moduleGraph,
resolvedUrls: null,
ssrTransform(code, inMap, url, originalCode = code) {
return ssrTransform(code, inMap, url, originalCode, server.config);
},
transformRequest(url, options) {
return transformRequest(url, server, options);
},
transformIndexHtml: null,
async ssrLoadModule(url, opts) {
if (isDepsOptimizerEnabled(config, true)) {
await initDevSsrDepsOptimizer(config, server);
}
await updateCjsSsrExternals(server);
return ssrLoadModule(url, server, undefined, undefined, opts?.fixStacktrace);
},
ssrFixStacktrace(e) {
if (e.stack) {
const stacktrace = ssrRewriteStacktrace(e.stack, moduleGraph);
rebindErrorStacktrace(e, stacktrace);
}
},
ssrRewriteStacktrace(stack) {
return ssrRewriteStacktrace(stack, moduleGraph);
},
async listen(port, isRestart) {
// 启动server
await startServer(server, port, isRestart);
if (httpServer) {
//解析地址,用于在终端打印
server.resolvedUrls = await resolveServerUrls(httpServer, config.server, config);
}
return server;
},
async close() {
if (!middlewareMode) {
process.off('SIGTERM', exitProcess);
if (process.env.CI !== 'true') {
process.stdin.off('end', exitProcess);
}
}
await Promise.all([
watcher.close(),
ws.close(),
container.close(),
closeHttpServer()
]);
server.resolvedUrls = null;
},
printUrls() {
// 打印: 如下
// resolvedUrls:
// ➜ Local: http://127.0.0.1:5173/
// ➜ Network:
if (server.resolvedUrls) {
printServerUrls(server.resolvedUrls, serverConfig.host, config.logger.info);
}
else if (middlewareMode) {
throw new Error('cannot print server URLs in middleware mode.');
}
else {
throw new Error('cannot print server URLs before server.listen is called.');
}
},
async restart(forceOptimize) {
if (!server._restartPromise) {
server._forceOptimizeOnRestart = !!forceOptimize;
server._restartPromise = restartServer(server).finally(() => {
server._restartPromise = null;
server._forceOptimizeOnRestart = false;
});
}
return server._restartPromise;
},
_ssrExternals: null,
_restartPromise: null,
_importGlobMap: new Map(),
_forceOptimizeOnRestart: false,
_pendingRequests: new Map()
};
server.transformIndexHtml = createDevHtmlTransformFn(server);
if (!middlewareMode) {
exitProcess = async () => {
try {
await server.close();
}
finally {
process.exit();
}
};
process.once('SIGTERM', exitProcess);
if (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) => {
//tips
debugger
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);
});
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
const postHooks = [];
for (const hook of config.getSortedPluginHooks('configureServer')) {
postHooks.push(await hook(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(lib$1.exports(typeof cors === 'boolean' ? {} : cors));
}
// proxy
const { proxy } = serverConfig;
// 如果有代理那么就注册代理的中间件
if (proxy) {
middlewares.use(proxyMiddleware(httpServer, proxy, config));
}
// base
const devBase = config.base;
if (devBase !== '/') {
// 注册中间件
middlewares.use(baseMiddleware(server));
}
// open in editor support
middlewares.use('/__open-in-editor', launchEditorMiddleware());
// 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
if (config.appType === 'spa') {
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());
// 我们这个项目appType就是spa
if (config.appType === 'spa' || config.appType === 'mpa') {
// 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));
let initingServer;
let serverInited = false;
const initServer = async () => {
if (serverInited) {
return;
}
if (initingServer) {
return initingServer;
}
initingServer = (async function () {
await container.buildStart({});
if (isDepsOptimizerEnabled(config, false)) {
// non-ssr
await initDepsOptimizer(config, server);
}
initingServer = undefined;
serverInited = true;
})();
return initingServer;
};
if (!middlewareMode && httpServer) {
// overwrite listen to init optimizer before server start
const listen = httpServer.listen.bind(httpServer);
httpServer.listen = (async (port, ...args) => {
try {
await initServer();
}
catch (e) {
httpServer.emit('error', e);
return;
}
return listen(port, ...args);
});
}
else {
await initServer();
}
return server;
}
这里我们先探究服务如何创建,启动。过多的细节暂时不去讨论。 可以看到下面2句代码
const middlewares = connect();
const httpServer = middlewareMode
? null
: await resolveHttpServer(serverConfig, middlewares, httpsOptions);
connect()如下:
function createServer$1() {
function app(req, res, next){
app.handle(req, res, next);
}
merge(app, proto);
merge(app, EventEmitter$3.prototype);
app.route = '/';
app.stack = [];
return app;
}
这里看到connect方法仅仅只是返回一个app。所以不会为null。那么对于
const httpServer = middlewareMode
? null
: await resolveHttpServer(serverConfig, middlewares, httpsOptions);
就会执行resolveHttpServer方法,该方法定义如下:
async function resolveHttpServer({ proxy }, app, httpsOptions) {
if (!httpsOptions) {
const { createServer } = await import('node:http');
// 使用node的http模块。返回server
return createServer(app);
}
。。。。
}
可以看到直接是使用了http模块中的createServer方法创建并返回Server实例。
回到对应的action中。在调用createServer后。直接调用了server.listen方法
async function createServer(inlineConfig = {}) {
const server = await createServer(...)
await server.listen();
....
}
这里需要注意的是server不是httpServer,而是一个普通的对象。如下:
const server = {
config,
middlewares,
httpServer, // httpServer是server的一个属性
watcher,
....
}
所以server.listen()调用的是server中的listen方法
async listen(port, isRestart) {
// 启动server
await startServer(server, port, isRestart);
if (httpServer) {
//解析地址,用于在终端打印
server.resolvedUrls = await resolveServerUrls(httpServer, config.server, config);
}
return server;
},
listen通过startServer启动服务
async function startServer(server, inlinePort, isRestart = false) {
// 获取到http模块创建的server
const httpServer = server.httpServer;
if (!httpServer) {
throw new Error('Cannot call server.listen in middleware mode.');
}
const options = server.config.server;
// 如果没有设置端口默认是5173
const port = inlinePort ?? options.port ?? 5173;
const hostname = await resolveHostname(options.host);
const protocol = options.https ? 'https' : 'http';
const info = server.config.logger.info;
const devBase = server.config.base;
const serverPort = await httpServerStart(httpServer, {
port,
strictPort: options.strictPort,
host: hostname.host,
logger: server.config.logger
});
又调用了httpServerStart
async function httpServerStart(httpServer, serverOptions) {
let { port, strictPort, host, logger } = serverOptions;
return new Promise((resolve, reject) => {
const onError = (e) => {
if (e.code === 'EADDRINUSE') {
if (strictPort) {
httpServer.removeListener('error', onError);
reject(new Error(`Port ${port} is already in use`));
}
else {
logger.info(`Port ${port} is in use, trying another one...`);
// 端口被占用,端口+1重试
httpServer.listen(++port, host);
}
}
else {
httpServer.removeListener('error', onError);
reject(e);
}
};
httpServer.on('error', onError);
// 这里的listen方法已经在createServer中替换了
httpServer.listen(port, host, () => {
httpServer.removeListener('error', onError);
resolve(port);
});
});
}
httpServerStart直接调用了listen方法。这里注意listen方法已经在createServer中替换了。
if (!middlewareMode && httpServer) {
// overwrite listen to init optimizer before server start
const listen = httpServer.listen.bind(httpServer);
httpServer.listen = (async (port, ...args) => {
try {
await initServer();
}
catch (e) {
httpServer.emit('error', e);
return;
}
return listen(port, ...args);
});
}
else {
await initServer();
}
可以看到 httpServer.listen首先调用initServer
const initServer = async () => {
if (serverInited) {
return;
}
// 如果initingServer存在直接返回initingServer
if (initingServer) {
return initingServer;
}
initingServer = (async function () {
await container.buildStart({});
if (isDepsOptimizerEnabled(config, false)) {
// non-ssr
await initDepsOptimizer(config, server);
}
initingServer = undefined;
serverInited = true;
})();
return initingServer;
};
最后直接调用真正的listen方法
listen(port, ...args);
回到createServer方法。
// 在终端打印vite版本信息,本地和远程地址
server.printUrls();
最后通过printUrls在终端打印信息。如下:
此时项目就真正的创建完成了。
此时点击地址浏览器就会展现index.html的页面。
那么思考浏览器怎么知道点击这个地址就去找index.html并渲染呢? 其实这就是vite中间件的作用了。
下回再分析吧。