vite源码解析记录(一、本地启动)

293 阅读3分钟

前言

本文是vite源码的第一章,看过vite仓库之后发现是一个monorepo,一共有一百三十多个项目,大部分其实都是playground(类似于vite的实操),package目录是最主要的项目代码,之前已经对create-vite做了解析

  • create-vite
  • plugin-legacy
  • plugin-react
  • plugin-vue
  • plugin-vue-jsx
  • vite

按照惯例打开vite目录安装依赖,这里就遇到问题了,有一个叫做playwright-chromium的依赖在安装的时候一直报错,为此专门在vite github上提了一个讨论,不过好像也一直没人鸟的样子

执行npm run dev后vite做了什么

项目执行npm run dev后,其实就是执行了npx vite,通过阅读packages\vite\bin\vite.js我们可以知道如果vite后不加参数就是执行默认命令(这部分直接跳过了,主要就是cac的使用),也就是createServer方法,内部的逻辑以流程图的形式展现的话就像这个样子

0e8e4795e43f4843aeefdb3c365120d8_tplv-k3u1fbpfcp-zoom-in-crop-mark_4536_0_0_0 (1).png

方法代码主要在packages\vite\src\node\server目录,源码分析如下

// inlineConfig:命令行配置
async function createServer(inlineConfig = {}) {
    // 解析vite配置 https://juejin.cn/post/7140925216993050661
    const config = await resolveConfig(inlineConfig, 'serve', 'development');
    const { root, server: serverConfig } = config;
    const httpsOptions = await resolveHttpsConfig(config.server.https);
    const { middlewareMode } = serverConfig;
    // 解析传递给Chokidar的参数,Chokidar是一个监听文件变化的库
    const resolvedWatchOptions = resolveChokidarOptions({
        disableGlobbing: true,
        ...serverConfig.watch
    });
    // connect:作为中间件使用的服务端框架
    const middlewares = connect();
    // 创建http和websocket服务
    const httpServer = middlewareMode
        ? null
        : await resolveHttpServer(serverConfig, middlewares, httpsOptions);
    const ws = createWebSocketServer(httpServer, config, httpsOptions);
    if (httpServer) {
        setClientErrorHandler(httpServer, config.logger);
    }
    // 创建chokidar监听实例
    const watcher = chokidar.watch(path.resolve(root), resolvedWatchOptions);
    // 创建依赖关系实例 https://juejin.cn/post/7082922754164391972
    const moduleGraph = new ModuleGraph((url, ssr) => container.resolveId(url, undefined, { ssr }));
    // 创建插件容器 https://juejin.cn/post/7083352352601669669
    const container = await createPluginContainer(config, moduleGraph, watcher);
    const closeHttpServer = createServerCloseFn(httpServer);
    let exitProcess;
    // 字面量的形式创建server
    const server = {
        ...
    };
    // 插件的 transformIndexHtml 钩子在这里就执行啦,用于转换 index.html
    server.transformIndexHtml = createDevHtmlTransformFn(server);
    // 监听vite进程关闭
    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);
        }
    }
    // packageCache是resolveConfig时new Map(),这个map的目的还不清楚
    // 这块主要是对这个map实例的set方法做了劫持
    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) => {
        ...
    });
    // 添加新文件时触发
    watcher.on('add', (file) => {
        ...
    });
    // 文件删除时触发
    watcher.on('unlink', (file) => {
        ...
    });
    
    if (!middlewareMode && httpServer) {
        httpServer.once('listening', () => {
            // 更新端口号,配置中指定的端口可能会被占用
            serverConfig.port = httpServer.address().port;
        });
    }
    // 插件的configureServer钩子可以选择在前置或后置执行
    // 使用postHooks收集后置的configureServer钩子
    const postHooks = [];
    for (const plugin of config.plugins) {
        if (plugin.configureServer) {
            postHooks.push(await plugin.configureServer(server));
        }
    }
    // 内部中间件 ------------------------------------------------------
    // 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
    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));
    }
    // 核心!转换中间件
    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));
    }
    // This is applied before the html middleware so that user middleware can
    // serve custom content instead of index.html.
    // 调用后置configureServer钩子
    postHooks.forEach((fn) => fn && fn());
    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) {
        // 重写监听函数 目的是在服务启动前执行预构建
        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;
}

总结

如果用一张图来梳理createServer函数的整个流程,大概就是下面这个

image.png

当我们敲下 vite 命令后,cac插件识别命令并执行createServer,之后 vite 在短短时间内做了解析配置、创建 http server、创建 ws、创建文件监听器、初始化模块依赖图、创建插件、预构建、启动服务等任务,最后返回server对象。 接收到server对象之后,执行server.listen()启动开发服务,并在控制台打印出我们经常看到的服务地址

image.png