背景
Vite是一个构建工具,旨在为现代web项目提供更快、更精简的开发体验。
vite主要分两个模块:
- 通过native ES module实现的本地开发服务,可以提供极快的热更新服务。
- 通过rollup对生产环境打包,可以极大地优化打包生产环境的代码。
vite 和 webpack 开发环境最大的区别就是vite 在开发环境抛弃了打包这一个理念,直接在开发环境使用Javascript module,减少打包带来的时间损耗,极大地方便了本地开发。
对于vite的学习,我主要总结了以下四个模块进行总结。
- 模块路径解析
- 不同格式文件处理
- 热更新
- 预打包
模块路径解析
对于一个native es module服务系统而言,不同模块的路径解析非常重要,这里面有以下几个问题:
- node_modules等特殊路径内容如何处理
- 相对路径不便于记录唯一文件路径
vite的解决方式:
- 将裸模块(node_modules)做转换 "vue.js" --> "/@modules/vue.js"
- 将相对路径转为绝对路径,便于vite统一文件路径识别。 import '../../a.js' --> '/src/a.js'
不同格式文件处理
通过不同格式的处理,我们可以理解类似于webpack loader对于不同文件是如何处理的, 了解vite工作机制。
- vue
- css
- json
- html
对于不同格式的文件,vite统一都处理成javascript格式,在返回的response 中添加
Content-Type: application/javascript; charset=utf-8
vue:
vue 的组件是一个单文件组件的机制。一个vue组件的定义基本分三个部分:
<template></template>
<script></script>
<style></style>
编译器会将一个vue组件的三部分分别处理。在vite中,请求一个组件的资源:
Helloworld.vue script 逻辑部分编译:
// 此文件可以理解为一个组件的script逻辑部分
import string from '/src/string.js'
const __script = {
name: 'HelloWorld',
props: {
msg: String
},
data() {
return {
age: 123
}
}
}
// 这里引入组件的template部分
import "/src/components/HelloWorld.vue?type=style&index=0"
// 这里引入组件的style部分
import {render as __render} from "/src/components/HelloWorld.vue?type=template"
__script.render = __render
__script.__hmrId = "/src/components/HelloWorld.vue"
__script.__file = "/Users/lizhuang/gitcode/vite-test/src/components/HelloWorld.vue"
export default __script
HelloWorld.vue?type=style 样式部分编译:
import { updateStyle } from "/vite/client"
const css = "\nh1 {\n background: red;\n}\n"
updateStyle("62a9ebed-0", css)
export default css
HelloWorld.vue?type=template 结构部分编译:
import {
toDisplayString as _toDisplayString,
createVNode as _createVNode,
createTextVNode as _createTextVNode,
Fragment as _Fragment,
openBlock as _openBlock,
createBlock as _createBlock
} from "/@modules/vue.js"
const _hoisted_1 = /*#__PURE__*/
_createVNode("p", null, "string1", -1 /* HOISTED */
)
const _hoisted_2 = /*#__PURE__*/
_createVNode("p", null, [/*#__PURE__*/
_createTextVNode("Edit "), /*#__PURE__*/
_createVNode("code", null, "components/HelloWorld.vue"), /*#__PURE__*/
_createTextVNode(" to test hot module replacement.")], -1 /* HOISTED */
)
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(),
_createBlock(_Fragment, null, [_createVNode("h1", null, _toDisplayString($props.msg), 1 /* TEXT */
), _createVNode("button", {
onClick: _cache[1] || (_cache[1] = $event=>($data.count++))
}, "count is: " + _toDisplayString($data.count), 1 /* TEXT */
), _hoisted_1, _createVNode("p", null, _toDisplayString($data.string), 1 /* TEXT */
), _hoisted_2], 64 /* STABLE_FRAGMENT */
))
}
JSON 文件格式处理
通过 rollup-pluginutils 的dataToEsm方法
{
custom: 'data',
to: ['treeshake']
}
转变为:
export const custom = 'data';
export const to = ['treeshake'];
export default { custom, to };
CSS 文件格式处理
其实在vue的样式部分已经有所涉及。
import { updateStyle } from "/vite/client"
const css = "\nh1 {\n background: red;\n}\n"
updateStyle("62a9ebed-0", css)
export default css
其中 updateStyle 是更新样式的关键函数,我们进行分析:
/**
* content: css文件内容
*/
function updateStyle(id, content) {
...
if (!style) {
style = new CSSStyleSheet()
style.replaceSync(content)
document.adoptedStyleSheets = [...document.adoptedStyleSheets, style]
} else {
style.replaceSync(content)
}
...
}
vite利用** CSSStyleSheet **代表一个样式表,利用javascript的接口编辑或者添加相关的样式。
当然还有一个特殊的情况就是css文件中有@import 等操作, 这种特殊的情况,vite直接使用style标签进行样式插入。
cosnt style = document.createElement('style')
style.setAttribute('type', 'text/css')
style.innerHTML = content
document.head.appendChild(style)
热更新
vite1 代码较少,这可以让我们低成本的学习一个开发环境热更新的具体细节
其中第四部处理不同文件的方式,列在了下方:
async function handleMessage(payload: HMRPayload) {
const { path, changeSrcPath, timestamp } = payload as UpdatePayload
switch (payload.type) {
case 'connected':
console.log(`[vite] connected.`)
break
case 'vue-reload':
queueUpdate(
import(`${path}?t=${timestamp}`)
.catch((err) => warnFailedFetch(err, path))
.then((m) => () => {
__VUE_HMR_RUNTIME__.reload(path, m.default)
console.log(`[vite] ${path} reloaded.`)
})
)
break
case 'vue-rerender':
const templatePath = `${path}?type=template`
import(`${templatePath}&t=${timestamp}`).then((m) => {
__VUE_HMR_RUNTIME__.rerender(path, m.render)
console.log(`[vite] ${path} template updated.`)
})
break
case 'style-update':
// check if this is referenced in html via <link>
const el = document.querySelector(`link[href*='${path}']`)
if (el) {
el.setAttribute(
'href',
`${path}${path.includes('?') ? '&' : '?'}t=${timestamp}`
)
break
}
// imported CSS
const importQuery = path.includes('?') ? '&import' : '?import'
await import(`${path}${importQuery}&t=${timestamp}`)
break
.... 还有很多,就不一一列举了
}
}
预打包
vite的预打包优化手段其实和小程序页面预加载技术,以及网页的prefetch,preload等的原理是基本一致的,当我们尽量少的打包过后,那么预打包那些没有处理的文件就是优化的手段之一。
vite 会去分析package.json 当中的依赖项,会将依赖进行打包并缓存:
其中lodash较为特殊,因为其文件众多,如果不进行预打包的话,开发项目将会请求很多相关文件,造成网页reload时性能衰减。所以vite预打包的另外一个重要的功能就是通过rollup或者esbuild(vite 不同版本实现不同),将过于零散的文件打包,减少网络请求,提高页面reload性能。