typescript实现map-Deno 是一个代码浏览器

本文最初发表于kitsonkelly.com网站,经原作者授权由InfoQ中文网翻译分享。

在 Ry 于 2018 年 5 月发布 Deno 原型后不久,我就开始从事这个项目。对于 Deno,人们最常见的问题之一是“包管理器去哪儿了?” 很多时候这不是问题,而只是抱怨。 他们会说“我知道 Deno 非常重视安全性,但是从互联网下载资源并不安全。” 或“如何在没有包管理器的情况下管理依赖项?”

我认为,我们应该改变我们的思维模式。 由于包管理器和集中式代码存储库如此普遍,许多人认为它们是理所当然的。 问题是,它们的受欢迎程度并不能证明它们是不可或缺的。 这些事情的发生是因为它们以某种方式解决问题,而你认为​​这是解决问题的唯一方法。 我认为这些想法都是不对的。

浏览

想象一下这样的场景:发布网站时,我们不是登录中央 Google 服务器,而是将网站上传到存储库。 然后,当有人想要查看我们的网站时,他们使用命令行工具,在我们本地计算机上的 browser.json 文件中添加一个条目,访问它并获取整个网站; 还获取我们本地网站目录的链接 对于所有其他网站,获取完成后将启动浏览器开始浏览。 这也太疯狂了吧? 那么为什么在运行代码时必须使用这些模式呢?

Deno CLI 的工作机制与浏览器非常相似,只不过您浏览的是代码而不是网页。 你在代码中导出一个 URL,Deno 会获取相应的代码并将其缓存在本地,就像浏览器一样。 与浏览器的另一个相似之处是您的代码在沙箱中运行,而沙箱对在其中运行的代码(无论其来源如何)的信任度为零。 您(调用代码的人)需要告诉上面的代码它可以从沙箱外部执行什么操作,不能执行什么操作。 最后,代码可以要求你执行某个操作,你可以选择授权或拒绝,就像浏览器一样。

HTTP 协议可以提供我们理解代码所需的一切,Deno 尝试充分利用这些合约,这样我们就不必发明新的合约。

代码检测

首先要认识到:就像浏览器一样,Deno CLI 不想对您运行的代码有任何意见。 它列出了如何获取代码以及如何在计算机上的沙箱中运行代码的规则。 我想运行时应该表达的意见就到此为止了。

typescript_typescript实现map_实现第二个百年奋斗目标

在 Node.js/npm 生态中,我们将本地计算机上的代码管理与集中的代码存储库相结合,帮助开发人员更轻松地发现代码。 我认为他们两个都有非常严重的缺陷。

在互联网的早期,我们尝试过像 npm 这样的代码发现模式。 您可以将您的网站添加到 Yahoo! 网站处于正确的类别,人们就会过来浏览; 他们可能会使用搜索功能,但这一切都是基于内容提供商的观点,并没有真正针对消费者的需求进行优化。 然后微软诞生了。 为什么微软是赢家? 因为它有效。 它使用这些方法通过将搜索词与满足需求的最相关的网页进行匹配来对网站进行索引; 索引过程考虑到多种激励因素,内容提供商提供的元数据只是其中之一。

虽然我们还没有将这个模型包含在 Deno 中,但它是一个可行的解决方案。 而且,我们使用微软是因为它为我们解决了问题,而不是因为有人说“你必须使用微软”; 除了微软还有其他可行的替代品。

我在 Facebook 上与 Laurie Voss 进行了一场辩论,我认为她非常了解 npm 生态系统。 他觉得 Deno 需要一个包管理器,这篇文章是我的观点的阐述。 但劳里提出了一个非常合理的观点。

GitHub 已成为开源代码之家,因为它运行良好并解决了很多问题; 它建立在 git 之上,git 是源代码版本控制工具的事实上的标准。 从 Deno CLI 的角度来看,源代码的来源应该没有技术限制。 我们需要更广泛的生态系统来创建和开发更多方式向社区展示 Deno 代码。 这些方式可能是我们创建 CLI 的人从未想到的创新方式。

