2022-11-29 Deno数据库前端体验之旅

2022-11-29  本文已影响0人  netppp

https://zhuanlan.zhihu.com/p/159067663

前言

距离denoV1.0发布已经过去一个多月了,相信部分同学已经对 deno 有了些了解。本文也是从作者对 deno 的初探、实战体会总结而来,为还没有了解 deno 的同学做一个简单介绍,总体分为以下几个部分:

1.deno是什么以及为什么有deno

2.deno有什么亮点

3.helloworld demo

4.基于mysql & oak 的渐进式deno实战

一、deno的诞生

deno这个名字听起来,就像是在“碰瓷”node,很多不了解他的人也有这么认为的,就如当年javascript碰瓷java一样,但实际上,deno不仅没有碰瓷node,反而还是在拯救node,至少它的作者Ryan Dahl是这么认为的。

他曾多次在公开场合指责node,在他看来node不是一门好的编程语言,在设计之初,node便存在了诸多缺陷,所以他要吸取node的“失败”,然后孕育出一门新的语言deno,替代node或者说是“destroy node”。

当然社区关于两者的争论从deno还没有开源时便已经热火朝天:

有人认为deno就是下一代node,是增强版的弥补了设计缺陷的node,将引领社区发展最终取代node。

有人认为deno绝非下一代node,只能说是v8和各种runtime的一个尝试,deno并不完全能替代node目前的各种场景。

这些争论没有什么对错,更多的是不同的人不同的坚持。但是在此之前,我们总要了解一下,deno到底是什么?

[图片上传失败...(image-ac51d6-1669728072277)]

看看作者给出的定义:一个新的js、ts以及WebAssembly 运行时,主要的特点有:

为什么要创造deno呢?

[图片上传失败...(image-f8ab35-1669728072277)]

在回答这个问题前,引用作者的话说下node存在的问题:
在09年node发布以后的几年时间里,js和web都发生了重大变化,js引入了es6新增了大量语法,其中promise接口、async方法和es模块化,等重要特性node都无法支持,加上中心化模块系统越来越复杂等原因,Ryan Dahl决定放弃node。node存在的问题具体有:

  1. 无法支持promise & es module
    由于历史原因,node必须支持回调函数,同时node.js模块格式commonJs与es模块不兼容。
  2. 复杂的中心化模块系统
    node.js的模块管理工具npm,随着前端的发展,逻辑越来越复杂;模块安装目录npm_modules极其庞杂,难以管理。
  3. 没有安全机制
    node.js中用户只要下载了外部模块,外部代码就可直接在本地运行,进行各种读写操作。
  4. 爆炸式扩增的外部工具
    由于node.js本身功能不完整,使用者开发时强依赖外部工具,导致工具层出不穷,如webpack,babel,typescript、eslint、prettier......

正因为node有以上这些难以修复的痛点,作者开发了node的替代品deno,于是deno就这么诞生了。

二、deno 的设计亮点

export { assert} form "https://deno.land/std@v0.39.0/testing/assert.ts"
export { green } form "https://deno.land/std@v0.39.0/fmt/color.ts" 

然后在需要的文件里引入就好了import { green } form "./deps.ts。

 SUBCOMMANDS:
     bundle         Bundle module and dependencies into single file  打包
     cache          Cache the dependencies   不执行脚本的时候,缓存依赖项
     completions    Generate shell completions 生成补全脚本
     doc            Show documentation for a module  根据jsDoc规范,生成模块文档
     eval           Eval script    执行脚本命令
     fmt            Format source files   格式化代码
     info           Show info about cache or info related to source file  展示代码缓存
     install        Install script as an executable  将脚本安装为可执行文件
     lint           Lint source files  代码lint 
     repl           Read Eval Print Loop  进入REPL(Read-Eval-Print Loop) 环境
     run            Run a program given a filename or url to the module.  执行命令
     ...

三、helloworld demo

deno介绍完了,来个demo练手,功能很简单:访问页面,页面上显示helloworld安装deno
deno的具体安装步骤,官网给的很详细,点击此处查看。
注:brew方式安装的话,默认安装的是0.0.24版本,因为版本过低,执行的时候会出现报各种奇奇怪怪的错误,可使用deno upgrade -- version xx.xx.xx来升级到指定版本;推荐采用curl方式,curl会默认安装最新版本,但是注意修改环境量。

import { listenAndServe } from 'https://deno.land/std/http/server.ts';
     listenAndServe({ port: 3000 }, async (req) => {
       if (req.method === 'GET') {
         req.respond({
           status: 200,
           body: "hello world"
         })
       }
     })

[图片上传失败...(image-783045-1669728072276)]

当你把.ts拓展名去掉之后,又会出现"找不到模块 https://deno.land/std/http/server ts(2307)"的错误。
那模块是不是就无法加载了?别担心,官网已经推出解决这个问题的vscode插件了,在拓展中搜索deno,安装即可。

>deno run helloworld.ts 
 Server running on localhost:3000
 error: Uncaught PermissionDenied: network access to "0.0.0.0:3000", run again with the --allow-net flag
     at unwrapResponse ($deno$/ops/dispatch_json.ts:43:11)
     at Object.sendSync ($deno$/ops/dispatch_json.ts:72:10)
     at Object.listen ($deno$/ops/net.ts:51:10)
     at Object.listen ($deno$/net.ts:155:22)
     at serve (https://deno.land/std/http/server.ts:256:25)
     at listenAndServe (https://deno.land/std/http/server.ts:276:18)
     at file:///Users/xx/Documents/denodemo/helloworld.ts:21:1

deno run -A --reload helloworld.ts 
Download https://deno.land/std/http/server.ts
Download https://deno.land/std/encoding/utf8.ts
...
Compile file:///Users/xx/Documents/denodemo/helloworld.t

访问http://localhost:3000/

[图片上传失败...(image-382397-1669728072276)]

至此,第一个helloword demo成功跑通。不知道大家会不会和我之前一样,有这样的疑问:没有node_modules,那么下载的文件在哪呢?
第二节介绍deno内置工具的时候,有个deno info命令,这个命令就可以帮助我们查看依赖的缓存信息:

>deno info
  DENO_DIR location: "/Users/xx/Library/Caches/deno"
  Remote modules cache: "/Users/xx/Library/Caches/deno/deps"
  TypeScript compiler cache: "/Users/xx/Library/Caches/deno/gen"

这里DENO_DIR为deno所有项目的默认依赖安装和编译后文件的存放地址,我们执行deno info helloworld.ts,可以看到当前名为denodemo的项目compiled文件地址以及deps的关系:

>deno info helloworld.ts
 local: /Users/xx/Documents/denodemo/helloworld.ts
 type: TypeScript
 compiled: /Users/xx/Library/Caches/deno/gen/file/Users/x/Documents/denodemo/helloworld.ts.js
 map: x/Users/xx/Library/Caches/deno/gen/file/Users/xx/Documents/denodemo/helloworld.ts.js.map
 deps:
 file:///Users/xx/Documents/denodemo/helloworld.ts
   └┬ https://deno.land/std/http/server.ts
     ├── https://deno.land/std/encoding/utf8.ts
     ├┬ https://deno.land/std/io/bufio.ts
     │├── https://deno.land/std/bytes/mod.ts
     │└── https://deno.land/std/_util/assert.ts
     ├─ https://deno.land/std/_util/assert.ts
     ├┬ https://deno.land/std/async/mod.ts
     │├── https://deno.land/std/async/deferred.ts
     │├── https://deno.land/std/async/delay.ts
     │└┬ https://deno.land/std/async/mux_async_iterator.ts
     │  └── https://deno.land/std/async/deferred.ts
     └┬ https://deno.land/std/http/_io.ts          
       └── ...

[{  "id": 1,  "name": "world" },{  "id": 2,  "name": "china" }]

修改helloworld.ts文件,如下:

import { listenAndServe } from 'https://deno.land/std/http/server.ts'; 
 listenAndServe({ port: 3000 }, async (req) => {
   if (req.method === 'GET' ) {
     const params = req.url.substr(1); 
     const buffers = Deno.readFileSync('./hello.json'); 
     const decoder = new TextDecoder("utf-8");
     const jsonStr = decoder.decode(buffers);
     const res = JSON.parse(jsonStr).find((it:any) => it.name === params);  
     await req.respond({
       status: 200,   
       body: `hello ${res.name}`
     })
   }
 })

文件helloworld.ts中,先从url中获取参数params,然后通过deno内置api读取hello.json文件得到buffer流,再利用TextDecoder实例对象解码buffer流得到json字符串,将字符串解析成数组,通过find函数找到参数对应的name,最后将hello ${res.name}作为body,最后返回response。
最后测试下这个新增功能,访问http://localhost:3000/china:

[图片上传失败...(image-c924a9-1669728072276)]

注:deno提供的很多内置api,通过deno或者deno repl进入repl,输入Deno,可以查看到Deno这个全局对象下的所有api:

> Deno
 {
   Buffer: [Function: Buffer],
   readAll: [AsyncFunction: readAll],
   readAllSync: [Function: readAllSync],
   writeAll: [AsyncFunction: writeAll],
   writeAllSync: [Function: writeAllSync],
   chmodSync: [Function: chmodSync],
   ...
 }

四:基于mysql & oak 的渐进式deno实战

上节中我们使用deno的标准库http实现了一个简单的路由访问功能,但是标准库提供的功能比较简单,像params获取等功能封装的还不是那么易用,不如node开发中express或koa那么直接。那么deno有没有类似的框架呢?

答案是有的,oak是基于deno的http中间件框架,可定位为node的koa。deno社区类似的第三方库还有deno-expressabc有兴趣的可以关注下。

基于上面的helloword的例子,将功能继续完善下,新增以下功能点:

1.引入oak,新增文件findHello.ts,getHellos.ts,createHello.ts, deleteHello.ts,updateHello.ts

//以createHello.ts为例
  import { Application, Router, Response} from "https://deno.land/x/oak/mod.ts";
  const router = new Router();
  const app = new Application();
  router.get('/createHello',( async ({ response }: {response: Response }) => { 
    try{ 
        const decoder = new TextDecoder("utf-8");
        const data = Deno.readFileSync('./hello.json');  
        const res = JSON.parse(decoder.decode(data));
        const hello = {
            "id": res.length + 1,
            "name": "new name",
        };
        res.push(hello);
        const encoded = new TextEncoder();
        Deno.writeFileSync('./hello.json', encoded.encode(JSON.stringify(res)));
        response.status = 200;
        response.body = `新增${JSON.stringify(hello)}成功`;   
    }catch(err){
       console.log(err)
       response.status= 500;
    }
  });
  app.use(router.routes());
  app.use(router.allowedMethods());
  await app.listen({port: 3000})

createHello.ts文件中,加载oak模块,实例化一个应用以及路由对象,创建路由'/createHello',当用户访问该路由时,正常情况下的逻辑是,新增一个对象hello并对其编码成字节流buffers,并buffers写入到hello.json文件中,最后返回response,异常情况返回状态码500。

运行下代码,执行denon run -A createHello.ts,借助postman,验证createHello方法:

[图片上传失败...(image-4ecaaf-1669728072276)]

2.公共方法抽离 & 目录改造

在上面的createHello.ts中我们给应用创建了一个实例,然而在真实的项目里,不存在每个文件都实例化一次,针对错误处理也不需要每个方法都去实现一遍,这导致代码非常臃肿也难以维护。因此这里我们将错误处理抽离出来放在middlewares/error.ts文件中实现,应用实例化放在index.ts。

基于以上考虑,我们的目录改为:

controllers 目录: 存放路由控制器函数,负责解析用户的输入,处理后返回相应的结果;

middlewares 目录: 存放中间件,对错误的请求进行处理;

models 目录: 定义 Hello接口;

db 目录: 本地模拟数据;

index.ts: 应用的入口文件;

routing.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 };
  }
};

error.ts文件声明了一个中间件处理器,该处理器主要是对接口请求的异常情况统一处理。这里导出一个函数,该函数接受两个参数:response和next,其中response表示接口响应对象 ,next来表示下一个中间件函数。如果当前中间件函数请求没有结束,则调用next函数,将控制权交给下一个中间件,否则,将请求挂起,响应状态码为500,返回response。

//定义Hello接口
export default interface Hello {
  id: string;
  name?: string;
 }

hello.ts文件,定义了Hello接口,包含了必选id属性和可选name属性。

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

const createHello = (name: string) => {
 return [
    {
      id: v4.generate(), //生成一个随机的 ID 字符串
      name,
    },
  ]};
 //...  
export { createHello };

import { Request, Response } from "https://deno.land/x/oak/mod.ts";
import { createHello } from "../db/hello.ts";

export default async ({
  request,
  response,
 }: {
  request: Request;
  response: Response;
}) => {
  if (!request.hasBody) {
    response.status = 400;
    response.body = {success: false, msg: "没有传参" };
    return;
  }
  const { value: {name} } = await request.body();
  const helloData = createHello(name);
  response.status = 200;
  response.body = { success: true, msg: `创建${name}成功`, data: helloData };
};

 import { Router } from "https://deno.land/x/oak/mod.ts";
 import getHellos from "./controllers/getHellos.ts"
 import createHello from "./controllers/createHello.ts";
 import deleteHello from "./controllers/deleteHello.ts";
 import updateHello from "./controllers/updateHello.ts";
 import findHello from "./controllers/findHello.ts";
 const router = new Router();
 router
   .get("/getHellos", getHellos)
   .post("/createHello", createHello)
   .get("/findHello/:id", findHello)
   .put("/updateHellos/:id", updateHello)
   .delete("/deleteHello/:id", deleteHello);
 export default router;

