Deno 是新的 JavaScript 和 TypeScript 运行时。Node.js 的发明者 Ryan Dahl 于 2020 年发布了 Deno,作为 Node.js 的改进。但是 Deno 不是 Node.js,而是全新的 JavaScript 运行时,同时也支持 TypeScript。与 Node.js 相似,Deno 可用于服务器端 JavaScript,但其目的是消除 Node.js 所犯的错误。它就像 Node.js 2.0 一样,只有时间才能告诉我们是否会像 2009 年使用 Node.js 一样去使用它。

为什么会有 Deno

Node(2009)和 Deno(2020)的发明者 Ryan Dahl 发布了 Deno 作为 JavaScript 生态系统的补充。当 Ryan 在会议上第一次宣布 Deno 时,他谈到了 Node.js 中的错误。Node.js 已经成为 JavaScript 生态中不可或缺的工具,已被数百万人使用,但是 Ryan Dahl 对当时做出的决定感到不满。现在 Ryan Dahl 希望通过 Deno 解决 Node 的设计缺陷。 Deno 是由 V8 JavaScript 引擎、Rust 和 TypeScript 实现的用于安全服务器端的 JavaScript 和 TypeScript 全新运行时。

  • 语言:JavaScript 和 TypeScript 是 Deno 运行时的第一语言。无论你用哪种编写 Deno 程序,仅需要一个文件扩展名即可。尽管 TypeScript 不断受到欢迎,但具有一流 TypeScript 支持的 Deno 可能是这种趋势的合适答案。
  • 兼容性:Deno 尝试与 Web 兼容——这意味着 Deno 程序应该可以在 Deno 和浏览器中运行。毕竟它只是一个可执行的 JavaScript(或 TypeScript)文件,不需要过多关注其环境。在考虑所有这些兼容性的同时,Deno 希望通过使用现代 JavaScript 和 TypeScript 功能来确保能够面向未来。
  • 安全性:默认情况下,Deno 是安全的。除非开发人员允许,否则不会进行文件、网络或环境的访问。这可以防止 Deno 脚本的恶意使用,这种恶意使用极有可能与 Node 脚本一样多。
  • 标准库:Deno 带有标准库,这意味着 Deno 中的应用程序比 Node 程序更自洽,因为 Deno 在 JavaScript 之上具有许多内部工具函数。此外 Deno 带有一些内置工具,可改善开发体验。

以下各节将详细介绍所有这些要点,同时从头开始逐步实现一个小的 Deno 程序。之后我们将继续用 Deno 开发真实的 Web 应用。

在 MacOS、Windows 和 Linux 上安装 Deno

有多种方法来设置 Deno 应用程序。对你而言,这取决于你的操作系统和在计算机上安装程序的工具链。例如我在 MacOS 上用 Homebrew 来管理计算机上的程序。对于你来说,可能还有其他选择,所以你应该从 Deno 网站获取的这个方法列表中为你的计算机使用适当的命令。这些命令应在集成终端或命令行界面中执行:

# Shell (Mac, Linux):
curl -fsSL https://Deno.land/x/install/install.sh | sh
 
# PowerShell (Windows):
iwr https://Deno.land/x/install/install.ps1 -useb | iex
 
# Homebrew (Mac):
brew install Deno
 
# Chocolatey (Windows):
choco install Deno
 
# Scoop (Windows):
scoop install Deno
 
# 用 Cargo 从源码构建并安装
cargo install Deno

安装 Deno 后,可以在命令行上验证其安装。你的版本可能比我的版本新,因为就我而言,我安装了 Deno 的第一个发行版本 1.0.0。但是以下各节将假定你安装了最新的 Deno 版本:

Deno --version
-> Deno 1.0.0

如果要升级Deno的版本,可以使用 Deno upgrade。另外还可以通过命令行执行下面的远程 Deno 程序,来验证 Deno 在你的计算机上是否能够正确运行:

Deno run https://Deno.land/std/examples/welcome.ts
-> Welcome to Deno

这个 Deno 程序只是在你的命令行上输出一段文本。但是它还向你展示了如何通过动态下载和编译 Deno 程序来从远程源执行该程序。如果你无法在计算机上设置 Deno,请按照 Deno 官方网站 上的安装说明进行操作。

HELLO Deno

每次我们学习新的编程语言知识时,都从 “Hello World” 示例开始。让我们的第一个 Deno 应用程也从这里开始。在命令行中,为你的 Deno 项目创建一个文件夹,进入到该文件夹​​,并创建一个新文件。你自己决定如何命名文件夹和文件:

