Deno指南:使用新的服务器端JavaScript和TypeScript运行时的示例



朋友们,美好的一天!



我提请您注意Flavio Copes撰写的文章“ Deno手册:带有代码示例的TypeScript运行时教程”的翻译。



在本文中,我们将学习如何与Deno合作。我们将其与Node.js进行比较,并使用它构建一个简单的REST API。



什么是迪诺?



如果您熟悉流行的服务器端JavaScript生态系统Node.js,则Deno几乎是同一回事。差不多,但是不完全是。



让我们从我最喜欢的Deno功能列表开始:



  • 它基于现代JavaScript
  • 它具有可扩展的标准库
  • 它具有标准的TypeScript支持(这意味着您不必手动编译TypeScript,Deno会自动进行编译)
  • 支持ES模块
  • 它没有包管理器
  • 它具有全球性 await
  • 它具有内置的测试设施
  • 其目标是最大程度地提高浏览器兼容性。为此,它提供了一个内联fetch对象和一个全局对象window


在本教程中,我们将探讨所有这些可能性。



熟悉Deno及其功能之后,Node.js似乎有点过时了。



特别是因为Node.js基于回调函数(它是在promise和async / await之前编写的)。它们不太可能出现在此处,因为这意味着需要进行根本的更改。



Node.js非常棒,并且将仍然是JavaScript世界中的事实上的标准。但是,我相信Deno将由于其TypeScript支持和现代标准库而迅速普及。



Deno可以负担得起现代代码,因为它不需要向后兼容。当然,不能保证此代码在下一个十年内会保持最新,但是今天仍然如此。



为什么是德诺?为什么现在?



Deno是大约2年前由JSConf EU的Node.js创建者Ryan Dahl宣布的。观看YouTube视频,它非常有趣,并且必须查看您是否正在使用Node.js和JavaScript。



每个项目经理(创建者)都必须做出决定。 Ryan对Node中的一些早期决定感到遗憾。此外,技术日新月异,如今的JavaScript与2009年的Node完全不同。回想一下ES6 / 2016/2017等。



因此,他决定启动一个新项目,这是服务器端JavaScript应用程序的第二次浪潮。



我现在只写这篇文章的原因是因为这项技术要花很长时间才能成熟。最终,我们获得了Deno 1.0(它于2020年5月13日发布),这是第一个稳定的版本。



这似乎是一个普通数字,但1.0表示直到Deno 2.0之前都不会有任何大的变化。学习新技术时,您不希望它变化太快。



您应该学习Deno吗?



好问题。



学习像Deno这样的新东西需要付出很多努力。我的建议:如果您刚开始使用服务器端JS并且还不了解Node.js,并且以前从未编写过TypeScript,请从Node开始。



从来没有人因为选择Node而被解雇(用著名的说法解释)。



但是,如果您喜欢不依赖于大量npm软件包的TypeScript,并且想在各处使用它await,那么Deno可能就是您想要的。



它将取代Node.js吗?



没有。Node.js是一个巨大的权威,这是一种获得了令人难以置信的良好支持的技术,该技术在未来十年内不会出现。



一流的TypeScript支持



Deno用Rust和TypeScript编写,这两种语言在当今世界都很流行。



这意味着即使我们正在编写JavaScript,我们也可以从TypeScript中获得很多好处。



使用Deno运行TypeScript代码不需要编译-Deno会自动进行编译。



您不必编写TypeScript代码,但是Deno的核心是用TypeScript编写的事实带来了巨大的变化。



首先,很大一部分JavaScript开发人员喜欢TypeScript。



其次,您使用的工具可以获得有关TypeScript软件(如Deno)的大量信息。



这意味着,例如,当我们使用VS Code编写代码(自诞生以来就与TypeScript紧密集成)时,我们将获得诸如编写代码时进行类型检查或高级IntelliSense功能的好处。换句话说,代码编辑器的帮助变得更加高效。



与Node.js的区别



由于Deno本质上是Node.js的替代品,因此比较两者是有意义的。



一般:



  • 两者均基于V8引擎
  • 两者都非常适合服务器端JavaScript开发


