Dart 全栈之服务端

本文介绍了Dart服务端开发的新框架Arowana,因aqueduct和Angel框架的停更而诞生。Arowana基于shelf库增强HTTP处理,并参考Go的Gin框架实现高性能路由。作者提供了注册登录的完整示例,展示了如何使用Arowana处理路由、鉴权和数据库操作。此外,文章还分享了自定义身份验证中间件的实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Dart 的服务端开发

本文视频
本文已录制视频上传B站,请配合视频食用 《Dart 全栈之服务端》


本文是博主《Flutter全栈式开发》系列课程的拓展。我们在Flutter课程中,编写了一个注册登录案例,详细讲解了常见的注册、登录、API接口鉴权功能,前后端是如何配合实现的。其中后台服务使用了Dart语言中较为知名的aqueduct框架。

近来由于Dart版本迭代过于频繁和激烈,尤其是不兼容的空安全特性,导致课程中使用的服务端框架aqueduct停止维护。维护该框架的公司没有多余的精力使之兼容新版本,目前亦无社区接手项目,可能永久停止。相继的,Dart的第二大服务端框架angel亦停止维护,不得不说是Dart的一大遗憾。我们的课程已经发布有较长时间,其中的案例,现在只能使用旧版本才能运行。

为了大家有更好的课程体验,能持续跟进Dart的新特性,我特别编写了一个轻量级的支持空安全特性的Dart HTTP服务器框架:arowana,它可以直接在Flutter中使用,可以运行在移动端。

arowana这个单词是龙鱼的意思。龙鱼最早见于我国山海经记载:

“龙鱼陵居在其北,状如狸。一曰“鱼段”。即有神圣乘此以行九野。”

龙鱼有须,胸鳍似龙爪,加上浑身金鳞(或红鳞),因而形似神龙。加上其盘旋灵动,华贵端庄,静时安若处子,动时迅如脱兔,所以其神更似神龙。目前龙鱼为濒危物种,列入《世界自然保护联盟》(IUCN) 濒危物种红色名录。

arowana 框架基于Dart语言官方提供的 shelf 库,以处理HTTP请求,但shelf功能较弱,arowana 对其进行了部分功能增强。同时,还参考了我较为欣赏的Go语言Gin框架,编写了一个基于前缀树搜索的高性能路由组件,完全避免了正则匹配。我们知道,Dart语言中的正则处理性能较差,在AOT编译时可能还会称为性能瓶颈。arowana中还参考了aqueduct,封装了对Isolate并发处理的操作,使得并发处理更加简单。arowana没有太多依赖,尤其没有依赖一些动态特性(反射),因此它可以在移动端运行。

众所周知,Dart语言非常缺乏一个好的数据库ORM框架,其中一部分原因是因为Dart语言反射能力较差,甚至在Flutter中无法使用反射。因此arowana 并未提供或整合数据库模块,大家可自由组合目前可用的数据库连接模块,使用SQL操作数据。下面看一个简单示例:

class MyAChannel extends DefaultChannel {
  @override
  Future prepare() async {
    print('current isolate [${Isolate.current.debugName}]');
  }

  @override
  void entryPoint() {
    // 注册get请求路由
    get('/hello', (r) {
      return Response.ok('<h1>Hello, arowana!</h1>',
          headers: {'content-type': 'text/html; charset=UTF-8'});
    });
  }
}

在Flutter中使用:

void main() {
  var app = Application(MyAChannel());
  // numberOfInstances: 启动两个后台isolate处理请求
  app.start(numberOfInstances: 2,consoleLogging: true);
  runApp(const MyApp());
}

注册登录实现

来看一个完整例子,我们用arowana重写《Flutter全栈式开发》课程中的注册登录案例:

添加依赖。目前arowana处于试验版,没有发布仓库,使用Git协议添加依赖:

dependencies:
  dart_jsonwebtoken: ^2.3.2
  arowana:
    git: https://github.com/arcticfox1919/arowana.git
  sqlite3: ^1.2.0
  crypto: ^3.0.1

代码结构:

首先编写一个Channel,注册路由:

/// server.dart

class MyAChannel extends DefaultChannel{
  late SqliteDb db;

  @override
  Future prepare() async{
    db = SqliteDb.Connect();
  }

  @override
  void entryPoint() {
    post('/register', RegisterController(db));
    post('/login', LoginController(db));

    // 分组路由
    var r = group('/info');
    // 给该组设置鉴权中间件
    r.use(Auth.bearer(AuthVerifier()));

    r.get('/list', (request){
      return ResponseX.ok({
        'language': [
          {'id': 1, 'name': 'dart'},
          {'id': 2, 'name': 'java'},
          {'id': 3, 'name': 'c'},
          {'id': 4, 'name': 'golang'},
          {'id': 5, 'name': 'python'}
        ]
      });
    });
  }
}

