快速集成一个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 ''
  }
}
相关推荐
Avan_菜菜5 小时前
AI 能写代码了,为什么我反而开始要求它先写文档?
前端·github·ai编程
JieE2128 小时前
LeetCode 226. 翻转二叉树|JS 递归超详细拆解,二叉树入门经典题
javascript·算法
JieE2128 小时前
LeetCode 104. 二叉树的最大深度|递归思路超详细拆解
javascript·算法
爱勇宝9 小时前
鸿蒙生态的下半场:开发者不只要能开发,还要能赚钱
android·前端·程序员
IT_陈寒12 小时前
SpringBoot这个自动配置坑我跳了三次
前端·人工智能·后端
kyriewen12 小时前
我用 AI 一周写完了整个项目,上线第一天就崩了——这是我踩过最贵的 5 个坑
前端·javascript·ai编程
Larcher13 小时前
AI Loop:让AI像人一样自主完成任务的核心机制
javascript·人工智能·设计模式
默_笙13 小时前
🃏 JS 只有 8 种数据类型,但我花了 2 天才搞懂 null 和 undefined 的区别
javascript
牧艺13 小时前
从零到协同:构建类飞书在线文档系统的五个技术重难点
前端·人工智能
jump_jump13 小时前
流式 HTML:从 htmx 片段装配到浏览器原生增量渲染
javascript·性能优化·前端工程化