前言
Deno从诞生之初,就一直受到广大开发者的关注。曾经还因为issue事件,掀起一阵全民狂欢。主要还是因为他的作者太特殊,那个创造了node的男人。本人也是众多关注者中的一个,恰好团队把Deno立为一个技术规划方向,于是就抽出时间好好研究了下,趁着周末和大家分享一波,也作为自己的一个总结。
本文会对Deno做个全方面的介绍,包括基础篇,架构篇,实现篇,生态篇
预备知识
在学习Deno之前,需要先掌握一些前置知识,以便更好的理解Deno的实现和代码细节
ArrayBuffer
ArrayBuffer诞生的背景是因为WebGL,为了满足 JavaScript 与显卡之间大量的、实时的数据交换,它们之间的数据通信必须是二进制的,而不能是传统的文本格式.于是设计了这一套标准
要操作这段内存空间,需要使用视图,JS中定义了两种类型的视图:
- TypedArray—数组成员都是同一个数据类型视图
- DataView—数组成员是不同数据类型的视图
TypedArray是一系列视图的合集,包括Uint8Array(无符号 8 位整数)数组视图, Int16Array(16 位整数)数组视图等等,DataView则是这些视图的组合(数组中会包含多种数据类型),来个例子:
const buffer = new ArrayBuffer(12);
const x1 = new Int32Array(buffer);
x1[0] = 1;
const x2 = new Uint8Array(buffer);
x2[0] = 2;
x1[0] // 2
//x1和x2共享同一段内存,所以会互相影响,如果设置x2[1] = 2,x1[0]=?
编解码
主要就是将字符串和TypedArray进行互相转换,Deno中文件处理会大量使用,例子:
new TextEncoder().encode('copy') ===> Uint8Array(4) [99, 111, 112, 121]
基础篇
可能大家对Deno都不是很了解,先简单介绍下
从Deno的发展历程中,可以看出,作者创造Deno时,借鉴了很多业界的先进经验,以及自己多年的技术沉淀,所以在设计Deno时会有一些和node明显的区别,先上一段代码感受下:
//index.js
import { serve } from "https://deno.land/std@0.79.0/http/server.ts";
let str: string = "hello";
const text = Deno.readFileSync("./data.txt");
window.addEventListener("load", ()=>{});
window.addEventListener("unload", ()=>{});
deno run --allow-read index.js
从中可以看出一些特色:
内置APi
Deno中api很多都是直接挂载在Deno全局对象上的,不像node进行了分类:fs,crypto,path,url这些模块。使用比较方便,但似乎不太好管理
模块系统
Deno中的模块都是直接通过url直接引入的,没有node_modules这个黑洞。将npm的中心化管理变成了去中心化,对此业界讨论比较大,可能url引入不是最完美的,但是至少是解决了npm的很多问题,对模块化的一种积极尝试,未来让我们拭目以待
安全模型
Deno中对应用的权限进行了分类,需要显式指定文件、网络和环境权限,否则运行文件会报错 一般通过 --allow-*来指定对应的权限,详细的细节,会在实现篇中进行讲解
Typescript
Deno中内置了ts解析引擎,直接编写ts代码即可,Deno会自动解析,转成对应的JS代码
兼容浏览器
Deno从设计之初,就是拥抱浏览器,ECMAScript等标准,让前端代码尽可能地在Deno中运行。我们可以在Deno中使用window对象,还可以用addEventListener监听事件,以及使用console和URL等等,未来支持的会越来越多
和node简单的区别
- 事件和异步
- 回调 ===> promise
- 多进程
- child_process ===> worker
- IO操作
- Buffer ===> TypedArray
- 事件循环
- libuv ===> Tokio
到这,如果之前完全没接触过Deno的,应该有个大致的认识了,了解了Deno的一些编写风格,由于本篇不是一个使用手册,不会对所有细节进行阐述,大家可以参考官方文档 查看更多细节。
内置工具集
- 内置格式化工具:deno fmt myfile1.ts
- // deno-fmt-ignore ---忽略下一行代码
- // deno-fmt-ignore-file---忽略整个文件
- 构建工具:deno bundle './colors.ts' colors.bundle.js
- 生成文档:deno doc add.ts --json
- 代码检查:deno lint --unstable myfile1.ts
Deno中内置了很多开发工具,可以看出想把当前的node生态太过分散,开发者们疲于应付各种层出不穷的工具的困扰的现状进行改进,让代码编码,构建,语法规范这些都交给Deno来做,未来这些工具集的不断完善,很有可能会改变前端百花齐放的生态
架构篇
首先,我们去github上看下Deno的源码结构:
这是目前(2020-12-5)Deno代码库的划分,对其中的一些模块进行了标注,方便大家自行研究时有个分类
谈到架构,就得上官网的核心架构图,不过这张图更新太慢,有些实现已经变了
主要变化就是libdeno已经替换成rusty_v8,js可以直接和rust进行交互了,使用rust就可以完成功能开发,后面会介绍。Tokio是一个事件驱动的非阻塞I/O平台,用于使用Rust编程语言编写异步应用程序,相当于node中libuv。Deno里面js与rust的交互都会通过Deno.core.send和Deno.core.recv这两个方法,send发起同步任务,recv用来触发异步回调。Deno为了安全的原因,对资源都做了一层映射,原本系统的文件描述符或者其他资源都映射成了一个rid(非负整数)。rust和Tokio之间通过ops进行任务的关联,ops是一个系统操作映射表,将操作和一个唯一的id进行关联,结构为:
这是官网的一张类比表,可以看出Deno团队想把Deno打造的和linux一样,成为一个JS的工作平台,而不单是一门服务端语言
实现篇
接下来,我们一起从源码的角度去看下Deno中的一些模块的具体实现
安全模型
上文提过,我们通过命令行运行Deno程序的时候,都会显示地指定具体的权限:
deno run --allow-read=/foo/bar 'https://xxxx'
在Deno内部是通过permission模块来管理这些权限的,上面的命令在Deno源码中会被解析为:
const desc = { name: "read", path: "/foo/bar" } as const;
Deno还会暴露三个核心Api来让用户去管理权限:
console.log(await Deno.permissions.query(desc));
// PermissionStatus { state: "granted" }
const status1 = await Deno.permissions.request(desc);
// ⚠️ Deno requests read access to "/foo". Grant? [g/d (g = grant, d = deny)] g
console.log(status1);
// PermissionStatus { state: "granted" }
console.log(await Deno.permissions.revoke(desc));
// PermissionStatus { state: "prompt" }
permission类中通过状态去标识对应的权限:
pub enum PermissionState {
Granted = 0,
Prompt = 1,
Denied = 2,
}
pub struct Permissions {
pub read: UnaryPermission<PathBuf>,
pub write: UnaryPermission<PathBuf>,
pub net: UnaryPermission<String>,
pub env: PermissionState,
pub run: PermissionState,
pub plugin: PermissionState,
pub hrtime: PermissionState,
}
从代码中可以清晰看到Deno的权限都有哪几类,接下来,我们以env权限为例,因为它的代码较少,方便梳理,源码中的实现为:
pub fn request_env(&mut self) -> PermissionState {
if self.env == PermissionState::Prompt {
if permission_prompt("Deno xxx") {
self.env = PermissionState::Granted;
} else {
self.env = PermissionState::Denied;
}
}
self.env
}
pub fn query_env(&self) -> PermissionState {
self.env
}
pub fn revoke_env(&mut self) -> PermissionState {
if self.env == PermissionState::Granted {
self.env = PermissionState::Prompt;
}
self.env
}
可以看到代码结构很清晰,其中permission_prompt
方法会输出终端,接受用户输入,更改权限状态,其他的逻辑就是根据对应的类型修改状态了
那么Deno程序中是怎么运用这个权限的呢,我们的程序就是一个mainWorker,会有一个全局状态:
permission就是其中的一个属性,flags属性也很关键,是我们命令行的一些参数构成的对象,通过这张图就能清楚的知道permission是怎么运作的了:
执行机制
在讲执行机制之前,先过一下之前提到过的Tokio:
Tokio中的异步模型,是基于future实现的,熟悉Rust语言的应该很了解。基于我们熟悉的promise,简单对比一下,future的基本特征:
future特征:
trait SimpleFuture {
type Output;
fn poll(&mut self, wake: fn()) -> Poll<Self::Output>;
}
enum Poll<T> {
Ready(T),
Pending,
}
js中的promise状态转移靠的是push,它本身就是一个事件源,能够主动去改变自身状态,我们在then函数中注册的回调,会自动在下一个微任务队列中执行;而future靠的是poll,需要一个excutor去轮询获取其状态,也就是说excutor会在适当的时候执行future的poll方法,当返回状态为Poll::Pending也就意味需要excutor等待进行下一次轮询,当返回Poll::Ready也就是意味future已经完成,然后根据返回的值去判断是否出错,去执行下一步代码逻辑。 当然了,excutor不会不间断地去轮询,所以future的poll方法里面是可以设置一个waker(唤醒任务),去通知excutor什么时候该来轮询的。
接下来,分析下Deno程序是怎么执行的
入口函数:cli/main.rs的main方法,主要干了两件事:
- 解析命令行参数然后创建对应的future(任务)
- 启动Tokio执行future(任务)
通过代码可以看出,关键的步骤就是Run对应的逻辑,也就是run_command方法
核心逻辑就是Deno会创建一个MainWorker,就是我们的主进程,然后是四个关键步骤:
执行用户逻辑代码->触发load事件->事件循环->触发unload事件
JS与Rust交互
代码路径:core/core.js
deno里面JS与Rust的交互只能通过send和recv这两个方法
send会根据opId去调用对应的rust方法,同步方法会直接返回,异步的话,需要通过recv去获取异步结果
Object.assign(window.Deno.core, {
jsonOpAsync,
jsonOpSync,
setAsyncHandler,
dispatch: send,
ops,
close,
sharedQueue: {},
...
})
其中ops很关键,之前有提到过,它是一个系统操作映射表,将操作和一个唯一的id进行关联,是JS和Rust之间沟通的桥梁
整个core.js就是给Window.Deno.core定义这些api方法,Deno中触发任务的代码如下:
//同步
Deno.core.dispatch(opId, scratchBytes, ...zeroCopy);
//异步
Deno.core.setAsyncHandler(opId,cb)
Deno.core.dispatch(opId, scratchBytes, ...zeroCopy);
接下来,我们通过关键代码,看下基本流程是什么样的
通过setAsyncHandler设定的回调函数,会进行初始化,回调函数会存储到全局的handles表里。初始化函数里设置recv的回调函数,供Tokio异步任务完成后触发。handleAsyncMsgFromRust函数则会从全局的回调队列中取出队首任务,根据opId去执行对应的handler
这一套机制和我们平时进行hybrid开发的设计是不是有点类似,对比下设计图就清晰了:
核心代码实现:
最后来到core/bingding.rs的initialize_context,在这里是deno初始化核心方法的地方(都是挂在Deno.core这个对象下),send和recv也是在这里注入到JS中的
用一张流程图,将上述的片段串一下:
生态篇
接下开看下正在茁壮成长的Deno,都有哪些周边生态
开发框架-oak
很明显,oak对标的是koa,连取名都这么刚,实现功能基本一样,使用它可以开发基础的web应用,不过相比koa和node,第三方插件就少一些。还有像MongoDB,Redis等数据库,都有对应的Deno版本,开发一些服务端应用还是可以的,国内很火的企业级开发框架egg并没有对应的Deno版本,开发大型企业应用这块,目前还是一个探索领域
跨端
Electron包含chromium,node.js,原生Api三大部分,借助它,我们可以开发跨端的桌面应用
Electron目前还不支持Deno,如果在Deno中想开发桌面应用,可以使用webview_deno这个第三方库,它基于webview_rust实现,虽然在功能上与Electron有一定差距,但是可以让我们去探索Deno中的桌面世界
Serverless
Serverless代表一种无服务器架构(也被称为“无服务器计算”),并不表示不需要物理服务器,而是指不需要关注和管理服务器,直接使用服务即可,其实就是一种服务外包或者说服务托管,这些服务由第三方供应商提供。
Serverless领域最具代表性的是阿里云和亚马逊,不过他们均不原生支持Deno的运行时。我们可以通过自定义运行时来实现,以阿里云为例,实现一个 Custom Runtime,只需要满足以下 3 个条件:
-
创建一个 HTTP Server,监听在固定端口(端口可以读取环境变量 FC_SERVER_PORT,默认为 9000)。
-
HTTP Serverr 需要在 15 秒内完成启动。
-
connection 最好设置为 keep alive,请求超时时间至少设置在 15 分钟以上
借助官方命令行工具funcraft初始化一个函数,然后配置Deno的运行时就可以了。就是学习Deno时,本机安装的过程,放到了云服务器上。执行流程大概是:
RPC(远程过程调用)
RPC基于 TCP/IP 来实现调用远程服务器的方法,在后端分布式架构中应用被广泛应用,实现RPC的三种方式:
- Thrift:Go、Haskell、Java、Node.js、C、Perl、php、Python、Ruby ...
- HTTP
- MQ
目前,Thrift还不支持Deno,所以构建的Deno应用想使用RPC,只能使用下面的两种形式
WebAssembly
WebAssembly是一种新的编码方式,可以在现代的网络浏览器中运行 - 它是一种低级的类汇编语言,具有紧凑的二进制格式,可以接近原生的性能运行,并为诸如C / C ++等语言提供一个编译目标,以便它们可以在Web上运行。它也被设计为可以与JavaScript共存,允许两者一起工作。(MDN官方定义)
浏览器端一般通过js中间代码(胶水代码)做兼容,将其他语言编写的功能,用js来进行模拟。比如用web worker模拟多进程,用localStorage模拟存储等等
如果想在非浏览器端实现WebAssembly应用,需要提供WASI(WebAssembly System Interface)
WASI 通过增加“抽象层”的方式,解决了 Wasm 抽象机器(V-ISA)与实际操作系统调用之间的可移植性问题,这可以保证我们基于 WASI 编写的 Wasm 应用(模块)真正做到“一次编译,到处运行”。抽象出的“Wasm 系统调用层”将交由具体的底层基础设施(虚拟机 / 运行时)来提供实现和支持
Deno原生内置了WASI模块,在Deno可以执行运行wasm代码,上一个官网示例:
const wasmCode = new Uint8Array([
0, 97, 115, 109, 1, 0, 0, 0, 1, 133, 128, 128, 128, 0, 1, 96, 0, 1, 127,
3, 130, 128, 128, 128, 0, 1, 0, 4, 132, 128, 128, 128, 0, 1, 112, 0, 0,
5, 131, 128, 128, 128, 0, 1, 0, 1, 6, 129, 128, 128, 128, 0, 0, 7, 145,
128, 128, 128, 0, 2, 6, 109, 101, 109, 111, 114, 121, 2, 0, 4, 109, 97,
105, 110, 0, 0, 10, 138, 128, 128, 128, 0, 1, 132, 128, 128, 128, 0, 0,
65, 42, 11
]);
const wasmModule = new WebAssembly.Module(wasmCode);
const wasmInstance = new WebAssembly.Instance(wasmModule);
const main = wasmInstance.exports.main as CallableFunction
console.log(main().toString()); //42
可以看到wasm代码就是Uint8Array的数组,运行时按照WebAssembly MVP标准去解析它,拿到exports导出函数,去执行我们的逻辑
总结
最后,做一个简短的Deno总结:
- 弥补了node的一些设计缺陷,和node共存一段时间
- 兼容浏览器,让前端代码在deno中直接运行
- 内置工具集待完善,周边生态建设中
- 拥抱Rust生态圈,主打安全和高并发
- 拓展前端边界,让JS在服务端大展拳脚
Ryan基于近些年业界的技术发展和实践,倾注了很多心血,创造了Deno。我们可以从中学习到一些设计和实现,这一点是最关键的。至于大家争论的未来是node还是Deno主导,会不会替代node,就显得没那么重要了。技术车轮一直向前,我们需要保持自己的学习力和理解力,多主动思考。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!