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 功能