您当前的位置:首页 > 电脑百科 > 程序开发 > 编程百科

了不起的 Deno 实战教程

时间:2020-05-20 16:21:41  来源:  作者:

「干货」了不起的 Deno 实战教程

 

作者:semlinker

转发链接:https://mp.weixin.qq.com/s/J4A5EYL7Kk8cx_X7Kh36Iw

前言

最近Deno正式版已经发布了。有的程序员说Deno可以完全取代Node.js,有的程序员说比Node.js 有优势,众说风云。不管三七二十一,我们一起了解了解。对 Deno 还不了解的读者,建议先阅读本文文「干货」通俗易懂的Deno 入门教程 这篇文章。

Deno 正式发布,彻底弄明白和 node 的区别

「干货」通俗易懂的Deno 入门教程

一、Oak 简介

相信接触过 Node.js 的读者对 Express、Hapi、Koa 这些 Web 应用开发框架都不会陌生,在 Deno 平台中如果你也想做 Web 应用开发,可以考虑直接使用以下现成的框架:

  • deno-drash:A REST microframework for Deno with zero dependencies。
  • deno-express:Node Express way for Deno。
  • oak:A middleware framework for Deno's net server 。
  • pogo:Server framework for Deno。
  • servest:A progressive http server for Deno。

写作本文时,目前 Star 数最高的项目是 Oak,加上我的一个 Star,刚好 720。下面我们来简单介绍一下 Oak:

A middleware framework for Deno's http server, including a router middleware.

This middleware framework is inspired by Koa and middleware router inspired by koa-router.

很显然 Oak 的的灵感来自于 Koa,而路由中间件的灵感来源于 koa-router 这个库。如果你以前使用过 Koa 的话,相信你会很容易上手 Oak。不信的话,我们来看个示例:

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

const app = new Application();

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

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

以上示例对于每个 HTTP 请求,都会响应 "Hello Semlinker!"。只有一个中间件是不是感觉太 easy 了,下面我们来看一个更复杂的示例(使用多个中间件):

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!";
});

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

为了更好地理解 Oak 中间件流程控制,我们来一起回顾一下 Koa 大名鼎鼎的 “洋葱模型”:

「干货」了不起的 Deno 实战教程

 

koa-onion-model

从 “洋葱模型” 示例图中我们可以很清晰的看到一个请求从外到里一层一层的经过中间件,响应时从里到外一层一层的经过中间件。

上述代码成功运行后,我们打开浏览器,然后访问 http://localhost:8000/URL 地址,之后在控制台会输出以下结果:

➜  learn-deno deno run --allow-net oak/oak-middlewares-demo.ts
GET http://localhost:8000/ - 0ms
GET http://localhost:8000/favicon.ico - 0ms

好了,介绍完 Oak 的基本使用,接下来我们开始进入正题,即使用 Oak 开发 REST API。

二、Oak 实战

本章节我们将介绍如何使用 Oak 来开发一个 Todo REST API,它支持以下功能:

  • 添加新的 Todo
  • 显示 Todo 列表
  • 获取指定 Todo 的详情
  • 移除指定 Todo
  • 更新指定 Todo

小伙伴们,你们准备好了没?让我们一起步入 Oak 的世界!

步骤一:初始化项目结构

首先我们在 learn-deno 项目中,创建一个新的 todos 目录,然后分别创建以下子目录和 TS 文件:

  • handlers 目录: 存放路由处理器;
  • middlewares 目录: 存放中间件,用于处理每个请求;
  • models 目录: 存放模型定义,在我们的示例中只包含 Todo 接口;
  • services 目录: 存放服务层程序;
  • db 目录:作为本地数据库,存放 Todo 数据;
  • config.ts:包含应用的全局配置信息;
  • index.ts :应用的入口文件;
  • routing.ts:包含 API 路由信息。

完成项目初始化之后,todos 项目的目录结构如下所示:

└── todos
    ├── config.ts
    ├── db
    ├── handlers
    ├── index.ts
    ├── middlewares
    ├── models
    ├── routing.ts
    └── services

如你所见,这个目录结构看起来像一个小型 Node.js Web 应用程序。下一步,我们来创建 Todo 项目的入口文件。

步骤二:创建入口文件

index.ts

import { Application } from "https://deno.land/x/oak/mod.ts";
import { APP_HOST, APP_PORT } from "./config.ts";
import router from "./routing.ts";
import notFound from "./handlers/notFound.ts";
import errorMiddleware from "./middlewares/error.ts";

