Skip to content

SSR 实现

vite 示例

原理:构建出客户和服务端,通过 node 运行服务端,将服务端渲染后的 html 返回给客户端,同时激活客户端的代码

在 vue3 中, setup 函数可以在服务端执行到,await 异步组件也可以被等待,因此拿到的 html 是完整的。 如果是只需要在客户端进行的逻辑,可以在 onMounted hook 中执行,避免 setInterval 等带副作用的函数滥用,因为服务端也会执行该代码,但是服务端没有 onUnMounted hook,因此该副作用将无法被清除(客户端不影响)

数据的获取: 通过服务端的 ctx 传递要获取的数据,并通过字符串替换的方式注入到 window 中,然后客户端判断有无该数据从而决定是使用还是执行请求

typescript
import { ref, useSSRContext } from 'vue'
import { SSR_CTX } from '~/types/comm'

function createSSRProvide() {
    const _map = new Map<string, any>()

    async function SSRRef<T>(SSRKey: string, generator?: () => Promise<T>) {
        if (_map.has(SSRKey) && generator)
            throw new Error(`SSR_Key: ${SSRKey} already exists, please use another unique string`)
        else _map.get(SSRKey)

        const reactiveValue = ref<T>()
        if (import.meta.env.SSR) {
            const context = useSSRContext<SSR_CTX>()!
            const data = generator ? await generator() : context.injectData[SSRKey]
            generator && Reflect.set(context.injectData, SSRKey, data)
            reactiveValue.value = data
        }
        else {
            const data = window.__INJECT_DATA__[SSRKey] || _map.get(SSRKey) || (await generator!())
            !_map.has(SSRKey) && _map.set(SSRKey, data)
            reactiveValue.value = data
        }

        return reactiveValue
    }
    return { SSRRef }
}

export const { SSRRef } = createSSRProvide()

useSSRContext 用法

javascript
// entry-server.ts
const ctx = { name: 'rick' };
const html = await renderToString(app, ctx);
// App.vue
const ctx = useSSRContext();
const message = ctx?.name ? ctx.name : "message";
// 这里可以拿到 ctx 的上下文,然后进行逻辑处理

pinia 使用

entry-client.ts
javascript
import { createApp } from './main'

const { app, pinia } = createApp()
window.__pinia && (pinia.state.value = window.__pinia) // pinia 会自动将数据进行合并(有些可以,但有些不行,所以要保证数据来源都能一一对应,这样才不会出现意料外的情况) __pinia = { userStore: { state: { name: 'rick' } } }
entry-server.ts
typescript
import { getMessage } from '@/api'
import { renderToString } from 'vue/server-renderer'
import { createApp } from './main'

export async function render(url: string, ssrManifest: string) {
    const { app, pinia } = createApp()
    const ctx = {}
    const html = await renderToString(app, ctx)
    pinia.state.value.userStore.userInfo = await getMessage() // 修改 store 中的数据
    const __pinia = JSON.stringify(pinia.state.value) // 将 store 数据进行序列化

    return { html, __pinia }
    // server.js 将 __pinia 渲染到 index.html 中
    // template.replace(`<!--__pinia-->`, `window.__pinia = ${rendered.__pinia ?? ''}`)
}