mkdir Deno-project
cd Deno-project
touch index.js

然后在你喜欢的编辑器或 IDE 中打开新创建的 index.js 文件。输入以下 JavaScript 代码:

console.log('Hello Deno');

然后在命令行通过以下命令启动 Deno 程序。

Deno run index.js
-> Hello Deno

你的第一个 Deno 程序输出 “Hello Deno”。你已经为 Deno 项目创建了一个文件夹,为实现细节创建了一个 JavaScript 文件,并在命令行上通过 Deno 运行了该文件。无需其他设置。

Deno 的权限

以下各节将通过逐步介绍 Deno 的每个方面,来改进我们的第一个 Deno 程序。本节将讨论 Deno 中的权限,因为 Deno 在默认情况下是安全的。在本示例中,我们将了解这究竟意味着什么。

如果你想像我一样随时了解技术主题,你可能已经知道 Hacker News。你可以在这个网站上阅读有关技术的最新新闻。我喜欢在自己的教程中使用 Hacker News 的 API。为了学习有关 Deno 和权限中的数据获取的知识,我们将用这个 API 来获取数据。如果浏览 Hacker News API,则能够找到以下 URL 来请求有关某个主题下的文章:

http://hn.algolia.com/api/v1/search?query=...

我们将在 Deno 项目的 index.js 文件中使用此URL,来获取有关 JavaScript 的 Hacker News 文章:

const url = 'http://hn.algolia.com/api/v1/search?query=javascript';

接下来,用 Deno 内置的 fetch 函数处理 URL,该函数在 URL 上执行 HTTP GET 请求,并返回 JavaScript promise。你可以通过将其转换为 JSON 并用日志记录语句输出其结果来解决这个 promise:

const url = 'http://hn.algolia.com/api/v1/search?query=javascript';
 
fetch(url)
  .then((result) => result.json())
  .then((result) => console.log(result.hits));

如果你用 JavaScript 写过前端程序 ,则可能已经注意到,我们所使用的浏览器 API 为客户端程序提供了相同的 fetch API(或至少使用相同实现细节的接口)。如前所述,Deno 尝试与 Web 兼容,并且任何 Deno 程序在执行其代码时都应该能够在浏览器中以相同的方式工作。因此 Deno 确保客户端 JavaScript 程序中可用的 API 也可以在服务器端 Deno 应用程序中使用。

现在,在命令行上再次启动 Deno:

Deno run index.js

你应该会看到 Deno 提示的错误:“Uncaught PermissionDenied: network access to “http://hn.algolia.com/api/v1/search?query=javascript", run again with the –allow-net flag”。出现这个错误的原因是,在默认情况下 Deno 是安全的。如果我们在 Deno 的域中操作,可以无需授予Deno任何许可而做很多事情而。但是如果我们想超越 Deno 的职责范围,则需要明确允许它。在这种从远程 API 获取数据的情况下,需要允许网络请求:

Deno run --allow-net index.js

再次运行 Deno 程序后,你应该在命令行上看到一系列 Hacker News 文章。这个数组中的每个项目都有许多信息,为了便于阅读,让我们精简每个项目(文章)的属性。之后输出会应更具可读性:

const url = 'http://hn.algolia.com/api/v1/search?query=javascript';
 
fetch(url)
  .then((result) => result.json())
  .then((result) => {
    const stories = result.hits.map((hit) => ({
      title: hit.title,
      url: hit.url,
      createdAt: hit.created_at_i,
    }));
 
    console.log(stories);
  });

在本节中,你了解了 Deno 在默认情况下是安全的。我们必须允许自己能够访问 Deno 领域以外的所有内容,可能是网络访问或文件访问,否则 Deno 将会拒绝工作。

Deno的兼容性

前面你已经看到了怎样在 Deno 中使用 fetch。我们对浏览器中的 fetch API 是很熟悉的。所以在Deno 中可以用与浏览器端完全相同的接口,而不必为 Deno 使用新的 API。在使用 Deno 时我们不需要重新考虑自己的方法。

Deno 尝试跟上现代 JavaScript 功能,无论是在客户端还是在服务器上。以 async/await 为例,它仅在较新的 Node.js 版本中可用,默认情况下在 Deno 中是可用的。你不仅可以使用 async/await,而且还可以使用 async 的 top level await(这在 Node.js 中已经存在很长时间了):

const url = 'http://hn.algolia.com/api/v1/search?query=javascript';
 
const result = await fetch(url).then((result) => result.json());
 
const stories = result.hits.map((hit) => ({
  title: hit.title,
  url: hit.url,
  createdAt: hit.created_at_i,
}));
 
console.log(stories);

作为常规 promise 的 then 和 catch 块的代替,可以用 await 同步运行代码。在 await 语句之后的所有代码仅在 promise 解决后执行。如果这种实现要在函数中运行,则必须把函数声明为异步。开箱即用的 Deno 中提供了 Async/await 和 top level await。

Deno 的标准库

Deno 带有一组实用函数,这些函数被称为 Deno 的标准库(简称:Deno std)。 Deno 并没有从外部库中导入所有内容,而是尝试通过提供几种内部解决方案来使其可用。接下来我们尝试用下面的标准库解决方案之一来设置 Web 服务器:

import { serve } from 'https://Deno.land/std/http/server.ts';
 
const server = serve({ port: 8000 });
 
for await (const req of server) {
  req.respond({ body: 'Hello Deno' });
}

首先我们用绝对路径从标准库中进行命名导入。在 Deno 中,所有库导入(无论是从标准库还是从第三方库)均使用指向专用文件的绝对路径来完成。你从这个 以服务器文件形式存在的 http 库 导出一个名为served的函数。

serve 函数为我们创建了一个 Web 服务器,可通过已定义的端口对其进行访问。 JavaScript for await … of 用于遍历每个传入此服务器的请求。对于每个请求,服务器在响应正文中返回相同的文本。

再次运行你的 Deno 程序,然后在浏览器中导航到 http://localhost:8000 。因为要再次使用网络,所以需要授权:

Deno run --allow-net index.js

http://localhost:8000 和带有结尾斜杠的 http://localhost:8000/ 这两个 URL 在浏览器中的工作方式相同。当在浏览器中打开其中一个 URL 时,都会向 Deno 程序发出 HTTP GET 请求,并且该请求返回带有 Hello Deno 正文的 HTTP 响应,然后该响应将显示在浏览器中。

接下来用前面的代码扩展该示例。我们不会从服务器(Deno)上将硬编码文本发送回客户端(浏览器),而是从 Hacker News 获取最重要的 JavaScript 文章并将其发送给客户端:

import { serve } from 'https://Deno.land/std/http/server.ts';
 
const url = 'http://hn.algolia.com/api/v1/search?query=javascript';
 
const server = serve({ port: 8000 });
 
for await (const req of server) {
  const result = await fetch(url).then((result) => result.json());
 
  const stories = result.hits.map((hit) => ({
    title: hit.title,
    url: hit.url,
    createdAt: hit.created_at_i,
  }));
 
  req.respond({ body: JSON.stringify(stories) });
}

再次启动 Deno 程序后,应该能够看到从 fetch 请求中得到的结果以 JSON 的形式打印在浏览器中。以前我们只在命令行上看到这个结果,但是现在有了 Web 服务器,可以将结果发送到客户端(浏览器)。

Deno 库

只依赖 Deno 的标准库还不足以创建 Deno 程序,这就需要第三方库(也称为外部库或库)发挥作用了。如果你再次从浏览器的最后一部分中检查结果,可能会注意到 createdAt 的格式对人类很不友好,我们将用 date-fns 库来使其可读:

Deno 中的库通过绝对路径直接从 Web 导入。在浏览器中再次打开 URL,并阅读其中的源代码,并检查它是否真的导出了默认函数,即此处的 format 函数:

import { serve } from 'https://Deno.land/std/http/server.ts';
import format from 'https://Deno.land/x/date_fns/format/index.js';
 
const url = 'http://hn.algolia.com/api/v1/search?query=javascript';
 
const server = serve({ port: 8000 });
 
for await (const req of server) {
  const result = await fetch(url).then((result) => result.json());
 
  const stories = result.hits.map((hit) => ({
    title: hit.title,
    url: hit.url,
    createdAt: format(
      new Date(hit.created_at_i * 1000),
      'yyyy-MM-dd'
    ),
  }));
 
  req.respond({ body: JSON.stringify(stories) });
}

format 函数有两个强制性参数:日期和格式化日期的模式。我们从 Hacker News API 收到的日期是一个 unix 时间戳 ,以秒为单位;所以要先把它转换为毫秒,然后再从中创建 JavaScript 日期。为函数第二个参数提供的模式使日期易于阅读。

再次启动 Deno 程序后,你会看到它从库中下载了 format 函数以及所有依赖项。由于使用了函数的直接 URL,所以只下载了库的这一部分。如果试着包含整个库路径,将会看到整个库将被下载, format 只是许多命名的导出(大括号)之一:

import { serve } from 'https://Deno.land/std/http/server.ts';
import { format } from 'https://Deno.land/x/date_fns/index.js';
 
const url = 'http://hn.algolia.com/api/v1/search?query=javascript';
 
const server = serve({ port: 8000 });
 
for await (const req of server) {
  ...
}

当再次启动 Deno 时,都会使用被缓存的库,所以无需再次下载。它都会检查所有的导入,将其下载并捆绑到一个可执行文件中。在 Deno 中导入库的方式受到 Go 语言 的启发。不必在文件中保留依赖项列表(例如,Node.js 的*package.json*),也不需要使所有模块在项目中可见(例如,Node.js 的 *node_modules*)。

Deno 的导入

你已经了解到 Deno 的标准库或第三方库的导入是通过绝对路径执行的。这种方法的灵感来自 Go 语言,因为它不会产生太多混淆的空间。因为你的 Deno 程序有多个文件,因此可以用相对路径导入它们。

来看看它是怎样工作的:首先,在项目中创建一个名为 stories.js 的文件,该文件应该与 index.js 文件在同一路径下。在 stories.js 文件中,输入以下代码实现,这段代码本质上上是我们之前在其他文件中所做的映射:

import { format } from 'https://Deno.land/x/date_fns/index.js';
 
export const mapStory = (story) => ({
  title: story.title,
  url: story.url,
  createdAt: format(
    new Date(story.created_at_i * 1000),
    'yyyy-MM-dd'
  ),
});

stories.js 文件中进行命名导出,并在 index.js 文件中进行命名导入,并在稍后的代码中使用该函数:

import { serve } from 'https://Deno.land/std/http/server.ts';
 
import { mapStory } from './stories.js';
 
const url = 'http://hn.algolia.com/api/v1/search?query=javascript';
 
const server = serve({ port: 8000 });
 
for await (const req of server) {
  const result = await fetch(url).then((result) => result.json());
 
  const stories = result.hits.map(mapStory);
 
  req.respond({ body: JSON.stringify(stories) });
}

这就是在 Deno 中导出和导入文件的过程。与之前所用的绝对路径不同,我们用相对路径来导入必要的内容。还要注意的是,无论绝对路径还是相对路径,我们都必须始终包含文件扩展名,因为不能留下任何产生歧义的余地。

在 Deno 中进行测试

在编程的过程中,测试不应该事后再去考虑,在 Deno 中也一样,测试是必不可少的。接下来通过编写第一个单元测试来了解其工作原理。首先创建一个新的 stories.test.js 文件。然后编写以下代码:

import { mapStory } from './stories.js';
 
Deno.test('maps to a smaller story with formatted date', () => {
 
});

Deno 为我们提供了一个 test 功能,用来定义名称、说明和实际的测函数。怎样在函数主体中实现测试取决于我们自己。我们已经导入了要测试的函数(即 mapStory),该函数实际上只接收一个文章列表数组,并返回具有较少属性和格式化日期的新文章数组。我们需要做的就是定义一个用于 mapStory 的文章列表和一个其假定为该函数的输出的文章列表:

import { assertEquals } from 'https://Deno.land/std/testing/asserts.ts';
 
import { mapStory } from './stories.js';
 
Deno.test('maps to a smaller story with formatted date', () => {
  const stories = [
    {
      id: '1',
      title: 'title1',
      url: 'url1',
      created_at_i: 1476198038,
    },
  ];
 
  const expectedStories = [
    {
      title: 'title1',
      url: 'url1',
      createdAt: '2016-10-11',
    },
  ];
 
  assertEquals(stories.map(mapStory), expectedStories);
});

Deno 的标准库提供了 assertEquals 函数用来声明两个值。第一个值是要测试的函数的输出,第二个值是预期的输出。如果两者都匹配,则测试应变为绿色。如果它们不匹配,则测试应失败并变为红色。在命令行上运行所有测试:

Deno test
 
-> running 1 tests
-> test maps to a smaller story with formatted date ... ok (9ms)
 
-> test result: ok. (10ms)
-> 1 passed;
-> 0 failed;
-> 0 ignored;
-> 0 measured;
-> 0 filtered out

测试变成绿色。用 Deno test 命令将拾取所有具有命名模式 test.{js,ts,jsx,tsx} 的文件。你也可以用 Deno test <file_name> 仅测试特定文件,在本例中为 Deno test stories.test.js

在 Deno 中使用 TypeScript

Deno 支持把 JavaScript 和 TypeScript 同时作为第一语言。这就是为什么进行文件导入时要始终包含文件扩展名的原因——无论这些文件是从 Deno 项目的相对路径导入还是从 Deno 标准库或第三方库绝对路径导入。

由于 Deno 把 TypeScript 作为一等公民提供支持,所以可以把 stories.js 文件重命名为 stories.ts。你会注意到需要调整所有导入——在 index.jsstories.test.js 中指向该文件,因为文件扩展名从 .js 被改为了 *.ts*。

带有所有实现细节的 stories.ts 文件现在需要类型。我们将通过 StoryFormattedStory 提供函数输入和输出的接口:

import { format } from 'https://Deno.land/x/date_fns/index.js';
 
interface Story {
  title: string;
  url: string;
  created_at_i: number;
}
 
interface FormattedStory {
  title: string;
  url: string;
  createdAt: string;
}
 
export const mapStory = (story: Story): FormattedStory => ({
  title: story.title,
  url: story.url,
  createdAt: format(
    new Date(story.created_at_i * 1000),
    'yyyy-MM-dd'
  ),
});

现在这个 function 已完全类型化了。通过将 stories.test.js 文件重命名为 *stories.test.ts*,并将 index.js 文件重命名为 *index.ts*,你可以自己继续把 JavaScript 转换为 TypeScript。这些新的 TypeScript文件并不是都需要添加类型或接口,因为大多数类型是自动推导的。

如果要再次启动 Deno 应用程序,这时必须调整 Deno 脚本的文件扩展名:

Deno run --allow-net index.ts

Deno 带有默认的 TypeScript 配置。如果要自定义它,可以添加自定义 tsconfig.json 文件。毕竟,由于 TypeScript 和 JavaScript 一样,都是一等的公民,所以由你自己决定为将来的 Deno 项目选择哪种文件扩展名。

Deno中的环境变量

环境变量非常适合隐藏有关 Deno 程序的敏感信息。这可以是 API 密钥、密码或他人不应该看到的数据。这就是我们要通过创建 .env 文件来隐藏敏感信息的原因。接下来我们将创建这个文件,并把以下信息传给服务器程序的端口:

PORT=8000

index.ts 文件中,我们可以把这个环境变量与第三方库在一起配合使用:

import { serve } from 'https://Deno.land/std/http/server.ts';
import { config } from 'https://Deno.land/x/dotenv/mod.ts';
 
import { mapStory } from './stories.ts';
 
const url = 'http://hn.algolia.com/api/v1/search?query=javascript';
 
const server = serve({
  port: parseInt(config()['PORT']),
});
 
for await (const req of server) {
  const result = await fetch(url).then((result) => result.json());
 
  const stories = result.hits.map(mapStory);
 
  req.respond({ body: JSON.stringify(stories) });
}

config 函数从 .env 文件返回带有有所有键值对的对象。我们必须将 'PORT' 键的值解析为数字,因为它可以在对象中作为字符串使用。现在该信息不会存在于源代码中,而仅在环境变量文件中可用。

再次启动 Deno 程序后,你应该在命令行上看到另一个权限错误:*“Uncaught PermissionDenied: read access to “/Users/mydspr/Developer/Repos/Deno-example”, run again with the –allow-read flag”*。可以用另一个权限标志来允许访问环境变量:

Deno run --allow-net --allow-read index.ts

重要提示:.env 文件不应在每个人都可以看到的公共存储库中共享。如果你将源代码公开(例如在GitHub上),请考虑将 .env 文件添加到 .gitignore 文件中。

毕竟服务器程序的端口不是敏感数据的最好例子。我们使用端口是为了了解环境变量。但是一旦你处理了Deno 程序的更多功能,最终可能会得到源代码所中使用的信息,这些信息对于其他人不可见。


总结

本文向你介绍了 Deno 所有的基础知识。从小型脚本到功能完善的服务器应用,Deno 将在与 Node.js 相同的领域中使用,但其默认设置会大大改善。在默认情况下,它是权限是安全的,并与许多客户端的 API 兼容,有着诸如 top level await 等现代功能,并支持 JavaScript 和 TypeScript 。