Vite源码阅读1-Vite指令的原理

431 阅读3分钟

1 Vite搭建项目

Vite搭建项目很简单,2步。

1 Vite先全局安装vite:

npm i -g vite

2 使用Vite命令创建项目:

npm create vite@latest 

创建项目后。终端输入npm run dev就会出现一系列信息

image.png

包括Vite版本信息,本机以及内网地址

2 Debugger调试Vite源码

查看Vite项目的package.json文件

image.png

当我们在终端输入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。

AB079FCF-DC56-4246-80A6-F141363BCBE1.png

浏览器中打开箭头中地址。

image.png

此时我们发现有一个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会出现很多提示:

022E4FA9-AEA3-408E-B962-872D9483B1B8.png

而这就是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在终端打印信息。如下:

image.png

此时项目就真正的创建完成了。

此时点击地址浏览器就会展现index.html的页面。

那么思考浏览器怎么知道点击这个地址就去找index.html并渲染呢? 其实这就是vite中间件的作用了。

下回再分析吧。