背景
vue的ssr通常用nuxt来做,但是有时候项目比较复杂,不仅需要承接ssr,还需要承接后端接口聚合、ab实验、多语言等业务,nuxt并不能很好地适配;老项目也有可能不是nuxt搭建的,而是基于webpack搭建的ssr项目;有时候因为某些原因,我们也不想按照nuxt的设计规范来,想按照团队自定义的规范来。本文讲的是快速集成一个express+vite+vue3的ssr项目模板
项目结构
bash
project
- client
- pages # 存放每个页面的代码
- about
- home
- routes # 路由
- App.vue
- entry-client.ts # 客户端入口
- entry-server.ts # 服务端入口
- main.ts
- server
- app.vite.ts
- vite
- vite-ssr.ts
- vite.config.ts
vite的一些配置
server/app.vite.ts
的源码为
ts
import express from 'express'
import { createServer } from 'vite'
import { viteSsrRender } from '../vite/vite-ssr'
const app = express()
const root = process.cwd()
async function startServer() {
const vite = await await createServer({
configFile: `${root}/vite/vite.config.ts`,
})
app.use(vite.middlewares)
app.get('*', async (req, res) => {
const html = await viteSsrRender(req, res, { vite })
res.status(200).set({ 'Content-Type': 'text/html' }).end(html)
})
app.listen(3000, () => {
console.log('Server is running at http://localhost:3000')
})
}
startServer()
createServer
方法可以让开发者通过代码方式启动vite开发服务器,而不是通过命令行运行vite,这样做可以更方便地集成到node服务端代码中(nestjs、express等),可以更灵活地修改覆盖vite配置,而不是依赖静态的配置文件。
vite.middlewares
用于拦截处理静态资源请求(如ts、vue文件)、支持hmr等。
vite.config.ts
ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
server: {
// 中间件模式,适合将vite集成到自定义node服务器中,而不是单独运行vite
middlewareMode: true,
},
// 不自动处理html文件,开发者需要手动控制html的生成渲染
appType: 'custom',
})
终端代码
entry-client.ts
作为客户端渲染/激活的入口文件
ts
import { createApp } from './main'
async function main() {
const { app, router } = createApp()
await router.isReady()
app.mount('#app')
}
main()
entry-server.ts
作为服务端渲染的入口文件
ts
import { createApp as _createApp } from './main'
export async function createApp(context: { url: string }) {
const { app, router } = _createApp()
if (!context.url) {
console.error('context.url is required')
}
router.push(context.url) // 此处注意,需要手动push
await router.isReady()
return { app }
}
main.ts
公共逻辑
ts
import { createSSRApp } from 'vue'
import App from './App.vue'
import { createRouterInstance } from './routes'
export function createApp() {
const app = createSSRApp(App)
const router = createRouterInstance()
app.use(router)
return { app, router }
}
createWebHistory
方法依赖浏览器的history api,而服务端是没有history api的,所以需要createMemoryHistory
方法
ts
import { createRouter, createMemoryHistory, createWebHistory } from 'vue-router'
export function createRouterInstance() {
// 判断是在服务端执行的,还是客户端执行的
const isServer = typeof window === 'undefined'
return createRouter({
history: isServer ? createMemoryHistory() : createWebHistory(),
routes: [
{ path: '/about',
component: () => import('../pages/about/App.vue'),
name: 'about'
},
{
path: '/',
component: () => import('../pages/home/App.vue'),
name: 'home',
},
],
})
}
ssr渲染
ts
import path from 'node:path'
import { renderToString } from 'vue/server-renderer'
const root = process.cwd()
export async function viteSsrRender (req, res, { vite }) {
try {
const entryServerPath = path.join(root, 'client/entry-server.ts')
// 动态加载服务端渲染的入口文件
const createApp = (await vite.ssrLoadModule(entryServerPath)).createApp
// 注入所需上下文数据并执行
const { app } = await createApp({ url: req.url })
const renderedHtml = await renderToString(app)
const html = `
<!DOCTYPE html>
<html>
<head>
<title>SSR App</title>
</head>
<body>
<div id="app">${renderedHtml}</div>
<script type="module" src="/client/entry-client.ts"></script>
</body>
</html>
`
return html
} catch (e) {
console.error(e)
return ''
}
}