BASEプロダクトチームブログ

ネットショップ作成サービス「BASE ( https://thebase.in )」、ショッピングアプリ「BASE ( https://thebase.in/sp )」のプロダクトチームによるブログです。

GitHub Codespaces環境でDenoを使ってSlack Botを作ってみよう!(Deno基礎知識 + 環境構築編)

GitHub Codespaces環境でDenoを使ってSlack Botを作ってみよう!(Deno基礎知識 + 環境構築編)

この記事はBASE Advent Calendar 2021の4日目の記事です

devblog.thebase.in

ごあいさつ

はじめましての人ははじめまして、こんにちは!フロントエンドエンジニアのがっちゃん( @gatchan0807 )です

今回は、フロントエンドエンジニア界隈で話題になっているDenoについて調査し、プラスでSlack Botを書いてみることで現時点のDenoはどんな感じで使えるのかを検証したので、その経験をシェアできればと思っています!(実際にSlack Botを書くのは次回の記事の予定です)

つらつらとDenoを触っていて気になったポイントを書いたためにかなり長い記事になってしまいましたが、お付き合いいただけますと幸いです!

Denoの基礎知識

DenoはNode.jsの作者、Ryan Dahl( https://github.com/ry )さんがNode.jsの反省点を活かして よりセキュアで生産的になるように 開発されているJavaScirpt / TypeScriptランタイムです

2018年5月14日に Initial commit が発行され、執筆時現在の最新版は v1.16.3 が最新となっていますが、開発スピードは非常に速く、執筆開始時点から約1週間でminorバージョン1つ、patchバージョンが3つ進んでいるので最新バージョンの情報は公式ページを確認してください⚠

個人的には、日本Node.jsユーザグループ代表の古川さんの記事をきっかけに国内では話題になっていて知った記憶があります( Node.js における設計ミス By Ryan Dahl ) ※上記の記事の元になった、Ryan Dahlさん本人による発表・資料はこちら(YouTubeの発表動画 / 発表資料PDF

また、国内コミュニティには日本語Slackチャンネルがあり、日本人Denoコミッターのkt3kさんもいらっしゃって、そこで色々情報交換などもされているので、少しでもDenoに興味があればぜひ参加されることをおすすめします(Deno-ja Slackへの参加方法

公式でとりわけ特徴として上げている部分

ここからは公式ドキュメントで取り上げられているDenoの推しポイントリストを紹介しつつ、普段Node.js環境で開発をしている自分が触っていて特徴的だなと感じた部分も合わせて紹介していきます。

特徴的な機能

  • 可能な限りWeb互換で利用できるようにする。例えば、ESModuleやfetchの対応など
  • 明示的に指定しない限りネットワークやファイル、その他環境へのアクセスは行えず、セキュアに保つ
  • インストールしてすぐにTypeScriptが使える(サポートしている)
  • 単一の実行可能ファイル( deno
  • ビルトインで様々なユーティリティツールが利用可能。( deno fmt: フォーマット機能、 deno lint: リンター機能、 deno test: テストランナー機能)
  • Denoチームによって動作保証(レビュー)されている標準ライブラリ
  • 単一JavaScriptファイルへのバンドル機能
原文:
- Web compatible where possible, for example through usage of ES modules, and support for fetch.
- Secure by default. No file, network, or environment access (unless explicitly enabled).
- Supports TypeScript out of the box.
- Ships a single executable (deno).
- Has built-in utilities like a code formatter (deno fmt), a linter (deno lint), and a test runner (deno test).
- Has a set of reviewed (audited) standard library that are guaranteed to work with Deno.
- Can bundle scripts into a single JavaScript file.

https://deno.land/manual#feature-highlights

他にも概要を調べていて個人的に特徴的だな〜と思ったのが↓のあたりのポイントです

  • URLによる依存ライブラリの取得と隠蔽されたパッケージファイル管理(No package.json & npm)。
    • 一度ロードされたパッケージはローカルにキャッシュされている(バージョンごとに自動で入るので、手でパッケージ内の一部ファイルを消してしまったとかのよほどのことが無いと気にすることはなさそう)
  • Comparison to Node.js(Node.jsとの比較)内にかかれているDeno always dies on uncaught errors. の通り、適切にキャッチして処理されないエラーは常に死ぬようになっている点

適切にキャッチして処理されないエラーは常に死ぬようになっている点に関しての補足(クリックで開閉)

これを見て、最初は「とはいえ、死なないことなんて起きるのか…?」と思って調べていたのですが、Node.jsでは uncaughtException / unhandledRejection のイベントリスナーを設定することで、どこからもエラーがキャッチされずにprocessまで上がってしまった場合の後処理が書けることを知りました。

uncaughtException / unhandledRejection のイベントリスナーは本来、メモリリーク防止の為の後処理や、開いたまま終わると問題になるネットワーク・ファイルのクローズ処理を書くためにあるもので、Node.jsの公式ドキュメントにも注意して利用するように記載されています (https://nodejs.org/api/process.html#process_event_uncaughtexception

デフォルトではprocessまでエラーが上がった場合にはスタックトレースに出力してexit-code 1で終了させるだけですが、ここを書き換えることによってexit-codeを0で終了させたり無理やり復旧させることができてしまうのが問題になっていました

そのため、Denoでは適切に処理されない場合には常に死ぬように設定されているようです

また、ちょうどこの辺りを調べていた時にDeno側でちょっと面白い議論を見つけて、記事執筆時の2日前に動きがあったのであわせて共有しておきます( https://github.com/denoland/deno/issues/7013#issuecomment-737621469 ) ざっくりいうと、「ライブラリ側で何らかの理由でキャッチされずに放置されたRejectPromiseの失敗時の対応)があった場合に有無を言わせずに死ぬので、どこで死んだかの調査がとても大変&アプリケーションになると現実的に無理ぽ。」というお話

これはLinterでもチェックされないし、人間は完璧にコードを書けるわけじゃないから対応してほしいよ〜😢というIssueがあり、ちょうど2日前に、ひとまずNode.js互換モードとして onunhandledrejection の実装が決まったようです https://github.com/denoland/deno/issues/7013#issuecomment-974256318

Denoの性質上、すべての非同期処理がPromiseで返されるので Unhandled Rejection を対応できるようにする方法は Uncaught Exception を対応できるようにする方法と同義になるんじゃね?とかのコメントを見て全体像が理解できたので、個人的にすごく学びがあったIssueでした

あとついでに Foot-gun というワードとその意味を知れたのも学び https://twitter.com/MengTangmu/status/1069751647771877376?s=20

Deno CLIから提供されるツール群

Denoの基本的な思想も面白いのですが、CLIコマンドが豊富でNode.js環境だと乱立しているテストランナーやフォーマッターなどがランタイムに標準装備されているのも面白いポイントです(JSConfでも、yosuke_furukawaさんとkt3kさんが「揺り戻しのよう全部入りのランタイムになってきているというのが面白い」という話がありました)

主だったツールのコマンドと利用シーン

  • 定義されたテストを実行する deno test
    • chaiやsinonなどと同時に使うことも可能
  • 指定のコードをフォーマットする deno fmt
  • Lint実行を行う deno lint
  • CLI上でJS/TSの実行結果をインスタントに試せる deno repl
  • 依存ライブラリ一覧を表示する deno info
    • ファイル指定を行わずに実行すればライブラリキャッシュの在り処を知れる
  • スクリプトをCLIコマンドとしてインストールする deno install
    • インストール時にきちんと --allow-net などで必要な権限を付与した上で実行する必要があるので注意
  • 複数JS/TSファイルを実行可能・インストール可能な形にコンパイルする deno compile
    • コンパイル時にも必要な権限を付与して実行する必要があるので注意
  • 複数JS/TSファイルをES Module形式でまとめられる deno bundle
    • 出来上がったファイルはES Moduleなので、直接ブラウザに読み込むことも可能
  • ドキュメントを自動生成する deno doc

Denoの環境を作って触っていく

ここまででざっくり基礎知識を得たので、実際にDeno環境を作って肌感覚を得ていこうと思います

今回は最近GAになり、なおかつ個人アカウントの場合に無料で利用できる GitHub Codespaces に環境構築を行おうと思います

雑なコードが残っているだけですが、こちらのリポジトリに実際に試した設定ファイルなどが残っているので良ければご覧ください

Codespacesの準備

この辺りはおなじみの光景なのでサクッと飛ばしますが、まずリポジトリを作成します

f:id:gatchan0807:20211201195755p:plain

適当に README.md ファイルなどを置いて、ファイル一覧の右上に表示される < > Codeボタンを押下するとCodespaceを作成するボタンが出てくるので New codespaceボタンをポチッとしてください

f:id:gatchan0807:20211201195750p:plain

しばらく待つと、下記のように見慣れたVS Codeの画面がブラウザに表示され、ターミナルも起動されるので準備完了🙆 (作成中に表示される緑色の Open this codespace in VS Code Desktop ボタンを押すと、ブラウザではなく手元のPCのVSCodeが起動して、そこからCodespacesに接続する形になります)

f:id:gatchan0807:20211201195745p:plain

Denoのインストール

ここからはCodespaces上に構築されたLinux環境にDenoをインストールしていきます

https://deno.land/

まずは公式ドキュメントに書かれている通りにCurl / ダウンロードされるShellScriptを実行してダウンロード → deno -V でインストールの完了を確認します

$ curl -fsSL https://deno.land/x/install/install.sh | sh

f:id:gatchan0807:20211201195741p:plain

f:id:gatchan0807:20211201195738p:plain

サンプルコードの実行

動作環境が用意できたので、サンプルコードの実行を行っていきます ドキュメントに書かれている通り、 deno run コマンドの引数にURLでTypeScriptファイルを指定し、実行してみましょう

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

すると下記のように自動的にファイルがダウンロードされ、実行するファイルに必要な権限などのチェック後、そのファイルに書かれたコードが実行されます このようにコマンド一つでファイルのダウンロードから実行まで行ってくれるのもDenoの特徴ですね

f:id:gatchan0807:20211201195735p:plain

※今回の例で使っているコマンドにはバージョン指定が入っていないので、「自動で最新のバージョンのコマンド実行するよ。」というWarningが出ていますが基本問題はないのでスルーでOKです

Codespacesのコンテナ設定でDeno環境を整えるように変更

続いて、毎回Codespacesの環境にDenoのインストールするところから始めるのは面倒なので、devcontainer.jsonという仕組みを使ってDenoを利用する設定をコード化し、Git管理化に設定ファイルを置いておきましょう


やることはVSCodeコマンドを実行して、選択肢をポチポチするだけでOKです

まずCodespacesが起動している状態で、 Cmd + Shift + P でCommand Palleteを起動し、add development と入力して下記の Codespaces: Add Development Container Configuration Files... のVS Codeコマンドを実行します

f:id:gatchan0807:20211201195732p:plain

続いて、(最初は deno を入力しても、よく使われるDefinitionsの選択肢には出てこないので、) Show All Definitions... をクリックして再度 deno を検索してください

f:id:gatchan0807:20211201195728p:plain

f:id:gatchan0807:20211201195725p:plain

最後に、ベースになるDebianのOSバージョンを聞かれるので、デフォルトの最新安定版を選択することで…

f:id:gatchan0807:20211201195722p:plain

bullseye はDebianのバージョン11のコードネームです( https://www.debian.org/releases/index.ja.html

完成! .devcontainer ディレクトリと、その配下に devcontainer.json / Dockerfile ができているはずなので、それらをコミットしておいてあげれば次回Codespaceを起動しなおしたときに自動的にDenoの環境構築がされたコンテナが適用されます

ちなみに、テンプレで作成されるDockerfileも基本やってることは一緒

# [Choice] Debian OS version: bullseye, buster
ARG VARIANT=bullseye
FROM --platform=linux/amd64 mcr.microsoft.com/vscode/devcontainers/base:0-${VARIANT}

ENV DENO_INSTALL=/deno
RUN mkdir -p /deno \
    && curl -fsSL https://deno.land/x/install/install.sh | sh \
    && chown -R vscode /deno

ENV PATH=${DENO_INSTALL}/bin:${PATH} \
    DENO_DIR=${DENO_INSTALL}/.cache/deno

便利ポイントとしては同時にDeno用の公式VS Code拡張のインストールや初期設定をしてくれる

https://marketplace.visualstudio.com/items?itemName=denoland.vscode-deno

他にもREADMEや解説記事などでは初期設定の実行 Deno: Initialize Workspace Configuration を行うように書かれている事が多いですが、下記の通りすでに設定が入っているのでスルーでOKです

{
    "name": "Deno",
    "runArgs": ["--init"],
    "build": {
        "dockerfile": "Dockerfile",
        // Update 'VARIANT' to pick an Debian OS version: bullseye, buster
        "args": {
            "VARIANT": "bullseye"
        }
    },
    "settings": {
        // Enables the project as a Deno project
        "deno.enable": true,
        // Enables Deno linting for the project
        "deno.lint": true,
        // Sets Deno as the default formatter for the project
        "editor.defaultFormatter": "denoland.vscode-deno"
    },

    // This will install the vscode-deno extension
    "extensions": [
        "denoland.vscode-deno"
    ],

    "remoteUser": "vscode"
}

公式ドキュメントを確認していく

以上でDeno環境の構築が完了したので、公式ドキュメントのTOPページを上からなぞって、書かれている公式ドキュメント等の役割をあわせて紹介していきます!

公式Docsのローディング画面めっちゃかわいい

https://doc.deno.land/builtin/stable

f:id:gatchan0807:20211201195717p:plain

ビルトインの関数・メソッドたちを確認できるドキュメントを開くと、可愛すぎるローディングアニメーションを見ることができます。一旦可愛い恐竜くんたちを愛でましょう

その後にちゃんと内容を見て、 Deno 名前空間と WebAssembly 名前空間が使われてるんだ〜とか思ったりしましたが、細かいところは使ってみながらの方が理解しやすそうなので、自分は一旦流し見で終了しました

公式マニュアルを眺めながら構築した環境で触っていく

https://deno.land/manual

f:id:gatchan0807:20211201195713p:plain

続いて公式マニュアル(Denoの開発方針から何から細かいことが書かれているドキュメント)を見始めたのですが、ここはボリュームがあるので後述の項で別途まとめていきます!

標準ライブラリ(標準モジュール)のドキュメントをざっくり確認

https://deno.land/std@0.116.0

f:id:gatchan0807:20211201195708p:plain

http モジュールとか crypto モジュールとか uuid モジュールとかのNode.jsと同じような各種モジュールが std 配下から色々提供されています。これらは3rdパーティモジュールと同じようにURL経由で提供されていますが、Denoチームがメンテナンスし、動作を保証してくれているものです

バージョンがものすごいスピードで上がっていて、この記事を書いてる5日の間にもにょきにょきバージョンが伸びていて、直近いろいろな改修が加えられているのをひしひしと感じています👀

3rdパーティモジュールをWebから読み込める

https://deno.land/x

f:id:gatchan0807:20211201195704p:plain

こちらは deno.land/x 配下にGitHubでホスティングされているパブリックライブラリを登録できる仕組みです

実際には直接JSをダウンロードできるURLがあれば、ここに登録せずにそのURLからライブラリのコードを配布しても良いのですが、ライブラリ提供者がここに登録して提供することで、Denoユーザーが覚えやすいURL( deno.land/x で現在は固定)を払い出すことができるので、ぜひやってね〜というテンションで運用されているようです

GitHub WebhookとGit Tagを使って設定を行うことで、特定のバージョンごとにDeno上にキャッシュを残してくれる動きをします

ライブラリのドキュメントを自動で書き出してブラウザからも見れる

https://doc.deno.land/

f:id:gatchan0807:20211201195700p:plain

ライブラリのコード内にJSDoc記法で書かれたドキュメントを取得して、Web UIとして表示できるジェネレーター機能を持ったページです

利用しているライブラリのURLを直接指定してあげれば、自動で deno doc --json で出力されたJSONファイルを元にページをジェネレートして表示してくれます(ライブラリ作者はこのJSONファイルも一緒に提供しておくのが推奨されています)

公式マニュアルをなぞってDenoを触っていくぞ!!

一通りDenoの環境構築と、Denoが提供している公式ドキュメントの全体像がわかってきたので、公式マニュアルを参考にGetting Startedからやっていこうと思います!

流石にここまでStep by Stepで書くのはあんまり意味ない気がするので、普段JavaScript/TypeScript + Node.jsを触っている人間が気になったポイントだけまとめていこうと思います

セルフアップデートできるの便利よ

f:id:gatchan0807:20211201195657p:plain

https://deno.land/manual/getting_started/installation#updating

Node.jsだととてもカオスになってたランタイムバージョン管理がランタイム自体でできるの地味に嬉しいですね

nvm やら nodenv やら nodebrew やら n やら ndenv やらが不要なのが地味に嬉しいですね

特定バージョンを指定してのインストールもここでできるみたいです👀

CodespacesでもTCPリクエストのサンプルコード全然動かせる

ここはDenoがどうこうという話ではなく、Codespacesの機能としてちょっと不安だったローカルのネットワークを触るサンプルコードがちゃんと動くのかを試して、問題なく動くのが確認できて感動した話です

f:id:gatchan0807:20211201195653p:plain

上記の結果はこのサンプルコードを実行して出てきたものなのですが、コード実行時にポップアップが出てきて、そのままブラウザの別タブに遷移できるようにしてくれるのでほとんど気にすることがなかったのが嬉しかったです

f:id:gatchan0807:20211201195649p:plain

またCodespacesの場合にのみ表示されるPORTSタブでは、自分でポートフォワーディングの設定を登録することも可能で、自動でドメインを発行して指定したポートをGitHub Authで守られた状態で自動的にバインドしてくれたりもします

f:id:gatchan0807:20211201195645p:plain

さらに、ここで特定のポートをクリックひとつで公開状態にできるので、バインドされたURLを他の人に共有してかんたんに動作確認をしてもらうことも可能なのが便利で嬉しいポイントです!

f:id:gatchan0807:20211201195642p:plain

基本的にはコンフィグファイルいらないよ!!という圧を感じる

{
  "compilerOptions": {
    "allowJs": true,
    "lib": ["deno.window"],
    "strict": true
  },
  "lint": {
    "files": {
      "include": ["src/"],
      "exclude": ["src/testdata/"]
    },
    "rules": {
      "tags": ["recommended"],
      "include": ["ban-untagged-todo"],
      "exclude": ["no-unused-vars"]
    }
  },
  "fmt": {
    "files": {
      "include": ["src/"],
      "exclude": ["src/testdata/"]
    },
    "options": {
      "useTabs": true,
      "lineWidth": 80,
      "indentWidth": 4,
      "singleQuote": true,
      "proseWrap": "preserve"
    }
  }
}

https://deno.land/manual/getting_started/configuration_file#example

こんな感じで設定をファイルにまとめてできる(実際のスキーマはこっち)のですが、基本的にはデフォルトを使ってね。複数人プロジェクトでどうしても必要な場合に作ってね。というニュアンスで書かれていました

また、このファイル自体は将来的に自動で探してきてロードするようになる予定なので deno.json / deno.jsonc で名前つけておくのがおすすめとのこと

デフォルトではできないことリスト

f:id:gatchan0807:20211201195638p:plain

https://deno.land/manual/getting_started/permissions#permissions-list

Denoではランタイムオプションを指定しないとできないことが意外とあるので、ちゃんとリストを知っておけると良さそうだなと思いました 一応、 -A, --allow-all: 全許可 なんてものもありはしますが、せっかくDeno使っているのであんまり使わないほうが良さそうかなぁ…と思います

また、このランタイムオプションには特定のコマンド / ファイル / URL / IPのみアクセスを許可することもできて、結構柔軟なので、ライブラリや動作環境をセキュアに保つために設定しておくのが良さそうに感じました🙆

The Runtimeに定義されているAPI群をみて感じたWeb互換の意識の強さ

全体的に、普段ブラウザJSを書いていると馴染んでいるAPIが並んでいてびっくりしました

Permission APIsはPWA機能を使うときによく叩くAPIだし、Program lifecycleはページロード時の処理とほぼ一緒だし、イベントの処理とかも普段使っている感じのやつだし…

特に Location API / Web Storage API に関してはそこまで考えて挙動作ってるの…?って思うぐらいに作られていてシンプルにすごいなと思いました…

ただ、この辺りで1点わからないポイントがあり、1.16で追加された fetch のファイル読み込み機能はファイルの動的インポートに使うのが想定ユースケースなのかなんなのか…?という疑問点が残っているので、有識者の方いらっしゃったらコメント等いただけますと嬉しいです…🙏

Denoで新規プロジェクトを始めるときに気になりそうなポイント

ここからは、次回のカレンダー(2021/12/12の1と2がいっぱいある日に公開予定)で実際にSlack Botを書いていく予定なので、Denoでコードを書く時のポイントの理解だけではなく、プロジェクトとしてすすめるのに必要そうな部分をまとめて紹介して行こうと思います

lock fileの存在

Denoはnpm / package.jsonが無い代わりにURLベースの依存ライブラリ管理方法を取っているため、パッケージ配布元のURLが変わったり、該当URLのライブラリが乗っ取られて悪意のあるコードが埋め込まれて内容に変わってしまった場合に気づく術が無いため、Denoではデフォルトで整合性チェック用のHashを持っています

1人で1端末で開発しているだけならDenoがデフォルトで管理されている、 /deno/.cache/deno/deps/https/deno.land あたりの [hash].meta.json ファイルたちだけで問題はありません

しかし、複数人での開発の場合はそれらのバージョン情報を共有する必要があるので、プロジェクトコードに lock.json を作り、Git管理下に置く方法が紹介されていました

f:id:gatchan0807:20211201195635p:plain

--lock=[path/to/lock.json] --lock-write をコマンド実行時に指定することでいい感じに作ってくれるので、プロジェクトではエントリポイント指定してGit管理下に置いておいた方が良さそうですね👍

Import mapsの存在

f:id:gatchan0807:20211201195632p:plain

https://deno.land/manual/linking_to_external_code/import_maps#import-maps

同様に、package.jsonがないことのデメリットとして、インポートする依存ライブラリの指定が各ファイルに散らばってしまうこと(個人的にどう解決してるのか一番気になってた問題)の解決方法として、WICGで仕様が定義されている import-maps を使って一箇所で依存ライブラリのURL・バージョン指定をできるようにしています(公式ではこの方法が推奨されているっぽいですが、他にも deps.ts というファイルを作り、そこでライブラリインポートを管理して、他のファイルからそれをまるっと読み込むパターンも有るようでした)

なので、プロジェクトの初期にこのファイルを作成しておいて、ライブラリの追加時にはここに追記して実際のアプリケーション側からはバージョン情報などを省いた省略記法で記述していくのが良さそうですね


この辺りを見て個人的には、

  • deno.json => Linter、Bundler、Formatter等の各種設定ファイルの集合体
  • import_map.json => package.jsonの依存ライブラリ指定部分の代替
  • lock.json => package-lock.json / yarn.lockの代替

として3ファイルを作るのが自然なのかなという理解をしたので、次回のSlack Bot作成ではこの形で各設定を管理し、実装を行っていこうと思います!

Deno基礎知識 + 環境構築編のまとめ

さて、ここまでDenoの基礎知識から実際に触れる環境を用意してマニュアルをなぞってざっくり全体感を理解してきました

2年半ほど前の発表時に話題になっているのを見つけ、そこからの進化をちょろっと横目で見ていた程度だったので、どういう機能が実装されているのかなどを知らなかったのですが、これである程度はDeno環境でコードを書けるようになりました

なので、次回の記事では社内のSlackで動くBotを実際に作って、Denoの触り心地をより理解していこうと思います!

先日Slack社がSlack Appの次世代開発プラットフォームをDenoを使って提供していくことを発表しました。

自分はこれにクローズドベータの申込みをしたものの、まだ参加できていないので、 もし次回の記事執筆までにクローズドベータに参加できれば、そちらの新しいプラットフォームを使って slack コマンドを使ったApps開発をまるっと構築をする。参加できなければ(非公式ではあるものの) deno-slack-sdk で管理されているDenoライブラリが公開されているのでそちらを使ってSlack Botを構築してみる。という形で記事を書こうと思います!

では、長々とお付き合いいただきありがとうございました!次回もお楽しみに!

明日のアドベントカレンダーは基板チームの右京さんの記事です!こちらもお楽しみに!

参考