路由的定义放在了router.ts里统一管理,这里我们定义了常用的增删改查接口。

 import { Application } from "https://deno.land/x/oak/mod.ts";
 import router from "./router.ts";
 import notFound from "./controllers/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);
 await app.listen({port: 3000});

index.ts作为主入口文件,完成中间件注册,路由注册,端口监听,一系列主要能力。

3. 引入数据库

在这之前都是通过db/hello.ts里定义的函数进行增删改除,让我们进一步完善我们的项目,引入数据库,让demo更贴近真实项目。

import { Client } from "https://deno.land/x/mysql/mod.ts";
const client = await new Client(); 
client.connect({
  hostname: "127.0.0.1",
  username: "root",
  password: "", //目前Deno MySQL模块无法连接有密码的用户,所以请确保root用户没有设置密码。
  db: "",
});
const run = async () => {
   await client.execute(`CREATE DATABASE IF NOT EXISTS deno`);
   await client.execute(`USE ${DATABASE}`);
   await client.execute(`DROP TABLE IF EXISTS hello`);
   await client.execute(`
      CREATE TABLE hello (
         id int(11) NOT NULL AUTO_INCREMENT,
         name varchar(100) NOT NULL,
         PRIMARY KEY (id)
     ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
   `);
}; 
run(); 
export default client;

 import Hello from "../models/hello.ts"; //接口定义
 import client from "../db/client.ts"; //数据库连接
 export default {
   createHello: async ( { name }: Hello ) => {
     return await client.query(
      `INSERT INTO hello (name) values(?)`,
       [ name ],
      );
   },
   getAll: async () => {}, 
   findHello: async () => {},
   updateHello: async () => {},
   deleteHello: async () => {},
  }

 import { v4 } from "https://deno.land/std/uuid/mod.ts";
 import HelloServer from "../services/hello.ts"; //新增
 export default async ({
   request,
   response,
  }) => {
  if (!request.hasBody) {
   response.status = 400;
   response.body = {success: false, msg: "没有传参" };
   return;
 }
  await HelloServer.createHello({name}); //新增
  response.status = 200;
  response.body = { success: true, msg: `创建${name}成功`};
 }

4.功能验证

至此,所有功能都已经完善了,您可以点击此处查看完整代码。接下来让我们来验证下api,依旧是借助postman辅助测试,执行denon run -A index.ts

调用createHello接口,创建数据,依次创建数据 world china hangzhou

[图片上传失败...(image-145b7d-1669728072275)] qian

调用getHellos接口,获取上面创建的所有数据

[图片上传失败...(image-e3491c-1669728072275)]前端

调用updateHello接口,更新id为3的数据,将name由hangzhou改为beijing

[图片上传失败...(image-38ff97-1669728072275)] 前端

调用deleteHello接口,删除id为4的数据时,查询不到该数据,返回不存在,删除id为1的数据时,返回删除成功。

[图片上传失败...(image-f0cf3f-1669728072275)]

[图片上传失败...(image-1dcd71-1669728072275)]

调用findHello接口,查找id为3的数据

[图片上传失败...(image-3cc254-1669728072275)]

最后,deno的初次体验之旅到这结束了,该文主要是从个人角度做了下总结,希望能带给读者一定的收获。

发布于 2020-07-13 12:30

上一篇下一篇

猜你喜欢

热点阅读