Web API の 3 回目、本講座の最終回は、オリジン間リソース共有(ブラウザーから直接サードパーティ API にアクセスすることは通常はできないという話)と、その問題に対処するための簡単なサーバーサイドプログラムについて説明します。
雛形コード
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>petite-vue入門</title>
<style>
[v-cloak] { display: none; }
</style>
</head>
<body v-cloak @vue:mounted="init">
<p>{{data}}</p>
<script src="https://unpkg.com/petite-vue"></script>
<script src="script.js"></script>
</body>
</html>
'use strict';
PetiteVue.createApp({
async init() {
},
data: false
}).mount();
同一オリジンポリシー
前回・前々回のチャプターでは fetch 関数を使ってサードパーティ API を使用してきましたが、ここまで “同一オリジンポリシー/Same Origin Policy” という厄介な問題(というより大事な機能なのですが)を避けてきました。
オリジン というのは、例えば「https://zenn.dev」の部分です。「https://」の部分も含みます。より正確には、ポート番号も含む「https://zenn.dev:443」の部分をオリジンといいます。普段よく見ているURLにポート番号が書かれていないのは、ポート 80 番と 443 番(http と https の既定ポート番号)が省略可能だからです。書かれてなくても「:80」や「:443」と書いてあると思ってください。VSCode の Live Server 拡張機能によるプレビュー機能を使ってきた人は 5500 番ポートを使用しているので、ブラウザーのアドレスバーが「https://localhost:5500」となっていたかと思います。
同一オリジンポリシーというのは、ブラウザー上で動いている JavaScript から直接アクセスできるリソースは同じオリジンからに制限しましょうというのものです。例えば、index.html と同じフォルダ(ディレクトリ)の下に置いてある画像などは「同じオリジンのリソース」なので問題なく読み込めます。しかし、サードパーティ API は別のオリジンからリソースを取得しようというものなので、この制限に引っかかってしまいます。前回・前々回のチャプターで使ってきたサードパーティ API は、同一オリジンポリシーを守らずに「誰でも利用できるようになっているものを選んできました。
試しに「郵便番号検索 API」を使ってみましょう。以下の URL をブラウザーのアドレスバーに打ち込んで Enter キーを押してみてください。これは GET リクエストになっています。
https://zipcloud.ibsnet.co.jp/api/search?zipcode=7830060
郵便番号「7830060」の住所情報がブラウザーに表示されたと思います(これは郵便番号検索 API のウェブサイトに掲載されている例そのままです)。
次に、以下の JavaScript コードを実行してみてください(雛形コードの init メソッドのみ記載しています)。クエリ付きの GET リクエストです。
async init() {
const url = 'https://zipcloud.ibsnet.co.jp/api/search?zipcode=7830060';
const res = await fetch(url);
const obj = await res.json();
console.log(JSON.stringify(obj, null, 2));
}
ブラウザーはただの白い画面になるかと思いますが、コンソールはエラーメッセージで真っ赤になっているはずです。以下は Chrome(バージョン 92.x)の例です。
Access to fetch at 'https://zipcloud.ibsnet.co.jp/api/search?zipcode=7830060' from origin 'null' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
上記のエラーメッセージの中に「blocked by CORS policy」と書かれています。CORS とは “オリジン間リソース共有/Cross-Origin Resource Sharing” の略で、「CORS policy」というのは同一オリジンポリシーと(おそらく)同義で使われています。
同じブラウザーからの実行なのに、アドレスバーから Web API にアクセスした場合と JavaScript から Web API にアクセスした場合とでは何が異なるのでしょう。
アドレスバーに URL を打ち込んだ場合、その GET リクエストの「送信元」は郵便番号検索 API の AP サーバーになります。AP サーバーと送信先が同じオリジン(https://zipcloud.ibsnet.co.jp)になるので、同一オリジンポリシーは守られており、接続は正常に行われます。
一方、上記のサンプルコードを JavaScript から実行した場合、アドレスバーの表記は「https://localhost:5500」もしくは「file://パス/ファイル名」になっていると思います(前者は Live Server を使用した場合です)。このアドレス/パスが GET リクエストの送信元となり、郵便番号検索 API のサーバーのオリジンとは異なるため、同一オリジンポリシーを満たしません。
なお、index.html と script.js をウェブサーバーにアップロードして、ウェブサイトとしてインターネット公開しても結果は同じです。そのウェブサーバーのオリジンと AP サーバーのオリジンは異なっているからです。
異なるドメイン間でリソースを共有する
世の中にはサードパーティ API が溢れているのにブラウザーから使えない…というのはおかしな話ですので、もちろん回避策…というか正しい利用方法があります。
ひとつは、さきほど登場したオリジン間リソース共有(CORS)を正しく設定することです。これは名称のとおり、(異なる)オリジンの間でリソースを共有するためのブラウザーの仕組みです。しかし、この CORS の設定は AP サーバー側で行うものなので、サードパーティ API のほうで誰でも使えるように設定してくれないかぎり、こちらは何もできることがありません。さきほど郵便番号検索 API に GET リクエストした際のエラーメッセージに「No 'Access-Control-Allow-Origin' header is present」という文言がありましたが、これが出ているのは AP サーバー側が CORS の設定をしていないということです。
CORS を利用する以外に、もうひとつ回避方法があります。同一オリジンポリシー(CORS の仕組み)はブラウザーの機能なので、Web API にリクエストを送る「仲介プログラム」をブラウザー以外の場所から実行する方法です。
仲介プログラムにはいろいろありますが、ウェブアプリケーションの場合、ウェブサーバーや AP サーバーの上でプログラムを動かすのが一般的です。これを “サーバーサイド” のプログラムといいます。サーバーサイドに対して、ブラウザー側のことを “クライアントサイド” と呼びます。
本講座シリーズではこれまでクライアントサイドのみを扱ってきましたが、サーバーサイドでは、サードパーティ API とやりとりする他、データベースや認証サーバーとやりとりするプログラムを動かしたります。Firebase や MongoDB Atlas など、ブラウザーから直接アクセスできる BaaS(Backend as a Service)と呼ばれるクラウドサービスもありますが、“ウェブアプリケーション” といえば一般にサーバーサイドの開発も含みます。
Deno を使った Web API 仲介プログラム
ということで、サードパーティ API とのやりとりはサーバーサイドの仲介プログラムに記述する必要があるのですが、サーバーサイドは本講座のトピックからは外れますので、自分の PC だけでモックアップとしてアプリケーションを動かす簡単な方法を紹介します。
サーバーサイドといっても、自分の PC でアプリケーションが動作すればよいので、自分の PC でサーバーを動かします。「サーバー = インターネット上にあるすごいコンピューター」というイメージを持っている人もいるかもしれませんが、サーバーというのはただのプログラムです。何かしらサーバーの役割を果たすプログラムが動いていれば、自分の PC もサーバーになります(インターネットからアクセスできないというだけで)。
そのような「サーバーの役割を果たすプログラム」を作ることのできるツールのひとつに Deno があります。Deno は JavaScript の “ランタイム/Runtime” と呼ばれるもので、ブラウザー組み込み言語である JavaScript をブラウザー以外で使えるようにしたものです。JavaScript ランタイムといえば Node.js が有名ですが、Deno は同じ作者が開発している新バージョンです(Node.js も開発は続いています)。
Deno を使うためには “ターミナル/Termial” が必要です。Windows だと「コマンドプロンプト」か「PowerShell」、Mac だと「ターミナル」を使います。以下では Visual Studio Code(VSCode)から使用する方法を紹介しますが、本チャプターの範囲ではターミナルの種類の違いを意識することはほぼありません。
Deno のインストール
作業用の新しいフォルダを作成してください。フォルダ名は何でもよいですが、説明の便宜上、ここでは「app」という名前にします。app フォルダの下に「public」という名前のフォルダを作ってください。そして、雛形の index.html と script.js をその public フォルダに置きます。
次に、VSCode の「フォルダを開く」から app フォルダを開いてください。そして、app フォルダの直下でファイルを新規作成します。説明の都合上、「main.js」という名前で保存してください。自分でアプリケーション開発する際は好きな名称で OK です。
以下のようなファイル構成になります。CSS ファイルや画像データがある場合は public フォルダの下に置きます(images などのフォルダをさらに作っても構いません)。つまり、ウェブサイトデータとして公開する HTML・CSS・JavaScript ファイルはすべて public の下に入れます。なお、これらの「普通のウェブサイトとして提供するファイル」のことを “静的ファイル/Static files” と呼びます。
この状態で、VSCode の[表示]メニューから[ターミナル]という項目を選んでみてください(Mac の場合は[統合ターミナル])。ショートカットキーが項目の横に書かれているので、次からはそのショートカットキーを使ってくださいね。なお、トップメニューの中に[ターミナル]というメニューがあるのですが、本講座の範囲では[表示]→[ターミナル]を使ってください。
エディターの下にターミナルが出現したと思います。ターミナルの右上のほうのメニューの左端にターミナルの種類が表示されています。Windows なら「pwsh」、Mac なら「bash」になっているのではないかと思います。もし Windows で「cmd」となっていたら、Deno のインストール時のみ、その右側にある「+」ボタン右の ▽ から種類の一覧を表示して「PowerShell」を選んでください。
このターミナルにコマンドを貼り付けて実行することにより、Deno をインストールすることができます。ごちゃごちゃと文字が表示されているかもしれませんが、一番下におそらくファイルのパスが表示されている行があり、右端にカーソルがあると思います(この文字列のことをプロンプトといいます)。カーソルのところをクリックすると文字が打ち込めると思います(試しに打ち込んだら消しておいてください)。
それぞれ対応する OS のコードをターミナルにコピペして、Enter キーを押します。
iwr https://deno.land/x/install/install.ps1 -useb | iex
curl -fsSL https://deno.land/x/install/install.sh | sh
いろいろと表示されますが、そのまましばらく待ちます。「Deno was installed successfully」などとそれらしいメッセージがターミナルのどこかに表示されていたら成功です。
Mac の場合の追加作業
Mac の場合、以下のようなメッセージが表示されているかもしれません。「***」の部分はあなたのユーザー名が入っていると思います(家族共用ならそのユーザー名)。
Manually add the directory to your $HOME/.bash_profile (or similar)
export DENO_INSTALL="/home/***/.deno"
export PATH="$DENO_INSTALL/bin:$PATH"
これはホームディレクトリ(/home/ユーザー名)というフォルダの下にある「.bash_profile」というファイルにこの 2 行を追加しなさいということです。
ターミナルで以下のコマンドを実行すると VSCode の新しいウィンドウで「.bash_profile」という名前のファイルが開くので、
code ~/.bash_profile
ターミナルに表示された 2 行を入力して保存してください。もし「(管理者)権限がない」といったメッセージが表示される場合は sudo code ~/.bash_profile
と sudo を付けてコマンド実行してください(※そのユーザーのパスワードが求められます)。
ファイルを保存できたら、ターミナルを再起動(ゴミ箱ボタンを押してターミナルを閉じて、再度ターミナルを開く)するか、ターミナルから source ~/.bash_profile
というコマンドを打ち込めば、あとは以下の手順に進めると思います。なお、これはインストール直後に 1 度だけ必要となる作業です。
試しに、ターミナルに deno --version
と打ち込んで Enter キーを押してみてください(一文字でも間違えると失敗するので慎重に)。それらしい表示がされれば成功です。タイプミスもないのにエラーが表示されたら、VSCode を再起動してから再度試してみてください。
仲介プログラムの作成と実行
まず Deno 版の「Hello World」プログラムを動かしてみましょう。以下のコマンドをターミナルにコピペして実行(Enter キーを押す)してください。うまくインストール(と設定)ができていれば、「Welcome to Deno 🦕」と表示されるかと思います。
deno run https://deno.land/std/examples/welcome.ts
Deno の構文は以下のようになります。オプションは必要なときにだけ付けるのですが、後述するように仲介プログラムには必須になります。
deno run [オプション] スクリプト
なお、スクリプトにはローカル(自分の PC)のファイルだけでなく、前述の Hello World プログラムのようにインターネットに公開されているファイルも指定できます。
では、fetch を仲介するプログラムの雛形を動かしてみましょう。CORS に対応していなかった郵便番号検索 API を動かしてみます。コードの説明は後ほどしますので、まずは各ファイルをコピペしてください。
import Aqua from 'https://deno.land/x/aqua@v1.2.4/mod.ts';
const portN = 8800;
const app = new Aqua(portN);
console.log(`Server running at http://localhost:${portN}`);
app.serve("public", "/public");
app.get('/', async (req) => {
return await app.render('public/index.html');
});
app.get('/api', async (req) => {
const url = 'https://zipcloud.ibsnet.co.jp/api/search?zipcode=7830060';
const res = await fetch(url);
const obj = await res.json();
// console.log(JSON.stringify(obj, null, 2));
return JSON.stringify(obj);
});
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Deno + petite-vue</title>
<style>
[v-cloak] { display: none; }
</style>
</head>
<body v-cloak>
<button @click="getResource">リソース取得</button>
<p v-if="data">{{data}}</p>
<script src="https://unpkg.com/petite-vue"></script>
<script src="/public/script.js"></script>
</body>
</html>
'use strict';
PetiteVue.createApp({
async getResource() {
const res = await fetch('api');
const obj = await res.json();
console.log(JSON.stringify(obj, null, 2));
this.data = obj.results[0];
},
data: false
}).mount();
ターミナルから Deno で実行しましょう。
Deno から動かすスクリプトは「main.js」です。オプションが 2 つ付いていますが、ネットワークとファイル読み込みを許可するものです。
deno run --allow-net --allow-read main.js
上記のコマンドを実行(コピペ&Enter キー)すると、しばらくのあいだ、必要な裏方ファイルをダウンロードしている様子がターミナルに表示されます。これは初回起動時のみです。
それが終わると「Server running at http://localhost:8800」と表示されるかと思います。ターミナル上でこのメッセージの URL の上にマウスポインタを重ねると、リンクが直接ブラウザーで開けることがわかります(Windows の場合は Control キーを押しながらクリックです)。
ブラウザーが起動すると[リソース取得]というボタンが表示されるかと思います。ボタンをクリックすると、無事に郵便番号が取得できました。
一方、ターミナルの方はプロンプトが表示されなくなり、カーソルがなくなって文字入力を受け付けない(もしくは反応しない)状態になります。サーバーがエラーで強制終了するか、サーバーを手動で終了させるか、ターミナル(や VSCode)を終了しないかぎり、この状態は続きます。main.js に console.log を記述していると、この無反応のターミナルに表示されます。
手動でサーバーを終了させるには、ターミナルにフォーカスを当てた上で、Windows なら Ctrl-C(Control キーを押しながら'C'キーを押す)、Mac なら Cmd-C(Command キー + 'C' キー)を押します。
仲介プログラムコードの解説
雛形を改造して好きなサードパーティ API を使えるように、さきほどの雛形コードを理解しましょう。
index.html
まず、index.html は script.js のパスが変わるだけです。頭に「public/」を付けます。
<!-- script.jsのパスを変える -->
<script src="/public/script.js"></script>
ウェブサイト制作でパスを勉強してきた私たちにはちょっとややこしいのですが、public フォルダに置いた静的ファイルにおいては、すべて「/public/***」という形の絶対パスになります。つまり、index.html に記述する相対パスは「index.html から見てどこにあるか」といったようなことは考える必要がなく、どのファイルに記述するパスも public フォルダから見た絶対パスになります(このほうがわかりやすいですよね)。
例えば、以下のようなファイル構成のアプリケーションを作ったとしましょう。
それぞれのファイルに記述されるパスは例えば次のようになります。何も考えずに public フォルダをルート(先頭)としたパスを指定すればいいだけですね。
<link rel="stylesheet" href="/public/css/style.css">
<img src="/public/image/bird.jpg" alt="鳥さん">
<script src="/public/script.js"></script>
document.querySelector("img").src = "/public/image/bird.jpg";
background-image: url("/public/image/bg.png");
script.js
script.js はかなり簡略化されて次のようになります。
const res = await fetch('api');
fetch 関数を使った ファイルからのデータ取得 に近い感じになります。「リソースの URL」がなぜ “api” になるのかは後述しますが、ここでは api というファイルを開いて JSON を読み込んだ…と認識しておけば OK です。あとは、これまでどおり await res.json()
で JSON を取得していきます。
main.js
では、肝心の main.js に移りましょう。上から順番に説明します。
まず先頭行で、Aqua オブジェクトをインターネットから読み込んでいます。
import Aqua from 'https://deno.land/x/aqua@v1.2.4/mod.ts';
Aqua は Deno 用の ウェブフレームワークで、ウェブサーバーや AP サーバーを簡潔に記述することができます。petite-vue のウェブサーバー/AP サーバー版ようなものです。
次に、サーバープログラムを動かします。
const portN = 8800;
const app = new Aqua(portN);
console.log(`Server running at http://localhost:${portN}`);
Aqua の場合、new Aqua(ポート番号)
という形で Aqua オブジェクトを生成するだけでサーバーとして動きます。ポート番号は他で使われていなければ自由に付けて構いません(例えば、5500 番は Live Server 拡張機能で使われていますね)。console.log はわかりやすいように追加しただけで必須ではありません(が、これがないと実行しても本当に無言です)。
以降は、Aqua オブジェクトが格納された定数 app のメソッドを呼び出しつつ、設定を行っていきます。
serve メソッドで、ウェブサイトデータである静的ファイルの置き場所を設定します。
app.serve("public", "/public");
この記述がないと AP サーバーとなり、ウェブサーバーの機能(静的ファイルの配布)は持ちません。2 つの引数で、プログラムコードから使用するパス名と実際のフォルダ名を紐づけます。パス名とフォルダ名は同じにしておくのがわかりやすいと思います。静的ファイル置き場のフォルダ名を public でないものにしたい場合はここで指定することになります。
ここから Aqua オブジェクトの get メソッドが 2 つ続きます。第 1 引数に指定したパスに対して GET リクエストが届いたときの処理を第 2 引数の無名関数に記述します。
1 つ目の get メソッドは「/」に GET リクエストが送られたとき、つまり、ブラウザーのアドレスバーに「http:localhost:8800」とだけ記述されたときにどうするか…です。
app.get('/', async (req) => {
return await app.render('public/index.html');
});
私たちの目的では index.html を表示させたいので、app.render メソッドでパスを指定します。ここのパスは main.js からの相対パスであって、絶対パス「/public/index.html」ではありませんので注意してください。
2 つ目の get メソッドがメインの部分です。ここまでは雛形として何も考えずにコピペで使用できますが、ここだけは自分の使いたいサードパーティ API に合わせて書き換える必要があります。
app.get('/api', async (req) => {
// サードパーティAPIへのHTTPリクエスト
const url = 'https://zipcloud.ibsnet.co.jp/api/search?zipcode=7830060';
const res = await fetch(url);
const obj = await res.json();
// console.log(JSON.stringify(obj, null, 2)); // ターミナルで確認できる
// JSONの文字列としてレスポンスを返す
return JSON.stringify(obj);
});
といっても、第 1 引数で GET リクエストを受けるパス名を設定して、最後に JSON.stringify したオブジェクトを return するだけです。
そのあいだに記述するサードパーティ API に対する処理は前回・前々回のチャプターで説明した方法と同じです[1]。GET リクエストに限らず、POST など他の HTTP リクエストも記述できます。「Aqua オブジェクトの get メソッドの中なのに…?」と思う人がいるかもしれませんが、以下のような 2 段階構造になっていることを思い出してください。この get メソッドは script.js からの GET リクエストをさばくための関数です。
get メソッドの第 1 引数のパスは「/パス名」というように記述します。ここを「/api」にしたので、script.js から fetch('api')
でアクセスできるようになっているわけです。複数の HTTP リクエストを利用したい場合には get メソッドを追加して、パス毎に異なる HTTP リクエストを担当させるとよいでしょう。
アプリケーションからのパラメータを受け取る
最後に、自作アプリケーションからパラメータを受け取る例を見ておきます。
せっかく petite-vue を使ってフォーム入力データを簡単に取得できるようになったので、サードパーティ API に送るパラメータをアプリケーション上で決めたいですよね。そのためには、仲介プログラム(main.js)への GET リクエストにクエリを渡す必要があります。
ブラウザーのフォームから検索したい郵便番号を入力して、それを仲介プログラム(main.js)に渡す修正を加えましょう。HTML 側では、テキストフォームの入力値を v-model によってデータプロパティ zipcode と紐づけます。
<label>
郵便番号:<input v-model="zipcode">
</label>
<button @click="getResource">リソース取得</button>
<p v-if="data">{{data}}</p>
JavaScript 側では zipcode の値を URLSearchParams オブジェクトを介して渡します。たった 1 個のクエリなのでテンプレート文字列でも構いません。
PetiteVue.createApp({
// データプロパティ
data: false,
zipcode: '',
// メソッド
async getResource() {
// クエリの設定
const query = new URLSearchParams({
zipcode: this.zipcode,
});
const res = await fetch('api?' + query);
const obj = await res.json();
this.data = obj.results[0];
}
}).mount();
静的ファイルの修正だけならば、サーバー(deno run コマンド)は動かしたまま、ブラウザーを再読み込みすれば適用されます。main.js でクエリを受け取らなくてもエラーにはなりませんから、この状態で(サーバーを停止していなかった人は)ブラウザーを再読み込みして確認しましょう。
次に、仲介プログラム(main.js)を修正します。
GET リクエストで受け取った情報は、無名関数の引数 req に入っており、送られたクエリは query プロパティから取り出せます。
app.get('/api', async (req) => {
// クエリーを取得
const zipcode = req.query.zipcode;
// クエリーの値をサードパーティAPIへのfetchに使用
const url = `https://zipcloud.ibsnet.co.jp/api/search?zipcode=${zipcode}`;
// あとは同じ
const res = await fetch(url);
const obj = await res.json();
return JSON.stringify(obj);
});
main.js はサーバーを動かしているコードなので、修正したらサーバーを再起動する必要があります。サーバーの停止は、ターミナルにフォーカスを当てた上で Ctrl-C もしくは Cmd-C です。サーバーを停止すると、ターミナルの画面にプロンプトが表示され、再びキー入力を受け付けるようになります。ときどき 1 回では終了してくれないことがあるので、止まらないな…と思ったらもう一度押してください。
サーバーを終了したら、再び Deno のコマンドを実行してサーバーを起動します。以下のコマンドを再度コピペしてもよいですが、カーソルキーの上矢印を押すと過去のコマンドの履歴が出てきます。直前に実行しているはずですから、上矢印を 1 回押せばコマンドが表示されるはずです。
deno run --allow-net --allow-read main.js
サーバーを再起動したら、ブラウザーも再読み込みします。試しに、自分の家の郵便番号をテキストフォームに入力して[リソース取得]ボタンを押してみましょう。うまく動いたでしょうか。
以上、script.js から main.js に GET リクエストを送る方法で説明しましたが、POST リクエストにしてメッセージボディに画像を付けることもできます。main.js 側で POST リクエストを受ける方法は Aqua のドキュメント を参照してください。
おわりに
以上で『文系のための petite-vue 入門』は終わりです。最後の 3 回は、petite-vue とは直接関係のない Web API の話をしましたが、サードパーティ API を使えるだけでアプリケーションの幅が大きく広がります。localStorage と組み合わせればブラウザーを閉じてもデータが残せるため、ちょっとしたアプリケーションのモックアップを作ることができます。
この講座の内容だけでは実現できないことはたくさんあるのですが、大きなところでは次の 2 点があります。
- ユーザー認証(ログイン機能)
- 他のユーザーとのデータ共有
これらが欠けているというのは、ウェブアプリケーションとしてはなかなかに致命的ですよね。しかし上の方でも触れましたが、これらの機能をブラウザーから直接利用することのできる Firebase などの BaaS が台頭してきているので、サーバーサイドの世界に踏み込むよりも前に、まずはそちらの勉強を進めてみてもいいかもしれません。
また、アプリケーション開発は通常チームで行いますから、クライアントサイド(フロントエンド)とサーバーサイド(バックエンド)を分けて開発するということも可能です。本講座シリーズで学んだ人はクライアントサイドのユーザーインタフェースの部分を担当するのがよいでしょう。HTML・CSS・JavaScript と petite-vue だけでもなかなかに奥深いと思います。
-
これは同じであって当たり前…ではなく、Deno が意識的にブラウザーの Fetch API と Deno の Fetch API のインタフェース(使い方)を同じにしているからです。本講座で Deno を採用した理由のひとつです。 ↩︎