基于 Deno 构建 HTTP Server 实践指南 | 🏆 技术专题第一期征文

6,416 阅读7分钟

大家好,我是俊宁,这是一篇介绍如何使用 Deno 构建 HTTP Server 的实践指南,如果你还不了解Deno是什么,可以移步我的另一篇 Deno入门文章

本文还使用到了 Docker,如果不熟悉可以看一下 一个前端工程师的Docker学习笔记【持续更新】

mongodb 入门可以看一下 MongoDB 教程

环境准备

  • deno: 使用 deno -V 查看是否正确安装了 deno
  • VSCode Deno插件: 支持 Deno 开发的 VSCode 插件
  • VSCode REST Client插件: 直接在VSCode中进行接口测试的插件

基础体验

官方示例解析

import { serve } from "https://deno.land/std/http/server.ts";

const s = serve({ port: 8080 });
console.log("http://localhost:8080/");

// 一个会等待每一个请求的 for 循环
for await (const req of s) {
  console.log(req.url);
  req.respond({ body: "Hello World\n" });
}

让我们来看看上面这段代码做了什么:

  1. 首先我们引入 server 模块: 这里使用了 ES 模块,第三方模块通过 URL 导入。

    注意:Deno 不支持 require 语法。模块也不是集中管理的,而是通过 URL 导入。

  2. 使用 serve 函数初始化一个 HTTP 服务

  3. 使用 for-await-of 语法监听请求,for-await-of 语句创建一个循环,循环遍历异步可迭代对象以及同步可迭代对象。

    注意:Deno不再捆绑在 async 函数之中,所以可以全局使用

解析请求体

const decoder = new TextDecoder("utf-8");
const data: Uint8Array = await Deno.readAll(req.body);
const body = decoder.decode(data) ? JSON.parse(decoder.decode(data)) : {};

简易REST API

1、这个Demo提供了两个api,分别是从文件读取数据返回和从网络获取数据并返回。结合数据库的放到后面使用框架的部分讲解:

import { serve } from "./deps.ts";

const s = serve({ port: 8080 });
console.log("http://localhost:8080/");

const juejin = "https://xiaoce-timeline-api-ms.juejin.im/v1/getListByLastTime?pageNum=1";

for await (const req of s) {
  if (req.url === "/books") {
    const body = await Deno.readFile("./books.json");
    req.respond({ body: body });
  } else if(req.url === "/juejin"){
    const response = await fetch(juejin);
    const jsonData = await response.json();
    req.respond({
      body: JSON.stringify(jsonData), // body 不能接受对象
    });
  }
}

2、执行 deno run --allow-read --allow-net index.ts

3、使用 VSCode REST Client 访问一下试试:

注意:如果 localhost 请求失败,请使用 ip 的形式。4090ok

技术选型

截止2020年7月30日,GitHub比较热门的 HTTP Server 框架有5个,分别是 oak、servest、deno-drash、abc、pogo(排名分先后)。

起初我也和大家一样面对这么多框架不知如何选,直到使用了Star History 对比了他们的star趋势后,毫无犹豫的选择了 oak。

但是本着技术探究的角度,我们还是分别体验一下这5个框架的 Hello World,然后再利用oak进行实战演习。

Oak

介绍

Oak 是最有前景的 Deno HTTP server 中间件框架,包含一个 路由中间件,目前能找到的社区资源最多。这款框架的灵感来自 Koa,路由中间件的灵感来自 @koa/router

Demo

创建一个 server.ts 文件并编写一个简单的 server:

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

const app = new Application();

app.use((ctx) => {
  ctx.response.body = "Hello Oak!";
});

console.log(`🦕 oak server running at http://127.0.0.1:8001/ 🦕`)

await app.listen("127.0.0.1:8001");

执行 deno run --allow-net server.ts开启服务,并使用 VSCode REST Client 测试:

编写一个拥有两个自定义中间件的Demo:

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

const app = new Application();

// Logger
app.use(async (ctx, next) => {
  await next();
  const rt = ctx.response.headers.get("X-Response-Time");
  console.log(`${ctx.request.method} ${ctx.request.url} - ${rt}`);
});

// Timing
app.use(async (ctx, next) => {
  const start = Date.now();
  await next();
  const ms = Date.now() - start;
  ctx.response.headers.set("X-Response-Time", `${ms}ms`);
});

