what vite
新一代的前端构建工具, 能提升前端开发体验,主要包括两部分
- 一个开发服务器,基于ES原生 Module, 提供了丰富的内置功能,比如对非js模块的转换处理,热更新等;
- 一套构建指令,基于Rollup打包代码;
使用层面
在开发环境采用不打包, 基于ES Module提供源码,在浏览器请求源码时进行转换提供代码,其中涉及的转换有(jsx, css, vue文件转为js可以解析的文件),还包括将一些大的依赖采用esbuild进行预构建;
生产环境基于Rollup打包代码, 因为生产环境需要有最佳的性能,多次加载未打包ESM, 效率底下,所以在生产环境还是将代码进行tree-shaking, chunk分割等优化打包处理
how vite bundless
这里我们主要结合vite源码来看一下,vite在开发环境,采用不打包的内部实现
开发服务器
- 基于node:http模块创建本地服务器, 然后启动本地服务器
配置处理
在resoveConfig中, 对配置进行了各种处理,包括vite.cofig.json中的配置,插件配置,各个打包环境的配置,还有做了一系列的格式化检查,对配置的兼容性检查,对日志统一输出检查,对文件名的有效性检测等;
文件转换
对于css, vue等其他类型的资源文件,vite需要进行资源转换,浏览器才能解析,下面将结合css这个类型资源来简要说一下文件转换, vite中会将css资源文件转成js文件输出;
css
.dir-import {
color: grey;
}
js
import {createHotContext as __vite__createHotContext} from "/@vite/client";
import.meta.hot = __vite__createHotContext("/src/style.css");
import {updateStyle as __vite__updateStyle, removeStyle as __vite__removeStyle} from "/@vite/client"
const __vite__id = "vite-project/src/style.css"
const __vite__css = ":root {\n color: grey; }\n"
__vite__updateStyle(__vite__id, __vite__css)
import.meta.hot.accept()
import.meta.hot.prune( () => __vite__removeStyle(__vite__id))
主要是通过vite:css插件进行转换
在vite源码中,通过middleware拦截到请求后,通过loadAndTransform来加载文件内容,转化等
js
async function loadAndTransform(environment, id, url, options, timestamp, mod, resolved) {
// 第一步:加载源文件内容
const loadResult = await pluginContainer.load(id);
// 第二步: 判断是否有缓存
mod ??= await moduleGraph._ensureEntryFromUrl(url, void 0, resolved);
// 第三步: 通过插件提供的统一出口,转化文件内容
const transformResult = await pluginContainer.transform(code, id, {
inMap: map
});
}
transform中的源码, 在vite内置plugin中,对css的code进行转换
js
async transform(css, id) {
if (
!isCSSRequest(id) ||
commonjsProxyRE.test(id) ||
SPECIAL_QUERY_RE.test(id)
) {
return
}
css = stripBomTag(css)
// cache css compile result to map
// and then use the cache replace inline-style-flag
// when `generateBundle` in vite:build-html plugin and devHtmlHook
const inlineCSS = inlineCSSRE.test(id)
const isHTMLProxy = htmlProxyRE.test(id)
if (inlineCSS && isHTMLProxy) {
if (styleAttrRE.test(id)) {
css = css.replace(/"/g, '"')
}
const index = htmlProxyIndexRE.exec(id)?.[1]
if (index == null) {
throw new Error(`HTML proxy index in "${id}" not found`)
}
addToHTMLProxyTransformResult(
`${getHash(cleanUrl(id))}_${Number.parseInt(index)}`,
css,
)
return `export default ''`
}
const inlined = inlineRE.test(id)
const modules = cssModulesCache.get(config)!.get(id)
const modulesCode =
modules &&
!inlined &&
dataToEsm(modules, { namedExports: true, preferConst: true })
if (config.command === 'serve') {
const getContentWithSourcemap = async (content: string) => {
if (config.css?.devSourcemap) {
const sourcemap = this.getCombinedSourcemap()
if (sourcemap.mappings) {
await injectSourcesContent(sourcemap, cleanUrl(id), config.logger)
}
return getCodeWithSourcemap('css', content, sourcemap)
}
return content
}
if (isDirectCSSRequest(id)) {
return null
}
// server only
if (this.environment.config.consumer !== 'client') {
return modulesCode || `export default ${JSON.stringify(css)}`
}
if (inlined) {
return `export default ${JSON.stringify(css)}`
}
const cssContent = await getContentWithSourcemap(css)
const code = [
`import { updateStyle as __vite__updateStyle, removeStyle as __vite__removeStyle } from ${JSON.stringify(
path.posix.join(config.base, CLIENT_PUBLIC_PATH),
)}`,
`const __vite__id = ${JSON.stringify(id)}`,
`const __vite__css = ${JSON.stringify(cssContent)}`,
`__vite__updateStyle(__vite__id, __vite__css)`,
// css modules exports change on edit so it can't self accept
`${modulesCode || 'import.meta.hot.accept()'}`,
`import.meta.hot.prune(() => __vite__removeStyle(__vite__id))`,
].join('\n')
return { code, map: { mappings: '' } }
}
// record css
if (!inlined) {
styles.set(id, css)
}
let code: string
if (modulesCode) {
code = modulesCode
} else if (inlined) {
let content = css
if (config.build.cssMinify) {
content = await minifyCSS(content, config, true)
}
code = `export default ${JSON.stringify(content)}`
} else {
// empty module when it's not a CSS module nor `?inline`
code = ''
}
return {
code,
map: { mappings: '' },
// avoid the css module from being tree-shaken so that we can retrieve
// it in renderChunk()
moduleSideEffects: modulesCode || inlined ? false : 'no-treeshake',
}
},
热更新
在文件转换阶段,css转换code的transform中,可以看到,有注入一些热更新的代码, 在服务端代码更新时,更新新的代码,做到动态替换;
js
import {createHotContext as __vite__createHotContext} from "/@vite/client";
import.meta.hot = __vite__createHotContext("/src/style.css");
__vite__updateStyle(__vite__id, __vite__css)
import.meta.hot.accept()
import.meta.hot.prune( () => __vite__removeStyle(__vite__id))
服务端,利用websockot建立双端通信长连接,并且通过chokidar来监听文件的修改,通知到客户端,在不刷新页面的同时,做到内容的更新
js
// 服务启动的时候,同时也创建了websocketServer
const ws = createWebSocketServer(httpServer, config, httpsOptions);
在onHMRUpdate中主要做三层维度的更新
依赖预构建
1、CommonJS和UMD兼容性,vite将CommonJS和UMD依赖项转为ESM;
2、性能:有些模块有数百个子模块,比如lodash-es,预构建可以将内部多个模块转换成单个模块,提升页面后续性能;
next
vite存在的问题
vite开发不打包,生产基于rollup打包,开发生产构建模式的不同,很难保证排查两边问题时,代码的一致性,为此vite团队,提出了RollDown, Rolldown使用Rust编写,目前还是开发阶段。