在Deno中开始使用Oak的详细指南(附代码示例)

1,026 阅读5分钟

Oak是Koa的继任者(Koa是Node.js中Express的精神继任者),当谈到用Deno构建Web应用时,它是最受欢迎的选择。然而,当用Deno说网络应用时,往往不是为了在浏览器中可见的东西(不包括前端应用的服务器端渲染)。相反,Oak是Deno的一个网络应用程序框架,使你能够在Deno中构建服务器应用程序。作为一个后端应用,它是你的前端应用和潜在的数据库或其他数据源(如REST APIs,GraphQL APIs)之间的粘合剂。只是为了给你一个概念,以下是构建客户端-服务器架构的技术栈列表:

  • React.js(前端) + Oak(后端) + PostgreSQL(数据库)
  • Vue.js (前端) + Oak (后端) + MongoDB (数据库)
  • Angular.js (前端) + Oak (后端) + Neo4j (数据库)

Oak可与其他Web应用框架交换后端,就像React.js可与Vue.js和Angular.js交换前端应用一样。Deno生态系统并不只提供一种解决方案,而是提供各种解决方案,这些解决方案都有其优势和劣势。然而,在这个应用中,我们将使用Oak服务器,因为它是用Deno构建JavaScript后端应用时最流行的选择。

Deno中的Oak

让我们先在你的Deno应用程序中使用Oak。在你的src/server.tsTypeScript文件中,使用下面的代码来导入Oak,创建一个Oak应用程序的实例,并将其作为Oak服务器启动:

import { Application } from 'https://deno.land/x/oak/mod.ts';

const port = 8000;
const app = new Application();

app.addEventListener('listen', () => {
  console.log(`Listening on localhost:${port}`);
});

await app.listen({ port });

记住要把事件监听器(addEventListener)放在实际监听(listen)的前面,否则监听器将永远不会被执行。在你的Oak应用程序启动后,所有应该发生的事情都会进入addEventListener 方法的回调函数listen 方法的第一个参数是一个带有端口的配置对象--我们用一个对象中的属性缩写来初始化它--用于运行中的应用程序。这就是为什么在最后启动它之后,应该通过浏览器中的http://localhost:8000

Oak应用程序有两个方法:使用和监听。listen 方法启动服务器并开始用注册的中间件处理请求,而use 方法则首先设置了中间件。在以后更深入地探讨这个话题之前,我们将用Oak设置一个基本的中间件:

import { Application } from 'https://deno.land/x/oak/mod.ts';

const port = 8000;
const app = new Application();

app.use((ctx) => {
  ctx.response.body = 'Hello Deno';
});

app.addEventListener('listen', () => {
  console.log(`Listening on localhost:${port}`);
});

await app.listen({ port });

这个新的中间件作为一个函数将处理所有传入的Oak服务器的请求。自己试试吧,在命令行上运行deno run --allow-net server.ts ,然后访问你的浏览器http://localhost:8000 。你应该看到 "Hello Deno "的文字显示出来。

Oak中的上下文

Oak中的Context表示当前通过Oak中间件的请求。在代码中,你经常看到它是contextctx 。在前面的代码中,我们使用Oak的上下文,通过使用上下文的响应对象的主体,向我们的浏览器返回一个文本:

...

app.use((ctx) => {
  ctx.response.body = 'Hello Deno';
});

...

这是Oak中间件中对上下文最直接的使用之一。上下文拥有几个有用的属性。例如,你可以通过ctx.request 来访问当前来自客户端的请求,同时你也可以通过ctx.response 来决定向客户端返回什么。在接下来的章节中,你将看到如何在不同的用例中使用上下文。

Oak中的中间件

从本质上讲,每个Oak应用程序都是一系列的中间件函数调用。如果有一个以上的中间件,我们需要了解它们是如何被调用的,以及如何确定调用栈的顺序。让我们从一个中间件开始,当在浏览器中访问应用程序时,在命令行中打印HTTP方法和传入请求的URL:

import { Application } from 'https://deno.land/x/oak/mod.ts';

const port = 8000;
const app = new Application();

app.use((ctx) => {
  console.log(`HTTP ${ctx.request.method} on ${ctx.request.url}`);
  ctx.response.body = 'Hello Deno';
});

app.addEventListener('listen', () => {
  console.log(`Listening on localhost:${port}`);
});

await app.listen({ port });

命令行应该输出HTTP GET on http://localhost:8000/ 。每次用户在浏览器中访问一个URL时,都会向Web服务器执行HTTP GET方法。在我们的例子中,HTTP请求只向浏览器返回一个文本,内容是 "Hello Deno"。现在,如果我们有两个中间件而不是一个,会发生什么:

import { Application } from 'https://deno.land/x/oak/mod.ts';

const port = 8000;
const app = new Application();

app.use((ctx) => {
  console.log(`HTTP ${ctx.request.method} on ${ctx.request.url}`);
});

