Vite Manifest 报错?Laravel+Inertia 生产环境实战解决
2025-05-04 18:02:48
好的,这是你要的博客文章内容:
解密 Vite 生产环境下的 'Unable to locate file in Vite manifest' 报错 (Laravel + Inertia 实战)
问题来了:生产环境 V.S. 开发环境
写 Laravel + Inertia + Vue 项目,用 Vite 做前端构建,这套组合拳打起来挺顺手的。开发环境 (npm run dev
) 下一切正常,页面唰唰地出来,热更新也给力。可等到准备上线,执行 npm run build
打包,然后把环境切换到生产 (APP_ENV=production
),突然就傻眼了——页面报错:
Unable to locate file in Vite manifest: resources/js/Pages/MainPage.vue.
怪了,MainPage.vue
这个文件明明就在那儿,npm run dev
的时候 Vite 找得到,怎么 build
之后就找不到了?而且看浏览器网络请求,相关的 JS 文件(比如 app-xxxx.js
和 MainPage-yyyy.js
)其实也加载成功了,但 Vue 组件就是没挂载,放在 setup
里的 console.log
也没反应。
出问题的代码在 app.blade.php
的 <head>
里,长这样:
@routes
@vite(['resources/js/app.ts', "resources/js/Pages/{$page['component']}.vue"])
@inertiaHead
看样子,问题就出在这个 @vite
指令上。特别是后面那个动态拼接的部分 "resources/js/Pages/{$page['component']}.vue"
,嫌疑很大。
刨根问底:为什么 npm run build
后就找不到文件了?
要搞清楚原因,得先明白 npm run dev
和 npm run build
背后 Vite 干的事儿有啥不一样。
npm run dev
(开发模式): Vite 这时候像个灵活的管家。它启动一个开发服务器,直接提供你的源代码文件。当浏览器请求某个模块(比如app.ts
或者某个 Vue 组件)时,Vite 会实时处理、转换代码(比如把 TypeScript 转成 JavaScript),然后发送给浏览器。它知道你的原始文件路径,比如resources/js/Pages/MainPage.vue
。npm run build
(生产模式): 这时候 Vite 是个打包工。它会分析你的代码依赖,把所有需要的 JavaScript、CSS 等资源进行优化、压缩、合并,并生成带有 hash 值的文件名(比如app-3c9a2cd6.js
、MainPage-0889c7a1.js
)放到public/build/assets
目录下。同时,它还会生成一个重要的文件:public/build/manifest.json
。
这个 manifest.json
文件就是关键!它像一个地图,记录了原始资源文件路径(作为 key)和最终构建生成的带 hash 的文件名(作为 value)之间的对应关系。例如,它可能长这样(简化版):
{
"resources/js/app.ts": {
"file": "assets/app-3c9a2cd6.js",
"src": "resources/js/app.ts",
"isEntry": true,
"dynamicImports": ["resources/js/Pages/MainPage.vue"], // Vite 知道 app.ts 动态导入了 MainPage.vue
"css": ["assets/app-f3ca4f7f.css"]
},
"resources/js/Pages/MainPage.vue": {
"file": "assets/MainPage-0889c7a1.js",
"src": "resources/js/Pages/MainPage.vue",
"isDynamicEntry": true, // 注意,通常是动态入口
"imports": ["resources/js/app.ts"] // 它依赖主入口文件提供的运行时或共享代码
}
// ... 可能还有其他 CSS 或资源项
}
当你在 Laravel 的 Blade 模板里使用 @vite()
指令,并且 APP_ENV
是 production
时,Laravel 的 Vite 集成插件会读取这个 manifest.json
文件。它会根据你传给 @vite()
的原始资源路径 ,去 manifest 里查找对应的、最终生成的带 hash 的文件名 ,然后输出正确的 <script>
和 <link>
标签。
现在回过头看报错的代码:
@vite(['resources/js/app.ts', "resources/js/Pages/{$page['component']}.vue"])
这行代码在生产环境下,等于告诉 Laravel 去 manifest.json
里查找两个 key:
resources/js/app.ts
resources/js/Pages/MainPage.vue
(假设当前页面是MainPage
)
resources/js/app.ts
通常是你 vite.config.js
里定义的入口(entry point),所以在 manifest.json
里肯定能找到对应的记录,没问题。
但 resources/js/Pages/MainPage.vue
呢?在标准的 Inertia + Vite 配置下,页面组件(*.vue
文件)通常不是 直接作为 Vite 的主入口点(entry point)。它们是被你的主入口文件 (app.ts
或 app.js
) 动态导入 (dynamically imported) 的。Vite 在构建时会识别这些动态导入,进行代码分割(code splitting),为这些页面组件生成单独的 chunk 文件(比如 MainPage-0889c7a1.js
)。
虽然 manifest 里可能有关于 resources/js/Pages/MainPage.vue
的条目(如上例所示),但 @vite
指令的设计初衷是用来加载主入口文件 。它可能无法正确处理这种直接引用非入口、动态导入的组件源路径的情况,或者说,它期望你只提供 vite.config.js
中定义的 input
。当它试图查找 resources/js/Pages/MainPage.vue
这个 key 并期望它是一个“顶级入口”时,就可能找不到或处理出错,于是抛出 "Unable to locate file in Vite manifest" 的错误。
那为什么把 "resources/js/Pages/{$page['component']}.vue"
从 @vite
指令里删掉后,Vue 组件就不挂载了呢?
因为你虽然不再让 @vite
指令去查找那个有问题的 manifest key,但也没有告诉浏览器需要加载这个特定页面的 JS chunk (MainPage-0889c7a1.js
)。虽然 app-3c9a2cd6.js
被加载了,但 Inertia 和 Vue 运行起来后,发现缺少了当前页面组件的代码,自然无法渲染页面。
所以,问题根源在于:在生产环境下,错误地尝试让 @vite
Blade 指令直接加载一个通过动态导入处理的页面组件的原始路径,而不是仅仅加载定义好的主入口文件,并让 Inertia 的前端部分负责按需加载页面组件的代码块。
对症下药:搞定 Vite Manifest 报错
明白了原因,解决起来就思路清晰了。核心思想是:让专业的人干专业的事。让 @vite
指令负责加载主入口,让 Inertia 和 Vite 的动态导入机制负责加载页面组件。
方案一:回归 Inertia 的正轨(推荐)
这是最标准、最推荐的做法。
原理:
@vite
Blade 指令只需要负责加载你在 vite.config.js
里定义的入口(entry)文件 ,通常就是 resources/js/app.js
或 resources/js/app.ts
。加载了主入口文件后,Inertia 的前端适配器(在 app.ts
中设置)会接管后续工作。它会根据从后端 $page
变量中获取的组件名称 ('MainPage'
, 'Users/Index'
, etc.),利用 Vite 支持的动态导入 import()
语法,自动去请求和加载对应的页面组件 JS chunk (如 MainPage-0889c7a1.js
)。Vite 在 build
时已经处理好了代码分割和 manifest 映射,这一切都能无缝衔接。
步骤与代码:
-
修改
app.blade.php
:
把@vite
指令改回最简单的形式,只包含主入口文件。{{-- resources/views/app.blade.php --}} <head> {{-- ... 其他 meta 标签 ... --}} <title inertia>{{ config('app.name', 'Laravel') }}</title> {{-- Fonts --}} <link rel="stylesheet" href="https://fonts.bunny.net/css2?family=Nunito:wght@400;600;700&display=swap"> {{-- Scripts --}} @routes {{-- 只保留主入口文件! --}} @vite(['resources/js/app.ts']) @inertiaHead </head> <body class="font-sans antialiased"> @inertia </body> </html>
-
确认
resources/js/app.ts
(或app.js
) 配置正确:
确保你的 Inertia 应用初始化代码使用了 Vite 支持的动态导入方式来解析页面组件。通常使用import.meta.glob
。// resources/js/app.ts import './bootstrap'; import '../css/app.css'; // 如果有全局 CSS import { createApp, h, DefineComponent } from 'vue'; import { createInertiaApp } from '@inertiajs/vue3'; // 或 @inertiajs/inertia-vue3 import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers'; import { ZiggyVue } from '../../vendor/tightenco/ziggy/dist/vue.m'; // 根据你的 Ziggy 路径调整 const appName = window.document.getElementsByTagName('title')[0]?.innerText || 'Laravel'; createInertiaApp({ title: (title) => `${title} - ${appName}`, // resolve: (name) => resolvePageComponent(`./Pages/${name}.vue`, import.meta.glob('./Pages/**/*.vue')), // 旧方式,仍然可用 // 推荐使用 Vite 的 import.meta.glob,确保它能找到你的页面组件 // 这个函数告诉 Inertia 如何根据组件名 (如 'MainPage', 'Users/Index') 找到并加载对应的 Vue 文件 resolve: (name) => { const pages = import.meta.glob<DefineComponent>('./Pages/**/*.vue', { eager: false }); // 使用非 eager 模式进行懒加载 let page = pages[`./Pages/${name}.vue`]; if (!page) { throw new Error(`Unknown page component: ${name}. Please check your page path.`); } // page() 返回一个 Promise,Inertia 会处理这个 Promise return page(); }, setup({ el, App, props, plugin }) { createApp({ render: () => h(App, props) }) .use(plugin) .use(ZiggyVue, (window as any).Ziggy) // 类型断言可能需要 .mount(el); }, progress: { color: '#4B5563', }, }); // 确保你的 console.log 在 setup 之外,比如这里,确认 app.ts 本身是否执行 console.log('app.ts initialized');
关键是
resolve
函数。import.meta.glob('./Pages/**/*.vue')
会被 Vite 处理。在build
时,Vite 分析这个 glob,为每个匹配的.vue
文件创建可能的代码分割点。当resolve(name)
被调用时,它会找到对应的动态导入函数(比如() => import('./Pages/MainPage-fac3a2b1.js')
),执行它,从而加载需要的 JS chunk。
进阶技巧/检查:
- 检查
vite.config.js
: 确保你的input
配置只包含了resources/js/app.ts
(或其他主入口),并且laravel
插件配置正确。// vite.config.js import { defineConfig } from 'vite'; import laravel from 'laravel-vite-plugin'; import vue from '@vitejs/plugin-vue'; export default defineConfig({ plugins: [ laravel({ input: ['resources/js/app.ts'], // 确认只有主入口 ssr: 'resources/js/ssr.ts', // 如果用了 SSR refresh: true, }), vue({ template: { transformAssetUrls: { base: null, includeAbsolute: false, }, }, }), ], });
- 确认
manifest.json
生成: 运行npm run build
后,检查public/build/manifest.json
是否存在且内容看起来合理。应该有resources/js/app.ts
的条目,可能还会有各个页面组件作为dynamicImports
或单独的isDynamicEntry: true
条目出现。 - 清理缓存: 有时候缓存也会捣乱。可以尝试删除
public/build
目录、node_modules
目录,然后重新npm install
和npm run build
。同时执行php artisan optimize:clear
清理 Laravel 的各种缓存。
方案二:检查 Vite 配置和构建环境
虽然方案一通常能解决问题,但也可能存在配置或环境上的小坑。
原理:
确保 Vite 配置本身没有错误,构建过程顺利完成,并且文件路径等没有因为环境问题(比如 Windows 的路径分隔符,虽然 Vite 通常处理得不错)导致异常。
步骤/代码:
-
详细检查
vite.config.js
:plugins
: 确认laravel
和vue
插件都已正确引入和配置。input
: 再次确认input
数组里是正确的入口文件路径。build.outDir
: 默认应该是public/build
,确认没被修改。build.manifest
: 确认这个选项是true
(laravel-vite-plugin 默认会设置)。
-
检查
package.json
中的 build 脚本:
确认"build"
脚本正确调用了vite build
(或者包含 SSR 的vite build && vite build --ssr
)。// package.json { "private": true, "scripts": { "dev": "vite", "build": "vite build" // 如果有 SSR: "vite build && vite build --ssr" }, // ... dependencies and devDependencies }
-
彻底清理再构建:
如果怀疑是缓存或者旧的构建产物干扰:# 删除旧的构建产物 rm -rf public/build # (可选) 删除 node_modules 并重装依赖 rm -rf node_modules npm install # 清理 Laravel 缓存 php artisan optimize:clear php artisan view:clear php artisan route:clear php artisan config:clear # 重新构建 npm run build
安全建议/注意点:
- 文件命名/大小写: 尤其在 Windows 环境下开发,部署到 Linux 服务器时,要注意文件名和引用路径的大小写是否完全一致。虽然本例错误信息里大小写看起来没问题,但这是个常见的坑。
- 路径分隔符: Vite 和 Node.js 通常能较好地处理不同操作系统的路径分隔符 (
/
vs\
),但如果手动拼接路径字符串时要小心,尽量使用 Node.js 的path
模块或确保使用/
。
方案三:强行指定页面入口(一般不推荐)
这种方法可以“绕过”问题,让 manifest.json
里确实包含所有页面组件作为直接的入口,但它通常不是 推荐的方式,因为它破坏了 Inertia 设计的初衷和 Vite 代码分割的优势。
原理:
修改 vite.config.js
,把所有的 Pages/**/*.vue
文件也添加到 input
数组中。这样 Vite 会把每个页面都当作一个独立的入口点来构建,manifest.json
里自然就会有 resources/js/Pages/MainPage.vue
这样的 key,原始的 @vite([...])
指令就能找到它了。
步骤/代码:
// vite.config.js
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import vue from '@vitejs/plugin-vue';
import { glob } from 'glob'; // 需要安装 glob 包: npm install -D glob
import path from 'path'; // Node.js 内置模块
// 获取所有 Pages 下的 Vue 文件路径
const pageFiles = glob.sync('resources/js/Pages/**/*.vue').map(file => path.resolve(__dirname, file));
export default defineConfig({
plugins: [
laravel({
// 把 app.ts 和所有页面组件都作为入口
input: [
'resources/js/app.ts',
...pageFiles // 将页面文件路径添加到 input 数组
],
ssr: 'resources/js/ssr.ts',
refresh: true,
}),
vue({
// ... vue plugin config
}),
],
});
注意: 上面的代码示例使用 glob
动态查找文件。如果你想手动列出,也是可以的,但不灵活。
重要提示 (为什么不推荐):
- 效率降低: 把每个页面都作为入口,可能会导致生成更多、更小的 JS 文件,或者重复打包一些公共代码,降低了代码分割的优化效果。浏览器可能需要发起更多的请求。
- 违背 Inertia 设计: Inertia 的核心就是后端驱动前端,前端根据后端给的组件名动态加载组件。这种方式让 Blade 视图强行知道了所有可能的页面组件,破坏了这种解耦。
- 维护困难: 每次新增页面,都需要确保
vite.config.js
里的input
更新(如果是手动列出的话),或者依赖 glob 能正确匹配。
仅在特殊场景或调试时考虑此方案,首选方案一。
总结一下,"Unable to locate file in Vite manifest" 这个报错,在 Laravel + Inertia + Vite 的组合拳下,多半是因为在生产环境下,错误地在 @vite
Blade 指令里包含了非入口文件的页面组件路径。正确的姿势是只在 @vite
里加载主入口 JS 文件 (app.ts
/app.js
),然后让 Inertia 的前端部分配合 Vite 的动态导入能力,自动按需加载对应的页面组件代码块。检查并修正 app.blade.php
中的 @vite
指令,同时确保 app.ts
中 Inertia 的 resolve
函数配置正确,通常就能药到病除。