vite:它基于 原生 ES 模块
提供了 丰富的内建功能
,如速度快到惊人的 模块热更新(HMR)
。
SFC单文件组件:代码块可以使用 lang
这个 attribute 来声明预处理器语言
Vue 单文件组件分为三部分:<template>
、<script>
、<style>
使用的语言分别是:HTML
、JavaScript
、CSS
设置 lang="pug"
,能够用 pug 更简洁的编写 HTML
例如:
<script setup>const msg = 'Hello world!';</script><template lang="pug">.example {{ msg }}</template><style>.example { color: red;}</style>
12345678910111213
问题描述
运行 npm run serve
启动开发服务器,然后修改 *.vue 文件内容。
<template lang="pug">
模板添加预处理器:
可以发现,如果只修改:<script>
、<template>
其中一个代码片段,vite 服务器可以热更新。
但是如果同时修改 <script>
、<template>
代码片段,控制台就会出现以下错误:
[Vue warn]: Property "xxx" was accessed during render but is not defined on instance.
而且页面显示异常,热更新失败。
通过使用 set DEBUG=vite:transform && npm run dev
启动开发服务器
可以看到更新的顺序:(js
文件在vue
文件之前)
vite:transform 13.20ms /src/main.ts +0msvite:transform 1.38ms /src/style.css +4msvite:transform 4.53ms /node_modules/.vite/deps/vue.js?v=77658bce +5msvite:transform 11.46ms /src/App.vue +12msvite:transform 133.41ms plugin-vue:export-helper +133msvite:transform 133.89ms /src/App.vue?vue&type=style&index=0&lang.css +0msvite:transform 135.03ms /src/App.vue?vue&type=template&lang.js +2msvite:transform 11.17ms /@vite/client +12msvite:transform 0.33ms /node_modules/vite/dist/client/env.mjs +4ms[vite] hmr update /src/App.vue, /src/App.vue?vue&type=template&lang.jsvite:transform 4.72ms /src/App.vue?vue&type=template&lang.js +17svite:transform 2.01ms /src/App.vue +2msvite:transform 0.73ms /src/App.vue?vue&type=style&index=0&lang.css +1ms
hmr 正常情况:
只修改 <script>
代码片段更新顺序就是正常的
[vite] hmr update /src/App.vuevite:transform 2.56ms /src/App.vue +9svite:transform 4.70ms /src/App.vue?vue&type=style&index=0&lang.css +5msvite:transform 5.12ms /src/App.vue?vue&type=template&lang.js +0ms
把 lang="pug"
去掉:
<script setup>const msg = 'Hello world!';</script><template lang="pug"><template>.example {{ msg }}</template><style>.example { color: red;}</style>
123455678910111213
同时修改 <script>
、<template>
代码片段,hmr 正常
[vite] hmr update /src/App.vuevite:transform 0.00ms /src/App.vue?vue&type=template&lang.jsvite:transform 3.26ms /src/App.vue +22svite:transform 0.75ms /src/App.vue?vue&type=style&index=0&lang.css +1ms
可以看到少了一行 &type=template&lang.js
,<template>
代码块内联到 render
函数里了
hmr 异常情况:
在使用 pug 预处理器的情况下,同时修改 <script>
、<template>
代码片段:
[Vue warn]: Property "xxx" was accessed during render but is not defined on instance.
寻找源头
调试-DEBUG:
通过开发工具的 网络 面板查看 hmr 的 websocket 可以看到顺序是对的
{ "type": "update", "updates": [ { "type": "js-update", "timestamp": 1689409360384, "path": "/src/App.vue", "explicitImportRequired": false, "acceptedPath": "/src/App.vue" }, { "type": "js-update", "timestamp": 1689409360384, "path": "/src/App.vue?vue&type=template&lang.js", "explicitImportRequired": false, "acceptedPath": "/src/App.vue?vue&type=template&lang.js" } ]}
12345678910111213141516171819
开发工具查看 http 请求的时间:
发现:虽然 &type=template&lang.js
文件在 .vue
文件之后发送请求,但是不会等待 .vue
文件处理完成,而是提前返回了,所以获取不到 <script>
里面声明的变量。
通过查看源码,可以发现编译 <template>
代码块时,会去获取 <script>
代码块信息:
@vitejs/plugin-vue/dist/index.mjs
function resolveTemplateCompilerOptions(descriptor, options, ssr) { const block = descriptor.template; if (!block) { return; } const resolvedScript = getResolvedScript(descriptor, ssr); const hasScoped = descriptor.styles.some((s) => s.scoped); const { id, filename, cssVars } = descriptor;
184185186187188189190191
然后获取代码块的声明,传递给编译器。
return { ...options.template, id, filename, scoped: hasScoped, slotted: descriptor.slotted, isProd: options.isProduction, inMap: block.src ? void 0 : block.map, ssr, ssrCssVars: cssVars, transformAssetUrls, preprocessLang: block.lang, preprocessOptions, compilerOptions: { ...options.template?.compilerOptions, scopeId: hasScoped ? `data-v-${id}` : void 0, bindingMetadata: resolvedScript ? resolvedScript.bindings : void 0, expressionPlugins, sourceMap: options.sourceMap } };}
230231232233234235236237238239240241242243244245246247248249250251
getResolvedScript()
会获取 <script>
代码片段编译的缓存:
@vitejs/plugin-vue/dist/index.mjs
function getResolvedScript(descriptor, ssr) { return (ssr ? ssrCache : clientCache).get(descriptor);}
263264265
编译 .vue
文件 => transformMain
=> genScriptCode
=> resolveScript
会设置 <script>
代码片段缓存:
@vitejs/plugin-vue/dist/index.mjs
function resolveScript(descriptor, options, ssr) { if (!descriptor.script && !descriptor.scriptSetup) { return null; } const cacheToUse = ssr ? ssrCache : clientCache; const cached = cacheToUse.get(descriptor); if (cached) { return cached; } let resolved = null; resolved = options.compiler.compileScript(descriptor, { ...options.script, id: descriptor.id, isProd: options.isProduction, inlineTemplate: isUseInlineTemplate(descriptor, !options.devServer), reactivityTransform: options.reactivityTransform !== false, templateOptions: resolveTemplateCompilerOptions(descriptor, options, ssr), sourceMap: options.sourceMap, genDefaultAs: canInlineMain(descriptor, options) ? scriptIdentifier : void 0 }); if (!options.isProduction && resolved?.deps) { for (const [key, sfcs] of typeDepToSFCMap) { if (sfcs.has(descriptor.filename) && !resolved.deps.includes(key)) { sfcs.delete(descriptor.filename); } } for (const dep of resolved.deps) { const existingSet = typeDepToSFCMap.get(dep); if (!existingSet) { typeDepToSFCMap.set(dep, /* @__PURE__ */ new Set([descriptor.filename])); } else { existingSet.add(descriptor.filename); } } } cacheToUse.set(descriptor, resolved); return resolved;}
273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
按照上面的逻辑进行分析:
- 文件改变后,浏览器同时获取
.vue
文件与 &type=template&lang.js
文件。 - vite 服务器先收到
.vue
文件请求,开始编译 .vue
文件。 - 还没编译完成
.vue
文件,又收到 &type=template&lang.js
文件请求。 - 编译
&type=template&lang.js
文件获取不到 .vue
文件编译的 <script>
代码片段缓存。 - 得不到
bindingMetadata
,所以模板渲染无法绑定对应的变量,请求完成。 - 浏览器渲染模板,模板变量访问失败。
解决问题
通过搜索 issues
发现 #76,找到了一个 pull#106
辛苦调试半天原来有人解决了/(ㄒoㄒ)/~~,google大法不如直接搜github…
这个 pull 还没有合并发布新版本,只能自己改源码了:
@vitejs/plugin-vue/dist/index.mjs
function compile(code, descriptor, options, pluginContext, ssr) { const filename = descriptor.filename; resolveScript(descriptor, options, ssr); const result = options.compiler.compileTemplate({ ...resolveTemplateCompilerOptions(descriptor, options, ssr), source: code });
161162163164165166167
重新运行 set DEBUG=vite:transform && npm run dev
同时修改 <template>
、<script>
代码片段后:
[vite] hmr update /src/App.vue, /src/App.vue?vue&type=template&lang.jsvite:transform 5.56ms /src/App.vue?vue&type=template&lang.js +4svite:transform 1.27ms /src/App.vue +2msvite:transform 0.58ms /src/App.vue?vue&type=style&index=0&lang.css +1ms
虽然顺序没变,还是先完成 &type=template&lang.js
请求。
但是在编译模板时会先编译 <script>
代码片段,hmr 热更新正常。
增加本地包:
因为 pull#106 请求还没有正式合并发布,
所以修改源代码,避免被包管理重新覆盖需要增加本地包:
复制 node_modules
下面的 @vitejs/plugin-vue
文件夹,然后加入补丁。
再修改 package.json
文件(文件路径为 file:./
协议):
package.json
{ "name": "hmrtest", "private": true, "version": "0.0.0", "type": "module", "scripts": { "dev": "vite", "build": "vue-tsc && vite build", "preview": "vite preview" }, "dependencies": { "pug": "^3.0.2", "vue": "^3.3.4" }, "devDependencies": { "@vitejs/plugin-vue": "^4.2.3", "@vitejs/plugin-vue": "file:./packages/plugin-vue", "typescript": "^5.0.2", "vite": "^4.4.0", "vue-tsc": "^1.8.3" }}
12345678910111213141516161718192021
运行 npm install
,重新测试,hmr 正常