路由使用,与Gin相似,这里使用了分组路由,给'/info'下的所有子路由设置了一个身份验证的中间件,访问子路由必须鉴权。arowana 提供了一个简单的身份验证中间件Auth。开发者只需要实现一个自己的验证器AuthVerifier,从而自定义验证逻辑,它需要继承自AuthValidator

两个post请求的路由分别实现注册和登录功能,两者逻辑相似,都使用json格式传递数据:

/// ctrl.dart

class RegisterController{
  SqliteDb db;
  RegisterController(this.db);

  Future<Response> call(Request request)async{
    var body = await request.body;
    var json = body.json;
    if (json != null) {
      // 从请求参数中构造User实体类
      var user = User.from(json);
      if (user.check()) {
        user.save(db);
        // 注册成功,给客户端返回令牌
        return ResponseX.token(user.generateToken());
      }
    }
    return ResponseX.badRequest('Invalid username or password!');
  }
}

class LoginController{
  SqliteDb db;
  LoginController(this.db);

  Future<Response> call(Request request) async {
    var body = await request.body;
    var json = body.json;
    if (json != null) {
      var user = User.from(json);
      if (user.verify(db)) {
        // 登录成功,给客户端返回令牌
        return ResponseX.token(user.generateToken());
      }
    }
    return ResponseX.badRequest('Invalid username or password!');
  }
}

如果你使用表单提交用户名和密码,你的请求头中需要指定application/x-www-form-urlencode类型,在服务端,你需要使用下面的方式获取参数:

var body = await request.body;
var form = body.formParams;
if (form != null) {
    print(form['uname']);
    print(form['passwd']);
}

看一下User类的逻辑:

class User {
  int? id;
  String? uname;
  String? passwd;

  User.from(Map<String, dynamic> json) {
    uname = json['uname'];
    passwd = json['passwd'];
  }
  
  // 检查是否提交了有效的用户名、密码
  bool check() =>
      uname != null &&
      uname!.isNotEmpty &&
      passwd != null &&
      passwd!.isNotEmpty;

  // 验证登录
  bool verify(SqliteDb db) {
    if (!check()) return false;
    // 对密码进行hash处理
    passwd = md5.convert(utf8.encode(passwd!)).toString();
    // 去数据库查询用户名是否注册,密码是否正确
    var u = db.getUser(this);
    if(u == null) return false;
    id = u.id;
    return true;
  }
  
  // 保存用户名、密码到数据库
  void save(SqliteDb db){
    if(check()){
      // hash passwd
      passwd = md5.convert(utf8.encode(passwd!)).toString();
      var u = db.save(this);
      id = u.id;
    }
  }
  
  // 生成JWT格式令牌
  AuthToken generateToken({Duration expiration = const Duration(hours: 24)}) {
    var now = DateTime.now();
    var expirationDate = now.add(expiration);
    var t = createToken(
        this, expirationDate.millisecondsSinceEpoch~/1000);
    return AuthToken(t, now, expirationDate);
  }
}

逻辑比较简单,这里主要使用了JWT格式生成Token,我们Flutter课程中已经介绍过JWT,但课程当时的案例用的另一种OAuth2.0生成的Token。

最后看一下验证器的实现:

class AuthVerifier extends AuthValidator{

  @override
  FutureOr<Authorization?> validate<T>(AuthorizationParser<T> parser, T authorizationData) {
    if (parser is AuthorizationBearerParser) {
      return _verify(authorizationData as String);
    }
    throw ArgumentError(
        "Invalid 'parser' for 'AuthValidator.validate'. Use 'AuthorizationBearerHeader'.");
  }

  FutureOr<Authorization?> _verify(String accessToken) async {
    try {
      // 校验客户端带来的Token
      final jwt = verifyToken(accessToken);
      return Authorization((jwt.payload as Map)['id'].toString(), this);
    } on JWTUndefinedError catch(e){
      print(e.error);
      // 校验失败,颁发的Token已经过期了,客户端需要刷新令牌或者重新登录
      if(e.error is JWTExpiredError){
        throw TokenExpiredException('Error: the token has expired, please refresh');
      }
    }
  }
}

这里,对JWT Token的处理,使用了另一个库 dart_jsonwebtoken。给Auth中间件提供我们自定义的验证器,当访问注册了身份验证中间件的子路由/info/list时,首先会校验请求的令牌,未登录或令牌过期,则校验失败,无权访问该路由。

最后,我们可以使用Postman工具来检验成果:

访问需要鉴权的接口

完整示例代码,请访问 这里


关注公众号:编程之路从0到1

课程后续会不断更新,持续跟进Flutter技术迭代,关注我,你不会失望!

编程之路从0到1

或关注博主的视频课程

云课堂

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

编程之路从0到1

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值