此篇文章實際上就是《 前端開發的瓶頸與將來》的番外篇。主要想從實用的角度給你們介紹下 Deno 在咱們項目中的應用案例,現階段咱們只關注應用層面的問題,不涉及過於底層的知識。
咱們從它的官方介紹裏面能夠看出來加粗的幾個單詞:secure, JavaScript, TypeScript。簡單譯過來就是:前端
一個 JavaScript 和 TypeScript 的安全運行時
那麼問題來了,啥叫運行時(runtime)?能夠簡單的理解成能夠執行代碼的一個東西。那麼 Deno 就是一個能夠執行 JavaScript 和 TypeScript 的東西,瀏覽器就是一個只能執行 JavaScript 的運行時。node
Mac/Linux 下命令行執行:webpack
curl -fsSL https://deno.land/x/install/install.sh | sh
也能夠去 Deno 的官方代碼倉庫下載對應平臺的源(可執行)文件,而後將它放到你的環境變量裏面直接執行。若是安裝成功,在命令行裏面輸入:deno --help
會有以下輸出:git
➜ ~ deno --help deno 1.3.0 A secure JavaScript and TypeScript runtime Docs: https://deno.land/manual Modules: https://deno.land/std/ https://deno.land/x/ Bugs: https://github.com/denoland/deno/issues ...
之後若是想升級可使用內置命令 deno upgrade
來自動升級 Deno 版本,至關方便了。es6
Deno 內置了豐富的命令,用來知足咱們平常的需求。咱們簡單介紹幾個:github
直接執行 JS/TS 代碼。代碼能夠是本地的,也能夠是網絡上任意的可訪問地址(返回JS或者TS)。咱們使用官方的示例來看看效果如何:web
deno run https://deno.land/std/examples/welcome.ts
若是執行成功就會返回下面的信息:typescript
➜ ~ deno run https://deno.land/std/examples/welcome.ts Download https://deno.land/std/examples/welcome.ts Warning Implicitly using latest version (0.65.0) for https://deno.land/std/examples/welcome.ts Download https://deno.land/std@0.65.0/examples/welcome.ts Check https://deno.land/std@0.65.0/examples/welcome.ts Welcome to Deno 🦕
能夠看到這段命令作了兩個事情:1. 下載遠程文件 2. 執行裏面的代碼。咱們能夠經過命令查看這個遠程文件裏面內容究竟是啥:npm
➜ ~ curl https://deno.land/std@0.65.0/examples/welcome.ts console.log("Welcome to Deno 🦕");
不過須要注意的是上面的遠程文件裏面沒有 顯示的 指定版本號,實際下載 std 中的依賴的時候會默認使用最新版,即:std@0.65.0
,咱們可使用 curl 命令查看到源文件是 302
重定向到帶版本號的地址的:編程
➜ ~ curl -i https://deno.land/std/examples/welcome.ts HTTP/2 302 date: Fri, 14 Aug 2020 01:53:06 GMT content-length: 0 set-cookie: __cfduid=d3e9dfbd32731defde31eba271f19933b1597369985; expires=Sun, 13-Sep-20 01:53:05 GMT; path=/; domain=.deno.land; HttpOnly; SameSite=Lax; Secure location: /std@0.65.0/examples/welcome.ts x-deno-warning: Implicitly using latest version (0.65.0) for https://deno.land/std/examples/welcome.ts cf-request-id: 048c44c2dc000019dd710cc200000001 expect-ct: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct" server: cloudflare cf-ray: 5c270a4afd5719dd-SIN
header 頭中的 location 就是實際文件的下載地址:
location: /std@0.65.0/examples/welcome.ts
這就涉及到一個問題:實際使用的時候到底應不該該手動添加版本號?通常來講若是是生產環境的項目引用必定要是帶版本號的,像這種示例代碼裏面就不須要了。
上面說到 Deno 也能夠執行本地的,那咱們也試一試,寫個本地文件,而後 運行它:
➜ ~ echo 'console.log("Welcome to Deno <from local>");' > welecome_local.ts ➜ ~ ls welecome_local.ts welecome_local.ts ➜ ~ deno run welecome_local.ts Check file:///Users/zhouqili/welecome_local.ts Welcome to Deno <from local>
能夠看到輸出了咱們想要的結果。
這個例子太簡單了,再來個複雜點的吧,用 Deno 實現一個 Http 服務器。咱們使用官方示例中的代碼:
import { serve } from "https://deno.land/std@0.65.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" }); }
保存爲 test_serve.ts,而後使用 deno run
運行它,你會發現有報錯信息:
➜ ~ deno run test_serve.ts Download https://deno.land/std@0.65.0/http/server.ts Download https://deno.land/std@0.65.0/encoding/utf8.ts Download https://deno.land/std@0.65.0/io/bufio.ts Download https://deno.land/std@0.65.0/_util/assert.ts Download https://deno.land/std@0.65.0/async/mod.ts Download https://deno.land/std@0.65.0/http/_io.ts Download https://deno.land/std@0.65.0/async/deferred.ts Download https://deno.land/std@0.65.0/async/delay.ts Download https://deno.land/std@0.65.0/async/mux_async_iterator.ts Download https://deno.land/std@0.65.0/async/pool.ts Download https://deno.land/std@0.65.0/textproto/mod.ts Download https://deno.land/std@0.65.0/http/http_status.ts Download https://deno.land/std@0.65.0/bytes/mod.ts Check file:///Users/zhouqili/test_serve.ts error: Uncaught PermissionDenied: network access to "0.0.0.0:8000", run again with the --allow-net flag at unwrapResponse (rt/10_dispatch_json.js:24:13) at sendSync (rt/10_dispatch_json.js:51:12) at opListen (rt/30_net.js:33:12) at Object.listen (rt/30_net.js:204:17) at serve (server.ts:287:25) at test_serve.ts:2:11
PermissionDenied
意思是你沒有網絡訪問的權限,可使用 --allow-net
的標識來容許網絡訪問。這就是文章開頭特性裏面提到的默認安全。
默認安全就是說被 Deno 執行的代碼會默認被放進一個沙箱中執行,代碼使用到的 API 接口都受制於 Deno 的宿主環境,Deno 固然是有網絡訪問、文件系統等能力的。可是這些系統級別的訪問須要 deno 命令的 執行者 受權。
這個權限控制不少人以爲不必,由於當咱們運行代碼時提示了受限,咱們確定手動添加上容許而後再執行嘛。可是區別是 Deno 把這個受權交給了執行者,好處就是若是執行的代碼是第三方的,那麼執行者就能夠主動拒絕一些危險性很高的操做。
好比咱們安裝一些命令行工具,而通常命令行工具都是不須要網絡的,咱們就能夠不給它網絡訪問的權限。從而避免了程序偷偷地上傳/下載文件。
執行一段 JS/TS 字符串代碼。這個和 JavaScript 中的 eval 函數有點相似。
➜ ~ deno eval "console.log('hello from eval')" hello from eval
安裝一個 deno 腳本,一般用來安裝一個命令行工具。舉個例子,在以前的 Deno 版本中有一個命令特別好用:deno xeval
能夠按行執行 eval 命令,相似於 Linux 中的 xargs
命令。後來這個內置命令被移除了,可是 deno 的開發人員編寫了一個 deno 腳本,咱們能夠經過 install 命令安裝它。
➜ ~ deno install -n xeval https://deno.land/std@0.65.0/examples/xeval.ts Download https://deno.land/std@0.65.0/examples/xeval.ts Download https://deno.land/std@0.65.0/flags/mod.ts Download https://deno.land/std@0.65.0/io/bufio.ts Download https://deno.land/std@0.65.0/bytes/mod.ts Download https://deno.land/std@0.65.0/_util/assert.ts Check https://deno.land/std@0.65.0/examples/xeval.ts ✅ Successfully installed xeval /Users/zhouqili/.deno/bin/xeval ➜ ~ xeval xeval Run a script for each new-line or otherwise delimited chunk of standard input. Print all the usernames in /etc/passwd: cat /etc/passwd | deno run -A https://deno.land/std/examples/xeval.ts "a = $.split(':'); if (a) console.log(a[0])" A complicated way to print the current git branch: git branch | deno run -A https://deno.land/std/examples/xeval.ts -I 'line' "if (line.startsWith('*')) console.log(line.slice(2))" Demonstrates breaking the input up by space delimiter instead of by lines: cat LICENSE | deno run -A https://deno.land/std/examples/xeval.ts -d " " "if ($ === 'MIT') console.log('MIT licensed')", USAGE: deno run -A https://deno.land/std/examples/xeval.ts [OPTIONS] <code> OPTIONS: -d, --delim <delim> Set delimiter, defaults to newline -I, --replvar <replvar> Set variable name to be used in eval, defaults to $ ARGS: <code> []
-n xeval
表示全局安裝的命令行名稱,安裝完之後你就可使用 xeval
了。
舉個例子,咱們使用 xeval 過濾日誌文件,僅僅展現 WARN 類型的行:
➜ ~ cat catalina.out | xeval "if ($.includes('WARN')) console.log($.substring(0, 40)+'...')" 2020-08-12 13:37:39.020 WARN 202 --- [I... 2020-08-12 13:37:39.020 WARN 202 --- [I... 2020-08-12 13:37:39.019 WARN 202 --- [I... 2020-08-12 13:34:42.822 WARN 202 --- [o... 2020-08-12 13:34:42.822 WARN 202 --- [o... 2020-08-12 13:34:42.814 WARN 202 --- [o... 2020-08-12 13:34:42.805 WARN 202 --- [o...
$
美圓符表示當前行,程序會自動按行讀取讓執行 xeval 命令後面的 JS 代碼。
catalina.out
是我本地的一個文本日誌文件。你可能會以爲這樣挺麻煩的,直接 | grep WARN
不香嘛?可是 xeval
的可編程性就高不少了。
deno 內置了一個簡易的測試框架,能夠知足咱們平常的單元測試需求。咱們寫一個簡單的測試用例試試,新建一個文件 test_case.ts
,保存下面的內容:
import { assertEquals } from "https://deno.land/std/testing/asserts.ts"; Deno.test("1 + 1 在任何狀況下都不等於 3", () => { assertEquals(1 + 1 == 3, false) assertEquals("1" + "1" == "3", false) })
使用 test 命令跑這個測試用例:
➜ deno test test_case.ts Check file:///Users/zhouqili/.deno.test.ts running 1 tests test 1 + 1 在任何狀況下都不等於 3 ... ok (3ms) test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out (3ms)
能夠看到測試經過了。
還有其它不少好用的命令,可是在我並沒用太多的實際使用經驗,就很少介紹了。
上面說了這麼多基礎知識,終於能夠講點實際應用場景了。咱們在本身的一個 SDK 項目中使用了 Deno 來作自動化單元測試的任務。整個流程走下來仍是挺流暢的。代碼就不放出來了,我只簡單的說明下這個 SDK 須要作哪些事情,理想的開發流程是什麼樣的。
若是你的場景和上面的吻合,那麼就可使用 Deno 來開發。本質上講咱們開發的時候寫的仍是 TypeScript,只是須要咱們在發佈 NPM 包的時候稍微的進行一下處理便可。
咱們以實現一個 fetch 請求的封裝方法爲例來走通整個流程。
➜ ~ mkdir mysdk ➜ ~ cd mysdk ➜ mysdk npm init -y
創建好文件夾目錄,及主要文件:
➜ mysdk mkdir src tests ➜ mysdk touch src/index.ts ➜ mysdk touch src/request.ts ➜ mysdk touch tests/request.test.ts
若是你使用的是 vscode 編輯器,能夠安裝好 deno 插件(denoland.vscode-deno),而且設置 deno.enable
爲 true
。你的目錄結構應該是這樣的:
├── package.json ├── src │ ├── index.ts │ └── request.ts └── tests └── request.test.ts
index.ts
爲對外提供的導出 API。
使用 tsp --init 來初始化項目的 typescript 配置:
tsc --init
更新 tsconfig.json 爲下面的配置:
{ "compilerOptions": { "target": "ES5", "lib": ["es6", "dom", "es2017"], "declaration": true, "outDir": "./build", "strict": true, "allowUmdGlobalAccess": true, "forceConsistentCasingInFileNames": true }, "include": [ "src/**/*.ts" ] }
注意指定 outDir
爲 build
方便咱們將編譯完的 JS 統一管理。
爲了演示,這裏就簡單寫下。request.ts
代碼實現以下:
export async function request(url: string, options?: Partial<RequestInit>) { const response = await fetch(url, options) return await response.json() }
調用端封閉好 GET/POST 請求的快捷方法,而且從 index.ts
文件導出:
import {request} from "./request.ts"; export async function get(url: string, options?: Partial<RequestInit>) { return await request(url, { ...options, method: "GET" }) } export async function post(url: string, data?: object) { return await request(url, { body: JSON.stringify(data), method: "POST" }) }
在 tests/request.test.ts
目錄寫上單元測試用例:
import { assertEquals } from "https://deno.land/std/testing/asserts.ts"; import {get, post} from "../src/index.ts"; Deno.test("request 正常返回 GET 請求", async () => { const data = await get("http://httpbin.org/get?foo=bar"); assertEquals(data.args.foo, "bar") }) Deno.test("request 正常返回 POST 請求", async () => { const data = await post("http://httpbin.org/post", {foo: "bar"}); assertEquals(data.json.foo, "bar") })
最後在命令行使用 deno test
命令跑測試用例。注意添加 --allow-net
參數來容許代碼訪問網絡:
➜ mysdk deno test --allow-net tests/request.test.ts Check file:///Users/zhouqili/mysdk/.deno.test.ts running 2 tests test request 正常返回 GET 請求 ... ok (632ms) test request 正常返回 POST 請求 ... ok (342ms) test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out (974ms)
咱們能夠看到測試都經過了,下面就能夠安心的發佈 NPM 包了。
須要注意一點 Deno 寫 TypeScript 的時候嚴格要求導入的 文件路徑 必須添加 .ts
後綴。可是 TS 語言並不須要顯式的添加這個後綴,TS 認爲引入(import)的是一個 模塊 而不是文件。這一點 TS 作的比較極端,tsc 要求你必須刪除掉 .ts
後綴才能編譯經過,這個我我的認爲是很是不合理的。可是 Deno 有它的考慮,由於沒有嚴格的文件名後綴引發程序 BUG 我本身也遇到過。
上面的幾步都相對流暢,惟獨到發佈 NPM 包這一步就比較麻煩。由於本質上講 Deno 只是 TypeScript/JavaScript 的運行時,並不兼容 NPM 這種包管理工具。並且 NPM 是爲 Node.JS 設計的,它也沒有辦法直接發佈 TypeScript 的包,咱們只能把 TypeScript 編譯成 JavaScript 再進行發佈。
發佈這裏咱們的需求有兩點:
window.MySDK
訪問到第二個簡單,咱們直接使用 tsc
的命令就能夠完成:
tsc -m esnext -t ES5 --outDir build/esm
這時你會發現我上面提到的問題,tsc 報錯了:
➜ mysdk tsc -m esnext -t ES5 --outDir build/esm src/index.ts:1:23 - error TS2691: An import path cannot end with a '.ts' extension. Consider importing './request' instead. 1 import {request} from "./request.ts";
說我不能使用 `.ts`! 這就尷尬了,deno 要求我必須添加,TS 又要求我不能添加。你到底想讓人家怎麼樣嘛? 並且還有一個問題,咱們如今實現的功能還很簡單,引入的文件不多,能夠手動修改下。可是之後功能多了怎麼辦?文件不少手動修改確定不是辦法啊。實在不行仍是算了,不用 Deno 了? 其實嘛,解決方法仍是有的,上面咱們不是介紹過 Deno 安裝腳本功能了嗎。咱們本身寫個腳本放在 NPM Script 裏面,每次編譯發佈前這個腳本自動把 `.ts` 去掉,發佈完再自動改回來不就行了。 因而乎我本身寫了一個 Deno 腳本,專門用來給項目的文件批量添加或者刪除引用路徑上面的 `.ts` 後綴: 源代碼我就不所有貼出來了,簡單講就是用正則匹配出每一個 ts 文件中的頭部的 import 語句,按命令傳入的參數去處理後綴就能夠了。代碼我放到了 gist 上,有興趣的能夠研究下: > https://gist.github.com/keelii/d95492873f35f96d95f3a169bee934c6 你可使用下面的命令來安裝並使用它:
deno install --allow-read --allow-write -f -n deno_ext https://gist.githubuserconten...
使用 deno_ext 命令便可:
~ deno_ext
✘ error with command.
Remove or restore [.ts] suffix from your import stmt in deno project.
Usage:
deno_ext remove <files>...
deno_ext restore <files>...
Examples:
deno_ext remove */.ts
deno_ext restore src/*.ts
工具告訴你如何使用它,remove/restore 兩個子命令+目標文件便可。 咱們配合 `tsc` 能夠實現發佈時自動更新後綴,發佈完還原回去,參考下面的 NPM script:
{
"scripts": {
"proc:rm_ext": "deno_ext remove src/*.ts", "proc:rs_ext": "deno_ext restore src/*.ts", "tsc": "tsc -m esnext -t ES5 --outDir build/esm", "build": "npm run proc:rm_ext && npm run tsc && npm run proc:rs_ext"
}
}
咱們使用 `npm run build` 命令就能夠完成打包 ESModule 的功能:
➜ mysdk npm run build
mysdk@1.0.0 build /Users/zhouqili/mysdk
npm run proc:rm_ext && npm run tsc && npm run proc:rs_ext
mysdk@1.0.0 proc:rm_ext /Users/zhouqili/mysdk
deno_ext remove src/*.ts
Processing remove [/Users/zhouqili/mysdk/src/index.ts]
Processing remove [/Users/zhouqili/mysdk/src/request.ts]
mysdk@1.0.0 tsc /Users/zhouqili/mysdk
tsc -m esnext -t ES5 --outDir build/esm
mysdk@1.0.0 proc:rs_ext /Users/zhouqili/mysdk
deno_ext restore src/*.ts
Processing restore [/Users/zhouqili/mysdk/src/index.ts]
Processing restore [/Users/zhouqili/mysdk/src/request.ts]
最終打包出來的文件都在 build 目錄裏面:
build
└── esm
├── index.d.ts ├── index.js ├── request.d.ts └── request.js
接下來咱們還須要將源代碼打包成單獨的一個 UMD 模塊,並展出到全局變量 `window.MySDK` 上面。雖然 TypeScript 是支持編譯到 UMD 格式模塊的,可是它並不支持將源代碼 bundle 到一個文件裏面,也不能添加全局變量引用。由於本質上講 TypeScript 是一個編譯器,只負責把模塊編譯到支持的模塊規範,自己沒有 bundle 的能力。 可是實際上當你選擇 --module=amd 時,TypeScript 實際上是能夠把文件打包 concat 到一個文件裏面的。可是這個 concat 只是簡單地把每一個 AMD 模塊拼裝起來,並無 rollup 這類的專門用來 bundle 模塊的高級功能,好比 tree-shaking 什麼的。 因此想達到咱們目標還得引入模塊 bundler 的工具,這裏咱們使用 rollup 來實現。什麼?你問我爲啥不用 webpack?別問,問就是「人生苦短,學不動了」。 rollup 咱們也就不搞什麼配置文件了,越簡單越好,直接安裝 devDependencies 依賴:
npm i rollup -D
而後在 package.json 中使用 rollup 把 tsc 編譯出來的 esm 模塊再次 bundle 成 UMD 模塊:
"scripts": {
"rollup:umd": "./node_modules/.bin/rollup build/esm/index.js --file build/umd/index.bundle.js --format umd --name 'MySDK'"
}
而後能夠經過執行 `npm run rollup:umd` 來實現打包成 UMD 並將 API 綁定到全局變量 `MySDK` 上面。咱們能夠直接將 `build/umd/index.bundle.js` 的代碼複製進瀏覽器控制檯執行,而後 看看 window 上有沒有這個 `MySDK` 變量,不出意外的話,就會看到了。 ![mysdk-window-global-ns](https://vip1.loli.net/2020/08/14/NTwQ7oiAmzc6Hyg.png) 咱們在 `index.ts` 文件中 export 了兩個 function:get/post 都有了。來試試看能不能運行起來 **注意**:有的瀏覽器可能還不支持 async/await,因此咱們使用了 Promise 來發送請求 ![mysdk-get-request](https://vip1.loli.net/2020/08/14/ua4eb2CUyMvm6ki.png) 到此,咱們全部的需求都知足了,至少對於開發一個 SDK 級別的應用應該是沒問題了。相關代碼能夠參考這裏:https://github.com/keelii/mysdk 須要注意的幾個問題: 1. 咱們代碼中能使用 fetch 的緣由是 Deno 和瀏覽器都支持這個 API,對於瀏覽器支持 Deno 不支持的就沒辦法寫測試用例了,好比:LocalStorage 目前 Deno 還不支持 2. 用 Deno 腳本移除 `.ts` 的後綴這個操做是比較有風險的,若是你的項目比較大,就不建議直接這麼處理了,這個腳本目前也只在咱們一個項目裏面實際用到過。正則匹配換後綴這種作法總不是 100% 安全的