在本文中,我们将探讨Deno[https://deno.land/],这是一个相对较新的工具,它是Node.js的竞争对手/替代产品,它提供了更安全的环境,并提供了TypeScript支持。
我们将使用Deno构建命令行工具,以向第三方API发出请求,并查看Deno提供的功能,与Node的区别以及使用时的感觉。
Deno是一个用TypeScript编写的,更自以为是的运行时,它包含自己的代码格式化程序(deno fmt
),并使用ES模块-看不到CommonJSrequire
语句。默认情况下,它也是非常安全的:您必须显式地授予代码权限以发出网络请求或从磁盘读取文件,这是Node默认情况下允许程序执行的操作。在本文中,我们将介绍安装Deno,设置环境以及构建简单的命令行应用程序以发出API请求。
与以往一样,您可以在GitHub上找到本文附带的代码[https://github.com/sitepoint-editors/deno-star-wars-api]。
安装Deno
您可以查看Deno网站上的完整说明。如果您使用的是macOS或Linux,则可以将此命令复制到终端中:
curl -fsSL https://deno.land/x/install/install.sh | sh
您还需要将install目录添加到$PATH
。
如果您使用的是Windows,请不要担心,因为您可以通过Chocolatey等软件包管理器来安装Deno:
choco install deno
如果Chocolatey不是您的理想选择,deno_install会列出多种安装方法,因此请选择最适合您的安装方法。
您可以通过运行以下命令来检查是否已安装Deno:
deno -V
这应该输出Deno版本。在撰写本文时,最新版本是1.7.5,这是我正在使用的版本。
如果您使用的是VS Code,我强烈建议您安装Deno VS Code插件[https://github.com/denoland/vscode_deno]。如果您使用其他编辑器,请查看Deno文档[https://deno.land/manual@v1.7.4/getting_started/setup_your_environment#editors-and-ides]以找到正确的插件。
请注意,如果您使用的是VS Code,则默认情况下,在加载项目时不会启用Deno插件。您应该.vscode/settings.json
在存储库中创建一个文件,并添加以下内容以启用该插件:
{ "deno.enable": true }
同样,如果您不是VS Code用户,请查看上面的手册,为您选择的编辑器找到正确的设置。
编写我们的第一个脚本
确保我们已启动并运行Deno。创建index.ts
以下内容并放入其中:
console.log("hello world!");
我们可以这样运行deno run index.ts
:
$ deno run index.ts Check file:///home/jack/git/deno-star-wars-api/index.ts hello world
请注意,我们可能会在编辑器中看到TypeScript错误:
'index.ts' cannot be compiled under '--isolatedModules' because it is considered a global script file. Add an import, export, or an empty 'export {}' statement to make it a module.ts(1208)
发生此错误是因为TypeScript不知道此文件将使用ES模块导入。很快,因为我们要添加导入,但是与此同时,如果要删除错误,可以export
在脚本底部添加一个空语句:
export {}
这将使TypeScript编译器确信我们正在使用ES模块,并摆脱了该错误。我不会在博客文章的任何代码示例中包含此内容,但是如果我们添加它(除了消除TypeScript噪音),则不会更改任何内容。
在Deno中获取
Deno实现了对与我们在浏览器中使用的相同Fetch API的支持。它内置于Deno中-这意味着没有要安装或配置的软件包。通过向我们将在此处使用的API(星球大战API(或SWAPI))发出第一个请求,来了解它的工作原理。
提出要求后,https://swapi.dev/api/people/1/
我们将获得Luke Skywalker所需的所有数据。让我们更新index.ts
文件以发出该请求。更新index.ts
为如下所示:
const json = fetch("https://swapi.dev/api/people/1"); json.then((response) => { return response.json(); }).then((data) => { console.log(data); });
尝试使用以下命令在您的终端中运行此命令deno run
:
$ deno run index.ts Check file:///home/jack/git/deno-star-wars-api/index.ts error: Uncaught (in promise) PermissionDenied: network access to "swapi.dev", run again with the --allow-net flag throw new ErrorClass(res.err.message);
默认情况下,Deno是安全的,这意味着脚本需要权限才能执行可能被视为危险的任何操作,例如读/写文件系统和发出网络请求。我们必须在Deno脚本运行时授予其权限,以允许他们执行此类操作。我们可以使用以下--allow-net
标志启用我们的标志:
$ deno run --allow-net index.ts Check file:///home/jack/git/deno-star-wars-api/index.ts { name: "Luke Skywalker", ...(data snipped to save space)... }
但是此标志已授予脚本访问任何URL的权限。我们可以更明确一些,只允许脚本访问我们添加到允许列表中的URL:
$ deno run --allow-net=swapi.dev index.ts
如果我们运行的脚本是我们自己编写的,我们可以相信它们不会做他们不应该做的任何事情。但是很高兴知道,默认情况下,如果没有我们首先允许它的执行,我们执行的任何Deno脚本都不会造成太大的破坏。从现在开始,每当我在本文中谈论运行脚本时,这就是我正在运行的命令:
$ deno run --allow-net=swapi.dev index.ts
我们还可以使用顶级await编写此脚本,该脚本稍有不同,这使我们可以使用await
关键字而不是处理承诺:
const response = await fetch("https://swapi.dev/api/people/1/"); const data = await response.json(); console.log(data);
这是我喜欢的样式,将在本文中使用,但是如果您希望兑现承诺,请放心。
安装第三方依赖项
现在,我们可以向《星球大战》 API发出请求了,让我们开始考虑如何允许我们的用户使用此API。我们将提供命令行标志,以使它们指定要查询的资源(例如人,电影或行星)以及用于对其进行过滤的查询。因此,对我们的命令行工具的调用可能如下所示:
$ deno run --allow-net=swapi.dev index.ts --resource=people --query=luke
我们可以手动解析这些额外的命令行参数,也可以使用第三方库。在Node.js中,最好的解决方案是Yargs,并且Yargs还支持Deno,因此我们可以使用Yargs解析和处理我们要支持的命令行标志。
但是,没有Deno的软件包管理器。我们不会创建package.json
和安装依赖项。相反,我们从URL导入。Deno软件包的最佳来源是Deno软件包存储库,您可以在其中搜索所需的软件包。现在,最流行的npm软件包也支持Deno,因此通常在那里有很多选择,而且很可能会找到所需要的东西。
在撰写本文时,在Denoyargs
存储库中进行搜索会给我yargs 16.2.0。要在本地使用它,我们必须从其URL导入它:
import yargs from "https://deno.land/x/yargs/deno.ts";
现在,当我们运行脚本时,我们将首先看到很多输出:
$ deno run --allow-net=swapi.dev index.ts Download https://deno.land/x/yargs/deno.ts Warning Implicitly using latest version (v16.2.0-deno) for https://deno.land/x/yargs/deno.ts Download https://deno.land/x/yargs@v16.2.0-deno/deno.ts Download https://deno.land/x/yargs@v16.2.0-deno/build/lib/yargs-factory.js Download https://deno.land/x/yargs@v16.2.0-deno/lib/platform-shims/deno.ts Download https://deno.land/std/path/mod.ts Download https://deno.land/x/yargs_parser@v20.2.4-deno/deno.ts...(more output removed to save space)
Deno第一次看到我们正在使用一个新模块时,它将在本地下载并缓存它,因此我们不必每次使用该模块并运行脚本时都下载它。
请注意以上输出中的这一行:
Warning Implicitly using latest version (v16.2.0-deno) for https://deno.land/x/yargs/deno.ts
这是Deno告诉我们的,我们在导入Yargs时未指定特定版本,因此它只是下载了最新版本。这对于快速的辅助项目可能很好,但是通常,将导入内容固定到我们要使用的版本是一个好习惯。我们可以通过更新URL来做到这一点:
import yargs from "https://deno.land/x/yargs@v16.2.0-deno/deno.ts";
我花了一些时间弄清楚该URL。我发现它是通过识别在Deno存储库中搜索“ yargs”时使用的URL来找到的https://deno.land/x/yargs@v16.2.0-deno
。然后,我回头看了看控制台输出,意识到Deno实际上给了我确切的路径:
Warning Implicitly using latest version (v16.2.0-deno) for https://deno.land/x/yargs/deno.ts Download https://deno.land/x/yargs@v16.2.0-deno/deno.ts
我强烈建议像这样固定您的版本号。这将避免有一天出现一个令人惊讶的问题,因为您恰好在新版本的依赖项之后运行。
deno fmt
在继续构建命令行工具之前,请先快速浏览一下。Deno带有内置的格式化程序,deno fmt
可以自动将代码格式化为一致的样式。可以将其像Prettier一样,但专门针对Deno,并且是内置的。我喜欢可以为您提供所有这些功能而无需进行任何配置的工具。
我们可以这样在本地运行格式化程序:
$ deno fmt
这将格式化当前目录中的所有JS和TS文件,或者我们可以给它一个文件名来格式化:
$ deno fmt index.ts
或者,如果我们拥有VS Code扩展名,则可以转到.vscode/settings.json
,我们在前面启用了Deno插件,并添加了以下两行:
{ "deno.enable": true, "editor.formatOnSave": true, "editor.defaultFormatter": "denoland.vscode-deno"}
这将VS Code配置为deno fmt
在我们保存文件时自动运行。完美的!
使用Yargs
我不会介绍Yargs的全部细节(如果您想熟悉它可以做的所有事情,可以阅读文档),但是这是我们声明要采用两个命令行参数的方式所需的:--resource
和--query
:
import yargs from "https://deno.land/x/yargs@v16.2.0-deno/deno.ts";const userArguments: { query: string; resource: "films" | "people" | "planets";} = yargs(Deno.args) .describe("resource", "the type of resource from SWAPI to query for") .choices("resource", ["people", "films", "planets"]) .describe("query", "the search term to query the SWAPI for") .demandOption(["resource", "query"]) .argv;console.log(userArguments);
注意:现在我们有了一条import
语句,我们不再需要export {}
使TypeScript错误消失。
不幸的是,在编写TypeScript时,似乎并没有掌握所有的类型定义:的返回类型yargs(Deno.args)
设置为{}
,所以让我们整理一下。我们可以定义自己的TypeScript接口,该接口涵盖我们所依赖的Yargs API的所有部分:
interface Yargs<ArgvReturnType> { describe: (param: string, description: string) => Yargs<ArgvReturnType>; choices: (param: string, options: string[]) => Yargs<ArgvReturnType>; demandOption: (required: string[]) => Yargs<ArgvReturnType>; argv: ArgvReturnType;}
在这里,我声明了我们正在使用的函数,并且它们返回了相同的Yargs接口(这就是让我们链接调用的原因)。我还采用了通用类型,ArgvReturnType
它表示在Yargs处理完参数后返回的参数的结构。这意味着我可以声明一个UserArguments
类型并将其结果强制转换yargs(Deno.argv)
为它:
interface Yargs<ArgvReturnType> { describe: (param: string, description: string) => Yargs<ArgvReturnType>; choices: (param: string, options: string[]) => Yargs<ArgvReturnType>; demandOption: (required: string[]) => Yargs<ArgvReturnType>; argv: ArgvReturnType;}interface UserArguments { query: string; resource: "films" | "people" | "planets";}const userArguments = (yargs(Deno.args) as Yargs<UserArguments>) .describe("resource", "the type of resource from SWAPI to query for") .choices("resource", ["people", "films", "planets"]) .describe("query", "the search term to query the SWAPI for") .demandOption(["resource", "query"]) .argv;
我敢肯定,将来Yargs可能会立即提供这些类型,因此,如果您使用的Yargs版本高于16.2.0,则值得检查。
查询《星球大战》 API
现在,我们有了接受用户输入的方法,让我们编写一个函数,该函数接受输入的内容并正确查询《星际大战》 API:
async function queryStarWarsAPI( resource: "films" | "people" | "planets", query: string,): Promise<{ count: number; results: object[];}> { const url = `https://swapi.dev/api/${resource}/?search=${query}`; const response = await fetch(url); const data = await response.json(); return data;}
我们将接受两个参数:要搜索的资源,然后是搜索词本身。“星球大战” API返回的count
结果将返回一个对象,该对象包括(结果数)和一个results
数组,该数组是API查询中所有匹配资源的数组。我们将在本文的后面部分讨论如何改进这种类型的安全性,但是到目前为止,我已经object
开始着手了。它不是一个很好的类型,因为它非常自由,但是有时候我更喜欢使某些东西起作用,然后在以后改进这些类型。
现在我们有了这个功能,我们可以接受Yargs解析的参数并获取一些数据!
const result = await queryStarWarsAPI( userArguments.resource, userArguments.query,);console.log(`${result.count} results`);
现在运行此命令:
$ deno run --allow-net=swapi.dev index.ts --resource films --query phantomCheck file:///home/jack/git/deno-star-wars-api/index.ts1 results
我们看到我们得到一个结果(不久之后我们将对不正确的复数进行处理!)。让我们根据用户搜索的资源做一些工作以获得更好的输出。首先,我将做一些TypeScript的工作来改进返回类型,以便我们在编辑器中从TypeScript获得更好的支持。
首先要做的是创建一个表示资源的新类型,我们让用户查询以下资源:
type StarWarsResource = "films" | "people" | "planets";
然后,我们可以使用此类型而不是复制它,首先是将其传递给Yargs,其次是定义queryStarWarsAPI
函数:
interface UserArguments { query: string; resource: StarWarsResource;}// ...async function queryStarWarsAPI( resource: StarWarsResource, query: string,): Promise<{ count: number; results: object[];}> { ... }
接下来,让我们看一下《星球大战》 API,并创建代表我们将获得的不同资源的接口。这些类型并不详尽(API返回更多)。我为每种资源选择了一些项目:
interface Person { name: string; films: string[]; height: string; mass: string; homeworld: string;}interface Film { title: string; episode_id: number; director: string; release_date: string;}interface Planet { name: string; terrain: string; population: string;}
一旦拥有了这些类型,就可以创建一个函数来处理每种类型的结果,然后对其进行调用。我们可以使用类型转换来告诉TypeScript result.results
(它认为是object[]
)实际上是我们的接口类型之一:
console.log(`${result.count} results`);switch (userArguments.resource) { case "films": { logFilms(result.results as Film[]); break; } case "people": { logPeople(result.results as Person[]); break; } case "planets": { logPlanets(result.results as Planet[]); break; }}function logFilms(films: Film[]): void { ... }function logPeople(people: Person[]): void { ... }function logPlanets(planets: Planet[]): void { ... }
一旦完成了一些日志记录,我们的CLI工具就完成了!
function logFilms(films: Film[]): void { films.forEach((film) => { console.log(film.title); console.log(`=> Directed by ${film.director}`); console.log(`=> Released on ${film.release_date}`); });}function logPeople(people: Person[]): void { people.forEach((person) => { console.log(person.name); console.log(`=> Height: ${person.height}`); console.log(`=> Mass: ${person.mass}`); });}function logPlanets(planets: Planet[]): void { planets.forEach((planet) => { console.log(planet.name); console.log(`=> Terrain: ${planet.terrain}`); console.log(`=> Population: ${planet.population}`); });}
最后,让我们解决以下事实:它输出1 results
而不是1 result
:
function pluralise(singular: string, plural: string, count: number): string { return `${count} ${count === 1 ? singular : plural}`;}console.log(pluralise("result", "results", result.count));
现在,我们的CLI输出看起来不错!
$ deno run --allow-net=swapi.dev index.ts --resource planets --query tatCheck file:///home/jack/git/deno-star-wars-api/index.ts1 resultTatooine=> Terrain: desert=> Population: 200000
整理
现在,我们所有的代码都是一个大index.ts
文件。让我们创建一个api.ts
文件,并将大多数API逻辑移入其中。
不要忘了export
在此文件中将所有类型,接口和函数添加到最前面,因为我们需要将它们导入index.ts
:
// api.tsexport type StarWarsResource = "films" | "people" | "planets";export interface Person { name: string; films: string[]; height: string; mass: string; homeworld: string;}export interface Film { title: string; episode_id: number; director: string; release_date: string;}export interface Planet { name: string; terrain: string; population: string;}export async function queryStarWarsAPI( resource: StarWarsResource, query: string,): Promise<{ count: number; results: object[];}> { const url = `https://swapi.dev/api/${resource}/?search=${query}`; const response = await fetch(url); const data = await response.json(); return data;}
然后我们可以从中导入它们index.ts
:
import { Film, Person, Planet, queryStarWarsAPI, StarWarsResource,} from "./api.ts"
现在我们index.ts
看起来更加整洁,并且已经将API的所有详细信息移到了单独的模块中。
分布
假设我们现在想将此脚本分发给朋友。我们可以与他们共享整个存储库,但是如果他们只想运行脚本,那就太过分了。
我们可以deno bundle
将所有代码捆绑到一个JavaScript文件中,并安装所有依赖项。这样,共享脚本就是共享一个文件的情况:
$ deno bundle index.ts out.js
我们可以deno.run
像以前一样将此脚本传递给。现在的区别在于,Deno不必进行任何类型检查或安装任何依赖项,因为它们已经out.js
为我们全部使用了。这意味着运行这样的捆绑脚本可能比从TypeScript源代码运行更快:
$ deno run --allow-net=swapi.dev out.js --resource films --query phantom1 result The Phantom Menace=> Directed by George Lucas=> Released on 1999-05-19
我们拥有的另一种选择是使用生成单个可执行文件deno compile[https://deno.land/manual@v1.7.4/tools/compiler]
。请注意,在撰写本文时,这被认为是实验性的,因此请谨慎行事,但是我希望将其包括在内,因为我希望它在将来会变得稳定并变得更加普遍。
我们可以deno compile --unstable --allow-net=swapi.dev index.ts
要求Deno为我们构建一个自包含的可执行文件。该--unstable
标志是必需的,因为此功能是试验性的,尽管将来不应该使用。这样做的好处是,我们在编译时传递了安全标志-在我们的情况下,它允许访问Star Wars API。这意味着,如果我们将此可执行文件提供给用户,则他们将不必了解有关配置标志的信息:
$ deno compile --unstable --allow-net=swapi.dev index.ts Check file:///home/jack/git/deno-star-wars-api/index.ts Bundle file:///home/jack/git/deno-star-wars-api/index.ts Compile file:///home/jack/git/deno-star-wars-api/index.ts Emit deno-star-wars-api
现在我们可以直接运行该可执行文件:
$ ./deno-star-wars-api --resource people --query jar jar 1 result Jar Jar Binks => Height: 196 => Mass: 66
我怀疑将来这将成为分发用Deno编写的命令行工具的主要方式,希望它不久就可以失去实验状态。
结论
在本文中,通过构建CLI工具,我们学习了如何使用Deno从第三方API中获取数据并显示结果。我们了解了Deno如何实现对我们习惯于在浏览器中使用的同一Fetch API的支持,如何fetch
内置于Deno标准库中以及如何await
在程序的顶层使用而无需将所有内容包装在IFFE。
我希望您会同意我的观点,关于Deno有很多值得一去的地方。它提供了一个非常有生产力的环境,并带有TypeScript和格式化程序。不用软件包管理器的开销真是太好了,尤其是在编写小型辅助工具时,并且能够编译成一个可执行文件的功能意味着与您的同事和朋友共享这些工具真的很容易。
网友评论文明上网理性发言 已有0人参与
发表评论: