Lucifer Lightbringer

Lucifer 是一个快速、轻量级的 Dart Web 框架。

它构建在原生的HttpServer之上,提供了一种简单的方式来满足当今许多现代 Web 服务器的需求。

Lucifer 是开放的、高效的,并内置了许多功能,可以执行许多种操作。

安装

安装 Dart SDK

您可以使用 lucy 命令创建一个新项目。

$ dart pub global activate lucy

$ lucy create desire

第一个命令将激活名为 lucy 的 lucifer 命令行界面 (CLI),使其可以在您的终端中访问。然后 lucy create desiredesire 目录中创建一个名为 desire 的新项目。

您可以随意使用任何您想要的任何项目名称。

开始

现在我们准备好构建我们的 Web 服务器了。

打开项目 bin 目录中的 main.dart 文件,查看一个简单的 lucifer 应用程序的结构。

import 'package:lucifer/lucifer.dart';

void main() {
  final app = App();
  final port = env('PORT') ?? 3000;

  app.use(logger());

  app.get('/', (Req req, Res res) async {
    await res.send('Hello Detective');
  });

  await app.listen(port);
  print('Server running at http://${app.host}:${app.port}');
  app.checkRoutes();
}

通过命令测试运行它。

$ cd desire

$ lucy run

并在浏览器中打开 URL https://:3000

如果一切顺利,它将显示 Hello Detective 并将此消息打印到您的终端。

Server running at https://:3000

基础知识

我们可以通过理解 lucy create 命令生成的 main.dart 代码来学习 Lucifer 的基础。

import 'package:lucifer/lucifer.dart';

void main() async {
  final app = App();
  final port = env('PORT') ?? 3000;

  app.use(logger());

  app.get('/', (Req req, Res res) async {
    await res.send('Hello Detective');
  });

  await app.listen(port);
  
  print('Server running at http://${app.host}:${app.port}');
  app.checkRoutes();
}

这些简短的代码行在后台做了几件事情。

首先,我们导入 lucifer 并通过将一个新的 App 对象赋给 app 来创建一个 Web 应用程序。

import 'package:lucifer/lucifer.dart';

final app = App();

然后,我们通过位于项目根目录的 .env 文件将服务器端口设置为 3000

您可以随意更改为任何您想要的端口号。

final port = env('PORT') ?? 3000;

一旦我们有了 app 对象,我们就使用 app.get() 让它监听路径 / 上的 GET 请求。

app.get('/', (Req req, Res res) async {
  
});

Lucifer 为每个 HTTP 动词提供了自己的方法:get()post()put()delete()patch(),第一个参数对应路由路径。

app.get('/', (Req req, Res res) {

});

app.post('/', (Req req, Res res) {

});

app.put('/', (Req req, Res res) {

});

app.delete('/', (Req req, Res res) {

});

app.patch('/', (Req req, Res res) {

});

紧接着,我们看到一个回调函数,当一个传入请求被处理时,它将被调用,并随之发送响应。

要处理传入的请求并发送响应,我们可以在其中编写代码。

app.get('/', (Req req, Res res) async {
  await res.send('Hello Detective');
});

Lucifer 提供了两个对象,reqres,它们代表 ReqRes 实例。

Req 是一个基于原生 HttpRequest 构建的 HTTP 请求。它包含了关于传入请求的所有信息,例如请求参数、查询字符串、标头、正文等等。

Res 是一个基于原生 HttpResponse 构建的 HTTP 响应。它主要用于操作要发送给客户端的响应。

我们之前所做的是使用 res.send() 向客户端发送一个字符串消息 Hello Detective。此方法将字符串设置在响应正文中,然后关闭连接。

我们代码的最后一行启动服务器并监听指定 port 上的传入请求。

await app.listen(port);
print('Server running at http://${app.host}:${app.port}');

或者,我们可以像这样使用 app.listen()

await app.listen(port, '127.0.0.1');

await app.listen(port, () {
  print('Server running at http://${app.host}:${app.port}');
});

await app.listen(port, 'localhost', () {
  print('Server running at http://${app.host}:${app.port}');
});

环境变量

环境变量是进程(例如,ENV、PORT 等)已知的一组变量。建议在开发过程中通过从 .env 文件读取来模拟生产环境。

当我们使用 lucy create 命令时,会在项目根目录中创建一个 .env 文件,其中包含这些默认值。

ENV = development
PORT = 3000

然后通过 env() 方法从 Dart 代码中调用它。

void main() {
  final app = App();
  final port = env('PORT') ?? 3000; // get port from env

  // get ENV to check if it's in a development or production stage
  final environment = env('ENV'); 

  ...
}

为了我们自己的安全,我们应该使用环境变量来处理重要的事情,例如数据库配置和 JSON Web Token (JWT) 密钥。

还有一件事,如果您打开根目录中的 .gitignore 文件,您可以看到 .env 已包含在其中。这意味着我们的 .env 文件不会上传到 github 等远程仓库,并且其中的值永远不会暴露给公众。

