快速集成一个express+vite+vue3的ssr项目模板

背景

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 ''
  }
}
相关推荐
MaCa .BaKa8 分钟前
37-智慧医疗服务平台(在线接诊/问诊)
java·vue.js·spring boot·tomcat·vue·maven
ドロロ8061 小时前
element-plus点击重置表单,却没有进行重置操作
javascript·vue.js·elementui
海盐泡泡龟1 小时前
ES6新增Set、Map两种数据结构、WeakMap、WeakSet举例说明详细。(含DeepSeek讲解)
前端·数据结构·es6
t_hj2 小时前
Ajax案例
前端·javascript·ajax
bigHead-3 小时前
9. 从《蜀道难》学CSS基础:三种选择器的实战解析
前端·css
阿里小阿希3 小时前
解决 pnpm dev 运行报错的坎坷历程
前端·node.js
未脱发程序员3 小时前
分享一款开源的图片去重软件 ImageContrastTools,基于Electron和hash算法
前端·javascript·electron
geovindu4 小时前
vue3: pdf.js 2.16.105 using typescript
javascript·vue.js·typescript·pdf
视频砖家4 小时前
Web前端VSCode如何解决打开html页面中文乱码的问题(方法2)
前端·vscode·vscode乱码·vscode中文乱码·vscode中文编码
2401_837088504 小时前
CSS transition过渡属性
前端·css