Alfred

一个高性能、类express.js的服务器框架,易于使用,并且集成了一切所需。

Build Status

快速入门

import 'package:alfred/alfred.dart';

void main() async {
  final app = Alfred();

  app.get('/example', (req, res) => 'Hello world');

  await app.listen();
}

核心原则

  • 最少的依赖,
  • 最少的代码,并贴近Dart核心库——易于维护!
  • 易用性
  • 可预测的、成熟的语义
  • 90%以上您所需的功能都已准备就绪

阅读关于项目背景或其与shelf的不同之处

用法概述

如果您曾使用过express.js,您应该会感到很熟悉

import 'dart:io';

import 'package:alfred/alfred.dart';

void main() async {
  final app = Alfred();

  app.get('/text', (req, res) => 'Text response');

  app.get('/json', (req, res) => {'json_response': true});

  app.get('/jsonExpressStyle', (req, res) {
    res.json({'type': 'traditional_json_response'});
  });

  app.get('/file', (req, res) => File('test/files/image.jpg'));

  app.get('/html', (req, res) {
    res.headers.contentType = ContentType.html;
    return '<html><body><h1>Test HTML</h1></body></html>';
  });

  await app.listen(6565); //Listening on port 6565
}

它应该能满足您的预期。处理请求体时需要“await”

import 'package:alfred/alfred.dart';

void main() async {
  final app = Alfred();

  app.post('/post-route', (req, res) async {
    final body = await req.body; //JSON body
    body != null; //true
  });

  await app.listen(); //Listening on port 3000
}

Dart内部提供了请求体解析器,因此无需额外依赖。

最大的区别在于您可以选择不调用res.sendres.json等——尽管您仍然可以这样做。
每个路由都接受一个Future作为响应。目前,您可以传递以下内容,它将得到适当的处理

返回Dart类型 返回REST类型
List<dynamic> JSON
Map<String, Object?> JSON
可序列化对象 (Object.toJSON 或 Object.toJson) *请参见注释 JSON
字符串 纯文本
Stream<List<int>> 二分
List<int> 二分
File(文件) 二进制,MIME类型根据扩展名推断
目录 提供静态文件服务

* 如果您的对象有一个“toJSON”或“toJson”函数,Alfred将运行它,然后返回结果

如果您想返回HTML,只需将内容类型设置为HTML,如下所示

import 'dart:io';

import 'package:alfred/alfred.dart';

void main() async {
  final app = Alfred();

  app.get('/html', (req, res) {
    res.headers.contentType = ContentType.html;
    return '<html><body><h1>Title!</h1></body></html>';
  });

  await app.listen(); //Listening on port 3000
}

如果您想返回其他类型并让其自动处理,您可以扩展Alfred,使用
自定义类型处理器.

快速入门指南

如果一切都有些令人不知所措,@iapicca 整理了一个快速入门指南,其中包含了一些
更详细的内容:https://medium.com/@iapicca/alfred-an-express-like-server-framework-written-in-dart-1661e8963db9

路由和入站请求

路由遵循与更基础的ExpressJS路由类似的模式。虽然有一些正则表达式
匹配,但大多数情况下,只需遵循Express的路由名称和参数语法即可

  • /path/to/:id/property

Express语法已扩展,以支持参数模式和类型。为了强制执行参数
验证,应在参数名称后提供正则表达式或类型说明符,使用
另一个:作为分隔符

  • /path/to/:id:\d+/property 将确保“id”是仅由数字组成的字符串
  • /path/to/:id:[0-9a-f]+/property 将确保“id”是仅由十六进制数字组成的字符串
  • /path/to/:word:[a-z]+/property 将确保“word”是仅由字母组成的字符串
  • /path/to/:id:uuid/property 将确保“id”是表示UUID的字符串

可用的类型说明符有

  • int:十进制整数
  • uint:正十进制整数
  • double:浮点数(十进制);请注意,不支持科学计数法
  • date:UTC日期,格式为“年/月/日”;请注意此类型如何“吸收”URI的多个段
  • timestamp:UTC日期,以自Epoch以来的毫秒数表示
  • uuid:类似于UUID的字符串(十六进制数字,格式为xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx);请注意,这里不做任何努力来确保这是一个有效的UUID
类型说明符 正则表达式 Dart类型
整数 -?\d+ 整数
uint \d+ 整数
双精度 -?\d+(?:\.\d+) 双精度
date -?\d{1,6}/(?:0[1-9]|1[012])/(?:0[1-9]|[12][0-9]|3[01]) DateTime(日期时间)
timestamp -?\d+ DateTime(日期时间)
uuid [0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12} 字符串

例如

import 'package:alfred/alfred.dart';