请求参数

对所有请求对象属性及其使用方法的简单参考。

我们之前已经了解到 req 对象保存了 HTTP 请求信息。req 有一些您可能会在应用程序中访问的属性。

属性 描述
app 持有 Lucifer app 对象的引用
uriString 请求的 URI 字符串
path URL 路径
method 正在使用的 HTTP 方法
params 路由命名的参数
query 一个包含请求中使用的所有查询字符串的 map 对象
body 包含在请求正文中提交的数据(必须先解析才能访问)
cookies 包含请求发送的 cookie(需要 `cookieParser` 中间件)
protocol 请求协议(http 或 https)
secure 如果请求安全(使用 HTTPS),则为 true

GET 查询字符串

现在我们将看到如何检索 GET 查询参数。

查询字符串是 URL 路径之后的部分,以问号 ? 开头,例如 ?username=lucifer

可以使用字符 & 添加多个查询参数。

?username=lucifer&age=10000

我们如何获取这些值?

Lucifer 提供了一个 req.query 对象,可以轻松获取这些查询值。

app.get('/', (Req req, Res res) {
  print(req.query);
});

这个对象包含每个查询参数的 map。

如果没有查询,它将是一个空 map 或 {}

我们可以轻松地使用 for 循环来遍历它。这将打印每个查询键及其值。

for (var key in req.query.keys) {
  var value = req.query[key];
  print('Query $key: $value');
}

或者您也可以直接使用 res.q() 访问值。

req.q('username'); // same as req.query['username']

req.q('age'); // same as req.query['age']

POST 请求数据

POST 请求数据由 HTTP 客户端发送,例如从 HTML 表单,或从 Postman 或 JavaScript 代码发出的 POST 请求。

我们如何访问这些数据?

如果它以 Content-Type: application/json 发送为 json,我们需要使用 json() 中间件。

final app = App();

// use json middleware to parse json request body
// usually sent from REST API
app.use(json());
// use xssClean to clean the inputs
app.use(xssClean());

如果它以 urlencoded Content-Type: application/x-www-form-urlencoded 发送,请使用 urlencoded() 中间件。

final app = App();

// use urlencoded middleware to parse urlencoded request body
// usually sent from HTML form
app.use(urlencoded());
// use xssClean to clean the inputs
app.use(xssClean());

现在我们可以从 req.body 访问数据。

app.post('/login', (Req req, Res res) {
  final username = req.body['username'];
  final password = req.body['password'];
});

或者简单地使用 req.data()

app.post('/login', (Req req, Res res) {
  final username = req.data('username');
  final password = req.data('password');
});

除了 json()urlencoded(),还有其他内置的 body 解析器可供我们使用。

  • raw():将请求正文获取为原始字节。
  • text():将请求正文获取为纯字符串。
  • json():解析 json 请求正文。
  • urlencoded():解析 urlencoded 请求正文。
  • multipart():解析 multipart 请求正文。

为了确保核心框架保持轻量级,Lucifer 不会对您的请求正文做任何假设。因此,您可以根据需要在应用程序中选择并应用适当的解析器。

但是,如果您想安全起见并需要能够处理所有形式的请求正文,只需使用 bodyParser() 中间件即可。

final app = App();

app.use(bodyParser());

它将自动检测请求正文的类型,并为每个传入请求使用适当的解析器。

发送响应

在上面的示例中,我们使用了 res.send() 向客户端发送了一个简单的响应。

app.get('/', (Req req, Res res) async {
  await res.send('Hello Detective');
});

如果您传递一个字符串,lucifer 将 Content-Type 标头设置为 text/html

如果您传递一个 map 或 list 对象,它将被设置为 application/json,并将数据编码为 JSON。

res.send() 会自动设置正确的 Content-Length 响应标头。

res.send() 完成后也会关闭连接。

您可以使用 res.end() 方法发送一个空的响应,响应正文中没有任何内容。

app.get('/', (Req req, Res res) async {
  await res.end();
});

另一件事是,您可以直接发送数据,而无需 res.send()

app.get('/string', (req, res) => 'string');

app.get('/int', (req, res) => 25);

app.get('/double', (req, res) => 3.14);

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

app.get('/list', (req, res) => ['Lucifer',  'Detective']);

HTTP 状态响应

您可以使用 res.status() 方法设置 HTTP 状态响应。

res.status(404).end();

或者

res.status(404).send('Not Found');

或者简单地使用 res.sendStatus()

// shortcut for res.status(200).send('OK');
res.sendStatus(200); 

// shortcut for res.status(403).send('Forbidden');
res.sendStatus(403);

// shortcut for res.status(404).send('Not Found');
res.sendStatus(404);

// shortcut for res.status(500).send('Internal Server Error');
res.sendStatus(500);

JSON 响应

除了我们之前使用的 res.send() 方法,我们还可以使用 res.json() 向客户端发送 json 数据。

它接受一个 map 或 list 对象,并使用 jsonEncode() 自动将其编码为 json 字符串。

