返回

Vite Manifest 报错?Laravel+Inertia 生产环境实战解决

vue.js

好的,这是你要的博客文章内容:


解密 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.jsMainPage-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 devnpm 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.jsMainPage-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_ENVproduction 时,Laravel 的 Vite 集成插件会读取这个 manifest.json 文件。它会根据你传给 @vite()原始资源路径 ,去 manifest 里查找对应的、最终生成的带 hash 的文件名 ,然后输出正确的 <script><link> 标签。

现在回过头看报错的代码:

@vite(['resources/js/app.ts', "resources/js/Pages/{$page['component']}.vue"])

这行代码在生产环境下,等于告诉 Laravel 去 manifest.json 里查找两个 key:

  1. resources/js/app.ts
  2. 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.tsapp.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.jsresources/js/app.ts。加载了主入口文件后,Inertia 的前端适配器(在 app.ts 中设置)会接管后续工作。它会根据从后端 $page 变量中获取的组件名称 ('MainPage', 'Users/Index', etc.),利用 Vite 支持的动态导入 import() 语法,自动去请求和加载对应的页面组件 JS chunk (如 MainPage-0889c7a1.js)。Vite 在 build 时已经处理好了代码分割和 manifest 映射,这一切都能无缝衔接。

步骤与代码:

  1. 修改 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>
    
  2. 确认 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 installnpm run build。同时执行 php artisan optimize:clear 清理 Laravel 的各种缓存。

方案二:检查 Vite 配置和构建环境

虽然方案一通常能解决问题,但也可能存在配置或环境上的小坑。

原理:

确保 Vite 配置本身没有错误,构建过程顺利完成,并且文件路径等没有因为环境问题(比如 Windows 的路径分隔符,虽然 Vite 通常处理得不错)导致异常。

步骤/代码:

  1. 详细检查 vite.config.js:

    • plugins: 确认 laravelvue 插件都已正确引入和配置。
    • input: 再次确认 input 数组里是正确的入口文件路径。
    • build.outDir: 默认应该是 public/build,确认没被修改。
    • build.manifest: 确认这个选项是 true (laravel-vite-plugin 默认会设置)。
  2. 检查 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
    }
    
  3. 彻底清理再构建:
    如果怀疑是缓存或者旧的构建产物干扰:

    # 删除旧的构建产物
    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 动态查找文件。如果你想手动列出,也是可以的,但不灵活。

重要提示 (为什么不推荐):

  1. 效率降低: 把每个页面都作为入口,可能会导致生成更多、更小的 JS 文件,或者重复打包一些公共代码,降低了代码分割的优化效果。浏览器可能需要发起更多的请求。
  2. 违背 Inertia 设计: Inertia 的核心就是后端驱动前端,前端根据后端给的组件名动态加载组件。这种方式让 Blade 视图强行知道了所有可能的页面组件,破坏了这种解耦。
  3. 维护困难: 每次新增页面,都需要确保 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 函数配置正确,通常就能药到病除。