手摸手,使用Dart语言开发后端应用,来吧!

## 前言 这几天连续发了几篇关于 `Dart` 开发后端应用的文章,主要是介绍了 `Dart` 的一些优点,比如异步任务,并发处理,编译部署等等。 俗话说,光说不练假把式,今天我们来真正开始一个 `Dart` 后端应用。 ## 我们要开发什么应用 假设我们现在要开发一个社区应用,类似于`掘金`,`CSDN`等等,基本的功能是用户发文章,发观点。 发文章,类似于传统的CMS系统 发观点,类似于现在的微博系统 围绕核心,还有标签,分类,评论等等。 ## 我们用什么框架 既然打算使用 `Dart` 开发,有个开发框架还是有很大帮助的。 然而 `Dart` 的后端框架并不多,`aqueduct`, `jaguar`, `DartMars` 等等, 在这里,我们使用 `DartMars`。 源码在此 https://github.com/tangpanqing/dart_mars 文档在此 https://tangpanqing.github.io/dart_mars_docs/zh/ 打开文档首页,如此 ![微信图片_20210703095222.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6241c11482354afebd9a48c4e50469cd~tplv-k3u1fbpfcp-watermark.image) 嗯嗯,浓浓的 `vuepress` 味道。 ## 开始一个项目如此简单 根据`DartMars`的指引,在安装`Dart` 后,我们可以执行以下命令来创建项目 ```shell # 安装DartMars dart pub global activate --source git https://github.com/tangpanqing/dart_mars.git # 创建项目 dart pub global run dart_mars --create project_name # 进入目录 cd project_name # 获取依赖 dart pub global run dart_mars --get # 启动项目 dart pub global run dart_mars --serve dev ``` 手摸手,我们一步一步来 ### 第一步,安装DartMars 打开命令行工具,执行 ```shell dart pub global activate --source git https://github.com/tangpanqing/dart_mars.git ``` 感谢墙的存在,我等了将近1分钟,提示我如下: ```shell Activated dart_mars 1.0.4 from Git repository "https://github.com/tangpanqing/dart_mars.git" ``` 这就表示安装好了。 ### 第二步,创建项目 项目暂定名称 `community` 社区,执行如下命令 ```shell dart pub global run dart_mars --create community ``` 经过以上命令,`DartMars` 有了提示 ```shell project community has been created you can change dir with command: cd community and then get dependent with command: dart pub global run dart_mars --get and then start it with command: dart pub global run dart_mars --serve dev ``` 意思说,项目已经创建,接下来你需要进入目录,并且获取依赖,最后执行。 并且显示了相关命令,是不是很贴心? 谈恋爱的时候,一定是个暖男。 ### 第三步,进入目录 执行命令 ```shell cd community ``` ### 第四步,获取依赖 执行命令 ```shell dart pub global run dart_mars --get ``` 经过以上命令,`DartMars` 有了提示 ```shell Got dependencies! ``` 表示加载依赖完成 ### 第五步,启动项目 ```shell dart pub global run dart_mars --serve dev ``` 经过以上命令,`DartMars` 有了提示 ```shell route config file has been updated, see ./lib/config/route.dart $ dart run bin\community.dart --serve dev INFO::2021-07-03 10:14:13.601023::0::Server::Http Server has start, port=80 INFO::2021-07-03 10:14:13.608004::1::Server::Env type is dev INFO::2021-07-03 10:14:13.624571::2::Server::Open browser and vist http://127.0.0.1:80 , you can see some info ``` 启动成功,通过以上信息,我们可知: 1. 路由配置文件已经更新, 2. HTTP 服务已经开始,在80端口,目前使用的是开发环境 打开浏览器,访问 http://127.0.0.1:80 我们就看到了经典的 ```html hello world ``` ## 按部就班地继续编码 先看一眼项目结构 ![微信图片_20210703115930.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a7e91c48177b4fe8821c487666e14079~tplv-k3u1fbpfcp-watermark.image) `bin` 目录是执行文件的入口 `lib` 目录是整个项目的开发目录 其他目录都是一些辅助性的,如名字所示。接下来,我们要按部就班的完成基本功能。 先完成第一个,用户的增查改删,并且做成标准,以后使用。 ### 创建用户表 我已经提前准备好了相关的`sql` 语句 ```sql CREATE TABLE IF NOT EXISTS `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` varchar(40) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户ID', `user_mobile` varchar(11) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户手机号', `user_password` varchar(60) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户密码', `user_nickname` varchar(60) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户昵称', `user_avatar` varchar(60) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户头像', `user_description` varchar(120) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户介绍', `create_time` bigint(20) NOT NULL DEFAULT '0' COMMENT '创建时间', `update_time` bigint(20) NOT NULL DEFAULT '0' COMMENT '更新时间', `delete_time` bigint(20) NOT NULL DEFAULT '0' COMMENT '删除时间', PRIMARY KEY (`id`), UNIQUE KEY `user_id` (`user_id`), KEY `user_mobile` (`user_mobile`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='用户表'; ``` 放到`mysql` 去执行 ### 创建用户模型 用户模型用来与数据表进行对应的,方便面向对象开发。 在目录 `lib/extend/model/` 下,新建模型文件 `User.dart`,键入如下内容 ```dart class User { int id; String userId; String userMobile; String userPassword; String userNickname; String userAvatar; String userDescription; int createTime; int updateTime; int deleteTime; } ``` 这里只是定义了类名,以及相关属性,还需要补充一些方法。补充模型类的方法,是一个枯燥的事情,建议使用工具。 如果你使用的是 `VSCode`,并且安装了 `Dart Data Class Generator` 插件,此时点击类名,将会出现帮助,点击下图红色框框内,将补充完成代码。 ![微信图片_20210703120712.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/309ff4d45f184190890a30dd8492a271~tplv-k3u1fbpfcp-watermark.image) 我们将得到以下结果 ```dart import 'dart:convert'; class User { int id; String userId; String userMobile; String userPassword; String userNickname; String userAvatar; String userDescription; int createTime; int updateTime; int deleteTime; User({ this.id, this.userId, this.userMobile, this.userPassword, this.userNickname, this.userAvatar, this.userDescription, this.createTime, this.updateTime, this.deleteTime, }); Map<String, dynamic> toMap() { return { 'id': id, 'userId': userId, 'userMobile': userMobile, 'userPassword': userPassword, 'userNickname': userNickname, 'userAvatar': userAvatar, 'userDescription': userDescription, 'createTime': createTime, 'updateTime': updateTime, 'deleteTime': deleteTime, }; } factory User.fromMap(Map<String, dynamic> map) { return User( id: map['id'], userId: map['userId'], userMobile: map['userMobile'], userPassword: map['userPassword'], userNickname: map['userNickname'], userAvatar: map['userAvatar'], userDescription: map['userDescription'], createTime: map['createTime'], updateTime: map['updateTime'], deleteTime: map['deleteTime'], ); } String toJson() => json.encode(toMap()); factory User.fromJson(String source) => User.fromMap(json.decode(source)); @override String toString() { return 'User(id: $id, userId: $userId, userMobile: $userMobile, userPassword: $userPassword, userNickname: $userNickname, userAvatar: $userAvatar, userDescription: $userDescription, createTime: $createTime, updateTime: $updateTime, deleteTime: $deleteTime)'; } } ``` 经过刚才的操作,可以看到 多了三个实例化函数 `User`, `User.fromMap`, `User.fromJson` 多了三个方法 `toMap`, `toJson`, `toString` 为什么要做这些,归根到底是因为 `Dart` 禁用反射,当我们从其他地方拿到数据,无法直接转成模型对象。只能先转成`map`,或者`json`字符串,然后再手工转成模型对象。 是稍稍复杂了点,为了更好的性能,不算大问题。 ### 创建服务 服务用来处理实际业务,被控制器所调用。 在目录 `lib/extend/service/` 下,新建服务文件 `UserService.dart`,键入如下内容 ```dart import 'package:community/bootstrap/db/Db.dart'; import 'package:community/bootstrap/db/DbColumn.dart'; import 'package:community/bootstrap/helper/ConvertHelper.dart'; import 'package:community/extend/helper/PasswordHelper.dart'; import 'package:community/extend/helper/TimeHelper.dart'; import 'package:community/extend/helper/UniqueHelper.dart'; import 'package:community/extend/model/Page.dart'; import 'package:community/extend/model/User.dart'; class UserService { static String _table = "user"; /// 分页查询 static Future<Page> query( List condition, int pageNum, int pageSize) async { int totalCount = await Db(_table).where(condition).count('*'); List<Map<String, dynamic>> mapList = await Db(_table) .where(condition) .page(pageNum, pageSize) .order("create_time desc") .select(); List list = mapList.map((e) => User.fromMap(ConvertHelper.keyToHump(e))).toList(); return Page(totalCount, pageNum, pageSize, list); } /// 根据用户ID查询 static Future findById(String userId) async { List where = [ DbColumn.fieldToUnderLine("userId", "=", userId), DbColumn.fieldToUnderLine("deleteTime", "=", 0), ]; Map<String, dynamic> map = await Db(_table).where(where).find(); if (null == map) throw "没有找到用户"; return User.fromMap(ConvertHelper.keyToHump(map)); } /// 添加用户 static Future add( String userMobile, String userPassword, String userNickname, String userAvatar, String userDescription, ) async { Map<String, dynamic> userMap = await _findByMobile(userMobile); if (null != userMap) throw '该手机号已存在'; User user = User( userId: UniqueHelper.userId(), userMobile: userMobile, userPassword: PasswordHelper.password(userPassword), createTime: TimeHelper.timestamp(), userNickname: userNickname, userAvatar: userAvatar, userDescription: userDescription, updateTime: 0, deleteTime: 0); user.id = await Db(_table).insert(ConvertHelper.keyToUnderLine(user.toMap())); return user; } /// 修改用户昵称 static Future updateNickname(String userId, String userNickname) async { User user = await findById(userId); user.userNickname = userNickname; await _updateField(user.toMap(), 'userId', ['userNickname']); return user; } /// 根据用户ID删除,软删除 static Future delete(String userId) async { User user = await findById(userId); user.deleteTime = TimeHelper.timestamp(); await _updateField(user.toMap(), 'userId', ['deleteTime']); return user; } /// 根据用户手机号查询 static Future<Map<String, dynamic>> _findByMobile(String userMobile) async { List condition = [ DbColumn.fieldToUnderLine("userMobile", "=", userMobile), DbColumn.fieldToUnderLine("deleteTime", "=", 0), ]; Map<String, dynamic> map = await Db(_table).where(condition).find(); return map; } /// 更新表字段 static Future _updateField( Map<String, dynamic> map, String keyName, List fieldList) async { List condition = [ DbColumn.fieldToUnderLine(keyName, '=', map[keyName]) ]; Map<String, dynamic> updateMap = {}; fieldList.forEach((fieldName) { updateMap[fieldName] = map[fieldName]; }); return await Db(_table) .where(condition) .update(ConvertHelper.keyToUnderLine(updateMap)); } } ``` 上述代码,是对数据的增查改删,和其他语言的代码,大同小异,一些容易迷惑的地方,稍微解释下。 在分页查询中 ```dart List list = mapList.map((e) => User.fromMap(ConvertHelper.keyToHump(e))).toList(); ``` 这里主要的作用是,将 `mapList` 这个键值对的列表,转换成 `User` 对象列表。 另外,因为我们数据库的字段名是下划线格式的,而模型类的属性是驼峰格式的,所以需要一个转换过程。 `ConvertHelper.keyToHump` 的作用是将键名为 `下划线格式` 的键值对,转换成键名为 `驼峰格式` 的键值对。 ### 创建控制器 控制器用于接收用户请求参数,并调用服务来处理业务,最后返回信息 在目录 `lib/app/controller/` 下,新建模型文件 `UserController.dart`,键入如下内容 ```dart import 'package:community/bootstrap/Context.dart'; import 'package:community/bootstrap/db/DbColumn.dart'; import 'package:community/bootstrap/db/DbTrans.dart'; import 'package:community/bootstrap/helper/VerifyHelper.dart'; import 'package:community/bootstrap/meta/RouteMeta.dart'; import 'package:community/extend/model/Page.dart'; import 'package:community/extend/model/User.dart'; import 'package:community/extend/service/UserService.dart'; class UserController { @RouteMeta('/home/user/query', 'GET|POST') static void query(Context ctx) async { int pageNum = ctx.getPositiveInt('pageNum', def: 1); int pageSize = ctx.getPositiveInt('pageSize', def: 20); await DbTrans.simple(ctx, () async { List condition = []; Page res = await UserService.query(condition, pageNum, pageSize); ctx.showSuccess('已获取', res.toMap()); }); } @RouteMeta('/home/user/findById', 'GET|POST') static void findById(Context ctx) async { String userId = ctx.getString('userId'); if (VerifyHelper.empty(userId)) return ctx.showError('用户ID不能为空'); await DbTrans.simple(ctx, () async { User res = await UserService.findById(userId); ctx.showSuccess('已获取', res.toMap()); }); } @RouteMeta('/home/user/add', 'GET|POST') static void add(Context ctx) async { String userMobile = ctx.getString('userMobile'); String userPassword = ctx.getString('userPassword'); String userNickname = ctx.getString('userNickname'); String userAvatar = ctx.getString('userAvatar'); String userDescription = ctx.getString('userDescription'); if (VerifyHelper.empty(userMobile)) return ctx.showError('用户手机号不能为空'); if (VerifyHelper.empty(userPassword)) return ctx.showError('用户密码不能为空'); if (VerifyHelper.empty(userNickname)) return ctx.showError('用户昵称不能为空'); if (VerifyHelper.empty(userAvatar)) return ctx.showError('用户头像不能为空'); if (VerifyHelper.empty(userDescription)) return ctx.showError('用户描述不能为空'); await DbTrans.simple(ctx, () async { User res = await UserService.add( userMobile, userPassword, userNickname, userAvatar, userDescription); ctx.showSuccess('已添加', res.toMap()); }); } @RouteMeta('/home/user/updateNickname', 'GET|POST') static void updateNickname(Context ctx) async { String userId = ctx.getString('userId'); String userNickname = ctx.getString('userNickname'); if (VerifyHelper.empty(userId)) return ctx.showError('用户ID不能为空'); if (VerifyHelper.empty(userNickname)) return ctx.showError('用户昵称不能为空'); await DbTrans.simple(ctx, () async { User res = await UserService.updateNickname(userId, userNickname); ctx.showSuccess('已更改', res.toMap()); }); } @RouteMeta('/home/user/delete', 'GET|POST') static void delete(Context ctx) async { String userId = ctx.getString('userId'); if (VerifyHelper.empty(userId)) return ctx.showError('用户ID不能为空'); await DbTrans.simple(ctx, () async { User res = await UserService.delete(userId); ctx.showSuccess('已删除', res.toMap()); }); } } ``` 有必要说明一下: `RouteMeta` 是 `DartMars` 定义的路由元数据,类似于`java` 里的注解。 相同的作用是,可以对代码进行描述,让开发者知道所描述的代码的功能。 不同的是,因为 `DartMars` 没有反射,所以程序不能在运行的时候获取元数据或者说注解的信息,也就无法完成类似于`java`里注解生成代码的功能。 当然,既然运行的时候不能生成代码,我们另寻他图,在编译之前生成即可。 ### 自动更新路由配置 接下来,我们启动项目,执行如下命令: ```shell dart pub global run dart_mars --serve dev ``` 请注意,控制台打印的有这样一句话 ```shell route config file has been updated, see ./lib/config/route.dart ``` 说路由配置文件已经更新,地址是 `./lib/config/route.dart`,我们看看去 ```dart import '../bootstrap/helper/RouteHelper.dart'; import '../app/controller/HomeController.dart' as app_controller_HomeController; import '../app/controller/UserController.dart' as app_controller_UserController; /// /// don't modify this file yourself, this file content will be replace by DartMars /// /// for more infomation, see doc about Route /// /// last replace time 2021-07-03 14:53:51.588722 /// void configRoute(){ RouteHelper.add('GET', '/', app_controller_HomeController.HomeController.index); RouteHelper.add('GET', '/user', app_controller_HomeController.HomeController.user); RouteHelper.add('GET', '/city/:cityName', app_controller_HomeController.HomeController.city); RouteHelper.add('GET|POST', '/home/user/query', app_controller_UserController.UserController.query); RouteHelper.add('GET|POST', '/home/user/findById', app_controller_UserController.UserController.findById); RouteHelper.add('GET|POST', '/home/user/add', app_controller_UserController.UserController.add); RouteHelper.add('GET|POST', '/home/user/updateNickname', app_controller_UserController.UserController.updateNickname); RouteHelper.add('GET|POST', '/home/user/delete', app_controller_UserController.UserController.delete); } ``` 果然,最后面添加了 `5` 个路由规则,和我们刚才在 `UserController` 里定义的一样。 另外,如文件所提示的,这个文件不要手动更改,当你运行 `--serve` 命令时, `DartMars`会自动更新。 ## 测试接口 测试接口的工作非常简单了,可以使用专业工具,也可以在浏览器中直接来。文章篇幅有限,我就测试 `2` 个,其他的接口,有兴趣的同学自己来。 ### 测试添加用户接口 ```html http://127.0.0.1/home/user/add?userMobile=18512345679&userPassword=123456&userNickname=tang&userAvatar=http://www.test.com/1.jpg&userDescription=test ``` 返回如下 ```json { "code": 200, "msg": "已添加", "data": { "id": 2, "userId": "1625295731292004882", "userMobile": "18512345679", "userPassword": "4616221982a9d1759d1d0cec7249a6d71da960d3", "userNickname": "tang", "userAvatar": "http://www.test.com/1.jpg", "userDescription": "test", "createTime": 1625295731, "updateTime": 0, "deleteTime": 0 } } ``` 一切正常,非常棒。 ### 测试查询单个用户接口 ```html http://127.0.0.1/home/user/findById?userId=1625295731292004882 ``` 返回如下 ```json { "code": 200, "msg": "已获取", "data": { "id": 2, "userId": "1625295731292004882", "userMobile": "18512345679", "userPassword": "4616221982a9d1759d1d0cec7249a6d71da960d3", "userNickname": "tang", "userAvatar": "http://www.test.com/1.jpg", "userDescription": "test", "createTime": 1625295731, "updateTime": 0, "deleteTime": 0 } } ``` 一切正常,非常棒。 ## 总结 能够看到这里的同学,想必都是真爱了。 由上述流程走下来,可以看出,用 `Dart` 开发后端应用,与其他语言开发,并无太大的区别。也说明一个事情,其他语言的开发者,想转用 `Dart` 开发后端应用程序,是一件很容易的事情。 加之 `Dart` 在客户端开发领域的成功, 一种语言完成客户端与服务端绝对不再是梦想。 That's All, Enjoy.
上一篇:flutter初学,慢慢填充


下一篇:Dart 2.1 正式发布:提升性能以及改进可用性