void main() async {
  final app = Alfred();
  app.all('/example/:id:int/:name', (req, res) {
    req.params['id'] != null;
    req.params['id'] is int;
    req.params['name'] != null;
  });
  app.all('/example/:id/:name', (req, res) {
    req.params['id'] != null;
    req.params['name'] != null;
  });
  app.get('/blog/:date:date/:id:int', (req, res) {
    req.params['date'] != null;
    req.params['date'] is DateTime;
    req.params['id'] != null;
    req.params['id'] is int;
  });
  await app.listen();
}

您还可以为路由使用通配符,并且如果还没有其他路由解析该
响应,它将被命中。例如,如果您想对API的整个部分进行身份验证,您可以
这样做

import 'dart:async';
import 'dart:io';

import 'package:alfred/alfred.dart';

FutureOr _authenticationMiddleware(HttpRequest req, HttpResponse res) async {
  res.statusCode = 401;
  await res.close();
}

void main() async {
  final app = Alfred();

  app.all('/resource*', (req, res) => _authenticationMiddleware);

  app.get('/resource', (req, res) {}); //Will not be hit
  app.post('/resource', (req, res) {}); //Will not be hit
  app.post('/resource/1', (req, res) {}); //Will not be hit

  await app.listen();
}

路由参数

您可以从req.params对象中访问路由的任何参数,如下所示

import 'package:alfred/alfred.dart';

void main() async {
  final app = Alfred();
  app.all('/example/:id/:name', (req, res) {
    req.params['id'] != null;
    req.params['name'] != null;
  });
  await app.listen();
}

查询字符串变量

查询字符串变量在请求中的req.uri.queryParameters对象中公开,如下所示

import 'package:alfred/alfred.dart';

void main() async {
  final app = Alfred();

  app.post('/route', (req, res) async {
    /// Handle /route?qsvar=true
    final result = req.uri.queryParameters['qsvar'];
    result == 'true'; //true
  });

  await app.listen(); //Listening on port 3000
}

请求体解析

要访问请求体,只需调用await req.body

Alfred将根据内容类型标头解释请求体类型并进行适当的解析。它开箱即用地处理url编码、multipart和json请求体。

import 'package:alfred/alfred.dart';

void main() async {
  final app = Alfred();

  app.post('/post-route', (req, res) async {
    final body = await req.body; //JSON body
    body != null; //true
  });

  await app.listen(); //Listening on port 3000
}

文件上传

要上传文件,请求体解析器将负责公开您所需的数据。实际上很简单
只需尝试一下,设置一个断点,看看请求体解析器返回什么。

下面是一个文件上传的工作示例,供您开始

import 'dart:io';

import 'package:alfred/alfred.dart';

final _uploadDirectory = Directory('uploadedFiles');

Future<void> main() async {
  final app = Alfred();

  app.get('/files/*', (req, res) => _uploadDirectory);

  /// Example of handling a multipart/form-data file upload
  app.post(
      '/upload',
      (req, res) => (HttpRequest req, HttpResponse res) async {
            final body = await req.bodyAsJsonMap;

            // Create the upload directory if it doesn't exist
            if (await _uploadDirectory.exists() == false) {
              await _uploadDirectory.create();
            }

            // Get the uploaded file content
            final uploadedFile = (body['file'] as HttpBodyFileUpload);
            var fileBytes = (uploadedFile.content as List<int>);

            // Create the local file name and save the file
            await File('${_uploadDirectory.absolute}/${uploadedFile.filename}')
                .writeAsBytes(fileBytes);

            /// Return the path to the user
            ///
            /// The path is served from the /files route above
            return ({
              'path':
                  'https://${req.headers.host ?? ''}/files/${uploadedFile.filename}'
            });
          });

  await app.listen();
}

中间件

您可以通过使用通配符为所有路由指定中间件

import 'package:alfred/alfred.dart';

void main() async {
  final app = Alfred();
  app.all('*', (req, res) {
    // Perform action
    req.headers.add('x-custom-header', "Alfred isn't bad");

    /// No need to call next as we don't send a response.
    /// Alfred will find the next matching route
  });

  app.get('/otherFunction', (req, res) {
    //Action performed next
    return {'message': 'complete'};
  });

  await app.listen();
}

这样声明的中间件将按照添加的顺序执行。

您也可以为路由添加中间件,这对于强制执行端点的身份验证等非常有用

import 'dart:async';
import 'dart:io';

import 'package:alfred/alfred.dart';

FutureOr exampleMiddlware(HttpRequest req, HttpResponse res) {
  // Do work
  if (req.headers.value('Authorization') != 'apikey') {
    throw AlfredException(401, {'message': 'authentication failed'});
  }
}

void main() async {
  final app = Alfred();
  app.all('/example/:id/:name', (req, res) {}, middleware: [exampleMiddlware]);

  await app.listen(); //Listening on port 3000
}

什么?没有‘next’?我该怎么办?

