前言

说到用一种语言搞定前后端,大家应该首先想到的是 Node.js ,在不是特别追求性能的情况下,使用 Node.js 可以让前端非常轻松的编写前后端程序,降低了学习成本的同时,可以大量复用代码,快速完成开发。

当然这回咱不讨论 Node.js ,主写 Go + Flutter 的我,平时最常做的事情之一就是挂着 JSON to GoJSON to Dart 互相去转模型,然后去写一套服务器端代码,写一套客户端代码。脑子也要在 Go 和 Dart 之间切换。

那么,有没有什么方法在小项目里面简化一下开发流程呢,降低一点儿自己的心智负担呢?如果不限定 b/s 架构的话,之前我尝试过 Go + Fyne 来做,确实不用切换脑子了,但是用 Go 去写 GUI 是一件非常痛苦的事情,当然 Fyne 库是非常好用的(前提是不做复杂 GUI ,不涉及视频),而且不能追求界面太好看,只是“能用就行”。

今天,咱们就来看“更爽一点”的好东西:Serverpod ! 这套东西是用 Dart 写前后端,Dart 这个语言很奇妙,开发时是 JIT ,打包出来 AOT ,所以性能还是很不错的,感兴趣的话可以看一下 Dart 和 Go 的基础性能比较:Dart VS Go benchmarks, Which programming language or compiler is faster ,也可以在这个网站看下 Dart 和 Javascript 的比较

既然是 Dart + Flutter ,那么想做 b/s 或者 c/s 都可以了,那么下面就来试一试吧!

Tips:截至我写项目的时候,serverpod 版本在 2.0.1,写文的时候是 2.0.2 ,目前版本变动还是比较快,有一些功能还在实验性阶段,很多功能比较有限。所以个人建议只拿来写小工具的时候用一下,暂时还是不要上生产环境为好。

界面展示

后端

可以打包成二进制文件轻松在 Linux 跑起来,不需要装 Dart 环境,当然,官方推荐的部署方法是用容器来跑,提供了参考用的 GitHub Action 以及 Dockerfile ,所以在 K8s 里面使用也很简单

image-20240728222258220

前端

做的是 b/s ,Material Design 风,浏览器打开使用

image-20240728222832043

image-20240728222947676

目录结构

serverpod 的 CLI 会给我们直接创建好目录,一共 4 个

目录 说明
.github/workflows
yuuka_tools_version_manager_client 自动生成的代码,前端(客户端)公用 SDK
yuuka_tools_version_manager_flutter 客户端/前端代码( b/s 、c/s 均可)
yuuka_tools_version_manager_server 后端代码

代码生成

开发的大致顺序是:先写后端代码,写后端代码,先创建模型,之后执行代码生成,能自动生成类、数据库(只能使用 pgsql)迁移语句(支持版本管理),写 Endpoint 、再执行代码生成,生成 Dart SDK ,客户端开发

模型

lib/src/models 文件夹存放的是模型,文件需要以 [类名].spy.yaml 命名

示例如下,比较简单,不需要特别的注释也能看懂

class: YuukaApps

table: yuuka_apps

fields:
appIdentity: String
appName: String
developer: String
versionsNumber: int
newestVersion: String?
createdDate: DateTime
release: bool
documentLink: String?
appDescription: String?
indexes:
yuukaApps_appIdentity:
fields: appIdentity
unique: true

Endpoint

lib/src/endpotins 下,以 [类名]_endpoint.dart 命名

示例如下,扩展 Endpoint ,也是比较简单,不加注释也能看懂,操作数据库就是 [类名].db

class YuukaAppsEndpoint extends Endpoint {
Future<ServerGeneralReturn> getYuukaAppsList(Session session) async {
var serverReturn =
new ServerGeneralReturn(statusCode: 0, statusMessage: "");
try {
serverReturn.yuukaApps = await YuukaApps.db.find(
session,
orderBy: (t) => t.id,
);
if (serverReturn.yuukaApps != null) {
for (var app in serverReturn.yuukaApps!) {
app.versionsNumber = (await YuukaAppVersion.db.find(
session,
where: (t) => t.appIdentity.equals(app.appIdentity),
))
.length;
List<YuukaAppVersion>? resultForNewestVersion =
await YuukaAppVersion.db.find(session,
where: (t) =>
t.appIdentity.equals(app.appIdentity) &
t.publish.equals(true));
if (resultForNewestVersion.isNotEmpty) {
app.newestVersion = (await YuukaAppVersion.db.find(session,
where: (t) =>
t.appIdentity.equals(app.appIdentity) &
t.publish.equals(true)))
.last
.version;
}
}
}
serverReturn.statusCode = 200;
serverReturn.statusMessage = "获取所有应用列表成功";
return serverReturn;
} catch (e) {
serverReturn.statusCode = 500;
serverReturn.statusMessage = "获取应用列表失败,异常信息:$e";
return serverReturn;
}
}
}

鉴权

官方教程就提供了三种方式,邮件、Google OAuth、Apple OAuth,其中邮件方式改改就可以变成自定义后端鉴权,不让任意注册。

邮件方式客户端使用很简单:

// 直接引入以下 Widget
SignInWithEmailButton(
caller: client.modules.auth,
),

请求示例

引入 client 包,使用非常方便,不需要单独去写请求了

Future<void> _loadYuukaApps() async {
try {
SmartDialog.showLoading(builder: (_) => const CustomLoading(type: 0));
final serverReturn = await client.yuukaApps.getYuukaAppsList(); // 这里就是请求,看,非常简单!一行代码就搞定!
if (serverReturn.statusCode == 200) {
setState(() {
_yuukaApps = serverReturn.yuukaApps;
});
} else {
_connectionFailed(Exception(serverReturn.statusMessage)); // 这个是异常处理方法
commonErrorDialog(
context, "啊哦", "获取应用列表异常:${serverReturn.statusMessage}", "啊这");
}
} catch (e) {
_connectionFailed(e);
commonErrorDialog(context, "啊哦", "获取应用列表异常:$e", "啊这");
} finally {
SmartDialog.dismiss(status: SmartStatus.loading);
}
}

不需要关心 payload ,自动生成都搞定了~

实际 payload 如下,Endpoint 都是一个,使用 method 区分:

{auth: "xxx", method: "getYuukaAppsList"}

小结

使用 “ Dart 版 Node.js ” Serverpod ,可以非常轻松的搞定前后端,降低开发时语言切换的学习成本和心智负担,利用 Dart 全栈语言的特性和开发时 JIT 发版时 AOT 的特性,能够兼顾开发便捷性和运行时的高效。可以快速完成小型项目的开发,爽就完了!颇有一种 “人生苦短,我用 Dart 前后端一把梭!” 的感觉。不过只建议小打小闹使用,正式项目建议继续观望等生态更好一些了再考虑。