简介
viteDevServer是整个vite运行的核心,下面我们来简单熟悉一下它的运行机制。首先分析一下组成:它是由5大模块和15大中间件组成的
5大模块
- httpServer——node http 服务器
- fsWatcher——chokidar 监听器
- pluginContainer——插件容器,可为特定文件运行插件钩子
- webSocketServer——web socket 服务器
- moduleGraph——跟踪导入关系,url到文件映射;hmr状态模块图
15大中间件(按顺序执行)
- timeMiddleware——记录响应时间
- corsMiddleware——设置跨域
- proxyMiddleware——设置代理
- baseMiddleware——设置base url
- launchEditorMiddleware——加载编辑器
- viteHMRPingMiddleware——HMR心跳检测
- decodeURIMiddleware——解码
- servePublicMiddleware——public目录文件处理
- transformMiddleware——核心转换模块
- serveRawFsMiddleware——fs模块处理
- serveStaticMiddleware——static目录文件处理
- spaFallbackMiddleware——单页应用处理
- indexHtmlMiddleware——index html入口文件处理
- vite404Middleware——404处理
- errorMiddleware——错误处理
下面使用一张图来直观的看一下它们之间的联系
简单介绍一下主要中间件
1、timeMiddleware
import { performance } from 'perf_hooks'
import { Connect } from 'types/connect'
import { createDebugger, prettifyUrl, timeFrom } from '../../utils'
const logTime = createDebugger('vite:time')
export function timeMiddleware(root: string): Connect.NextHandleFunction {
// Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...`
return function viteTimeMiddleware(req, res, next) {
const start = performance.now()
const end = res.end
res.end = (...args: any[]) => {
// 打印时间
logTime(`${timeFrom(start)} ${prettifyUrl(req.url!, root)}`)
// @ts-ignore
return end.call(res, ...args)
}
next()
}
}
2、corsMiddleware
import corsMiddleware from 'cors'
const { cors } = serverConfig
if (cors !== false) {
middlewares.use(corsMiddleware(typeof cors === 'boolean' ? {} : cors))
}
// 在vite.config.ts中配置cors,传给corsMiddleware
3、proxyMiddleware
export function proxyMiddleware(
httpServer: http.Server | null,
config: ResolvedConfig
): Connect.NextHandleFunction {
const options = config.server.proxy!
// lazy require only when proxy is used
const proxies: Record<string, [HttpProxy.Server, ProxyOptions]> = {}
Object.keys(options).forEach((context) => {
let opts = options[context]
if (typeof opts === 'string') {
opts = { target: opts, changeOrigin: true } as ProxyOptions
}
const proxy = httpProxy.createProxyServer(opts) as HttpProxy.Server
proxy.on('error', (err) => {
config.logger.error(`${chalk.red(`http proxy error:`)}\n${err.stack}`, {
timestamp: true,
error: err
})
})
if (opts.configure) {
opts.configure(proxy, opts)
}
// clone before saving because http-proxy mutates the options
proxies[context] = [proxy, { ...opts }]
})
if (httpServer) {
httpServer.on('upgrade', (req, socket, head) => {
const url = req.url!
for (const context in proxies) {
if (doesProxyContextMatchUrl(context, url)) {
const [proxy, opts] = proxies[context]
if (
(opts.ws || opts.target?.toString().startsWith('ws:')) &&
req.headers['sec-websocket-protocol'] !== HMR_HEADER
) {
if (opts.rewrite) {
req.url = opts.rewrite(url)
}
debug(`${req.url} -> ws ${opts.target}`)
proxy.ws(req, socket, head)
return
}
}
}
})
}
// Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...`
return function viteProxyMiddleware(req, res, next) {
const url = req.url!
for (const context in proxies) {
if (doesProxyContextMatchUrl(context, url)) {
const [proxy, opts] = proxies[context]
const options: HttpProxy.ServerOptions = {}
if (opts.bypass) {
const bypassResult = opts.bypass(req, res, opts)
if (typeof bypassResult === 'string') {
req.url = bypassResult
debug(`bypass: ${req.url} -> ${bypassResult}`)
return next()
} else if (isObject(bypassResult)) {
Object.assign(options, bypassResult)
debug(`bypass: ${req.url} use modified options: %O`, options)
return next()
} else if (bypassResult === false) {
debug(`bypass: ${req.url} -> 404`)
return res.end(404)
}
}
debug(`${req.url} -> ${opts.target || opts.forward}`)
if (opts.rewrite) {
req.url = opts.rewrite(req.url!)
}
proxy.web(req, res, options)
return
}
}
next()
}
}
// 在vite.config.ts中配置proxy代理信息,中间件会接收代理进行配置
4、transformMiddleware
// 这是核心转换模块,处理文件路径,文件类型转换等等
export function transformMiddleware(
server: ViteDevServer
): Connect.NextHandleFunction {
const {
config: { root, logger, cacheDir },
moduleGraph
} = server
// determine the url prefix of files inside cache directory
let cacheDirPrefix: string | undefined
if (cacheDir) {
// 缓存地址
const cacheDirRelative = normalizePath(path.relative(root, cacheDir))
if (cacheDirRelative.startsWith('../')) {
// if the cache directory is outside root, the url prefix would be something
// like '/@fs/absolute/path/to/node_modules/.vite'
cacheDirPrefix = `/@fs/${normalizePath(cacheDir).replace(/^\//, '')}`
} else {
// if the cache directory is inside root, the url prefix would be something
// like '/node_modules/.vite'
cacheDirPrefix = `/${cacheDirRelative}`
}
}
// Keep the named function. The name is visible in debug logs via `DEBUG=connect:dispatcher ...`
return async function viteTransformMiddleware(req, res, next) {
if (req.method !== 'GET' || knownIgnoreList.has(req.url!)) {
return next()
}
if (
server._pendingReload &&
// always allow vite client requests so that it can trigger page reload
!req.url?.startsWith(CLIENT_PUBLIC_PATH) &&
!req.url?.includes('vite/dist/client')
) {
// missing dep pending reload, hold request until reload happens
server._pendingReload.then(() =>
// If the refresh has not happened after timeout, Vite considers
// something unexpected has happened. In this case, Vite
// returns an empty response that will error.
setTimeout(() => {
// Don't do anything if response has already been sent
if (res.writableEnded) return
// status code request timeout
res.statusCode = 408
res.end(
`<h1>[vite] Something unexpected happened while optimizing "${req.url}"<h1>` +
`<p>The current page should have reloaded by now</p>`
)
}, NEW_DEPENDENCY_BUILD_TIMEOUT)
)
return
}
let url = decodeURI(removeTimestampQuery(req.url!)).replace(
NULL_BYTE_PLACEHOLDER,
'\0'
)
const withoutQuery = cleanUrl(url)
try {
const isSourceMap = withoutQuery.endsWith('.map')
// since we generate source map references, handle those requests here
if (isSourceMap) {
const originalUrl = url.replace(/\.map($|\?)/, '$1')
const map = (await moduleGraph.getModuleByUrl(originalUrl))
?.transformResult?.map
if (map) {
return send(req, res, JSON.stringify(map), 'json')
} else {
return next()
}
}
// check if public dir is inside root dir
const publicDir = normalizePath(server.config.publicDir)
const rootDir = normalizePath(server.config.root)
if (publicDir.startsWith(rootDir)) {
const publicPath = `${publicDir.slice(rootDir.length)}/`
// warn explicit public paths
if (url.startsWith(publicPath)) {
logger.warn(
chalk.yellow(
`files in the public directory are served at the root path.\n` +
`Instead of ${chalk.cyan(url)}, use ${chalk.cyan(
url.replace(publicPath, '/')
)}.`
)
)
}
}
if (
isJSRequest(url) ||
isImportRequest(url) ||
isCSSRequest(url) ||
isHTMLProxy(url)
) {
// strip ?import
url = removeImportQuery(url)
// Strip valid id prefix. This is prepended to resolved Ids that are
// not valid browser import specifiers by the importAnalysis plugin.
url = unwrapId(url)
// for CSS, we need to differentiate between normal CSS requests and
// imports
if (
isCSSRequest(url) &&
!isDirectRequest(url) &&
req.headers.accept?.includes('text/css')
) {
url = injectQuery(url, 'direct')
}
// check if we can return 304 early
const ifNoneMatch = req.headers['if-none-match']
if (
ifNoneMatch &&
(await moduleGraph.getModuleByUrl(url))?.transformResult?.etag ===
ifNoneMatch
) {
isDebug && debugCache(`[304] ${prettifyUrl(url, root)}`)
res.statusCode = 304
return res.end()
}
// 通过这个方法生成新的依赖ID,并加载进来,更新moduleGraph
// resolve, load and transform using the plugin container
const result = await transformRequest(url, server, {
html: req.headers.accept?.includes('text/html')
})
if (result) {
const type = isDirectCSSRequest(url) ? 'css' : 'js'
const isDep =
DEP_VERSION_RE.test(url) ||
(cacheDirPrefix && url.startsWith(cacheDirPrefix))
return send(
req,
res,
result.code,
type,
result.etag,
// allow browser to cache npm deps!
isDep ? 'max-age=31536000,immutable' : 'no-cache',
result.map
)
}
}
} catch (e) {
return next(e)
}
next()
}
}
关于viteDevServer,这里就简单的介绍一下它的组成,在我们使用的过程中,是没有bundle打包的,它是利用原生ES模块的方式来处理引入文件进行热更新,这种方式使得vite开发在冷启动和模块重载方面的体验非常巴适。。。