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

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

首页战争策略攻城风云手游更新时间:2024-11-03

作者:semlinker

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

前言

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

一、Oak 简介

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

写作本文时,目前 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"; constapp=newApplication(); app.use((ctx)=>{ ctx.response.body="HelloSemlinker!"; }); awaitapp.listen({port:8000});

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

import{Application}from"https://deno.land/x/oak/mod.ts"; constapp=newApplication(); //Logger app.use(async(ctx,next)=>{ awaitnext(); constrt=ctx.response.headers.get("X-Response-Time"); console.log(`${ctx.request.method}${ctx.request.url}-${rt}`); }); //Timing app.use(async(ctx,next)=>{ conststart=Date.now(); awaitnext(); constms=Date.now()-start; ctx.response.headers.set("X-Response-Time",`${ms}ms`); }); //HelloWorld! app.use((ctx)=>{ ctx.response.body="HelloWorld!"; }); awaitapp.listen({port:8000});

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

koa-onion-model

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

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

➜learn-denodenorun--allow-netoak/oak-middlewares-demo.ts GEThttp://localhost:8000/-0ms GEThttp://localhost:8000/favicon.ico-0ms

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

二、Oak 实战

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

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

步骤一:初始化项目结构

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

完成项目初始化之后,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"; importrouterfrom"./routing.ts"; importnotFoundfrom"./handlers/notFound.ts"; importerrorMiddlewarefrom"./middlewares/error.ts"; constapp=newApplication(); app.use(errorMiddleware); app.use(router.routes()); app.use(router.allowedMethods()); app.use(notFound); console.log(`Listeningon${APP_PORT}...`); awaitapp.listen(`${APP_HOST}:${APP_PORT}`);

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

步骤三:创建配置文件

config.ts

constenv=Deno.env.toObject(); exportconstAPP_HOST=env.APP_HOST||"127.0.0.1"; exportconstAPP_PORT=env.APP_PORT||3000; exportconstDB_PATH=env.DB_PATH||"./db/todos.json";

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

步骤四:添加 Todo 模型

models/todo.ts