res.json({ 'name': 'Lucifer', 'age': 10000 });

res.json(['Lucifer', 'Detective', 'Amenadiel']);

Cookies

使用 res.cookie() 来管理您应用程序中的 cookies。

res.cookie('username', 'Lucifer');

此方法接受带有各种选项的其他参数。

res.cookie(
  'username', 
  'Lucifer', 
  domain: '.luciferinheaven.com',
  path: '/admin',
  secure: true,
);

res.cookie(
  'username',
  'Lucifer',
  expires: Duration(milliseconds: DateTime.now().millisecondsSinceEpoch + 900000),
  httpOnly: true,
);

以下是一些您可以使用的 cookie 参数。

Value 类型 描述
domain 字符串 cookie 的域名。默认为应用程序的域名。
expires 日期 cookie 的过期日期(GMT 格式)。如果未指定或设置为 0,则创建会话 cookie,该 cookie 将在客户端关闭浏览器时删除。
httpOnly 布尔值 将 cookie 标记为仅供 Web 服务器访问。
maxAge 整数 方便的选项,用于设置相对于当前时间的毫秒数过期时间。
path 字符串 cookie 的路径。默认为 /。
secure 布尔值 将 cookie 标记为仅用于 HTTPS。
signed 布尔值 指示 cookie 是否应签名。
sameSite 布尔值或字符串 设置 SameSite cookie 的值。

可以使用以下方法删除 cookie。

res.clearCookie('username');

或者清除所有 cookie。

res.clearCookies();

安全 Cookie

您可以使用 secureCookie() 中间件来保护您应用程序中的 cookie。

String cookieSecret = env('COOKIE_SECRET_KEY');

app.use(secureCookie(cookieSecret));

COOKIE_SECRET_KEY 需要在 .env 文件中设置,并且应该是您的应用程序独有的随机字符串。

HTTP 标头

我们可以从 req.headers 获取请求的 HTTP 标头。

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

或者我们使用 req.get() 来获取单个标头值。

app.get('/', (req, res) {
  final userAgent = req.get('User-Agent');

  // same as 

  req.header('User-Agent');
});

要更改响应给客户端的 HTTP 标头,我们可以使用 res.set()res.header()

res.set('Content-Type', 'text/html');

// same as 

res.header('Content-Type', 'text/html');

还有其他方法可以处理 Content-Type 标头。

res.type('.html'); // res.set('Content-Type', 'text/html');

res.type('html'); // res.set('Content-Type', 'text/html');

res.type('json'); // res.set('Content-Type', 'application/json');

res.type('application/json'); // res.set('Content-Type', 'application/json');

res.type('png'); // res.set('Content-Type', 'image/png');

重定向

在 Web 应用程序中进行重定向是很常见的。您可以使用 res.redirect()res.to() 在您的应用程序中重定向响应。

res.redirect('/get-over-here');

// same as 

res.to('/get-over-here');

这将创建一个带有默认 302 状态码的重定向。

我们也可以这样使用它。

res.redirect(301, '/get-over-here');

// same as 

res.to(301, '/get-over-here');

