本文基于
vite 4.3.0-beta.1
版本的源码进行剖析
文章内容
vite
本地服务器的创立流程剖析vite
预构建流程剖析vite
middlewares 拦挡申请资源剖析vite
热更新 HMR 流程剖析
1. 入口 npm run dev
在我的项目的 package.json
中注册对应的 scripts
命令,当咱们运行 npm run dev
时,实质就是运行了vite
{
"scripts": {"dev": "vite",}
}
而
vite
命令是在哪里注册的呢?
在 node_modules/vite/package.json
中
{
"bin": {"vite": "bin/vite.js"}
}
在 node_modules/vite/bin/vite.js
中
#!/usr/bin/env node
const profileIndex = process.argv.indexOf('--profile')
function start() {return import('../dist/node/cli.js')
}
if (profileIndex > 0) {//...} else {start()
}
最终调用的是打包后的 dist/node/cli.js
文件
解决用户的输出后,调用 ./chunks/dep-f365bad6.js
的createServer()
办法,如上面所示,最终调用server.listen()
const {createServer} = await import('./chunks/dep-f365bad6.js').then(function (n) {return n.G;});
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();
createServer()
async function createServer(inlineConfig = {}) {const config = await resolveConfig(inlineConfig, 'serve');
if (isDepsOptimizerEnabled(config, false)) {
// start optimizer in the background, we still need to await the setup
await initDepsOptimizer(config);
}
const {root, server: serverConfig} = config;
const httpsOptions = await resolveHttpsConfig(config.server.https);
const {middlewareMode} = serverConfig;
const resolvedWatchOptions = resolveChokidarOptions(config, {
disableGlobbing: true,
...serverConfig.watch,
});
const middlewares = connect();
const httpServer = middlewareMode
? null
: await resolveHttpServer(serverConfig, middlewares, httpsOptions);
const ws = createWebSocketServer(httpServer, config, httpsOptions);
const watcher = chokidar.watch(
// config file dependencies and env file might be outside of root
[root, ...config.configFileDependencies, path$o.join(config.envDir, '.env*')], resolvedWatchOptions);
const moduleGraph = new ModuleGraph((url, ssr) => container.resolveId(url, undefined, { ssr}));
const server = {
config,
middlewares,
httpServer,
watcher,
pluginContainer: container,
ws,
moduleGraph,
...
};
const initServer = async () => {if (serverInited)
return;
if (initingServer)
return initingServer;
initingServer = (async function () {await container.buildStart({});
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;
}
createServer()
源码太长,上面将分为多个小点进行剖析,对于一些不是该点剖析的代码将间接省略:
- 创立本地 node 服务器
- 预构建
- 申请资源拦挡
- 热更新 HMR
createServe 思维导图
2. 创立本地 node 服务器
// 只保留本地 node 服务器的相干代码
async function createServer(inlineConfig = {}) {
// 创立 http 申请
const middlewares = connect();
const httpServer = middlewareMode
? null
: await resolveHttpServer(serverConfig, middlewares, httpsOptions);
const server = {
config,
middlewares,
httpServer,
watcher,
pluginContainer: container
...,
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;
}
}
const initServer = async () => {if (serverInited)
return;
if (initingServer)
return initingServer;
initingServer = (async function () {await container.buildStart({});
initingServer = undefined;
serverInited = true;
})();
return initingServer;
};
//...
await initServer();
return server;
}
下面代码蕴含着多个知识点,咱们上面将开展剖析
2.1 connect()创立 http 服务器
Connect 模块介绍
Connect 是一个 Node.js
的可扩大 HTTP 服务框架,用于将各种 "middleware"
粘合在一起以解决申请
var app = connect();
app.use(function middleware1(req, res, next) {
// middleware 1
next();});
app.use(function middleware2(req, res, next) {
// middleware 2
next();});
var connect = require('connect');
var http = require('http');
var app = connect();
// gzip/deflate outgoing responses
var compression = require('compression');
app.use(compression());
// respond to all requests
app.use(function(req, res){res.end('Hello from Connect!\n');
});
//create node.js http server and listen on port
http.createServer(app).listen(3000);
源码剖析
会先应用 connect()
创立 middlewares,而后将 middlewares 作为 app 属性名传入到 resolveHttpServer()
中
最终也是应用 Node.js
的Http
模块创立本地服务器
// 只保留本地 node 服务器的相干代码
async function createServer(inlineConfig = {}) {const middlewares = connect();
const httpServer = middlewareMode
? null
: await resolveHttpServer(serverConfig, middlewares, httpsOptions);
//...
}
async function resolveHttpServer({proxy}, app, httpsOptions) {if (!httpsOptions) {const { createServer} = await import('node:http');
return createServer(app);
}
// #484 fallback to http1 when proxy is needed.
if (proxy) {const { createServer} = await import('node:https');
return createServer(httpsOptions, app);
}else {const { createSecureServer} = await import('node:http2');
return createSecureServer({
// Manually increase the session memory to prevent 502 ENHANCE_YOUR_CALM
// errors on large numbers of requests
maxSessionMemory: 1000,
...httpsOptions,
allowHTTP1: true,
},
// @ts-expect-error TODO: is this correct?
app);
}
}
2.2 启动 http 服务器
在 dist/node/cli.js
文件的剖析中,咱们晓得
在创立 server
实现后,咱们会调用server.listen()
// dist/node/cli.js
const {createServer} = await import('./chunks/dep-f365bad6.js').then(function (n) {return n.G;});
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();
而 server.listen()
最终调用的也是 Node.js
的Http
模块的监听办法,即下面 Connect
模块介绍示例中的http.createServer(app).listen(3000)
async function createServer(inlineConfig = {}) {const middlewares = connect();
const httpServer = middlewareMode
? null
: await resolveHttpServer(serverConfig, middlewares, httpsOptions);
const server = {
httpServer,
//...
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;
}
};
}
async function startServer(server, inlinePort) {
const httpServer = server.httpServer;
//...
await httpServerStart(httpServer, {
port,
strictPort: options.strictPort,
host: hostname.host,
logger: server.config.logger,
});
}
async function httpServerStart(httpServer, serverOptions) {let { port, strictPort, host, logger} = serverOptions;
return new Promise((resolve, reject) => {httpServer.listen(port, host, () => {httpServer.removeListener('error', onError);
resolve(port);
});
});
}
3. 预构建
3.1 预构建的起因
CommonJS 和 UMD 兼容性
在开发阶段中,Vite 的开发服务器将所有代码视为原生 ES 模块。因而,Vite 必须先将以 CommonJS 或 UMD 模式提供的依赖项转换为 ES 模块。
在转换 CommonJS 依赖项时,Vite 会进行智能导入剖析,这样即便模块的导出是动态分配的(例如 React),具名导入(named imports)也能失常工作:
// 合乎预期
import React, {useState} from 'react'
性能
为了进步后续页面的加载性能,Vite 将那些具备许多外部模块的 ESM 依赖项转换为单个模块。
有些包将它们的 ES 模块构建为许多独自的文件,彼此导入。例如,lodash-es
有超过 600 个内置模块!当咱们执行 import {debounce} from 'lodash-es'
时,浏览器同时收回 600 多个 HTTP 申请!即便服务器可能轻松解决它们,但大量申请会导致浏览器端的网络拥塞,使页面加载变得显著迟缓。
通过将 lodash-es
预构建成单个模块,当初咱们只须要一个 HTTP 申请!
留神
依赖预构建仅实用于开发模式,并应用esbuild
将依赖项转换为 ES 模块。在生产构建中,将应用@rollup/plugin-commonjs
3.2 预构建整体流程(流程图)
接下来会依据流程图的外围流程进行源码剖析
3.3 预构建整体流程(源码整体概述)
Vite 会将预构建的依赖缓存到 node_modules/.vite。它依据几个源来决定是否须要从新运行预构建步骤:
- 包管理器的 lockfile 内容,例如 package-lock.json,yarn.lock,pnpm-lock.yaml,或者 bun.lockb
- 补丁文件夹的批改工夫
- 可能在 vite.config.js 相干字段中配置过的
- NODE_ENV 中的值
只有在上述其中一项产生更改时,才须要从新运行预构建。
咱们会先检测是否有预构建的缓存,如果没有缓存,则开始预构建:发现文件依赖并寄存于 deps
,而后将deps
打包到 node_modules/.vite
中
async function createServer(inlineConfig = {}) {const config = await resolveConfig(inlineConfig, 'serve');
if (isDepsOptimizerEnabled(config, false)) {
// start optimizer in the background, we still need to await the setup
await initDepsOptimizer(config);
}
//...
}
async function initDepsOptimizer(config, server) {if (!getDepsOptimizer(config, ssr)) {await createDepsOptimizer(config, server);
}
}
async function createDepsOptimizer(config, server) {
// 第一步:3.4 获取缓存
const cachedMetadata = await loadCachedDepOptimizationMetadata(config, ssr);
// 第二步:3.5 没有缓存时进行依赖扫描
const deps = {};
discover = discoverProjectDependencies(config);
const deps = await discover.result;
// 第三步:3.6 没有缓存时进行依赖扫描,而后进行依赖打包到 node_modules/.vite
optimizationResult = runOptimizeDeps(config, knownDeps);
}
3.4 获取缓存 loadCachedDepOptimizationMetadata
async function createDepsOptimizer(config, server) {
// 第一步:3.4 获取缓存
const cachedMetadata = await loadCachedDepOptimizationMetadata(config, ssr);
if (!cachedMetadata) {
// 第二步:3.5 没有缓存时进行依赖扫描
discover = discoverProjectDependencies(config);
const deps = await discover.result;
// 第三步:3.6 依赖扫描后进行打包 runOptimizeDeps(),存储到 node_modules/.vite
optimizationResult = runOptimizeDeps(config, knownDeps);
}
}
async function loadCachedDepOptimizationMetadata(config, ssr, force = config.optimizeDeps.force, asCommand = false) {const depsCacheDir = getDepsCacheDir(config, ssr);
if (!force) {
// 3.4.1 获取_metadata.json 文件数据
let cachedMetadata;
const cachedMetadataPath = path$o.join(depsCacheDir, '_metadata.json');
cachedMetadata = parseDepsOptimizerMetadata(await fsp.readFile(cachedMetadataPath, 'utf-8'), depsCacheDir);
// 3.4.2 比对 hash 值
if (cachedMetadata && cachedMetadata.hash === getDepHash(config, ssr)) {return cachedMetadata;}
}
// 3.4.3 清空缓存
await fsp.rm(depsCacheDir, { recursive: true, force: true});
}
3.4.1 获取_metadata.json 文件数据
通过 getDepsCacheDir()
获取 node_modules/.vite/deps
的缓存目录,而后拼接 _metadata.json
数据,读取文件并且进行简略的整顿 parseDepsOptimizerMetadata()
后造成校验缓存是否过期的数据
const depsCacheDir = getDepsCacheDir(config, ssr);
let cachedMetadata;
const cachedMetadataPath = path$o.join(depsCacheDir, '_metadata.json');
cachedMetadata = parseDepsOptimizerMetadata(await fsp.readFile(cachedMetadataPath, 'utf-8'), depsCacheDir);
上面
metadata
的数据结构是在_metadata.json
数据结构的根底上叠加一些数据
function parseDepsOptimizerMetadata(jsonMetadata, depsCacheDir) {const { hash, browserHash, optimized, chunks} = JSON.parse(jsonMetadata, (key, value) => {if (key === 'file' || key === 'src') {return normalizePath$3(path$o.resolve(depsCacheDir, value));
}
return value;
});
if (!chunks ||
Object.values(optimized).some((depInfo) => !depInfo.fileHash)) {
// outdated _metadata.json version, ignore
return;
}
const metadata = {
hash,
browserHash,
optimized: {},
discovered: {},
chunks: {},
depInfoList: [],};
//... 解决 metadata
return metadata;
}
3.4.2 比对 hash 值
if (cachedMetadata && cachedMetadata.hash === getDepHash(config, ssr)) {return cachedMetadata;}
最终生成预构建缓存时,
_metadata.json
中的hash
是如何计算的?是依据什么文件失去的 hash 值?
getDepHash()
的逻辑也不简单,次要的流程为:
- 先进行
lockfileFormats[i]
文件是否存在的检测,比方存在yarn.lock
,那么就间接返回yarn.lock
,赋值给content
- 检测是否存在
patches
文件夹,进行content += stat.mtimeMs.toString()
- 将一些配置数据进行
JSON.stringify()
增加到content
的前面 - 最终应用
content
造成对应的hash
值,返回该hash
getDepHash()
的逻辑总结下来就是:
- 包管理器的锁文件内容,例如 package-lock.json,yarn.lock,pnpm-lock.yaml,或者 bun.lockb
- 补丁文件夹的批改工夫
- vite.config.js 中的相干字段
- NODE_ENV 的值
只有在上述其中一项产生更改时,
hash
才会发生变化,才须要从新运行预构建
const lockfileFormats = [{ name: 'package-lock.json', checkPatches: true},
{name: 'yarn.lock', checkPatches: true},
{name: 'pnpm-lock.yaml', checkPatches: false},
{name: 'bun.lockb', checkPatches: true},
];
const lockfileNames = lockfileFormats.map((l) => l.name);
function getDepHash(config, ssr) {
// 第一局部:获取配置文件初始化 content
const lockfilePath = lookupFile(config.root, lockfileNames);
let content = lockfilePath ? fs$l.readFileSync(lockfilePath, 'utf-8') : '';
// 第二局部:检测是否存在 patches 文件夹,减少 content 的内容
if (lockfilePath) {
//...
const fullPath = path$o.join(path$o.dirname(lockfilePath), 'patches');
const stat = tryStatSync(fullPath);
if (stat?.isDirectory()) {content += stat.mtimeMs.toString();
}
}
// 第三局部:将配置增加到 content 的前面
const optimizeDeps = getDepOptimizationConfig(config, ssr);
content += JSON.stringify({
mode: process.env.NODE_ENV || config.mode,
//...
});
return getHash(content);
}
function getHash(text) {return createHash$2('sha256').update(text).digest('hex').substring(0, 8);
}
拿到 getDepHash()
计算失去的 hash
,跟目前node_modules/.vite/deps/_metadata.json
的hash
属性进行比对,如果一样阐明预构建缓存没有任何扭转,无需从新预构建,间接应用上次预构建缓存即可
上面是
_metadata.json
的示例
{
"hash": "2b04a957",
"browserHash": "485313cf",
"optimized": {
"lodash-es": {
"src": "../../lodash-es/lodash.js",
"file": "lodash-es.js",
"fileHash": "d69f60c8",
"needsInterop": false
},
"vue": {
"src": "../../vue/dist/vue.runtime.esm-bundler.js",
"file": "vue.js",
"fileHash": "98c38b51",
"needsInterop": false
}
},
"chunks": {}}
3.4.3 清空缓存
如果缓存过期或者带了 force=true
参数,代表缓存不可用,应用 fsp.rm
清空缓存文件夹
"dev": "vite --force"
代表不应用缓存
await fsp.rm(depsCacheDir, { recursive: true, force: true});
3.5 没有缓存时进行依赖扫描 discoverProjectDependencies()
async function createDepsOptimizer(config, server) {
// 第一步:3.4 获取缓存
const cachedMetadata = await loadCachedDepOptimizationMetadata(config, ssr);
if (!cachedMetadata) {
// 第二步:3.5 没有缓存时进行依赖扫描
discover = discoverProjectDependencies(config);
const deps = await discover.result;
// 第三步:3.6 依赖扫描后进行打包 runOptimizeDeps(),存储到 node_modules/.vite
optimizationResult = runOptimizeDeps(config, knownDeps);
}
}
function discoverProjectDependencies(config) {const { cancel, result} = scanImports(config);
return {
cancel,
result: result.then(({deps, missing}) => {const missingIds = Object.keys(missing);
return deps;
}),
};
}
discoverProjectDependencies
理论调用就是scanImports
function scanImports(config) {
// 3.5.1 计算入口文件 computeEntries
const esbuildContext = computeEntries(config).then((computedEntries) => {
entries = computedEntries;
// 3.5.2 打包入口文件 esbuild 插件初始化
return prepareEsbuildScanner(config, entries, deps, missing, scanContext);
});
// 3.5.3 开始打包
const result = esbuildContext
.then((context) => {...}
return {result, cancel}
}
3.5.1 计算入口文件 computeEntries()
由官网文档对于 optimizeDeps.entries 能够晓得,
- 默认状况下,Vite 会抓取你的
index.html
来检测须要预构建的依赖项(疏忽node_modules
、build.outDir
、__tests__
和coverage
) - 如果指定了
build.rollupOptions?.input
,即在vite.config.js
中配置rollupOptions
参数,指定了入口文件,Vite 将转而去抓取这些入口点 - 如果这两者都不合你意,则能够应用
optimizeDeps.entries
指定自定义条目——该值须要遵循 fast-glob 模式,或者是绝对于 Vite 我的项目根目录的匹配模式数组,能够简略了解为入口文件匹配的正则表达式,能够进行多个文件类型的匹配
如果应用
optimizeDeps.entries
,留神默认只有node_modules
和build.outDir
文件夹会被疏忽。如果还需疏忽其余文件夹,你能够在模式列表中应用以 ! 为前缀的、用来匹配疏忽项的模式optimizeDeps.entries
具体的示例如下所示,具体能够参考 fast-glob 模式
- file-{1..3}.js — matches files: file-1.js, file-2.js, file-3.js.
- file-(1|2) — matches files: file-1.js, file-2.js.
本文中咱们将间接应用默认的模式,也就是 globEntries('**/*.html', config)
进行剖析,会间接匹配到 index.html
入口文件
function computeEntries(config) {let entries = [];
const explicitEntryPatterns = config.optimizeDeps.entries;
const buildInput = config.build.rollupOptions?.input;
if (explicitEntryPatterns) {entries = await globEntries(explicitEntryPatterns, config);
} else if (buildInput) {const resolvePath = (p) => path$o.resolve(config.root, p);
if (typeof buildInput === 'string') {entries = [resolvePath(buildInput)];
} else if (Array.isArray(buildInput)) {entries = buildInput.map(resolvePath);
} else if (isObject$2(buildInput)) {entries = Object.values(buildInput).map(resolvePath);
}
} else {entries = await globEntries('**/*.html', config);
}
entries = entries.filter((entry) => isScannable(entry) && fs$l.existsSync(entry));
return entries;
}
3.5.2 打包入口文件 esbuild 插件初始化 prepareEsbuildScanner
在下面的剖析中,咱们执行完 3.5.1
步骤的 computeEntries()
后,会执行 prepareEsbuildScanner()
的插件筹备工作
function scanImports(config) {
// 3.5.1 计算入口文件 computeEntries
const esbuildContext = computeEntries(config).then((computedEntries) => {
entries = computedEntries;
// 3.5.2 打包入口文件 esbuild 插件初始化
return prepareEsbuildScanner(config, entries, deps, missing, scanContext);
});
// 3.5.3 开始打包
const result = esbuildContext
.then((context) => {...}
return {result, cancel}
}
上面将会
prepareEsbuildScanner()
的流程开展剖析
在计算出入口文件后,前面就是启动 esbuild
插件进行打包,因为打包流程波及的流程比较复杂,咱们在 3.5 的剖析中,只会剖析预构建相干的流程局部:
- 先进行了
vite
插件的初始化:container = createPluginContainer()
- 而后将
vite
插件container
作为参数传递到esbuild
插件中,后续逻辑须要应用container
提供的一些能力 - 最终进行
esbuild
打包的初始化,应用3.5.1 计算入口文件 computeEntries
拿到的入口文件作为stdin
,即esbuild
的input
,而后将刚刚注册的plugin
放入到plugins
属性中
esbuild 相干知识点能够参考【根底】esbuild 应用详解或者官网文档
async function prepareEsbuildScanner(config, entries, deps, missing, scanContext) {
// 第一局部: container 初始化
const container = await createPluginContainer(config);
if (scanContext?.cancelled)
return;
// 第二局部: esbuildScanPlugin()
const plugin = esbuildScanPlugin(config, container, deps, missing, entries);
const {plugins = [], ...esbuildOptions } = config.optimizeDeps?.esbuildOptions ?? {};
return await esbuild.context({absWorkingDir: process.cwd(),
write: false,
stdin: {contents: entries.map((e) => `import ${JSON.stringify(e)}`).join('\n'),
loader: 'js',
},
bundle: true,
format: 'esm',
logLevel: 'silent',
plugins: [...plugins, plugin],
...esbuildOptions,
});
}
第一局部 createPluginContainer
插件治理类 container 初始化
async function createServer(inlineConfig = {}) {const config = await resolveConfig(inlineConfig, 'serve');
// config 蕴含了 plugins 这个属性
const container = await createPluginContainer(config, moduleGraph, watcher);
}
// ======= 初始化所有 plugins =======start
function resolveConfig() {resolved.plugins = await resolvePlugins(resolved, prePlugins, normalPlugins, postPlugins);
return resolved;
}
async function resolvePlugins(config, prePlugins, normalPlugins, postPlugins) {
return [resolvePlugin({ ...}),
htmlInlineProxyPlugin(config),
cssPlugin(config),
...
].filter(Boolean);
}
// ======= 初始化所有 plugins =======end
function createPluginContainer(config, moduleGraph, watcher) {const { plugins, logger, root, build: { rollupOptions}, } = config;
const {getSortedPluginHooks, getSortedPlugins} = createPluginHookUtils(plugins);
const container = {async resolveId() {//... 应用了 getSortedPlugins()这个办法,这个办法里有 plugins
}
}
return container;
}
function createPluginHookUtils(plugins) {function getSortedPlugins(hookName) {if (sortedPluginsCache.has(hookName))
return sortedPluginsCache.get(hookName);
// 依据 hookName,即对象属性名,拼接对应的 key-value 的 plugin
const sorted = getSortedPluginsByHook(hookName, plugins);
sortedPluginsCache.set(hookName, sorted);
return sorted;
}
}
function getSortedPluginsByHook(hookName, plugins) {const pre = [];
const normal = [];
const post = [];
for (const plugin of plugins) {const hook = plugin[hookName];
if (hook) {//...pre.push(plugin)
//...normal.push(plugin)
//...post.push(plugin)
}
}
return [...pre, ...normal, ...post];
}
如下面代码所示,在 createServer()
->resolveConfig()
->resolvePlugins()
的流程中,会进行 vite
插件的注册
vite
插件具体有什么呢?
所有的插件都放在 vite
源码的 src/node/plugins/**
中,每一个插件都会有对应的name
,比方上面这个插件vite:css
罕用办法 container.resolveId()
从下面插件初始化的剖析中,咱们能够晓得,getSortedPlugins('resolveId')
就是检测该插件是否有 resolveId
这个属性,如果有,则增加到返回的数组汇合中,比方有 10 个插件中有 5 个插件具备 resolveId
属性,那么最终 getSortedPlugins('resolveId')
拿到的就是这 5 个插件的 Array 数据
因而 container.resolveId()
中运行插件的个数不止一个,但并不是每一个插件都能返回对应的后果 result
,即const result = await handler.call(...)
可能为undefined
当有插件解决后 result
不为 undefined
时,会间接执行 break
,而后返回container.resolveId()
的后果
//getSortedPlugins 最终调用的就是 getSortedPluginsByHook
function getSortedPluginsByHook(hookName, plugins) {const pre = [];
const normal = [];
const post = [];
for (const plugin of plugins) {const hook = plugin[hookName];
if (hook) {//...pre.push(plugin)
//...normal.push(plugin)
//...post.push(plugin)
}
}
return [...pre, ...normal, ...post];
}
async resolveId(rawId, importer = join$2(root, 'index.html'), options) {for (const plugin of getSortedPlugins('resolveId')) {if (!plugin.resolveId)
continue;
if (skip?.has(plugin))
continue;
const handler = 'handler' in plugin.resolveId
? plugin.resolveId.handler
: plugin.resolveId;
const result = await handler.call(...);
if (!result)
continue;
if (typeof result === 'string') {id = result;} else {
id = result.id;
Object.assign(partial, result);
}
break;
}
if (id) {partial.id = isExternalUrl(id) ? id : normalizePath$3(id);
return partial;
} else {return null;}
}
第二局部初始化 dep-scan 的 vite 插件
esbuild
插件中,提供了两种办法onResolve
和onLoad
onResolve
和onLoad
的第 1 个参数为filter
(必填)和namespaces
(可选)
钩子函数必须提供过滤器filter
正则表达式,但也能够抉择提供namespaces
以进一步限度匹配的门路
依照 esbuild
插件的 onResolve()
和onLoad()
流程进行一系列解决
async function prepareEsbuildScanner(...) {const container = await createPluginContainer(config);
if (scanContext?.cancelled)
return;
const plugin = esbuildScanPlugin(...);
//... 省略 esbuild 打包配置
}
const htmlTypesRE = /\.(html|vue|svelte|astro|imba)$/;
function esbuildScanPlugin(config, container, depImports, missing, entries) {const resolve = async (id, importer, options) => {// 第一局部内容:container.resolveId()
const resolved = await container.resolveId();
const res = resolved?.id;
return res;
};
return {
name: 'vite:dep-scan',
setup(build) {
// 第二个局部内容:插件执行流程
build.onResolve({filter: htmlTypesRE}, async ({path, importer}) => {const resolved = await resolve(path, importer);
return {
path: resolved,
namespace: 'html',
};
});
build.onLoad({filter: htmlTypesRE, namespace: 'html'}, async ({path}) => {...});
//....
}
}
}
在
esbuildScanPlugin()
的执行逻辑中,如下面代码块正文所示,分为两局部内容:
container.resolveId()
的具体逻辑,波及到container
的初始化,具体的插件执行等逻辑build.onResolve
和build.onLoad
的具体逻辑上面将应用简略的具体实例依照这两局部内容开展剖析:
从index.html
->main.js
->import vue
的流程进行剖析
index.html 文件触发 container.resolveId()
当咱们执行入口
index.html
文件的打包解析时,咱们通过调试能够晓得,咱们最终会命中插件:vite:resolve
的解决,接下来咱们将针对这个插件开展剖析
传入参数 id
就是门路,比方 "/Users/wcbbcc/blog/Frontend-Articles/vite-debugger/index.html"
或者 "/src/main.js"
传入参数 importer
就是援用它的模块,比方 "stdin"
或者"/Users/wcbbcc/blog/Frontend-Articles/vite-debugger/index.html"
而 container.resolveId()
逻辑就是依据目前门路的类型,比方是绝对路径、相对路径、模块路或者其余门路类型,而后进行不同的解决,最终返回拼凑好的残缺门路
return {
name: 'vite:resolve',
async resolveId(id, importer, resolveOpts) {
//...
// URL
// /foo -> /fs-root/foo
if (asSrc && id[0] === '/' && (rootInRoot || !id.startsWith(root))) {...}
// relative
if (id[0] === '.' ||
((preferRelative || importer?.endsWith('.html')) &&
startsWithWordCharRE.test(id))) {...}
// drive relative fs paths (only windows)
if (isWindows$4 && id[0] === '/') {...}
// absolute fs paths
if (isNonDriveRelativeAbsolutePath(id) &&
(res = tryFsResolve(id, options))) {...}
// external
if (isExternalUrl(id)) {...}
// data uri: pass through (this only happens during build and will be
// handled by dedicated plugin)
if (isDataUrl(id)) {return null;}
// bare package imports, perform node resolve
if (bareImportRE.test(id)) {...}
}
}
index.html 文件触发onResolve 和 onLoad
一开始咱们打包 index.html
入口文件时
- 触发
filter: htmlTypesRE
的筛选,命中onResolve()
的解决逻辑,返回namespace: 'html'
和整顿好的门路path
传递给下一个阶段 - 触发
filter: htmlTypesRE
和namespace: 'html'
的筛选条件,命中onLoad()
的解决逻辑,应用regex.exec(raw)
匹配出index.html
中的<script>
标签,拿出外面对应的src
的值,最终返回content:"import'/src/main.js'\n export default {}"
每个 未标记 为
external:true
的惟一门路 / 命名空间的文件加载实现会触发onLoad()
回调,onLoad()
的工作是返回模块的内容并通知 esbuild 如何解释它
return {
name: 'vite:dep-scan',
setup(build) {
// html types: extract script contents -----------------------------------
build.onResolve({filter: htmlTypesRE}, async ({path, importer}) => {const resolved = await resolve(path, importer);
return {
path: resolved,
namespace: 'html',
};
});
// extract scripts inside HTML-like files and treat it as a js module
build.onLoad({filter: htmlTypesRE, namespace: 'html'}, async ({path}) => {let raw = await fsp.readFile(path, 'utf-8');
const isHtml = path.endsWith('.html');
//scriptModuleRE = /(<script\b[^>]+type\s*=\s*(?:"module"|'module')[^>]*>)(.*?)<\/script>/gis;
//scriptRE = /(<script(?:\s[^>]*>|>))(.*?)<\/script>/gis;
const regex = isHtml ? scriptModuleRE : scriptRE;
while ((match = regex.exec(raw))) {const [, openTag, content] = match;
let loader = 'js';
if (lang === 'ts' || lang === 'tsx' || lang === 'jsx') {loader = lang;} else if (path.endsWith('.astro')) {loader = 'ts';}
//srcRE = /\bsrc\s*=\s*(?:"([^"]+)"|'([^']+)'|([^\s'">]+))/i;
const srcMatch = openTag.match(srcRE);
if (srcMatch) {const src = srcMatch[1] || srcMatch[2] || srcMatch[3];
js += `import ${JSON.stringify(src)}\n`;
} else if (content.trim()) {//...}
}
return {
loader: 'js',
contents: js,
};
});
}
}
onLoad()
返回content:"import'/src/main.js'\n export default {}"
后再度触发onResolve()
的执行
main.js 文件触发container.resolveId()
从下面 index.html
对于插件 vite:resolve
的resolveId()
剖析,咱们晓得会进行多种模式门路的解决,而 src/main.js
则触发 /foo -> /fs-root/foo
的解决,逻辑就是合并root
,而后造成最终的门路返回
name: 'vite:resolve',
async resolveId(id, importer, resolveOpts) {
//...
// /foo -> /fs-root/foo
if (asSrc && id[0] === '/' && (rootInRoot || !id.startsWith(root))) {const fsPath = path$o.resolve(root, id.slice(1));
if ((res = tryFsResolve(fsPath, options))) {return ensureVersionQuery(res, id, options, depsOptimizer);
}
}
}
main.js 文件触发onResolve 和 onLoad
此时 id
="/src/main.js"
,通过resolve()
后拼接成残缺的门路 resolved
="xxxxx/vite-debugger/src/main.js"
,最终返回path
="xxxxx/vite-debugger/src/main.js"
,而后触发onLoad()
解析 main.js
的内容进行返回
build.onResolve({filter: /.*/,}, async ({path: id, importer, pluginData}) => {
// id="/src/main.js"->"xxxxx/vite-debugger/src/main.js"
const resolved = await resolve(id, importer, ...);
if (resolved) {
//...
return {path: path$o.resolve(cleanUrl(resolved)),
namespace,
};
}
});
build.onLoad({filter: JS_TYPES_RE}, async ({path: id}) => {let ext = path$o.extname(id).slice(1);
let contents = await fsp.readFile(id, 'utf-8');
const loader = config.optimizeDeps?.esbuildOptions?.loader?.[`.${ext}`] ||
ext;
return {
loader,
contents,
};
});
onLoad()
解析 main.js
的内容如下所示,实质也就是读取对应 main.js
自身的内容
return {
loader: "js",
content: `import {createApp} from "vue";
import App from "./App";
import {toString, toArray} from "lodash-es";
console.log(toString(123));
console.log(toArray([]));
createApp(App).mount("#app");`
}
onLoad()
返回content
中蕴含了多个import
语句,再度触发onResolve()
的执行
.vue 文件触发container.resolveId()
此时 id
="vue"
,会触发vite:resolve
插件的 resolveId()
办法解决,最终触发了 tryNodeResolve()
的解决,因为代码十分繁多,精简过后的代码如下所示:
resolvePackageData()
: 获取"vue"
所在的package.json
数据resolvePackageEntry()
: 依据package.json
数据的字段去获取对应的入口
name:"vite:resolve"
async resolveId() {if (bareImportRE.test(id)) {if ((res = tryNodeResolve(id, importer, options, targetWeb, depsOptimizer, ssr, external))) {return res;}
}
}
function tryNodeResolve(id, ...) {const { root, ...} = options;
let basedir;
//... 省略一系列条件
basedir = root;
const pkg = resolvePackageData(pkgId, basedir, preserveSymlinks, packageCache);
const resolveId = deepMatch ? resolveDeepImport : resolvePackageEntry;
const unresolvedId = deepMatch ? '.' + id.slice(pkgId.length) : pkgId;
let resolved = resolveId(unresolvedId, pkg, targetWeb, options);
if (!options.ssrOptimizeCheck &&
(!isInNodeModules(resolved) || // linked
!depsOptimizer || // resolving before listening to the server
options.scan) // initial esbuild scan phase
) {return { id: resolved};
}
}
function resolvePackageData(pkgName, basedir, preserveSymlinks = false, packageCache) {while (basedir) {const pkg = path$o.join(basedir, 'node_modules', pkgName, 'package.json');
if (fs$l.existsSync(pkg)) {const pkgPath = preserveSymlinks ? pkg : safeRealpathSync(pkg);
const pkgData = loadPackageData(pkgPath);
return pkgData;
}
const nextBasedir = path$o.dirname(basedir);
if (nextBasedir === basedir)
break;
basedir = nextBasedir;
}
return null;
}
resolvePackageEntry()
的代码逻辑十分繁多,遍历了 package.json
多个字段,找对应的入口,次要经验了:
exports
browser/module
field
main
最终找到对应的 entry
,跟dir
进行合并,造成最终的门路,此时
dir
="/Users/wcbbcc/blog/Frontend-Articles/vite-debugger/node_modules/vue"
entry
="./dist/vue.runtime.esm-bundler.js"
function resolvePackageEntry(id, { dir, data, setResolvedCache, getResolvedCache}, targetWeb, options) {
let entryPoint;
if (data.exports) {entryPoint = resolveExportsOrImports(data, '.', options, targetWeb, 'exports');
}
//... 省略 browser/module、field 的逻辑判断
entryPoint || (entryPoint = data.main);
const entryPoints = entryPoint
? [entryPoint]
: ['index.js', 'index.json', 'index.node'];
for (let entry of entryPoints) {
//...
const entryPointPath = path$o.join(dir, entry);
const resolvedEntryPoint = tryFsResolve(entryPointPath, options, true, true, skipPackageJson);
if (resolvedEntryPoint) {return resolvedEntryPoint;}
}
}
.vue 文件触发onResolve
此时 id
="vue"
,通过resolve()
后拼接成残缺的门路resolved
="xxx/vite-debugger/node_modules/vue/dist/vue.runtime.esm-bundler.js"
最终进行depImports["vue"]
="xxx/vite-debugger/node_modules/vue/dist/vue.runtime.esm-bundler.js"
最终返回 externalUnlessEntry({path: id})
={external: true,path: "vue"}
,因为external
为true
,因而不会触发 onLoad()
的执行
// bare imports: record and externalize ----------------------------------
build.onResolve({
// avoid matching windows volume
filter: /^[\w@][^:]/,
}, async ({path: id, importer, pluginData}) => {const resolved = await resolve(id, importer, { ...});
if (resolved) {if (isInNodeModules(resolved) || include?.includes(id)) {if (isOptimizable(resolved, config.optimizeDeps)) {depImports[id] = resolved;
}
return externalUnlessEntry({path: id});
}
//...
}
});
同理
"./App"
的解析能够参考"main.js"
的流程剖析:先进行门路的整顿,而后获取具体的 source 内容,再度触发解析"lodash-es"
的解析能够参考"vue"
的流程剖析:先进行门路的整顿,而后寄存在depImports
中,最初完结流程
第三局部返回 esbuild 插件打包代码
esbuild.context()
是esbuild
提供的 API,相比拟esbuild.build()
,应用给定context
实现的所有构建共享雷同的构建选项,并且后续构建是增量实现的(即它们重用以前构建的一些工作以进步性能)
间接返回return await esbuild.context({})
,实质就是返回一个Promise
async function prepareEsbuildScanner(...) {const container = await createPluginContainer(config);
if (scanContext?.cancelled)
return;
const plugin = esbuildScanPlugin(...);
return await esbuild.context({absWorkingDir: process.cwd(),
write: false,
stdin: {contents: entries.map((e) => `import ${JSON.stringify(e)}`).join('\n'),
loader: 'js',
},
bundle: true,
format: 'esm',
logLevel: 'silent',
plugins: [...plugins, plugin],
...esbuildOptions,
});
}
3.5.3 开始打包
在下面 esbuild 插件打包: esbuild.context()
的剖析中,咱们晓得,esbuildContext
实质就是await esbuild.context()
依据 esbuild 官网文档的形容,
Rebuild
模式容许您手动调用构建
在经验了 3.5.1
和3.5.2
步骤之后,咱们开始了 context.rebuild
的执行,也就是触发 3.5.2
步骤中的 esbuild
打包流程
打包过程中,失去所有 node_moduels
的依赖放入到 deps
对象中,打包实现后,将 deps
数据返回
function scanImports(config) {const deps = {};
// 3.5.1 计算入口文件 computeEntries
const esbuildContext = computeEntries(config).then((computedEntries) => {
entries = computedEntries;
// 3.5.2 打包入口文件 esbuild 插件初始化
return prepareEsbuildScanner(config, entries, deps, missing, scanContext);
});
const result = esbuildContext
.then((context) => {
// 3.5.3 开始打包
return context
.rebuild()
.then(() => {
return {deps: orderedDependencies(deps),
missing,
};
});
})
return {result, cancel}
}
3.6 依赖扫描后进行打包 runOptimizeDeps()
在 3.5.2
步骤的剖析中,咱们晓得对于 node_modules
的打包,咱们会存储到 depImports["vue"]
="xxx/vite-debugger/node_modules/vue/dist/vue.runtime.esm-bundler.js"
,而后scanImports()
会返回存储的所有 deps
数据
function scanImports(config) {const deps = {};
// 3.5.1 计算入口文件 computeEntries
const esbuildContext = computeEntries(config).then((computedEntries) => {
entries = computedEntries;
// 3.5.2 打包入口文件 esbuild 插件初始化
return prepareEsbuildScanner(config, entries, deps, missing, scanContext);
});
const result = esbuildContext
.then((context) => {
// 3.5.3 开始打包
return context
.rebuild()
.then(() => {
return {deps: orderedDependencies(deps),
missing,
};
});
})
return {result, cancel}
}
在 3.6
步骤中,咱们提取出所有获取到的 deps
数据,即discover.result
,而后执行runOptimizeDeps()
async function createDepsOptimizer(config, server) {
// 第一步:3.4 获取缓存
const cachedMetadata = await loadCachedDepOptimizationMetadata(config, ssr);
if (!cachedMetadata) {
// 第二步:3.5 没有缓存时进行依赖扫描
discover = discoverProjectDependencies(config);
const deps = await discover.result;
// 第三步:3.6 依赖扫描后进行打包 runOptimizeDeps(),存储到 node_modules/.vite
optimizationResult = runOptimizeDeps(config, knownDeps);
}
}
runOptimizeDeps()
的代码中,次要分为两个局部:
- 获取
xxx/node_modules/.vite/deps
的残缺门路depsCacheDir
,而后初始化_metadata.json
,往_metadata.json
写入打包的缓存信息 prepareEsbuildOptimizerRun()
执行esbuild
打包预构建的库到.vite/deps
文件夹上面
上面代码曾经详细分析了
_metadata.json
每一个属性的写入逻辑,接下来将着重剖析下prepareEsbuildOptimizerRun()
的打包逻辑
function runOptimizeDeps() {
// 失去缓存数据的门路,也就是.vite/deps 文件夹的残缺门路
const depsCacheDir = getDepsCacheDir(resolvedConfig, ssr);
// 创立.vite/package.json 文件
fs$l.writeFileSync(path$o.resolve(processingCacheDir, 'package.json'), `{\n "type": "module"\n}\n`);
// 初始化_metadata.json
const metadata = initDepsOptimizerMetadata(config, ssr);
// 写入_metadata.json 的 browserHash 属性
metadata.browserHash = getOptimizedBrowserHash(metadata.hash, depsFromOptimizedDepInfo(depsInfo));
// esbuild 打包初始化
const preparedRun = prepareEsbuildOptimizerRun(resolvedConfig, depsInfo, ssr, processingCacheDir, optimizerContext);
const runResult = preparedRun.then(({context, idToExports}) => {
// 执行 esbuild 打包
return context
.rebuild()
.then((result) => {
//... 写入_metadata.json 的 optimized 属性
//... 写入_metadata.json 的 chunks 属性
});
});
return {async cancel() {
optimizerContext.cancelled = true;
const {context} = await preparedRun;
await context?.cancel();
cleanUp();},
result: runResult,
};
}
function initDepsOptimizerMetadata(config, ssr, timestamp) {const hash = getDepHash(config, ssr);
return {
hash,
browserHash: getOptimizedBrowserHash(hash, {}, timestamp),
optimized: {},
chunks: {},
discovered: {},
depInfoList: [],};
}
3.6.1 具体打包 node_modules 库进行到.vite/deps 的逻辑
次要是进行打包前的参数筹备,其中有几个参数须要留神下:
entryPoints
: 将node_modules
的依赖库的src
平铺成为数组的模式作为esbuild.context()
打包的入口entryPoints
outdir
: 将node_modeuls/.vite/deps
文件夹作为输入的目录bundle
: 设置为true
时,打包一个文件意味着将任何导入的依赖项内联到文件中。这个过程是递归的,因为依赖的依赖(等等)也将被内联。默认状况下,esbuild
将不会打包输出的文件,也就是vue.js
的所有import
依赖都会打包到vue.js
中,而不会应用import
的模式引入其它依赖库format
: 打包文件输入格局为ES Module
format
有三个可能的值:iife
、cjs
与esm
async function prepareEsbuildOptimizerRun(resolvedConfig, depsInfo, ssr, processingCacheDir, optimizerContext) {for (const id in depsInfo) {const src = depsInfo[id].src;
const exportsData = await (depsInfo[id].exportsData ??
extractExportsData(src, config, ssr));
flatIdDeps[flatId] = src;
idToExports[id] = exportsData;
}
plugins.push(esbuildDepPlugin(flatIdDeps, external, config, ssr));
const context = await esbuild.context({entryPoints: Object.keys(flatIdDeps),
outdir: processingCacheDir,
bundle: true,
format: 'esm',
plugins,
...
});
return {context, idToExports};
}
function runOptimizeDeps() {
// esbuild 打包初始化
const preparedRun = prepareEsbuildOptimizerRun(resolvedConfig, depsInfo, ssr, processingCacheDir, optimizerContext);
const runResult = preparedRun.then(({context, idToExports}) => {
// 执行 esbuild 打包
return context
.rebuild()
.then((result) => {
//... 写入_metadata.json 的 optimized 属性
//... 写入_metadata.json 的 chunks 属性
});
})
}
3.7 预构建目标实现原理剖析
针对的是 node_modules
依赖的预构建,不包含理论的业务代码
3.7.1 CommonJS 和 UMD 兼容性
CommonJS 代码如何革新,从而反对 ESModule 模式导入?
在 https://cn.vitejs.dev/guide/dep-pre-bundling.html 官网文档中,应用
React
作为例子,咱们间接应用React
作为示例跑起来
将 vite
预构建的 react.js
进行整顿,造成上面的代码块:
var __getOwnPropNames = Object.getOwnPropertyNames;
var __commonJS = function (cb, mod) {return function __require() {
return mod ||
(0, cb[__getOwnPropNames(cb)[0]])
((mod = { exports: {} }).exports, mod), mod.exports;
};
}
// node_modules/react/cjs/react.development.js
var require_react_development = __commonJS({"node_modules/react/cjs/react.development.js"(exports, module) {
"use strict";
if (true) {(function () {
//react 的 common.js 代码
exports.xx = xxx;
exports.xxxx = xxxxx;
})();}
}
});
// node_modules/react/index.js
var require_react = __commonJS({"node_modules/react/index.js": function (exports, module) {if (false) {module.exports = null;} else {module.exports = require_react_development();
}
}
});
export default require_react();
手动创立 mod
和mod.exports
传入 (exports, module)
中,此时
mod
=module
mod.exports
=exports
在 CommonJs
代码中,比方上面示例的 cjs/react.development.js
中,会应用传入的 exports
进行变量的赋值
最终输入export default module.exports
,即export default {xxx, xxx, xxx, xxx}
从源码层级,是如何实现下面的转化流程?
从源码中,咱们能够看到,最终在第二次 esbuild
打包中,应用 format: 'esm'
,利用esbuild
提供的能力将 cjs
格局转化为 esm
格局
async function prepareEsbuildOptimizerRun(resolvedConfig, depsInfo, ssr, processingCacheDir, optimizerContext) {for (const id in depsInfo) {const src = depsInfo[id].src;
const exportsData = await (depsInfo[id].exportsData ??
extractExportsData(src, config, ssr));
flatIdDeps[flatId] = src;
idToExports[id] = exportsData;
}
plugins.push(esbuildDepPlugin(flatIdDeps, external, config, ssr));
const context = await esbuild.context({entryPoints: Object.keys(flatIdDeps),
outdir: processingCacheDir,
bundle: true,
format: 'esm',
plugins,
...
});
return {context, idToExports};
}
function runOptimizeDeps() {
// esbuild 打包初始化
const preparedRun = prepareEsbuildOptimizerRun(resolvedConfig, depsInfo, ssr, processingCacheDir, optimizerContext);
const runResult = preparedRun.then(({context, idToExports}) => {
// 执行 esbuild 打包
return context
.rebuild()
.then((result) => {
//... 写入_metadata.json 的 optimized 属性
//... 写入_metadata.json 的 chunks 属性
});
})
}
CommonJS->ESModule 须要留神的点
node_moduels
的依赖库如果自身是 commonjs
格局,会应用 esbuild
主动转化为 esm
格局,然而咱们本人编写的业务代码,比方:
咱们能够发现,尽管咱们本人编写的业务代码是齐全模拟 react.js
的导入模式从新书写了一遍,然而间接运行就是报错
通过下面的源码调试,其实咱们曾经可能猜到,之所以报错,就是没有转化 indexB.js
为esmodule
格局
- 在第一次 esbuild 打包时,会应用
index.html
作为 esbuild 打包的 input,而后应用build.onResolve()
和build.onLoad()
解决main.js
等文件相干门路以及内容中蕴含的import
门路,一直触发build.onResolve()
和build.onLoad()
,递归解决所有波及到的文件的门路,同时收集所有node_modules
到deps
中 - 在第二次 esbuild 打包中,会应用所有
node_modules
的依赖入口文件作为 esbuild 打包的 input,而后进行打包,此时所有commonjs
的文件会被转化为esmodule
以及内联到同一个文件中
从下面的剖析中,咱们能够发现,没有转化 indexB.js
为esmodule
格局就是因为第二次 esbuild 打包没有退出 indexB.js
那么如果咱们强行退出 indexB.js
在第二次 esbuild 打包中,如下图所示,而后咱们 main.js
间接就应用node_modules/.vite/deps/indexB.js
"react"
会被主动转化为"node_modules/.vite/deps/react.js"
门路
后果如咱们料想中一样,失常运行,并且 indexB.js
也会转化为 esmodule
格局,同时它也被写入到 node_modules/.vite/deps/indexB.js
中
从中咱们就明确了一个事件,vite
预构建针对的是 node_modules
的依赖,而实现 CommonJS 和 UMD 兼容性
目标实质借助的是 esbuild
的打包能力,借助它 format:"esm"
的输入格局转化 commonjs
为esmodule
格局
那如果咱们硬要在业务代码中应用
commonjs
和require 语句
?咱们该如何做呢?
业务代码中应用 CommonJS 代码
上面剖析内容大部分都参考 https://github.com/evanw/esbuild/issues/506 和 https://github.com/vitejs/vite/issues/3409
vite
应用 esbuild
进行预构建,能够将 commonjs
转化为esmodule
而
commonjs
转化为esmodule
的原理在下面咱们也剖析过,就是在commonjs
代码的根底上再注入一些代码,进行export default {xx, xx}
然而 esbuild
无奈转化 require
语句,从 esbuild/issues/506 也能够看出,就算 node_modules
依赖库中有 require
语句也无奈转化,内部业务代码的 require
语句更加无奈转化,因为内部的业务代码都没通过第二次 esbuild 打包转化为 esm
格局
而 vite
除了 esbuild
局部的代码并不反对 require
语句,只反对 import
语句
因而为了可能在 vite
的业务代码中应用 commonjs
,将业务代码的require
语句转化为 import
语句,咱们须要引入对应的plugin
: vite-plugin-commonjs
import {viteCommonjs} from '@originjs/vite-plugin-commonjs'
export default {
plugins: [viteCommonjs()
]
}
如果须要将 node_modules
的require
语句转化为 import
语句,须要申明:
import {esbuildCommonjs} from '@originjs/vite-plugin-commonjs'
export default {
optimizeDeps:{
esbuildOptions:{
plugins:[esbuildCommonjs(['react-calendar','react-date-picker'])
]
}
}
}
3.7.2 性能
为了进步后续页面的加载性能,Vite 将那些具备许多外部模块的 ESM 依赖项转换为单个模块。
有些包将它们的 ES 模块构建为许多独自的文件,彼此导入。例如,lodash-es
有超过 600 个内置模块!当咱们执行import {debounce} from 'lodash-es'
时,浏览器同时收回 600 多个 HTTP 申请!即便服务器可能轻松解决它们,但大量申请会导致浏览器端的网络拥塞,使页面加载变得显著迟缓。
通过将lodash-es
预构建成单个模块,当初咱们只须要一个 HTTP 申请!
esbuild
打包时设置bundle:true
,打包一个文件会将任何导入的依赖项内联到文件中
4. 申请资源 middlewares
留神:调试
app.use(xxxMiddleware(server))
时须要关上浏览器,例如[http://127.0.0.1:5173/](http://127.0.0.1:5173/)
触发申请发送,能力触发xxxMiddleware
相干逻辑断点,能力调试本地服务器的相干代码
有多种性能的中间件,比方 proxyMiddleware
能够在本地开发环境中跨域申请,servePublicMiddleware
解决 public
资源,serveStaticMiddleware
和 serveRawFsMiddleware
解决动态文件
async function createServer(inlineConfig = {}) {const middlewares = connect();
// proxy
const {proxy} = serverConfig;
if (proxy) {middlewares.use(proxyMiddleware(httpServer, proxy, config));
}
// serve static files under /public
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));
}
这些中间件中,
transformMiddleware
的性能最为简单和重要,本文将只针对transformMiddleware
开展剖析,其它中间件请参考其它文章
4.1 transform
在初始化 createServer()
中,咱们会应用 middlewares.use(transformMiddleware(server))
进行文件内容的转化
async function createServer(inlineConfig = {}) {const middlewares = connect();
// main transform middleware
middlewares.use(transformMiddleware(server));
}
4.1.1 transformMiddleware 流程图
4.1.2 应用 transformMiddleware 的起因
- 对于
esmodule
,不反对import xx from "vue"
这种导入的,须要转化为相对路径或者绝对路径的模式 - 浏览器只意识
js
,不反对其它后缀的文件名称,比方.vue
、.ts
,须要进行解决
4.1.3 transformMiddleware 整体流程(源码整体概述)
function transformMiddleware(server) {const { config: { root, logger}, moduleGraph, } = server;
return async function viteTransformMiddleware(req, res, next) {if (req.method !== 'GET' || knownIgnoreList.has(req.url)) {return next();
}
let url = decodeURI(removeTimestampQuery(req.url)).replace(NULL_BYTE_PLACEHOLDER, '\0');
//... 解决 xxx.js.map 的状况
//... 解决 url 是 public/xx 结尾的状况,提醒正告
if (isJSRequest(url) ||
isImportRequest(url) ||
isCSSRequest(url) ||
isHTMLProxy(url)) {if (isCSSRequest(url) &&
!isDirectRequest(url) &&
req.headers.accept?.includes('text/css')) {url = injectQuery(url, 'direct');
}
const result = await transformRequest(url, server, {html: req.headers.accept?.includes('text/html'),
});
if (result) {const depsOptimizer = getDepsOptimizer(server.config, false); // non-ssr
const type = isDirectCSSRequest(url) ? 'css' : 'js';
const isDep = DEP_VERSION_RE.test(url) || depsOptimizer?.isOptimizedDepUrl(url);
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();};
}
4.1.4 transformRequest & doTransform
判断是否有缓存数据,即 server.moduleGraph.getModuleByUrl().transformResult
是否存在,如果存在,则间接返回
如果没有缓存数据,则调用 pluginContainer.resolveId()
->loadAndTransform()
进行内容的转化
function transformRequest(url, server, options = {}) {const request = doTransform(url, server, options, timestamp);
return request;
}
async function doTransform(url, server, options, timestamp) {const { config, pluginContainer} = server;
const module = await server.moduleGraph.getModuleByUrl(url, ssr);
// check if we have a fresh cache
const cached = module && (ssr ? module.ssrTransformResult : module.transformResult);
if (cached) {return cached;}
// resolve
const id = (await pluginContainer.resolveId(url, undefined, { ssr}))?.id || url;
const result = loadAndTransform(id, url, server, options, timestamp);
getDepsOptimizer(config, ssr)?.delayDepsOptimizerUntil(id, () => result);
return result;
}
4.1.5 pluginContainer.resolveId
在 3.5.2
步骤中剖析过,getSortedPlugins('resolveId')
就是检测初始化时注册的插件是否有 resolveId
这个属性,如果有,则增加到返回的数组汇合中,比方有 10 个插件中有 5 个插件具备 resolveId
属性,那么最终 getSortedPlugins('resolveId')
拿到的就是这 5 个插件的 Array 数据
因而 container.resolveId()
中运行插件的个数不止一个,但并不是每一个插件都能返回对应的后果 result
,即const result = await handler.call(...)
可能为undefined
当有插件解决后 result
不为 undefined
时,会间接执行 break
,而后返回container.resolveId()
的后果
async resolveId(rawId, importer = join$2(root, 'index.html'), options) {for (const plugin of getSortedPlugins('resolveId')) {if (!plugin.resolveId)
continue;
if (skip?.has(plugin))
continue;
const handler = 'handler' in plugin.resolveId
? plugin.resolveId.handler
: plugin.resolveId;
const result = await handler.call(...);
if (!result)
continue;
if (typeof result === 'string') {id = result;} else {
id = result.id;
Object.assign(partial, result);
}
break;
}
if (id) {partial.id = isExternalUrl(id) ? id : normalizePath$3(id);
return partial;
} else {return null;}
}
4.1.6 loadAndTransform
从上面精简后的代码能够晓得,次要分为三个局部:
- 第一局部
pluginContainer.load
: 读取文件内容 - 第二局部
moduleGraph.ensureEntryFromUrl
: 创立 moduleGraph 缓存 - 第三局部
pluginContainer.transform
: 转化文件内容
async function loadAndTransform(id, url, server, options, timestamp) {
//...
// 第一局部:读取文件内容
const loadResult = await pluginContainer.load(id, { ssr});
if (loadResult == null) {if (options.ssr || isFileServingAllowed(file, server)) {code = await promises$2.readFile(file, 'utf-8');
}
if (code) {map = (convertSourceMap.fromSource(code) ||
(await convertSourceMap.fromMapFileSource(code, createConvertSourceMapReadMap(file))))?.toObject();
code = code.replace(convertSourceMap.mapFileCommentRegex, blankReplacer);
}
} else {if (isObject$2(loadResult)) {
code = loadResult.code;
map = loadResult.map;
} else {code = loadResult;}
}
// 第二局部:创立 moduleGraph 缓存,将上面 transform 后果存入 mod 中
const mod = await moduleGraph.ensureEntryFromUrl(url, ssr);
ensureWatchedFile(watcher, mod.file, root);
// 第三局部:transform 转化文件内容
const transformStart = isDebug$3 ? performance$1.now() : 0;
const transformResult = await pluginContainer.transform(code, id, {
inMap: map,
ssr,
});
//... 简略解决下转化后的文件后果
const originalCode = code;
const result = ssr && !server.config.experimental.skipSsrTransform
? await server.ssrTransform(code, map, url, originalCode)
: {
code,
map,
etag: etag_1(code, { weak: true}),
};
if (timestamp > mod.lastInvalidationTimestamp) {if (ssr)
mod.ssrTransformResult = result;
else
mod.transformResult = result;
}
return result;
}
第一局部读取文件内容:pluginContainer.load
pluginContainer.load()
跟之前剖析的 pluginContainer.resolveId()
相似,都是去遍历所有注册的插件,而后返回后果,找到满足条件的那个插件
如果没有插件合乎题意,即 loadResult==null
时,会进行文件读取的形式获取该文件的内容
async function loadAndTransform(id, url, server, options, timestamp) {
//...
// 第一局部:读取文件内容
const loadResult = await pluginContainer.load(id, { ssr});
if (loadResult == null) {if (options.ssr || isFileServingAllowed(file, server)) {code = await promises$2.readFile(file, 'utf-8');
}
}
}
async load(id, options) {for (const plugin of getSortedPlugins('load')) {if (!plugin.load)
continue;
const handler = 'handler' in plugin.load ? plugin.load.handler : plugin.load;
const result = await handler.call(ctx, id, { ssr});
if (result != null) {if (isObject$2(result)) {updateModuleInfo(id, result);
}
return result;
}
}
return null;
}
那什么状况下
loadResult
能够读取到?什么状况下读取不到呢?会触发什么类型的插件执行load()
获取到loadResult
呢?
为了更好地了解 pluginContainer.load()
的调用逻辑,咱们应用示例 Index.vue
进行剖析
<template>
<div class="index-wrapper"> 这是 Index.vue</div>
</template>
<script>
export default {
name: "Index",
setup() {
const indexData = "index Data";
return {}}
}
</script>
<style scoped>
.index-wrapper {background-color: rebeccapurple;}
</style>
一些原始的文件,比方 main.js
、main.css
、Index.vue
则间接应用 readFile
进行内容的读取
而一些须要插件进行获取的文件数据,比方 Index.vue
文件通过 transform
流程解析 <style>
失去的数据、解析 <script>
失去的数据,都须要特定的插件进行解决获取数据
能够了解为,如果单纯读取文件内容,间接应用
readFile()
即可,如果还须要对内容进行加工或者革新,则须要走插件进行解决
第二局部初始化缓存:moduleGraph.ensureEntryFromUrl
简略的逻辑,依据 url
创立对应的 new ModuleNode()
缓存对象,期待 pluginContainer.transform
返回 result
后,将 result
存入到 mod
中
async function loadAndTransform(id, url, server, options, timestamp) {
//...
// 第二局部:创立 moduleGraph 缓存,将上面 transform 后果存入 mod 中
const mod = await moduleGraph.ensureEntryFromUrl(url, ssr);
ensureWatchedFile(watcher, mod.file, root);
// 第三局部:transform 转化文件内容
const transformStart = isDebug$3 ? performance$1.now() : 0;
const transformResult = await pluginContainer.transform(code, id, {
inMap: map,
ssr,
});
if (timestamp > mod.lastInvalidationTimestamp) {if (ssr)
mod.ssrTransformResult = result;
else
mod.transformResult = result;
}
}
async ensureEntryFromUrl(rawUrl, ssr, setIsSelfAccepting = true) {const [url, resolvedId, meta] = await this.resolveUrl(rawUrl, ssr);
let mod = this.idToModuleMap.get(resolvedId);
if (!mod) {mod = new ModuleNode(url, setIsSelfAccepting);
this.urlToModuleMap.set(url, mod);
mod.id = resolvedId;
this.idToModuleMap.set(resolvedId, mod);
const file = (mod.file = cleanUrl(resolvedId));
let fileMappedModules = this.fileToModulesMap.get(file);
if (!fileMappedModules) {fileMappedModules = new Set();
this.fileToModulesMap.set(file, fileMappedModules);
}
fileMappedModules.add(mod);
} else if (!this.urlToModuleMap.has(url)) {this.urlToModuleMap.set(url, mod);
}
return mod;
}
第三局部转化文件内容:pluginContainer.transform
pluginContainer.transform()
跟之前剖析的 pluginContainer.resolveId()
相似,都是去遍历所有注册的插件,而后返回后果,而依据不同的文件类型,会调用不同类型的插件进行 transform()
解决,比方:
vite:css
:css
编译插件,上面4.3.6
步骤解析.vue
文件失去的<style>
造成的语句最终会调用vite:css
插件进行解析vite:esbuild
:.ts
、.jsx
和.tsx
转化.js
的插件,用来代替传统的tsc
转化性能
async transform(code, id, options) {for (const plugin of getSortedPlugins('transform')) {if (!plugin.transform)
continue;
let result;
const handler = 'handler' in plugin.transform
? plugin.transform.handler
: plugin.transform;
result = await handler.call(ctx, code, id, { ssr});
if (!result)
continue;
//... 将 result 赋值给 code
}
return {
code,
map: ctx._getCombinedSourcemap(),};
}
4.1.7 小结
插件流程
在下面的 transformMiddleware
的剖析流程中,咱们波及到多个插件的 resolveId()
、load()
、transform()
流程,这实质是一套标准的 rollup
插件流程
比方
transform()
流程,见上面剖析内容
transform
是 rollup 插件规定的 Build Hooks,具体能够参考 Rollup 插件文档
rollup 插件整体的构建流程如下所示:
middleware 解决流程跟预构建流程的差异
预构建也有门路 resolveId()解决,middleware 解决流程也有 resolveId()门路解决,这两方面有什么差异?
预构建有获取内容,middleware 解决流程也有获取内容,这两方面有什么差异呢?
预构建的门路 resolveId()
,是为了可能失去残缺的门路,而后进行readFile()
读取文件内容,最终依据内容找到依赖的其它文件,而后触发其它依赖文件执行相干的 build.onResolve()
和build.onLoad()
,从而遍历完所有的文件,进行预构建 node_modules
相干文件的依赖收集
最终收集实现输入 deps
数据,依据 deps
数据进行预构建:esbuild 打包到 node_modeuls/.vite/xxx
文件中
而 middleware
解决流程的 resolveId()
流程,波及到 node_modules
相干门路的获取,会依据预构建失去的 depsOptimizer
拿到对应的门路数据,其它门路的获取则跟预构建流程统一,最终获取到绝对路径
而后触发对应的 load()
->transform()
插件流程,这个时候不同类型的数据会依据不同的插件进行解决,比方 .scss
文件交由 vite:css
文件进行转化为 css
数据,.vue
文件交由 vite:vue
进行单页面的解析成为多个局部进行数据的获取,最终造成浏览器能够辨认的 js
数据内容,而后返回给浏览器进行执行和显示
在
4.1.5
步骤中,咱们简略剖析了loadAndTransform()
的整体流程,然而波及到的一些插件没有具体开展剖析,上面咱们将应用具体的例子,将波及到的插件简略进行剖析
4.2 常见插件源码剖析
4.2.1 vite:import-analysis 剖析
当浏览器申请 main.js
时,由 4.3.5
的第一局部
的剖析中,咱们晓得 pluginContainer.load()
返回后果为空,会间接应用 readFile()
读取文件内容
而后触发 pluginContainer.transform("main.js")
,此时会触发插件vite:import-analysis
的transform()
办法
如上面代码块所示,在这个办法中,咱们会提取出所有 import
的数据,而后进行遍历,在遍历过程中
- 应用
normalizeUrl()
去掉rootDir
的前缀,调用pluginContainer.resolveId()
进行门路的重写 - 增加到
staticImportedUrls
,提前触发transformRequest()
进行import
文件的转化
name: 'vite:import-analysis',
async transform(source, importer, options) {
let imports;
let exports;
[imports, exports] = parse$e(source);
for (let index = 0; index < imports.length; index++) {
const { s: start, e: end, ss: expStart, se: expEnd, d: dynamicIndex,
n: specifier, a: assertIndex, } = imports[index];
// resolvedId="/Users/wcbbcc/blog/Frontend-Articles/vite-debugger/node_modules/.vite/deps/vue.js?v=da0b3f8b"
// url="/node_modules/.vite/deps/vue.js?v=da0b3f8b"
const [url, resolvedId] = await normalizeUrl(specifier, start);
if (!isDynamicImport) {
// for pre-transforming
staticImportedUrls.add({url: hmrUrl, id: resolvedId});
}
}
if (config.server.preTransformRequests && staticImportedUrls.size) {staticImportedUrls.forEach(({ url}) => {url = removeImportQuery(url);
transformRequest(url, server, { ssr}).catch((e) => {});
});
}
}
const normalizeUrl = async (url, pos, forceSkipImportAnalysis = false) => {const resolved = await this.resolve(url, importerFile);
if (resolved.id.startsWith(root + '/')) {url = resolved.id.slice(root.length);
}
//url="/node_modules/.vite/deps/vue.js?v=c1e0320d"
return [url, resolved.id];
};
pluginContainer.resolveId()逻辑
跟下面预构建的流程雷同,都是触发插件 vite:resolve
的执行,然而此时的 depsOptimizer
曾经存在,因而会间接从 depsOptimizer
中获取对应的门路数据,返回门路 node_modules/.vite/deps/xxx
的数据
name: "vite:resolve"
async resolveId() {if (bareImportRE.test(id)) {const external = options.shouldExternalize?.(id);
if (!external &&
asSrc &&
depsOptimizer &&
!options.scan &&
(res = await tryOptimizedResolve(depsOptimizer, id, importer))) {return res;}
if ((res = tryNodeResolve(id, importer, options, targetWeb, depsOptimizer, ssr, external))) {return res;}
}
}
vite:import-analysis 小结
vite:import-analysis
插件重写了import
语句的门路,比方import {createApp} from "vue"
重写为import {createApp} from "/node_modules/.vite/deps/vue.js?v=da0b3f8b"
- 除了替换了文件内容
code
中那些导入模块import
的门路,还提前触发这些门路的transformRequest()
调用
4.2.2 vite:vue 剖析
借助
@vitejs/plugin-vue
独立的插件,能够进行.vue
文件的解析
当浏览器申请一般构造的 Index.vue
时,会触发 vite:vue
办法的解析,而后触发 transformMain()
办法解析
name: 'vite:vue',
async transform(code, id, opt) {
//...
if (!query.vue) {
return transformMain(
code,
filename,
options,
this,
ssr,
customElementFilter(filename)
);
} else {//...}
}
在这个插件中,会进行 <script>
、<template>
、<style>
三种标签的数据解析
其中
stylesCode
会解析失去"import'xxxxx/vite-debugger/src/Index.vue?vue&type=style&index=0&scoped=3d84b2a7&lang.css'"
之后会触发插件"vite:css"
进行transform()
的转化
而后应用 output.join("\n")
拼成数据返回
async function transformMain(code, filename, options, pluginContext, ssr, asCustomElement) {const { code: scriptCode, map: scriptMap} = await genScriptCode(
descriptor,
options,
pluginContext,
ssr
);
const hasTemplateImport = descriptor.template && !isUseInlineTemplate(descriptor, !devServer);
if (hasTemplateImport) {({ code: templateCode, map: templateMap} = await genTemplateCode(
descriptor,
options,
pluginContext,
ssr
));
}
const stylesCode = await genStyleCode(
descriptor,
pluginContext,
asCustomElement,
attachedProps
);
const output = [
scriptCode,
templateCode,
stylesCode,
customBlocksCode
];
if (!attachedProps.length) {output.push(`export default _sfc_main`);
} else {
output.push(`import _export_sfc from '${EXPORT_HELPER_ID}'`,
`export default /*#__PURE__*/_export_sfc(_sfc_main, [${attachedProps.map(([key, val]) => `['${key}',${val}]`).join(",")}])`
);
}
let resolvedCode = output.join("\n");
return {code: resolvedCode};
}
Index.vue
的代码如下所示:
<template>
<div class="index-wrapper"> 这是 Index.vue</div>
</template>
<script>
export default {
name: "Index",
setup() {
const indexData = "index Data";
return {}}
}
</script>
<style scoped>
.index-wrapper {background-color: rebeccapurple;}
</style>
Index.vue
通过 vite:vue
的transform()
转化后的 output
如下图所示,一共分为 4 个局部:
<template>
: 转化为createElement()
编译后的语句<script>
:export default
转化为const _sfc_main=
语句<style>
: 转化为import "xxx.vue?vue&lang.css"
的语句- 其它代码: 热更新代码和其它运行时代码
参考文章
- Vite 源码剖析,是时候弄清楚 Vite 的原理了
- Vite 原理及源码解析
- vite2 源码剖析(一) — 启动 vite
- Vite 原理剖析