vite源码学习-启动本地服务背景 自从vue3.0面世后,也随即发布了新的构建工具vite,那么接着本次复习的机会就来
背景
自从vue3.0面世后,也随即发布了新的构建工具vite,那么接着本次复习的机会就来学习一下
脚手架
我们根据vite官网的说明进行项目脚手架的搭建,搭建后大致如下图结构
├── README.md
├── index.html
├── node_modules
├── package.json
├── public
│ └── favicon.ico
├── src
│ ├── App.vue
│ ├── assets
│ │ └── logo.png
│ ├── components
│ │ └── HelloWorld.vue
│ └── main.js
└── vite.config.js
我们这里关注到index.html
内的代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
通过html
的代码,我们得知我们的代码是通过ES module
的方式进行引入的。
我们通过指令npm install
安装完依赖后,我们可以通过package.json
中的对应的dev
启动本地调试服务。 接着我们可以打开本地的http://localhost:3000/
查看道我们的应用是否启动成功。
看到以上基本上就就是成功
学习dev启动服务
回到我们的package.json
, 我们看到scripts
中的dev
对应着vite
指令,我们可以在node_modules/.bin/vite
找到对应执行,同时我们结束进程,重新通过vscode的挑食能力重新启动服务, 方便我们对于代码进行调试
bin
#!/usr/bin/env node
const { performance } = require('perf_hooks')
if (!__dirname.includes('node_modules')) {
try {
// only available as dev dependency
require('source-map-support').install()
} catch (e) {}
}
global.__vite_start_time = performance.now()
// check debug mode first before requiring the CLI.
// 检查各种指令输入
const debugIndex = process.argv.findIndex((arg) => /^(?:-d|--debug)$/.test(arg))
const filterIndex = process.argv.findIndex((arg) =>
/^(?:-f|--filter)$/.test(arg)
)
const profileIndex = process.argv.indexOf('--profile')
if (debugIndex > 0) {
let value = process.argv[debugIndex + 1]
if (!value || value.startsWith('-')) {
value = 'vite:*'
} else {
// support debugging multiple flags with comma-separated list
value = value
.split(',')
.map((v) => `vite:${v}`)
.join(',')
}
process.env.DEBUG = `${
process.env.DEBUG ? process.env.DEBUG + ',' : ''
}${value}`
if (filterIndex > 0) {
const filter = process.argv[filterIndex + 1]
if (filter && !filter.startsWith('-')) {
process.env.VITE_DEBUG_FILTER = filter
}
}
}
// 执行node_modules/vite/dist/node/cli.js
function start() {
require('../dist/node/cli')
}
if (profileIndex > 0) {
process.argv.splice(profileIndex, 1)
const next = process.argv[profileIndex]
if (next && !next.startsWith('-')) {
process.argv.splice(profileIndex, 1)
}
const inspector = require('inspector')
const session = (global.__vite_profile_session = new inspector.Session())
session.connect()
session.post('Profiler.enable', () => {
session.post('Profiler.start', start)
})
} else {
start()
}
cli.ts
这里我们看到*/dist/*
的路径后,就知道我们调试的是打包后的代码,这里就只能对应着源码来看。源码可以自行上github clone下来查看, 我们看到源码中的
packages/vite/src/node/cli.ts
// packages/vite/src/node/cli.ts
import { performance } from 'perf_hooks'
import { cac } from 'cac'
import colors from 'picocolors'
import type { BuildOptions } from './build'
import type { ServerOptions } from './server'
import type { LogLevel } from './logger'
import { createLogger } from './logger'
import { resolveConfig } from '.'
const cli = cac('vite')
// global options
interface GlobalCLIOptions {
'--'?: string[]
c?: boolean | string
config?: string
base?: string
l?: LogLevel
logLevel?: LogLevel
clearScreen?: boolean
d?: boolean | string
debug?: boolean | string
f?: string
filter?: string
m?: string
mode?: string
}
/**
* removing global flags before passing as command specific sub-configs
*/
function cleanOptions<Options extends GlobalCLIOptions>(
options: Options
): Omit<Options, keyof GlobalCLIOptions> {
const ret = { ...options }
delete ret['--']
delete ret.c
delete ret.config
delete ret.base
delete ret.l
delete ret.logLevel
delete ret.clearScreen
delete ret.d
delete ret.debug
delete ret.f
delete ret.filter
delete ret.m
delete ret.mode
return ret
}
cli
.option('-c, --config <file>', `[string] use specified config file`)
.option('--base <path>', `[string] public base path (default: /)`)
.option('-l, --logLevel <level>', `[string] info | warn | error | silent`)
.option('--clearScreen', `[boolean] allow/disable clear screen when logging`)
.option('-d, --debug [feat]', `[string | boolean] show debug logs`)
.option('-f, --filter <filter>', `[string] filter debug logs`)
.option('-m, --mode <mode>', `[string] set env mode`)
// 我们运行的是`vite`命令所以我们命中当前逻辑
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('--https', `[boolean] use TLS + HTTP/2`)
.option('--open [path]', `[boolean | string] open browser on startup`)
.option('--cors', `[boolean] enable CORS`)
.option('--strictPort', `[boolean] exit if specified port is already in use`)
.option(
'--force',
`[boolean] force the optimizer to ignore the cache and re-bundle`
)
.action(async (root: string, options: ServerOptions & GlobalCLIOptions) => {
// output structure is preserved even after bundling so require()
// is ok here
// 动态引入server文件
const { createServer } = await import('./server')
try {
// 创建服务
const server = await createServer({
root,
base: options.base,
mode: options.mode,
configFile: options.config,
logLevel: options.logLevel,
clearScreen: options.clearScreen,
server: cleanOptions(options)
})
if (!server.httpServer) {
throw new Error('HTTP server not available')
}
// 启动服务其中调用startServer方法
await server.listen()
const info = server.config.logger.info
info(
colors.cyan(`\n vite v${require('vite/package.json').version}`) +
colors.green(` dev server running at:\n`),
{
clear: !server.config.logger.hasWarned
}
)
// 打印地址
server.printUrls()
// @ts-ignore
if (global.__vite_start_time) {
// @ts-ignore
const startupDuration = performance.now() - global.__vite_start_time
info(
`\n ${colors.cyan(`ready in ${Math.ceil(startupDuration)}ms.`)}\n`
)
}
} catch (e) {
createLogger(options.logLevel).error(
colors.red(`error when starting dev server:\n${e.stack}`),
{ error: e }
)
// 推出进程
process.exit(1)
}
})
// 中间省略代码
cli.help()
cli.version(require('../../package.json').version)
cli.parse()
vite/src/node/server/index.ts
我们看到dev
的指令中主要就是通过createServer
创建服务,并启动服务。那么我们来看packages/vite/src/node/server/index.ts
中的createServer
方法中做了什么
createServer
// packages/vite/src/node/server/index.ts
export async function createServer(
inlineConfig: InlineConfig = {}
): Promise<ViteDevServer> {
// 格式化数据
const config = await resolveConfig(inlineConfig, 'serve', 'development')
// root 项目的根路径
// serverConfig = {
// preTransformRequests: true,
// fs: {
// strict: true,
// allow: [
// "*", // 跟root一致
// ],
// deny: [
// ".env",
// ".env.*",
// "*.{crt,pem}",
// ],
// },
// }
const { root, server: serverConfig } = config
// config.cacheDir 为 vite的缓存文件的配置项 通常为"node_modules/.vite"
// 解析是否包含https相关的配置
const httpsOptions = await resolveHttpsConfig(
config.server.https,
config.cacheDir
)
let { middlewareMode } = serverConfig
if (middlewareMode === true) {
middlewareMode = 'ssr'
}
// 创建middlewares其中包含中间件的基础方法以及EventEmitter对应的原型方法
const middlewares = connect() as Connect.Server
// 通过node的http*模块创建服务
const httpServer = middlewareMode
? null
: await resolveHttpServer(serverConfig, middlewares, httpsOptions)
// 创建websocket
const ws = createWebSocketServer(httpServer, config, httpsOptions)
// 获取配置的watch相关配置项
const { ignored = [], ...watchOptions } = serverConfig.watch || {}
// 监听跟节点的文件变化
const watcher = chokidar.watch(path.resolve(root), {
ignored: [
'**/node_modules/**',
'**/.git/**',
...(Array.isArray(ignored) ? ignored : [ignored])
],
ignoreInitial: true,
ignorePermissionErrors: true,
disableGlobbing: true,
...watchOptions
}) as FSWatcher
// 实例化模块图
const moduleGraph: ModuleGraph = new ModuleGraph((url, ssr) =>
container.resolveId(url, undefined, { ssr })
)
// 为插件创建上下文,其中container返回的主要是对应rollup的钩子构建的上下文对象
// 其中包含以下钩子
// buildStart(钩子)
// close(钩子)
// getModuleInfo(rollup的上下文方法)
// load(钩子)
// options(钩子)
// resolveId(钩子)
// transform(钩子)
const container = await createPluginContainer(config, moduleGraph, watcher)
// 对httpServer上挂在的socket进行采集,当调用closeHttpServer时,循环执行socket的destroy方法并尝试关闭服务
const closeHttpServer = createServerCloseFn(httpServer)
// eslint-disable-next-line prefer-const
let exitProcess: () => void
// 声明server
const server: ViteDevServer = {
// 配置项
config,
// 中间件
middlewares,
// 服务
httpServer,
// 文件监听
watcher,
// 插件上下文
pluginContainer: container,
// ws
ws,
// 模块图
moduleGraph,
ssrTransform(code: string, inMap: SourceMap | null, url: string) {
return ssrTransform(code, inMap, url, {
json: { stringify: server.config.json?.stringify }
})
},
transformRequest(url, options) {
return transformRequest(url, server, options)
},
transformIndexHtml: null!, // to be immediately set
async ssrLoadModule(url, opts?: { fixStacktrace?: boolean }) {
if (!server._ssrExternals) {
let knownImports: string[] = []
const optimizedDeps = server._optimizedDeps
if (optimizedDeps) {
await optimizedDeps.scanProcessing
knownImports = [
...Object.keys(optimizedDeps.metadata.optimized),
...Object.keys(optimizedDeps.metadata.discovered)
]
}
server._ssrExternals = resolveSSRExternal(config, knownImports)
}
return ssrLoadModule(
url,
server,
undefined,
undefined,
opts?.fixStacktrace
)
},
ssrFixStacktrace(e) {
if (e.stack) {
const stacktrace = ssrRewriteStacktrace(e.stack, moduleGraph)
rebindErrorStacktrace(e, stacktrace)
}
},
ssrRewriteStacktrace(stack: string) {
return ssrRewriteStacktrace(stack, moduleGraph)
},
// 启动服务
listen(port?: number, isRestart?: boolean) {
return startServer(server, port, isRestart)
},
// 关闭服务
async close() {
process.off('SIGTERM', exitProcess)
if (!middlewareMode && process.env.CI !== 'true') {
process.stdin.off('end', exitProcess)
}
await Promise.all([
watcher.close(),
ws.close(),
container.close(),
closeHttpServer()
])
},
// 打印服务的地址
printUrls() {
if (httpServer) {
printCommonServerUrls(httpServer, config.server, config)
} else {
throw new Error('cannot print server URLs in middleware mode.')
}
},
// 重启
async restart(forceOptimize?: boolean) {
if (!server._restartPromise) {
server._forceOptimizeOnRestart = !!forceOptimize
server._restartPromise = restartServer(server).finally(() => {
server._restartPromise = null
server._forceOptimizeOnRestart = false
})
}
return server._restartPromise
},
_optimizedDeps: null,
_ssrExternals: null,
_restartPromise: null,
_importGlobMap: new Map(),
_forceOptimizeOnRestart: false,
_pendingRequests: new Map()
}
// 生成一个函数用于处理plugins中所有注册的transformIndexHtml钩子
server.transformIndexHtml = createDevHtmlTransformFn(server)
// 推出当前进程
exitProcess = async () => {
try {
await server.close()
} finally {
process.exit()
}
}
process.once('SIGTERM', exitProcess)
if (!middlewareMode && process.env.CI !== 'true') {
process.stdin.on('end', exitProcess)
}
const { packageCache } = config
const setPackageData = packageCache.set.bind(packageCache)
packageCache.set = (id, pkg) => {
if (id.endsWith('.json')) {
watcher.add(id)
}
return setPackageData(id, pkg)
}
// 根部监听器增加事件监听change
watcher.on('change', async (file) => {
file = normalizePath(file)
// 无效化package.json
if (file.endsWith('/package.json')) {
return invalidatePackageData(packageCache, file)
}
// invalidate module graph cache on file change
// 无效化当前文件
moduleGraph.onFileChange(file)
if (serverConfig.hmr !== false) {
try {
// 尝试进行热更新
await handleHMRUpdate(file, server)
} catch (err) {
ws.send({
type: 'error',
err: prepareError(err)
})
}
}
})
// 根部监听器增加事件监听add
watcher.on('add', (file) => {
handleFileAddUnlink(normalizePath(file), server)
})
// 根部监听器增加事件监听unlink
watcher.on('unlink', (file) => {
handleFileAddUnlink(normalizePath(file), server)
})
// 获取服务启动时的端口
if (!middlewareMode && httpServer) {
httpServer.once('listening', () => {
// update actual port since this may be different from initial value
serverConfig.port = (httpServer.address() as AddressInfo).port
})
}
// apply server configuration hooks from plugins
// 取出插件中的configureServer周期
const postHooks: ((() => void) | void)[] = []
for (const plugin of config.plugins) {
if (plugin.configureServer) {
postHooks.push(await plugin.configureServer(server))
}
}
// Internal middlewares ------------------------------------------------------
// 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
if (config.base !== '/') {
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))
}
// main transform middleware
middlewares.use(transformMiddleware(server))
// serve static files
middlewares.use(serveRawFsMiddleware(server))
middlewares.use(serveStaticMiddleware(root, server))
// spa fallback
if (!middlewareMode || middlewareMode === 'html') {
middlewares.use(spaFallbackMiddleware(root))
}
// run post config hooks
// This is applied before the html middleware so that user middleware can
// serve custom content instead of index.html.
// 翻译:
// 运行配置后挂钩
// 这在 html 中间件之前应用,以便用户中间件可以
// 提供自定义内容而不是 index.html。
postHooks.forEach((fn) => fn && fn())
if (!middlewareMode || middlewareMode === 'html') {
// 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))
const initOptimizer = () => {
if (!config.optimizeDeps.disabled) {
// 创建优化服务,同时进行优化
server._optimizedDeps = createOptimizedDeps(server)
}
}
// 默认配置下middlewareMode为空
if (!middlewareMode && httpServer) {·
let isOptimized = false
// overwrite listen to init optimizer before server start
const listen = httpServer.listen.bind(httpServer)
httpServer.listen = (async (port: number, ...args: any[]) => {
// 如果没有优化过,在启动前进行优化
if (!isOptimized) {
try {
// 执行buildStart钩子
await container.buildStart({})
initOptimizer()
isOptimized = true
} catch (e) {
httpServer.emit('error', e)
return
}
}
// 然后才进入启动服务的流程
return listen(port, ...args)
}) as any
} else {
await container.buildStart({})
initOptimizer()
}
// 返回服务
return server
}
好的基本的服务创建我们已经了解完了,那么后续我们将启动服务那会涉及到另一个函数startServer
startServer
async function startServer(
server: ViteDevServer,
inlinePort?: number,
isRestart: boolean = false
): Promise<ViteDevServer> {
const httpServer = server.httpServer
if (!httpServer) {
throw new Error('Cannot call server.listen in middleware mode.')
}
// 取出server的配置,同serverConfig
const options = server.config.server
// 端口号
const port = inlinePort ?? options.port ?? 3000
const hostname = resolveHostname(options.host)
// 协议
const protocol = options.https ? 'https' : 'http'
const info = server.config.logger.info
const base = server.config.base
/*
* 对httpServer的启动增加了error事件监听,如果是端口号重复++,其他错误就直接抛出
* 同时调用httpServer的listen方法,启动服务(注意: 这里内部调用的是node的https/http模块的createServer 所创建的 server启动的服务)
* listen方法内部调用了container.buildStart({})
* 以及预设好优化相关的的依赖
*/
// 返回启动成功后的对应端口
const serverPort = await httpServerStart(httpServer, {
port,
strictPort: options.strictPort,
host: hostname.host,
logger: server.config.logger
})
// @ts-ignore
const profileSession = global.__vite_profile_session
if (profileSession) {
profileSession.post('Profiler.stop', (err: any, { profile }: any) => {
// Write profile to disk, upload, etc.
if (!err) {
const outPath = path.resolve('./vite-profile.cpuprofile')
fs.writeFileSync(outPath, JSON.stringify(profile))
info(
colors.yellow(
` CPU profile written to ${colors.white(colors.dim(outPath))}\n`
)
)
} else {
throw err
}
})
}
// 如果配置了打开浏览器,那么就自动打开
if (options.open && !isRestart) {
const path = typeof options.open === 'string' ? options.open : base
openBrowser(
path.startsWith('http')
? path
: `${protocol}://${hostname.name}:${serverPort}${path}`,
true,
server.config.logger
)
}
return server
}
那么到这里我们的服务就启动成功了。但是启动的过程中有一个关于依赖的重要函数我们忽略了,那就是createOptimizedDeps
我们深入了解一下其中发生了什么
vite/src/node/optimizer/registerMissing.ts
createOptimizedDeps
// 以下省略大量代码
export function createOptimizedDeps(server: ViteDevServer): OptimizedDeps {
const { config } = server
const { logger } = config
const sessionTimestamp = Date.now().toString()
// 读取之前所缓存的元信息
const cachedMetadata = loadCachedDepOptimizationMetadata(config)
// 如果之前有元信息,那就使用之前的,如果没有就创建新的元信息
const optimizedDeps: OptimizedDeps = {
metadata:
cachedMetadata || createOptimizedDepsMetadata(config, sessionTimestamp),
registerMissingImport
}
let handle: NodeJS.Timeout | undefined
let newDepsDiscovered = false
let newDepsToLog: string[] = []
let newDepsToLogHandle: NodeJS.Timeout | undefined
const logNewlyDiscoveredDeps = () => {
if (newDepsToLog.length) {
config.logger.info(
colors.green(
`✨ new dependencies optimized: ${depsLogString(newDepsToLog)}`
),
{
timestamp: true
}
)
newDepsToLog = []
}
}
// 创建新的promise然后将promise的resover跟promise返回回来
let depOptimizationProcessing = newDepOptimizationProcessing()
let depOptimizationProcessingQueue: DepOptimizationProcessing[] = []
const resolveEnqueuedProcessingPromises = () => {
// Resolve all the processings (including the ones which were delayed)
for (const processing of depOptimizationProcessingQueue) {
processing.resolve()
}
depOptimizationProcessingQueue = []
}
let enqueuedRerun: (() => void) | undefined
let currentlyProcessing = false
// If there wasn't a cache or it is outdated, perform a fast scan with esbuild
// to quickly find project dependencies and do a first optimize run
// 翻译:
// 如果没有缓存或缓存过时,使用 esbuild 执行快速扫描
// 快速找到项目依赖项并进行第一次优化运行
if (!cachedMetadata) {
currentlyProcessing = true
// 同上
const scanPhaseProcessing = newDepOptimizationProcessing()
optimizedDeps.scanProcessing = scanPhaseProcessing.promise
const warmUp = async () => {
try {
debug(colors.green(`scanning for dependencies...`), {
timestamp: true
})
// 取出元信息
const { metadata } = optimizedDeps
/*
* 1. 借助esbuild自定义插件,得到对应的依赖表(比如针对html文件就去找对应的依赖文件)
2. 如果配置了includes,那就只保留includes中的选项
3. 生成discovered信息,其中包含源文件的esm路径跟预计生成的优化文件路径以及浏览器hash
*/
// discovered的数据结构大致情况
// {
// lodash: {
// id: "lodash",
// file: "/**/**/vue-vite-study/node_modules/.vite/deps/lodash.js",
// src: "/**/**/vue-vite-study/node_modules/lodash/lodash.js",
// browserHash: "******",
// },
// vue: {
// id: "vue",
// file: "/**/**/vue-vite-study/node_modules/.vite/deps/vue.js",
// src: "/**/**/vue-vite-study/node_modules/vue/dist/vue.runtime.esm-bundler.js",
// browserHash: "******",
// },
// }
const discovered = await discoverProjectDependencies(
config,
sessionTimestamp
)
// Respect the scan phase discover order to improve reproducibility
// 将发现文件加入元信息
for (const depInfo of Object.values(discovered)) {
addOptimizedDepInfo(metadata, 'discovered', {
...depInfo,
processing: depOptimizationProcessing.promise
})
}
debug(
colors.green(
`dependencies found: ${depsLogString(Object.keys(discovered))}`
),
{
timestamp: true
}
)
scanPhaseProcessing.resolve()
optimizedDeps.scanProcessing = undefined
// 执行优化
runOptimizer()
} catch (e) {
logger.error(e.message)
if (optimizedDeps.scanProcessing) {
scanPhaseProcessing.resolve()
optimizedDeps.scanProcessing = undefined
}
}
}
setTimeout(warmUp, 0)
}
async function runOptimizer(isRerun = false) {
// Ensure that rerun is called sequentially
enqueuedRerun = undefined
currentlyProcessing = true
// Ensure that a rerun will not be issued for current discovered deps
if (handle) clearTimeout(handle)
// a succesful completion of the optimizeDeps rerun will end up
// creating new bundled version of all current and discovered deps
// in the cache dir and a new metadata info object assigned
// to optimizeDeps.metadata. A fullReload is only issued if
// the previous bundled dependencies have changed.
// if the rerun fails, optimizeDeps.metadata remains untouched,
// current discovered deps are cleaned, and a fullReload is issued
// 翻译
// 成功完成 optimizeDeps 重新运行将结束
// 创建所有当前和发现的依赖的新捆绑版本
// 在缓存目录中并分配一个新的元数据信息对象
// 优化Deps.metadata。只有在以下情况下才会发出 fullReload
// 1. 之前捆绑的依赖已经改变。
// 2. 如果重新运行失败,optimizeDeps.metadata 保持不变, 清理当前发现的 deps,并发出 fullReload
let { metadata } = optimizedDeps
// All deps, previous known and newly discovered are rebundled,
// respect insertion order to keep the metadata file stable
// 翻译
// 所有的 deps,以前已知的和新发现的都重新打包,
// 尊重插入顺序以保持元数据文件稳定
const newDeps: Record<string, OptimizedDepInfo> = {}
// Clone optimized info objects, fileHash, browserHash may be changed for them
// 拷贝优化过的信息对象,有可能浏览器的hash已经变化
for (const dep of Object.keys(metadata.optimized)) {
newDeps[dep] = { ...metadata.optimized[dep] }
}
for (const dep of Object.keys(metadata.discovered)) {
// Clone the discovered info discarding its processing promise
// 拷贝新发现的信息剔除对应的process promise
const { processing, ...info } = metadata.discovered[dep]
newDeps[dep] = info
}
newDepsDiscovered = false
// Add the current depOptimizationProcessing to the queue, these
// promises are going to be resolved once a rerun is committed
// 将当前的 depOptimizationProcessing 添加到队列中,这些一旦提交重新运行,promise 将被解决
depOptimizationProcessingQueue.push(depOptimizationProcessing)
// Create a new promise for the next rerun, discovered missing
// dependencies will be asigned this promise from this point
// 为下一次重新运行创建一个新的 Promise,发现丢失从此时起,依赖项将被分配这个 Promise
depOptimizationProcessing = newDepOptimizationProcessing()
try {
// 执行优化Deps
const processingResult = await runOptimizeDeps(config, newDeps)
// *此时我们的node_modules/.vite/processing中已经生成了对应文件了
// 取出新的元信息
const newData = processingResult.metadata
// After a re-optimization, if the internal bundled chunks change a full page reload
// is required. If the files are stable, we can avoid the reload that is expensive
// for large applications. Comparing their fileHash we can find out if it is safe to
// keep the current browser state.
// 翻译
// 重新优化后,如果内部捆绑的块改变了整页重新加载
// 是必须的。如果文件稳定,我们可以避免昂贵的重新加载
// 对于大型应用程序。比较他们的 fileHash,我们可以确定它是否安全
// 保持当前浏览器状态。
const needsReload =
metadata.hash !== newData.hash ||
Object.keys(metadata.optimized).some((dep) => {
return (
metadata.optimized[dep].fileHash !== newData.optimized[dep].fileHash
)
})
const commitProcessing = async () => {
// 写入元数据文件,删除`deps`文件夹并将新的`processing`文件夹重命名为`deps`同步
await processingResult.commit()
// While optimizeDeps is running, new missing deps may be discovered,
// in which case they will keep being added to metadata.discovered
// 当 optimizeDeps 运行时,可能会发现新的缺失的 deps,\
// 在这种情况下,它们将继续添加到 metadata.discovered
for (const id in metadata.discovered) {
if (!newData.optimized[id]) {
addOptimizedDepInfo(newData, 'discovered', metadata.discovered[id])
}
}
// If we don't reload the page, we need to keep browserHash stable
// 如果不加载页面我们需要保证browserHash的稳定
if (!needsReload) {
newData.browserHash = metadata.browserHash
for (const dep in newData.chunks) {
newData.chunks[dep].browserHash = metadata.browserHash
}
for (const dep in newData.optimized) {
newData.optimized[dep].browserHash = (
metadata.optimized[dep] || metadata.discovered[dep]
).browserHash
}
}
// Commit hash and needsInterop changes to the discovered deps info
// object. Allow for code to await for the discovered processing promise
// and use the information in the same object
for (const o in newData.optimized) {
const discovered = metadata.discovered[o]
if (discovered) {
const optimized = newData.optimized[o]
discovered.browserHash = optimized.browserHash
discovered.fileHash = optimized.fileHash
discovered.needsInterop = optimized.needsInterop
discovered.processing = undefined
}
}
if (isRerun) {
newDepsToLog.push(
...Object.keys(newData.optimized).filter(
(dep) => !metadata.optimized[dep]
)
)
}
metadata = optimizedDeps.metadata = newData
// 解决当前队列中的promise
resolveEnqueuedProcessingPromises()
}
if (!needsReload) {
await commitProcessing()
if (!isDebugEnabled) {
if (newDepsToLogHandle) clearTimeout(newDepsToLogHandle)
newDepsToLogHandle = setTimeout(() => {
newDepsToLogHandle = undefined
logNewlyDiscoveredDeps()
}, 2 * debounceMs)
} else {
debug(colors.green(`✨ optimized dependencies unchanged`), {
timestamp: true
})
}
} else {
if (newDepsDiscovered) {
// There are newly discovered deps, and another rerun is about to be
// excecuted. Avoid the current full reload discarding this rerun result
// We don't resolve the processing promise, as they will be resolved
// once a rerun is committed
processingResult.cancel()
debug(
colors.green(
`✨ delaying reload as new dependencies have been found...`
),
{
timestamp: true
}
)
} else {
await commitProcessing()
if (!isDebugEnabled) {
if (newDepsToLogHandle) clearTimeout(newDepsToLogHandle)
newDepsToLogHandle = undefined
logNewlyDiscoveredDeps()
}
logger.info(
colors.green(`✨ optimized dependencies changed. reloading`),
{
timestamp: true
}
)
fullReload()
}
}
} catch (e) {
logger.error(
colors.red(`error while updating dependencies:\n${e.stack}`),
{ timestamp: true, error: e }
)
resolveEnqueuedProcessingPromises()
// Reset missing deps, let the server rediscover the dependencies
metadata.discovered = {}
}
currentlyProcessing = false
// @ts-ignore
enqueuedRerun?.()
}
// 通过ws重新刷新
function fullReload() {
// Cached transform results have stale imports (resolved to
// old locations) so they need to be invalidated before the page is
// reloaded.
server.moduleGraph.invalidateAll()
server.ws.send({
type: 'full-reload',
path: '*'
})
}
return optimizedDeps
}
我们发现获取完对应依赖后,是在runOptimizeDeps
函数完成的代码优化,我们跟进runOptimizeDeps
进行学习吧
vite/src/node/optimizer/index.ts
runOptimizeDeps
export async function runOptimizeDeps(
config: ResolvedConfig,
depsInfo: Record<string, OptimizedDepInfo>
): Promise<DepOptimizationResult> {
config = {
...config,
command: 'build'
}
// 初始化缓存目录
const depsCacheDir = getDepsCacheDir(config)
// 初始化过程目录
const processingCacheDir = getProcessingDepsCacheDir(config)
// Create a temporal directory so we don't need to delete optimized deps
// until they have been processed. This also avoids leaving the deps cache
// directory in a corrupted state if there is an error
// 创建一个临时目录,这样我们就不需要删除优化的 deps
// 直到它们被处理。这也避免了离开 deps 缓存
// 如果有错误,目录处于损坏状态
if (fs.existsSync(processingCacheDir)) {
emptyDir(processingCacheDir)
} else {
fs.mkdirSync(processingCacheDir, { recursive: true })
}
// a hint for Node.js
// all files in the cache directory should be recognized as ES modules
// 写入一个package.json
writeFile(
path.resolve(processingCacheDir, 'package.json'),
JSON.stringify({ type: 'module' })
)
const metadata = createOptimizedDepsMetadata(config)
metadata.browserHash = getOptimizedBrowserHash(
metadata.hash,
depsFromOptimizedDepInfo(depsInfo)
)
// We prebundle dependencies with esbuild and cache them, but there is no need
// to wait here. Code that needs to access the cached deps needs to await
// the optimizedDepInfo.processing promise for each dep
const qualifiedIds = Object.keys(depsInfo)
if (!qualifiedIds.length) {
return {
metadata,
commit() {
// Write metadata file, delete `deps` folder and rename the `processing` folder to `deps`
return commitProcessingDepsCacheSync()
},
cancel
}
}
// esbuild generates nested directory output with lowest common ancestor base
// this is unpredictable and makes it difficult to analyze entry / output
// mapping. So what we do here is:
// 1. flatten all ids to eliminate slash
// 2. in the plugin, read the entry ourselves as virtual files to retain the
// path.
// 翻译:
// esbuild 生成具有最低公共祖先基数的嵌套目录输出
// 这是不可预测的,并且难以分析输入/输出
// 映射。所以我们在这里做的是:
// 1. 展平所有 id 以消除斜线
// 2.在插件中,自己读取入口作为虚拟文件保留路径
const flatIdDeps: Record<string, string> = {}
const idToExports: Record<string, ExportsData> = {}
const flatIdToExports: Record<string, ExportsData> = {}
const { plugins = [], ...esbuildOptions } =
config.optimizeDeps?.esbuildOptions ?? {}
// 初始化 es-module-lexer
await init
for (const id in depsInfo) {
const flatId = flattenId(id)
const filePath = (flatIdDeps[flatId] = depsInfo[id].src!)
let exportsData: ExportsData
if (config.optimizeDeps.extensions?.some((ext) => filePath.endsWith(ext))) {
// For custom supported extensions, build the entry file to transform it into JS,
// and then parse with es-module-lexer. Note that the `bundle` option is not `true`,
// so only the entry file is being transformed.
// 对于自定义支持的扩展,构建入口文件转换成JS,
// 然后用 es-module-lexer 解析。请注意,`bundle` 选项不是 `true`,
// 所以只有入口文件被转换。
const result = await build({
...esbuildOptions,
plugins,
entryPoints: [filePath],
write: false,
format: 'esm'
})
exportsData = parse(result.outputFiles[0].text) as ExportsData
} else {
// 默认命中逻辑,取出文件内容
const entryContent = fs.readFileSync(filePath, 'utf-8')
try {
// 取出导出的数据段落
exportsData = parse(entryContent) as ExportsData
} catch {
const loader = esbuildOptions.loader?.[path.extname(filePath)] || 'jsx'
debug(
`Unable to parse dependency: ${id}. Trying again with a ${loader} transform.`
)
const transformed = await transformWithEsbuild(entryContent, filePath, {
loader
})
// Ensure that optimization won't fail by defaulting '.js' to the JSX parser.
// This is useful for packages such as Gatsby.
esbuildOptions.loader = {
'.js': 'jsx',
...esbuildOptions.loader
}
exportsData = parse(transformed.code) as ExportsData
}
for (const { ss, se } of exportsData[0]) {
const exp = entryContent.slice(ss, se)
if (/export\s+\*\s+from/.test(exp)) {
exportsData.hasReExports = true
}
}
}
idToExports[id] = exportsData
flatIdToExports[flatId] = exportsData
}
const define: Record<string, string> = {
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || config.mode)
}
// 取出定义的文件中的全局常量
for (const key in config.define) {
const value = config.define[key]
define[key] = typeof value === 'string' ? value : JSON.stringify(value)
}
const start = performance.now()
// 通过esbuild对采集的flatIdDeps也就是dep信息进行打包
const result = await build({
absWorkingDir: process.cwd(),
entryPoints: Object.keys(flatIdDeps),
bundle: true,
format: 'esm',
target: config.build.target || undefined,
external: config.optimizeDeps?.exclude,
logLevel: 'error',
splitting: true,
sourcemap: true,
outdir: processingCacheDir,
ignoreAnnotations: true,
metafile: true,
define,
plugins: [
...plugins,
esbuildDepPlugin(flatIdDeps, flatIdToExports, config)
],
...esbuildOptions
})
// 输入输出的元信息
const meta = result.metafile!
// the paths in `meta.outputs` are relative to `process.cwd()`
// 获取相对位置
const processingCacheDirOutputPath = path.relative(
process.cwd(),
processingCacheDir
)
for (const id in depsInfo) {
// 得到对应模块的输出
const output = esbuildOutputFromId(meta.outputs, id, processingCacheDir)
// 把模块信息增加到元信息中
addOptimizedDepInfo(metadata, 'optimized', {
...depsInfo[id],
needsInterop: needsInterop(id, idToExports[id], output),
// We only need to hash the output.imports in to check for stability, but adding the hash
// and file path gives us a unique hash that may be useful for other things in the future
fileHash: getHash(
metadata.hash + depsInfo[id].file + JSON.stringify(output.imports)
),
browserHash: metadata.browserHash
})
}
// 寻找输入中不是依赖模块的文件,那它就一定是chalk包
for (const o of Object.keys(meta.outputs)) {
if (!o.match(jsMapExtensionRE)) {
const id = path
.relative(processingCacheDirOutputPath, o)
.replace(jsExtensionRE, '')
const file = getOptimizedDepPath(id, config)
if (
!findOptimizedDepInfoInRecord(
metadata.optimized,
(depInfo) => depInfo.file === file
)
) {
addOptimizedDepInfo(metadata, 'chunks', {
id,
file,
needsInterop: false,
browserHash: metadata.browserHash
})
}
}
}
// 元信息的地址
const dataPath = path.join(processingCacheDir, '_metadata.json')
// 写入原地址
writeFile(dataPath, stringifyOptimizedDepsMetadata(metadata, depsCacheDir))
debug(`deps bundled in ${(performance.now() - start).toFixed(2)}ms`)
return {
metadata,
commit() {
// Write metadata file, delete `deps` folder and rename the new `processing` folder to `deps` in sync
return commitProcessingDepsCacheSync()
},
cancel
}
// 挪动文件夹
async function commitProcessingDepsCacheSync() {
// Processing is done, we can now replace the depsCacheDir with processingCacheDir
// Rewire the file paths from the temporal processing dir to the final deps cache dir
removeDirSync(depsCacheDir)
await renameDir(processingCacheDir, depsCacheDir)
}
// 取消本次的优化
function cancel() {
removeDirSync(processingCacheDir)
}
}
总结
那么这里我们就完成了对于整个dev
的初始化过程的学习,实际上整个初始化过程总结下来就包含如下流程:
-
初始化配置项,包括
1.1 服务
1.2 ws
1.3 插件
1.4 监控
等一些其他的相关配置
-
启动服务
2.1 启动服务
2.2 根据入口文件查询依赖
2.3 构建元信息,通过esbuild的能力优化依赖
那么关于dev
的过程命令的过程我们就到这里就结束学习,下面我们学习一下当输入网址访问对应的服务时中间件做了什么事情
下一章:vite源码学习-访问本地服务
转载自:https://juejin.cn/post/7098996209800445989