從實際案例講 Deno 的應用場景

此篇文章實際上就是《 前端開發的瓶頸與將來》的番外篇。主要想從實用的角度給你們介紹下 Deno 在咱們項目中的應用案例,現階段咱們只關注應用層面的問題,不涉及過於底層的知識。

簡介

deno

咱們從它的官方介紹裏面能夠看出來加粗的幾個單詞:secure, JavaScript, TypeScript。簡單譯過來就是:前端

一個 JavaScript 和 TypeScript 的安全運行時

那麼問題來了,啥叫運行時(runtime)?能夠簡單的理解成能夠執行代碼的一個東西。那麼 Deno 就是一個能夠執行 JavaScript 和 TypeScript 的東西,瀏覽器就是一個只能執行 JavaScript 的運行時。node

特性

  • 默認是 安全的,這意味着初始的狀況下你是 不能夠 訪問網絡、文件系統、環境變量的。
  • 開箱即用的 TypeScript 支持,就是說你能夠直接使用 Deno 運行 TypeScript 而 不須要 使用 tsc 編譯
  • Deno 的構建版只有一個可執行文件,那麼你能夠直接下載這個可執行文件到本地執行,而 不須要 編譯、安裝的操做
  • 內置了一些工具集,好比:依賴檢查器、代碼格式化。咱們用到的測試框架竟然沒有被重點提起
  • 一系列的通過代碼 review 的內置模塊,這表示當你使用 Deno 的時候,一些經常使用的工具方法都內置了,不須要再添加三方依賴
  • 部分瀏覽器特性兼容,這個並非官方宣傳的特性,可是我認爲是很重要的一點。這個我意味着若是設計合理,你的代碼便可以跑在 Deno 裏面,也能夠在瀏覽器裏面。

安裝

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 內置命令

Deno 內置了豐富的命令,用來知足咱們平常的需求。咱們簡單介紹幾個:github

deno run

直接執行 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 把這個受權交給了執行者,好處就是若是執行的代碼是第三方的,那麼執行者就能夠主動拒絕一些危險性很高的操做。

好比咱們安裝一些命令行工具,而通常命令行工具都是不須要網絡的,咱們就能夠不給它網絡訪問的權限。從而避免了程序偷偷地上傳/下載文件。

deno eval

執行一段 JS/TS 字符串代碼。這個和 JavaScript 中的 eval 函數有點相似。

➜  ~ deno eval "console.log('hello from eval')"
hello from eval

deno install

安裝一個 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

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 須要作哪些事情,理想的開發流程是什麼樣的。

  1. SDK 以 NPM 包的形式發佈,給調用者使用
  2. SDK 主要提供一些封裝方法,好比:網絡請求、事件發佈訂閱系統等
  3. SDK 的代碼一般不依賴 DOM 接口,而且調用的宿主環境方法與 Deno 兼容
  4. 測試用例不須要在瀏覽器裏面跑,使用 Deno 在命令行中自動化完成
  5. 若是能夠最好能作到瀏覽器使用能夠獨立打包成 UMD 模塊,NPM 安裝則能夠直接引用 ES 版模塊

若是你的場景和上面的吻合,那麼就可使用 Deno 來開發。本質上講咱們開發的時候寫的仍是 TypeScript,只是須要咱們在發佈 NPM 包的時候稍微的進行一下處理便可。

咱們以實現一個 fetch 請求的封裝方法爲例來走通整個流程。

初始化一個 NPM 包

➜  ~ 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.enabletrue。你的目錄結構應該是這樣的:

├── package.json
├── src
│   ├── index.ts
│   └── request.ts
└── tests
    └── request.test.ts

index.ts 爲對外提供的導出 API。

初始化 tsconfig

使用 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"
  ]
}

注意指定 outDirbuild 方便咱們將編譯完的 JS 統一管理。

編寫 request 方法

爲了演示,這裏就簡單寫下。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 包

上面的幾步都相對流暢,惟獨到發佈 NPM 包這一步就比較麻煩。由於本質上講 Deno 只是 TypeScript/JavaScript 的運行時,並不兼容 NPM 這種包管理工具。並且 NPM 是爲 Node.JS 設計的,它也沒有辦法直接發佈 TypeScript 的包,咱們只能把 TypeScript 編譯成 JavaScript 再進行發佈。

發佈這裏咱們的需求有兩點:

  1. 能夠將最終的代碼包合成到一個文件中編譯成 UMD,瀏覽器引入這個腳本能夠經過全局變量 window.MySDK 訪問到
  2. 經過 NPM 安裝的最好默認使用 ESModule

第二個簡單,咱們直接使用 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% 安全的
相關文章
相關標籤/搜索