// Hello World!
app.use((ctx) => {
  ctx.response.body = "Hello World!";
});

console.log(`🦕 oak server running at http://127.0.0.1:8889/ 🦕`);

await app.listen({ port: 8889 });

servest

介绍

用于Deno的渐进式http服务器

Servest 是一个适用于 Deno 的 http 模块,它由三个主要的 HTTP 协议的 API 组成

  • App API: 通用HTTP路由服务器
  • Server API: 处理的 HTTP/1.1 请求的低级的 HTTP API
  • Agent API: 处理 HTTP/1.1 的 Keep-Alive 连接的低级API

为了实验和进步,Serveststd/http 之外实现了自己的 HTTP/1.1 server。

Demo

与 std/http 高度兼容:

import { createApp } from "https://servestjs.org/@v1.1.1/mod.ts";
const app = createApp();
app.handle("/", async (req) => {
  await req.respond({
    status: 200,
    headers: new Headers({
      "content-type": "text/plain",
    }),
    body: "Hello, Servest!",
  });
});
app.listen({ port: 8899 });

专为实际业务而设计:

import { createApp } from "https://servestjs.org/@v1.1.1/mod.ts";

const app = createApp();

app.post("/post", async (req) => {
  const body = await req.json();
  await req.respond({
    status: 200,
    body: JSON.stringify(body),
  });
});

app.listen({ port: 8888 });

支持websoket:

import { createApp } from "https://servestjs.org/@v1.1.1/mod.ts";

const app = createApp();

app.ws("/ws", async (sock) => {
  for await (const msg of sock) {
    if (typeof msg === "string") {
      console.log("[index]", msg);
      // handle messages...
    }
  }
});

app.listen({ port: 8888 });

内置 jsx/tsx 支持,无需任何配置:

默认情况下,JSX文件(.jsx.tsx)将由 React.createElement()转换。因此,您必须在jsx/tsx文件的头上导入React。

// @deno-types="https://servestjs.org/@v1.1.1/types/react/index.d.ts"
import React from "https://dev.jspm.io/react/index.js";
// @deno-types="https://servestjs.org/@v1.1.1/types/react-dom/server/index.d.ts"
import ReactDOMServer from "https://dev.jspm.io/react-dom/server.js";
import { createApp } from "https://servestjs.org/@v1.1.1/mod.ts";

const app = createApp();

app.handle("/", async (req) => {
  await req.respond({
    status: 200,
    headers: new Headers({
      "content-type": "text/html; charset=UTF-8",
    }),
    // @ts-ignore
    body: ReactDOMServer.renderToString(
      <html>
        <head>
          <meta charSet="utf-8" />
          <title>servest</title>
        </head>
        <body>
          <h1>Hello Servest!</h1>
        </body>
      </html>,
    ),
  });
});

app.listen({ port: 8899 });

deno-drash

A REST microframework for Deno's HTTP server with zero dependencies.

import { Drash } from "https://deno.land/x/drash@v1.x/mod.ts";

class HomeResource extends Drash.Http.Resource {
  static paths = ["/"];
  public GET() {
    this.response.body = "Hello World!";
    return this.response;
  }
}

const server = new Drash.Http.Server({
  response_output: "text/html",
  resources: [HomeResource],
});

server.run({
  hostname: "127.0.0.1",
  port: 8888,
});

console.log(`🦕 drash server running at http://127.0.0.1:8888/ 🦕`);

abc

A better Deno framework to create web application.

import { Application } from "https://deno.land/x/abc@v1/mod.ts";

const app = new Application();

app.get("/hello", () => {
  return "Hello, Abc!";
});

app.start({ port: 8888 });

console.log(`🦕 abc server running at http://127.0.0.1:8888/ 🦕`);

Pogo

Pogo是用于编写Web服务器和应用程序的易于使用,安全且富有表现力的框架,它的灵感来自 hapi。

import React from "https://dev.jspm.io/react";
import pogo from "https://deno.land/x/pogo/main.ts";

const server = pogo.server({ port: 8888 });

server.router.get("/", () => {
  return <h1>Hello, world!</h1>;
});

server.start();

console.log(`🦕 pogo server running at http://127.0.0.1:8888/ 🦕`);

oak 实战

