当前位置: 首页 > news >正文

基于Koa实现的服务端渲染 ✅

前段时间刚写完毕业论文,现在一上来就是“基于”,哈哈。🤯 这篇文章持续更新,涉及到的技术栈是Koa、Vue和Vite (用React手搓服务端渲染好麻烦)。但是现在能上生产的服务端渲染估计是Next(配合React)和Nuxt(配合Vue)用的比较多。关于Next框架的学习见煮啵的另一篇文章,也将持续更新。

目录

1️⃣ 最基本的服务端渲染
2️⃣ Koa配合Vue
3️⃣ Koa配合Vue和Vite


最基本的服务端渲染

懒得BB,直接上代码:

// koa-pro/demos/basic_ssr.js
export default async (ctx) => {ctx.type = 'html';ctx.body = `<!DOCTYPE html><html><head><title>Hello</title></head><body><h1>Hello World</h1></body></html>`;
}// koa-pro/index.js
import Koa from 'koa';
import Router from '@koa/router';
import basicSSR from './demos/basic_ssr.js'const koa = new Koa();
const router = new Router();router.get('/basic_ssr', basicSSR)
koa.use(router.routes());
koa.listen(3001, () => console.log('Server is running on port 3001'))

这串代码我们平时根本不屑一顾,但他却实现了最基本的SSR。因为它在Koa服务端处理了一个 HTTP 请求,并返回了一段完整的 HTML 内容。


Koa配合Vue

Koa配合Vue来实现SSR,一开始煮啵是跟着Vue3官方文档的教程走的,但是它提供的Demo上有大坑 (也可能不是坑,是因为煮啵技术不行),煮啵只介绍自己实现的过程。首先我们将创建Vue应用的逻辑封装在自定义的createApp函数中:

// koa-ssr/scripts/vue_ssr/app.js
const isBrowser = typeof window !== 'undefined' && typeof window.document !== 'undefined';
export default async function createApp() {let createSSRApp; // Vue提供的创建SSR应用的APIif (isBrowser) {createSSRApp = (await import("https://unpkg.com/vue@3.5.13/dist/vue.esm-browser.js")).createSSRApp;} else {createSSRApp = (await import('vue')).createSSRApp;}// 创建我们自己的应用return createSSRApp({data: () => ({ count: 1 }),template: `<button @click="count++">{{ count }}</button>`,});
}

createApp这个函数的封装在Vue3官方文档里也有,但最大的区别就是官方文档里是直接用import createSSRApp from ‘vue’,而此处却需要根据当前JS代码所处的运行环境(Node或浏览器)来动态的引入createSSRApp这个玩意。为什么这样做见下文分析。

其次我们需要处理HTML模版,并用Koa的路由来挂载:

// koa-ssr/demos/vue_ssr.js
import { renderToString } from '@vue/server-renderer';
import createApp from '../scripts/vue_ssr/app.js';
export default async (ctx) => {const app = await createApp()const html = await renderToString(app);ctx.type = 'html';ctx.body = `<!DOCTYPE html><html><head><title>Vue SSR</title></head><body><div id="app">${html}</div><script type="module" src="/vue_ssr/client.js"></script></body></html>`
}// koa-ssr/index.js
// ...已有内容省略,见上一处代码块。在已有的基础上添加下面这些代码⬇️:
import vueSSR from './demos/vue_ssr.js'
import koaStatic from 'koa-static';
import path from "path";
import { fileURLToPath } from "url";const filename = fileURLToPath(import.meta.url);
const dirname = path.dirname(filename);router.get('/vue_ssr', vueSSR)
koa.use(koaStatic(path.join(dirname, 'scripts')));

此处,在模版中用script标签来加载脚本文件是必要的,否则SSR只返回了“空壳”,而无法提供任何交互。此外,用script标签来载脚本文件必须对外暴露(此处用的是koa-static)来实现。

