You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Laravel+Inertia SSR环境下如何仅客户端加载SplideJS组件?

问题描述

技术栈

  • Laravel 10
  • @inertiajs/vue3@2.0.2
  • inertiajs/inertia-laravel v2.0.0(Inertia.js的Laravel适配器)
  • Vite v5.4.11
  • @vitejs/plugin-vue@5.1.3
  • @splidejs/vue-splide@0.6.12

需求

启用SSR时,让依赖SplideJS-vue的Vue组件跳过SSR阶段,仅在客户端进行水合(组件需动态适配屏幕尺寸,服务端无法处理)。

尝试的配置与代码

vite.config.mjs

import {defineConfig} from 'vite';
import laravel from 'laravel-vite-plugin';
import vue from '@vitejs/plugin-vue';
import {fileURLToPath, URL} from 'url';

import vuetify from "vite-plugin-vuetify";

export default defineConfig({

    plugins: [
        laravel({
            input: ['resources/js/main.js'],
            ssr: 'resources/js/ssr.js',
            refresh: true,
        }),
        vue({
            template: {
                compilerOptions: {
                    isCustomElement: (tag) => tag.includes('Splide'),
                }
            },
        }),
        vuetify({
            autoImport: true,
        }),
    ],
    ssr:
        {
            noExternal: ['@inertiajs/vue3/server'],
            external: ['@splidejs/vue-splide'],
        },
    resolve: {
        alias: {
            '@': fileURLToPath(new URL('./resources/js', import.meta.url)),
        },
        resolve: {
            dedupe: [
                'vue'
            ]
        },
    },

});

main.js

import { createApp, h } from "vue";
import { createInertiaApp, Link, Head } from "@inertiajs/vue3";
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
import { ZiggyVue } from '../../vendor/tightenco/ziggy';
import { createPinia } from 'pinia';
import piniaPluginPersistedState from "pinia-plugin-persistedstate";

import '../css/app.css';
import VueSplide from '@splidejs/vue-splide';
import '@splidejs/splide/css';
const appName = "SSR Test";
const pinia = createPinia();
pinia.use(piniaPluginPersistedState);

createInertiaApp({
    title: (title) => `${title} | ${appName}`,
    resolve: (name) =>
        resolvePageComponent(
            `./Pages/${name}.vue`,
            import.meta.glob('./Pages/**/*.vue')
        ),
    setup({ el, App, props, plugin }) {
        createApp({ render: () => h(App, props) })
            .use(plugin)
            .use(ZiggyVue)
            .use(pinia)
            .use(VueSplide)
            .component("Link", Link)
            .component("Head", Head)
            .mount(el);
    },
    progress: {
        color: '#000000',
    },
});

ssr.js

import {createInertiaApp, Head, Link} from '@inertiajs/vue3';
import createServer from '@inertiajs/vue3/server'
import { renderToString } from '@vue/server-renderer';
import { createSSRApp, h } from 'vue';
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
import { createPinia } from 'pinia';
import { ZiggyVue } from 'ziggy-js';

const pinia = createPinia();

createServer((page) =>
    createInertiaApp({
        page,
        render: renderToString,
        title: (title) => `testtitle`,
        resolve: (name) =>
            resolvePageComponent(
                `./Pages/${name}.vue`,
                import.meta.glob('./Pages/**/*.vue'),
            ),
        setup({ App, props, plugin }) {

            return createSSRApp({
                render: () => h(App, props),
            })
                .use(plugin)
                .use(pinia)
                .use(ZiggyVue, {
                    ...page.props.ziggy,
                    location: new URL(page.props.ziggy.location),
                })
                .component("Link", Link)
                .component("Head", Head);
        },
    }),
);

组件代码

<template>
    <div v-if="isClient">
        <component :is="splideComponent" :options="options" aria-label="Unser Kundenservice"
        >
            <SplideSlide>
                Slide 1
            </SplideSlide>
            <SplideSlide>
                Slide 2
            </SplideSlide>
            <SplideSlide>
                Slide 3
            </SplideSlide>
        </component>
    </div>
</template>

<script setup>
import ContentBaseWhite from '@/Shared/Components/c-content-base-white.vue';
import {onMounted, ref} from "vue";

const isClient = ref(false);
const splideComponent = ref(null);

