vite的基本原理
vite 依靠浏览器对es6module的支持,当浏览器解析JavaScript文件的时候,遇到import语句的时候,会构造http请求向服务器文件,例如
import fs from 'fs'
import hello from './uitls.js'
import home from './home.vue'
import 'hello.css'
对于上面的三句import语句,浏览器会构造3个http请求,向服务器请求文件,因此vite的总体思路是,在本地启动一个server,通过解析浏览器的请求url,来解析不同的文件返回给浏览器。
我们现在来写一些基本代码来加深对vite的理解。
新建Vue项目
添加vue项目的的模版,具体情况如下所示。
具体代码可以点击链接复制即可
打开index.html我们可以看到这样子一行代码
<script type="module" src="/src/main.js"></script>type属性启用es6 module的支持,src属性则用来请求项目的入口文件,然后我们在App.vue文件中我们看到了import HelloWorld from './components/HelloWorld.vue'这又请求HelloWorld.vue文件,因此我们必须在本地启动一个server拦截请求镜像处理。
使用koa处理浏览器的请求
对于本地server我们选择使用koa,通过npm安装koa之后,我们在项目的根目录下载新建一个index.js文件来写一些代码让koa跑起来,处理来自浏览器的请求。
import koa from 'koa'
const app = new koa()
app.use((ctx, next) => {
console.info(ctx.path)
})
app.listen(3000)
运行koa之后,如果我们打开浏览器输入localhost:3000,我们会在控制台看到二个输出。
/
/favicon.icon
对于/favicon.icon我们不管,对于/的请求,我们把它重定向为index.html,然后使用koa-static读取本地文件返回给浏览器。
import koa from 'koa'
import staticSever from 'koa-static'
const root = process.cwd() // 项目运行的文件路径
const app = new koa()
app.use(async (ctx, next) => {
if (ctx.path === '/') {
ctx.redirect('/index.html')
return
}
await next()
})
app.use(staticSever())
app.listen(3000)
在浏览器输入localhost:3000,得到以下结果。
可以看到我们已经成功的把'/'重定向为'/index.html',并且也成功的读取index.html,但是控制台却报了如下错误。
这是因为main.js文件中import {createApp} from 'vue'的导入路径不合法,导入路径应该为'/','./','..'开头的。但是在开发的过程中我们导入node_modules依赖的时候为了跟本地的文件区别都是会与字母或者@开头的,因此我们必须解决这个问题。
解决方案也不难,我们可以使用koa的ctx.path看是否为.js结尾的js文件请求,然后使@babel/parser这个包来编译js文件,然后是查看是否有导入/node_modules的情况,如果有的话把路径前面添加上/@modules/即可。根据以上思路我们很快就能写出以下代码。
app.use(async (ctx, next) => {
if (ctx.path === '/') {
ctx.redirect('/index.html')
return
}
await next()
}).use(moduleRewrite)
async function moduleRewrite(ctx, next) {
if (!ctx.path.endsWith('.js')) {
return next()
}
const path = root + ctx.path
let content
try {
content = (await fs.readFile(path, "utf-8")).toString()
} catch (error) {
ctx.status = 404
}
ctx.type = 'js'
ctx.body = rewrite(content)
}
function rewrite(source) {
const ast = parse(source, {
plugins: [
'bigInt',
'optionalChaining',
'nullishCoalescingOperator'
],
sourceType: 'module'
}).program.body
let s = new MagicString(source)
ast.forEach((node) => {
if (node.type === 'ImportDeclaration') {
if (/^[^\.\/]/.test(node.source.value)) {
s.overwrite(
node.source.start,
node.source.end,
`"/@modules/${node.source.value}"`
)
}
}
})
return s.toString()
}
可以看到我们成功解决了第一条import路径的问题之后,后面二条import语句也发出去请求了。但是此时浏览器还是浏览器仍然没有vue文件,因此我们被在本地node_modules正确读取vue文件返回给浏览器。
对于这种node_modules依赖的请求,因为前面同一添加了'/@modules/'因此很容易同其他请求区别开来,所以我们重点考虑如果拼接出库文件在本地正确的路径,我们想想库文件一定在(项目地址 + node_modules + 库文件名称)的目录下 项目地址我们可以使用process.cwd()来得到,而node_modules是固定不变的目录名称,现在我们不清楚的就是库名称了,其实我们在用babel/parser解析的时候我们已经知道了库的名称了,因此我们只好缓存一下即可。
我们拼接出正确的路径之后,我们还要考虑可能出现2种情况。
- 本地依赖还没有安装。
- 本地依赖安装但是却没有打包成es6 module形式。
对于第一种情况,我们使用resolve-from这个包来解决,这个包在给定的路径库文件不存在的情况下会报错,第二种情况解决方法是我们在确定库文件存在的情况下,加载package.json文件如果package.json有用module或者main字段指定入口文件的话,说明支持es6 modules反正就是不支持。以此我们可以写成以下代码。
const moduleIdCache = new Map()
app.use(async (ctx, next) => {
if (ctx.path === '/') {
ctx.redirect('/index.html')
return
}
await next()
}).use(moduleRewrite)
.use(nodeModuleResolve)
async function nodeModuleResolve(ctx, next) {
if (!ctx.path.startsWith('/@modules')) {
return next()
}
// vue是特殊情况
if (ctx.path.endsWith('vue')) {
let vuePath
let vueSource
try {
const userVuePkg = resolve(root, 'vue/package.json')
vuePath = path.join(
path.dirname(userVuePkg),
'dist/vue.runtime.esm-browser.js'
)
vueSource = (await fs.readFile(vuePath, 'utf-8')).toString()
} catch (error) {
ctx.status = 404
return
}
ctx.type = 'js'
ctx.body = vueSource
return
}
// 其他node_modules依赖
let modulePath
let source
const id = moduleIdCache.get(ctx.path)
try {
modulePath = resolve(cwd, `${id}/package.json`)
// module resolved, try to locate its "module" entry
const pkg = JSON.parse((await fs.readFile(modulePath, 'utf-8')).toString())
modulePath = path.join(path.dirname(modulePath), pkg.module || pkg.main)
source = (await fs.readFile(modulePath, 'utf-8')).toString()
} catch (error) {
ctx.status = 404
return
}
ctx.type = 'js'
ctx.body = source
}
function rewrite(source) {
// 其他代码如上
ast.forEach((node) => {
if (node.type === 'ImportDeclaration') {
if (/^[^\.\/]/.test(node.source.value)) {
moduleIdCache.set('/@modules' + node.source.value, node.source.value)
// 省略
}
}
处理以.vue结尾的单文件组件
对于.vue结尾的单文件组件,我们使用vue官方提供的@vue/complier-sfc来编译,我们主要使用它的三个函数parse, compileTemplate, compileStyle。
使用parse编译之后会得到一个object,里面包含很多用的字段,我们只关心三个字段,分别是script,template,和style。看名字也知道分别对应组件的js代码,template模版,和style代码。如下图所示。
如果你自己有用parse去编译sfc,会发现我们3个问题需要进一步处理。
- script内包含的代码有可能import node_module依赖,至少它一定会导入vue,此时路径需要重写。
- template代码没有编译成render函数,仍然是原始的代码。
- styles里面css代码,如何下载到浏览器。
我们先来解决1和2,对于1我们刚才已经做过了,重写调用rewrite即可,问题2也简单调用comilpeTempalte即可。因此我们很快就能写出下面的代码。
app.use(async (ctx, next) => {
// 代码省略
}).use(moduleRewrite)
.use(nodeModuleResolve)
.use(sfcComplie)
async function sfcComplie(ctx, next) {
if (!ctx.path.endsWith('.vue')) {
return next()
}
let sourcePath = root + ctx.path
let content
try {
content = (await fs.readFile(sourcePath, 'utf-8')).toString()
} catch (error) {
ctx.status = 404
return
}
const desc = sfcParse(content, {
filename: sourcePath
})
let code = ''
code += rewrite(desc.descriptor.script.content)
code += compileTemplate({
source: desc.descriptor.template.content,
filename: sourcePath
}).code
ctx.type = 'js'
ctx.body = code
}
我们先把main.js中的import "style.css"先删除一下,然后刷新浏览器此时我们得到如下结果。
现在我们能够直接返回给浏览器了吗?实际上还是不行的,这是因为export default defineComponent 中defineComponent返回的对象缺失render函数(render函数是负责创建组件的虚拟dom的,有兴趣的可以找一些vue源码解析文章来看)。
解决办法也容易想到我们把export default defineComponent 改写成为let __script; export default (__script = , 然后再最后添加一句_script.render = reder即可。
修改一下rewrite,使其支持改写export default
function rewrite(source, asSFC = false) {
// 其他代码如上
let s = new MagicString(source)
ast.forEach((node) => {
if (node.type === 'ImportDeclaration') {
if (/^[^\.\/]/.test(node.source.value)) {
s.overwrite(
node.source.start,
node.source.end,
`"/_modules/${node.source.value}"`
)
}
} else if (asSFC && node.type == 'ExportDefaultDeclaration') {
s.overwrite(
node.start,
node.declaration.start,
`let __script; export default (__script = `
)
s.appendRight(node.end, `)`)
}
})
return s.toString()
}
然后把scfComplier代码修改如下。
async function sfcComplier(ctx, next) {
// 其他代码省略
if (desc.descriptor.script) {
code += rewrite(desc.descriptor.script.content, true)
} else {
// 组件没有script代码,导出一个空对象,后续看有没有render添加即可
code += `const __script = {}; export default __script`
}
if (desc.descriptor.template) {
code += compileTemplate({
source: desc.descriptor.template.content,
filename: sourcePath
}).code
code += `_script.render = render`
}
ctx.type = 'js'
ctx.body = rewrite(code) // 去除template编译引入的
}
此时刷新浏览器,可以看到文件都请求到,但是控制台却报错了。
我们打开App.vue编译过的template部分的代码,就能够知道怎么回事了。
我们可以看到源文件中src属性以相对路径开头的都被编译成为了import模式,而import请求的js文件,但是我们返回却是svg文件因此报错了。
明白了怎么回事解决办法也是简单的,我们把svg文件转换为dataURI,然后拼接export default后面返回即可。
app.use(async (ctx, next) => {
// 其他代码如上
}).use(moduleRewrite)
.use(nodeModuleResolve)
.use(sfcComplie)
.use(convertSvgToDataUri)
async function convertSvgToDataUri(ctx, next) {
if (!ctx.path.endsWith('.svg')) {
return next()
}
const path = root + ctx.path
let content
try {
content = (await fs.readFile(path, 'utf-8')).toString()
} catch (error) {
ctx.status = 404
return
}
const dataUrl = svgToTinyDataUri(content)
ctx.type = 'js'
ctx.body = `
let url = "${dataUrl}"
export default url
`
return
}
然后刷新浏览器即可
我们可以看到我们要的文件全部加载了,也没有报错,点击按钮交互也是正常了的,接下来我们的工作是加载css了。
对于css可以创建一个style标签,然后把编译过的css代码添加style标签内textContent即可。
async function sfcComplie(ctx, next) {
// 其他代码同上
let jsCode = ''
if (desc.descriptor.script) {
jsCode += rewrite(desc.descriptor.script.content, true)
} else {
// 组件没有script代码,导出一个空对象,后续看有没有render添加即可
jsCode += `const __script = {}; export default __script`
}
if (desc.descriptor.template) {
jsCode += compileTemplate({
source: desc.descriptor.template.content,
filename: sourcePath
}).code
jsCode += `;__script.render = render`
}
jsCode = rewrite(jsCode) // 去除template编译引入vue依赖路径问题
if (desc.descriptor.styles) {
desc.descriptor.styles.forEach((style, index) => {
let id = hash(sourcePath)
const { code, errors } = compileStyle({
source: style.content,
filename: sourcePath,
id
})
if (errors.length) {
ctx.status = 500
return
}
let str = `
const styleId = 'vue-style-${id}-${index}'
const style = document.createElement('style')
style.id = styleId
document.head.appendChild(style)
style.textContent = ${JSON.stringify(code)}
`
jsCode += str
})
}
ctx.type = 'js'
ctx.body = jsCode
}
刷新浏览器即可看到以下效果
可以看到组件内的css已经成功的加载了。
现在有一个需求,我们想支持css写在独立的文件内,然后通过import './styles.css'导入css。对于这个需求我们可以用上面的方法来实现。
在src文件夹中新建style.css文件,然后添加以下css代码。
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
.card {
padding: 2em;
}
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}
import 'styles.css'同样是js请求,因此我们必须返回js代码。经过以上分析我们很容易写出下面的代码。
app.use(async (ctx, next) => {
// 省略其他代码
await next()
}).use(moduleRewrite)
.use(nodeModuleResolve)
.use(sfcComplie)
.use(convertSvgToDataUri)
.use(handleCss)
async function handleCss(ctx, next) {
if (!ctx.path.endsWith('.css')) {
return next()
}
const path = root + ctx.path
let content
try {
content = (await fs.readFile(path, 'utf-8')).toString()
} catch (error) {
ctx.status = 404
return
}
let str = `
const styleId = 'vue-global-${hash(path)}'
const style = document.createElement('style')
style.id = styleId
document.head.appendChild(style)
style.textContent = ${JSON.stringify(content)}
`
ctx.type = 'js'
ctx.body = str
return
}
写完之后我们刷新浏览器即可看到以下效果。
实现hmr
为了实现hmr,我们应该实现下面三个需求。
- 应该有某种方式,当本地的文件被修改的时候,我们能够知道。
- 知道本地文件被修改后,本地server应该以某种方式,把修改文件主动推送消息给浏览器。
- 浏览器接到通知之后,应该根据文件的信息重新请求文件。
对于需求1我们可以使用chokidar这个类库即可,需求2我们使用websocket技术。 至于需求3我们要好好的分析了,当然最简单的方法是,当我们接到通知的使用window.location.reload()来刷新,然后所有请求会重新发送,自然能够得到最新的文件了,这种方式对于稍微大一点的项目简直是灾难,因此我们必须寻求其他方式更新。
理想的情况下对于vue的sfc,template或者script的改变应该只有影响自己而不会影响其他部分,而script的改变才导致整个sfc的更新,但是sfc的script刷新,应该只有影响到自己,而不影响其他组件。幸运的是vue的HMRRuntime提供了rerender和reload方法,rerender方法可以把template更新的部分,重新渲染到页面,reload方法可以重新渲染组件,而至于style部分我们是自己加载的,因此我们也可以自行解决。
现在我们先来把整体的代码框架写出来。
我们先通过npm安装chokidar和ws。安装完成后我们打开index.js添加以下代码。
import { WebSocketServer } from 'ws'
//其他代码如上
const server = app.listen(3000)
const wss = new WebSocketServer({ server })
const connects = new Set()
wss.on('connection', (conn) => {
connects.add(conn)
conn.send(JSON.stringify({type: 'connected'}))
console.info('socket connect')
conn.on('close', () => {
connects.delete(conn)
})
})
wss.on('error', (e) => {
if (e.code !== 'EADDRINUSE') {
console.error(e)
}
})
为了测试我们websocket是否有建立成功,我们打开index.html文件,添加如下代码。
<-- 其他代码省略-->
<script type="module">
const ws = new WebSocket(`ws://${location.host}`)
ws.addEventListener('message', ({ data }) => {
const { type, path, id, index } = JSON.parse(data)
switch (type) {
case 'connected':
console.info('[vite] connected')
break
}
})
</script>
刷新浏览器后,我们在控制台可以看到输出了[vite] connected可以证明我们浏览器可以正确接受来自server的消息
我们写检测本地文件改变的逻辑。往index.js添加如下代码。
function createFileWatcher(cwd, notify) {
const fileWatch = chokidar.watch(cwd, {
ignored: [/node_modules/]
})
fileWatch.on('change', async (file) => {
const resourcePath = '/' + path.relative(cwd, file)
const send = (payload) => {
console.log(`[hmr] ${JSON.stringify(payload)}`)
notify(payload)
}
if (file.endsWith('.vue')) {
send({
type: 'reload',
path: resourcePath
})
} else {
send({
type: 'full-reload'
})
}
})
}
createFileWatcher(
root,
(payload) => connects.forEach(conn.send(payload))
)
我们使用chokidar.watch观察项目目录除了node_modules下的一切文件的改变,一旦监听的change事件就调用已经建立连接的websocket,往浏览器主动发消息。
我们往index.html添加一些代码来测试一下效果。
<script type="module">
// 其他代码同上
ws.addEventListener('message', ({ data }) => {
const { type, path, id, index } = JSON.parse(data)
switch (type) {
case 'connected':
console.info('[vite] connected')
break
case 'reload':
console.info('sfc fille change path is', path)
break
case 'full-reload':
console.info('wow! has to full-reload')
break
}
})
</script>
重启nodejs,并且刷新浏览器看一下。在本地分别在App.vue和main.js的末尾添加一些换行符,结果在控制台输入如下。
至此我们的框架已经搭建起来了,是时候考虑一下vue sfc如何更新了。
我们首先要解决的第一个问题是,sfc由三部分组成(script,template, style),我们要知道具体是哪一部分被改变。
对于这个问题我们解决方案是,编译sfc然后分别检查script,template,style是否相比于旧文件编译出来结果不同。前面处理.vue为结尾的js请求,我们也是编译vue文件,因此我们把这部分的逻辑抽取出来。
const cache = new Map()
async function parseSFC(
cwd,
filename,
saveCache = false,
) {
const content = await fs.readFile(filename, 'utf-8')
const { descriptor, errors } = parse(content, {
filename
})
if (errors) {
throw Error('compile sfc error')
}
const prev = cache.get(filename)
if (saveCache) {
cache.set(filename, descriptor)
}
return [descriptor, prev]
}
同时我们要修改函数sfcComplie的逻辑,修改如下。
async function sfcComplie(ctx, next) {
// 其他代码省略
const [descriptor] = await parseSFC(root, ctx.path, true)
if (!descriptor) {
ctx.status = 404
return
}
let jsCode = ''
if (descriptor.script) {
jsCode += rewrite(descriptor.script.content, true)
} else {
// 组件没有script代码,导出一个空对象,后续看有没有render添加即可
jsCode += `const __script = {}; export default __script`
}
}
注意此时要第调用parseSFC的时候三个参数设置为true,来缓存编译结果,以便后续hmr比较是否发生更新。
检查文件修改的逻辑如下。
async function createFileWatcher(cwd, notify) {
const fileWatch = chokidar.watch(cwd, {
ignored: [/node_modules/]
})
fileWatch.on('change', async (file) => {
const resourcePath = '/' + path.relative(cwd, file)
const send = (payload) => {
console.log(`[hmr] ${JSON.stringify(payload)}`)
notify(JSON.stringify(payload))
}
if (file.endsWith('.vue')) {
const [descriptor, prevDescriptor] = await parseSFC(cwd, resourcePath)
if (!descriptor || !prevDescriptor) {
return
}
if (!isEqual(descriptor.script, prevDescriptor.script)) {
console.info('[hmr] vue component script was chaned')
send({
type: 'reload',
path: resourcePath
})
return
}
if (!isEqual(descriptor.template, prevDescriptor.template)) {
console.info('[hmr] vue component template was chaned')
send({
type: 'rerender',
path: resourcePath
})
return
}
} else {
send({
type: 'full-reload'
})
}
})
}
function isEqual(a, b) {
if (!a || !b) return false
if (a.content !== b.content) return false
const keysA = Object.keys(a.attrs)
const keysB = Object.keys(b.attrs)
if (keysA.length !== keysB.length) {
return false
}
return keysA.every((key) => a.attrs[key] === b.attrs[key])
}
此时你可以在App.vue改写一些代码,看是控制台是否打印出定义的log。
现在我们要对complie重构,使其支持分别编译script,template和style。
async function sfcComplie(ctx, next) {
if (!ctx.path.endsWith('.vue')) {
return next()
}
const parsed = url.parse(ctx.url, true)
const pathname = parsed.pathname
const query = parsed.query
const filename = path.join(root, pathname.slice(1))
const [descriptor] = await parseSFC(filename, true)
if (!descriptor) {
ctx.status = 404
return
}
// 首次请求
if (!query.type) {
return compileSFCMain(ctx, descriptor, pathname, query.t)
}
if (query.type === 'template') {
return compileSFCTemplate(
ctx,
descriptor.template,
filename,
pathname
)
}
if (query.type === 'style') {
return compileSFCStyle(
ctx,
descriptor.styles[Number(query.index)],
query.index,
filename,
pathname
)
}
}
compileSFCMain函数实现如下。
function compileSFCMain(
ctx,
descriptor,
pathname,
timestamp
) {
timestamp = timestamp ? `&t=${timestamp}` : ``
let code = ''
if (descriptor.script) { // let __script; export default (__script =
code += rewrite(
descriptor.script.content,
true /* rewrite default export to `script` */
)
} else {
code += `const __script = {}; export default __script`
}
if (descriptor.styles) {
descriptor.styles.forEach((_, i) => {
code += `\nimport ${JSON.stringify(
pathname + `?type=style&index=${i}${timestamp}`
)}`
})
}
if (descriptor.template) {
code += `\nimport { render as __render } from ${JSON.stringify(
pathname + `?type=template${timestamp}`
)}`
code += `\n__script.render = __render`
}
code += `\n__script.__hmrId = ${JSON.stringify(pathname)}`
sendJS(ctx, code)
}
complieSFCstyle函数实现如下。
function compileSFCStyle(
ctx,
style,
index,
filename,
pathname
) {
const id = hash(pathname)
const { code, errors } = compileStyle({
source: style.content,
filename,
id: ''
})
if (errors.length) {
ctx.status = 500
return
}
sendJS(
ctx,
`
const id = "vue-style-${id}-${index}"
let style = document.getElementById(id)
if (!style) {
style = document.createElement('style')
style.id = id
document.head.appendChild(style)
}
style.textContent = ${JSON.stringify(code)}
`.trim()
)
}
现在刷新一下浏览器,看到一下结果。
现在对每个sfc多出了2次请求,分别是请求template和css。
这样子处理之后我们实现sfc热更新就简单了,只要server检查sfc文件改变,然后把文件路径和对应是什么类型发生改变,发送到浏览器,浏览器根据路径拼接出请求路径,然后使用import请求资源即可,在对应的更新资源拿到后,调用__VUE_HMR_RUNTIME__(改对象只要下载了vue文件,浏览器的全局环境就会有该对象)对象下对应的方法更新即可。
我们先来实现template的热更新,打开index.html文件。
ws.addEventListener('message', ({ data }) => {
const { type, path, id, index } = JSON.parse(data)
switch (type) {
case 'connected':
console.info('[vite] connected')
break
case 'reload':
import(`${path}?t=${Date.now()}`).then((m) => {
__VUE_HMR_RUNTIME__.reload(path, m.default)
console.log(`[vite][hmr] ${path} reloaded.`)
})
break
case 'rerender':
import(`${path}?type=template&t=${Date.now()}`).then((m) => {
__VUE_HMR_RUNTIME__.rerender(path, m.render)
console.log(`[vite][hmr] ${path} template updated.`)
})
break
case 'full-reload':
window.location.reload()
break
}
}, true)
此时如果你往App.vue的编辑template或者script代码,我们不用刷新浏览器就能够在看到了结果了。
接下来我们要解决sfc中css热更新的问题。
我们打开index.js文件,往creatFileWatcher补充如下检查css代码是否发生变化的代码。
async function createFileWatcher(root, notify) {
// 省略其他代码
if (file.endsWith('.vue')) {
// 其他代码省略
const prevStyles = prevDescriptor.styles || []
const nextStyles = descriptor.styles || []
nextStyles.forEach((_, i) => {
if (!prevStyles[i] || !isEqual(prevStyles[i], nextStyles[i])) {
send({
type: 'style-update',
path: resourcePath,
index: i
})
}
})
prevStyles.slice(nextStyles.length).forEach((_, i) => {
send({
type: 'style-remove',
path: resourcePath,
id: `${hash_sum(resourcePath)}-${i + nextStyles.length}`
})
})
}
}
循环新的数组以此取得新数组每一项跟旧的比较,如果发现新旧不相同要把新的更新上去。当新的循环完之后,我们用新的数组长度来slice旧数组,这种情况下有两种情况,1.旧数组比新数组长度短将会slice到一个空数组不会进入循环,2.新数组比旧的短,这种情况下意味着旧数组的style我们要删除掉。
然后我们打开index.html把浏览器接受到css更新后要进行的处理补充完整。
<script>
switch (type) {
switch (type) {
case 'connected':
console.info('[vite] connected')
break
case 'style-update':
console.log(`[vite] ${path} style${index > 0 ? `#${index}` : ``} updated.`)
import(`${path}?type=style&index=${index}&t=${Date.now()}`)
break
case 'style-remove':
const style = document.getElementById(`vue-style-${id}`)
if (style) {
style.parentNode!.removeChild(style)
}
break
case 'full-reload':
console.info('wow! has to full-reload')
break
}
}
</script>
至此我们逻辑已经全部处理完成了,希望本文章对你学习深入学习vite源码有所帮助完整代码链接点击领取