为什么必须对外暴露?因为浏览器在拿到Koa返回的HTML后,会请求script的脚本文件,在这个Demo中浏览器发起的资源请求的URL即为:http://localhost:3001/scripts/vue_ssr/client.js。问题来了,如果没有用koa-static对外暴露,Koa便没有处理这个请求的逻辑,会返回404。

最后我们需要实现这个client.js脚本文件,客户端就是靠这个脚本来“接管”服务端返回的HTML。这个文件只在客户端执行

// koa-ssr/scripts/vue_ssr/client.js
import createApp from './app.js';
(async function activate() {const app = await createApp();app.mount('#app');
})()

此处最重要的便是app.mount(“#app”)这句代码,它的作用是在客户端激活Vue。

接着我们回答在封装createApp的时候遗留的问题:为什么需要根据JS代码的运行环境来动态导入createSSRApp因为koa-ssr/scripts/vue_ssr/app.js这个文件是需要执行两次的,一次在客户端,一次在服务端。
在服务端执行的时候(即在koa-router收到http://127.0.0.1:3001/vue_ssr请求的时候),它负责将Vue组件渲染成HTML内容,然后发送到客户端。
在客户端执行的时候,此时浏览器会加载client.js文件,client.jsimpoetcreateApp这个函数。这个文件用于做Hydration(激活)操作,将Vue组件的行为附加到已经渲染的HTML上
但问题是,app.js在客户端执行的时候,如果代码为import { createSSRApp } from “vue”是会报错的,在浏览器中运行时无法解析“vue”模块,因为浏览器并不像Node或Vite开发环境那样有“模块解析系统”,它无法直接识别“vue”是个什么路径。


Koa配合Vue和Vite

既然用到Vite,也算是半只脚踏入“工程化”的大门了。但是Vite大多数情况下我们会用来开发一个SPA应用,此处可以抛出一个困扰了煮啵很久的一个问题:考虑到如果使用了SSR服务端渲染,那么每次切换浏览器的导航,是否都需要服务端都需要根据请求来生成HTML页面并返回给客户端?那是否可以认为用Vite构建的SSR应用是无法实现SPA应用的?或者说这两者一定是对立的?

答案当然是否定的。SSR和SPA并不是完全对立的,两者常常结合使用,可以大致分为以下两个步骤:

  1. 服务端渲染:在第一次访问时,Vite会生成HTML并返回给浏览器。这些 HTML 包括了从服务器渲染的数据。
  2. 客户端接管 (Hydration):一旦页面加载完成,Vue、React或其他前端框架会在客户端 “接管” 这个页面,即把页面变成一个SPA。浏览器加载应用的脚本代码,绑定事件,并且使得页面可以进行客户端路由和状态管理。

总的来说,SPA和SSR的结合我们可以认为是先SSR,然后由客户端接管为SPA的过程。且煮啵始终认为,再复杂的应用,只要是SPA应用,服务端渲染便往往只发生在首页渲染的时候(这个“首页”可以是SPA应用中的任意一个页面)。借助Vite官网提供的教程,我们可以快速搭建一个基于Vite的Vue&SSR应用:

// pro/src/entry-client.js
// 客户端入口文件,浏览器靠这个文件在客户端激活Vue,接管服务端返回的HTML
import { createApp } from './main'
const { app } = createApp()
app.mount('#app')// pro/src/entry-serve.js
// 服务端入口文件,返回的stream用于构建HTML文件
import { renderToWebStream } from 'vue/server-renderer'
import { createApp } from './main'
export function render() {const { app } = createApp()const ctx = {}const stream = renderToWebStream(app, ctx)return { stream }
}// pro/src/main.js
// 创建应用实例。此文件在客户端和服务端各执行一次,具体原因见《Koa配合Vue》中的分析。
import { createSSRApp } from 'vue'
import App from './App.vue'
import router from './router/index'
export function createApp() {const app = createSSRApp(App)app.use(router)return { app }
}// pro/src/App.vue
<template><div>Hello World!</div><li><router-link to="/home">首页</router-link></li><li><router-link to="/user">个人中心</router-link></li><div><router-view></router-view></div>
</template>// pro/src/router/index.js
// 路由文件
import { createRouter, createMemoryHistory, createWebHistory } from 'vue-router'
import Home from '../views/home.vue'
import User from '../views/user.vue'
const isSSR = typeof window === 'undefined'
const routes = [{path: '/home',component: Home,name: 'home',meta: { ssr: true }},{path: '/user',component: User,name: 'user',meta: { ssr: true }},
]
const router = createRouter({// 此处必须判断当前所处环境时服务端还是客户端。// 因为createWebHistory的实现依赖于浏览器全局对象window// 而在服务端渲染的时候是没有window的history: isSSR ? createMemoryHistory() : createWebHistory(),routes,
})
export default router

主要的文件就是上面这几个了。之后每次进入前端应用的时候,浏览器发送GET请求得到的HTML文件中的内容都不再是只有一个id为app的div那么简单,而是会将App.vue中静态的内容渲染出来。但是有一个问题:在SSR的时候能不能获取数据库中的数据来填充HTML文件中的内容,然后再返回给客户端?

如果是客户端渲染,我们通常会在onmount这个生命周期中做响应操作,但是onmount中的副作用永远属于客户端行为,它只有在客户端接管HTML后才能发生作用。
但是,这种需求在Next中实现起来很简单:实现getServerSideProps即可。且在Nuxt中估计也是类似的。框架永远有一个好处就是帮我们封装了很多很繁琐的东西,再向上暴露有限的接口。
但是如果硬要用Vite来操作,煮啵现在并没找到很好的方法🤯。唯一能想到的就是从pro/src/entry-server.js或者pro/server.js这两个文件中切入来实现,后续找到方法再来分享。

还有一个问题:既然配合了vue-router使用,那能不能在首次进入页面的时候,让SSR返回的HTML内容中包含当前路径映射到的组件,而不仅仅是一个App.vue的内容?
在Vite最基础的框架上煮啵也没找到方法🤯,尝试过在定义路由表的时候给每个路由选项都添加meta: { ssr: true }这样的配置,但没有生效,之后再找方法吧。但是话又说回来了🤓在Next中这样的功能是内置的,框架已经帮你搞定了。

相关文章:

  • Linux——虚拟地址空间
  • Cribl 数据脱敏 更多方法 MASK (三)
  • C++使用accumulate函数对数组进行快速求和
  • DBeaver虚拟主键会影响实际的数据库吗
  • 《AI大模型应知应会100篇》第41篇:多轮对话设计:构建高效的交互式应用
  • VM虚拟机安装CentOS7.9
  • spring-cloud-alibaba最新版本聚合项目创建
  • 理解计算机系统_网络编程(6)_web服务器
  • 完美中国制度流程体系建设(70页PPT)(文末有下载方式)
  • 拉宾公钥密码算法实现
  • Dubbo(88)如何设计一个跨地域的Dubbo服务?
  • Leetcode刷题记录24——最大子数组和
  • 在 Modal 平台上高效部署 DeepSeek 模型:从环境准备到实战案例
  • 小白dockerfile
  • 数字智慧方案5972丨智慧农业大数据平台解决方案(65页PPT)(文末有下载方式)
  • 协议(消息)配置
  • ctfshow web入门 web44
  • 如何用AI生成生成个人简历
  • 2025深圳杯、东三省数学建模B题数模AI全网专业性第一
  • MATLAB R2024a安装教程
  • 十四届全国人大常委会举行第四十四次委员长会议
  • 融创服务全面退出彰泰服务集团:约8.26亿元出售广西彰泰融创智慧80%股权
  • 来伊份一季度净利减少近八成,今年集中精力帮助加盟商成功
  • 历史新高!上海机场一季度营收增至31.72亿元,净利润增34%
  • 庆祝中华全国总工会成立100周年暨全国劳动模范和先进工作者表彰大会隆重举行,习近平发表重要讲话
  • 美加征“对等关税”后,调研显示近半外贸企业将减少对美业务