差异:



  • Node用C ++和JavaScript编写。Deno用Rust和TypeScript编写。
  • Node有一个正式的软件包管理器npmDeno没有这样的管理器,而是让您使用URL导入任何模块。
  • Node使用CommonJS语法导入软件包。Deno使用官方方式-ES模块。
  • Deno ECMAScript , Node.js .
  • Deno () . . Node.js , .
  • Deno , .. , , Go, . .




没有程序包管理器并使用URL来获取和导入程序包有其优点和缺点。主要优点之一是创建包而无需将其发布到npm之类的存储库所具有的极大灵活性。



我认为Deno软件包管理器的某些替代方案迟早会出现。



Deno官方网站托管第三方软件包:https : //deno.land/x/



安装Deno



够说话了!让我们安装Deno。



最简单的方法是使用Homebrew:



    brew install deno 






其他安装方法在此处列出



安装后,该命令变为可用deno输入以下内容即可获得帮助deno --help



flavio@mbp~> deno --help
deno 0.42.0
A secure JavaScript and TypeScript runtime

Docs: https://deno.land/std/manual.md
Modules: https://deno.land/std/ https://deno.land/x/
Bugs: https://github.com/denoland/deno/issues

To start the REPL, supply no arguments:
  deno

To execute a script:
  deno run https://deno.land/std/examples/welcome.ts
  deno https://deno.land/std/examples/welcome.ts

To evaluate code in the shell:
  deno eval "console.log(30933 + 404)"

Run 'deno help run' for 'run'-specific flags.

USAGE:
    deno [OPTIONS] [SUBCOMMAND]

OPTIONS:
    -h, --help
            Prints help information

    -L, --log-level <log-level>
            Set log level [possible values: debug, info]

    -q, --quiet
            Suppress diagnostic output
            By default, subcommands print human-readable diagnostic messages to stderr.
            If the flag is set, restrict these messages to errors.
    -V, --version
            Prints version information


SUBCOMMANDS:
    bundle         Bundle module and dependencies into single file
    cache          Cache the dependencies
    completions    Generate shell completions
    doc            Show documentation for a module
    eval           Eval script
    fmt            Format source files
    help           Prints this message or the help of the given subcommand(s)
    info           Show info about cache or info related to source file
    install        Install script as an executable
    repl           Read Eval Print Loop
    run            Run a program given a filename or url to the module
    test           Run tests
    types          Print runtime TypeScript declarations
    upgrade        Upgrade deno executable to newest version

ENVIRONMENT VARIABLES:
    DENO_DIR             Set deno's base directory (defaults to $HOME/.deno)
    DENO_INSTALL_ROOT    Set deno install's output directory
                         (defaults to $HOME/.deno/bin)
    NO_COLOR             Set to disable color
    HTTP_PROXY           Proxy address for HTTP requests
                         (module downloads, fetch)
    HTTPS_PROXY          Same but for HTTPS


迪诺队