您可以传递绝对路径(/get-over-here)、绝对 URL(https://scorpio.com/get-over-here)、相对路径(get-over-here)或 .. 来返回上一级。

res.redirect('../get-over-here');

res.redirect('..');

或者简单地使用 res.back() 根据客户端在请求标头中发送的 HTTP Referer 值重定向回上一个 URL(如果未设置,则默认为 /)。

res.back();

路由

路由是确定调用 URL 时应该发生什么以及应用程序的哪些部分需要处理请求的过程。

在之前的示例中,我们使用了。

app.get('/', (req, res) async {

});

这会创建一个将根路径 / 和 HTTP GET 方法映射到我们在回调函数中提供的响应的路由。

我们可以使用命名参数来监听自定义请求。

假设我们想提供一个接受字符串作为用户名的个人资料 API,并返回用户详细信息。我们希望字符串参数是 URL 的一部分(而不是作为查询字符串)。

所以我们像这样使用命名参数。

app.get('/profile/:username', (Req req, Res res) {
  // get username from URL parameter
  final username = req.params['username'];

  print(username);
});

您可以在同一个 URL 中使用多个参数,它们会自动添加到 req.params 值中。

或者,您可以使用 req.param() 来访问 req.params 的单个值。

app.get('/profile/:username', (Req req, Res res) {
  // get username from URL parameter
  final username = req.param('username');

  print(username);
});

高级路由

我们可以使用 app.router() 中的 Router 对象来构建一个有组织的路由。

final app = App();
final router = app.router();

router.get('/login', (req, res) async {
  await res.send('Login Page');
});

app.use('/auth', router);

现在登录页面将可以通过 https://:3000/auth/login 访问。

您可以在您的应用程序中注册多个路由器。

final app = App();

final auth = app.router();
final user = app.router();

// register routes for auth

auth.get('/login', (Req req, Res res) async {
  await res.send('Login Page');
});

auth.post('/login', (Req req, Res res) async {
  // process POST login
});

auth.get('/logout', (Req req, Res res) async {
  // process logout
});

// register routes for user

user.get('/', (Req req, Res res) async {
  await res.send('List User');
});

user.get('/:id', (Req req, Res res) async {
  final id = req.param('id');
  await res.send('Profile $id');
});

user.post('/', (Req req, Res res) async {  
  // create user
});

user.put('/:id', (Req req, Res res) async {
  // edit user by id
});

user.delete('/', (Req req, Res res) async {
  // delete all users
});

user.delete(':id', (Req req, Res res) async {
  // delete user
});

// apply the router
app.use('/auth', auth);
app.use('/user', user);

使用 app.router() 是组织端点的好方法。您可以将它们拆分到独立的文件中,以维护清晰、结构化且易于阅读的代码。

组织应用程序的另一种方法是使用 app.route()

final app = App();

app.route('/user')
  .get('/', (Req req, Res res) async {
    await res.send('List User');
  })
  .get('/:id', (Req req, Res res) async {
    final id = req.param('id');
    await res.send('Profile $id');
  })
  .post('/', (Req req, Res res) async {
    // create user
  })
  .put('/:id', (Req req, Res res) async {
    // edit user by id
  })
  .delete('/', (Req req, Res res) async {
    // delete all users
  })
  .delete('/:id', (Req req, Res res) async {
    // delete user
  });

使用这个 app.route(),您也可以使用 Controller。当您构建 REST API 时,这尤其有用。

让我们在 controller 目录中创建一个新的控制器。

class UserController extends Controller {
  UserController(App app) : super(app);

  @override
  FutureOr index(Req req, Res res) async {
    await res.send('User List');
  }

  @override
  FutureOr view(Req req, Res res) async {
    await res.send('User Detail');
  }

  @override
  FutureOr create(Req req, Res res) async {
    await res.send('Create User');
  }

  @override
  FutureOr edit(Req req, Res res) async {
    await res.send('Edit User');
  }

  @override
  FutureOr delete(Req req, Res res) async {
    await res.send('Delete User');
  }

  @override
  FutureOr deleteAll(Req req, Res res) async {
    await res.send('Delete All Users');
  }
}

然后在您的主应用程序中像这样使用它。

final app = App();
final user = UserController(app);

// This will add all associated routes for all methods
app.route('/user', user);

// The 1-line code above is the same as 
// manually adding these yourself
app.route('/user')
  .get('/', user.index)
  .post('/', user.create)
  .delete('/', user.deleteAll)
  .get('/:id', user.view)
  .put('/:id', user.edit)
  .delete('/:id', user.delete);

将路由拆分到其自己的独立控制器中是一个好习惯。

同样,您可以随意向您的 Controller 添加更多方法。

class UserController extends Controller {

  ...

  FutureOr vip(Req req, Res res) async {
    await res.send('List of VIP Users');
  }
}

然后通过链接 app.route() 来应用该方法。

final app = App();
final user = UserController(app);

// this will add route GET /user/vip into your app
// along with all the standard routes above
app.route('/user', user).get('/vip', user.vip);

为了帮助您将 Controller 添加到项目中,Lucifer 提供了另一个命令,如下所示。

$ lucy c post

这些命令将在 /bin/controller 目录中创建一个 post_controller.dart 文件,并自动用样板 PostController 类填充它。

您可以像这样使用它来创建多个 Controller

$ lucy c post news user customer

静态文件

通常会在公共文件夹中包含图片、css 和 javascript,并将其公开。

您可以通过使用 static() 中间件来实现这一点。

final app = App();

app.use(static('public'));

现在,如果您在 public 目录中有一个 index.html 文件,当您访问 https://:3000 时,它将被自动提供。

发送文件

Lucifer 提供了一种简单的方法,可以通过 res.download() 将文件作为附件发送给客户端。

当用户访问发送文件的路由时,浏览器将提示用户下载。它不会在浏览器中显示,而是会保存到本地磁盘。

app.get('/downloadfile', (Req req, Res res) async {
  await res.download('thefile.pdf');

  // same as

  await res.sendFile('thefile.pdf');
});

您可以发送带有自定义文件名的文件。

app.get('/downloadfile', (Req req, Res res) async {
  await res.download('thefile.pdf', 'File.pdf');
});

要处理发送文件时的错误,请使用此方法。

app.get('/downloadfile', (Req req, Res res) async {
  final err = await res.download('./thefile.pdf', 'File.pdf');

  if (err != null) {
    // handle error
  }
});

CORS

在浏览器中运行的客户端应用程序通常只能访问与服务器相同域(源)的资源。

加载图片或脚本/样式通常可以工作,但对另一个服务器的 XHR 和 Fetch 调用会失败,除非服务器实现了允许该连接的方法。

这种方法就是 CORS(跨域资源共享)。

使用 @font-face 加载 Web 字体默认也具有同源策略,以及其他不太流行的功能(如 WebGL 纹理)。

如果您没有设置允许第三方域的 CORS 策略,它们的请求将会失败。

跨域请求失败,如果它被发送

  • 到不同的域
  • 到不同的子域
  • 到不同的端口
  • 到不同的协议

这是为了您的安全,以防止任何恶意用户利用您的资源。

但是,如果您同时控制服务器和客户端,您就有充分的理由允许它们相互通信。

使用 cors 中间件来设置 CORS 策略。

例如,假设您有一个没有 cors 的简单路由。

final app = App();

app.get('/no-cors', (Req req, Res res) async {
  await res.send('Risky without CORS');
});

如果您从另一个域使用 fetch 请求访问 /no-cors,它将引发 CORS 问题。

您只需要使用内置的 cors 中间件并将其传递给请求处理程序即可使其正常工作。

final app = App();

app.get('/yes-cors', cors(), (Req req, Res res) async {
  await res.send('Now it works');
});

您可以通过使用 app.use()cors 应用于所有传入请求。

final app = App();

app.use(cors());

app.get('/', (Req req, Res res) async {
  await res.send('Now all routes will use cors');
});

默认情况下,cors 会设置跨域标头以接受任何传入请求。您可以将其更改为仅允许一个源并阻止所有其他源。

final app = App();

app.use(cors(
  origin: 'https://luciferinheaven.com'
));

app.get('/', (Req req, Res res) async {
  await res.send('Now all routes can only accept request from https://luciferinheaven.com');
});

您还可以将其设置为允许多个源。

final app = App();

app.use(cors(
  origin: [
    'https://yourfirstdomain.com',
    'https://yourseconddomain.com',
  ],
));

app.get('/', (Req req, Res res) async {
  await res.send('Now all routes can accept request from both origins');
});

会话

我们需要使用会话来在多个请求之间识别客户端。

默认情况下,Web 请求是无状态的、顺序的,两个请求无法相互关联。无法知道请求是否来自之前执行过另一个请求的客户端。

除非我们使用某种魔力,否则用户无法被识别。

这就是会话(JSON Web Token 也是如此)。

如果处理得当,您的应用程序或 API 的每个用户都将被分配一个唯一的会话 ID,这使您可以存储用户状态。

我们可以使用 lucifer 中内置的 session 中间件。

final app = App();

app.use(session(secret: 'super-s3cr3t-key'));

现在您应用程序中的所有请求都将使用会话。

secret 是唯一必需的参数,但还有许多其他参数可供您使用。secret 应该使用随机字符串,是您的应用程序独有的。或者使用从 randomkeygen 生成的字符串。

此会话现在已激活并附加到请求。您可以从 req.session() 访问它。

app.get('/', (Req req, Res res) {
  print(req.session()); // print all session values
});

要从会话中获取特定值,可以使用 req.session(name)

final username = req.session('username');

或者使用 req.session(name, value) 来添加(或替换)会话中的值。

final username = 'lucifer';

req.session('username', username);

会话可用于在中间件之间通信数据,或在下一个请求中稍后检索它。

我们将这些会话存储在哪里?

嗯,这取决于我们为会话设置的配置。

它可以存储在

  • 内存:这是默认设置,但在生产环境中不要使用。
  • 数据库:如 Postgres、SQLite、MySQL 或 MongoDB。
  • 内存缓存:如 Redis 或 Memcached。

上述所有会话存储都只在 cookie 中设置会话 ID,并将实际数据保留在服务器端。

客户端将收到此会话 ID,并在其每个后续 HTTP 请求中将其发送回来。然后服务器可以使用它来获取与这些会话关联的存储数据。

内存是会话的默认设置,它非常简单,无需您进行任何设置。但是,不建议在生产环境中使用。

最有效的方式是使用 Redis 等内存缓存,但这需要您在设置基础架构方面付出更多的努力。

JSON Web Token

JSON Web Token (JWT) 是一种开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用于在各方之间安全地传输信息作为 JSON 对象。

这些信息由于经过数字签名,因此可以被验证和信任。JWT 可以使用密钥(使用 HMAC 算法)或使用 RSA 或 ECDSA 的公钥/私钥对进行签名。

您可以通过使用 Jwt 的实例来签名和验证令牌,从而在 Lucifer 中使用 JWT 功能。请记住将 jwt 密钥放在环境变量中。

final app = App();
final port = env('PORT') ?? 3000;

final jwt = Jwt();
final secret = env('JWT_SECRET');

app.get('/login', (Req req, Res res) {

  ...

  final payload = <String, dynamic>{
    'username': 'lucifer',
    'age': 10000,
  };

  final token = jwt.sign(
    payload, 
    secret, 
    expiresIn: Duration(seconds: 86400),
  );

  // Send token to the client by putting it  
  // into 'x-access-token' header
  res.header('x-access-token', token);

  ...

});

使用 jwt.verify() 来验证令牌。

final app = App();
final port = env('PORT') ?? 3000;

final jwt = Jwt();
final secret = env('JWT_SECRET');

app.get('/', (Req req, Res res) {
  // Get token from 'x-access-token' header
  final token = req.header('x-access-token');

  try {
    final data = jwt.verify(token, secret);

    if (data != null) {
      print(data['username']);
    }
  } on JWTExpiredError {
    // handle JWTExpiredError
  } on JWTError catch (e) {
    // handle JWTError
  } on Exception catch (e) {
    // handle Exception
  }

  ...

});

验证令牌的另一种方法。

final app = App();
final port = env('PORT') ?? 3000;

final jwt = Jwt();
final secret = env('JWT_SECRET');

app.get('/', (Req req, Res res) {
  // Get token from client 'x-access-token' header
  final token = req.header('x-access-token');

  jwt.verify(token, secret, (error, data) {
    if (data != null) {
      print(data['username']);
    }

    if (error != null) {
      print(error);
    }
  });

  ...

});

中间件

中间件是钩入路由过程的函数。它在执行路由回调处理程序之前执行一些操作。

中间件通常用于编辑请求或响应对象,或在请求到达路由回调之前终止请求。

您可以像这样添加中间件。

app.use((Req req, Res res) async {
  // do something
});

这与定义路由回调有些相似。

大多数时候,您将足够使用 Lucifer 提供的内置中间件,例如我们之前使用的 staticcorssession

但是,如果您需要,您可以轻松创建自己的中间件,并通过将其放置在路由和回调处理程序之间来为特定路由使用它。

final app = App();

// create custom middleware
final custom = (Req req, Res res) async {
  // do something here
};

// use the middleware for GET / request
app.get('/', custom, (Req req, Res res) async {
  await res.send('angels');
});

您可以将多个中间件应用于任何您想要的路由。

final app = App();

final verifyToken = (Req req, Res res) async {
  // do something here
};

final authorize = (Req req, Res res) async {
  // do something here
};

app.get('/user', [ verifyToken, authorize ], (req, res) async {
  await res.send('angels');
});

如果您想在中间件中保存数据并在其他中间件或路由回调中访问它,请使用 res.local()

final app = App();

final verifyToken = (Req req, Res res) async {
  // saving token into the local data
  res.local('token', 'jwt-token');
};

final authorize = (Req req, Res res) async {
  // get token from local data
  var token = res.local('token');
};

app.get('/user', [ verifyToken, authorize ], (req, res) async {
  // get token from local data
  var token = res.local('token');
});

这些中间件没有 next() 调用(不像 Express 等其他框架)。

下一个的处理由 lucifer 自动处理。

lucifer 应用程序将始终在当前处理堆栈中运行到下一个中间件或回调……

除非您在中间件中向客户端发送了任何响应,这将关闭连接并自动停止所有后续中间件/回调的执行。

由于这些调用是自动的,您需要记住在调用异步函数时使用适当的 async await

例如,在使用 res.download() 将文件发送给客户端时。

app.get('/download', (Req req, Res res) async {
  await res.download('somefile.pdf');
});

一个简单的规则是:如果您调用返回 FutureFutureOr 的函数,请安全起见并使用 async await

如果在测试应用程序的过程中,您在控制台中看到一个带有类似 HTTP headers not mutableheaders already sent 消息的错误,这表明您的应用程序的某些部分需要使用适当的 async await

表单

现在让我们学习如何使用 Lucifer 处理表单。

假设我们有一个 HTML 表单。

<form method="POST" action="/login">
  <input type="text" name="username" />
  <input type="password" name="password" />
  <input type="submit" value="Login" />
</form>

当用户按下提交按钮时,浏览器将自动向同一来源页面的 /login 发出 POST 请求,并将一些数据发送到服务器,数据编码为 application/x-www-form-urlencoded

在这种情况下,数据包含 usernamepassword

表单也可以使用 GET 发送数据,但大多数情况下它会使用标准的 & 安全的 POST 方法。

这些数据将附加在请求正文中。要提取它,您可以使用内置的 urlencoded 中间件。

final app = App();

app.use(urlencoded());
// always use xssClean to clean the inputs
app.use(xssClean());

我们可以测试为 /login 创建一个 POST 端点,并且提交的数据将可以从 req.body 中获得。

app.post('/login', (Req req, Res res) async {
  final username = req.body['username']; // same as req.data('username');
  final password = req.body['password']; // same as req.data('password');

  ...

  await res.end();
});

文件上传

学习如何通过表单处理文件上传。

假设您有一个允许用户上传文件的 HTML 表单。

<form method="POST" action="/upload">
  <input type="file" name="document" />
  <input type="submit" value="Upload" />
</form>

当用户按下提交按钮时,浏览器将自动向同一来源的 /upload 发送 POST 请求,并从文件输入中发送文件。

它不是像通常的标准表单那样以 application/x-www-form-urlencoded 发送,而是以 multipart/form-data 发送。

手动处理 multipart 数据可能很棘手且容易出错,因此我们将使用一个内置的 FormParser 工具,您可以通过 app.form() 访问它。

final app = App();
final form = app.form();

app.post('/upload', (Req req, Res res) async {
  await form.parse(req, (error, fields, files) {
    if (error) {
      print('$error');
    }

    print(fields);
    print(files);
  });
});

您可以为每个文件处理通知事件使用它。这还通知其他事件,例如处理结束时、接收其他非文件字段时,或发生错误时。

final app = App();
final form = app.form();

app.post('/upload', (Req req, Res res) async {
  await form
    .onField((name, field) {
      print('${name} ${field}');
    })
    .onFile((name, file) {
      print('${name} ${file.filename}');
    })
    .onError((error) {
      print('$error');
    })
    .onEnd(() {
      res.end();
    })
    .parse(req);
});

或者像这样使用它。

final app = App();
final form = app.form();

app.post('/upload', (Req req, Res res) async {
  await form
    .on('field', (name, field) {
      print('${name} ${field}');
    })
    .on('file', (name, file) {
      print('${name} ${file}');
    })
    .on('error', (error) {
      print('$error');
    })
    .on('end', () {
      res.end();
    })
    .parse(req);
});

无论哪种方式,您都会获得一个或多个 UploadedFile 对象,这些对象将为您提供有关上传文件的信息。以下是一些您可以使用的值。

  • file.name:获取文件输入中的名称。
  • file.filename:获取文件名。
  • file.type:获取文件的 MIME 类型。
  • file.data:获取上传文件的原始字节数据。

默认情况下,FormParser 只包含原始字节数据,而不将其保存到任何临时文件夹中。您可以像这样轻松地自己处理。

import 'package:path/path.dart' as path;

// file is an UploadedFile object you get before

// save to uploads directory
String uploads = path.absolute('uploads');

// use the same filename as sent by the client,
// but feel free to use other file naming strategy
File f = File('$uploads/${file.filename}');

// check if the file exists at uploads directory
bool exists = await f.exists();

// create file if not exists
if (!exists) {
  await f.create(recursive: true);
}

// write bytes data into the file
await f.writeAsBytes(file.data);

print('File is saved at ${f.path}');

模板

Lucifer 提供默认的模板引擎 Mustache。它使用 mustache_template 包,该包是根据 官方 Mustache 规范 实现的。

默认情况下,为了保持核心框架轻量级,lucifer 不会将任何模板引擎附加到您的应用程序。要使用 mustache 中间件,您需要先应用它。

final app = App();

app.use(mustache());

然后这些 mustache 可以渲染项目中 views 目录中的任何模板。

假设您在 views 目录中有这个 index.html

<!DOCTYPE html>
<html>
  <head></head>
  <body>
    <h2>{{ title }}</h2>
  </body>
</html>

并使用 res.render() 渲染模板。

final app = App();

app.use(mustache());

app.get('/', (Req req, Res res) async {
  await res.render('index', { 'title': 'Hello Detective' });
});

如果您运行命令 lucy run 并在浏览器中打开 https://:3000,它将显示一个 HTML 页面,其中包含 Hello Detective

您可以将默认的 views 更改为您想要的任何其他目录。

final app = App();

// now use 'template' as the views directory
app.use(mustache('template'));

app.get('/', (Req req, Res res) async {
  // can also use res.view()
  await res.view('index', { 'title': 'Hello Detective' });
});

现在,如果您将此 index.html 文件添加到 template 目录中。

<!DOCTYPE html>
<html>
  <head></head>
  <body>
    <h2>{{ title }} from template</h2>
  </body>
</html>

然后运行应用程序并在浏览器中打开它,它将显示另一个 HTML 页面,其中包含 Hello Detective from template

有关使用 Mustache 模板引擎的更完整详细信息,您可以阅读 mustache 手册

要使用其他引擎,例如 jinjajaded,您可以自行管理模板渲染,然后通过调用 res.send() 发送 html。

app.get('/', (Req req, Res res) async {
  // render your jinja/jaded template into 'html' variable
  // then send it to the client
  await res.send(html);
});

或者您可以创建一个自定义中间件来处理您选择的引擎的模板。

这里有一个您可以从 mustache 中间件学习的例子,来创建您自己的自定义模板。

// 
// name it with anything you want
// 
Callback customTemplating([String? views]) {
  return (Req req, Res res) {
    // 
    // you need to overwrite res.renderer
    // using the chosen template engine
    //
    res.renderer = (String view, Map<String, dynamic> data) async {
      // 
      // most of the time, these 2 lines will stay
      // 
      String directory = views ?? 'views';
      File file = File('$directory/$view.html');

      // 
      // file checking also stay
      // 
      if (await file.exists()) {
        // 
        // mostly, all you need to do is edit these two lines 
        // 
        Template template = Template(await file.readAsString());
        String html = template.renderString(data);

        // 
        // in the end, always send the rendered html
        //
        await res.send(html);
      }
    };
  };
}

要应用新的模板中间件,请像以前一样使用 app.use()

final app = App();

app.use(customTemplating());

安全

Lucifer 内置了 security 中间件,它涵盖了数十种标准的安全性保护措施来守护您的应用程序。要使用它们,只需通过 app.use() 将其添加到您的应用程序即可。

final app = App();

app.use(security());

点击此处了解更多关于 Web 安全的信息。

错误处理

Lucifer 会自动处理您应用程序中发生的错误。但是,您可以通过 app.on() 设置自己的错误处理。

final app = App();

app.on(404, (req, res) {
  // handle 404 Not Found Error in here
  // such as, showing a custom 404 page
});

// another way is using StatusCode
app.on(StatusCode.NOT_FOUND, (req, res) { });
app.on(StatusCode.INTERNAL_SERVER_ERROR, (req, res) { });
app.on(StatusCode.BAD_REQUEST, (req, res) { });
app.on(StatusCode.UNAUTHORIZED, (req, res) { });
app.on(StatusCode.PAYMENT_REQUIRED, (req, res) { });
app.on(StatusCode.FORBIDDEN, (req, res) { });
app.on(StatusCode.METHOD_NOT_ALLOWED, (req, res) { });
app.on(StatusCode.REQUEST_TIMEOUT, (req, res) { });
app.on(StatusCode.CONFLICT, (req, res) { });
app.on(StatusCode.UNPROCESSABLE_ENTITY, (req, res) { });
app.on(StatusCode.NOT_IMPLEMENTED, (req, res) { });
app.on(StatusCode.SERVICE_UNAVAILABLE, (req, res) { });

您可以从中间件或回调函数触发 HTTP 异常。

app.get('/unauthorized', (Req req, Res res) async {
  throw UnauthorizedException();
});

以下是我们可用的所有默认异常列表。

BadRequestException
UnauthorizedException
PaymentRequiredException
ForbiddenException
NotFoundException
MethodNotAllowedException
RequestTimeoutException
ConflictException
UnprocessableException
InternalErrorException
NotImplementedException
ServiceUnavailableException

并行处理

Dart/Lucifer 默认支持并行和多线程处理。这可以通过将进程均匀地分布到各种 Isolate 来实现。

这是其中一种方法。

import 'dart:async';
import 'dart:isolate';

import 'package:lucifer/lucifer.dart';

void main() async {
  // Start an app
  await startApp();

  // Spawn 10 new app with each own isolate
  for (int i = 0; i < 10; i++) {
    Isolate.spawn(spawnApp, null);
  }
}

void spawnApp(data) async {
  await startApp();
}

Future<App> startApp() async {
  final app = App();
  final port = env('PORT') ?? 3000;

  app.get('/', (Req req, Res res) async {
    await res.send('Hello Detective');
  });

  await app.listen(port);
  print('Server running at http://${app.host}:${app.port}');

  return app;
}

Web Socket

如果您的 Web 应用程序需要持久的客户端与服务器之间的通信,Web Socket 是必不可少的部分。以下是 Lucifer 中使用 Web Socket 的示例。

import 'dart:io';

import 'package:lucifer/lucifer.dart';

void main() async {
  final app = App();
  final port = env('PORT') ?? 3000;

  app.use(static('public'));

  app.get('/', (Req req, Res res) async {
    await res.sendFile('chat.html');
  });

  app.get('/ws', (Req req, Res res) async {
    List clients = [];

    final socket = app.socket(req, res);

    socket.on('open', (WebSocket client) {
      clients.add(client);
      for (var c in clients) {
        if (c != client) {
          c.send('New human has joined the chat');
        }
      }
    });
    socket.on('close', (WebSocket client) {
      clients.remove(client);
      for (var c in clients) {
        c.send('A human just left the chat');
      }
    });
    socket.on('message', (WebSocket client, message) {
      for (var c in clients) {
        if (c != client) {
          c.send(message);
        }
      }
    });
    socket.on('error', (WebSocket client, error) {
      res.log('$error');
    });
    
    await socket.listen();
  });

  await app.listen(port);
  print('Server running at http://${app.host}:${app.port}');
}

贡献

欢迎您以任何方式为项目做出贡献。这包括代码审查、拉取请求、文档、教程,或报告您在 Lucifer 中发现的错误。

许可证

MIT 许可

版权所有 (c) 2021 Lucifer

特此授予任何人获取此软件及相关文档文件(“软件”)副本的权利,在不受限制的范围内进行处理,包括但不限于使用、复制、修改、合并、发布、分发、再许可和/或出售软件副本的权利,并允许接收软件副本的人这样做,但须满足以下条件:

以上版权声明和本许可声明应包含在软件的所有副本或实质性部分中。

软件按“原样”提供,不提供任何形式的保证,明示或暗示,包括但不限于适销性、特定用途的适用性和非侵权的保证。在任何情况下,作者或版权持有者均不对任何索赔、损害或其他责任负责,无论是合同、侵权或其他行为,包括但不限于软件的使用或交易,
软件。
软件。

GitHub

查看 Github