快速集成一个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 ''
  }
}
相关推荐
靠近彗星10 分钟前
Django:构建高性能Web应用
前端·python·django·sqlite
顺凡26 分钟前
深入剖析 Browser Use:49.9K+ Star 的 AI 驱动浏览器自动化框架
前端·aigc·测试
沐土Arvin43 分钟前
深入 SVG:矢量图形、滤镜与动态交互开发指南
开发语言·前端·javascript·css·html
IT、木易1 小时前
如何利用 CSS 的clip - path属性创建不规则形状的元素,应用场景有哪些?
前端·css
2501_906800761 小时前
低代码配置式组态软件-BY组态
前端·后端·物联网·低代码·数学建模·web
梦想家加一1 小时前
elementUI el-image图片加载失败解决
javascript·vue.js·elementui
新中地GIS开发老师1 小时前
快速入门 JSON 数据格式
javascript·信息可视化·json·gis开发·地理信息科学
海盗强1 小时前
vue子组件生命周期的执行顺序
前端·javascript·vue.js
活宝小娜2 小时前
在Vue 3 + TypeScript + Vite 项目中安装和使用 SCSS
vue.js·typescript·scss