Alfred
一个高性能、类express.js的服务器框架,易于使用,并且集成了一切所需。
快速入门
import 'package:alfred/alfred.dart';
void main() async {
final app = Alfred();
app.get('/example', (req, res) => 'Hello world');
await app.listen();
}
核心原则
- 最少的依赖,
- 最少的代码,并贴近Dart核心库——易于维护!
- 易用性
- 可预测的、成熟的语义
- 90%以上您所需的功能都已准备就绪
用法概述
如果您曾使用过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.send或res.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.send或res.json等——尽管您仍然可以这样做。
每个路由都接受一个Future作为响应。目前,您可以传递以下内容,它将得到适当的处理
List<dynamic>– JSONMap<String, Object?>– JSONString– 纯文本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驱动程序,它们都在幕后使用它们
- MongoDB – https://pub.dev/packages/mongo_dart
- PostgreSQL – https://pub.dev/packages/postgres
- SQLLite – https://pub.dev/packages/sqlite3
您会没事的。我就是这样使用的,而且它们确实有效。
我创建了自己的类,充当一种 ORM,尤其是在 MongoDB 方面。它的效果出奇地好
而且不需要太多代码。
我想做的事情没有列出
虽然内置了很多辅助函数——但您仍然可以直接访问可用
来自 dart:io 包的底层 API。所有辅助函数只是对…的扩展方法
- HttpRequest:https://api.dart.ac.cn/stable/2.10.5/dart-io/HttpRequest-class.html
- HttpResponse:https://api.dart.ac.cn/stable/2.10.5/dart-io/HttpResponse-class.html
因此,您可以组合并编写您能想象到的任何内容。如果您有想做的事情
在库中没有明确列出,您将能够通过最少的研究来完成
底层库。架构的核心部分是为了不将您局限于一种方式。
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 上运行。
贡献
欢迎并鼓励提交 PR!这是一个社区项目,只要 PR 符合列出的关键原则,它很可能会被接受。如果您有想添加的改进但不太确定,请在 issues 部分联系我们。
在提交代码之前,您可以运行 ci_checks.sh shell 脚本,它将执行 CI 套件将执行的许多测试。