手摸手,使用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

嗯嗯,浓浓的 vuepress 味道。

开始一个项目如此简单

根据DartMars的指引,在安装Dart 后,我们可以执行以下命令来创建项目

# 安装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

打开命令行工具,执行

dart pub global activate --source git https://github.com/tangpanqing/dart_mars.git

感谢墙的存在,我等了将近1分钟,提示我如下:

Activated dart_mars 1.0.4 from Git repository "https://github.com/tangpanqing/dart_mars.git"

这就表示安装好了。

第二步,创建项目

项目暂定名称 community 社区,执行如下命令

dart pub global run dart_mars --create community

经过以上命令,DartMars 有了提示

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

意思说,项目已经创建,接下来你需要进入目录,并且获取依赖,最后执行。

并且显示了相关命令,是不是很贴心? 谈恋爱的时候,一定是个暖男。

第三步,进入目录

执行命令

cd community

第四步,获取依赖

执行命令

dart pub global run dart_mars --get

经过以上命令,DartMars 有了提示

Got dependencies!

表示加载依赖完成

第五步,启动项目

dart pub global run dart_mars --serve dev

经过以上命令,DartMars 有了提示

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 我们就看到了经典的

hello world

按部就班地继续编码

先看一眼项目结构

微信图片_20210703115930.png

bin 目录是执行文件的入口

lib 目录是整个项目的开发目录

其他目录都是一些辅助性的,如名字所示。接下来,我们要按部就班的完成基本功能。

先完成第一个,用户的增查改删,并且做成标准,以后使用。

创建用户表

我已经提前准备好了相关的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,键入如下内容

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

我们将得到以下结果

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,键入如下内容

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<User>> query(
      List<DbColumn> 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<User> list =
        mapList.map((e) => User.fromMap(ConvertHelper.keyToHump(e))).toList();

    return Page<User>(totalCount, pageNum, pageSize, list);
  }

  /// 根据用户ID查询
  static Future<User> findById(String userId) async {
    List<DbColumn> 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<User> 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<User> 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<User> 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<DbColumn> condition = [
      DbColumn.fieldToUnderLine("userMobile", "=", userMobile),
      DbColumn.fieldToUnderLine("deleteTime", "=", 0),
    ];

    Map<String, dynamic> map = await Db(_table).where(condition).find();

    return map;
  }

  /// 更新表字段
  static Future<int> _updateField(
      Map<String, dynamic> map, String keyName, List<String> fieldList) async {
    List<DbColumn> 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));
  }
}

上述代码,是对数据的增查改删,和其他语言的代码,大同小异,一些容易迷惑的地方,稍微解释下。

在分页查询中

List<User> list =
        mapList.map((e) => User.fromMap(ConvertHelper.keyToHump(e))).toList();

这里主要的作用是,将 mapList 这个键值对的列表,转换成 User 对象列表。

另外,因为我们数据库的字段名是下划线格式的,而模型类的属性是驼峰格式的,所以需要一个转换过程。

ConvertHelper.keyToHump 的作用是将键名为 下划线格式 的键值对,转换成键名为 驼峰格式 的键值对。

创建控制器

控制器用于接收用户请求参数,并调用服务来处理业务,最后返回信息

在目录 lib/app/controller/ 下,新建模型文件 UserController.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<DbColumn> condition = [];
      Page<User> 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());
    });
  }
}

有必要说明一下:

RouteMetaDartMars 定义的路由元数据,类似于java 里的注解。

相同的作用是,可以对代码进行描述,让开发者知道所描述的代码的功能。

不同的是,因为 DartMars 没有反射,所以程序不能在运行的时候获取元数据或者说注解的信息,也就无法完成类似于java里注解生成代码的功能。

当然,既然运行的时候不能生成代码,我们另寻他图,在编译之前生成即可。

自动更新路由配置

接下来,我们启动项目,执行如下命令:

dart pub global run dart_mars --serve dev

请注意,控制台打印的有这样一句话

route config file has been updated, see ./lib/config/route.dart

说路由配置文件已经更新,地址是 ./lib/config/route.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 个,其他的接口,有兴趣的同学自己来。

测试添加用户接口

http://127.0.0.1/home/user/add?userMobile=18512345679&userPassword=123456&userNickname=tang&userAvatar=http://www.test.com/1.jpg&userDescription=test

返回如下

{
  "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
  }
}

一切正常,非常棒。

测试查询单个用户接口

http://127.0.0.1/home/user/findById?userId=1625295731292004882

返回如下

{
  "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.

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,482评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,377评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,762评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,273评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,289评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,046评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,351评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,988评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,476评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,948评论 2 324
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,064评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,712评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,261评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,264评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,486评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,511评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,802评论 2 345

推荐阅读更多精彩内容