2023.18 vite源码学习-Vite是怎么处理模块加载的

612 阅读5分钟

大家好,我是wo不是黄蓉,今年学习目标从源码共读开始,希望能跟着若川大佬学习源码的思路学到更多的东西。有想法的同学也可以加我微信进行交流:hp1256003949

前言:上次学习了vue2项目利用多模块入口实现根据文件夹进行打包,接上次学习内容之后。因为我太菜了,并且不太想写插件,然后就偷懒用 vue-vli默认的打包方式打包。 但是没有学到的知识还是要学习的,今天来看下vite的源码,是怎么搭建一个服务,且能根据请求加载到不同模块的。

我们目前新的项目都用的vue3+vite因此,我们以分析vite为例。

从官网可以了解到,vite是基于原生es模块的,这是vite为什么这么快的原因,他把一部分浏览器可以做的事情交给浏览器做了,服务端需要做的事情少了,自然就快了。

image.png

调试

源码调试,下载源码到本地,我的代码版本4.2.1,查看package.json,然后执行npm run dev会在packages\vite\dist生成一个dist的文件夹.

通过该方式可以调试cli.js部分代码,如果想启动一个服务,然后调试vite源码,建议自己本地搭建一个vite项目(或者也可以用我的项目我的vite项目),然后用vscode的调试模块,这样就可以启动一个服务,在node_modules\vite\dist\node\chunks\dep-3007b26d.js中打断点,调试相关代码,因为这边相当于是起了一个node服务,因此我们可以在vscode中进行调试。

image.png

回到主题,vite是怎么处理各个模块的并且让浏览器可以识别的?

要回答这个问题,还要回到vite的入口 ,从入口文件开始分析。

入口文件

查看package.json文件

main字段,是一个模块的ID,通常指向项目的入口文件。

bin字段,可执行的文件,进入到packages\vite\bin执行node ./vite.js会执行cli.js文件。关于bin和main的更多理解参考

当你初始化npm,它会创建一个符号链接到cli.js脚本到/usr/local/bin/npm,我们使用npm run dev的时候实际执行的是vite这个命令,但是直接执行这个命令是会报错的,因为我们系统变量中是没有这条命令的,但是通过npm i xxx的时候,npm会帮我们创建一个.bin的目录,并帮我们创建好可执行文件。.bin 目录,这个目录不是任何一个 npm 包。目录下的文件,表示这是一个个软链接。

image.png

具体解释可以参考这篇文章->运行 npm run xxx 的时候发生了什么?

{
      "bin": {
        "vite": "bin/vite.js"
      },
      "main": "./dist/node/index.js",
}

无论怎样,我们都是要从bin/vite.js这个文件入手的,其中主要就是执行了start方法,start方法有引入了../dist/node/cli.js,接下来看下cli.js代码

#!/usr/bin/env node
import { performance } from 'node:perf_hooks'

function start() {
  return import('../dist/node/cli.js')
}
start()

cli.js

cli.js里面的代码是用的一个cac的库做命令行处理用的,里面可以看到一些常见的配置项,例如:

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方法配置--host参数,如果你是开发环境我们使用npm run dev --host来将本机的地址暴露出去。

还有,例如,我们使用--port参数来改变默认的端口号

1681824011081.png

配置完命令后,执行创建服务器的代码,创建完服务器后,将从命令行中获取到的配置参数传入

createServer创建服务

packages\vite\dist\node\cli.js引入./chunks/dep-77774f3a.js模块的createServer方法,从这边将cli中的一些执行命令时的配置传入

const { createServer } = await import('./chunks/dep-77774f3a.js').then(function (n) { return n.D; });
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),
        });
//启动服务
 await server.listen();

接下来看下./chunks/dep-77774f3a.jscreateServer方法,返回值是一个server,启动、关闭、重启服务相关代码都在此,vite用的是node原生模块http坐的服务,我们模仿其写demo的时候可以使用koa或者express作为服务。这边我们还可以看到一些中间件的代码,这些中间件是处理各个模块加载的核心代码,也是回答我们本文问题的关键代码

async function createServer(inlineConfig = {}) {
    //处理配置信息
    const config = await resolveConfig(inlineConfig, 'serve');
      const server = {
        config,
        middlewares,
        httpServer,
        watcher,
        pluginContainer: container,
        ws,
        moduleGraph,
        resolvedUrls: null,
          //cli.js中执行server.listen执行的该函数
        async listen(port, isRestart) {
            await startServer(server, port);
            if (httpServer) {
                server.resolvedUrls = await resolveServerUrls(httpServer, config.server, config);
                if (!isRestart && config.server.open)
                    server.openBrowser();
            }
            return server;
        },
    };
    //转换代码
    middlewares.use(transformMiddleware(server));
    // serve static files
    middlewares.use(serveRawFsMiddleware(server));
    middlewares.use(serveStaticMiddleware(root, server));
    return server;
}

vite的介绍中介绍到,vite之所以快是因为他是基于原生ES模块提供了丰富的内建功能,vite将一部分的解析工作交给了浏览器来出来,现代的浏览器有的已经支持了import语法,默认的构建目标是能支持 原生 ESM 语法的 script 标签原生 ESM 动态导入 和 import.meta 的浏览器 。

因此vite是怎么处理这些解析工作的?从源码中可以看出来是用了中间件,从以下代码可以看出来该函数判断了是js、css、import、html请求时对其进行处理,具体处理了些什么我们下节再看,但是从这段代码中可以隐约看到vite对不同模块的请求的处理了

例如:如果是get请求时,请求路径为/时就直接返回,这里也就是我们的首页,即我启动的服务http://127.0.0.1:5173/#/这个页面。

function transformMiddleware(server) {
    const { config: { root, logger }, moduleGraph, } = server;
    // Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...`
    return async function viteTransformMiddleware(req, res, next) {
        if (req.method !== 'GET' || knownIgnoreList.has(req.url)) {
            return next();
        }
        try {
            const publicDir = normalizePath$3(server.config.publicDir);
            const rootDir = normalizePath$3(server.config.root);
            if (isJSRequest(url) ||
                isImportRequest(url) ||
                isCSSRequest(url) ||
                isHTMLProxy(url)) {
                // strip ?import
                url = removeImportQuery(url);
                // Strip valid id prefix. This is prepended to resolved Ids that are
                // not valid browser import specifiers by the importAnalysis plugin.
                url = unwrapId(url);
                // for CSS, we need to differentiate between normal CSS requests and
                // imports
                if (isCSSRequest(url) &&
                    !isDirectRequest(url) &&
                    req.headers.accept?.includes('text/css')) {
                    url = injectQuery(url, 'direct');
                }
                // check if we can return 304 early
                const ifNoneMatch = req.headers['if-none-match'];
                if (ifNoneMatch &&
                    (await moduleGraph.getModuleByUrl(url, false))?.transformResult
                        ?.etag === ifNoneMatch) {
                    isDebug && debugCache(`[304] ${prettifyUrl(url, root)}`);
                    res.statusCode = 304;
                    return res.end();
                }
                // resolve, load and transform using the plugin container
                const result = await transformRequest(url, server, {
                    html: req.headers.accept?.includes('text/html'),
                });
               
            }
        }
        next();
    };
}