基于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中这样的功能是内置的,框架已经帮你搞定了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/81264.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Linux运维——Vim基础

Vim基础 一、移动光标1.1、基础移动1.2、屏幕滚动 二、编辑操作2.1、插入模式2.2、删除与修改2.3、复制粘贴 三、搜索与替换3.1、搜索3.2、替换 4、分屏与窗口管理4.1、分屏操作4.2、窗口调整 五、宏与批量操作六、效率技巧七、操作符7.1、内置操作符7.2、操作符 文本对象&…

git操作合集

更新文件 在 Git 中更新已经上传到仓库的文件 1、检查当前状态 首先&#xff0c;打开终端或命令行工具&#xff0c;进入你的 Git 仓库目录&#xff08;即包含 .git 文件夹的目录&#xff09;。运行以下命令来查看当前仓库的状态&#xff1a; git status 此命令会显示哪些文…

【笔记】深度学习模型训练的 GPU 内存优化之旅⑤:内存分配篇

开设此专题&#xff0c;目的一是梳理文献&#xff0c;目的二是分享知识。因为笔者读研期间的研究方向是单卡上的显存优化&#xff0c;所以最初思考的专题名称是“显存突围&#xff1a;深度学习模型训练的 GPU 内存优化之旅”&#xff0c;英文缩写是 “MLSys_GPU_Memory_Opt”。…

SQL Server 存储过程开发手册

SQL Server 存储过程开发手册&#xff08;更新版&#xff09; 根据要求&#xff0c;重新整理并加入了事务控制、异常日志记录和返回状态码的设计。以下是详细说明&#xff1a; 1. 总则 1.1 目标 本手册旨在为 SQL Server 存储过程的编写提供一套完整的规范&#xff0c;确保系…

深海科技服务博客简介

人人可学&#xff0c;人人可用&#xff0c;IT与AI不是高不可攀&#xff01; 博客宗旨 深海科技服务博客致力于&#xff1a; 推广IT与AI的实际应用&#xff0c;降低入门门槛&#xff0c;让更多个人和中小企业能够以最少投入、高效实现信息化、智能化。 分享开源免费软件、简单…

本地大模型编程实战(29)查询图数据库NEO4J(2)

上一篇文章 用大语言模型LLM查询图数据库NEO4J(1) 介绍了使用GraphQACypherChain查询NEO4J。用它实现简单快捷&#xff0c;但是不容易定制&#xff0c;在生产环境中可能会面临挑战。 本文将基于langgraph 框架&#xff0c;用LLM(大语言模型)查询图数据库NEO4J。它可以定义清晰复…

RPG_5.角色动画

1.创建一个动画实例 2.创建该实例的c子类 3.继续创建该类的子类&#xff0c;但是作用是用来链接&#xff08;以后会详细解释&#xff09; 4.基于PlayerAnimInstance类创建一个子类 5.目前一共创建了四个c类&#xff0c; 最基的类 角色的类 玩家控制的角色的类 玩家控制的角…

Sigmoid函数导数推导详解

Sigmoid函数导数推导详解 在逻辑回归中&#xff0c;Sigmoid函数的导数推导是一个关键步骤&#xff0c;它使得梯度下降算法能够高效地计算。 1. Sigmoid函数定义 首先回顾Sigmoid函数的定义&#xff1a; g ( z ) 1 1 e − z g(z) \frac{1}{1 e^{-z}} g(z)1e−z1​ 2. 导…

MS31860T——8 通道串行接口低边驱动器

MS31860T 是一款 8 通道低边驱动器&#xff0c;包含 SPI 串口通信、 PWM斩波器配置、过流保护、短路保护、欠压锁定和过热关断功能&#xff0c; 芯片可以读取每个通道的状态。MS31860T 可以诊断开路的负载情况&#xff0c;并可以读取故障信息。外部故障引脚指示芯片的故障状态。…

腾讯 Kuikly 正式开源,了解一下这个基于 Kotlin 的全平台框架

在 3月的时候通过 《腾讯 TDF 即将开源 Kuikly 跨端框架&#xff0c;Kotlin 支持全平台》 我们大致知道了 Kuikly 的基本情况&#xff0c;Kuikly 是一个面向终端技术栈的跨端开发框架&#xff0c;完全基于kotlin语言开发&#xff0c;提供原生的性能和体验。 按照官方的说法&…

