目录

【译】Deno + MongoDB 构建 CRUD API

原文地址:Create a server with deno and mongo

原文作者:Kailas Walldoddi

译者:Tony.Xu

https://blog-1253413117.cos.ap-shanghai.myqcloud.com/content/posts/83cfa3bf614b47e6bba62ce40628a4ae.gif

读完这篇文章,你可以掌握:

  • 使用 deno_mongo 操作 mongodb 数据库
  • 构建 CRUD API 来管理员工信息
  • 构建 API 控制器(Controller
  • 使用简易 deno 框架 abc
  • 使用 denv 创建环境变量

准备工作

首先,安装 deno。可以查阅这篇文档,根据当前系统来选择合适的命令行进行安装。

PS: 截止作者(Kailas Walldoddi)写这篇文章时,deno 版本是 1.0.0

PSS:截止译者(Tony.Xu)写这篇译文和运行样例时,deno 版本是 1.1.1deno_mongo 版本是 0.8.0,推荐参考掘金上韩亦乐大佬这篇文章进行安装:Deno 钻研之术:(1) Hello,从多样化安装到简单实战

友情提示:目前 Mac 用户使用 homebrew 安装版本只有 0.4.2,运行本文章代码会报错

本文最后附 Mac 安装 1.1.1 版本 deno 的命令

正式开始

为了实现我们为服务器设计的功能,我们需要一个框架(类似 Node.js 中的 express)。在这里,我们使用 abc 这个简易的 deno 框架来创建 web 应用(除了 abc,你还有很多其他选择,比如:alosaurespressofenoak…… )

首先,我们在项目根目录下新建 .env 文件,用来声明环境变量。

1
2
DB_NAME=deno_demo
DB_HOST_URL=mongodb://localhost:27017

接着,构建异常处理中间件来处理控制器中捕获的报错。utils/middleware.ts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// utils/middleware.ts

import { MiddlewareFunc } from "https://deno.land/x/abc@v1.0.0-rc2/mod.ts";
export class ErrorHandler extends Error {
  status: number;
  constructor(message: string, status: number) {
    super(message);
    this.status = status;
  }
}
export const ErrorMiddleware: MiddlewareFunc = (next: any) =>
  async (c: any) => {
    try {
      await next(c);
    } catch (err) {
      const error = err as ErrorHandler;
      c.response.status = error.status || 500;
      c.response.body = error.message;
    }
  };

然后,编写服务器主程序。server.ts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// server.ts

// 通过 url 直接引入远程模块。首次运行后,deno 会下载并缓存该模块
import { Application } from "https://deno.land/x/abc@v1.0.0-rc2/mod.ts";
// 使用 denv 来加载 .env 中配置的环境变量
import "https://deno.land/x/denv/mod.ts";
// 余下代码和 `express` 几乎一样,没什么特别的。
import {
  fetchAllEmployees,
  createEmployee,
  fetchOneEmployee,
  updateEmployee,
  deleteEmployee,
} from "./controllers/employees.ts";
import { ErrorMiddleware } from "./utils/middlewares.ts";

const app = new Application();

app.use(ErrorMiddleware);

app.get("/employees", fetchAllEmployees)
  .post("/employees", createEmployee)
  .get("/employees/:id", fetchOneEmployee)
  .put("/employees/:id", updateEmployee)
  .delete("/employees/:id", deleteEmployee)
  .start({ port: 5000 });

console.log(`server listening on http://localhost:5000`);

代码第一行,通过 url 直接引入远程模块。首次运行后,deno 会下载并缓存该模块。

代码第二行,使用 denv 来加载 .env 中配置的环境变量。

余下代码和 express 几乎一样,没什么特别的。

接下来我们要为服务器配置 mongodb 连接。幸运的是,已经有现成的 deno 版本的 MongoDB 驱动:deno_mongo。虽然目前 deno_mongo 仍在开发中,并且还未涵盖 mongodb 驱动的全部方法,不过用来做一个小 demo 还是 OK 的。

config/ 目录下新建 db.ts 文件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// /config/db.ts
// 注:原文版本 deno_mongo 0.6.0,与 deno 1.1.1 不兼容,需要升级到 0.8.0
import { init, MongoClient } from "https://deno.land/x/mongo@v0.8.0/mod.ts";

// 注:原文创建 db 实例的操作有点复杂…… 以下参照了 deno_mongo 官方文档
// https://github.com/manyuanrong/deno_mongo/tree/master

// db 名称
const dbName = Deno.env.get("DB_NAME") || "deno_demo";
// db url
const dbHostUrl = Deno.env.get("DB_HOST_URL") || "mongodb://localhost:27017";
// 创建连接
const client = new MongoClient();
// 建立连接
client.connectWithUri(dbHostUrl);
const db = client.database(dbName);
export default db;

接下来,编写控制器,先从新建员工 createEmployee 开始

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// /controllers/employee.ts

import { HandlerFunc, Context } from "https://deno.land/x/abc@v1.0.0-rc2/mod.ts";
import db from '../config/db.ts';
import { ErrorHandler } from "../utils/middlewares.ts";

const employees = db.collection('employees');

// 定义 schema
interface Employee {
  _id: {
    $oid: string;
  };
  name: string;
  age: number;
  salary: number;
}

/**
 * 新增员工
 * @param c Context
 * @returns $oid 目前 deno_mongo 新增时只返回 _id
 */
export const createEmployee: HandlerFunc = async (c: Context) => {
  try {
    if (c.request.headers.get("content-type") !== "application/json") {
      throw new ErrorHandler("Invalid body", 422);
    }
    const body = await (c.body());
    if (!Object.keys(body).length) {
      throw new ErrorHandler("Request body can not be empty!", 400);
    }
    const { name, salary, age } = body;

    const insertedEmployee = await employees.insertOne({
      name,
      age,
      salary,
    });

    return c.json(insertedEmployee, 201);
  } catch (error) {
    throw new ErrorHandler(error.message, error.status || 500);
  }
};

目前 deno_mongo 新增时只返回 _id(希望后续版本会改进这一点)

请求:

https://blog-1253413117.cos.ap-shanghai.myqcloud.com/content/posts/a6ad0f28a0524951ba5f69c2e63d57b6.png

返回:

https://blog-1253413117.cos.ap-shanghai.myqcloud.com/content/posts/999580dafd3f432a820fbf34219028c3.png

查询全部员工:fetchAllEmployees

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
 * 全量查询
 * @param c Context
 * @returns json(Employee[])
 */
export const fetchAllEmployees: HandlerFunc = async (c: Context) => {
  try {
    const fetchedEmployees: Employee[] = await employees.find();

    if (fetchedEmployees) {
      const list = fetchedEmployees.length
        ? fetchedEmployees.map((employee) => {
          const { _id: { $oid }, name, age, salary } = employee;
          return { id: $oid, name, age, salary };
        })
        : [];
      return c.json(list, 200);
    }
  } catch (error) {
    throw new ErrorHandler(error.message, error.status || 500);
  }
};

指定 id 查询员工:fetchOneEmployee

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
/**
 * 指定 id 查询
 * @param c Context
 * @returns json(Employee)
 */
export const fetchOneEmployee: HandlerFunc = async (c: Context) => {
  try {
    const { id } = c.params as { id: string };

    const fetchedEmployee = await employees.findOne({ _id: { "$oid": id } });

    if (fetchedEmployee) {
      const { _id: { $oid }, name, age, salary } = fetchedEmployee;
      return c.json({ id: $oid, name, age, salary }, 200);
    }

    throw new ErrorHandler("Employee not found", 404);
  } catch (error) {
    throw new ErrorHandler(error.message, error.status || 500);
  }
};

请求:

https://blog-1253413117.cos.ap-shanghai.myqcloud.com/content/posts/0c866205c9f243be8dc7d86b3a61468d.png

返回:

https://blog-1253413117.cos.ap-shanghai.myqcloud.com/content/posts/81242556230e4a2a8df3e5aff59c158b.png

更新员工信息: updateEmployee

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
 * 更新员工
 * @param c Context
 * @returns msg string
 */
export const updateEmployee: HandlerFunc = async (c: Context) => {
  try {
    const { id } = c.params as { id: string };
    if (c.request.headers.get("content-type") !== "application/json") {
      throw new ErrorHandler("Invalid body", 422);
    }

    const body = await (c.body()) as {
      name?: string;
      salary: string;
      age?: string;
    };

    if (!Object.keys(body).length) {
      throw new ErrorHandler("Request body can not be empty!", 400);
    }

    const fetchedEmployee = await employees.findOne({ _id: { "$oid": id } });

    if (fetchedEmployee) {
      const { matchedCount } = await employees.updateOne(
        { _id: { "$oid": id } },
        { $set: body },
      );
      if (matchedCount) {
        return c.string("Employee updated successfully!", 204);
      }
      return c.string("Unable to update employee");
    }
    throw new ErrorHandler("Employee not found", 404);
  } catch (error) {
    throw new ErrorHandler(error.message, error.status || 500);
  }
};

更新成功后回返回对象包含三个字段:

  • matchedCount
  • modifiedCount
  • upsertedId

请求:

https://blog-1253413117.cos.ap-shanghai.myqcloud.com/content/posts/62460be68efb494c9428c6086320517d.png

返回:

https://blog-1253413117.cos.ap-shanghai.myqcloud.com/content/posts/8d25a17cc6e94150a26c13df9e79541d.png

最后,删除员工:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
 * 删除
 * @param c Context
 * @returns msg string
 */
export const deleteEmployee: HandlerFunc = async (c: Context) => {
  try {
    const { id } = c.params as { id: string };

    const fetchedEmployee = await employees.findOne({ _id: { "$oid": id } });

    if (fetchedEmployee) {
      const deleteCount = await employees.deleteOne({ _id: { "$oid": id } });
      if (deleteCount) {
        return c.string("Employee deleted successfully!", 204);
      }
      throw new ErrorHandler("Unable to delete employee", 400);
    }

    throw new ErrorHandler("Employee not found", 404);
  } catch (error) {
    throw new ErrorHandler(error.message, error.status || 500);
  }
};

请求:

https://blog-1253413117.cos.ap-shanghai.myqcloud.com/content/posts/bcfa5ea8257f43fa8abb6f9299a4e466.png

返回:

https://blog-1253413117.cos.ap-shanghai.myqcloud.com/content/posts/801e25f90242452988cf14fb50a9a0ce.png

代码部分完成了,现在启动服务吧~

1
2
deno run --allow-write --allow-read --allow-plugin --allow-net --allow-env --unstable ./server.ts 

为了确保程序安全执行,deno 默认阻止任何访问磁盘、网络或环境变量的操作。因此,如果想要服务成功运行,你需要加上这些标记:

  • –allow-write
  • –allow-read
  • –allow-plugin
  • –allow-net
  • –allow-env

可能这个时候你会问了:我咋记得住我要加哪些标记?不用担心,如果缺了哪个的话,控制台会告诉你的。

成功运行,撒花~

1
2
3
Compile file:///Users/xxxxxx/deno-demo/server.ts
INFO load deno plugin "deno_mongo" from local "/Users/xxxxxx/deno-demo/.deno_plugins/deno_mongo_8834xxxxxxxxxxxxxx8a4c.dylib"
server listening on http://localhost:5000

译注:可以通过 Postman 等其他工具验证一下接口

小结

在这篇文章里,我们实现了:

  • 使用 deno 构建 CRUD API 来管理员工信息
  • 使用 deno_mongo 操作 mongodb 数据库
  • 使用简易 deno 框架 abc 构建服务器
  • 使用 denv 声明环境变量

你可能注意到了 deno

  • 不需要初始化 package.json 文件或者在 node_modules 目录下安装模块
  • 通过 url 直接引入模块
  • 需要Add flags to secure the execution of the program.
  • Don’t install typescript locally because it’s compiled in Deno.

以上,

源码

原作者:https://github.com/slim-hmidi/deno-employees-api

译者:https://github.com/xunge0613/deno-demo

译注:XX 学完这个后,这几天又玩了下 Go 语言,发现 deno 好多地方和 Go 很相似,比如:

  • Go 也通过 url 引入模块(使用版本 1.14.4
  • deno 使用原生 TypesSriptGo 本身就是强类型,必须声明类型;而 Go 里声明 Struct 感觉类似 TS 里的 interface
  • 可能还有其他的一时想不起来了😝

附录:Mac 安装 Deno

代码截取自Deno 钻研之术:(1) Hello,从多样化安装到简单实战,有修改。

友情提示:目前 Mac 用户使用 homebrew 安装版本只有 0.4.2,运行本文章代码会报错。

推荐:Mac 系统使用 curl 方式安装高版本 deno

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 通过 curl 下载远程的安装脚本 install.sh 中的 deno.zip 压缩包到本地并立即执行
$ curl -fsSL https://deno.land/x/install/install.sh | sh
# Archive:  /Users/${USER_NAME}/.deno/bin/deno.zip
#   inflating: deno
# Deno was installed successfully to /Users/${USER_NAME}/.deno/bin/deno
# Manually add the directory to your $HOME/.bash_profile (or similar)
#   export DENO_INSTALL="/Users/${USER_NAME}/.deno"
#   export PATH="$DENO_INSTALL/bin:$PATH"
# Run '/Users/${USER_NAME}/.deno/bin/deno --help' to get started

# 输入 deno -V 并不能运行成功 deno 命令,需要我们手动配置环境变量来让终端知道 deno 命令该在哪执行。
$ deno -V
# zsh: command not found: deno

# 注意:${USER_NAME} 是你在自己操作系统下的用户名,需要手动改为自己的用户名。
$ export DENO_INSTALL="/Users/xuxun/.deno"
$ export PATH="$DENO_INSTALL/bin:$PATH"
$ deno -V
deno 1.1.1
$ which deno
/Users/xuxun/.deno/bin/deno

友情提示:curl 下载 deno 过程可能有点慢,可以选择扔在一边先不去管它。

结语

ps:上一篇公众号文章,还是2019年6月12日发布的,名副其实的年更公众号了。

注:本文部分翻译自 https://dev.to/slimhmidi/create-a-server-with-deno-and-mongo-206l,为了便于安装文章调试项目,所以在原文基础上有所删减,另外代码部分因 Deno 及相关插件版本原因有所改动~

因为第一次正式接触 denoGo,所以如果有不足之处欢迎指出~

本文排版用了 Markdown Nice,很好用

对了,欢迎点击阅读原文,之后如果文章内容有更新,会在我博客上进行更新~