项目源码已同步开源: youngjuning/deno-oak-mongo-demo,下文只对遇到的坑做介绍,具体代码请查看源码。

项目骨架

.
├── .env # 使用 denv 插件来获取
├── Dockerfile
├── config # 配置文件
│   └── db.ts
├── deps.ts # 官方推荐的依赖管理方式
├── controllers # 存放路由处理器
│   ├── createBooks.ts
│   ├── deleteBook.ts
│   ├── getBookDetails.ts
│   ├── getBooks.ts
│   ├── notFound.ts
│   └── updateBook.ts
├── middlewares # 存放中间件,用于处理每个请求
│   └── error.ts
├── models # 存放模型定义
│   └── Book.ts
├── publish.sh # 发布脚本
├── router.ts # 定义路由信息
├── server.ts # 服务入口文件
├── services # 存放模型定义
│   └── books.ts
├── test.http # VSCode REST Client 文件,用来调试接口
└── utils # 工具函数
    └── getParams.ts # 将 ctx.request.url.search 转成对象

遇到的坑

作为一个前端工程师,为了写这篇文章,专门学了 mongodb。由于第一次接触,遇到最多的坑也是关于它的。

多容器链接

1、使用 --link 参数链接 mongo 容器,deno_mongo 是我们指定的映射到 juejin 容器内的数据库别名(这个很重要,连接数据库时要用)

docker run -d \
  --restart always \
  --name mongo \
  -v mongo_configdb:/data/configdb \
  -v mongo_data:/data/db \
  -p 27017:27017 \
  mongo \
  --auth
docker run -d \
  --restart always \
  --name juejin \
  -p 1998:1998 \
  --link mongo:deno_mongo \
  juejin

2、跨容器连接时不设置身份校验,开启服务端无法连接上mongo数据库,所以必须事先配置好 mongodb 的账号密码,并通过 mongodb://root:123456@deno_mongo:27017/ 的形式连接。

3、虽然不开启 --auth 是可以使用 mongo 的,但是这样不安全,强烈建议启动容器的时候加上 --auth 参数。

deno_mongo

这个插件在 run 起来的时候依赖的文件在 github 上,我卡在这里一下午。docker 启动项目后,由于容器内访问不了 github,导致一直失败。

幸运的是,码云可以同步 github 上的项目,coding 可以上传单文件不超过 20M 的文件,我成功地完成了这篇文章最后的一步:docker 部署项目。

使用

其他部分就没什么好说了,clone 代码后,需要先配置一下 mongodb。然后再改代码,就是直接执行 ./publish.sh 就可以应用更改。

mongodb 初始配置

# 不带权限校验的模式开启 mongo
$ docker run -d \
  --restart always \
  --name mongo \
  -v mongo_data:/data/db \
  -p 27017:27017 \
  mongo \
# mongodb 默认不开启验证,只要能访问服务器,即可直接登录,所以需要配置一下账号密码进行校验。
$ docker exec -it mongo mongo admin
# 创建超级管理员
> db.createUser({ user: "root" , pwd: "123456", roles: ["root"]});
Successfully added user: {
   "user" : "root",
   "roles" : ["root"]
}
# 尝试使用上面创建的用户信息进行连接。
> db.auth("root","123456")
1
# 创建一个名为 admin,密码为 123456 的用户。
> db.createUser({ user: "admin", pwd: "123456", roles:["userAdminAnyDatabase", "dbAdminAnyDatabase", "readWriteAnyDatabase"]});
Successfully added user: {
   "user": "admin",
   "roles": [
   {
      "role": "userAdminAnyDatabase",
      "db": "admin"
   }
  ]
}
# 尝试使用上面创建的用户信息进行连接。
> db.auth("admin","123456")
1

脚本

完成了 mongodb 的初始化配置,之后就可以使用 ./publish.sh 一键发布应用。

  1. 给脚本赋予可执行权限:chmod a+x ./publish.sh
  2. 构建镜像并发布容器:./publish.sh

参考

后续

后续,我想基于本文所述的架构,开发一个婚礼请柬小程序的后台,之前不会操作数据库,曾想使用 leancloud。奋战两天之后,妈妈再也不担心我不会写接口了。最后来放上一只喝奶茶的吉祥物:

🏆 技术专题第一期 | 聊聊 Deno的一些事儿......