AI驱动UI自动化测试框架调研

随着应用复杂度增加&#xff0c;手动测试变得费时且易出错&#xff0c;而自动化测试可提高效率和可靠性。如何借助大模型和一些自动化测试框架进行自动化测试&#xff0c;是一个研发团队很重要的诉求。 目前主流的自动化测试框架很多&#xff0c;Midscene.js结合Playwright提供…

关系型数据库设计指南

1. 前言 在自己独立开发一个项目的过程中&#xff0c;我发现了一些以往写小 Demo 从来没有遇到过的问题。 最近在独立制作一个全栈的通知管理平台。一开始我没有考虑太多&#xff0c;直接根据头脑中零星的想法就开撸后端数据库 model 和 API&#xff0c;用的是学了半成品的 M…

详解TypeScript中的类型断言及其绕过类型检查机制

TypeScript中的类型断言及其绕过类型检查机制 一、类型断言的本质与工作原理编译时与运行时的区别TypeScript编译器处理类型断言的步骤 二、类型断言的详细语法与进阶用法基础语法对比链式断言断言修饰符1. 非空断言操作符 (!)代码分析1. getLength 函数分析用法说明&#xff1…

XLSX.utils.sheet_to_json设置了blankrows:true,但无法获取到开头的空白行

在用sheetJs的XLSX库做导入&#xff0c;遇到一个bug。如果开头行是空白行的话&#xff0c;调用sheet_to_json转数组获得的数据也是没有包含空白行的。这样会导致在设置对应的起始行时&#xff0c;解析数据不生效。 目前是直接跳过了开头的两行空白行 正确应该获得一下数据 问…

PostgreSQL 数据库下载和安装

官网&#xff1a; PostgreSQL: Downloads 推荐下载网站&#xff1a;EDB downloads postgresql 我选了 postgresql-15.12-1-windows-x64.exe 鼠标双击&#xff0c;开始安装&#xff1a; 安装路径&#xff1a; Installation Directory: D:\Program Files\PostgreSQL\15 Serv…

一、Javaweb是什么?

1.1 客户端与服务端 客户端 &#xff1a;用于与用户进行交互&#xff0c;接受用户的输入或操作&#xff0c;且展示服务器端的数据以及向服务器传递数据。 例如&#xff1a;手机app&#xff0c;微信小程序、浏览器… 服务端 &#xff1a;与客户端进行交互&#xff0c;接受客户…

奇偶ASCII值判断

奇偶ASCII值判断 Description 任意输入一个字符&#xff0c;判断其ASCII是否是奇数&#xff0c;若是&#xff0c;输出YES&#xff0c;否则&#xff0c;输出NO。例如&#xff0c;字符A的ASCII值是65&#xff0c;则输出YES&#xff0c;若输入字符B(ASCII值是66)&#xff0c;则输…

OpenCV 图形API(74)图像与通道拼接函数-----合并三个单通道图像(GMat)为一个多通道图像的函数merge3()

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 从3个单通道矩阵创建一个3通道矩阵。 此函数将多个矩阵合并以生成一个单一的多通道矩阵。即&#xff0c;输出矩阵的每个元素将是输入矩阵元素的…

多节点监测任务分配方法比较与分析

多监测节点任务分配方法是分布式系统、物联网&#xff08;IoT&#xff09;、工业监测等领域的核心技术&#xff0c;其核心目标是在资源受限条件下高效分配任务&#xff0c;以优化系统性能。以下从方法分类、对比分析、应用场景选择及挑战等方面进行系统阐述&#xff1a; 图1 多…

【推荐系统笔记】BPR损失函数公式

一、BPR损失函数公式 BPR 损失函数的核心公式如下&#xff1a; L BPR − ∑ ( u , i , j ) ∈ D ln ⁡ σ ( x ^ u i j ) λ ∣ ∣ Θ ∣ ∣ 2 L_{\text{BPR}} - \sum_{(u, i, j) \in D} \ln \sigma(\hat{x}_{uij}) \lambda ||\Theta||^2 LBPR​−(u,i,j)∈D∑​lnσ(x^ui…