快速集成一个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 ''
  }
}
相关推荐
有梦想的攻城狮2 分钟前
从0开始学vue:pnpm怎么安装
前端·javascript·vue.js
pzpcxy5204 分钟前
安装VUE客户端@vue/cli报错警告npm WARN deprecated解决方法 无法将“vue”项识别为 cmdlet、函数
前端·vue.js·npm
醉书生ꦿ℘゜এ4 分钟前
npm error Cannot read properties of null (reading ‘matches‘)
前端·npm·node.js
疯狂的沙粒19 分钟前
uni-app 如何实现选择和上传非图像、视频文件?
前端·javascript·uni-app
娃哈哈哈哈呀21 分钟前
html-pre标签
java·前端·html
$程22 分钟前
Uniapp 二维码生成与解析完整教程
前端·uni-app
Java永无止境24 分钟前
Web前端基础:HTML-CSS
java·前端·css·html·javaweb
安全系统学习41 分钟前
网络安全逆向分析之rust逆向技巧
前端·算法·安全·web安全·网络安全·中间件
花开月满西楼42 分钟前
保姆级【快数学会Android端“动画“】+ 实现补间动画和逐帧动画!!!
android·前端·android studio
KENYCHEN奉孝44 分钟前
基于 actix-web 框架的简单 demo
前端·rust