onMounted(() => {
    isClient.value = true;
    import('@splidejs/vue-splide').then(module => {
        splideComponent.value = module.Splide;
    });
});

const options = ref({
   // 自定义选项配置
});
</script>

遇到的问题

启动SSR服务器php artisan inertia:start-ssr后无服务端错误,但Firefox控制台出现警告:

[Vue warn]: Vue received a Component that was made a reactive object. This can lead to unnecessary performance overhead and should be avoided by marking the component with `markRaw` or using `shallowRef` instead of `ref`. 
Component that was made reactive:  
Object { name: "Splide", emits: (27) […], components: {…}, props: {…}, setup: setup(props, context), render: _sfc_render$1(_ctx, _cache, $props, $setup, $data, $options)
 }
 
  at <CCustomerService> 
  at <BasicStoreLayout> 

组件虽能滑动但完全不响应,禁用SSR时一切正常。


解决方案

1. 修复响应式组件警告

警告的核心是组件被意外转为响应式对象,有两种修复方式:

方案一:使用shallowRef存储组件

shallowRef不会深度响应化对象,适合存储组件:

<template>
    <div v-if="isClient">
        <component :is="splideComponent" :options="options" aria-label="Unser Kundenservice">
            <component :is="splideSlideComponent">Slide 1</component>
            <component :is="splideSlideComponent">Slide 2</component>
            <component :is="splideSlideComponent">Slide 3</component>
        </component>
    </div>
</template>

<script setup>
import ContentBaseWhite from '@/Shared/Components/c-content-base-white.vue';
import {onMounted, shallowRef, ref} from "vue";

const isClient = ref(false);
const splideComponent = shallowRef(null);
const splideSlideComponent = shallowRef(null); // 同时处理SplideSlide组件

onMounted(() => {
    isClient.value = true;
    import('@splidejs/vue-splide').then(module => {
        splideComponent.value = module.Splide;
        splideSlideComponent.value = module.SplideSlide;
    });
});

const options = ref({
   // 自定义选项配置
});
</script>

方案二:用markRaw标记组件为非响应式

如果坚持使用ref,可以在赋值时用markRaw阻止Vue将组件转为响应式:

<script setup>
import ContentBaseWhite from '@/Shared/Components/c-content-base-white.vue';
import {onMounted, ref, markRaw} from "vue";

const isClient = ref(false);
const splideComponent = ref(null);
const splideSlideComponent = ref(null);

onMounted(() => {
    isClient.value = true;
    import('@splidejs/vue-splide').then(module => {
        splideComponent.value = markRaw(module.Splide);
        splideSlideComponent.value = markRaw(module.SplideSlide);
    });
});

const options = ref({
   // 自定义选项配置
});
</script>

2. 优化SSR配置

移除vite.config.mjs中错误的isCustomElement配置(Splide是Vue组件,不是自定义元素),并完善SSR的external规则:

// vite.config.mjs
ssr: {
    noExternal: ['@inertiajs/vue3/server'],
    external: ['@splidejs/vue-splide', '@splidejs/splide'], // 同时排除Splide核心库
},

3. 移除全局注册的Splide

既然组件是动态导入,不需要在main.js中全局注册,删除以下代码:

import VueSplide from '@splidejs/vue-splide';
// ...
.use(VueSplide)

如果全局需要Splide样式,可以保留import '@splidejs/splide/css';。

4. 简化客户端判断逻辑

直接通过typeof window !== 'undefined'判断是否在客户端,无需等待onMounted:

<script setup>
import ContentBaseWhite from '@/Shared/Components/c-content-base-white.vue';
import {shallowRef, ref} from "vue";

const isClient = typeof window !== 'undefined';
const splideComponent = shallowRef(null);
const splideSlideComponent = shallowRef(null);

if (isClient) {
    import('@splidejs/vue-splide').then(module => {
        splideComponent.value = module.Splide;
        splideSlideComponent.value = module.SplideSlide;
    });
}

const options = ref({
   // 自定义选项配置
});
</script>

5. 重新构建并启动SSR

执行构建命令:

npm run build

启动SSR服务器:

php artisan inertia:start-ssr

调整后,Splide组件会完全在客户端加载,不参与SSR渲染,同时解决响应式警告,组件交互也会恢复正常。

内容的提问来源于stack exchange,提问作者dogma

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 19:33:11