您是否注意到该部分SUBCOMMANDS这是我们可以运行的所有命令的列表。我们有哪些团队?



  • bundle -将模块和项目依赖项收集到一个文件中
  • cache -缓存依赖项
  • completions -生成贝壳笔芯
  • doc -显示模块的文档
  • eval -用于计算代码块,例如 deno eval "console.log(1 + 2)"
  • fmt-内置代码格式化程序(如goFmtGo中的代码
  • help -显示辅助命令列表
  • info -显示有关缓存或文件的信息
  • install -将脚本设置为可执行
  • repl -读取-计算-输出周期(默认)
  • run -使用模块的给定名称或URL运行程序
  • test -运行测试
  • types -显示TypeScript功能列表
  • upgrade -将Deno更新到最新版本


您可以运行deno <subcommand> help以获取有关特定命令的信息,例如deno run --help



我们可以使用命令deno来启动read-evaluate-output循环:







这与starting相同deno repl



通常deno用于启动TypeScript文件中包含的Deno应用程序。



您可以同时运行TypeScript文件(.ts)和JavaScript文件(.js)。



如果您不熟悉TypeScript,请不要担心:Deno是用TypeScript编写的,但是您可以使用JavaScript编写客户端应用程序。



Deno上的第一个应用



让我们创建第一个应用程序。



为此,我们甚至不必编写代码,我们将使用URL在终端中运行它。



Deno下载程序,对其进行编译并运行:







当然,我不建议从Internet运行随机代码。在这种情况下,我们将从Deno的官方站点启动它,另外Deno还有一个沙箱,可以阻止程序执行我们明确不允许他们执行的操作。



这个程序非常简单,可以调用console.log()



console.log('Welcome to Deno ') //     ,    


如果在浏览器中打开https://deno.land/std/examples/welcome.ts,则会看到以下内容:很







奇怪,不是吗?您可能希望看到TypeScript文件,但得到了一个网页。关键是Deno网站服务器知道您正在使用浏览器,并为您提供了更加用户友好的页面。



加载使用相同的URL wget,例如,和得到text/plain代替text/html







当你重新启动程序,这要归功于缓存,不需要重新启动:一个强制重新启动可以进行







使用标志--reload:它







deno run有没有通过显示许多不同的功能deno --help为了查看它们,您应该运行deno run --help



flavio@mbp~> deno run --help
deno-run
Run a program given a filename or url to the module.

By default all programs are run in sandbox without access to disk, network or
ability to spawn subprocesses.
    deno run https://deno.land/std/examples/welcome.ts

Grant all permissions:
    deno run -A https://deno.land/std/http/file_server.ts

Grant permission to read from disk and listen to network:
    deno run --allow-read --allow-net https://deno.land/std/http/file_server.ts

Grant permission to read whitelisted files from disk:
    deno run --allow-read=/etc https://deno.land/std/http/file_server.ts

USAGE:
    deno run [OPTIONS] <SCRIPT_ARG>...

OPTIONS:
    -A, --allow-all
            Allow all permissions

        --allow-env
            Allow environment access

        --allow-hrtime
            Allow high resolution time measurement

        --allow-net=<allow-net>
            Allow network access

        --allow-plugin
            Allow loading plugins

        --allow-read=<allow-read>
            Allow file system read access

        --allow-run
            Allow running subprocesses

        --allow-write=<allow-write>
            Allow file system write access

        --cached-only
            Require that remote dependencies are already cached

        --cert <FILE>
            Load certificate authority from PEM encoded file

    -c, --config <FILE>
            Load tsconfig.json configuration file

    -h, --help
            Prints help information

        --importmap <FILE>
            UNSTABLE:
            Load import map file
            Docs: https://deno.land/std/manual.md#import-maps
            Specification: https://wicg.github.io/import-maps/
            Examples: https://github.com/WICG/import-maps#the-import-map
        --inspect=<HOST:PORT>
            activate inspector on host:port (default: 127.0.0.1:9229)

        --inspect-brk=<HOST:PORT>
            activate inspector on host:port and break at start of user script

        --lock <FILE>
            Check the specified lock file

        --lock-write
            Write lock file. Use with --lock.

    -L, --log-level <log-level>
            Set log level [possible values: debug, info]

        --no-remote
            Do not resolve remote modules

    -q, --quiet
            Suppress diagnostic output
            By default, subcommands print human-readable diagnostic messages to stderr.
            If the flag is set, restrict these messages to errors.
    -r, --reload=<CACHE_BLACKLIST>
            Reload source code cache (recompile TypeScript)
            --reload
                Reload everything
            --reload=https://deno.land/std
                Reload only standard modules
            --reload=https://deno.land/std/fs/utils.ts,https://deno.land/std/fmt/colors.ts
                Reloads specific modules
        --seed <NUMBER>
            Seed Math.random()

        --unstable
            Enable unstable APIs

        --v8-flags=<v8-flags>
            Set V8 command line options. For help: --v8-flags=--help


ARGS:
    <SCRIPT_ARG>...
            script args


代码示例



在Deno网站上还有其他示例,可以在这里找到



在撰写本文时,可以在指定的存储库中找到以下内容:



  • cat.ts -显示作为参数传递的文件的内容
  • catj.ts-做同样的事情,cat.ts但是首先对文件的内容执行一些操作
  • chat/ -聊天实现
  • colors.ts -使用模块样式化文本的示例
  • curl.ts-curl输出作为参数传递的URL内容的简单实现
  • echo_server.ts -TCP回显服务器
  • gist.ts -在gist.github.com中放置文件的程序
  • test.ts -测试程序
  • welcome.ts -我们启动的程序
  • xeval.ts-允许您运行从任何标准数据源获得的TypeScript。deno xeval已从官方队伍名单中删除


Deno上的第一个真实应用



让我们写一些代码。



我们启动的第一个应用程序deno run https://deno.land/std/examples/welcome.ts已经编写,因此您对Deno并没有学到任何新知识。



让我们从Deno网站上发布的标准示例开始:



import { serve } from 'https://deno.land/std/http/server.ts'
const s = serve({ port: 8000 })
console.log('http://localhost:8000/')
for await (const req of s) {
    req.respond({ body: 'Hello World\n' })
}


在这里,我们serve从模块导入函数http/server看到?我们不必安装它,它也不会像Node模块一样存储在我们的计算机上。这是快速安装Deno的原因之一。



在帮助下,https://deno.land/std/http/server.ts我们导入了模块的最新版本。可以使用@VERSION以下命令导入特定版本



import { serve } from 'https://deno.land/std@v0.42.0/http/server.ts'


这是什么功能serve



/**
 * Create a HTTP server
 *
 *     import { serve } from "https://deno.land/std/http/server.ts";
 *     const body = "Hello World\n";
 *     const s = serve({ port: 8000 });
 *     for await (const req of s) {
 *       req.respond({ body });
 *     }
 */
 export function serve(addr: string | HTTPOptions): Server {
  if (typeof addr === 'string') {
    const [hostname, port] = addr.split(':')
    addr = { hostname, port: Number(port) }
  }

  const listener = listen(addr)
  return new Server(listener)
}


接下来,我们调用该函数serve()并将其传递给带有property的对象port



然后,我们运行一个循环以响应服务器的每个请求:



for await (const req of s) {
  req.respond({ body: 'Hello World\n' })
}


请注意,我们使用关键字时await并未将代码包装在async函数中。



让我们在本地运行程序。我正在使用VS Code,但是您可以使用任何编辑器。



我建议从justjavac安装扩展(存在另一个具有类似名称的扩展,但已弃用,并且将来可能会消失):该







扩展提供了多个实用程序来帮助您编写Deno应用程序。



让我们创建一个文件app.ts并将其代码粘贴到其中:使用以下命令







运行deno run app.ts







Deno将加载程序所需的所有依赖项,但首先加载文件中导入的依赖项。https://deno.land/std/http/server.ts



文件具有多个依存关系:



import { encode } from '../encoding/utf8.ts'
import { BufReader, BufWriter } from '../io/bufio.ts'
import { assert } from '../testing/asserts.ts'
import { deferred, Deferred, MuxAsyncIterator } from '../async/mod.ts'
import {
    bodyReader,
    chunkedBodyReader,
    emptyReader,
    writeResponse,
    readRequest,
} from './_io.ts'
import Listener = Deno.Listener
import Conn = Deno.Conn
import Reader = Deno.Reader


这些依赖项将自动导入。



最后,我们有一个问题:







怎么回事?我们收到一个权限被拒绝的错误。



让我们谈谈沙盒。



沙盒



如前所述,Deno具有一个沙箱,可以防止程序执行未经其许可的程序。



这是什么意思?



正如Ryan在演讲中所说,有时候您想在浏览器之外运行JavaScript程序,而又不想让程序访问系统中的所有内容。或使用网络访问外界。



没有什么可以阻止Node.js应用程序从系统中获取SSH密钥或其他信息并将其发送到服务器的。这就是为什么我们通常只从受信任的源安装Node软件包。但是,我们如何知道我们正在使用的项目之一是否遭到黑客入侵?



Deno模仿浏览器使用的权限系统。在浏览器中运行的JavaScript代码无法对您的系统执行任何操作,除非您明确允许这样做。



回到Deno,如果程序需要网络访问,我们必须授予它访问权限。



这是使用标志完成的--allow-net



deno run --allow-net app.ts






服务器现在在端口8000上运行:







其他标志:



  • --allow-env -允许访问环境变量
  • --allow-hrtime -允许高分辨率测量
  • --allow-net=<allow-net> -允许访问网络
  • --allow-plugin -允许加载插件
  • --allow-read=<allow-read> -允许读取文件
  • --allow-run -允许启动子流程
  • --allow-write=<allow-write> -允许写入文件
  • --allow-all-授予所有权限(类似-A


的许可netread并且write可能是部分许可例如,我们可以允许位于某个目录中的只读文件:--allow-read=/dev



代码格式



我喜欢Go的一件事就是命令gofmt所有的Go代码看起来都一样。每个人都在使用它gofmt



JavaScript开发人员通常使用Prettier,deno fmt实际上也可以使用Prettier



假设您有一个格式错误的文件:







您开始deno fmt app.ts并且在缺少分号的情况下进行自动格式化:







标准库



尽管该项目年代久远,但Deno的标准库仍然相当广泛。



它包括以下内容:



  • archieve -归档实用程序
  • async -用于处理异步代码的实用程序
  • bytes -分割字节的辅助函数
  • datetime -解析日期/时间
  • encoding -不同格式的编码/解码
  • flags -解析命令行标志
  • fmt -形成和展示
  • fs -用于文件系统的应用程序界面
  • hash -加密库
  • http -HTTP服务器
  • io -输入/输出操作库
  • log -记录实用程序
  • mime -混合数据支持
  • node -具有Node的向后兼容层
  • path -使用路径
  • ws -网络插座


再举一个例子



让我们看看另一个官方示例- cat.ts



const filenames = Deno.args
for (const filename of filenames) {
    const file = await Deno.open(filename)
    await Deno.copy(file, Deno.stdout)
    file.close()
}


我们将filenames内容分配给一个变量Deno.args,该变量是一个包含使用命令行传递的参数的变量。



我们遍历它们,对于每一个,我们首先使用它们Deno.open()打开文件,然后Deno.copy()将内容复制到Deno.stdout最后,我们关闭文件。



如果您运行:



deno run https://deno.land/std/examples/cat.ts


该程序将加载并编译,但是没有任何反应,因为我们没有传递任何参数。



现在让我们尝试一下:



deno run https://deno.land/std/examples/cat.ts app.ts


我们







收到权限错误:默认情况下,Deno无权访问系统。让我们通过以下方式授予他这种许可--allow-read



deno run --allow-read=./ https://deno.land/std/examples/cat.ts app.ts






Deno是否存在Express / Hapi / Koa / *?



哦没问题。看一下以下项目:





示例:使用Oak创建REST API



我将使用Oak创建一个REST API。Oak之所以有趣,是因为它受到Kode(NOde.js流行的中间件)的启发,并具有类似的语法。



我们的API非常简单。



我们的服务器将在内存中存储狗的列表,它们的名称和年龄。



我们想要获得以下功能:



  • 将新狗添加到列表中
  • 获取所有狗的清单
  • 获取有关特定狗的信息
  • 从名单上删除一只狗
  • 更新狗的年龄


我们将使用Typescript编写代码,但是没有什么可以阻止您使用JavaScript编写代码-只是不指定数据类型。



我们创建一个文件app.ts



首先ApplicationRouter导入对象Oak



import { Application, Router } from 'https://deno.land/x/oak/mod.ts'


我们得到环境变量PORT和HOST:



const env = Deno.env.toObject()
const PORT = env.PORT || 4000
const HOST = env.HOST || '127.0.0.1'


默认情况下,我们的应用程序将在localhost:4000上运行。



创建一个Oak应用程序并启动它:



const router = new Router()

const app = new Application()

app.use(router.routes())
app.use(router.allowedMethods())

console.log(`Listening on port ${PORT}...`)

await app.listen(`${HOST}:${PORT}`)


该应用程序现在应该可以工作了。



我们检查:



deno run --allow-env --allow-net app.ts


Deno下载依赖项:







并开始侦听端口4000。



重新启动后,由于进行了缓存,将跳过安装步骤:







定义狗的接口,然后定义dog包含对象的数组Dog



interface Dog {
  name: string
  age: number
}

let dogs: Array<Dog> = [
  {
    name: 'Roger',
    age: 8,
  },
  {
    name: 'Syd',
    age: 7,
  },
]


让我们开始实现API。



一切就绪。让我们为访问指定端点的路由器添加一些功能:



const router = new Router()

router
    .get('/dogs', getDogs)
    .get('/dogs/:name', getDog)
    .post('/dogs', addDog)
    .put('/dogs/:name', updateDog)
    .delete('/dogs/:name', removeDog)


我们定义了以下内容:



  • GET /dogs
  • GET /dogs/:name
  • POST /dogs
  • PUT /dogs/:name
  • DELETE /dogs/:name




让我们一一实现这些路由。



让我们从开始GET /dogs,它返回所有狗的列表:



export const getDogs = ({ response }: { response: any }) => {
    response.body = dogs
}






以下是通过名称获取特定狗的方法:



export const getDog = ({
  params,
  response,
}: {
    params: {
        name: string
    },
    response: any
}) => {
    const dog = dogs.filter(dog => dog.name === params.name)
    if (dog.length) {
        response.status = 200
        response.body = dog[0]
        return
    }

    response.status = 400
    response.body = { msg: `Cannot find dog ${params.name}` }
}






这是将新狗添加到列表的方法:



export const addDog = async ({
    request,
    response,
}: {
    request: any
    response: any
}) => {
    const body = await request.body()
    const dog: Dog = await body.value
    dogs.push(dog)

    response.body = { msg: 'OK' }
    response.status = 200
}






这是更新狗的年龄的方法:



export const updateDog = async ({
    params,
    request,
    response,
}: {
    params: {
        name: string
    },
    request: any
    response: any
}) => {
    const temp = dogs.filter((existingDog) => existingDog.name === params.name)
    const body = await request.body()
    const { age }: { age: number } = await body.value

    if (temp.length) {
        temp[0].age = age
        response.status = 200
        response.body = { msg: 'OK' }
        return
    }

    response.status = 400
    response.body = { msg: `Cannot find dog ${params.name}` }
}






以下是从列表中删除狗的方法:



export const removeDog = ({
    params,
    response,
}: {
    params: {
        name: string
    },
    response: any
}) => {
    const lengthBefore = dogs.length
    dogs = dogs.filter((dog) => dog.name !== params.name)

    if (dogs.length === lengthBefore) {
        response.status = 400
        response.body = { msg: `Cannot find dog ${params.name}` }
        return
    }

    response.body = { msg: 'OK' }
    response.status = 200
}






完整的应用程序代码:



import { Application, Router } from 'https://deno.land/x/oak/mod.ts'

const env = Deno.env.toObject()
const PORT = env.PORT || 4000
const HOST = env.HOST || '127.0.0.1'

interface Dog {
  name: string
  age: number
}

let dogs: Array<Dog> = [
  {
    name: 'Roger',
    age: 8,
  },
  {
    name: 'Syd',
    age: 7,
  },
]

export const getDogs = ({ response }: { response: any }) => {
  response.body = dogs
}

export const getDog = ({
  params,
  response,
}: {
  params: {
    name: string
  },
  response: any
}) => {
  const dog = dogs.filter(dog => dog.name === params.name)
  if (dog.length) {
    response.status = 200
    response.body = dog[0]
    return
  }

  response.status = 400
  response.body = { msg: `Cannot find dog ${params.name}` }
}

export const addDog = async ({
  request,
  response,
}: {
  request: any
  response: any
}) => {
  const body = await request.body()
  const { name, age }: { name: string; age: number } = await body.value
  dogs.push({
    name: name,
    age: age,
  })

  response.body = { msg: 'OK' }
  response.status = 200
}

export const updateDog = async ({
  params,
  request,
  response,
}: {
  params: {
    name: string
  },
  request: any
  response: any
}) => {
  const temp = dogs.filter((existingDog) => existingDog.name === params.name)
  const body = await request.body()
  const { age }: { age: number } = await body.value

  if (temp.length) {
    temp[0].age = age
    response.status = 200
    response.body = { msg: 'OK' }
    return
  }

  response.status = 400
  response.body = { msg: `Cannot find dog ${params.name}` }
}

export const removeDog = ({
  params,
  response,
}: {
  params: {
    name: string
  },
  response: any
}) => {
  const lengthBefore = dogs.length
  dogs = dogs.filter(dog => dog.name !== params.name)

  if (dogs.length === lengthBefore) {
    response.status = 400
    response.body = { msg: `Cannot find dog ${params.name}` }
    return
  }

  response.body = { msg: 'OK' }
  response.status = 200
}

const router = new Router()
router
  .get('/dogs', getDogs)
  .get('/dogs/:name', getDog)
  .post('/dogs', addDog)
  .put('/dogs/:name', updateDog)
  .delete('/dogs/:name', removeDog)

const app = new Application()

app.use(router.routes())
app.use(router.allowedMethods())

console.log(`Listening on port ${PORT}...`)

await app.listen(`${HOST}:${PORT}`)


感谢您的关注。