const app = new Application();

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

console.log(`Listening on ${APP_PORT}...`);

await app.listen(`${APP_HOST}:${APP_PORT}`);

在第一行代码中,我们使用了 Deno 所提供的功能特性,即直接从网络上导入模块。除此之外,这里没有什么特别的。我们创建一个应用程序,添加中间件,路由,最后启动服务器。整个流程就像开发普通的 Express/Koa 应用程序一样。

步骤三:创建配置文件

config.ts

const env = Deno.env.toObject();
export const APP_HOST = env.APP_HOST || "127.0.0.1";
export const APP_PORT = env.APP_PORT || 3000;
export const DB_PATH = env.DB_PATH || "./db/todos.json";

为了提高项目的灵活性,我们支持从环境中读取配置信息,同时我们也为每个配置项目提供了相应的默认值。其中 Deno.env() 相当于Node.js 平台中的 process.env。

步骤四:添加 Todo 模型

models/todo.ts

export interface Todo {
  id: number;
  userId: number;
  title: string;
  completed: boolean;
}

在 Todo 模型中,我们定义了 id、userId、title 和 completed 四个属性,分别表示 todo 编号、用户编号、todo 标题和 todo 完成状态。

步骤五:添加路由

routing.ts

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

import getTodos from "./handlers/getTodos.ts";
import getTodoDetail from "./handlers/getTodoDetail.ts";
import createTodo from "./handlers/createTodo.ts";
import updateTodo from "./handlers/updateTodo.ts";
import deleteTodo from "./handlers/deleteTodo.ts";

const router = new Router();

router
  .get("/todos", getTodos)
  .get("/todos/:id", getTodoDetail)
  .post("/todos", createTodo)
  .put("/todos/:id", updateTodo)
  .delete("/todos/:id", deleteTodo);

export default router;

同样,没有什么特别的,我们创建一个 router 并添加 routes。它看起来几乎与 Express.js 应用程序一模一样。

步骤六:添加路由处理器

handlers/getTodos.ts

import { Response } from "https://deno.land/x/oak/mod.ts";
import { getTodos } from "../services/todos.ts";

export default async ({ response }: { response: Response }) => {
  response.body = await getTodos();
};

getTodos 处理器用于返回所有的 Todo。如果你从未使用过 Koa,则 response 对象类似于 Express 中的 res 对象。在 Express 应用中我们会调用 res 对象的 json 或 send 方法来返回响应。而在 Koa/Oak 中,我们需要将响应值赋给 response.body 属性。


handlers/getTodoDetail.ts

import { Response, RouteParams } from "https://deno.land/x/oak/mod.ts";
import { getTodo } from "../services/todos.ts";

export default async ({
  params,
  response,
}: {
  params: RouteParams;
  response: Response;
}) => {
  const todoId = params.id;

  if (!todoId) {
    response.status = 400;
    response.body = { msg: "Invalid todo id" };
    return;
  }

  const foundedTodo = await getTodo(todoId);
  if (!foundedTodo) {
    response.status = 404;
    response.body = { msg: `Todo with ID ${todoId} not found` };
    return;
  }

  response.body = foundedTodo;
};

getTodoDetail 处理器用于返回指定 id 的 Todo,如果找不到指定 id 对应的 Todo,会返回 404 和相应的错误消息。


handlers/createTodo.ts

import { Request, Response } from "https://deno.land/x/oak/mod.ts";
import { createTodo } from "../services/todos.ts";

export default async ({
  request,
  response,
}: {
  request: Request;
  response: Response;
}) => {
  if (!request.hasBody) {
    response.status = 400;
    response.body = { msg: "Invalid todo data" };
    return;
  }

  const {
    value: { userId, title, completed = false },
  } = await request.body();

  if (!userId || !title) {
    response.status = 422;
    response.body = {
      msg: "Incorrect todo data. userId and title are required",
    };
    return;
  }

  const todoId = await createTodo({ userId, title, completed });

  response.body = { msg: "Todo created", todoId };
};

createTodo 处理器用于创建新的 Todo,在执行新增操作前,会验证是否缺少 userId 和 title 必填项。


handlers/updateTodo.ts

import { Request, Response } from "https://deno.land/x/oak/mod.ts";
import { updateTodo } from "../services/todos.ts";