可重现的

typescript_typescript实现map_实现第二个百年奋斗目标

这是 npm 生态系统中的一个问题。 由于严重依赖语义版本控制和复杂的依赖关系图(通常来自 Node.js/npm 生态系统),使构建可重复是一个挑战。 Yarn 引入了锁定文件的概念,npm 也紧随其后。

就我个人而言,我觉得这有点像推倒一堵墙并填满另一堵墙:生态系统中开发人员的行为导致了问题,然后开发了一个不健全的解决方案来解决它。 与生态系统打交道多年的人都知道,许多问题的解决方案是 rm -rf node_modules package-lock.json && npm install。

而 Deno 为这个问题提供了两种解决方案。 第一个是 Deno 缓存模块。 它可以将缓存签入源代码版本控制,并使用 --cached-only 标志不检索远程模块。 DENO_DIR 环境变量可用于指定缓存的位置,以获得更大的灵活性。

其次,Deno 支持锁定文件。 --lock lock.json --lock-write 将写出一个锁定文件,其中包含给定负载的所有依赖项的哈希值。 当使用 --lock lock.json 时,这将用于验证未来的运行。

还有一些命令可以管理可重复的重构。 deno 缓存将解析所提供模块的所有依赖项并填充 Deno 缓存。 deno 包可用于生成加载的单个文件“build”,所有依赖项都已解析并包含在该文件中,因此将来的 deno 运行命令仅需要此一个文件。

信任规则

我觉得这是另一个需要突破固有思维的领域。 无论出于何种原因,我们无条件信任集中存储库中的代码。 我们甚至没有考虑这些信任是否合理。 不仅如此,我们还相信其中的代码已经完全审查了所有依赖项并信任这些依赖项。 我们打开快速搜索框,输入 npm install some-random-package,并认为我们已经准备好了。 我认为人们被 npm 包的丰富生态系统宠坏了。

为了应对这些懒惰和傲慢的风险,我们在工具链中添加了安全监控软件,它会剖析我们的依赖关系和无数的代码,告诉我们什么可能是安全风险。 一些公司开发自己的私有存储库,其上托管的软件包可能会比公共存储库中的软件包经过更严格的初步审查。

这似乎是卧室里的小象。 最好的策略是我们不应该信任任何代码。 只要我们构建了这些认识,面对小象就会变得更容易。 然而,如果我们认为包管理器和集中存储库可以解决这个问题,甚至有助于缓解这个问题,那我们真的是在自欺欺人。 事实上,我认为他们的受欢迎程度已经提高了我们的警惕性。 “反正放在npm上,如果有什么隐患,肯定会有人拿下来。”

Deno 在这方面的工作还不够完美,但至少有了一个好的开始。 它的启动是零信任的,并提供相当细粒度的权限调整。 我个人不喜欢的一件事是 -A 标志,它基本上表示“好吧,然后允许所有权限”。 如果不弄清楚他们真正需要什么权限,那么遭受重创的开发人员很难抵制它的诱惑。

也很难收回这些权限,比如“这段代码可以做到这一点,但这里的另一段代码不能”,或者当它提示自己提升权限时弄清楚代码来自哪里 - 这些都是非常麻烦的操作。 希望我们能够找到一种易于使用的机制,并结合一些好的、有效的方法来在运行时解决这一挑战。

然而,最近我认为非常好的一个变化是 Deno 不再允许你降级导入。 如果单个内容是从 导出的,则此类内容只能从其他位置导出。 这与严格禁止降级传输的浏览器模型是一致的。 不过,我还是觉得,从长远来看,最好取消所有不通过的远程导出,就像服务Workers需要HTTPS一样。 我们拭目以待。

依赖管理

实现第二个百年奋斗目标_typescript_typescript实现map

