大家好,我是wo不是黄蓉,今年学习目标从源码共读开始,希望能跟着若川大佬学习源码的思路学到更多的东西。
image.png image.png上街讲了怎么创建服务,并且引出了在哪儿处理文件转换和解析的。
这节接上次内容,来看下启动本地服务后,进入首页可以看到很多的请求,有些请求甚至不是我们写的文件,这些文件是从哪里加载的?
而且即便是我只引入了组件但是没有引用,为什么
vite
还要帮我加载哪些没有引用的组件呢?
server启动之后做了什么事情?
上次看到启动服务后,在浏览器打开http://127.0.0.1:5173/
就会调用transformMiddleware
方法,处理请求内容。在看transformMiddleware
方法之前,我发现vite
会先遍历整个项目的文件和文件夹,并对遍历到的文件进行标记,标记是否是想要的文件夹,是否是想要的文件等,目前还没看出来哪些文件会被标记
class ReaddirpStream extends Readable {
constructor(options = {}) {
this._maxDepth = opts.depth;
this._wantsDir = [DIR_TYPE, FILE_DIR_TYPE, EVERYTHING_TYPE].includes(type);
this._wantsFile = [FILE_TYPE, FILE_DIR_TYPE, EVERYTHING_TYPE].includes(type);
this._wantsEverything = type === EVERYTHING_TYPE;
this._root = sysPath$3.resolve(root);
this._isDirent = ('Dirent' in fs$8) && !opts.alwaysStat;
this._statsProp = this._isDirent ? 'dirent' : 'stats';
this._rdOptions = { encoding: 'utf8', withFileTypes: this._isDirent };
// Launch stream with one parent, the root dir.
this.parents = [this._exploreDir(root, 1)];
this.reading = false;
this.parent = undefined;
}
async _read(batch) {
if (this.reading) return;
this.reading = true;
try {
while (!this.destroyed && batch > 0) {
const { path, depth, files = [] } = this.parent || {};
if (files.length > 0) {
const slice = files.splice(0, batch).map(dirent => this._formatEntry(dirent, path));
for (const entry of await Promise.all(slice)) {
if (this.destroyed) return;
const entryType = await this._getEntryType(entry);
if (entryType === 'directory' && this._directoryFilter(entry)) {
if (depth <= this._maxDepth) {
this.parents.push(this._exploreDir(entry.fullPath, depth + 1));
}
if (this._wantsDir) {
this.push(entry);
batch--;
}
} else if ((entryType === 'file' || this._includeAsFile(entry)) && this._fileFilter(entry)) {
if (this._wantsFile) {
this.push(entry);
batch--;
}
}
}
} else {
const parent = this.parents.pop();
if (!parent) {
this.push(null);
break;
}
this.parent = await parent;
if (this.destroyed) return;
}
}
} catch (error) {
this.destroy(error);
} finally {
this.reading = false;
}
}
}
接下来看transformMiddleware
关键部分代码
//忽略监听的文件
const knownIgnoreList = new Set(['/', '/favicon.ico']);
function transformMiddleware(server) {
const { config: { root, logger }, moduleGraph, } = server;
return async function viteTransformMiddleware(req, res, next) {
//判断请求方法是否为get,或者请求的url不在监听列表就跳过,直接返回
if (req.method !== 'GET' || knownIgnoreList.has(req.url)) {
return next();
}
let url;
try {
url = decodeURI(removeTimestampQuery(req.url)).replace(NULL_BYTE_PLACEHOLDER, '\0');
}
catch (e) {
return next(e);
}
//去掉查询参数
const withoutQuery = cleanUrl(url);
try {
//获取到public文件夹的绝对路径
const publicDir = normalizePath$3(server.config.publicDir);
//获取根目录的绝对路径
const rootDir = normalizePath$3(server.config.root);
if (publicDir.startsWith(rootDir)) {
//...处理公共目录下文件访问地址
}
//判断是否是js、import、css或html请求
if (isJSRequest(url) ||
isImportRequest(url) ||
isCSSRequest(url) ||
isHTMLProxy(url)) {
//处理css请求的文件
if (isCSSRequest(url) &&
!isDirectRequest(url) &&
req.headers.accept?.includes('text/css')) {
url = injectQuery(url, 'direct');
}
// transformRequest,方法
const result = await transformRequest(url, server, {
html: req.headers.accept?.includes('text/html'),
});
//返回代码内容
if (result) {
return send$1(req, res, result.code, type, {
etag: result.etag,
// allow browser to cache npm deps!
cacheControl: isDep ? 'max-age=31536000,immutable' : 'no-cache',
headers: server.config.server.headers,
map: result.map,
});
}
}
}
next();
};
}
transformRequest
方法核心代码
function transformRequest(url, server, options = {}) {
const request = doTransform(url, server, options, timestamp);
return request;
}
doTransform
方法核心代码
async function doTransform(url, server, options, timestamp) {
const result = loadAndTransform(id, url, server, options, timestamp);
return result;
}
发现上面这两个方法并没有什么用,最终做转换的是loadAndTransform
,这种编码的思路我们可以借鉴,每个函数做的工作都是比较明确的,有依赖关系的可以利用返回值进行处理
async function loadAndTransform(id, url, server, options, timestamp) {
const { config, pluginContainer, moduleGraph, watcher } = server;
const { root, logger } = config;
const prettyUrl = isDebug$2 ? prettifyUrl(url, config.root) : '';
const ssr = !!options.ssr;
const file = cleanUrl(id);
let code = null;
let map = null;
// load
const loadStart = isDebug$2 ? performance.now() : 0;
//这边传入的id就是要加载的文件的绝对路径
const loadResult = await pluginContainer.load(id, { ssr });
// ensure module in graph after successful load
const mod = await moduleGraph.ensureEntryFromUrl(url, ssr);
ensureWatchedFile(watcher, mod.file, root);
// transform
const transformStart = isDebug$2 ? performance.now() : 0;
//pluginContainer-》const container = await createPluginContainer(config, moduleGraph, watcher);
const transformResult = await pluginContainer.transform(code, id, {
inMap: map,
ssr,
});
const originalCode = code;
const result = {
code,
map,
etag: etag_1(code, { weak: true }),
};
return result;
}
createPluginContainer
相关代码,关键代码result = await handler.call(ctx, code, id, { ssr });
但是不知道handler到底是个啥东西,看到后面发现是插件本身,插件本身暴露出来有一个transform
方法,因此执行await handler.call(ctx, code, id, { ssr })
就是插件将代码转换过程,返回的result
就是转换后的代码。
const container = {
async transform(code, id, options) {
const ctx = new TransformContext(id, code, inMap);
for (const plugin of getSortedPlugins('transform')) {
try {
//这里的handler是什么?handler就是transform自己,tranform又是插件里面提供的一个函数,因此代码分析部分是在这里做的
result = await handler.call(ctx, code, id, { ssr });
}
catch (e) {
ctx.error(e);
}
if (!result)
continue;
if (isObject$1(result)) {
if (result.code !== undefined) {
//获取请求文件中code部分
code = result.code;
}
}
else {
code = result;
}
}
return {
code,
map: ctx._getCombinedSourcemap(),
};
},
}
从上面流程看下来,好像没有解析代码的相关的代码,那么是如何加载到不同文件呢?
transform
钩子函数
回到createServer
,这些插件是从哪儿插入的?
答:resolveConfig
还有resolvePlugins
这两个地方对插件进行了处理。
const container = await createPluginContainer(config, moduleGraph, watcher);
resolvePlugins
能看到一些熟悉的插件htmlInlineProxyPlugin
async function resolvePlugins(config, prePlugins, normalPlugins, postPlugins) {
return [
htmlInlineProxyPlugin(config),
cssPlugin(config),
config.esbuild !== false ? esbuildPlugin(config.esbuild) : null,
jsonPlugin({
namedExports: true,
...config.json,
}, isBuild),
webWorkerPlugin(config),
assetPlugin(config),
...normalPlugins,
definePlugin(config),
cssPostPlugin(config),
isBuild && buildHtmlPlugin(config),
assetImportMetaUrlPlugin(config),
...buildPlugins.pre,
dynamicImportVarsPlugin(config),
importGlobPlugin(config),
...postPlugins,
...buildPlugins.post,
// internal server-only plugins are always applied after everything else
...(isBuild
? []
: [clientInjectionsPlugin(config), importAnalysisPlugin(config)]),
].filter(Boolean);
}
最后在'vite:import-analysis'
这个插件上找到了分析代码的模块,并且返回内容为
"import { createApp } from "/node_modules/.vite/deps/vue.js?v=c570bfa5"\nimport ElementPlus from "/node_modules/.vite/deps/element-plus.js?v=c570bfa5"\nimport "/node_modules/element-plus/dist/index.css"\nimport "/src/index.scss"\nimport App from "/src/App.vue"\nimport axios from "/node_modules/.vite/deps/axios.js?v=c570bfa5"\n//引入vue路由\nimport Router from "/src/router/index.ts"\n\nconst app = createApp(App)\n\n//vue3挂载全局组件\napp.config.globalProperties.$axios = axios\napp.use(Router)\napp.use(ElementPlus, { autoInsertSpace: true }).mount("#app")\n"
所以当你引用的内容是项目外的代码,vite
会自动将加载node_modules
中的代码,然后把这个文件再进行递归,最后翻译成浏览器可以识别的代码
翻译工作完成后开始执行加载操作load
方法
async load(id, options) {
const ssr = options?.ssr;
const ctx = new Context();
ctx.ssr = !!ssr;
for (const plugin of getSortedPlugins('load')) {
if (!plugin.load)
continue;
ctx._activePlugin = plugin;
const handler = 'handler' in plugin.load ? plugin.load.handler : plugin.load;
const result = await handler.call(ctx, id, { ssr });
if (result != null) {
if (isObject$1(result)) {
updateModuleInfo(id, result);
}
return result;
}
}
return null;
},
执行加载方法时如果遇到还需要继续解析的文件,就还是回到loadAndTransform
继续进行递归解析文件的操作,以此类推
这样一个文件的解析工作就完成了。
了解了怎么创建一个vite
服务,并且vite
是怎么处理文件加载的,下节目标,自己能够实现一下。
网友评论