exportinterfaceTodo{ 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"; importgetTodosfrom"./handlers/getTodos.ts"; importgetTodoDetailfrom"./handlers/getTodoDetail.ts"; importcreateTodofrom"./handlers/createTodo.ts"; importupdateTodofrom"./handlers/updateTodo.ts"; importdeleteTodofrom"./handlers/deleteTodo.ts"; constrouter=newRouter(); router .get("/todos",getTodos) .get("/todos/:id",getTodoDetail) .post("/todos",createTodo) .put("/todos/:id",updateTodo) .delete("/todos/:id",deleteTodo); exportdefaultrouter;

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

步骤六:添加路由处理器

handlers/getTodos.ts

import{Response}from"https://deno.land/x/oak/mod.ts"; import{getTodos}from"../services/todos.ts"; exportdefaultasync({response}:{response:Response})=>{ response.body=awaitgetTodos(); };

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"; exportdefaultasync({ params, response, }:{ params:RouteParams; response:Response; })=>{ consttodoId=params.id; if(!todoId){ response.status=400; response.body={msg:"Invalidtodoid"}; return; } constfoundedTodo=awaitgetTodo(todoId); if(!foundedTodo){ response.status=404; response.body={msg:`TodowithID${todoId}notfound`}; 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"; exportdefaultasync({ request, response, }:{ request:Request; response:Response; })=>{ if(!request.hasBody){ response.status=400; response.body={msg:"Invalidtododata"}; return; } const{ value:{userId,title,completed=false}, }=awaitrequest.body(); if(!userId||!title){ response.status=422; response.body={ msg:"Incorrecttododata.userIdandtitlearerequired", }; return; } consttodoId=awaitcreateTodo({userId,title,completed}); response.body={msg:"Todocreated",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"; exportdefaultasync({ params, request, response, }:{ params:any; request:Request; response:Response; })=>{ consttodoId=params.id; if(!todoId){ response.status=400; response.body={msg:"Invalidtodoid"}; return; } if(!request.hasBody){ response.status=400; response.body={msg:"Invalidtododata"}; return; } const{ value:{title,completed,userId}, }=awaitrequest.body(); awaitupdateTodo(todoId,{userId,title,completed}); response.body={msg:"Todoupdated"}; };

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


handlers/deleteTodo.ts

import{Response,RouteParams}from"https://deno.land/x/oak/mod.ts"; import{deleteTodo,getTodo}from"../services/todos.ts"; exportdefaultasync({ params, response }:{ params:RouteParams; response:Response; })=>{ consttodoId=params.id; if(!todoId){ response.status=400; response.body={msg:"Invalidtodoid"}; return; } constfoundTodo=awaitgetTodo(todoId); if(!foundTodo){ response.status=404; response.body={msg:`TodowithID${todoId}notfound`}; return; } awaitdeleteTodo(todoId); response.body={msg:"Tododeleted"}; };

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


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

handlers/notFound.ts

import{Response}from"https://deno.land/x/oak/mod.ts"; exportdefault({response}:{response:Response})=>{ response.status=404; response.body={msg:"NotFound"}; };步骤七:添加服务

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

services/util.ts

import{v4asuuid}from"https://deno.land/std/uuid/mod.ts"; exportconstcreateId=()=>uuid.generate();

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


services/db.ts

import{DB_PATH}from"../config.ts"; import{Todo}from"../models/todo.ts"; exportconstfetchData=async():Promise<Todo[]>=>{ constdata=awaitDeno.readFile(DB_PATH); constdecoder=newTextDecoder(); constdecodedData=decoder.decode(data); returnJSON.parse(decodedData); }; exportconstpersistData=async(data:Todo[]):Promise<void>=>{ constencoder=newTextEncoder(); awaitDeno.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

exportfunctionreadFile(path:string):Promise<Uint8Array>;

Deno.readFile 使用示例:

constdecoder=newTextDecoder("utf-8"); constdata=awaitDeno.readFile("hello.txt"); console.log(decoder.decode(data));

2. Deno.writeFile

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

Deno.writeFile 使用示例:

constencoder=newTextEncoder(); constdata=encoder.encode("Helloworld\n"); //overwrite"hello1.txt"orcreateit awaitDeno.writeFile("hello1.txt",data); //onlyworksif"hello2.txt"exists awaitDeno.writeFile("hello2.txt",data,{create:false}); //setpermissionsonnewfile awaitDeno.writeFile("hello3.txt",data,{mode:0o777}); //adddatatotheendofthefile awaitDeno.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"; typeTodoData=Pick<Todo,"userId"|"title"|"completed">; //获取Todo列表 exportconstgetTodos=async():Promise<Todo[]>=>{ consttodos=awaitfetchData(); returntodos.sort((a,b)=>a.title.localeCompare(b.title)); }; //获取Todo详情 exportconstgetTodo=async(todoId:string):Promise<Todo|undefined>=>{ consttodos=awaitfetchData(); returntodos.find(({id})=>id===todoId); }; //新建Todo exportconstcreateTodo=async(todoData:TodoData):Promise<string>=>{ consttodos=awaitfetchData(); constnewTodo:Todo={ ...todoData, id:createId(), }; awaitpersistData([...todos,newTodo]); returnnewTodo.id; }; //更新Todo exportconstupdateTodo=async( todoId:string, todoData:TodoData ):Promise<void>=>{ consttodo=awaitgetTodo(todoId); if(!todo){ thrownewError("Todonotfound"); } constupdatedTodo={ ...todo, ...todoData, }; consttodos=awaitfetchData(); constfilteredTodos=todos.filter((todo)=>todo.id!==todoId); persistData([...filteredTodos,updatedTodo]); }; //删除Todo exportconstdeleteTodo=async(todoId:string):Promise<void>=>{ consttodos=awaitgetTodos(); constfilteredTodos=todos.filter((todo)=>todo.id!==todoId); persistData(filteredTodos); };步骤八:添加异常处理中间件

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

middlewares/error.ts

import{Response}from"https://deno.land/x/oak/mod.ts"; exportdefaultasync( {response}:{response:Response}, next:()=>Promise<void> )=>{ try{ awaitnext(); }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 命令:

$denorun-Aindex.ts Listeningon3000...

在以上命令中的 -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-add-todo

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

todos-json

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

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

推荐Vue学习资料文章:

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

《》

作者:semlinker

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

查看全文
大家还看了
也许喜欢
更多游戏

Copyright © 2024 妖气游戏网 www.17u1u.com All Rights Reserved