前言
這幾天連續發了幾篇關於 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.