使用 Deno 和 Oak 构建内存 REST API





5.00/5 (1投票)
使用 Deno 和 Oak 构建 REST API 的概述。
欢迎来到使用 Deno 创建基本 REST API 的世界。Deno 是基于 Chrome v8 运行时最有前途的服务器端语言(Node.js 的替代品)。
Deno 是什么?
如果您熟悉 Node.js,即流行的服务器端 JavaScript 生态系统,那么 Deno 就和 Node 一样。只是在很多方面都得到了深入的改进。
- 它基于 JavaScript 语言的现代特性。
- 它有一个广泛的标准库。
- 它以 TypeScript 为核心,这在许多不同方面都带来了巨大的优势,包括一流的 TypeScript 支持(您无需单独编译 TypeScript,Deno 会自动完成)。
- 它支持 ES 模块。
- 它没有包管理器。
- 它有一流的 await。
- 它内置了测试功能。
- 它致力于尽可能与浏览器兼容,例如通过提供
它会取代 Node.js 吗?
不会。Node.js 无处不在,是一个成熟的、得到令人难以置信的支持的技术,将在未来几十年内继续存在。
Deno 可以被视为 Node.js 的一种替代语言。
在深入研究代码之前,让我们先看看我们将要创建的 API 的结构。
对于我们的演示代码,我们将为我们的“绘图应用程序/构思可视化应用程序”创建一个具有内存存储的基本后端。
Info(信息)
Deno 默认使用 TypeScript。我们也可以选择使用 JavaScript。在本文中,我们将坚持使用 TypeScript。因此,如果您的 TypeScript 生疏了,请快速复习一下。
数据结构
表示一条记录的数据结构包含以下属性
{
id,
type,
text
}
我们的后端数据存储是我们简单的普通数组,如下所示
[
{
id: "1",
type: "rect",
text: "Idea 2 Visuals",
},
{
id: "2",
type: "circle",
text: "Draw",
},
{
id: "3",
type: "circle",
text: "elaborate",
},
{
id: "4",
type: "circle",
text: "experiment",
},
]
我们的目标是创建 REST API 来对上述数组执行 CRUD 操作(如果您好奇,可以随意使用后端数据库)。
让我们先尝试一下 API,以便您能体验到我们正在编码的后端。
GET 请求
按 ID 获取
添加一个形状 (POST)
将 content-type 设置为 application/json
,如下所示(如果您使用的是 postman 或其他工具)
设置 post 主体
让我们查询所有形状,以验证我们新添加的记录是否已成功保存。
更新一个形状 (PUT)
首先,让我们验证 ID 为 2 的形状。
不要忘记设置 content type(请参阅上面的 POST
部分)。
让我们发出 PUT
请求进行更新。
让我们通过 get
请求验证更新。
删除
让我们删除 ID = 3 的记录。请注意我们正在传递的 URL 参数。
让我们开始编写代码
首先,从 https://github.com/denoland/deno_install 安装 deno。
安装后,您可以通过运行以下命令来验证安装
deno
上面的命令应该会启动 REPL。暂时退出 REPL。
一个快速的 deno 魔术。您可以从 URL 运行程序。例如,尝试以下代码
deno run https://deno.land/std/examples/welcome.ts
您将获得如下所示的输出
构建我们的 REST API
要构建我们的 API,我们将使用 OAK 框架和 TypeScript。Oak 是一个受 Koa 框架启发的中间件。
Oak:Deno 网络服务器的中间件框架 🦕
https://github.com/oakserver/oak
启动您喜欢的编辑器并创建一个 app.ts 文件(因为我们使用的是 TypeScript),然后创建以下三个文件
- app.ts
- routes.ts
- controller.ts
app.ts 文件将是我们应用程序的入口点。routes.ts 定义 REST 路由,controller.ts 包含路由的代码。
让我们开始导入 oak 中的 Application 对象。
import { Application } from 'https://deno.land/x/oak/mod.ts'
Application
类包装了 http
包中的 serve()
函数。它有两个方法:.use()
和 .listen()
。中间件通过 .use()
方法添加,.listen()
方法将启动服务器并开始处理已注册中间件的请求。
让我们设置一些将被应用程序使用的环境变量,特别是 HOST
和 PORT
。
const env = Deno.env.toObject()
const HOST = env.HOST || '127.0.0.1'
const PORT = env.PORT || 7700
下一步是创建一个 Application 实例并启动我们的服务器。但请注意,我们的服务器将无法运行,因为我们需要一个中间件来处理我们的请求(因为我们将使用 Oak)。
const app = new Application();
// routes config goes here
console.log(`Listening on port ${PORT}...`)
await app.listen(`${HOST}:${PORT}`)
在这里,我们创建一个新的 Application
实例,并在指定的 host 和 port 上监听 app 对象。
下一步是创建路由和控制器(注意:控制器不是强制性的,但我们根据职责分离代码)。
路由
路由代码非常易于理解。请注意,为了保持代码整洁,实际的请求/响应处理代码是从 controller.ts 加载的。
注意:
Deno 使用 URL 来导入模块。
import { Router } from 'https://deno.land/x/oak/mod.ts'
import { getShapes, getShape, addShape, updateShape, deleteShape
} from './controller.ts'
const router = new Router()
router.get('/shapes', getShapes)
.get('/shapes/:id', getShape)
.post('/shapes', addShape)
.put('/shapes/:id', updateShape)
.delete('/shapes/:id', deleteShape)
export default router
在这里,我们首先从 oak 包导入 {Router
}。为了使此代码正常工作,我们必须在 controller.ts 中创建 getShapes
、getShape
、addShape
、updateShape
、deleteShape
方法,我们稍后将进行操作。
然后我们创建一个路由器实例,并挂载到 get
、post
、put
和 delete
方法。动态查询字符串参数用“:
”表示。
最后,我们导出路由器,以便其他模块可以导入它。
现在,在开始处理最后一项,controller.ts 之前,让我们为我们的 app.ts 填充剩余的代码,如下所示
import { Application } from 'https://deno.land/x/oak/mod.ts'
import router from './routes.ts'
const env = Deno.env.toObject()
const HOST = env.HOST || '127.0.0.1'
const PORT = env.PORT || 7700
const app = new Application();
app.use(router.routes())
app.use(router.allowedMethods())
console.log(`Listening on port ${PORT}...`)
await app.listen(`${HOST}:${PORT}`)
在这里,我们使用 import
语句通过传递相对路径来导入路由文件。
import router from './routes.ts'
然后,我们通过以下方法调用加载中间件
app.use(router.routes())
app.use(router.allowedMethods())
allowedMethods
根据 oak 文档,需要传递 allowedMethods
。它也接受一个参数,该参数可以进一步自定义。
让我们开始编写控制器代码。
controller.ts
我们首先为我们的模型创建一个接口。我们将称之为 IShape
,因为我们正在处理绘图/绘图应用程序。
interface IShape {
id: string;
type: string;
text: string;
}
让我们模拟一个内存存储。当然,数组在这里很出色。所以,我们将创建一个带有某些示例数据的数组。
let shapes: Array<IShape> = [
{
id: "1",
type: "rect",
text: "Idea 2 Visuals",
},
{
id: "2",
type: "circle",
text: "Draw",
},
{
id: "3",
type: "circle",
text: "elaborate",
},
{
id: "4",
type: "circle",
text: "experiment",
},
]
现在,在开始处理主 API 代码之前,让我们编写一个辅助方法,使用 ID 作为参数来获取记录。此方法将在 delete
和 update
方法中使用。
// Helper method to find record by id
const findById = (id: string): ( IShape | undefined ) =>{
return shapes.filter(shape => shape.id === id )[0]
}
在这里,我们只是使用了老式的数组 filter 方法。但正如您可能知道的,filter
方法总是返回一个数组,即使只有一个结果,我们也使用 [0]
来获取第一个元素。
提示
拥有一个一致的方法命名和返回值约定对我们的方法来说是个好主意。在大多数框架和库中,findById
被认为是只返回一条记录/块。
现在已经完成了初步工作,让我们通过实现“GET
”方法来开始我们的 API。如果您还记得我们的路由讨论,对 /shapes
URL 的请求会期望调用 getShapes
方法。
关于请求/响应的说明
默认情况下,所有 Oak 请求都可以访问 context
对象。context
对象公开了以下重要属性
app
– 调用此中间件的 Application 的引用request
– Request 对象,包含请求的详细信息response
– Response 对象,将用于形成发送回请求者/客户端的响应params
– 由路由添加state
– 应用程序状态的记录
GET /shapes
getShapes
方法非常简单。如果我们这里有一个真实的数据库,那么您将只在此方法中获取所有记录(或根据分页的记录)。
但在我们的例子中,数组 shapes
就是数据存储。我们通过将 response.body
设置为要返回的数据,即将完整的“shapes
”数组,将数据返回给调用者。
const getShapes = ({ response }: { response: any }) => {
response.body = shapes
}
response
response
对象由 http/oak 框架传递给我们。此对象将用于将响应发送回请求者。它还传递一个请求对象,我们稍后将对其进行检查。
GET /shapes/2
一个好的 API 服务器也应该能够只获取选定的记录。这就是第二个 GET
方法的作用。在这里,我们将参数 id
作为查询字符串传递给 /shapes
路由。
const getShape = ({ params, response }: { params: { id: string }; response: any }) => {
const shape: IShape | undefined = findById(params.id)
if (shape) {
response.status = 200
response.body = shape
} else {
response.status = 404
response.body = { message: `Shape not found.` }
}
}
在 getShape
方法中,我们解构了 params
和 response
对象。如果找到了 shape
,我们则发送 200 OK 状态以及响应体中的 shape。否则,则返回 404 错误。
POST /shapes
在 REST 世界中,要创建新的资源/记录,我们使用 POST
方法。POST
方法在主体中接收其参数,而不是在 URL 中。
让我们看看代码
// Create a new shape
const addShape = async ({request, response}: {request: any; response: any}) => {
const body = await request.body()
const shape: IShape = body.value
shapes.push(shape)
response.body = {
message: "OK"
}
response.status = 200
}
请注意,我们使用 await request.body()
,因为 body()
方法是 async
的。我们获取值并将其推回到数组,然后以 **OK** 消息进行响应。
PUT /shapes (更新)
PUT
/PATCH
方法用于更新记录/实体。
// Update an existing shape data
const updateShape = async ({ params, request, response }:
{ params: { id: string }; request: any; response: any }) => {
let shape: IShape | undefined = findById(params.id)
if (shape) {
const body = await request.body()
const updates: { type?: string; text?: string } = body.value
shape = {...shape, ...updates} // update
// Update the shape back to array
shapes = [...shapes.filter(s => s.id !== params.id), shape]
response.status = 200
response.body = {
message: "OK"
}
} else {
response.status = 404;
response.body = {
message: "Shape not found"
}
}
}
update
方法看起来相当复杂,但实际上很简单。让我简单概述一下过程。
- 通过 ID 获取要编辑的实体/资源
- 如果找到,则获取请求的主体,其中包含 JSON 格式的更新数据(在请求头中设置
content-type: application/json
) - 获取更新后的哈希值。
- 将当前找到的
shape
对象与更新后的值合并。 - 此时,“
shape
”变量包含最新的更新。 - 将更新后的“
shape
”合并回“shapes
”数组。 - 将响应发送回客户端。
现在,最后,让我们看看 DELETE
方法。
DELETE (/shapes)
delete
方法很简单。使用 ID 获取要删除的 shape
。在这种情况下,我们只需过滤掉被删除的记录。
// Delete a shape by it's ID
const deleteShape = ({ params, response }: { params: { id: string }; response: any }) => {
shapes = shapes.filter(shape => shape.id !== params.id)
response.body = { message: 'OK' }
response.status = 200
}
注意:在实际项目中,请在服务器端进行必要的验证。
运行服务器
deno run --allow-env --allow-net app.ts
注意:出于安全原因,Deno 不允许程序在未经明确许可的情况下访问网络。要允许访问网络,请使用命令行标志
您就拥有了一个正在运行的 deno REST API 服务器。🦕
完整源代码可以在 https://github.com/rajeshpillai/deno-inmemory-rest-api 找到。
历史
- 2020 年 7 月 3 日:初始版本