Vite
介绍
Vite
是Vue
作者尤雨溪又一个备受关注的开源项目,它是一个前端构建工具,可类比Webpack
。它主要解决了传统bundle-base
服务器在开发时遇到的两个问题:
- 服务器启动速度慢,而且其启动时间是跟应用规模成正比的。
- 在更新时,即便使用了
HMR
,但是其热更新的时间仍是会随着应用规模的增长而直线下降。
它解决的是开发的时候的效率问题,对于生产环境则是交给了Rollup
。除此之外,它还有以下优点:
- 对
ts
、jsx
、css
等开箱即用,无需配置。 - 对于库开发者也是可以通过简单的配置即可打包输出多种格式的包。
- 开发和生产共享了
rollup
的插件接口,大部门的rollup
插件可以在vite
上使用。 - 类型化配置,配置文件可以使用
ts
,具有配置类型提示。
Get Started
创建项目模板
Vite
提供了一个快捷创建各类型项目模板的包@vitejs/create-app
的包,对外暴露了可执行的bin
文件:
yarn create @vitejs/app my-vue-app --template vue
顺便提一下, yarn create
是yarn
提供的一个聚合两个命令的语法糖,上面命令等价:
yarn global add @vitejs/create-app
@vite/create-app my-vue-app --template vue
这里是创建vue3
模板。除此之外,vite
还支持
vanilla
vue-ts
react
react-ts
preact
preact-ts
lit-element
lit-element-ts
启动vite
开发环境,启动服务器
vite
生产环境,打包应用或包
vite build
这里使用的都是vite的核心包 vite
,所有的优化都集中在这个包中,另外vite
还提供了vite.config.ts
的配置文件,允许针对整个构建过程做出一些配置。
上面的vite
和vite build
是做了什么呢?
vite
命令会执行vite.js
这个脚本,而在这个脚本中会执行start
函数,最终引入了node
目录下的cli
脚本,接着看看cli
脚本中干了什么?
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3RbFNBxs-1623254893999)(https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a92b9ef738b048c4911fc443f9df0380~tplv-k3u1fbpfcp-watermark.image)]
cli
脚本中主要监听了vite
和vite build
两个命令,这便是vite
开发和生产的入口了。
Vite
原理
前文提到了vite
主要解决了传统开发构建工具启动和更新慢的问题,那么vite
是怎样解决这两个问题呢?
vite
主要通过 esbuild
预构建依赖和让浏览器接管部分打包程序两种手段解决了这两个问题,下面细讲这两大手段。
esbuild
预构建依赖
vite
将代码分为源码和依赖两部分并分别处理,所谓依赖便是应用使用的第三方包,一般存在于node_modules
目录中,一个较大项目的依赖及其依赖的依赖,加起来可能达到上千个包,这些代码可能远比我们源码代码量要大,这些依赖通常是不会改变的(除非你要进行本地依赖调试),所以无论是webpack
或者vite
在启动时都会编译后将其缓存下来。区别的是,vite
会使用esbuild
进行依赖编译和转换(commonjs包转为esm),而webpack
则是使用acorn
或者tsc
进行编译,而esbuild
是使用Go
语言写的,其速度比使用js
编写的acorn
速度要快得多。
esbuild
官方做了一个测试,打包生产环境的three.js
包十次,上图是各大工具的打包时长。esbuild
在打包速度上比现在前端打包工具快10-100倍。
而且vite
在打包之后,还会对这些依赖包的请求设置cache-control: max-age=31536000,immutable;
,即设置了强缓存,之后针对依赖的请求将不会到达服务器。如果要进行依赖调试,可以在启动服务器时使用 --force
标志,它会重新打包依赖。下面看看源码中是怎样实现这些的?
node/cli.ts
// dev
cli
.command('[root]') // default command
.alias('serve')
.option('--host [host]', `[string] specify hostname`)
.option('--port <port>', `[number] specify port`)
.option('--https', `[boolean] use TLS + HTTP/2`)
.option('--open [path]', `[boolean | string] open browser on startup`)
.option('--cors', `[boolean] enable CORS`)
.option('--strictPort', `[boolean] exit if specified port is already in use`)
.option('-m, --mode <mode>', `[string] set env mode`)
.option(
'--force',
`[boolean] force the optimizer to ignore the cache and re-bundle`
)
.action(async (root: string, options: ServerOptions & GlobalCLIOptions) => {
// 核心代码在server脚本中
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) as ServerOptions
})
// 监听端口
await server.listen()
} catch (e) {
createLogger(options.logLevel).error(
chalk.red(`error when starting dev server:\n${
e.stack}`)
)
process.exit(1)
}
})
node/server/index.ts
文件中:
import {
DepOptimizationMetadata, optimizeDeps } from '../optimizer'
export async function createServer(
inlineConfig: InlineConfig = {
}
): Promise<ViteDevServer> {
// 省略无关代码
// 优化
const runOptimize = async () => {
// cacheDir为缓存目录,一般为node_modules/.vite 目录
if (config.cacheDir) {
server._isRunningOptimizer = true
try {
// 依赖预构建
server._optimizeDepsMetadata = await optimizeDeps(config)
} finally {
server._isRunningOptimizer = false
}
server._registerMissingImport = createMissingImporterRegisterFn(server)
}
}
if (!middlewareMode && httpServer) {
// overwrite listen to run optimizer before server start
const listen = httpServer.listen.bind(httpServer)
httpServer.listen = (async (port: number, ...args: any[]) => {
try {
// 执行所有插件的 buildStart方法
await container.buildStart({
})
// 依赖预构建优化
await runOptimize()
} catch (e) {
httpServer.emit('error', e)
return
}
return listen(port, ...args)
}) as any
}
}
node/optimizer/index.ts
:
import {
build, BuildOptions as EsbuildBuildOptions } from 'esbuild'
export async function optimizeDeps(
config: ResolvedConfig,
force = config.server.force,
asCommand = false,
newDeps?: Record<string, string> // missing imports encountered after server has started
): Promise<DepOptimizationMetadata | null> {
// 缓存目录存在一个保存预构建信息的配置
const dataPath = path.join(cacheDir, '_metadata.json')
const mainHash = getDepHash(root, config)
const data: DepOptimizationMetadata = {
hash: mainHash,
browserHash: mainHash,
optimized: {
}
}
// 强制重新预构建
if (!force) {
let prevData
try {
prevData = JSON.parse(fs.readFileSync(dataPath, 'utf-8'))
} catch (e