我认为我们需要诚实地对待 npm 生态系统中的依赖关系。 说实话,这个生态是有问题的。 在这个生态系统中,这 5 行代码每周被下载 3000 万次:

但过去 9 年里所有浏览器都有这段代码,Node.js 根本不需要它——这样的生态系统并不正常。 在这种情况下,实际代码只有 132 字节,但打包后就变成了 3.4kb。 可运行代码仅占包大小的 3.8%。 “那也好啊!”

我认为目前这种情况的背后有几个原因。 重要的一点是我们走了相反的方向并使用了颠倒的模型。 问题在于,这种回归模型早已改变了我们创建网站的方式。 虽然没有中央存储库,但在构建网站时,我们会下载所有依赖代码并将它们烘焙到服务器上加载的内容中,然后用户将一堆代码下载到本地计算机。 一些证据表明,下载的代码中只有大约 10% 是所访问的站点或 Web 应用程序所独有的,其余的代码是我们在开发工作站上下载和打包的代码。 像 Snowpack 这样的解决方案试图解决这些由于方向错误而导致的问题。

另一个重要问题是我们的依赖项没有与我们的代码耦合。 我们将依赖项加载到 package.json 中,但是我们的代码实际上会使用这个依赖项吗? 另一方面,虽然我们的代码表明我们正在使用另一段代码中的某些内容,但它与前者的版本并不紧密耦合。 问题是,另一段代码直接影响我们正在编写的代码,因为它们确实具有依赖关系。

现在是 Deno 模型,我喜欢将其称为 Deps-in-JS,因为大家都在使用这些别名。 该模型将我们的外部依赖项显式声明为 URL,这意味着我们的代码与其他代码之间的依赖关系是简洁的,并且我们的代码和依赖项将紧密耦合。 如果您想查看依赖关系图,只需对本地或远程模块使用 deno info 即可:

$ deno info https://deno.land/x/oak/examples/server.ts
local: $deno/deps/https/deno.land/d355242ae8430f3116c34165bdae5c156dca21aeef521e45acb51fcd21c9f724
type: TypeScript
compiled: $deno/gen/https/deno.land/x/oak/examples/server.ts.js
map: $deno/gen/https/deno.land/x/oak/examples/server.ts.js.map
deps:
https://deno.land/x/oak/examples/server.ts
  ├── https://deno.land/std@0.53.0/fmt/colors.ts
  └─┬ https://deno.land/x/oak/mod.ts
    ├─┬ https://deno.land/x/oak/application.ts
    │ ├─┬ https://deno.land/x/oak/context.ts
    │ │ ├── https://deno.land/x/oak/cookies.ts
    │ │ ├─┬ https://deno.land/x/oak/httpError.ts
    │ │ │ └─┬ https://deno.land/x/oak/deps.ts
    │ │ │ ├── https://deno.land/std@0.53.0/hash/sha256.ts
    │ │ │ ├─┬ https://deno.land/std@0.53.0/http/server.ts
    │ │ │ │ ├── https://deno.land/std@0.53.0/encoding/utf8.ts
    │ │ │ │ ├─┬ https://deno.land/std@0.53.0/io/bufio.ts
    │ │ │ │ │ ├─┬ https://deno.land/std@0.53.0/io/util.ts
--snip--

Deno 不太关心代码的“版本”。 网址就是网址。 虽然 Deno 需要适当的媒体类型来知道如何处理代码,但有关提供哪些代码的所有“意见”都由 Web 服务器决定。 服务器可以强制执行其核心内容的语义版本控制,或者执行任意数量的 URL 到所需资源的“神奇”映射。 德诺并不关心这个。 例如,它实际上只是一个 URL 重定向服务器,它会重新绘制 URL 以在重定向的 URL 中包含 git commit-ish 引用。 所以 @v4.0.0/mod.ts 就变成了,这里 GitHub 扮演了一个很好的版本化模块的角色。 当然,将“版本化”远程 URL 分散在整个代码库中没有多大意义,所以不要这样做。 尽管依赖项只是代码,但最好的部分是您可以按照您想要的任何形式构建它们。 常见的约定是使用 deps.ts,它将重新导入您可能需要的任何依赖项。 看一下 Oak 服务器示例:

