Vite源码浅析
Vite应用的入口文件引入type='module'的script标签
<script type='module' src='/src/main.js' />
浏览器开始原生支持模块功能,也就是说在main.js中可以用ES模块的语句,import或者export,浏览器根据main.js的import依赖发起http请求依赖的模块。
为什么import语句需要转化?
在开发模式中看到main.js中的import Vue from ‘vue’;被转化为
什么是裸模块?
import { a } from './a.js';
import { createApp } from 'vue';
裸模块:没有路径标识
浏览器不支持裸模块的导入,在执行时会报错,被拦截。所以Vite花了大量代码将裸模块的路径转化为绝对或相对路径。
Vite如何实现路径转化?
export async function createServer(
inlineConfig: InlineConfig = {}
): Promise<ViteDevServer> {
const config = await resolveConfig(inlineConfig, 'serve', 'development')
const root = config.root
const serverConfig = config.server
const httpsOptions = await resolveHttpsConfig(config)
let { middlewareMode } = serverConfig
if (middlewareMode === true) {
middlewareMode = 'ssr'
}
const middlewares = connect() as Connect.Server
...
// main transform middleware
middlewares.use(transformMiddleware(server))
return server
}
transformMiddleware中间件:对拦截到请求文件,将其内容转换成浏览器能识别代码
export function transformMiddleware(
server: ViteDevServer
): Connect.NextHandleFunction {
if (
isJSRequest(url) ||
isImportRequest(url) ||
isCSSRequest(url) ||
isHTMLProxy(url)
) {
...
// 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()
}
}
export async function transformRequest(
url: string,
server: ViteDevServer,
options: TransformOptions = {}
): Promise<TransformResult | null> {
const { config, pluginContainer, moduleGraph, watcher } = server
...
// transform
const transformStart = isDebug ? Date.now() : 0
const transformResult = await pluginContainer.transform(code, id, map, ssr)
...
}
pluginContainer.transform() 通过es-module-lexer插件解析文件获取所有的import语句,循环,通过一系列插件用来解析出裸导入的真实路径。
vite配置
- server.port & server.strictPort
server.port 默认端口为3000,如果端口被占用,vite会自动尝试下一个可用的端口,这是基于server.strictPort为false的情况下
export async function httpServerStart(
httpServer: HttpServer,
serverOptions: {
port: number
strictPort: boolean | undefined
host: string | undefined
logger: Logger
}
): Promise<number> {
return new Promise((resolve, reject) => {
let { port, strictPort, host, logger } = serverOptions
const onError = (e: Error & { code?: string }) => {
if (e.code === 'EADDRINUSE') { // 只处理端口被占用的错误
if (strictPort) {
httpServer.removeListener('error', onError)
reject(new Error(`Port {port} is already in use`))
} else {
logger.info(`Port{port} is in use, trying another one...`)
httpServer.listen(++port, host)
}
} else {
httpServer.removeListener('error', onError)
reject(e)
}
}
httpServer.on('error', onError)
httpServer.listen(port, host, () => {
httpServer.removeListener('error', onError)
resolve(port)
})
})
}
- server.open
import open from 'open'
export function openBrowser(
url: string,
opt: string | true,
logger: Logger
): boolean {
// The browser executable to open.
// See https://github.com/sindresorhus/open#app for documentation.
const browser = typeof opt === 'string' ? opt : process.env.BROWSER || ''
if (browser.toLowerCase().endsWith('.js')) {
return executeNodeScript(browser, url, logger)
} else if (browser.toLowerCase() !== 'none') {
return startBrowserProcess(browser, url)
}
return false
}
function startBrowserProcess(browser: string | undefined, url: string) {
// If we're on OS X, the user hasn't specifically
// requested a different browser, we can try opening
// Chrome with AppleScript. This lets us reuse an
// existing tab when possible instead of creating a new one.
const shouldTryOpenChromeWithAppleScript =
process.platform === 'darwin' && (browser === '' || browser === OSX_CHROME)
if (shouldTryOpenChromeWithAppleScript) {
try {
// Try our best to reuse existing tab
// on OS X Google Chrome with AppleScript
execSync('ps cax | grep "Google Chrome"')
execSync('osascript openChrome.applescript "' + encodeURI(url) + '"', {
cwd: path.dirname(require.resolve('vite/bin/openChrome.applescript')),
stdio: 'ignore'
})
return true
} catch (err) {
// Ignore errors
}
}
// Another special case: on OS X, check if BROWSER has been set to "open".
// In this case, instead of passing the string `open` to `open` function (which won't work),
// just ignore it (thus ensuring the intended behavior, i.e. opening the system browser):
// https://github.com/facebook/create-react-app/pull/1690#issuecomment-283518768
if (process.platform === 'darwin' && browser === 'open') {
browser = undefined
}
// Fallback to open
// (It will always open new tab)
try {
const options: open.Options = browser ? { app: { name: browser } } : {}
open(url, options).catch(() => {}) // Prevent `unhandledRejection` error.
return true
} catch (err) {
return false
}
}
参考资料
1. https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Modules
2. https://cn.vitejs.dev/config/#server-port
3. https://juejin.cn/post/6965675731661783076#heading-6