app.use((ctx) => {
  console.log('returning a response ...');
  ctx.response.body = 'Hello Deno';
});

app.addEventListener('listen', () => {
  console.log(`Listening on localhost:${port}`);
});

await app.listen({ port });

命令行应该记录 "HTTP GET on http://localhost:8000/",而不是 "返回一个响应... "的文本。在一系列中间件中的第一个中间件被调用后,Oak就会停止。为了从一个中间件跳到下一个中间件,我们必须使用Oak的next函数与async/await:

...
app.use(async (ctx, next) => {  console.log(`HTTP ${ctx.request.method} on ${ctx.request.url}`);  await next();});
app.use((ctx) => {  console.log('returning a response ...');  ctx.response.body = 'Hello Deno';});
...

现在命令行上的输出应该是这样的:

HTTP GET on http://localhost:8000/
returning a response ...

最后两个中间件都是在彼此之后被调用的。你可以通过移动next 函数的调用来操纵每个中间件的调用顺序:

...

app.use(async (ctx, next) => {
  await next();
  console.log(`HTTP ${ctx.request.method} on ${ctx.request.url}`);
});

app.use((ctx) => {
  console.log('returning a response ...');
  ctx.response.body = 'Hello Deno';
});

...
...

命令行上的输出应该是:

returning a response ...
HTTP GET on http://localhost:8000/

基本上 "next "是你正在调用的一系列中间件中的下一个中间件。如果 "next "发生在使用它的中间件的实际执行逻辑之前(比如上一个例子),那么下一个中间件会在当前中间件的执行逻辑之前执行。

一个中间件函数,因为它是一个函数,所以可以被提取出来,在你的Deno应用程序中作为中间件重用。

import { Application, Context } from 'https://deno.land/x/oak/mod.ts';

const port = 8000;
const app = new Application();

const logging = async (ctx: Context, next: Function) => {
  console.log(`HTTP ${ctx.request.method} on ${ctx.request.url}`);
  await next();
};

app.use(logging);

app.use((ctx) => {
  console.log('returning a response ...');
  ctx.response.body = 'Hello Deno';
});

app.addEventListener('listen', () => {
  console.log(`Listening on localhost:${port}`);
});

await app.listen({ port });

通常抽象的中间件通常可以作为Oak的库。通过使用Oak的use 方法,我们可以选择加入任何第三方的中间件。最终你会在使用Deno进行大型项目时遇到一些这样的中间件。

Oak中的路由

后台的Web应用程序中的路由是用来将URI映射到中间件的。这些URI可以通过REST或GraphQL提供文本信息、HTML页面或JSON数据。在一个较大的应用程序中,这将意味着有几个路由(中间件),映射到几个URI。

在Oak中,路由器中间件是路由所需要的一切,因为路由只是中间件之上的另一个抽象。让我们用Oak的Router设置这样一个单一的路由。

import { Application, Router } from 'https://deno.land/x/oak/mod.ts';

const port = 8000;
const app = new Application();

const router = new Router();

router.get('/', (ctx) => {
  ctx.response.body = 'Hello Deno';
});

app.use(router.routes());
app.use(router.allowedMethods());

app.addEventListener('listen', () => {
  console.log(`Listening on localhost:${port}`);
});

await app.listen({ port });

路由指向你的域的根(/)。在浏览器中,你可以用http://localhost:8000/http://localhost:8000 来访问这个路由,但不要用尾部的斜线。启动Deno应用程序后,访问浏览器,看看它为你输出什么。你应该在那里看到打印的 "Hello Deno"。

路由器中间件还有一些麻烦事。例如,它可以被用于一个以上的URI:

...

router
  .get('/', (ctx) => {
    ctx.response.body = 'Hello Deno';
  })
  .get('/1', (ctx) => {
    ctx.response.body = 'Hello Deno 1';
  })
  .get('/2', (ctx) => {
    ctx.response.body = 'Hello Deno 2';
  });

...

当你在浏览器中访问正在运行的应用程序时,你可以导航到所有这些路径来接收不同的文本。也可以有一个以上的Oak Router,将你的应用程序的路由分组为域。

...

const routerOne = new Router();

routerOne.get('/1', (ctx) => {
  ctx.response.body = 'Hello Deno 1';
});

const routerTwo = new Router();

routerTwo.get('/2', (ctx) => {
  ctx.response.body = 'Hello Deno 2';
});

app.use(routerOne.routes());
app.use(routerOne.allowedMethods());

app.use(routerTwo.routes());
app.use(routerTwo.allowedMethods());

...

从本质上讲,每个Oak应用程序都只是一系列路由和中间件的函数调用。你已经看到了前者,即具有一个或多个路由的路由,以及后者用于启用这些路由或其他实用程序(如日志)。中间件和路由,都可以访问Oak的上下文对象,以处理请求。