项目完成小结:使用Blazor和gRPC开发大模型客户端
前言
先介绍下这个项目。
最近我一直在探索大语言模型,根据不同场景训练了好几个模型,为了让用户测试使用,需要开发前端。
这时候,用 Gradio 搭建的前端是不太够的,虽说 GitHub 上也有一堆开源的 ChatGPT 前端,但我看了一圈,并没有找到便于二次开发定制的,再一想,这么简单的功能,自己做就好啦,何必去 GitHub copy 呢?
那么就直接开始吧~
一开始我打算用 React 来做前端,然后使用 websocket 或者 eventsource 来实现聊天的打字机效果,但转念一想,不是有 Blazor Server 吗,这东西本来就和服务器建立了长链接,根本不需要折腾,于是决定试试。
先看效果
PC 端
| 主页 | 聊天界面 |
|---|---|
![]() |
![]() |
移动端
| 主页 | 聊天界面 |
|---|---|
![]() |
![]() |
项目设计
项目架构图

后端使用 gRPC 与各个模型的服务连接,然后使用 Blazor 来实现聊天功能。
关于 gRPC 的使用,在之前写的这篇博客: Asp-Net-Core学习笔记:gRPC快速入门
关于 Blazor
其实几年前我就有轻度使用了一下 Blazor 这个技术,详见文章: Asp-Net-Core学习笔记:4.Blazor-WebAssembly入门
一开始使用 Blazor ,我是有点嫌弃的,我还是比较倾向于传统的前后端分离,AspNetCore用来做后端,用 React 做前端,生态很丰富,要做啥组件都容易。
Blazor 有几个痛点:
- 每次对界面的一点小修改都需要重新编译才能看到效果
- 生态比较贫瘠,好用的组件库还比较少
- 无论 WebAssembly 还是 server rendered 模式,都无法直接操纵 DOM,需要通过 JSRuntime
所以这个项目一开始,我好几次产生了放弃 blazor ,重新用 React 实现的想法,不过随着熟练度提高,反而渐渐觉得,Blazor 好像也不错,开发小应用的效率挺高的。
这几个痛点虽然影响体验,但也不是不能忍受
- 每次都要编译,是慢了点,那就改完界面休息一下 需要反复修改的样式,直接在浏览器的开发者工具里面调整,调到满意再写到代码里
- 生态贫瘠问题也不大,Blazor 本质上还是前端,虽然 Blazor 原生组件少,但我可以自己写啊,前端生态无所不有,大多数直接拿来改改就可以用了
- 无法操作 DOM 是比较麻烦,只能多写点 JavaScript 来调用,Blazor 也并不是替代 JS,现在不会 JS 还是没办法做前端的
所以这个项目就这么磕磕绊绊的,边学 Blazor 边做,搞定了,效果竟然还可以
事实证明,Blazor (Server) 用在小项目上还是可以的,Server 模式的长链接可以做太多事了,数据交互这一块可以节省很多精力。
前端依赖
使用内置模板创建 Blazor 的项目,静态文件直接是附带在 wwwroot/lib 目录下
这个还是不趁手,我习惯用 npm 管理静态资源,第三方的前端依赖不添加到版本管理中
可以参考之前的博客: Asp-Net-Core开发笔记:使用NPM和gulp管理前端静态文件
本项目中,我使用了这些依赖
"dependencies": {
"@fortawesome/fontawesome-free": "^6.0.0",
"admin-lte": "^3.2.0",
"bootstrap": "^4.6.2",
"open-iconic": "^1.1.1"
}
然后发现 open-iconic 其实不怎么好看,还是 FontAwesome 好一点。
Blazor实现打字机效果
聊天界面需要实现类似 ChatGPT 的打字机效果
这就得用到流式输出,看了下 ChatGPT 的实现是 EventSource,不过我们用 Blazor Server 就不用考虑这些麻烦的数据交互问题了。
书接上回的 gRPC 调用,定义为服务端流式输出,然后我又在 Blazor 项目里封装了一个 ChatService
用生成器的方式,返回一个 IAsyncEnumerable 对象。(详见第三个参考资料)
public async IAsyncEnumerable<string> StreamingChat(string prompt) {
using var call = ClientRoute(prompt).StreamingChat(GetRequest(prompt));
await foreach (var resp in call.ResponseStream.ReadAllAsync()) {
yield return RenderText(resp.Response);
}
}
在 Blazor 组件里调用的时候,只需要用 await foreach 搭配 StateHasChanged(),就可以实现打字机效果了
await foreach (var resp in ChatService.StreamingChat(input.Content)) {
output.Content = resp;
StateHasChanged();
}
PS:我们 C# 实在是太好用啦~
DOM操作 (JS交互)
Blazor 无论是 server 模式,还是 WebAssembly 模式,都不能直接操作 DOM。
所以直接借助 JS,Blazor 提供了不错的 JS 互操作能力
我这里用到了,聊天界面自动滚动到页面底部的操作
首先写一个 js 函数
function scrollToEnd(elem) {
elem.scrollTop = elem.scrollHeight
}
一开始我看官网文档,JS 文件是可以和组件放在一起的
比如我的聊天组件,放在项目中的 Pages/Chat.razor 文件
然后 css 文件,是在 Pages/Chat.razor.css
它会自动把这俩关联起来
Pages/_Layout.cshtml 有一个 css 引用
<link href="AIHub.Blazor.styles.css" rel="stylesheet"/>
在 build 的时候,编译器会把所有的css都放在这个文件里面
但对于 js 并不是这样
在 Debug 模式运行,并不会自动复制 JS 文件
只有 Release 模式 publish 的时候,才会把 wwwroot 复制到发布目录里
然后还需要在 Blazor 组件里面手动引入 这个 JS Module
var module = await JS.InvokeAsync<IJSObjectReference>("import", "./Pages/Chat.razor.js");
之后通过这个 module 来执行 JS 调用。
但这样调试的时候是不会复制的,拿不到 JS 很不方便。
所以我最终没有采用这种方式,而是直接把 JS 放到 wwwroot/js 目录下面
然后在 Pages/_Layout.cshtml 里面引用
<script src="js/common.js"></script>
最后在 Blazor 组件里面使用就很简单了
聊天组件自动滚动到底部
前面说了 JS 互操作
现在可以在 Blazor 组件里面调用 JS 的方法来滚动到页面底部了
先定义个 ref (感觉和 vue 有点像)
private ElementReference _chatMessagesRef;
然后在元素上绑定
<div class="chat-messages" @ref="_chatMessagesRef"></div>
最后调用 JS 方法,把这个 ref 作为参数传入
await foreach (var resp in ChatService.StreamingChat(input.Content)) {
output.Content = resp;
StateHasChanged();
await Js.InvokeVoidAsync("scrollToEnd", _chatMessagesRef);
}
搞定。
PS:和 Vue 真的太像了。
页面自适应
聊天界面需要同时适配电脑版和手机版
参考 Bootstrap 的自适应设计
电脑版有侧边栏,高度可以吃满,但宽度得减去左侧栏的宽度
.chat-messages {
--large-width: calc(100vw - 250px - 70px);
max-width: var(--large-width);
min-width: var(--large-width);
}
手机版没有侧栏,变成了顶栏,宽度吃满,高度减去顶栏高度
@media screen and (max-width: 992px) {
:root {
--mobile-width: calc(100vw - 0);
--mobile-height: calc(100vh - 1.2rem - 50px);
}
.chat-wrapper {
height: var(--mobile-height);
}
.chat-messages {
max-width: var(--mobile-width);
min-width: var(--mobile-width);
}
.chat-controls {
--mobile-width: calc(100vw - 0);
max-width: var(--mobile-width);
min-width: var(--mobile-width);
}
}
Breakpoint 我只用了一个 992px ,相当于 Bootstrap 的 lg
这里是 Bootstrap 的宽度定义,可以参考一下。
| Breakpoint | Class infix | Dimensions |
|---|---|---|
| Extra small | None | <576px |
| Small | sm |
≥576px |
| Medium | md |
≥768px |
| Large | lg |
≥992px |
| Extra large | xl |
≥1200px |
| Extra extra large | xxl |
≥1400px |
PS:最后觉得,移动端还是单独做一个好了,自适应实在是别扭。我突然想到之前用 Flutter 做的 App,打包成 HTML 版本,竟然体验也还不错。
Blazor 组件封装
初步用起来很简单
比如首页的九宫格按钮,我就封装了一个组件
<div class="col-xl-2 col-lg-3 col-md-4 col-6 mt-2 mb-2">
<a class="btn btn-outline-dark btn-block" href="@Href" target="@Target" style="border-color: rgba(52,58,64,.8)">
<div class="mt-2" style="">
<i class="@IconClass" style="font-size: 6em;color: #bdc6d0;"></i>
</div>
<div class="mt-2">@Title</div>
</a>
</div>
@code {
[Parameter]
public string? IconClass { get; set; }
[Parameter]
public string? Href { get; set; }
[Parameter]
public string? Target { get; set; }
[Parameter]
public string? Title { get; set; }
}
使用很简单
<DialButton IconClass="oi oi-chat" Title="大语言模型" Href="chat"/>
搞定~
小结
这次只是个小 Demo 项目,试用了一下 Blazor ,从一开始的非常别扭,到越来越顺手
感觉 Blazor Server 写小项目还是挺好用的,后面继续完善项目,持续发掘 Blazor 的能力,到时再继续更新博客~
参考资料
- https://learn.microsoft.com/zh-cn/aspnet/core/grpc/
- 响应式设计:根据不同设备引不同css样式 - https://www.cnblogs.com/supe/p/6692379.html
- https://learn.microsoft.com/en-us/archive/msdn-magazine/2019/november/csharp-iterating-with-async-enumerables-in-csharp-8#a-tour-through-async-enumerables
- https://learn.microsoft.com/zh-cn/aspnet/core/blazor/
项目完成小结:使用Blazor和gRPC开发大模型客户端的更多相关文章
- 【.NET6】gRPC服务端和客户端开发案例,以及minimal API服务、gRPC服务和传统webapi服务的访问效率大对决
前言:随着.Net6的发布,Minimal API成了当下受人追捧的角儿.而这之前,程序之间通信效率的王者也许可以算得上是gRPC了.那么以下咱们先通过开发一个gRPC服务的教程,然后顺势而为,再接着 ...
- 无插件的大模型浏览器Autodesk Viewer开发培训-武汉-2014年8月28日 9:00 – 12:00
武汉附近的同学们有福了,这是全球第一次关于Autodesk viewer的教室培训. :) 你可能已经在各种场合听过或看过Autodesk最新推出的大模型浏览器,这是无需插件的浏览器模型,支持几十种数 ...
- 通过Blazor使用C#开发SPA单页面应用程序(3)
今天我们来看看Blazor开发的一些基本知识. 一.Blazor组件结构 Blazor中组件的基本结构可以分为3个部分,如下所示: //Counter.razor //Directives secti ...
- gRPC学习之二:GO的gRPC开发环境准备
欢迎访问我的GitHub 这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos gRPC学习系列文章链接 在CentOS7部署和设置G ...
- gRPC学习之三:初试GO版gRPC开发
欢迎访问我的GitHub https://github.com/zq2599/blog_demos 内容:所有原创文章分类汇总及配套源码,涉及Java.Docker.Kubernetes.DevOPS ...
- Blazor 组件库开发指南
翻译自 Waqas Anwar 2021年5月21日的文章 <A Developer's Guide To Blazor Component Libraries> [1] Blazor 的 ...
- 用antd和webview打造一款大数据客户端程序
要想提高工作效率,必须得有好的工具.大数据有很多组件,但是邪了门儿的就是,竟然没有一个好用的客户端程序. 没办法,我只好用antd+webview自己做了一款跨平台的桌面应用. 先看下效果. 这是gi ...
- C# 开发Modbus Rtu客户端 modbus测试Demo,Modbus 串口通信 , 虚拟MODBUS-RTU测试
前言 本文将使用一个NuGet公开的组件技术来实现一个ModBus RTU的客户端,方便的对Modbus rtu的服务器进行读写,这个服务器可以是电脑端C#设计的,也可以是PLC实现的,也可以是其他任 ...
- PowerDesigner 学习:十大模型及五大分类
个人认为PowerDesigner 最大的特点和优势就是1)提供了一整套的解决方案,面向了不同的人员提供不同的模型工具,比如有针对企业架构师的模型,有针对需求分析师的模型,有针对系统分析师和软件架构师 ...
- python 使用模板模式和工厂模式的混合设计开发各种邮件客户端发送邮件
1.使用模板模式和工厂模式的混合设计开发各种邮件客户端发送邮件. 2.模板模式的目的:能保证快速开发各种邮箱客户端,子类只需要重写模板类邮箱的抽象方法即可.之后再开发任何邮箱就只要加一个类,写3行代码 ...
随机推荐
- RDIFramework.NET敏捷开发框架助力企业BPM业务流程系统的开发与落地
现如今,很多企事业单位集团都自己有一套独特严密的业务生产经营流程,各个环节紧密相连.前后对应,一旦某个环节疏忽,整个流程就会出现问题.如何保证业务流程的标准化和规范化运营.减少人为差错,这就需要用到B ...
- odoo 开发入门教程系列-模块交互
模块交互 在上一章中,我们使用继承来修改模块的行为.在我们的房地产场景中,我们希望更进一步,能够为客户生成发票.Odoo提供了一个开发票模块,因此直接从我们的房地产模块创建发票是很简单的,也就是说,一 ...
- Ajax 方法返回值无效
遇到错误为再ajax 中返回数据不起作用 原来是因为在阿贾克斯success中不能直接return 需要执行完再进行返回 以下代码为正确代码 function TestAction(id ...
- TENGSHE-OS-渗透测试系统-win11版
下载ISO文件 创建新的虚拟机 VM17 已支持直接创建 win11 x64 稍后安装系统 选中win11 修改路径 win11需要设置8位加密密码 勾选安全引导 根据自身情况选择 默认即可 150G ...
- TypeScript 引用资源文件后提示找不到的异常处理
在tsx中引用图片,在文件文本编辑器中提示错误引用: typescript无法识别非代码文件(js是可以的).如果需要在ts中识别此文件资源,可以先声明文件类型. 新建一个ts文件,比如global. ...
- [Tensorflow]模型持久化的原理,将CKPT转为pb文件,使用pb模型预测
文章目录 [Tensorflow]模型持久化的原理,将CKPT转为pb文件,使用pb模型预测 一.模型持久化 1.持久化代码实现 convert_variables_to_constants固化模型结 ...
- Java 网络编程 —— 创建多线程服务器
一个典型的单线程服务器示例如下: while (true) { Socket socket = null; try { // 接收客户连接 socket = serverSocket.accept() ...
- 【Azure Developer】Azure AD 注册应用的 OAuth 2.0 v2 终结点获取的 Token 解析出来依旧为v1.0, 这是什么情况!
问题描述 使用 Azure AD 注册应用 Oauth2 v2.0的终结点(OAuth 2.0 token endpoint (v2):https://login.partner.microsofto ...
- 2022-10-28:以下go语言代码输出什么?A:false false;B:true false;C:true true;D:false true。 package main import “f
2022-10-28:以下go语言代码输出什么?A:false false:B:true false:C:true true:D:false true. package main import &qu ...
- 2021-08-04:给定一个字符串str,当然可以生成很多子序列。返回有多少个子序列是回文子序列,空序列不算回文。比如,str = “aba”,回文子序列:{a}、{a}、 {a,a}、 {b}、{
2021-08-04:给定一个字符串str,当然可以生成很多子序列.返回有多少个子序列是回文子序列,空序列不算回文.比如,str = "aba",回文子序列:{a}.{a}. {a ...



