手摸手,使用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/
打开文档首页,如此
嗯嗯,浓浓的 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
启动成功,通过以上信息,我们可知:
路由配置文件已经更新,
HTTP 服务已经开始,在80端口,目前使用的是开发环境
打开浏览器,访问 http://127.0.0.1:80 我们就看到了经典的
hello world
按部就班地继续编码
先看一眼项目结构
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
插件,此时点击类名,将会出现帮助,点击下图红色框框内,将补充完成代码。
我们将得到以下结果
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());
});
}
}
有必要说明一下:
RouteMeta
是 DartMars
定义的路由元数据,类似于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.
手摸手,使用Dart语言开发后端应用,来吧!的更多相关文章
- 手摸手教你让Laravel开发Api更得心应手
https://www.guaosi.com/2019/02/26/laravel-api-initialization-preparation/ 1. 起因 随着前后端完全分离,PHP也基本告别了v ...
- 【手摸手,带你搭建前后端分离商城系统】01 搭建基本代码框架、生成一个基本API
[手摸手,带你搭建前后端分离商城系统]01 搭建基本代码框架.生成一个基本API 通过本教程的学习,将带你从零搭建一个商城系统. 当然,这个商城涵盖了很多流行的知识点和技术核心 我可以学习到什么? S ...
- 【手摸手,带你搭建前后端分离商城系统】02 VUE-CLI 脚手架生成基本项目,axios配置请求、解决跨域问题
[手摸手,带你搭建前后端分离商城系统]02 VUE-CLI 脚手架生成基本项目,axios配置请求.解决跨域问题. 回顾一下上一节我们学习到的内容.已经将一个 usm_admin 后台用户 表的基本增 ...
- 手摸手教你微信小程序开发之自定义组件
前言 相信大家在开发小程序时会遇到某个功能多次使用的情况,比如弹出框.这个时候大家首先想到的是组件化开发,就是把弹出框封装成一个组件,然后哪里使用哪里就调用,对,看来大家都是有思路的人,但是要怎样实现 ...
- 【手摸手,带你搭建前后端分离商城系统】03 整合Spring Security token 实现方案,完成主业务登录
[手摸手,带你搭建前后端分离商城系统]03 整合Spring Security token 实现方案,完成主业务登录 上节里面,我们已经将基本的前端 VUE + Element UI 整合到了一起.并 ...
- 手摸手带你学移动端WEB开发
HTML常用标签总结 手摸手带你学CSS HTML5与CSS3知识点总结 手摸手带你学移动端WEB开发 好好学习,天天向上 本文已收录至我的Github仓库DayDayUP:github.com/Ro ...
- 【转】手摸手,带你用vue撸后台 系列一
前言 说好的教程终于来了,第一篇文章主要来说一说在开始写业务代码前的一些准备工作吧,但这里不会教你webpack的基础配置,热更新怎么做,webpack速度优化等等,有需求的请自行google. 目录 ...
- 【转】手摸手,带你用vue撸后台 系列二(登录权限篇)
前言 拖更有点严重,过了半个月才写了第二篇教程.无奈自己是一个业务猿,每天被我司的产品虐的死去活来,之前又病了一下休息了几天,大家见谅. 进入正题,做后台项目区别于做其它的项目,权限验证与安全性是非常 ...
- 【转】手摸手,带你用vue撸后台 系列三(实战篇)
前言 在前面两篇文章中已经把基础工作环境构建完成,也已经把后台核心的登录和权限完成了,现在手摸手,一起进入实操. Element 去年十月份开始用vue做管理后台的时候毫不犹豫的就选择了Elemen, ...
- 【转】手摸手,带你用vue撸后台 系列四(vueAdmin 一个极简的后台基础模板)
前言 做这个 vueAdmin-template 的主要原因是: vue-element-admin 这个项目的初衷是一个vue的管理后台集成方案,把平时用到的一些组件或者经验分享给大家,同时它也在不 ...
随机推荐
- MySQL数据库安装保姆教程及问题解决
使用Mysql的zip压缩包解压版,下载之后需进行一定的配置,才能使用它. 下面对Mysql压缩包版的安装方法进行详细的描述,如有疑问或错误,望及时反馈. 首先,mysql的官方下载地址点我进行下载 ...
- http和https分别是什么?
http中文名:超文本传输协议英文名:Hyper Text Transfer Protocol解释:是一个简单的请求-响应协议,它通常运行在TCP之上.它指定了客户端可能发送给服务器什么样的消息以及得 ...
- while.for循环和基本数据类型内置方法
while循环补充说明 流程控制之for循环 基本数据类型内置方法 内容详细 1.死循环 真正的死循环是一旦执行,Cpu的功耗会急速上升 知道系统采取紧急措施 所以 尽量不要让cpu长时间不断运算. ...
- 前端微信登录获取code,userInfo,openid
getUser(e) { wx.getUserProfile({ desc: '用户完善会员资料', success: res => { let userInfo = res.userInfo; ...
- value中放vue的参数
<input type="text" v-model="userInfo.name"/>
- HBase(1/5)
HBase学习(一) 一.了解HBase 官方文档:https://hbase.apache.org/book.html 1.1 HBase概述 HBase 是一个高可靠性.高性能.面向列.可伸缩的分 ...
- 从SpringBoot启动,阅读源码设计
目录 一.背景说明 二.SpringBoot工程 三.应用上下文 四.资源加载 五.应用环境 六.Bean对象 七.Tomcat服务 八.事件模型 九.配置加载 十.数据库集成 十一.参考源码 服务启 ...
- ASP.NET Core 中的模型绑定
微软官方文档:ASP.NET Core 中的模型绑定 Route 是通过MVC Route URL取值. 如:http://localhost:5000/Home/Index/2,id取出的值就会是2 ...
- Java学习之路:运算符
2022-10-10 10:34:08 1 运算符 算术运算符:+, -, *, /, %, ++, -- 赋值运算符:= 关系运算符:>, <, >=, <=, ==, != ...
- Autobus 方法记录
原题链接 [COCI2021-2022#4] Autobus 题目描述 在一个国家里有 \(n\) 座城市.这些城市由 \(m\) 条公交线路连接,其中第 \(i\) 条线路从城市 \(a_i\) 出 ...