大家好,我是wo不是黄蓉,今年学习目标从源码共读开始,希望能跟着若川大佬学习源码的思路学到更多的东西。有想法的同学也可以加我微信进行交流:hp1256003949。
前言:上次学习了vue2项目利用多模块入口实现根据文件夹进行打包,接上次学习内容之后。因为我太菜了,并且不太想写插件,然后就偷懒用
vue-vli默认的打包方式打包。 但是没有学到的知识还是要学习的,今天来看下vite的源码,是怎么搭建一个服务,且能根据请求加载到不同模块的。
我们目前新的项目都用的vue3+vite因此,我们以分析vite为例。
从官网可以了解到,vite是基于原生es模块的,这是vite为什么这么快的原因,他把一部分浏览器可以做的事情交给浏览器做了,服务端需要做的事情少了,自然就快了。
调试
源码调试,下载源码到本地,我的代码版本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中进行调试。
回到主题,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 包。目录下的文件,表示这是一个个软链接。
具体解释可以参考这篇文章->运行 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参数来改变默认的端口号
配置完命令后,执行创建服务器的代码,创建完服务器后,将从命令行中获取到的配置参数传入
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.js的createServer方法,返回值是一个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();
};
}