相信体验过 vite
的小伙伴都知道,vite
带给我们的开发体验真的是非常的流畅,甚至你感觉不到他的存在。
vite 解决了什么问题
从表面上看,vite
解决的是速度的问题,一个字就是 “快”。而大背景是:浏览器开始原生支持 ES 模块,意味着网站可以分模块加载。
我们都知道 vite
只是在开发阶段速度非常快,生产打包阶段是使用的 rollup
进行打包,生产打包和我们平时的打包速度是差不多的。
在 vite
的官网上可以知道,vite
是通过在一开始将应用中的模块区分为 依赖
和 源码
两类,改进了开发服务器启动时间。
快的原因
-
esbuild
比babel
快相信很多人都知道
esbuild
的这个打包工具,使用 Go 语言编写的,他们官网有个图,展示了他的构建速度,大概是webpack5
的100倍左右。那咱就说这么好的构建工具,为啥代替不了babel
呢 ? 他还是有一些缺点的。总的来说就是这个工具还在不断的完善中,目前在代码分割和CSS 处理方面的处理还是比较差。vite
的官网也说了:“当未来这些功能稳定后,我们也不排除使用 esbuild 作为生产构建器的可能”。总的来说还是很值得期待的。 -
代码模块拆分
代码模块拆分其实大致就是类似基于路由懒加载的这种感觉,我们开发的时候,并不是对所有的源代码进行修改,正常就是显示那个页面,改哪个页面,也就是说,如果浏览器不显示的页面,即使你改了这部分的源代码,我也可以不用加载他。我只需要更新加载目前打开这个页面,所需要的模块就行了。在
vite
的官网上有这样一个图:
-
缓存
在 Vite 中,HMR 是在原生 ESM 上执行的。当编辑一个文件时,Vite 只需要精确地使已编辑的模块与其最近的 HMR 边界之间的链失活[1](大多数时候只是模块本身),使得无论应用大小如何,HMR 始终能保持快速更新。
Vite 同时利用 HTTP 头来加速整个页面的重新加载(再次让浏览器为我们做更多事情):源码模块的请求会根据304 Not Modified
进行协商缓存,而依赖模块请求则会通过Cache-Control: max-age=31536000,immutable
进行强缓存,因此一旦被缓存它们将不需要再次请求。(31536000 = 24 * 365 * 3600)
源码学习
在 vite
源码中 packages/vite/src/node/cli.ts
这个文件里头,可以看到
我们就转到 packages/vite/src/node/server/index.ts
export async function createServer(inlineConfig = {}) {
const config = await resolveConfig(inlineConfig, 'serve', 'development')
const { root, server } = config
const httpsOptions = await resolveHttpsConfig(
config.server.https,
config.cacheDir
)
const { middlewareMode } = serverConfig
const middlewares = connect() as Connect.Server
// 1. 创建 httpServer
const httpServer = middlewareMode
? null
: await resolveHttpServer(serverConfig, middlewares, httpsOptions)
// 2. 创建 WebSocketServer
const ws = createWebSocketServer(httpServer, config, httpsOptions)
const { ignored = [], ...watchOptions } = serverConfig.watch || {}
// 3. chokidar:监听文件变化的库
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
// ModuleGraph: 记录每个模块之间的关系
const moduleGraph: ModuleGraph = new ModuleGraph((url, ssr) =>
container.resolveId(url, undefined, { ssr })
)
// PluginContainer
const container = await createPluginContainer(config, moduleGraph, watcher)
const server = {
config,
middlewares,
httpServer,
watcher,
pluginContainer: container,
ws,
moduleGraph,
ssrTransform() {},
transformRequest() {},
transformIndexHtml: null!, // to be immediately set
async ssrLoadModule() {},
ssrFixStacktrace(e) {},
ssrRewriteStacktrace(stack: string) {},
listen() {},
async close() {},
async printUrls() {},
async restart() {},
_ssrExternals: null,
_restartPromise: null,
_importGlobMap: new Map(),
_forceOptimizeOnRestart: false,
_pendingRequests: new Map()
}
// transformIndexHtml 插件
server.transformIndexHtml = createDevHtmlTransformFn(server)
const { packageCache } = config
// 进行缓存处理
const setPackageData = packageCache.set.bind(packageCache)
packageCache.set = (id, pkg) => {
if (id.endsWith('.json')) {
watcher.add(id)
}
return setPackageData(id, pkg)
}
// 监听文件修改
watcher.on('change', async (file) => {
file = normalizePath(file)
// invalidate module graph cache on file change
moduleGraph.onFileChange(file)
if (serverConfig.hmr !== false) {
await handleHMRUpdate(file, server)
}
})
// 新建文件
watcher.on('add', (file) => {
handleFileAddUnlink(normalizePath(file), server)
})
// 卸载依赖
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 net.AddressInfo).port
})
}
// apply server configuration hooks from plugins
const postHooks: ((() => void) | void)[] = []
for (const plugin of config.plugins) {
if (plugin.configureServer) {
postHooks.push(await plugin.configureServer(server))
}
}
// run post config hooks
// This is applied before the html middleware so that user middleware can
// serve custom content instead of index.html.
postHooks.forEach((fn) => fn && fn())
const initOptimizer = async () => {
if (isDepsOptimizerEnabled(config)) {
await initDepsOptimizer(config, server)
}
}
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) {
await container.buildStart({})
await initOptimizer()
isOptimized = true
}
return listen(port, ...args)
}) as any
} else {
await container.buildStart({})
await initOptimizer()
}
return server
}
去掉了一些实现的细节,是不是看着就清晰很多了呢?然后再梳理一下这个 createServer
的流程
- 创建
httpServer
、webSocketServer
都是 用于后面构建server
,属于server
中的配置 - 创建
watcher
用于监听文件模块与依赖的变化 - 创建
moduleGraph
记录每个模块之间的关系 - 创建
container
用于处理plugin
postHooks
处理模块中的hook
- 最后就返回一个完成的
server
对象
不知道大家会不会有这么一种感觉,就是看了和没看差不多,都说处理处理,那道理怎么处理呢?我对新的事物的态度,最开始都是了解,应该先去理解他的大致流程,具体实现细节,如果需要的话,再去研究。如果一开始就一头扎进细节里头去,很容易给自己整蒙圈了,也容易给自己带来挫败感,让自己放弃。如果后面还有继续学习里头的细节的话,x再来做记录。