Skip to content
0

SSR

Vite-SSR

一个典型的 SRR 应用应该有如下结构

  • src/
    • main.ts # 导出环境无关的(通用的)应用代码
    • entry-client.ts # 将应用挂载到一个 DOM 元素上
    • entry-server.ts # 使用某框架的 SSR API 渲染该应用
  • index.html
  • server.js # main application server

以 Vue 为例子而言,在平常的 SPA 应用中,我们是调用 vue 中暴露的 createApp,在 SSR 中,我们则需要调用 createSSRApp 方法,就是所谓的“激活”模式

// 该文件运行在浏览器中
import { createSSRApp } from 'vue'

const app = createSSRApp({
  // ...和服务端完全一致的应用实例
})

// 在客户端挂载一个 SSR 应用时会假定
// HTML 是预渲染的,然后执行激活过程,
// 而不是挂载新的 DOM 节点
app.mount('#app')

完整代码如下:

import { createSSRApp } from 'vue'
import App from './App.vue'

// SSR requires a fresh app instance per request, therefore we export a function
// that creates a fresh app instance. If using Vuex, we'd also be creating a
// fresh store here.
export function createApp() {
  const app = createSSRApp(App)
  return { app }
}
import './style.css'
import { createApp } from './main'

const { app } = createApp()

app.mount('#app')
import { renderToString } from 'vue/server-renderer'
import { createApp } from './main'

export async function render() {
  const { app } = createApp()
  // passing SSR context object which will be available via useSSRContext()
  // @vitejs/plugin-vue injects code into a component's setup() that registers
  // itself on ctx.modules. After the render, ctx.modules would contain all the
  // components that have been instantiated during this render call.
  const ctx = {}
  const html = await renderToString(app, ctx)

  return { html }
}

Dev 模式下 Vite SSR 的请求转换流程如下:

undefined
undefined

在 DEV 模式下,Vite 作为 express 的一个中间件使用,我们首先创建一个 Vite 实例

if (!isProduction) {
  const { createServer } = await import('vite')
  vite = await createServer({
    server: { middlewareMode: true },
    appType: 'custom',
    base
  })
  app.use(vite.middlewares)
}

在 express 的路由中,读取原始的 index.html 文件,并且通过 transformIndexHtml 进行转换原始 HTML 代码(运行带有转换 HTML 钩子的插件),得到转换后的 HTML 文件

然后调用 vite.ssrLoadModule ,将服务端的入口由 TS/ESM 转换为 CJS

// Always read fresh template in development
app.use('*', async (req, res) => {
  if (!isProduction) {
    template = await fs.readFile('./index.html', 'utf-8')
    template = await vite.transformIndexHtml(url, template)
    console.log({template})
    render = (await vite.ssrLoadModule('/src/entry-server.ts')).render
  }

  // 执行 render 方法,得到结果
  const rendered = await render(url, ssrManifest)
  const html = template
    .replace(`<!--app-head-->`, rendered.head ?? '')
    .replace(`<!--app-html-->`, rendered.html ?? '')

  // 返回带有内容的 html 模板(SSR)
  res.status(200).set({ 'Content-Type': 'text/html' }).end(html)
})

router 下的 SSR

实现起来也非常简单,在我们执行 renderToString 之前,通过切换 router ,来改变 APP 中的内容

import { createPinia } from 'pinia'
import { createSSRApp } from 'vue'
import App from './App.vue'
import { createRouter } from './router'

// SSR requires a fresh app instance per request, therefore we export a function
// that creates a fresh app instance. If using Vuex, we'd also be creating a
// fresh store here.
export function createApp() {
  const app = createSSRApp(App)
  const pinia = createPinia()
  app.use(pinia)
  const router = createRouter()
  app.use(router)
  return { app, router }
}
import { createApp } from './main.ts'
async function render(url, manifest) {
  const { app, router } = createApp()
  await router.push(url)
  await router.isReady()

  const ctx = {}

  const html = renderToString(app, ctx)
}

前端路由切换的时候,不会重新请求 HTML,而是执行 JS 改变路由以及元素,这正是现代 SSR 的好处:良好的首屏 SEO、页面切换也十分快速

vitejs/vite-plugin-vue/ssr-vue 例子,关于 CSS 的问题,没有提取在 HTML 的 HEAD 标签中,而是由 JS 动态加载的,但是不影响 SEO 功能

Released under the MIT License.