Closed WolfgangDrescher closed 1 year ago
I just came up with another idea:
// composables/useClientOnly.js
import { h, defineComponent } from 'vue';
const isBrowser = typeof window !== 'undefined';
export function useClientOnly(asyncComponent) {
return defineComponent({
setup(props, context) {
return () => isBrowser ? h(asyncComponent, { ...props, ...context.attrs }) : h('div');
}
});
};
And use it on the page like this:
<script setup>
// pages/song.page.vue
import { useClientOnly } from '../../composables/useClientOnly';
const MidiPlayer = defineAsyncComponent(() => import('../../components/MidiPlayer.vue'));
const AsyncMidiPlayer = useClientOnly(MidiPlayer);
</script>
<template>
<AsyncMidiPlayer />
</template>
Still I think that this is not the optimal solution for this problem.
I found another issue (https://github.com/egoist/vue-client-only/issues/122) that pointed me to the current ClientOnly
component from Nuxt.js version 3:
<script setup>
// pages/song.vue
import { ref, onMounted, nextTick } from 'vue';
import ClientOnly from '../components/ClientOnly.js';
import MidiPlayer from '../components/MidiPlayer.vue';
const midiPlayer = ref(null);
onMounted(() => {
nextTick(async () => {
console.log(midiPlayer.value);
});
});
</script>
<template>
<ClientOnly>
<MidiPlayer ref="midiPlayer" />
</ClientOnly>
</template>
In my case it is important that I get access to the component as template ref to call an exposed method. So I really would prefer not to use an async component but instead a regular component in combination with a ClientOnly
wrapper. Note that here I also need to use nextTick
to get the template reference (see: https://github.com/egoist/vue-client-only/issues/50).
If I run vite build && vite build --ssr && vite-plugin-ssr prerender
everything is perfectly fine and works finally as expected. But I still get an error when running node ./server/index.js
:
[vite] Error when evaluating SSR module /node_modules/verovio-humdrum/index.js?v=2f51debf:
ReferenceError: __dirname is not defined
Any idea why it works fine with vite build
but not with the development server?
This issue also is somewhat related when I tried to get it work with the useClientOnly.js
composable that I posted before: https://github.com/vuejs/core/issues/2671
NB: My package.json
setup or the vite-plugin-ssr prerender
seem to be somewhat relevant for this. I simplified the examples above. But in my current setup I linked a plugin that I'm working on at the same time as file:../../vue-verovio-canvas
in package.json
:
{
"dependencies": {
"vue-verovio-canvas": "file:../../vue-verovio-canvas"
}
}
Running vite-plugin-ssr prerender
I get another error:
vite-plugin-ssr 0.3.59 pre-rendering HTML...
Error: Cannot find module 'verovio-humdrum'
But if I change this dependency to the GitHub repository instead of the local file link everything works just fine again:
{
"dependencies": {
"vue-verovio-canvas": "github:WolfgangDrescher/vue-verovio-canvas"
}
}
The development server is still not running tough and still prints the same error as mentioned before:
[vite] Error when evaluating SSR module /node_modules/verovio-humdrum/index.js?v=2f51debf:
ReferenceError: __dirname is not defined
Most(/all?) seem to be user land issues.
But I agree a working example would be nice.
One thing you may want to try is import.meta.env.SSR
, see https://vitejs.dev/guide/ssr.html#conditional-logic. That should ensure that SSR will not consider your component at all.
I've used the ClientOnly component from vitepress with no issues ClientOnly.ts
https://vitepress.vuejs.org/guide/global-component.html#clientonly
Thanks @DoubleJ-G.
I will update the docs. I'm leaving this open until I do.
A similar issue i found far quicker though was a similar issue for react though. As such im linking to the PR for better observability.
https://github.com/brillout/vite-plugin-ssr/pull/488
A bit off topic: I was about to open an issue myself for the lack of documentation. By sheer luck i found this issue though. Guess my search queries about dynamic imports were far off :). I really enjoy vps a lot, albeit sometimes the documentation is too broad and concrete knowledge is documented on different pages (Especially irt the default Server // vite cli in combination with moving to ES6).
I will try the vitepress component soon. Thanks all involved!
I don't see the lack of documentation: I think https://vite-plugin-ssr.com/client-only-components and https://vite-plugin-ssr.com/dynamic-import covers it.
I'm unsure about my ability to provide a PR soon, but the current Statement of
UI Frameworks usually don't execute import() upon SSR (this is, for example, the case with React and Vue).
is untrue, as the code will be loaded in SSR mode. Especially, because the default renderer includes rendering everything to a string.
Using the Client Only Component does fix that issue though, but may make migration of an existing app (using a similar, but inferior Implementation of the renderer) somewhat more painful than these statements may make one believe. It's by no means challenging.
Which is why, i linked the above issues together for better visibility :)
Вариант решения для Quasar 2, vue 3
<template>
<q-no-ssr> <!-- Важно -->
<YaMap />
</q-no-ssr>
</template>
<script>
components: {
YaMap: defineAsyncComponent(() =>
import('components/modules/YaMap.vue') /* Важно */
)
},
</script>
@KirillOlegovichH Up for creating a full example?
@KirillOlegovichH Up for creating a full example?
Обновил ответ. Пример применим только для Quasar framework 2. Прошу прощение что не уточнил сразу
Closing as it's mostly done. Contribution welcome to create a repository example.
I don't see the lack of documentation: I think https://vite-plugin-ssr.com/client-only-components and https://vite-plugin-ssr.com/dynamic-import covers it.
A simple ClientOnly.vue can do the trick
<template>
<template v-if="isMounted"><slot /></template>
<template v-else><slot name="placeholder" /></template>
</template>
<script setup>
import { ref, onMounted } from 'vue'
const isMounted = ref(false)
onMounted(() => {
isMounted.value = true
})
</script>
and here's the one with Suspense if you have async setup in you component
<template>
<template v-if="isMounted">
<Suspense>
<slot />
</Suspense>
</template>
<template v-else><slot name="placeholder" /></template>
</template>
<script setup>
import { ref, onMounted, Suspense } from 'vue'
const isMounted = ref(false)
onMounted(() => {
isMounted.value = true
})
</script>
Here's how to use ClientOnly.vue
<template>
<ClientOnly>
<OnlyRenderOnClientComponent v-bind="someProps"/>
<template #placeholder>This is for SSR/SEO</template>
</ClientOnly>
</template>
@yuanoook Added to the docs a7dfce543c899487c49d4e9b5138704271633089.
@WolfgangDrescher What did you end up with? I'm looking for a <ClientOnly>
implementation that uses defineAsyncComponent()
.
Once Nuxt 3 was ready for production I started migrating to it. So I do not have an example for you with defineAsyncComponent
, sorry.
Reply to:
I was excited to use this component as it makes so much sense and straight forward. I wrapped my HJ29/vue3-tabs as so:
<ClientOnly>
<Tabs v-if="!ssr" v-model="selectedTab">
<Tab :val="'subject'" :label="'Subject'" :indicator="true"></Tab>
<Tab :val="'courses'" :label="'Courses'" :indicator="true"></Tab>
</Tabs>
<TabPanels v-model="selectedTab" :animate="true">
<TabPanel :val="'subject'">
<form @submit.prevent="onSubmit()">
<p>Make subject form</p>
</form>
</TabPanel>
<TabPanel :val="'courses'">
<p>List courses</p>
</TabPanel>
</TabPanels>
<template #placeholder>
<p>This is for SSR/SEO</p>
</template>
</ClientOnly>
but unfortunately for some reason, the Tabs component (which reads document
) is still run on server side despite the ClientOnly
wrapping it which should prevent that from happening. BUT, if I comment out the whole thing and add some static html code to save and render on live reload, then uncomment, save and live reload, it magically works temporarily until you refresh the page. I don't know what's going on and I have no Idea how to debug it.
For now what I did for my vue component is utilize the *page.client.vue
way of doing things. But this also presented an issue for me on my createSSRApp
where I used vue markRaw
. I get this Error: Object.defineProperty called on non-object
and I happen to fix it with lodash:
Page: _.isObject(pageContext.Page) ? markRaw(pageContext.Page) : pageContext.Page,
Pretty sure this is not the optimal/correct way of doing it but for now it works. Though I'm not happy with the warning sign of not having markRaw.
Here is the full context of the code. Need your insights on this:
async function onBeforeRender(pageContext: PageContextServer) {
const { app, store } = createApp(pageContext);
let err: unknown;
// Workaround: renderToString_() swallows errors in production, see https://github.com/vuejs/core/issues/7876
app.config.errorHandler = err_ => {
err = err_;
};
// Reference: https://vite-plugin-ssr.com/stream
const stream = renderToNodeStream(app);
const initialStoreState = store.state.value;
if (err) throw err;
return {
pageContext: {
initialStoreState,
stream,
},
};
}
import { App, createSSRApp, h, reactive, markRaw } from 'vue';
import { createMongoAbility } from '@casl/ability';
import { unpackRules } from '@casl/ability/extra';
import { abilitiesPlugin as casl } from '@casl/vue';
import { createPinia } from 'pinia';
import { setPageContext } from './usePageContext';
import { PageContext } from './types';
import { GuestLayout, InstructorLayout, StudentLayout } from '#root/layouts/index';
import _ from 'lodash';
export { createApp };
interface AppPageElement extends App<Element> {
changePage: (pageContext: PageContext) => void;
}
function createApp(pageContext: PageContext) {
let rootComponentContext: PageContext;
console.log('TEST mark', pageContext.Page)
const app = createSSRApp({
data: () => ({
Page: _.isObject(pageContext.Page) ? markRaw(pageContext.Page) : pageContext.Page,
pageProps: markRaw(pageContext.pageProps || {}),
Layout: markRaw(pageContext.exports.Layout || selectLayout(pageContext)),
}),
render() {
const renderLayoutSlot = () => h(this.Page, this.pageProps || {});
return h(this.Layout, {}, { default: renderLayoutSlot });
},
created() {
rootComponentContext = this;
},
}) as AppPageElement;
const store = createPinia();
app.use(store);
if (pageContext.ability) {
try {
const unpackedRules = unpackRules(pageContext.ability);
const ability = createMongoAbility(unpackedRules as any); // FIXME: lazy bypass
app.use(casl, ability, {
useGlobalProperties: true,
});
} catch (e) {
console.error('Failed to unpack ability. Error:\n', e);
}
}
// We use `app.changePage()` to do Client Routing, see `_default.page.client.js`
Object.assign(app, {
changePage: (pageContext: PageContext) => {
Object.assign(pageContextReactive, pageContext);
rootComponentContext.Page = markRaw(pageContext.Page);
rootComponentContext.pageProps = markRaw(pageContext.pageProps || {});
},
});
// When doing Client Routing, we mutate pageContext (see usage of `app.changePage()` in `_default.page.client.js`).
// We therefore use a reactive pageContext.
const pageContextReactive = reactive(pageContext);
setPageContext(app, pageContextReactive);
return { app, store };
}
function selectLayout(pageContext: PageContext) {
switch (pageContext.user?.role) {
case 'instructor':
return InstructorLayout;
case 'student':
return StudentLayout;
default:
return GuestLayout;
}
}
I am not sure how to also add what to render on server side for SEO similar to the feature of the ClientOnly
component on the #placeholder slot
. I tried adding an index.page.server.vue
alongside the index.page.client.vue
but what it does is that it ignores the .client.vue
component entirely and just uses the other one.
I wrote a little component that includes third party packages that cannot be rendered server side (
midi-player-js
andsoundfont-player
; they are using AudioContext and XMLHttpRequest). Following the docs the best way to solve this problem would be to use dynamic imports. So shouldn't it be enough to just usedefineAsyncComponent
because Vue wraps the component into a Promise?But if I use it like this the server still tries to render the component and throws
XMLHttpRequest is not defined
when runningnpm run dev
. With a condition on the component to only render it in browser it still failes withAudioContext is not defined
since the component gets loaded in SSR even if it's not displayed in the template.So I wrote a litte
<ClientOnly>
component:This seems to work better but I get an error:
Hydration completed but contains mismatches.
Is there a way to prevent this from happening?At the end the best way to do this for me was to add an additional component
AsyncMidiPlayer
:And to use it like this:
But since I have multiple components that cannot be interpreted server side I would like to have a better solution than writing a custom wrapper for each of them. I was not able to create a generic version of this because the import source needs to be specific (I ran into
The above dynamic import cannot be analyzed by vite.
). I'm sure there is a better solution for this. Did anyone find a better solution to this than me? It would be nice to extend the vite-plugin-ssr docs with a working vue 3 example.