export default async ({
  params,
  request,
  response,
}: {
  params: any;
  request: Request;
  response: Response;
}) => {
  const todoId = params.id;

  if (!todoId) {
    response.status = 400;
    response.body = { msg: "Invalid todo id" };
    return;
  }

  if (!request.hasBody) {
    response.status = 400;
    response.body = { msg: "Invalid todo data" };
    return;
  }

  const {
    value: { title, completed, userId },
  } = await request.body();

  await updateTodo(todoId, { userId, title, completed });

  response.body = { msg: "Todo updated" };
};

updateTodo 处理器用于更新指定的 Todo,在执行更新前,会判断指定的 Todo 是否存在,当存在的时候才会执行更新操作。


handlers/deleteTodo.ts

import { Response, RouteParams } from "https://deno.land/x/oak/mod.ts";
import { deleteTodo, getTodo } from "../services/todos.ts";

export default async ({
  params,
  response
}: {
  params: RouteParams;
  response: Response;
}) => {
  const todoId = params.id;

  if (!todoId) {
    response.status = 400;
    response.body = { msg: "Invalid todo id" };
    return;
  }

  const foundTodo = await getTodo(todoId);
  if (!foundTodo) {
    response.status = 404;
    response.body = { msg: `Todo with ID ${todoId} not found` };
    return;
  }

  await deleteTodo(todoId);
  response.body = { msg: "Todo deleted" };
};

deleteTodo 处理器用于删除指定的 Todo,在执行删除前会校验 todoId 是否为空和对应 Todo 是否存在。


除了上面已经定义的处理器,我们还需要处理不存在的路由并返回一条错误消息。

handlers/notFound.ts

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

export default ({ response }: { response: Response }) => {
  response.status = 404;
  response.body = { msg: "Not Found" };
};

步骤七:添加服务

在创建 Todo 服务前,我们先来创建两个小的 helper(辅助)服务。

services/util.ts

import { v4 as uuid } from "https://deno.land/std/uuid/mod.ts";

export const createId = () => uuid.generate();

在 util.ts 文件中,我们使用 Deno 标准库的 uuid 模块来为新建的 Todo 生成一个唯一的 id。


services/db.ts

import { DB_PATH } from "../config.ts";
import { Todo } from "../models/todo.ts";

export const fetchData = async (): Promise<Todo[]> => {
  const data = await Deno.readFile(DB_PATH);

  const decoder = new TextDecoder();
  const decodedData = decoder.decode(data);

  return JSON.parse(decodedData);
};

export const persistData = async (data: Todo[]): Promise<void> => {
  const encoder = new TextEncoder();
  await Deno.writeFile(DB_PATH, encoder.encode(JSON.stringify(data)));
};

在我们的示例中,db.ts 文件用于实现数据的管理,数据持久化方式使用的是本地的 JSON 文件。为了获取所有的 Todo,我们根据 DB_PATH 设置的路径,读取对应的文件内容。readFile 函数返回一个 Uint8Array 对象,该对象在解析为 JSON 对象之前需要转换为字符串。Uint8Array 和 TextDecoder 都来自核心 JAVAScript API。同样,在存储数据时,需要先把字符串转换为 Uint8Array。

为了让大家更好地理解上面表述的内容,我们来分别看一下 Deno 命名空间下 readFile 和 writeFile 这两个方法的定义:

1. Deno.readFile

 export function readFile(path: string): Promise<Uint8Array>;

Deno.readFile 使用示例:

const decoder = new TextDecoder("utf-8");
const data = await Deno.readFile("hello.txt");
console.log(decoder.decode(data));

2. Deno.writeFile

export function writeFile(    path: string,
    data: Uint8Array,
    options?: WriteFileOptions): Promise<void>;

Deno.writeFile 使用示例:

const encoder = new TextEncoder();
const data = encoder.encode("Hello worldn");
// overwrite "hello1.txt" or create it
await Deno.writeFile("hello1.txt", data);
// only works if "hello2.txt" exists
await Deno.writeFile("hello2.txt", data, {create: false});  
// set permissions on new file
await Deno.writeFile("hello3.txt", data, {mode: 0o777});  
// add data to the end of the file
await Deno.writeFile("hello4.txt", data, {append: true});  

接着我们来定义最核心的 todos.ts 服务,该服务用于实现 Todo 的增删改查。

services/todos.ts

import { fetchData, persistData } from "./db.ts";
import { Todo } from "../models/todo.ts";
import { createId } from "../services/util.ts";

type TodoData = Pick<Todo, "userId" | "title" | "completed">;

