关于 Deno 的依赖管理,有点糟糕,却也有药可救

2,439 阅读6分钟

Deno & Dep

自 Deno 1.0 发布以来,那些嘴上喊学不动的开发者们,身体却很诚实地上手了,至少敲下 brew install deno 以表示与之同在。当你跟着官方示例跑出第一个 Deno 程序时,你就会发现这只小恐龙野心十足,前途无量。

我们先来看下官方示例:

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

干净简练,单刀直入,仿佛干掉 sh 咫尺眼前,想想 JavaScript / TypeScript 统治世界的历史进程又进一步,还有点小兴奋呢。

但是这跟我写服务端有什么关系呢?于是官方给出了第二个实例:

import { serve } from "https://deno.land/std@0.54.0/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" });
}

嘿!齐活!看来 Deno 已经是个可以 run 的程序了,可以纳入生产使用考虑选项。然而动念的开始就是噩梦的开始......

进入主题

我们来看看 Deno 是如何引入模块依赖的:

import { serve } from "https://deno.land/std@0.54.0/http/server.ts";

如你所见,引入源、包名、版本号、模块名全部塞进了 URL 里,可真是个小机灵鬼呢!不愧是扬言要去中心化的 Deno,人狠话也多,这样一来只要我的脚本在线,我就可以 run anyway 了,比起在每个项目里放一个 node_modules 黑洞,Deno 全局统一 cache 远程引入的模块,项目结构干净了,磁盘空间也省不少,真香!

node_modules black hole

(注:其实在 npm 5.0 后黑洞本洞也没这么深了)

兴奋之余有没有觉得哪里怪怪的?没错,这种依赖引入方式像极了早年直接在 HTML 中引入 jQuery 的你,语义清晰却极为啰嗦,瞬间怀疑人生,难道 Deno 是在开历史倒车?

是,也不是,下文细聊。

当项目极小时,这种 absolute URL 的引入方式能帮助你快速开始一个项目,但当项目规模逐渐扩张后,目录结构开始变得复杂,开发协同人数也随之增加,这时候依赖管理就势在必行了。

那么 Deno 目前该怎么管理项目依赖呢?官方在手册中给出了方案 (Linking to external code),创建一个中心 deps.ts 文件,引入并导出你所需要的模块,然后再在其他文件中从 deps.ts 引入,代码如下:

// deps.ts
export {
  assert,
  assertEquals,
  assertStrContains,
} from "https://deno.land/std/testing/asserts.ts";
import { assertEquals, runTests, test } from "./deps.ts";

对此,我:??? 这就是你小恐龙管理依赖的方案?分明是不想让我们拥抱 Deno 开发项目!可以预期的是,如果采用此方式管理依赖,但凡有点规模的项目要维护好一份 deps.ts 都将是灾难般的存在,而且仔细想想这也不符合你所宣导的 ES Modules 语义,举个很实际的例子,如果我需要在引入代码中新增一个方法 createSign,我必须同时修改两个文件:

// deps.ts
export {
  assert,
  assertEquals,
  assertStrContains,
  createSign,
} from "https://deno.land/std/testing/asserts.ts";
import { assertEquals, runTests, test, createSign } from "./deps.ts";

这实在是太糟糕了!不仅繁琐易出错,且引入来源模块名极不清晰,甚至还需要使用 as 处理可能出现命名空间重名问题,想想就头大,实际开发体验绝不及 npm 使用如 import { assertEquals, createSign } from "asserts" 引入的一个零头,弃坑!本文终(误

别走!我们一起来看看 Deno 的依赖管理到底还有没有得救,毕竟 1.0 才发布两个星期,尚不完善情有可原。

开始诊断

首先来罗列一下 Deno 在模块管理上的设计优点

  • 支持直接从远端引入
  • 使用 ES Modules 规范
  • 面向文件引入
  • 没有 node_modules
  • 去中心化

再来罗列一下缺点

  • 绝对路径笨重

  • 手工管理繁琐易出错

  • 版本控制松散

  • 去中心化

    (你没看错,去中心化是优点也是缺点,文末我详谈对去中心化的看法)

对症下药

策略很简单,在不破坏优点的前提下修复缺陷。

1. 消除绝对路径

要消除臃肿的绝对路径,Deno 所支持的 Import maps 是一个有效的解决方案。创建一个 import_map.json 文件来描述模块名和引用源 URL 的对应关系,这样你就可以在需要时以模块名为目录引用了,如下:

// import_map.json
{
   "imports": {
      "http/": "https://deno.land/std/http/"
   }
}
import { serve } from "http/server.ts";

Nice! 这才是 ES Modules 该有的样子。

2. 工具化管理

你可以否定 npm 的霸权,但通过 npm or yarn 等 CLI 管理项目依赖的方式是需要拥护的,不然 maven, gradle, pod, pip 之流都该拖出去一并枪决,Go 也不会在杀到一半的时候峰回路转了。

而对于 Deno 而言,由于其内置了模块下载缓存和编译器,故只需要提供一个轻量级的 CLI 来管理依赖引用来源、版本、模块请求权限(有空会另开一篇文来专门介绍)并自动生成 Import map 即可。

3. 增加版本锁

Deno 的模块引入是灵活的,同时来了版本控制的松散,即便是 std (standard library) 也是默认支持从 master 直接引入,这构成了在生产环境使用 Deno 的最大阻力。个人认为,正确的版本控制不是在下载时检索版本然后用一个 .lock 文件锁死,而是在定义时锁死版本,避开任何埋坑的可能。

4. 去中心化与中心化

去中心化似乎是当今世界人心所向、大势所趋,而在开发领域,特别是依赖管理领域,一个中心化的公共托管仓库似乎还是有它存在的必要性和必然性,否则生态很难建立,也决定了一个开发语言的发展进程。大家可能已经发现在 Deno 官网有个 Third Party Modules 版块,贡献过 Deno 模块的朋友知道,目前些模块是由一个 database.json 文件维护,引入时由 deno.land/x 做中转,实际连接到对应 Github 仓库中的代码文件,那么模块映射文件是中心化的,Github 也是中心化的托管仓库,说好的去中心化呢?我想更多是在企业使用领域吧,构建一个私有仓库在 Deno 的模块设计下会变得更加简单便捷。

领取处方

综合以上思考,我给 Deno 写了一个依赖管理工具 dep ,同时提供了公共模块托管及 CDN,是对上述所有思考的具体实现。通过 dep cli 你可以轻松管理项目依赖或发布模块到 dep registry 壮大社区,比如:

添加依赖

dep add exec

# 添加 deno standard library 作为依赖
dep add std:path

# 添加 github repo 作为依赖
dep add github:acathur/store

移除依赖

dep remove exec

发布模块到 dep registry

dep publish

如果你的模块本身使用 dep 来管理依赖,dep 会在打包前将代码中所有 relative URL 替换成项目 Import map 中对应的源 URL,从而保证所有发布到 dep registry 的模块都能在无 Import map 的情况下随意引用和执行,也避免 Import maps 成为新的黑洞。

更多命令可以前往 Github 仓库 dep,或在安装 dep 后执行 dep help 查看。

写在最后

Dep 是一个基于 MIT License 的开源项目,欢迎大家参与到其中,report issues, pull requests 或是帮忙宣传,都会有所帮助。dep 旨在为 Deno 提供更好的依赖管理方案,推动 Deno 更快走向生产使用,如果你在依赖管理上有更好的想法也欢迎一起讨论、实践。

Deno Dep

Github: github.com/denodep/dep

来都来了 [手动眯眼笑],不去留个 star 么亲~