ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Vue 3 + Vite 静态资源路径、动态引用与部署避坑指南

Vue 3 + Vite 静态资源路径、动态引用与部署避坑指南 上周帮同事迁一个后台项目从 Vue 2 那套老构建管线换到 Vue 3 Vite。业务代码几乎没动页面一跑起来商品列表里的国旗图标全裂了控制台里排着一队这样的请求GET http://localhost:5173/goods/src/assets/flags/cn.png 404。他盯着代码看了半个小时反复确认文件确实躺在那个目录里表达式也没写错——./assets/flags/ item.code .png。问题不在路径写错而在于这串路径是运行时才拼出来的字符串构建工具在编译阶段根本没看见它自然也没机会把它换成打包后的真实地址。这个坑几乎每个从 webpack 时代走过来的人都要踩一次而 Vue 3 的模板编译配合 Vite 的资源处理管线让本地图片和其他静态资源这件事的规则变得比过去更值得写清楚。这篇东西想聊透的就是这件事在 Vue 3 项目里本地图片、字体、SVG、JSON 这些静态资源到底该怎么引、该放在哪个目录、动态路径怎么处理、怎么判断它们真的加载完了、部署到二级目录为什么又会集体挂掉。内容适合已经能写 Vue 3 组件但被资源路径反复折磨的人也适合想把这块规范一次性定下来的团队负责人。全文没有玄学都是可以照着改的代码和可以复现的排查过程。1. 动态拼出来的图片路径为什么一定会裂图1.1 模板里的字面量路径在编译阶段就被改写过了先看一个所有人都写过的写法template img src./assets/logo.png altlogo / /template这段模板编译之后大致等价于import _imports_0 from ./assets/logo.png // 渲染函数里实际使用 // img src{_imports_0} altlogo /也就是说src属性里的这个字符串被vitejs/plugin-vue识别为资源引用编译期就变成了一个 ES import。导入的结果是构建产物的真实 URL开发环境下它是/src/assets/logo.png生产构建之后它会变成/assets/logo-3f2a1c9e.png这种带内容哈希的名字。浏览器从头到尾都没有见过./assets/logo.png这个原始字符串。被这种改写覆盖的属性是一份固定清单常见的包括img的src与srcset、source的src与srcset、video的src与poster、use的href和xlink:href、image的href。清单之外的属性不会被动。如果你封装了自己的图片组件比如BaseImg src./assets/a.png /默认它不会被改写因为编译器不认识这个标签。要让它也参与改写可以在vite.config.js里扩展// vite.config.js import vue from vitejs/plugin-vue export default { plugins: [ vue({ template: { transformAssetUrls: { tags: { BaseImg: [src, data-src], }, }, }, }), ], }注意这个配置只对模板里的字面量字符串生效。你写成:srcsomeVar之后无论怎么配都救不回来。1.2 编译器只认字面量拼接表达式它选择放手再来看出问题的那行代码img :src./assets/flags/ item.code .png /编译器看到的是一个绑定表达式它需要在运行时求值。编译器无法知道item.code可能取哪些值也就无法枚举出需要 import 的文件。它的处理方式是原样保留这个表达式交给浏览器。浏览器拿到./assets/flags/cn.png这样一个相对路径会以当前文档的 URL为基准去解析。假设页面地址是http://localhost:5173/goods/list那浏览器请求的就变成了http://localhost:5173/goods/assets/flags/cn.png——多出来的那段路径完全取决于当前路由有多深。这里有个特别容易让人误判的细节在开启了 history 模式并配置了 SPA fallback 的项目里这个请求可能返回 200 而不是 404因为服务器把不存在的路径统统回落到index.html。图片标签拿到一坨 HTML控制台报的是解码失败或者资源类型不符看着完全不像路径问题。我遇到过有人在onerror里打了断点结果发现img.src打印出来是对的就彻底懵了——img.src打印的是你赋进去的字符串经过浏览器规范化后的绝对地址跟你实际请求到哪个文件不是一回事。1.3 四条可行路线各自适合什么场景动态引用本地资源不是没有解只是每条路的代价不一样需要按场景挑。方案适用场景构建行为是否支持动态主要代价静态import数量固定、明确知道有哪几张参与哈希与压缩未引用不打包否手写一堆 importimport.meta.glob一整个目录、按名字或 code 取图生成映射表可懒加载是需要目录约定new URL(..., import.meta.url)少量、路径模式统一构建时按模式扫描是写错不报错只在运行时 404放public/手动拼需要固定 URL 的文件原样拷贝无哈希是失去缓存失效能力这四条的详细写法放在第 4 章先说一个判断原则能静态就静态数量大就 glob路径带变量且格式单一才用new URL固定 URL 给外部系统用的才进public。反过来用就是给自己埋坑。2.public和src/assets到底该怎么分派2.1 两个目录在构建产物里的差别是根本性的这两个目录的区别不是一个给图片用一个给组件用而是是否参与构建管线的资源处理。把差别列清楚选择就不再靠感觉对比项src/assets/public/是否加内容哈希是否小文件是否内联小于assetsInlineLimit转 base64不会是否做压缩/优化走构建插件可处理原样拷贝引用写法相对路径或/别名绝对路径 import.meta.env.BASE_URL未被引用时不进入产物依然进入产物是否可用import拿到 URL是否只能自己拼字符串核心是哈希这一栏。带哈希的资源可以放心设置一年不过期的强缓存因为文件内容一变文件名就变了浏览器自然去拉新的public/里的文件名字永远不变你替换了一张同名图片用户浏览器很可能一直用旧的缓存副本直到你手动改文件名或者调整缓存头。这是很多我改了图怎么线上还是老图问题的真正原因。2.2 我的实际分派清单落到具体项目里我是这么切的参与页面渲染的图片、图标、字体、需要读取内容的 JSON全部进src/assets/让构建管线接管。favicon.ico、robots.txt、站点清单文件、第三方平台要求的校验文件进public/因为它们必须能被一个固定、可预测的 URL 访问到不能带哈希。需要被外部系统直接引用的图片比如邮件模板里的图、开放给别人贴的分享图进public/或干脆放对象存储。AI 生成的示例图、演示用的超大图如果只在一个演示页出现放src/assets/并配合动态 import避免打进首屏包。还有一条经验不要在同一个功能里混用两套规则。我见过一个项目一半图片写在src/assets用相对路径引一半放public用/img/xxx.png引后来换了个子目录部署前者好好的后者全挂排查半天才发现只有一半的代码需要改。2.3assetsInlineLimit会让你的 URL 变量变成一坨 base64这是个很隐蔽的坑。Vite 默认会把小于 4096 字节的资源转成 base64 内联进代码此时import拿到的不是一个路径字符串而是一个data:image/png;base64,iVBORw0...开头的超长字符串。import icon from ./assets/icon.png // 如果 icon.png 只有 1KBicon 的值可能是 // data:image/png;base64,iVBORw0KGgoAAAANSUhEUg...这带来两个后果。第一任何用扩展名判断类型用new URL再拼一次的逻辑都会失效第二如果你的图标目录里有几十个小 PNG它们全被内联JS 主包会明显变胖首屏反而更慢。我的做法是显式设定阈值并且给图标类目录单独约定// vite.config.js export default { build: { // 小图标内联能减少请求数但别让一整包图标都进去 assetsInlineLimit: 2048, rollupOptions: { output: { assetFileNames: assets/[name]-[hash][extname], }, }, }, }提示调试资源路径问题时先在控制台console.log一下 import 出来的值。如果看到data:前缀说明这个文件被内联了别再纠结它为什么没有/assets/前缀。3. 怎么判断一张图、一份资源真的加载完成了3.1 只用onload会漏掉四种情况资源加载完成检测这件事看起来是给图片挂个onload就完事实际上至少四种情况会让你误判。第一种缓存的图可能在你挂监听之前就已经好了。如果这张图刚刚被别的组件加载过浏览器缓存里已经有了赋值src之后事件可能在极短时间内触发甚至同步完成顺序稍有不慎就丢事件。判断办法是赋值前先看img.complete img.naturalWidth 0。第二种naturalWidth不能用来判断 SVG。如果 SVG 文件本身没写width/height属性只写了viewBox图片能正常显示但naturalWidth和naturalHeight都是 0。用它们做成功判断会把好好的 SVG 判成失败。这种情况改用onload事件本身作为成功信号。第三种decode()不是万能的。img.decode()返回 Promise本意是等图片解码完成再绘制但它在部分浏览器上对某些格式会直接 reject内存紧张时也可能失败。而且它成功只代表解码完成不代表已经绘制到屏幕上。把它当作更严格的成功判断是不成立的。第四种组件卸载或路由切换时的竞态。用户手快连点两次 tab同一个资源发了两次请求先发的后回来状态就被写乱了。同一个 URL 的请求需要做去重。3.2 一个够用的图片加载工具函数把上面四种情况都收进去代码量其实不大// src/utils/loadImage.js const inflight new Map() export function loadImage(src, { crossOrigin, timeout 15000 } {}) { if (inflight.has(src)) return inflight.get(src) const task new Promise((resolve, reject) { const img new Image() if (crossOrigin) img.crossOrigin crossOrigin let timer null const cleanup () { clearTimeout(timer) img.onload null img.onerror null } // 缓存命中此时再挂事件可能已经太晚 if (img.complete img.naturalWidth 0) { resolve(img) return } img.onload () { cleanup() resolve(img) } img.onerror () { cleanup() reject(new Error(资源加载失败: ${src})) } timer setTimeout(() { cleanup() img.src reject(new Error(资源加载超时: ${src})) }, timeout) img.src src }) inflight.set(src, task) // 失败的请求不要留在映射里否则重试永远拿到旧结果 task.catch(() inflight.delete(src)) return task }几个取舍说明。inflight用 Map 做请求去重键就是最终的 URL 字符串因为带哈希的 URL 天然唯一不用担心内容变了还命中旧记录。超时是必须的我遇到过 CDN 边缘节点抽风请求既不成功也不失败页面就一直卡在 loading 上用户只能刷新。失败后从映射里删掉是为了让重试按钮真的能重试。3.3 批量加载要限并发进度反馈才有意义预加载一个相册的三百张图如果直接Promise.all(list.map(loadImage))会瞬间创建三百个 Image 对象。浏览器对同域名并发连接有上限超出的会排队同时在移动端还会带来明显的解码内存压力低端机上甚至会把页面拖崩。限并发的版本长这样// src/utils/loadImages.js import { loadImage } from ./loadImage export async function loadImages(list, { concurrency 6, onProgress } {}) { const results new Array(list.length) const failed [] let cursor 0 let finished 0 async function worker() { while (cursor list.length) { const index cursor const src list[index] try { results[index] await loadImage(src) } catch (err) { failed.push({ src, err }) } finally { finished 1 onProgress?.(finished, list.length, src) } } } const workers Array.from( { length: Math.min(concurrency, list.length) }, () worker() ) await Promise.all(workers) return { results, failed } }并发数我一般取 4 到 8。传统上浏览器对单个域名大约允许 6 个并发连接现代协议下多路复用让这个数字的意义变弱了但图片解码所需的内存和 CPU 并没有变多所以并发数还是不能放太开。移动端我会降到 3 到 4。用Promise.allSettled一把梭当然更短但拿不到进行到第几张的进度也没法做并发控制。进度条这种东西用户能看见百分比和看见一个转圈体验差距很大。3.4 在 Vue 组件里怎么落地把加载逻辑包成组合式函数组件里就干净了// src/composables/useResourceState.js import { ref, shallowRef, onScopeDispose } from vue export function useResourceState(loader) { const loading ref(false) const error ref(null) const data shallowRef(null) let cancelled false async function run(...args) { loading.value true error.value null try { const result await loader(...args) if (cancelled) return data.value result } catch (err) { if (cancelled) return error.value err } finally { if (!cancelled) loading.value false } } // 组件卸载或作用域销毁后不再写状态 onScopeDispose(() { cancelled true }) return { loading, error, data, run } }这里的data用shallowRef而不是ref是个小但要紧的选择。加载结果是HTMLImageElement这类复杂对象用深层响应式包起来Vue 会尝试遍历它的属性建代理既浪费性能也可能因为浏览器原生对象的内部槽位问题出现意料之外的行为。凡是整体替换、不做深层修改的大对象都该用shallowRef。如果只是想在模板上做展示兜底其实不用这么重直接在标签上用事件就够了img :srcurl loadinglazy decodingasync loadonLoaded erroronFailed /loadinglazy交给浏览器做视口懒加载decodingasync把解码挪出主线程这两个原生属性比手写 IntersectionObserver 省事得多。但要注意loadinglazy在首屏关键图上是负优化LCP 指标会变差首屏大图应该显式加fetchpriorityhigh并去掉懒加载。4.import.meta.glob与new URL的实操细节4.1 glob 的默认模式是懒的很多人第一次都拿错值import.meta.glob默认返回的不是 URL而是一个路径到导入函数的映射const modules import.meta.glob(./assets/flags/*.png) // modules 长这样 // { // ./assets/flags/cn.png: () import(./assets/flags/cn.png), // ./assets/flags/us.png: () import(./assets/flags/us.png), // }所以modules[./assets/flags/cn.png]拿到的是函数不是字符串直接塞给src必然裂图。要拿到 URL得加eager并且指定默认导出const urls import.meta.glob(./assets/flags/*.png, { eager: true, import: default, })更实用的写法是顺手把键名整理成业务友好的形式// src/assets/flags.js const modules import.meta.glob(./flags/*.png, { eager: true, import: default, }) export const flagMap Object.fromEntries( Object.entries(modules).map(([path, url]) { const name path.split(/).pop().replace(/\.png$/, ) return [name.toLowerCase(), url] }) ) export function getFlag(code) { return flagMap[String(code).toLowerCase()] ?? }模板里就变成了:srcgetFlag(item.code)干干净净。找不到时返回空串配合error或者v-if做占位图比在模板里写三元表达式清楚得多。4.2 glob 只有字面量参数跨文件调用键名会变两个必须记住的限制。第一参数必须是字面量。下面这两种写法都是无效的// 全部无效 const dir ./assets/flags import.meta.glob(dir /*.png) import.meta.glob(${dir}/*.png)因为这是一个构建期被静态分析的宏需要能在编译时就把模式匹配出来。第二模式是相对于当前文件的。在src/views/Home.vue里写import.meta.glob(./assets/*.png)和在src/components/Foo.vue里写同样一行指向的是完全不同的目录而 glob 出来的键名也不同。如果一个项目里多个文件都自己 glob 一遍很快就会出现同一个图片在 A 处键名是./assets/x.png在 B 处是../assets/x.png的混乱。我的做法是全局只在一个文件里 glob就是上面那个src/assets/flags.js其他所有地方都从这个文件导出的getFlag取值。改目录、换命名规则、加一个 WebP 格式都只动这一个文件。4.3**才递归负向模式可以排除文件./assets/*.png只匹配直接子级想递归子目录必须写成./assets/**/*.png。这一点和很多人的直觉相反我第一次用的时候以为*是递归的结果子目录里的图一张都没匹配到getFlag一直返回空串页面上一排灰底占位。排除特定文件用!前缀Vite 支持负向模式const modules import.meta.glob([ ./assets/icons/**/*.png, !./assets/icons/**/draft-*.png, ], { eager: true, import: default })设计稿的临时导出文件、切图工具留下的2x冗余副本都可以用这种方式挡在打包之外。比事后在构建产物里发现它们要省心得多。4.4new URL(...)的写法与它最坑的地方写法本身很简单function flagUrl(code) { return new URL(./assets/flags/${code}.png, import.meta.url).href }它最大的坑是写错不会报错。Vite 在构建时会按模板字符串推断出一个 glob 模式去扫描文件扫描到就把匹配到的资源一起打包如果因为路径拼错、大小写不符、目录层级不对导致一个文件也没扫到构建过程不会给任何警告代码照常产出只是运行时拿到一个指向不存在的地址的 URL浏览器 404。等你在生产环境发现已经是上线之后了。所以用new URL的写法务必满足三个条件模板字符串的静态前缀是字面量第二个参数必须是import.meta.url路径相对于当前文件所在的目录。变量只能出现在文件名那一段不能出现在目录那段。另外一个实际经验new URL和import.meta.glob不要对同一个目录同时使用。两条路会各自生成一套映射虽然最终指向的文件是同一个但代码里会出现两套风格维护的人根本分不清哪个是权威。定一套用到底。5. 部署之后才暴露的坑base、SVG、字体与缓存5.1 部署到二级目录public里的资源集体失效开发环境跑得好好的部署到https://example.com/admin/之后public里的图标全挂。原因基本只有一个代码里硬编码了/icons/xxx.png。以/开头的路径是相对于域名根的浏览器会去https://example.com/icons/xxx.png找而文件其实在https://example.com/admin/icons/xxx.png。正确写法是用 Vite 注入的 base 值// 模板里 :src${baseUrl}icons/logo.png // 组合式函数里 const baseUrl import.meta.env.BASE_URL对应的构建配置// vite.config.js export default { // 结尾斜杠必须写Vite 的 BASE_URL 会原样带上它 base: process.env.DEPLOY_BASE || /, }import.meta.env.BASE_URL的值就是base配置的原样输出它以/结尾如果你配的是/admin/或者就是/。所以拼接时不要再自己加斜杠写成${baseUrl}/icons/x.png会得到/admin//icons/x.png虽然多数服务器能容忍双斜杠但看着难受某些 CDN 的缓存键也会因此不一致。注意src/assets/里的资源不受这个影响因为它们的 URL 是构建时生成并带回 base 的。只有public/和手写绝对路径的地方需要你亲自处理。5.2 SVG 是当图片用还是当组件用取决于要不要改颜色SVG 有两种完全不同的用法混起来会很难受。当图片用直接 import 拿 URL和 PNG 一模一样import logo from ./assets/logo.svg这种用法的 SVG 是独立的资源请求外部 CSS 改不了它内部的颜色fill: currentColor这类需求全部失效。当组件用需要把内容内联进 DOM。用?raw拿到源码字符串import iconRaw from ./assets/icons/check.svg?rawtemplate span classicon v-htmliconRaw / /template style scoped .icon :deep(svg) { width: 1em; height: 1em; fill: currentColor; } /style这样颜色就能跟着文字走了。注意scoped样式对内联进来的 SVG 无效因为那段 DOM 不是编译时生成的必须用:deep()穿透。还有 sprite 的方案把所有图标塞进一个sprite.svg用use href#icon-check引用。这个方案在内联 sprite 时可用但如果是引用外部文件里的 symbol不同来源环境下会有引用限制做静态站点时我更倾向于内联 sprite 或者直接用?raw。5.3 字体文件的路径规则和 CSS 里的url()字体放哪决定了它会不会被处理。放src/assets/fonts/并在样式里写相对路径构建工具会接管产出带哈希的字体文件放public/fonts/然后用/fonts/x.woff2引就回到了前面说的绝对路径问题。/* src/assets/styles/fonts.css —— 相对路径会被改写 */ font-face { font-family: AppSans; src: url(../fonts/AppSans-Regular.woff2) format(woff2); font-weight: 400; font-display: swap; }font-display: swap是个必加项。默认行为下字体没加载完时文字是不可见的用户会看到一段空白等字体到了再突然出现。swap先用系统字体渲染字体到了再换阅读体验好得多代价是一次短暂的字形跳动。至于检测字体加载完成图片那套方法不适用字体有专门的接口// 等所有字体加载完毕 await document.fonts.ready // 主动触发某一字体的加载再等它 await document.fonts.load(400 16px AppSans)这个在需要精确测量文本宽度的场景里很关键。做画布绘制、图表坐标轴、虚拟滚动高度计算时如果字体还没就位就去量量出来的宽度是错的等字体一到整个布局全歪。我的习惯是在这类组件的onMounted里先await document.fonts.ready再初始化。5.4 大小写和中文文件名本地跑得通不代表线上跑得通macOS 和 Windows 的默认文件系统大小写不敏感Logo.png和logo.png在本地是同一个文件。Linux 服务器上它们完全不同。我在 CI 上见过不止一次这样的构建失败或者线上 404本地import Flag from ./assets/Flag.png实际文件叫flag.png本地一点问题没有Linux 构建时直接报模块找不到。中文文件名和空格也有类似的麻烦。浏览器和构建工具大多能处理但中间夹一层 CDN、一层对象存储、一层老旧的发布脚本就可能被转义或者截断。所以资源文件名我坚持两条全小写、用中划线分词只用 ASCII 字符。改名的成本在写的时候是零出问题之后再改就要逐个文件去查引用。6. 把这件事一次性规范掉别名、单点模块与代码片段6.1 别名要在构建配置和编辑器配置里同时配/assets/logo.png这种写法在模板里能不能用取决于别名配没配。而且必须两处都配只配一处会出现编辑器飘红但能构建或者编辑器正常但构建失败的分裂状态。// vite.config.js import { fileURLToPath, URL } from node:url export default { resolve: { alias: { : fileURLToPath(new URL(./src, import.meta.url)), }, }, }// jsconfig.json 或 tsconfig.json 的 compilerOptions 里 { compilerOptions: { baseUrl: ., paths: { /*: [src/*] } } }别名在模板的src里是有效的因为 Vite 会去解析这个路径。但要注意别名不能让动态拼接生效。/assets/flags/${code}.png依然是个运行时字符串编译器同样不会帮你换成 import。别名解决的是书写便利不是动态解析。6.2 用一个单点模块收口所有资源引用前面反复提到全局只 glob 一次实际落地就是建一个资源出口模块// src/assets/index.js const images import.meta.glob(./images/**/*.{png,jpg,webp,svg}, { eager: true, import: default, }) function normalize(path) { return path.replace(/^\.\//, ).replace(/\.(png|jpg|webp|svg)$/i, ) } export const assets Object.fromEntries( Object.entries(images).map(([path, url]) [normalize(path), url]) ) export function asset(name) { const url assets[name] if (!url import.meta.env.DEV) { console.warn([assets] 未找到资源: ${name}检查文件名与大小写) } return url ?? }业务里就是:srcasset(images/banner/home)。这个模式有几个实际好处切换 CDN 只改asset一个函数加一层开发环境找不到就报错、生产环境返回空的兜底后续要做资源清单审计、打包体积分析也有一个统一的入口可以挂。提示那个console.warn只在开发环境触发实测能省掉大量为什么这张图是空的的排查时间。找不到资源时静默返回空字符串是最糟的设计问题会被推迟到很后面的环节才暴露。6.3 用代码片段把规范钉进日常书写规范写进文档没人看写进代码片段才会每天被执行。Vue 3 项目里我最常用的两个片段一个是带完整加载状态处理的img标签一个是资源加载的组合式函数骨架。VS Code 的用户片段配置文件 - 首选项 - 配置用户代码片段 - vue.json{ Vue3 Image With States: { scope: vue, prefix: vimg, body: [ img, :src\${1:src}\, :alt\${2:alt}\, loading\${3|lazy,eager|}\, decoding\async\, load\${4:onLoad}\, error\${5:onError}\, / ], description: 带加载与错误处理的图片标签 }, Vue3 Asset Glob Map: { scope: javascript,typescript, prefix: vglob, body: [ const modules import.meta.glob(${1:./assets/*.png}, {, eager: true,, import: default,, }), , export const ${2:assetMap} Object.fromEntries(, Object.entries(modules).map(([path, url]) [, path.split(/).pop().replace(/\\.[^.]$/, ),, url,, ]),, ), ], description: 资源目录映射表 } }scope那一栏要写对vue片段只在.vue文件里出现写javascript则会在所有 JS 文件里冒出来容易和变量名撞前缀。前綴我习惯用v开头因为img、map这种常见词很容易在打变量名的时候误触发反而添乱。片区里还有个小技巧${3|lazy,eager|}这种写法是下拉选择写代码的时候直接在两个合法值之间选比手打好记也不容易拼错。凡是取值只有两三种的属性都值得做成选择项。6.4 上线前过一遍这张表这块内容踩过的坑太多了我把它做成了发布前的检查项逐条对一遍比事后救火便宜得多检查项判断方法常见后果有没有运行时拼接的图片路径全局搜:src和 :src线上裂图开发环境正常public资源是否用了BASE_URL搜/icons/、/img/这类硬编码二级目录部署全挂资源文件名是否全小写 ASCII遍历src/assets目录Linux 构建失败是否有本该入src的大图放进了public看public目录体积更新后用户看到的还是旧图首屏关键图是否被误设loadinglazy看首屏组件的 img 标签首屏渲染指标变差批量加载是否限了并发看预加载逻辑有无并发池移动端内存吃紧、卡顿加载失败是否有兜底断网后刷新页面试页面上一片空白裂图字体是否设了font-display看font-face声明首屏文字短暂不可见7. 我在这块最实在的两条体会一条是资源路径的问题百分之九十都能靠看浏览器实际请求了什么解决而不是看代码写了什么。打开网络面板看那一行 Request URL 的完整地址和文件的真实物理位置对比一下路径多出来的那一段、少掉的那一段、大小写不一致的那一个字母全都藏在那里。我见过太多人在代码里翻来覆去地改表达式就是不肯往那个面板看一眼。img.src打印出来的值永远是对的因为它只是被浏览器规范化了一遍它不告诉你服务器上到底有没有这个文件。另一条是能不能加载出来和什么时候加载完是两个问题不要用同一套代码去解决。前者是路径正确性属于构建期和目录约定的事后者是时序问题属于运行时状态管理的事。把这两个问题分开之后你会发现目录约定定好之后基本不会再动而加载状态的组合式函数可以一直复用下去。混在一起写就会变成一个既改路径又判状态、到处复制粘贴的utils.js谁也不敢动它。至于选哪种引用方式我的默认答案永远是先写好目录约定能静态引用就静态引用需要动态就一个文件里 glob 一次然后全局复用。剩下的那些边角情况等真的遇到了再逐个处理别提前为不确定的需求设计资源加载框架——那玩意儿通常撑不过两个迭代。
返回列表