// 获取Todo列表
export const getTodos = async (): Promise<Todo[]> => {
  const todos = await fetchData();
  return todos.sort((a, b) => a.title.localeCompare(b.title));
};

// 获取Todo详情
export const getTodo = async (todoId: string): Promise<Todo | undefined> => {
  const todos = await fetchData();

  return todos.find(({ id }) => id === todoId);
};

// 新建Todo
export const createTodo = async (todoData: TodoData): Promise<string> => {
  const todos = await fetchData();

  const newTodo: Todo = {
    ...todoData,
    id: createId(),
  };

  await persistData([...todos, newTodo]);

  return newTodo.id;
};

// 更新Todo
export const updateTodo = async (
  todoId: string,
  todoData: TodoData
): Promise<void> => {
  const todo = await getTodo(todoId);

  if (!todo) {
    throw new Error("Todo not found");
  }

  const updatedTodo = {
    ...todo,
    ...todoData,
  };

  const todos = await fetchData();
  const filteredTodos = todos.filter((todo) => todo.id !== todoId);

  persistData([...filteredTodos, updatedTodo]);
};

// 删除Todo
export const deleteTodo = async (todoId: string): Promise<void> => {
  const todos = await getTodos();
  const filteredTodos = todos.filter((todo) => todo.id !== todoId);

  persistData(filteredTodos);
};

步骤八:添加异常处理中间件

如果用户服务出现错误,会发生什么情况?这将可能导致整个应用程序崩溃。为了避免出现这种情况,我们可以在每个处理程序中添加 try/catch 块,但其实还有一个更好的解决方案,即在所有路由之前添加异常处理中间件,在该中间件内部来捕获所有异常。

middlewares/error.ts

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

export default async (
  { response }: { response: Response },
  next: () => Promise<void>
) => {
  try {
    await next();
  } catch (err) {
    response.status = 500;
    response.body = { msg: err.message };
  }
};

步骤九:功能验证

Todo 功能开发完成后,我们可以使用 HTTP 客户端来进行接口测试,这里我使用的是 VSCode IDE 下的 REST Client 扩展,首先我们在项目根目录下新建一个 todo.http 文件,然后复制以下内容:

### 获取Todo列表
GET http://localhost:3000/todos HTTP/1.1

### 获取Todo详情

GET http://localhost:3000/todos/${todoId}

### 新增Todo

POST http://localhost:3000/todos HTTP/1.1
content-type: application/json

{
    "userId": 666,
    "title": "Learn Deno"
}

### 更新Todo
PUT http://localhost:3000/todos/${todoId} HTTP/1.1
content-type: application/json

{
    "userId": 666,
    "title": "Learn Deno",
    "completed": true
}

### 删除Todo
DELETE  http://localhost:3000/todos/${todoId} HTTP/1.1

友情提示:需要注意的是 todo.http 文件中的 ${todoId} 需要替换为实际的 Todo 编号,该编号可以先通过新增 Todo,然后从 db/todos.json 文件中获取。

万事具备只欠东风,接下来就是启动我们的 Todo 应用了,进入 Todo 项目的根目录,然后在命令行中运行 deno run -A index.ts 命令:

$ deno run -A index.ts
Listening on 3000...

在以上命令中的 -A 标志,与 --allow-all 标志是等价的,表示允许所有权限。

-A, --allow-all
        Allow all permissions
        --allow-env
            Allow environment access
        --allow-hrtime
            Allow high resolution time measurement
        --allow-net=<allow-net>
            Allow network access
        --allow-plugin
            Allow loading plugins
        --allow-read=<allow-read>
            Allow file system read access
        --allow-run
            Allow running subprocesses
        --allow-write=<allow-write>
            Allow file system write access

可能有一些读者还没使用过 REST Client 扩展,这里我来演示一下如何新增 Todo:

「干货」了不起的 Deno 实战教程

 

deno-add-todo

从返回的 HTTP 响应报文,我们可以知道 Learn Deno 的 Todo 已经新增成功了,安全起见让我们来打开 Todo 根目录下的 db 目录中的 todos.json 文件,验证一下是否 “入库” 成功,具体如下图所示:

「干货」了不起的 Deno 实战教程

 

todos-json

从图可知 Learn Deno 的 Todo 的确新增成功了,对于其他的接口有兴趣的读者可以自行测试一下。

Deno 实战之 Todo 项目源码:https://github.com/semlinker/deno-todos-api