// Copyright 2018-2020 the oak authors. All rights reserved. MIT license.
// This file contains the external dependencies that oak depends upon
// `std` dependencies
export { HmacSha256 } from "https://deno.land/std@0.51.0/hash/sha256.ts";
export {
  Response,
  serve,
  Server,
  ServerRequest,
  serveTLS,
} from "https://deno.land/std@0.51.0/http/server.ts";
export {
  Status,
  STATUS_TEXT,
} from "https://deno.land/std@0.51.0/http/http_status.ts";
export {
  Cookies,
  Cookie,
  setCookie,
  getCookies,
  delCookie,
} from "https://deno.land/std@0.51.0/http/cookie.ts";
export {
  basename,
  extname,
  join,
  isAbsolute,
  normalize,
  parse,
  resolve,
  sep,
} from "https://deno.land/std@0.51.0/path/mod.ts";
export { assert } from "https://deno.land/std@0.51.0/testing/asserts.ts";
// 3rd party dependencies
export {
  contentType,
  lookup,
} from "https://deno.land/x/media_types@v2.3.1/mod.ts";

我创建了 Oak 服务器并维护了大约一年半的时间,在此期间我经历了大约 40 个 Deno 和 Deno std 库的版本; 其中,我还将media_types从内部连接到Oak,移出std库typescript实现map,并让它从std库中“Eject”独立存在。 但我从未想过“嘿,我需要一个包管理器来帮忙”。 TypeScript 的一大优点是您可以完全验证您的代码是否与其他代码兼容。 如果您的依赖项是为 Deno 编写的“原始”TypeScript 是最好的,但假设您想利用 TypeScript 对 JavaScript 的预处理,同时仍然安全地使用该远程代码。 Deno 支持几种不同的方法来执行此操作,但最无缝的是对 X-TypeScript-Types 标头的支持。 此焦距向 Deno 指示键入的文件所在的位置,并且可以在对您依赖的 JavaScript 文件进行类型检测时使用。 Pika CDN 支持此功能。 CDN 上具有与其关联的类型的任何包都将充当该上下文,Deno 也会选择该类型并在检测文件类型时使用它。

总而言之,您可能始终需要将远程(或本地)依赖项“重新映射”到代码中表达的内容。 在这些情况下,可以使用导入映射的不稳定实现。 这是 W3C 提案规范。 它允许提供一个映射,将代码中的特定依赖项映射到另一个源,无论是本地文件还是远程模块。

我们在 Deno 中实现它很长时间了,因为我们真的希望它能被广泛采用。 遗憾的是,这只是 Chrome 中的一个实验,尚未得到更广泛的采用。 因此我们决定将其放在 Deno 1.0 中的 --unstable 标志旁边。 我个人认为,这仍然很有可能是一个死胡同,应该避免。

但是,但是,但是……

我想仍然会有很多人不同意 Deno 的模型。 我认为 Deno 试图采取的策略(我完全同意)是在出现实际问题时处理它们。 我看到的很多反对意见都来自 Deno 新手,他们没有参与过 Deno 项目,也没有尝试过了解不同的可能性。

话虽这么说,如果我们都遇到同样的问题并且迫切需要对 Deno CLI 进行个别更改,我相信 Deno 会这么做。 但许多所谓的问题根本不存在typescript实现map,或者有其他解决方案不需要您的运行时做那么多事情,或者与外部程序结合来管理代码。

所以,我希望你们在没有包管理器或集中式包存储库的情况下尝试一下,看看结果如何。 你可能永远不会回头!

关注我并转发本文,私信InfoQ“获取资料”,即可免费获得价值4999元的InfoQ迷你图书资料包!