Vite 介绍与源码分析
esbuild 简介 🔗
Vite 在开发和构建中都使用到了 esbuild,所以在正式 Vite 前,我们先来简单了解一下 esbuild。
Our current build tools for the web are 10-100x slower than they could be. The main goal of the esbuild bundler project is to bring about a new era of build tool performance, and create an easy-to-use modern bundler along the way.
esbuild 的主要定位是下一代构建工具,带来更好的性能和易用性。为什么 esbuild 能够带来更好的性能可以阅读 esbuild 官网这篇文章。以下是从文章中摘取的部分内容:
- It's written in Go and compiles to native code. esbuild 是基于 go 语言实现的。不同于 JavaScript,使用 JavaScript 编写的打包器在编译代码之前需要先解析打包器本身的代码。而 go 直接使用编译后的静态代码(native code),省略了这个步骤,所以打包速度更快。
- Parallelism is used heavily. 多线程。
- Everything in esbuild is written from scratch. esbuild 所有内容都是自己设计的,没有引入第三方库。
- Memory is used efficiently.
内置支持的文件类型 🔗
- JavaScript。对应
.js
.cjs(CommonJS module)
.mjs(ES module)
后缀的文件。esbuild 默认输出所有语言特性,不会转换到更低的 ES 版本。如果需要,需要指定target
。同时,它不支持转换到 ES5,更多详见。 - TypeScript。对应
.ts
.tsx
.mts
.cts
后缀的文件。 - JSX。
- JSON。
- TXT。
- Binary。
- Base64。
- Data URL。
- External file。 更多类型详见 esbuild 官网。
Production readiness (生产准备)🔗
I'm not planning on including major features that I'm not interested in building and/or maintaining. I also want to limit the project's scope so it doesn't get too complex and unwieldy, both from an architectural perspective, a testing and correctness perspective, and from a usability perspective. Think of esbuild as a "linker" for the web. It knows how to transform and bundle JavaScript and CSS. But the details of how your source code ends up as plain JavaScript or CSS may need to be 3rd-party code. ... esbuild is not meant to be an all-in-one solution for all frontend needs.
翻译:我不打算包括那些我对构建和/或维护不感兴趣的主要特性。同时我想要限制项目的范围,这样它不会变得太复杂和笨拙,无论是从体系结构的角度,测试和正确性的角度,还是从可用性的角度。可以把 esbuild 看作是 web 的“连接器”。它知道如何转换和打包 JavaScript 和 CSS。但是,源代码如何转换为 JavaScript 或 CSS 可能需要其它第三方工具。
总的来说,esbuild 并不意味着是一个针对所有前端需要的一体化解决方案。所以 Vite 也只是部分使用了 esbuild。
什么是 Vite
根据官网的定义,Vite 是下一代前端开发与构建工具,它有以下特性(引自 Vite 中文官网):
- 💡 极速的服务启动。使用原生 ESM 文件,无需打包!
- **⚡️ 轻量快速的热重载。**无论应用程序大小如何,都始终极快的模块热重载(HMR)。
- **🛠️ 丰富的功能。**对 TypeScript、JSX、CSS 等支持开箱即用。
- **📦 优化的构建。**可选 “多页应用” 或 “库” 模式的预配置 Rollup 构建。
- **🔩 通用的插件。**在开发和构建之间共享 Rollup-superset 插件接口。
- **🔑 完全类型化的API。**灵活的 API 和完整 TypeScript 类型。
Vite 集成了开发与构建两部分,主要由两部分组成:
- 一个开发服务器,它基于 原生 ES 模块 提供了 丰富的内建功能,如速度快到惊人的 模块热更新(HMR)。
- 一套构建指令,它使用 Rollup 打包你的代码,并且它是预配置的,可输出用于生产环境的高度优化过的静态资源。
Vite 意在提供开箱即用的配置,同时它的 插件 API 和 JavaScript API 带来了高度的可扩展性,并有完整的类型支持。
比较 Vite 和 Webpack
从定位角度来说,webpack core 是一个纯打包工具(对标 Rollup),而 Vite 其实是一个更上层的工具链方案,对标的是(webpack + 针对 web 的常用配置 + webpack-dev-server)。
webpack core 因为只针对打包不预设场景,所以设计得极其灵活,不局限于针对 web 打包,几乎所有可配置的环节都做成了可配置的。这种极度的灵活性对于一些特定场景依然不可替代。但反过来导致的缺点就是配置项极度复杂,插件机制和内部逻辑晦涩难懂,针对常见的 web 也需要大量的配置。另外大量 loader 插件虽然单独发布却存在各种隐式耦合,很容易配置不当互相影响。
Vite 的选择是缩窄预设场景来降低复杂度。如果预设了 web 的场景,那么大部分常见的 web 构建需求都可以直接做成默认内置。由于内置,可以适当的增加各个环节之间的耦合来进一步降低复杂度;同时浏览器场景下意味着可以利用原生 ESM,更进一步又可以基于原生 ESM 实现理论最优性能的热更新。
......
所以 Vite 能不能干掉 webpack?…… 这个也本来就不是 Vite 的目标 …… 开发新工具的目的不是 “干掉竞争对手”,而是让愿意用的人用得爽。
引自:尤雨溪知乎回答 ⬇
Vite 以 ESM 的形式提供源码,所以 Vite 不需要和 Webpack 一样在构建整个服务后才提供服务,只需要在浏览器请求源码时进行转换并按需提供源码。这部分可以直接阅读 Vite 官网文档(内容并不多)⬇
由于 Vite 使用原生的 ES Modules 提供源码,所以最好还要提前了解关于 ES Modules 的内容。可以阅读下面这篇文章,这篇文章图文并茂,详细地讲解了关于 ES Modules 的原理和加载过程。
ES modules: A cartoon deep-dive - Mozilla Hacks - the Web developer blog
源码分析 - 本地 dev server 🔗
开发时,Vite 使用 esbuild 预构建依赖,以 原生 ESM 的方式来提供源码。
我们从开发时启动 vite 服务器脚本命令开始,简单看看 vite 都做了些什么。
启动指令 - npm run dev
// 文件路径: packages/vite/src/node/cli.ts | line: 63
...
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')
}
await server.listen()
...
}
简单来看,vite 通过 createServer
函数创建了一个开发服务器实例,并监听端口。
程序入口 - createServer()
createServer 函数是程序主入口,主要做了以下一些事情,
- 通过 node 的
http
模块创建一个 httpServer 对象。 - 通过
connect
库创建一个中间件实例,用于拦截请求,做一些操作(比如重写文件内容等)。 - 创建一个 websocket 实例,用于 HMR 等。
- 使用
chokidar
库监听文件变动,用于 HMR 等。 - 创建
moduleGraph
对象,用于缓存编译后的文件。 - 使用
esbuild
预构建依赖并缓存。
预构建依赖
从结果来看,预构建依赖的结果会被缓存在项目 /node_modules/.vite
下,并生成 _metadata.json 文件。这个文件提供了预构建相关的一些信息,示例如下,
{
"hash": "3fc2a69b",
"browserHash": "3c407b52",
"optimized": {
"react": {
"file": "/Users/usr/Desktop/BundlerCompare/my-test-app/node_modules/.vite/react.js",
"src": "/Users/usr/Desktop/BundlerCompare/my-test-app/node_modules/.pnpm/react@17.0.2/node_modules/react/index.js",
"needsInterop": true
},
"react-dom": {
"file": "/Users/usr/Desktop/BundlerCompare/my-test-app/node_modules/.vite/react-dom.js",
"src": "/Users/usr/Desktop/BundlerCompare/my-test-app/node_modules/.pnpm/react-dom@17.0.2_react@17.0.2/node_modules/react-dom/index.js",
"needsInterop": true
},
"react/jsx-dev-runtime": {
"file": "/Users/usr/Desktop/BundlerCompare/my-test-app/node_modules/.vite/react_jsx-dev-runtime.js",
"src": "/Users/usr/Desktop/BundlerCompare/my-test-app/node_modules/.pnpm/react@17.0.2/node_modules/react/jsx-dev-runtime.js",
"needsInterop": true
}
}
}
- hash 是根据需要预构建的文件内容生成的,实现一个缓存的效果,在启动 dev server 时可以避免重复构建项目依赖;
- browserHash 是由 hash 和在运行时发现额外依赖时生成的,用于使预构建的依赖的浏览器请求无效;
- optimized 是一个包含所有需要预构建的依赖的对象,src 表示依赖的源文件,file 表示构建后所在的路径;
- needsInterop 表示是否对 CommoJS 的依赖引入代码进行重写。比如,当我们在 vite 项目中使用 react 时:
import React, { useState } from 'react'
,react 的 needsInterop 为 true,所以 importAnalysisPlugin 插件的会对导入 react 的代码进行重写。
下面我们介绍预构建过程。
当我们首次调用 server.listen() 时,会在启动服务器之前先触发 runOptimize
函数,如下
// 文件路径:vite/src/node/server/index.ts | line: 575
const runOptimize = async () => {
if (config.cacheDir) {
// 设置构建状态的标志位为true
server._isRunningOptimizer = true
try {
// 预构建依赖
server._optimizeDepsMetadata = await optimizeDeps(config)
} finally {
// 设置构建状态的标志位为flase
server._isRunningOptimizer = false
}
// 返回一个预构建函数可以随时进行预构建
server._registerMissingImport = createMissingImporterRegisterFn(server)
}
}
重点在 optimizeDeps(config)
这一行,通过 optimizeDeps 函数,进行了依赖的预构建。下面简单分析一下这个过程,
-
读取项目的 package-lock.json、yarn.lock 和 pnpm-lock.yaml 和配置文件 vite.config.js 中 optimizeDeps 的参数,生成 mainHash。
const dataPath = path.join(cacheDir, '_metadata.json') const mainHash = getDepHash(root, config) const data: DepOptimizationMetadata = { hash: mainHash, browserHash: mainHash, optimized: {} }
-
与上次构建的 hash 值做比较,判断是否需要重新构建。
// force 从 vite.config.js 中的 config.server.force 字段获取,用于强制构建依赖 if (!force) { let prevData: DepOptimizationMetadata | undefined try { // 读取 /node_modules/.vite/_metadata.json 文件内容 prevData = JSON.parse(fs.readFileSync(dataPath, 'utf-8')) } catch (e) {} // hash 一致,不需要重新构建 if (prevData && prevData.hash === data.hash) { log('Hash is consistent. Skipping. Use --force to override.') return prevData } }
-
如果需要构建,则删除之前的缓存文件夹(如果有),并重新创建。
if (fs.existsSync(cacheDir)) { emptyDir(cacheDir) } else { fs.mkdirSync(cacheDir, { recursive: true }) }
-
使用 esbuild 的 scanImports 函数扫描源码,找出与预构建相关的依赖项 deps。这里的 newDeps 表示我们在服务启动后,加入的新的依赖。
let deps: Record<string, string>, missing: Record<string, string> if (!newDeps) { // 扫描需要预优化的依赖 ;({ deps, missing } = await scanImports(config)) } else { deps = newDeps missing = {} }
-
加入 vite.config.js 中 optimizeDeps.include 的依赖。
const include = config.optimizeDeps?.include if (include) { const resolve = config.createResolver({ asSrc: false }) for (const id of include) { // normalize 'foo >bar` as 'foo > bar' to prevent same id being added // and for pretty printing const normalizedId = normalizeId(id) if (!deps[normalizedId]) { // 找到依赖的入口文件 const entry = await resolve(id) // 加入到 deps if (entry) { deps[normalizedId] = entry } else { throw new Error( `Failed to resolve force included dependency: ${colors.cyan(id)}` ) } } } }
-
使用 esbuild 构建依赖。
const result = await build({ absWorkingDir: process.cwd(), // 打包入口 entryPoints: Object.keys(flatIdDeps), // 将模块的依赖和模块自身打包成一个文件 bundle: true, // 输出格式为 esm format: 'esm', target: config.build.target || undefined, external: config.optimizeDeps?.exclude, logLevel: 'error', splitting: true, sourcemap: true, // 预优化的缓存文件夹,默认为 node_modules/.vite outdir: cacheDir, ignoreAnnotations: true, metafile: true, define, plugins: [ ...plugins, // 打包核心逻辑 esbuildDepPlugin(flatIdDeps, flatIdToExports, config, ssr) ], ...esbuildOptions })
esbuildDepPlugin 简单来说就是对依赖内容进行了分析,之后交给 esbuild 对应的 loader 去处理文件内容。
-
构建完成后,重新创建 _metadata.json 文件并写入本次构建内容。
const cacheDirOutputPath = path.relative(process.cwd(), cacheDir) for (const id in deps) { const entry = deps[id] data.optimized[id] = { file: normalizePath(path.resolve(cacheDir, flattenId(id) + '.js')), src: entry, needsInterop: needsInterop( id, idToExports[id], meta.outputs, cacheDirOutputPath ) } } writeFile(dataPath, JSON.stringify(data, null, 2))
💡 以上内容参考了文章 《vite介绍 | 与其他构建工具做比较,分析vite预构建和热更新的原理》
访问本地服务
预构建依赖后,vite 就启动了服务,我们可以通过 http://localhost:port/
访问。
访问时,我们请求的是项目根目录下的 index.html 文件,文件中引用了源码的入口文件,
<script type="module" src="/src/main.tsx"></script>
当 type 表示为 module
时,表示我们以 ES Modules 的形式引用,浏览器会将文件内 import
和 export
解析为 ES Modules 语法,并解析文件内容,根据内容的 import 引用的文件再去发起请求,获取对应文件,这样递归生成了一棵模块树。
以上述 main.tsx
文件为例,源码如下,
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
)
浏览器请求的响应如下,
var _jsxFileName = "/Users/didi/Desktop/\u5B66\u4E60/BundlerCompare/my-test-app/src/main.tsx";
import __vite__cjsImport0_react from "/node_modules/.vite/react.js?v=3c407b52";
const React = __vite__cjsImport0_react.__esModule ? __vite__cjsImport0_react.default : __vite__cjsImport0_react;
import __vite__cjsImport1_reactDom from "/node_modules/.vite/react-dom.js?v=3c407b52";
const ReactDOM = __vite__cjsImport1_reactDom.__esModule ? __vite__cjsImport1_reactDom.default : __vite__cjsImport1_reactDom;
import "/src/index.css";
import App from "/src/App.tsx?t=1642491067632";
import __vite__cjsImport4_react_jsxDevRuntime from "/node_modules/.vite/react_jsx-dev-runtime.js?v=3c407b52";
const _jsxDEV = __vite__cjsImport4_react_jsxDevRuntime["jsxDEV"];
ReactDOM.render(/* @__PURE__ */ _jsxDEV(React.StrictMode, {
children: /* @__PURE__ */ _jsxDEV(App, {}, void 0, false, {
fileName: _jsxFileName,
lineNumber: 9,
columnNumber: 5
}, this)
}, void 0, false, {
fileName: _jsxFileName,
lineNumber: 8,
columnNumber: 3
}, this), document.getElementById("root"));
可以看出,文件顶部的 import 部分和源码不同,这是因为 vite 拦截了浏览器的请求,在响应结果之前对内容做了处理。
我们知道,浏览器在加载文件时只能加载相对路径,无法处理 裸模块
。所以 vite 会把裸模块处理成一个相对路径,如下所示,
import React from 'react' // 这里的 'react' 就是一个裸模块
⬇
import __vite__cjsImport0_react from "/node_modules/.vite/react.js?v=3c407b52";
vite 在响应浏览器的请求之前,会先拦截请求并做一些处理。为此,vite 为服务定义了一些中间件,用于做各种处理,其中有一条是 transformMiddleware(server)
。这个函数用于转换源码,其中就包括了裸模块的重写。
// main transform middleware
middlewares.use(transformMiddleware(server))
摘取重点内容如下,
// /vite/src/node/server/middlewares/transform.ts
...
// 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, {
etag: result.etag,
// allow browser to cache npm deps!
// 如果是依赖,强缓存
cacheControl: isDep ? 'max-age=31536000,immutable' : 'no-cache',
headers: server.config.server.headers,
map: result.map
})
}
从上述代码可以看出,源码是通过 transformRequest
函数来进行转换的,我们继续到这个函数,发现是 doTransform
这个函数做了源码的加载和转换工作。
async function doTransform(
url: string,
server: ViteDevServer,
options: TransformOptions
) {
url = removeTimestampQuery(url)
const { config, pluginContainer, moduleGraph, watcher } = server
const { root, logger } = config
const prettyUrl = isDebug ? prettifyUrl(url, root) : ''
const ssr = !!options.ssr
// 获取当前 url 对应的 module
const module = await server.moduleGraph.getModuleByUrl(url, ssr)
// 如果 module 内容已经转换过,直接返回缓存内容 transformResult
const cached =
module && (ssr ? module.ssrTransformResult : module.transformResult)
if (cached) {
isDebug && debugCache(`[memory] ${prettyUrl}`)
return cached
}
...
// 准备一个 mod,将转换完成后的模块保存到 moduleGraph 中
const mod = await moduleGraph.ensureEntryFromUrl(url, ssr)
// 确保文件被监听
ensureWatchedFile(watcher, mod.file, root)
// transform
const transformStart = isDebug ? performance.now() : 0
// 转换源码
const transformResult = **await pluginContainer.transform**(code, id, {
inMap: map,
ssr
})
...
if (ssr) {
...
} else {
// 将转换结果保存到 mod 中并返回
return (mod.transformResult = {
code,
map,
etag: getEtag(code, { weak: true })
} as TransformResult)
}
pluginContaner.transform
函数内部遍历调用 vite plugin 对源码进行转换,如下,
async transform(code, id, options) {
...
const ctx = new TransformContext(id, code, inMap as SourceMap)
...
for (const plugin of plugins) {
...
try {
result = await plugin.transform.call(ctx as any, code, id, { ssr })
} catch (e) {
ctx.error(e)
}
...
}
return {
code,
map: ctx._getCombinedSourcemap()
}
}
对于一个基础的 react 应用, plugins 列表如下。
除了 react 额外引入的 @vitejs/plugin-react
的插件,其它插件都是 vite 内置的,开箱即用。
上述提到的与 import 相关内容的修改就在插件 vite:import-analysis
中。
PS:vite 插件执行顺序可以参考以下资料,
热替换 HMR (Hot Module Replacement)
热替换我们常称为热更新。热更新初始化如下,
// /vite/src/node/server/index.ts
import { createWebSocketServer } from './ws'
...
// 1. 创建一个 websocket 服务器
const ws = createWebSocketServer(httpServer, config, httpsOptions)
...
// 2. 使用 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
...
// 3. 文件变动回调钩子函数
watcher.on('change', async (file) => {
file = normalizePath(file)
console.log('\nfile on change: ', 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)
})
}
}
})
如上所示,当检测到文件发生变化时,会触发 await handleHMRUpdate(file, server)
函数来执行热更新流程。
export async function handleHMRUpdate(
file: string,
server: ViteDevServer
): Promise<any> {
const { ws, config, moduleGraph } = server
...
const mods = moduleGraph.getModulesByFile(file)
// check if any plugin wants to perform custom HMR handling
const timestamp = Date.now()
const hmrContext: HmrContext = {
file,
timestamp,
modules: mods ? [...mods] : [],
read: () => readModifiedFile(file),
server
}
...
updateModules(shortFile, hmrContext.modules, timestamp, server)
}
function updateModules(
file: string,
modules: ModuleNode[],
timestamp: number,
{ config, ws }: ViteDevServer
) {
const updates: Update[] = []
const invalidatedModules = new Set<ModuleNode>()
let needFullReload = false
for (const mod of modules) {
// 清除之前的转换结果(缓存)
invalidate(mod, timestamp, invalidatedModules)
if (needFullReload) {
continue
}
const boundaries = new Set<{
boundary: ModuleNode
acceptedVia: ModuleNode
}>()
const hasDeadEnd = propagateUpdate(mod, boundaries)
if (hasDeadEnd) {
needFullReload = true
continue
}
// 生成更新信息,之后发送给客户端
updates.push(
...[...boundaries].map(({ boundary, acceptedVia }) => ({
type: `${boundary.type}-update` as Update['type'],
timestamp,
path: boundary.url,
acceptedPath: acceptedVia.url
}))
)
}
if (needFullReload) {
...
// 页面重载
ws.send({
type: 'full-reload'
})
} else {
...
// 发送更新消息
ws.send({
type: 'update',
updates
})
}
}
从上述代码可以看出,vite 会收集需要更新的信息存放到 updates
信息中通过 ws.send()
函数发送给客户端,也就是我们的浏览器端。浏览器端对应代码如下,
// 监听 message
socket.addEventListener('message', async ({ data }) => {
handleMessage(JSON.parse(data))
})
async function handleMessage(payload: HMRPayload) {
switch (payload.type) {
...
case 'update':
notifyListeners('vite:beforeUpdate', payload)
...
payload.updates.forEach((update) => {
if (update.type === 'js-update') {
**queueUpdate(fetchUpdate(update))**
} else {
// css-update
// this is only sent when a css file referenced with <link> is updated
let { path, timestamp } = update
path = path.replace(/\?.*/, '')
// can't use querySelector with `[href*=]` here since the link may be
// using relative paths so we need to use link.href to grab the full
// URL for the include check.
const el = Array.from(
document.querySelectorAll<HTMLLinkElement>('link')
).find((e) => e.href.includes(path))
if (el) {
const newPath = `${base}${path.slice(1)}${
path.includes('?') ? '&' : '?'
}t=${timestamp}`
el.href = new URL(newPath, el.href).href
}
console.log(`[vite] css hot updated: ${path}`)
}
})
break
...
}
}
对于一个典型的 js 文件类型更新(包括了我们写的 .tsx,.ts 等文件,即经过源码转换后为 js 类型的文件),通过 queueUpdate(fetchUpdate(update))
来更新。fetchUpdate 函数用于向服务端重新发起请求,请求热更新后的文件。
async function fetchUpdate({ path, acceptedPath, timestamp }: Update) {
// 获取缓存的热更新模块
const mod = hotModulesMap.get(path)
if (!mod) {
return
}
// 创建一个新的模块
const moduleMap = new Map()
const isSelfUpdate = path === acceptedPath
// 使用 Set 类型确保模块不被重复导入
const modulesToUpdate = new Set<string>()
if (isSelfUpdate) {
// self update - only update self
modulesToUpdate.add(path)
} else {
// dep update
for (const { deps } of mod.callbacks) {
deps.forEach((dep) => {
if (acceptedPath === dep) {
modulesToUpdate.add(dep)
}
})
}
}
...
// 请求更新的文件
await Promise.all(
Array.from(modulesToUpdate).map(async (dep) => {
const disposer = disposeMap.get(dep)
if (disposer) await disposer(dataMap.get(dep))
const [path, query] = dep.split(`?`)
try {
**// 通过 import() 来请求要更新的文件**
const newMod = **await import**(
/* @vite-ignore */
base +
path.slice(1) +
`?import&t=${timestamp}${query ? `&${query}` : ''}`
)
// 将结果填充到 moduleMap 中
moduleMap.set(dep, newMod)
} catch (e) {
warnFailedFetch(e, dep)
}
})
)
...
}
该函数的重点在于使用 await import(...)
来请求要更新的文件。至此,热更新流程结束。
PS:源码中还有一些与 [import.meta.hot](<http://import.meta.hot>)
相关的部分,详见链接,
源码分析 - 构建
Vite 使用 rollup 来打包构建。
vite针对的是现代浏览器,现代浏览器已经广泛支持了ESM,我们能否像上古年代那样直接不经过打包,直接将代码部署到服务器?事实上,还是存在一些问题的:
首先,每个模块都会使用一个请求,可以想象一个应用会发出多少请求,这样即便使用HTTP2也会效率低下。
其次,现代应用为了性能,需要做tree-shaking、懒加载和代码分割等优化,以减小应用的体积和更好地做浏览器缓存。
那么vite是否可以直接使用esbuild进行打包,保持开发和生产的统一?从长期看来是可以这样的,但是就目前而言,esbuild对css和代码分割的支持不够友好,更多针对应用的构建能力还在持续开发中。因此,vite选择了同样采用ESM格式的rollup来进行打包,并且vite的插件采用了rollup的rollup-superset接口,这使大部分的rollup的插件都能在vite上使用。
本来想写一下关于这部分的内容,后来发现 vite 团队核心成员已经写了一篇关于 vite 构建的文章,大家直接可以阅读这篇文章,写得非常详尽。同时官方出手,对于 vite 构建的理解肯定也是最好的。
扩展阅读 - The Vite Ecosystem
一篇关于 Vite 生态的文章,由 Vite核心团队成员 patak 编写。
写在最后
首先非常感谢 SugarTurbos Team 团队的这篇关于 vite 分析的文章,文中在源码分析的很多地方都有参考到。
再者,非常感谢你能阅读到这里,如果觉得文章内容还不错的话,可以点一个小小的赞。
最后,作者水平有限,如果有错误,烦请及时指出!