好的,规则很简单。如果中间件解析了HTTP请求,那么未来的中间件将不会执行。

所以,如果您从中间件返回一个对象,您就阻止了后续中间件的执行。

如果您返回null,它将交给下一个中间件或路由。

** 返回null等同于‘next’ **

CORS

为您提供了一个方便的CORS中间件。它也是一个很好的例子,说明如何为Alfred编写中间件

import 'package:alfred/alfred.dart';
import 'package:alfred/src/middleware/cors.dart';

void main() async {
  final app = Alfred();

  // Warning: defaults to origin "*"
  app.all('*', cors(origin: 'myorigin.com'));

  await app.listen();
}

响应

Alfred非常简单,通常您只需返回JSON、文件、字符串或二进制流即可。

与express最大的区别在于,您会看到可以不调用res.sendres.json等——尽管您仍然可以这样做。
每个路由都接受一个Future作为响应。目前,您可以传递以下内容,它将得到适当的处理

  • List<dynamic> – JSON
  • Map<String, Object?> – JSON
  • String – 纯文本
  • Stream<List<int>> – 二进制
  • List<int> – 二进制
  • File – 二进制,MIME类型根据扩展名推断
  • Directory – 提供静态文件服务

上面列出的每种类型都有内置的类型处理器。您可以创建自己的自定义类型处理器

自定义类型处理器

得益于Dart的类型系统,Alfred拥有一个非常酷的机制,可以根据路由返回的类型自动解析响应。
这些被称为类型处理器

如果您想创建自定义类型处理器,只需将其添加到应用程序对象中的类型处理器
数组中即可。这有点高级,我预计它更多是
为扩展Alfred的开发者准备的

import 'package:alfred/alfred.dart';

class Chicken {
  String get response => 'I am a chicken';
}

void main() {
  final app = Alfred();

  app.typeHandlers.add(TypeHandler<Chicken>((req, res, Chicken val) async {
    res.write(val.response);
    await res.close();
  }));

  /// The app will now return the Chicken.response if you return one from a route

  app.get('/kfc', (req, res) => Chicken()); //I am a chicken;

  app.listen(); //Listening on 3000
}

静态文件、上传和删除

这个很简单——只需传入一个公共路径和一个Dart Directory对象,Alfred就会
处理其余的。

import 'dart:io';

import 'package:alfred/alfred.dart';

void main() async {
  final app = Alfred();

  /// Note the wildcard (*) this is very important!!
  app.get('/public/*', (req, res) => Directory('test/files'));

  await app.listen();
}

您也可以传入一个目录和一个POST或PUT命令,并将文件上传到本地目录,如果您
使用的是multipart/form编码。只需将字段指定为file

import 'dart:io';

import 'package:alfred/alfred.dart';

void main() async {
  final app = Alfred();

  app.post('/public', (req, res) => Directory('test/files'));

  await app.listen();
}

如果您想删除一个文件?

import 'dart:async';
import 'dart:io';

import 'package:alfred/alfred.dart';

FutureOr isAuthenticatedMiddleware(HttpRequest req, HttpResponse res) {
  if (req.headers.value('Authorization') != 'MYAPIKEY') {
    throw AlfredException(
        401, {'error': 'You are not authorized to perform this operation'});
  }
}

void main() async {
  final app = Alfred();

  /// Note the wildcard (*) this is very important!!
  ///
  /// You almost certainly want to protect this endpoint with some middleware
  /// to authenticate a user.
  app.delete('/public/*', (req, res) => Directory('test/files'),
      middleware: [isAuthenticatedMiddleware]);

  await app.listen();
}

安全性?构建一个中间件函数来对用户进行身份验证等。

文件下载

如上所述——如果您想返回一个文件,只需从路由回调中返回它即可。
但是浏览器可能会尝试在浏览器中渲染它,而不是下载它。

您可以设置正确的标头,但有一个方便的小助手可以为您完成所有工作。

请参阅下面的res.setDownload

import 'dart:io';

import 'package:alfred/alfred.dart';

void main() async {
  final app = Alfred();

  app.get('/image/download', (req, res) {
    res.setDownload(filename: 'image.jpg');
    return File('test/files/image.jpg');
  });

  await app.listen(); //Listening on port 3000
}

错误处理

您可以自己设置响应对象的状��码并手动发送数据,或者
您可以从任何路由执行此操作

app.get(“/”,(req, res) => throw AlfredException(400, {“message”: “invalid request”}));

如果任何路由抛出了未处理的错误,它将捕获该错误并抛出500错误。

如果您想处理抛出500错误时的逻辑,可以在
实例化应用程序时添加自定义处理程序。例如

import 'dart:async';

import 'package:alfred/alfred.dart';

void main() async {
  final app = Alfred(onInternalError: errorHandler);
  await app.listen();
  app.get('/throwserror', (req, res) => throw Exception('generic exception'));
}