Tags:Deno   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,如有任何标注错误或版权侵犯请与我们联系(Email:2595517585@qq.com),我们将及时更正、删除,谢谢。
▌相关推荐
【CSDN编者按】很多人仍然对Deno的模型感到怀疑,很多新手对其采取的一些策略持反对意见,但真正接触过Deno并尝试了解它的不同工作方式后,你还这么认为吗?作者 | Kitson Kelly译...【详细内容】
2020-06-08  Tags: Deno  点击:(45)  评论:(0)  加入收藏
作者:semlinker转发链接:https://mp.weixin.qq.com/s/J4A5EYL7Kk8cx_X7Kh36Iw前言最近Deno正式版已经发布了。有的程序员说Deno可以完全取代Node.js,有的程序员说比Node.js...【详细内容】
2020-05-20  Tags: Deno  点击:(69)  评论:(0)  加入收藏
▌简易百科推荐
本文分为三个等级自顶向下地分析了glibc中内存分配与回收的过程。本文不过度关注细节,因此只是分别从arena层次、bin层次、chunk层次进行图解,而不涉及有关指针的具体操作。前...【详细内容】
2021-12-28  linux技术栈    Tags:glibc   点击:(3)  评论:(0)  加入收藏
摘 要 (OF作品展示)OF之前介绍了用python实现数据可视化、数据分析及一些小项目,但基本都是后端的知识。想要做一个好看的可视化大屏,我们还要学一些前端的知识(vue),网上有很多比...【详细内容】
2021-12-27  项目与数据管理    Tags:Vue   点击:(2)  评论:(0)  加入收藏
程序是如何被执行的&emsp;&emsp;程序是如何被执行的?许多开发者可能也没法回答这个问题,大多数人更注重的是如何编写程序,却不会太注意编写好的程序是如何被运行,这并不是一个好...【详细内容】
2021-12-23  IT学习日记    Tags:程序   点击:(9)  评论:(0)  加入收藏
阅读收获✔️1. 了解单点登录实现原理✔️2. 掌握快速使用xxl-sso接入单点登录功能一、早期的多系统登录解决方案 单系统登录解决方案的核心是cookie,cookie携带会话id在浏览器...【详细内容】
2021-12-23  程序yuan    Tags:单点登录(   点击:(8)  评论:(0)  加入收藏
下载Eclipse RCP IDE如果你电脑上还没有安装Eclipse,那么请到这里下载对应版本的软件进行安装。具体的安装步骤就不在这赘述了。创建第一个标准Eclipse RCP应用(总共分为六步)1...【详细内容】
2021-12-22  阿福ChrisYuan    Tags:RCP应用   点击:(7)  评论:(0)  加入收藏
今天想简单聊一聊 Token 的 Value Capture,就是币的价值问题。首先说明啊,这个话题包含的内容非常之光,Token 的经济学设计也可以包含诸多问题,所以几乎不可能把这个问题说的清...【详细内容】
2021-12-21  唐少华TSH    Tags:Token   点击:(10)  评论:(0)  加入收藏
实现效果:假如有10条数据,分组展示,默认在当前页面展示4个,点击换一批,从第5个开始继续展示,到最后一组,再重新返回到第一组 data() { return { qList: [], //处理后...【详细内容】
2021-12-17  Mason程    Tags:VUE   点击:(14)  评论:(0)  加入收藏
什么是性能调优?(what) 为什么需要性能调优?(why) 什么时候需要性能调优?(when) 什么地方需要性能调优?(where) 什么时候来进行性能调优?(who) 怎么样进行性能调优?(How) 硬件配...【详细内容】
2021-12-16  软件测试小p    Tags:性能调优   点击:(20)  评论:(0)  加入收藏
Tasker 是一款适用于 Android 设备的高级自动化应用,它可以通过脚本让重复性的操作自动运行,提高效率。 不知道从哪里听说的抖音 app 会导致 OLED 屏幕烧屏。于是就现学现卖,自...【详细内容】
2021-12-15  ITBang    Tags:抖音防烧屏   点击:(25)  评论:(0)  加入收藏
11 月 23 日,Rust Moderation Team(审核团队)在 GitHub 上发布了辞职公告,即刻生效。根据公告,审核团队集体辞职是为了抗议 Rust 核心团队(Core team)在执行社区行为准则和标准上...【详细内容】
2021-12-15  InfoQ    Tags:Rust   点击:(25)  评论:(0)  加入收藏
相关文章
    无相关信息
最新更新
栏目热门
栏目头条