作者 | Kitson Kelly
譯者 | 王強(qiáng)
策劃 | 小智
2018 年 5 月,Ry 公開 Deno 的原型后不久我就開始參與這個(gè)項(xiàng)目了。對(duì)于 Deno,人們最常問的一個(gè)問題是“包管理器跑哪兒去了?”很多時(shí)候這都算不上問題,而只是一種吐槽。他們會(huì)說(shuō)“我知道 Deno 很重視安全性,可是從互聯(lián)網(wǎng)下載資源是不安全的。”或“沒包管理器我該怎么管理依賴項(xiàng)啊?”
本文最初發(fā)布于 網(wǎng)站,經(jīng)原作者授權(quán)由 InfoQ 中文站翻譯并分享。
在我看來(lái),我們應(yīng)該改變自己的思維模式。因?yàn)榘芾砥骱椭行幕a庫(kù)隨處可見,所以很多人把它們當(dāng)成了理所當(dāng)然的需求。問題在于它們的流行并不是它們不可或缺的證明。這些事物之所以會(huì)出現(xiàn),是因?yàn)樗鼈円阅撤N方式解決了問題,可大家就覺得這是解決問題的唯一途徑了。我認(rèn)為這種看法是不對(duì)的。
想象一下這樣的場(chǎng)景:在發(fā)布網(wǎng)站時(shí),我們不是登錄到一個(gè)谷歌中心服務(wù)器上,而是將我們的網(wǎng)站上傳到一個(gè)存儲(chǔ)庫(kù)中。然后當(dāng)有人想查看我們的網(wǎng)站時(shí),他們要使用命令行工具,在我們本地計(jì)算機(jī)上的 browser.json 文件中添加一個(gè)條目,再訪問并獲取整個(gè)網(wǎng)站;另外還要獲取鏈接到我們本地 目錄的其他所有網(wǎng)站,獲取完畢后才能啟動(dòng)瀏覽器開始瀏覽。這也太瘋狂了不是嗎?那么為什么跑代碼的時(shí)候就非得用這種模式呢?
Deno CLI 的工作機(jī)制和瀏覽器很像,只不過(guò)瀏覽的是代碼而非網(wǎng)頁(yè)。你在代碼中導(dǎo)入一個(gè) URL,Deno 將獲取對(duì)應(yīng)的代碼并將其緩存在本地,就像瀏覽器那樣。另一個(gè)和瀏覽器類似的地方是,你的代碼運(yùn)行在沙箱中,沙箱對(duì)其中運(yùn)行的代碼(不管其來(lái)源如何)的信任度為零。你(調(diào)用代碼的人)需要從沙箱外部告訴里面的代碼可以做什么,不能做什么。最后,代碼可以要求你執(zhí)行操作,你可以選擇授權(quán)或拒絕,這也和瀏覽器是一樣的。
我們了解代碼所需的一切信息都可以由 HTTP 協(xié)議提供,并且 Deno 試著充分利用這種協(xié)議,這樣就用不著發(fā)明新的了。
代碼發(fā)現(xiàn)
首先要認(rèn)識(shí)一點(diǎn):就像瀏覽器一樣,Deno CLI 不想對(duì)你運(yùn)行的代碼有任何意見(opinion)。它列出了如何獲取代碼,以及如何在計(jì)算機(jī)中使用沙箱運(yùn)行代碼的規(guī)則。我認(rèn)為運(yùn)行時(shí)應(yīng)該表達(dá)的意見就到此為止才對(duì)。
在 Node.js/npm 生態(tài)系統(tǒng)中,我們將本地計(jì)算機(jī)上的代碼管理與中心化代碼存儲(chǔ)庫(kù)結(jié)合在了一起,從而幫助開發(fā)人員更方便地發(fā)現(xiàn)代碼。我認(rèn)為它們兩者都有非常嚴(yán)重的缺陷。
在互聯(lián)網(wǎng)的早期,我們就嘗試過(guò) npm 這一類的代碼發(fā)現(xiàn)模式。你可以將你的網(wǎng)站添加到 Yahoo! 網(wǎng)站正確的分類下,然后人們就會(huì)來(lái)瀏覽;他們可能也會(huì)使用搜索功能,但所有這些都是基于內(nèi)容提供者的觀點(diǎn)來(lái)構(gòu)建的,卻并沒有真正針對(duì)消費(fèi)者的需求而優(yōu)化。后來(lái)谷歌誕生了。為什么谷歌成為了贏家?因?yàn)樗芎糜谩K鼘⑺阉髟~與滿足需求的最相關(guān)網(wǎng)頁(yè)匹配起來(lái),用這種方式來(lái)索引網(wǎng)站;索引過(guò)程考慮了多種因素,內(nèi)容提供商提供的元數(shù)據(jù)只是其中之一。
雖然我們還沒有把這套模型納入 Deno,但它是可行的方案。此外,我們之所以使用谷歌是因?yàn)樗鼮槲覀兘鉀Q了問題,而不是有人說(shuō)“你必須使用谷歌”;而且谷歌還有其他可行的替代方案。
我在推特上與 Laurie Voss 進(jìn)行了一場(chǎng)辯論,我覺得他非常了解 npm 生態(tài)系統(tǒng)。他認(rèn)為 Deno 需要包管理器,而本文是我的觀點(diǎn)的詳盡闡述。但是 Laurie 提出了一個(gè)非常合理的觀點(diǎn)。
GitHub 已成為開源代碼的家園,因?yàn)樗浅:糜貌⒔鉀Q了很多問題;它是基于源代碼版本控制工具的事實(shí)標(biāo)準(zhǔn) git 構(gòu)建的。從 Deno CLI 的角度來(lái)看,源代碼的來(lái)源應(yīng)該沒有技術(shù)上的限制,我們需要更加廣闊的生態(tài)系統(tǒng),以創(chuàng)造并發(fā)展更多途徑來(lái)向社區(qū)展示 Deno 中的代碼,這些途徑可能是我們這些創(chuàng)建 CLI 的人們從未想過(guò)的創(chuàng)新形式。
可重復(fù)構(gòu)建
在 npm 生態(tài)系統(tǒng)中這是一個(gè)問題。由于嚴(yán)重依賴語(yǔ)義版本控制,并且復(fù)雜的依賴圖往往來(lái)自 Node.js/npm 生態(tài)系統(tǒng),因此想要讓構(gòu)建可重復(fù)就成了一個(gè)挑戰(zhàn)。Yarn 引入了鎖定文件的概念,npm 也跟上了腳步。
我個(gè)人感覺這有點(diǎn)像是在拆東墻,補(bǔ)西墻:生態(tài)系統(tǒng)中開發(fā)人員的行為造成了一個(gè)問題,然后為了解決它又開發(fā)出了一個(gè)不完善的解決方案。長(zhǎng)年和生態(tài)系統(tǒng)打交道的人都知道,很多問題的解決辦法就是 rm -rf package-lock.json && npm install。
而 Deno 為這個(gè)問題提供了兩種解決方案。首先是 Deno 緩存模塊。它可以將緩存 check in 到源代碼版本控制中,使用 --cached-only 標(biāo)志就不會(huì)檢索遠(yuǎn)程模塊。 環(huán)境變量可用于指定高速緩存的位置,以提供更大的靈活性。
其次,Deno 支持鎖定文件。--lock lock.json --lock-write 將使用給定負(fù)載所有依賴項(xiàng)的哈希值寫出一個(gè)鎖定文件。當(dāng)使用 --lock lock.json 時(shí),這將用于驗(yàn)證將來(lái)的運(yùn)行。
還有一些命令可以管理可重復(fù)的構(gòu)建。deno cache 將解析所提供模塊的所有依賴項(xiàng),并填充 Deno 緩存。deno bundle 可用于生成負(fù)載的單個(gè)文件“構(gòu)建”,所有依賴項(xiàng)都已解析并包含在該文件中,因此將來(lái)的 deno run 命令只需要這一個(gè)文件即可。
信任規(guī)則
我認(rèn)為這是另一個(gè)需要打破固有思維的領(lǐng)域。無(wú)論出于何種原因,我們都無(wú)條件地信任中心化存儲(chǔ)庫(kù)中的代碼。我們甚至都沒有考慮過(guò)這種信任是不是合理。不僅如此,我們相信其中的代碼已完全審查了所有依賴項(xiàng),進(jìn)而信任這些依賴項(xiàng)。我們打開快速搜索框,輸入 npm install some-random-package,然后就覺得萬(wàn)事大吉了。我認(rèn)為豐富的 npm 軟件包生態(tài)系統(tǒng)把人們慣壞了。
為了應(yīng)對(duì)這種松懈和自滿帶來(lái)的風(fēng)險(xiǎn),我們?cè)诠ぞ哝溨屑尤肓税踩O(jiān)視軟件,用來(lái)分析我們的依賴項(xiàng)和數(shù)不清的代碼,告訴我們哪些代碼可能存在安全隱患。一些公司會(huì)開發(fā)自己的私有存儲(chǔ)庫(kù),上面托管的軟件包接受的審核可能比那個(gè)公共存儲(chǔ)庫(kù)中的更嚴(yán)格一些。
這就好像是房間中的那頭大象。最佳策略是我們不應(yīng)該信任任何代碼。只要我們建立了這種認(rèn)識(shí),那么正視那頭大象就會(huì)變得容易一些。但是,如果我們認(rèn)為包管理器和中心化存儲(chǔ)庫(kù)可以解決這個(gè)問題,或者哪怕是幫助緩解了這個(gè)問題,其實(shí)我們就是在自欺欺人。實(shí)際上,我認(rèn)為它們的流行讓我們的警惕性下降了。“反正它是放在 npm 上,如果它有什么隱患,肯定會(huì)有人把它撤下去的。”
Deno 在這方面的工作還不盡如人意,但它起碼有一個(gè)好的開始。它在啟動(dòng)時(shí)是零信任的,并提供了相當(dāng)精細(xì)的權(quán)限調(diào)整。我個(gè)人不喜歡的一件事是 -A 標(biāo)志,它基本上是在說(shuō)“好的,那就允許一切權(quán)限”。焦頭爛額的開發(fā)人員很難經(jīng)得住它的誘惑,而不會(huì)去弄清楚自己真正需要的是哪些權(quán)限。
收回這些授權(quán)也是很困難的事情,要指明“這段代碼可以做這件事,但這里的另一段代碼就不行”,或者當(dāng)代碼提示自己要提升權(quán)限時(shí),搞清楚這些代碼是從哪兒來(lái)的——這些都是很麻煩的操作。希望我們能找到一種易用的機(jī)制,并結(jié)合一些在運(yùn)行時(shí)好用且高效的方法來(lái)解決這些挑戰(zhàn)。
不過(guò),最近的一個(gè)變化在我看來(lái)是很不錯(cuò)的,那就是 Deno 不再允許你降級(jí) imports。如果從 導(dǎo)入了某些內(nèi)容,那么這些內(nèi)容只能從其他 位置導(dǎo)入。這和禁止降級(jí)傳輸?shù)臑g覽器模型是一致的。不過(guò)我還是認(rèn)為從長(zhǎng)遠(yuǎn)來(lái)看,最好取消所有未通過(guò) 進(jìn)行的遠(yuǎn)程導(dǎo)入,就像服務(wù) Workers 需要 HTTPS 一樣。對(duì)此我們將拭目以待。
依賴管理
我認(rèn)為我們需要坦率地談?wù)?npm 生態(tài)系統(tǒng)中的依賴項(xiàng)。老實(shí)說(shuō),這個(gè)生態(tài)是有問題的。在這個(gè)生態(tài)系統(tǒng)中,這區(qū)區(qū) 5 行代碼每周會(huì)下載 3 千萬(wàn)次:
可是過(guò)去 9 年來(lái)所有瀏覽器都有這些代碼,Node.js 根本用不著它們——這樣的生態(tài)系統(tǒng)是不正常的。在這個(gè)例子中,實(shí)際的代碼只有 132 個(gè)字節(jié),但打完包就變成了 3.4kb。可運(yùn)行代碼只占包大小的 3.8%。“這樣也行!”
我覺得這種現(xiàn)狀背后有幾點(diǎn)成因。其中很重要的一點(diǎn)是我們走反了方向,用的是顛倒過(guò)來(lái)的模型。問題在于,這種倒退的模式已經(jīng)改變了我們創(chuàng)建網(wǎng)站的方式。盡管沒有中央存儲(chǔ)庫(kù),但是在構(gòu)建網(wǎng)站時(shí),我們將下載所有依賴的代碼,并將它們烘焙到服務(wù)器上加載的內(nèi)容中,然后用戶將一堆代碼下載到他們的本地計(jì)算機(jī)上。一些證據(jù)表明,所下載的代碼中只有大約 10%是所訪問的站點(diǎn)或 Web 應(yīng)用程序獨(dú)有的,剩下的那些是我們下載到開發(fā)工作站并打包起來(lái)的代碼。 等解決方案就想要解決這種因?yàn)樽咤e(cuò)方向而導(dǎo)致的問題。
另一個(gè)重要的問題是我們的依賴項(xiàng)沒有與我們的代碼耦合起來(lái)。我們將依賴項(xiàng)放入 package.json,但我們的代碼是不是會(huì)真的使用這些依賴項(xiàng)呢?另一方面,雖然我們的代碼表示我們正在使用其他一段代碼中的內(nèi)容,但它與后者的版本之間并沒有緊密的耦合關(guān)系。問題是另外這段代碼會(huì)直接影響我們正在編寫的代碼,因?yàn)樗鼈冎g的確存在依賴關(guān)系。
下面就輪到 Deno 模型登場(chǎng)了,我喜歡稱其為 Deps-in-JS,因?yàn)榇蠹叶荚谟眠@種叫法。這種模型將我們的外部依賴項(xiàng)顯式聲明為 URL,意味著代碼與其他代碼之間的依賴關(guān)系簡(jiǎn)潔明了,并且我們的代碼和依賴項(xiàng)會(huì)緊密地耦合在一起。如果要查看依賴圖,只需對(duì)一個(gè)本地或遠(yuǎn)程模塊使用 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 沒那么在乎代碼的“版本”。URL 就是 URL。盡管 Deno 需要適當(dāng)?shù)拿襟w類型以了解如何處理代碼,但關(guān)于要提供哪些代碼的所有“意見”都留給了 Web 服務(wù)器來(lái)決定。服務(wù)器可以對(duì)其核心內(nèi)容實(shí)施語(yǔ)義版本控制,或者對(duì) URL 到所需資源進(jìn)行任何形式的“魔術(shù)”映射。Deno 并不在乎這些。例如, 實(shí)際上只是一個(gè) URL 重定向服務(wù)器,它會(huì)重寫 URL,以在重定向的 URL 中包含一個(gè) git commit-ish 引用。于是 @v4.0.0/mod.ts 變成了 ,這里 GitHub 扮演了一個(gè)不錯(cuò)的版本化()模塊的角色。當(dāng)然,在整個(gè)代碼庫(kù)中散布“版本化”的遠(yuǎn)程 URL 沒有多大意義,所以不要這樣做。盡管依賴項(xiàng)只是代碼而已,但最妙的是你可以按照自己想要的任何方式來(lái)構(gòu)造它們。常見的約定是使用 deps.ts,它將重新導(dǎo)出你可能需要的所有依賴項(xiàng)。看一看 oak 服務(wù)器的例子:
// 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";
我創(chuàng)建了 Oak 服務(wù)器,維護(hù)了大約一年半,期間經(jīng)歷了 Deno 和 Deno std 庫(kù)的大約 40 個(gè)發(fā)行版;其中我還將 從內(nèi)部移動(dòng)到了 Oak,移出 std 庫(kù),讓它從 std 庫(kù)中“彈出”來(lái)獨(dú)立存在。但我從來(lái)沒有想過(guò)“嘿,我需要一個(gè)包管理器來(lái)幫忙”。 的好處之一是,你可以全面驗(yàn)證代碼與其他代碼的兼容性。如果你的依賴項(xiàng)是為 Deno 編寫的“原始”,那就最好不過(guò)了,但是,假設(shè)你希望一邊利用 對(duì) 的預(yù)處理,另一邊還想安全地使用該遠(yuǎn)程代碼。Deno 支持幾種不同的方法來(lái)實(shí)現(xiàn)這一點(diǎn),但最無(wú)縫的是對(duì) X--Types 標(biāo)頭的支持。此標(biāo)頭向 Deno 指示類型文件所在的位置,可在類型檢查你所依賴的 文件時(shí)使用。Pika CDN 支持此功能。CDN 上任何具有與之相關(guān)聯(lián)的類型的軟件包都將充當(dāng)該標(biāo)頭,而 Deno 也將獲取這些類型,并在檢查文件類型時(shí)使用它。
綜上所述,你仍然可能需要將遠(yuǎn)程(或本地)依賴項(xiàng)“重新映射”到代碼中表達(dá)的內(nèi)容上。在這種情況下,可以使用 import-maps 的一個(gè)不穩(wěn)定實(shí)現(xiàn)。這是一個(gè) W3C 提案規(guī)范。它允許提供一個(gè)映射,該映射會(huì)將代碼中的特定依賴項(xiàng)映射到另一個(gè)源,可以是本地文件抑或遠(yuǎn)程模塊。
我們?cè)?Deno 中實(shí)現(xiàn)了它很長(zhǎng)一段時(shí)間,因?yàn)槲覀冋娴南M鼤?huì)被廣泛采用。遺憾的是,這只是 Chrome 的一項(xiàng)實(shí)驗(yàn),尚未得到更廣泛的采用。于是我們決定在 Deno 1.0 中將它放在 -- 標(biāo)志后面。我個(gè)人認(rèn)為它還是很有可能走向死胡同,應(yīng)該避免使用它。
但是,但是,但是...
我想還是會(huì)有很多人對(duì) Deno 的模型提出異議。我認(rèn)為 Deno 嘗試采取的策略(我非常贊同)是在出現(xiàn)實(shí)際問題時(shí)再做處理。我聽到的很多反對(duì)意見來(lái)自剛?cè)腴T Deno 的新手,他們從未與 Deno 項(xiàng)目合作過(guò),也沒有試圖理解不同的可能性。
話雖如此,如果我們都遇到了同一個(gè)問題,并且迫切需要在 Deno CLI 中進(jìn)行某些更改,我相信 Deno 會(huì)去做的。但是很多所謂的問題根本就不存在,或者還有其他解決方法,用不著你的運(yùn)行時(shí)操心那么多事情,或與外部程序耦合來(lái)管理代碼。
因此,我希望大家嘗試一下不用包管理器或中心化的包存儲(chǔ)庫(kù),看看這樣下來(lái)會(huì)有怎樣的結(jié)果。你可能再也不會(huì)回頭了!
*請(qǐng)認(rèn)真填寫需求信息,我們會(huì)在24小時(shí)內(nèi)與您取得聯(lián)系。