FutureOr errorHandler(HttpRequest req, HttpResponse res) {
  res.statusCode = 500;
  return {'message': 'error not handled'};
}

404处理

404处理与500错误处理(或未捕获的错误处理)相同。存在默认的
行为,但如果您想覆盖它,只需在应用程序声明中处理它即可。

import 'dart:async';
import 'dart:io';

import 'package:alfred/alfred.dart';

void main() async {
  final app = Alfred(onNotFound: missingHandler);
  await app.listen();
}

FutureOr missingHandler(HttpRequest req, HttpResponse res) {
  res.statusCode = 404;
  return {'message': 'not found'};
}

但是MongoDB或PostgreSQL或怎么办?

另外两个启发这个项目启动的系统——Aqueduct和Angel——都内置了
某种数据库集成。

您不需要这个。

直接访问您想要的数据库系统的Dart驱动程序,它们都在幕后使用它们

您会没事的。我就是这样使用的,而且它们确实有效。

我创建了自己的类,充当一种 ORM,尤其是在 MongoDB 方面。它的效果出奇地好
而且不需要太多代码。

我想做的事情没有列出

虽然内置了很多辅助函数——但您仍然可以直接访问可用
来自 dart:io 包的底层 API。所有辅助函数只是对…的扩展方法

因此,您可以组合并编写您能想象到的任何内容。如果您有想做的事情
在库中没有明确列出,您将能够通过最少的研究来完成
底层库。架构的核心部分是为了不将您局限于一种方式。

WebSockets

Alfred 也支持 WebSockets!

示例中有一个快速的聊天客户端

import 'dart:async';
import 'dart:io';

import 'package:alfred/alfred.dart';
import 'package:alfred/src/type_handlers/websocket_type_handler.dart';

Future<void> main() async {
  final app = Alfred();

  // Path to this Dart file
  var dir = File(Platform.script.path).parent.path;

  // Deliver web client for chat
  app.get('/', (req, res) => File('$dir/chat-client.html'));

  // Track connected clients
  var users = <WebSocket>[];

  // WebSocket chat relay implementation
  app.get('/ws', (req, res) {
    return WebSocketSession(
      onOpen: (ws) {
        users.add(ws);
        users
            .where((user) => user != ws)
            .forEach((user) => user.send('A new user joined the chat.'));
      },
      onClose: (ws) {
        users.remove(ws);
        users.forEach((user) => user.send('A user has left.'));
      },
      onMessage: (ws, dynamic data) async {
        users.forEach((user) => user.send(data));
      },
    );
  });

  final server = await app.listen();

  print('Listening on ${server.port}');
}

日志记录

有关日志记录的更多详细信息,请点击此处

打印路由

想快速打印出已注册的路由吗?(服务器启动时推荐)
调用 Alfred.printRoutes,例如

import 'package:alfred/alfred.dart';

void main() async {
  final app = Alfred();

  app.get('/html', (req, res) {});

  app.printRoutes(); //Will print the routes to the console

  await app.listen();
}

多线程和 Isolates

您可以在多线程模式下使用该应用程序。以这种方式生成时,请求会均匀分布
在各个 Isolates 之间。Alfred 在如何管理 Isolates 方面并不特别具有指导性
只是“它有效”当您启动多个时。

import 'dart:isolate';

import 'package:alfred/alfred.dart';

Future<void> main() async {
  // Fire up 5 isolates
  for (var i = 0; i < 5; i++) {
    unawaited(Isolate.spawn(startInstance, ''));
  }
  // Start listening on this isolate also
  startInstance(null);
}

/// The start function needs to be top level or static. You probably want to
/// run your entire app in an isolate so you don't run into trouble sharing DB
/// connections etc. However you can engineer this however you like.
///
void startInstance(dynamic message) async {
  final app = Alfred();

  app.all('/example', (req, res) => 'Hello world');

  await app.listen();
}

/// Simple function to prevent linting errors, can be ignored
void unawaited(Future future) {}

部署

有很多方法可以解决这个问题,您可以自己将源代码上传到 VPS,在本地构建一个二进制文件并将其上传到某个服务器,但一种相当优雅的方式来完成生产级别的部署是将服务器的 AOT 构建容器化并在 PAAS 上运行。

幸运的是,有一个教程!
https://ryan-knell.medium.com/build-and-deploy-a-dart-server-using-alfred-docker-and-google-cloud-run-from-start-to-finish-d5066e3ab3c6

贡献

欢迎并鼓励提交 PR!这是一个社区项目,只要 PR 符合列出的关键原则,它很可能会被接受。如果您有想添加的改进但不太确定,请在 issues 部分联系我们。

在提交代码之前,您可以运行 ci_checks.sh shell 脚本,它将执行 CI 套件将执行的许多测试。

GitHub

https://github.com/rknell/alfred