Open kangyana opened 1 year ago
Vite 是新一代的前端构建工具,在尤雨溪开发 Vue3.0 的时候诞生。 类似于 Webpack + Webpack-dev-server。 其主要利用浏览器 ESM 特性导入组织代码,在服务器端按需编译返回,完全跳过了打包这个概念,服务器随起随用。 生产中利用 Rollup 作为打包工具,号称下一代的前端构建工具。
Vite 有如下特点:
Webpack 是近年来使用量最大,同时社区最完善的前端打包构建工具,新出的5.x版本对构建细节进行了优化,在部分场景下打包速度提升明显。 Webpack 在启动时,会先构建项目模块的依赖图,如果在项目中的某个地方改动了代码,Webpack 则会对相关的依赖重新打包,随着项目的增大,其打包速度也会下降。
Vite 相比于 Webpack 而言,没有打包的过程,而是直接启动了一个开发服务器 devServer。 Vite 劫持浏览器的 HTTP 请求,在后端进行相应的处理将项目中使用的文件通过简单的分解与整合,然后再返回给浏览器(整个过程没有对文件进行打包编译)。 所以编译速度很快。
Snowpack 首次提出利用浏览器原生 ESM 能力的打包工具,其理念就是减少或避免整个 bundle 的打包。 默认在 dev 和 production 环境都使用 unbundle 的方式来部署应用。 但是它的构建时却是交给用户自己选择,整体的打包体验显得有点支离破碎。
而 Vite 直接整合了 Rollup,为用户提供了完善、开箱即用的解决方案,并且由于这些集成,也方便扩展更多的高级功能。 两者较大的区别是在需要 bundle 打包的时候 Vite 使用 Rollup 内置配置,而 Snowpack 通过其他插件将其委托给 webpack。
在了解 Vite 之前,需要先了解下 ESM。
ESM 是 JavaScript 提出的官方标准化模块系统,不同于之前的 CJS,AMD,CMD 等等,ESM 提供了更原生以及更动态的模块加载方案。
最重要的就是它是浏览器原生支持的,也就是说我们可以直接在浏览器中去执行 import
,动态引入我们需要的模块,而不是把所有模块打包在一起。
目前 ESM 模块化已经支持92%以上的浏览器,而且且作为 ECMA 标准,未来会有更多浏览器支持 ECMA 规范。
当我们在使用模块开发时,其实就是在构建一张模块依赖关系图,当模块加载时,就会从入口文件开始,最终生成完整的模块实例图。
ESM 的执行可以分为三个步骤:
从上面实例化的过程可以看出,ESM 使用实时绑定的模式,导出和导入的模块都指向相同的内存地址,也就是值引用。 而 CJS 采用的是值拷贝,即所有导出值都是拷贝值。
Vite 底层使用 Esbuild 实现对 .ts
、.jsx
、.js
代码文件的转化,所以先看下什么是 es-build。
Esbuild是一个JavaScript`` Bundler 打包和压缩工具,它提供了与 Webpack、Rollup 等工具相似的资源打包能力。 可以将J avaScript 和 TypeScript 代码打包分发在网页上运行。 但其打包速度却是其他工具的10~100倍。
目前他支持以下的功能:
Tree shaking
Source map
生成esbuild 总共提供了四个函数:transform
、build
、buildSync
、Service
。
在生产环境下,Vite 使用 Rollup 来进行打包。
Rollup 是基于 ESM 的 JavaScript 打包工具。
相比于其他打包工具如 Webpack,他总是能打出更小、更快的包。
因为 Rollup 基于 ESM 模块,比 Webpack 和 Browserify 使用的 CommonJS 模块机制更高效。
Rollup 的亮点在于同一个地方,一次性加载。
能针对源码进行 Tree Shaking
(去除那些已被定义但没被使用的代码),以及 Scope Hoisting
以减小输出文件大小提升运行性能。
Rollup 分为 build(构建)阶段和 output generate(输出生成)阶段。 主要过程如下:
module
,生成抽象语法树如果你的项目只有JavaScript,而没有其他的静态资源文件,使用 Webpack 就有点大才小用了。 因为 Webpack 打包的文件的体积略大,运行略慢,可读性略低。 这时候建议使用 Rollup。
详细阐述下:
module
时,如:
<script type="module" src="/src/main.js"></script>
main.js
文件:
// main.js
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
main.js
文件,会检测到内部含有 import
引入的包,又会 import
引用发起 HTTP 请求获取模块的内容文件,如App.vue
、vue
文件。Vite 其核心原理是利用浏览器现在已经支持 ES6 的 import
,碰见 import
就会发送一个 HTTP 请求去加载文件。
Vite 启动一个 koa 服务器拦截这些请求,并在后端进行相应的处理将项目中使用的文件通过简单的分解与整合,然后再以 ESM 格式返回返回给浏览器。
Vite 整个过程中没有对文件进行打包编译,做到了真正的按需加载,所以其运行速度比原始的 webpack 开发编译速度快出许多!
在 Vite 出来之前,传统的打包工具如 Webpack 是先解析依赖、打包构建再启动开发服务器, Dev Server 必须等待所有模块构建完成,当我们修改了 bundle模块中的一个子模块, 整个 bundle 文件都会重新打包然后输出。 项目应用越大,启动时间越长。
而 Vite 利用浏览器对 ESM 的支持,当 import
模块时,浏览器就会下载被导入的模块。
先启动开发服务器,当代码执行到模块加载时再请求对应模块的文件,本质上实现了动态加载。
灰色部分是暂时没有用到的路由,所有这部分不会参与构建过程。
随着项目里的应用越来越多,增加 route
,也不会影响其构建速度。
目前所有的打包工具实现热更新的思路都大同小异:主要是通过 WebSocket 创建浏览器和服务器的通信监听文件的改变,当文件被修改时,服务端发送消息通知客户端修改相应的代码,客户端对应不同的文件进行不同的操作的更新。
Vite 整个热更新过程可以分成四步:
在 Vite dev server 启动之前,Vite 会为 HMR 做一些准备工作: 比如创建 websocket 服务,利用 chokidar 创建一个监听对象 watcher 用于对文件修改进行监听等等,具体核心代码:
packages/vite/src/node/server/index.ts
export async function createServer(
inlineConfig: InlineConfig = {}
): Promise<ViteDevServer> {
....
const ws = createWebSocketServer(httpServer, config, httpsOptions)
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
....
watcher.on('change', async (file) => {
})
watcher.on('add', (file) => {
})
watcher.on('unlink', (file) => {
})
...
return server
}
createWebSocketServer
这个方法主是创建 WebSocket 服务并对错误进行一些处理,最后返回封装好的 on
、off
、 send
和 close
方法,用于后续服务端推送消息和关闭服务。
packages/vite/src/node/server/ws.ts
export function createWebSocketServer(
server: Server | null,
config: ResolvedConfig,
httpsOptions?: HttpsServerOptions
): WebSocketServer {
let wss: WebSocket
let httpsServer: Server | undefined = undefined
// 热更新配置
const hmr = isObject(config.server.hmr) && config.server.hmr
const wsServer = (hmr && hmr.server) || server
// 普通模式
if (wsServer) {
wss = new WebSocket({ noServer: true })
wsServer.on('upgrade', (req, socket, head) => {
// 监听通过vite客户端发送的websocket消息,通过HMR_HEADER区分
if (req.headers['sec-websocket-protocol'] === HMR_HEADER) {
wss.handleUpgrade(req, socket as Socket, head, (ws) => {
wss.emit('connection', ws, req)
})
}
})
} else { // 中间件模式
// vite dev server in middleware mode
wss = new WebSocket(websocketServerOptions)
}
wss.on('connection', (socket) => {
...
})
// 错误处理
wss.on('error', (e: Error & { code: string }) => {
...
})
// 返回
return {
on: wss.on.bind(wss),
off: wss.off.bind(wss),
send(payload: HMRPayload) {
...
},
close() {
...
}
}
}
接收到文件改动执行的回调,这里主要两个操作:moduleGraph.onFileChange
修改文件的缓存和 handleHMRUpdate
执行热更新:
packages/vite/src/node/server/index.ts
watcher.on('change', async (file) => {
file = normalizePath(file)
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)
})
}
}
})
moduleGraph
是 Vite 定义的用来记录整个应用的模块依赖图的类,除此之外还有 moduleNode
。
packages/vite/src/node/server/moduleGraph.ts
watcher.on('change', async (file) => {
file = normalizePath(file)
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)
})
}
}
})
moduleGraph
是由一系列 map 组成,而这些 map 分别是 url
、id
、file
等与 ModuleNode
的映射,
而 ModuleNode
是 Vite 中定义的 最小模块单位。
通过这两个类可以构建下面的模块依赖图:
可以看看 moduleGraph.onFileChange
这个函数:主要是用来清空被修改文件对应的 ModuleNode
对象的
transformResult
属性,使之前的模块已有的转换缓存失效。
这块也就是 Vite 在热更新里的缓存机制。
packages/vite/src/node/server/moduleGraph.ts
onFileChange(file: string): void {
const mods = this.getModulesByFile(file)
if (mods) {
const seen = new Set<ModuleNode>()
mods.forEach((mod) => {
this.invalidateModule(mod, seen)
})
}
}
invalidateModule(mod: ModuleNode, seen: Set<ModuleNode> = new Set()): void {
mod.info = undefined
mod.transformResult = null
mod.ssrTransformResult = null
invalidateSSRModule(mod, seen)
}
handleHMRUpdate
模块主要是监听文件的更改,进行处理和判断通过 WebSocket 给客户端发送消息通知客户端去请求新的模块代码。
packages/vite/packages/vite/src/node/server/hmr.ts
客户端:当我们配置了热更新且不是 ssr 的时候,Vite 底层在处理 html 的时候会把 HMR 相关的客户端代码写入在我们的代码中,如下:
当接收到服务端推送的消息,通过不同的消息类型做相应的处理,如(connected
、update
、custom
...),
在实际开发热更新中使用最频繁的是 update
(动态加载热更新模块)和 full-reload
(刷新整个页面)事件。
packages/vite/packages/vite/src/client/client.ts
同时,Vite 还利用 HTTP 加速整个页面的重新加载。
设置响应头使得依赖模块(dependency module)进行强缓存,而源码文件通过设置 304 Not Modified
而变成可依据条件而进行更新。
若需要对依赖代码模块做改动可手动操作使缓存失效:
vite --force
或者手动删除 node_modules/.vite
中的缓存文件。
node_modules/.vite
除此之外,我们常用的 lodash
工具库,里面有很多包通过单独的文件相互导入,
而 lodash-es
这种包会有几百个子模块,当代码中出现 import { debounce } from 'lodash-es'
会发出几百个 HTTP 请求,
这些请求会造成网络堵塞,影响页面的加载。
Vite 将有许多内部模块的 ESM 依赖关系转换为单个模块,以提高后续页面加载性能。
通过预构建 lodash-es
成为一个模块,也就只需要一个 HTTP 请求了!
引用尤大的一句话:“快”就一个字。
这是 Esbuild 首页的图。 新一代的打包工具,提供了与 Webpack、Rollup、Parcel 等工具相似的资源打包能力,但在时速上达到10~100倍的差距,耗时是 Webpack 2%~3%。
Vite 预编译之后,将文件缓存在 node_modules/.vite/
文件夹下。根据以下地方来决定是否需要重新执行预构建。
package.json
中:dependencies
发生变化。lockfile
。如果想强制让 Vite 重新预构建依赖,可以使用 --force
启动开发服务器,或者直接删掉 node_modules/.vite/
文件夹。
createServer
创建 server
对象后,当服务器启动会执行 httpServer.listen
方法。createServer
时,Vite 底层会重写 server.listen
方法:首先调用插件的 buildStart
再执行 runOptimize()
方法。runOptimize()
调用 optimizeDeps()
和 createMissingImporterRegisterFn()
方法。optimizeDeps()
主要是根据配置文件生成 hash,获取上次预购建的内容(存放在 _metadata.json
文件)。
如果不是强预构建就对比 _metadata.json
文件的 hash 和新生成的 hash:
一致就返回 _metadata.json
文件的内容,否则清空缓存文件调用 Esbuild 构建模块再次存入 _metadata.json
文件。const runOptimize = async () => {
if (config.cacheDir) {
server._isRunningOptimizer = true
try {
server._optimizeDepsMetadata = await optimizeDeps(
config,
config.server.force || server._forceOptimizeOnRestart
)
} finally {
server._isRunningOptimizer = false
}
server._registerMissingImport = createMissingImporterRegisterFn(server)
}
}
if (!middlewareMode && httpServer) {
let isOptimized = false
// overwrite listen to run optimizer before server start
const listen = httpServer.listen.bind(httpServer)
httpServer.listen = (async (port: number, ...args: any[]) => {
if (!isOptimized) {
try {
await container.buildStart({})
await runOptimize()
isOptimized = true
} catch (e) {
httpServer.emit('error', e)
return
}
}
return listen(port, ...args)
}) as any
} else {
await container.buildStart({})
await runOptimize()
}
核心代码都在 packages/vite/src/node/optimizer/index.ts
里面。
esbuildScanPlugin
。esbuildDepPlugin
。Vite 从 preact 的 WMR
中得到了启发,将 Vite Plugins 继承 Rollup Plugins API
,
在其基础上进行了一些扩展(如 Vite 特有的钩子等),同时 Vite 也基于 Rollup plugins 机制提供了强大的插件 API。
目前和 Vite 兼容或者内置的插件,可以查看 vite-rollup-plugins
。
使用 Vite 插件可以扩展 Vite 能力,通过暴露一些构建打包过程的一些时机配合工具函数,让用户可以自定义地写一些配置代码,执行在打包过程中。 比如解析用户自定义的文件输入,在打包代码前转译代码,或者查找。
在实际的实现中,Vite 仅仅需要基于 Rollup 设计的接口进行扩展,在保证兼容 Rollup 插件的同时再加入一些 Vite 特有的钩子和属性来进行扩展。
config
:可以在 Vite 被解析之前修改 Vite 的相关配置。
钩子接收原始用户配置 config
和一个描述配置环境的变量 env
。configResolved
:解析 Vite 配置后调用,配置确认。configureserver
:主要用来配置开发服务器,为 dev-server 添加自定义的中间件。transformindexhtml
:主要用来转换 index.html
,钩子接收当前的 HTML 字符串和转换上下文。handlehotupdate
:执行自定义 HMR
更新,可以通过 ws 往客户端发送自定义的事件。具体使用请移步这里。
options
:获取、操纵 Rollup 选项buildstart
:开始创建resolveId
:创建自定义确认函数,可以用来定位第三方依赖load
:可以自定义加载器,可用来返回自定义的内容transform
:在每个传入模块请求时被调用,主要是用来转换单个模块buildend
:在服务器关闭时被调用closeBundle
其它钩子使用请移步这里。
Vite 的官网可以看出:
Vite 插件可以用一个 enforce
属性(类似于 Webpack 加载器)来调整它的应用顺序。
enforce
的值可以是 pre
或 post
。
解析后的插件将按照以下顺序排列:
Alias
enforce:'pre'
的自定义插件enforce
的自定义插件enforce:'post'
的自定义插件export default function myVitePlugin () {
// 定义vite插件唯一id
const virtualFileId = '@my-vite-plugin'
// 返回的整个插件对象
return {
// 必须的,将会显示在 warning 和 error 中
name: 'vite-plugin',
// 钩子
// config
config: (config, env) => ({
console.log('config',config)
return {}
}),
// 确认config
configResolved: config => ({}),
options: options => ({}),
buildStart: options => ({}),
transformIndexHtml: (html, ctx) => ({
return html
}),
//确认
resolveId: (source, importer) => ({}),
// 转换
transform: (code, id) => ({})
}
}
vite.config.js/ts
中引用
// vite.config.js/ts
import myVitePlugin from '...'
export default defineConfig{ plugins:[vue(),myVitePlugin()] }
最后总结下 Vite 相关的优缺点:
No Bundle
和 esbuild 预构建,速度远快于 Webpack。Vite.js 虽然才在构建打包场景兴起,但在很多场景下基本都会优于现有的解决方案。
如果有生态、想要丰富的 loader
、plugins
的要求可以考虑成熟的 Webpack。
在其余情况下,Vite.js 不失为一个打包构建工具的好选择。
为什么这么快?生产环境能用吗?