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到底是什么?
- 那么deno是什么呢?
[图片上传失败...(image-ac51d6-1669728072277)]
看看作者给出的定义:一个新的js、ts以及WebAssembly 运行时,主要的特点有:
- 内置了v8引擎,用于解释 JavaScript;
- 使用rust语言开发,使得deno项目可以使用rust提供的现成模块,节约开发时间;
- 异步操作使用了 rust 语言的 Tokio 库,来实现事件循环(event loop);
- 内置tsc引擎,支持 TypeScript语法。
为什么要创造deno呢?
[图片上传失败...(image-f8ab35-1669728072277)]
在回答这个问题前,引用作者的话说下node存在的问题:
在09年node发布以后的几年时间里,js和web都发生了重大变化,js引入了es6新增了大量语法,其中promise接口、async方法和es模块化,等重要特性node都无法支持,加上中心化模块系统越来越复杂等原因,Ryan Dahl决定放弃node。node存在的问题具体有:
- 无法支持promise & es module
由于历史原因,node必须支持回调函数,同时node.js模块格式commonJs与es模块不兼容。 - 复杂的中心化模块系统
node.js的模块管理工具npm,随着前端的发展,逻辑越来越复杂;模块安装目录npm_modules极其庞杂,难以管理。 - 没有安全机制
node.js中用户只要下载了外部模块,外部代码就可直接在本地运行,进行各种读写操作。 - 爆炸式扩增的外部工具
由于node.js本身功能不完整,使用者开发时强依赖外部工具,导致工具层出不穷,如webpack,babel,typescript、eslint、prettier......
正因为node有以上这些难以修复的痛点,作者开发了node的替代品deno,于是deno就这么诞生了。
二、deno 的设计亮点
- 支持es6语法 & 支持es模块化
可以使用promise、async/await等方法,支持浏览器模块系统,即import url导入模块。 - 原生支持ts
不需要写 tsconfig.json,并且默认开启了严格模式。但可以通过命令deno run -c tsconfig.json [file-to-run.ts]
自定义ts规则,其中-c是--config 选项的缩写。 - 支持 Web API
deno实现部分了W3C标准规范,积极向浏览器靠齐。
提供 window、globalThis 全局对象,支持 fetch、webCrypto、worker 等 Web 标准,具体可以点击此处查看,还支持onload、onunload等事件操作函数,点击此处查看。 - 去中心化的包分发机制
由于deno 是通过url去加载模块,因此不需要像npm这种中心化的模块储存系统,也不需要package.json文件,也没有npm_modules 目录。那如果是不同文件的同一模块都要修改版本怎么办?不用担心,deno可以通过deps.ts来集中管理依赖:
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。
- 单文件分发
deno直接通过import url的方式加载模块,url会具体到模块的文件,不再需要像main这样显示配置入口文件了。 - 具有安全控制
默认情况下脚本都不具有读写权限,需要手动授权才可读写文件系统或者网络deno run --allow-env --allow-read --allow-net [file-to-run.ts]
,如果觉得授权过于繁琐,可以通过参数--allow-all
或者简写-A
授予文件的所有权限。当然也可以给权限设置白名单,比如`deno run --unstable --allow-env --allow-read --allow-net=https://www.baidu.com [file-to-run.ts],指定除了baidu之外的网络均不可访问。 - Deno 内置工具箱,不再依赖外部工具
deno提供了很多命令,可以使用deno -help查看:
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会默认安装最新版本,但是注意修改环境量。
- 新建helloworld.ts文件
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"
})
}
})
-
vs code安装deno插件
deno通过url加载模块,且必须带有脚本后缀名,而ts导入模块不支持拓展名,且无法从远程加载模块。这两者就会产生矛盾,在helloworld.ts文件中出现下面的报错:
[图片上传失败...(image-783045-1669728072276)]
当你把.ts
拓展名去掉之后,又会出现"找不到模块 https://deno.land/std/http/server ts(2307)"的错误。
那模块是不是就无法加载了?别担心,官网已经推出解决这个问题的vscode插件了,在拓展中搜索deno,安装即可。
- 运行脚本
deno的亮点之一就是对文件和网络的读写有权限控制,因此如果直接执行deno run helloworld.ts
,会有如下错误:
>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 helloworld.ts
,这里--reload是表示缓存更新,另外还可以通过白名单的方式,只更新指定依赖,如deno run -A --reload=https://deno.land helloworld.js
。
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
└── ...
- 文件监听以及自动重启
为了在开发环境下能够更方便的监听文件变化,自动重启服务。这里我们借助第三方库denon来达到这个目的。
执行命令deno install --allow-read --allow-run --allow-write --allow-net -f --unstable [https://deno.land/x/denon@v2.2.0/denon.ts](https://link.zhihu.com/?target=https%3A//deno.land/x/denon%40v2.2.0/denon.ts)
然后运行脚本,将deno改成denon,执行命令denon run -A helloworld
,就可以成功访问http://localhost:3000。
另外denon还提供脚本配置,您可点击denon查看更多。 - 增加路由访问
新增一个功能,根据不同的路由参数name,页面返回不同的hello ${name}
语句
新建文件hello.json,用于存储模拟数据:
[{ "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-express、abc有兴趣的可以关注下。
基于上面的helloword的例子,将功能继续完善下,新增以下功能点:
- 增加hello 语句
- 修改hello 语句
- 删除hello 语句
- 查询hello 语句
- 获取hello 语句
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: 创建路由。
接下来一步一步实现:
- 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 };
}
};
error.ts文件声明了一个中间件处理器,该处理器主要是对接口请求的异常情况统一处理。这里导出一个函数,该函数接受两个参数:response和next,其中response表示接口响应对象 ,next来表示下一个中间件函数。如果当前中间件函数请求没有结束,则调用next函数,将控制权交给下一个中间件,否则,将请求挂起,响应状态码为500,返回response。
- models/hello.ts
//定义Hello接口
export default interface Hello {
id: string;
name?: string;
}
hello.ts文件,定义了Hello接口,包含了必选id属性和可选name属性。
- db/hello.ts
4.1节中都是通过读/写文件的方式去修改静态数据,现在让我们定义一些操作数的方法:
import { v4 } from "https://deno.land/std/uuid/mod.ts";
const createHello = (name: string) => {
return [
{
id: v4.generate(), //生成一个随机的 ID 字符串
name,
},
]};
//...
export { createHello };
- controllers/getHellos.ts文件 因为公共部分被提取出来了,所以createHello.ts只需要关注接口请求和响应,可以简化为:
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 };
};
- router.ts
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里统一管理,这里我们定义了常用的增删改查接口。
- index.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更贴近真实项目。
- 新增文件:db/client.ts负责数据库的连接和创建,run函数用于创建数据库deno以及表hello:
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;
- 新增services/hello.ts,用于服务层进行数据处理,以createHello为例,接受Hello对象,并向表hello里插入数据:
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 () => {},
}
- 修改controllers/createHello.ts,原数据来自本地mock数据db/hello.ts,现我们通过向表里插入数据达到新增的目的:
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
网友评论