Nodeもわからない人がDenoの導入を検討してみた

# この記事の結論

Denoは速いが導入を検討するのも早い

正直に言えばこれタイトルに書きたかったが
それをするにはNodeの知識も何もかもたりてなかった

この記事は、DenoどころかNodeも知らない私のために書いた記事だ

Denoについて耳にしたことはあるが、ほとんど何もわからない人は
この記事が少しは参考になると思う

- JavaScript、Vueでプロジェクト開発を経験している
- TypeScriptの知識はない
- 最近やっと環境構築について興味を持った
- Nodeの知識はなく、JavaScriptを使うための呪文程度にしか考えていない
- Denoについて全く知らないが新しい開発環境になり得るため調査をはじめた

# 調査内容

- Denoとは何か
  - 動画説明：Nodeで後悔したこと
  - 動画説明：Denoに導入する仕組み
- Denoを試してみて体感したこと
  - package.jsonがないこと
    - Denoをインストールしてみる
    - Denoのフレームワークを試す
  - NPMが使えない、互換性がない
    - DenoでAPIを呼び出してみる
- 感想　Denoをプロジェクトに導入できるか

# Denoとは何か

Denoとは、Node.jsの開発者Ryan Dahl（ライアン・ダール氏）が
Nodeの設計について後悔している10の問題を解決するため
2018年に新しく作り出したランタイム環境

https://www.youtube.com/watch?v=M3BM9TB-8yA

## Nodeで後悔したこと

彼の述べたNode.jsに関する10の反省点は以下の通り
またなぜそれを後悔しているのか、動画を見ながら可能な限り解釈してまとめた


:::note warn
英語：動画内スレッド文章そのまま
日本語： 筆者の補足情報
:::


- Not sticking whis Promises
  - Promiseを使い続けなかったことによるasync/awaitのエコシステムの開発が遅れたこと
- Security
  - Nodeが全てのシステムコールにアクセスできてしまうことによるセキュリティの低下
  - デフォルトで全てのシステムにアクセス権限がある状態
- The Build System(GYP)
  - jipで複雑になってしまった依存関係とビルドシステム
- package.json
  - 直接URLで呼び出せば良いのに、モジュールという概念を生み出してしまったpackage.json
- node_modules
  - ローカルに落とすことで各プロジェクトが大きくなり圧迫するnode_modules
- requite("module") without the extentions ".js"
  - requiteモジュールに拡張機能を含めなかったためあまりにも非明示的になり、ファイルシステム調査時に非常に手間がかかるようになったこと
- index.js
  - ブラウザでも`/`で`/index.html`が読み込めるのに、nodeで`require("./")`で`require("./index.js")`を読み込むようにしたことでloaderを複雑にしてしまったこと


## Denoに導入する仕組み



- Security
  - Utillze the fact that JavaScript is a secure sandbox
    - デフォルトでアクセス権はなく事前に許可を設定することができる
    - 例: deno task --allow-net
  - Do not allow arbitrary native function tobe bound into V8
    - NodeではV8のネイティブ関数を直接呼び出すが、Denoではそれを行わない
    - なので前述した通り事前に許可する権限を付与する仕組みにしてある
- Deno Goals
  - Simplify the module system
    - ローカルへの相対パスか絶対パス、あるいはURLでしかモジュールを読み込めない
    - そのため既存のモジュールとの親和性は低くなる
    - URLは一度実行すればグローバルにキャッシュされる  
     node_modulesのように各ローカルへのインストールではない
    - --reloadで再度実行できる
  - TypeScript compiler build into the executable
  - Ship only a single executable with minimal linkage
    - Nodeのビルドプロセスように複雑な定義ファイルは読み込まない
  - その他
    - 未処理のpromiseは即座にシャットダウンする
    - top-level awaitをサポートする
      - 参考：Top-level await とは
        - https://zenn.dev/estra/books/js-async-promise-chain-event-loop/viewer/16-epasync-top-level-async#top-level-await-%E3%81%A8%E3%81%AF
    - ブラウザと機能が被ればDenoはブラウザの機能を使用できるようにする（fetchとかWindowsとか）


# 実際にDenoを試してみて体感したこと

5年前の動画の内容を読み解くのに時間をかけてしまったが
ここから先は実際に触れてみての感想となる

現在は2023年のため、2018年の動画で話してる内容と
現在のDenoは変化しているかもしれないが
触れてみてすぐに体感できたことは以下の通りだ

- package.jsonがないこと
  - URL指定で使いたいライブラリが指定できるのでわかりやすい
  - deno.jsonに書き込めば各ファイルでは定義名で読み込める
- NPMのライブラリが全て使えないわけではない
  - もちろん動画の中でも全て捨てたと言ってるのでほぼ使えないが、一部Denoで使えるようにしてくれている
    - 参考：https://github.com/denoland/deno/issues/13703
  - 改めて新しいライブラリを探したり自分で実装するのは大変なコストがかりそう

## package.jsonがないこと

Nodeとの違いは概ね理解できたので
実際にインストールして現状を確認してみる

Denoをインストールする（MAC OS BigSur）

```
$ brew install deno
```

バージョンの確認をして終わり
ものすごくはやい

```
$ deno --version
deno 1.32.5 (release, aarch64-apple-darwin)
v8 11.2.214.9
typescript 5.0.3
```

Denoではpackage.jsonの代わりに
設定ファイルとしてdeno.jsonができていた
deno.jsonには「imports」という項目がある

https://deno.com/manual@v1.34.0/getting_started/configuration_file#imports-and-scopes

必要なものはURLで直接指定する
moduleごとのパッケージがないからごちゃごちゃしない

各ファイルではimportsに書いた名前で呼び出せるため
全てのファイルでURLを書く必要はない

例：aleohjsフレームワークを試した時のもの

```json
{
  "imports": {
    "~/": "./",
    "std/": "https://deno.land/std@0.180.0/",
    "aleph/": "https://deno.land/x/aleph@1.0.0-beta.43/",
    "aleph/server": "https://deno.land/x/aleph@1.0.0-beta.43/server/mod.ts",
    "aleph/dev": "https://deno.land/x/aleph@1.0.0-beta.43/server/dev.ts",
    "aleph/react": "https://deno.land/x/aleph@1.0.0-beta.43/framework/react/mod.ts",
    "aleph/plugins/react": "https://deno.land/x/aleph@1.0.0-beta.43/framework/react/plugin.ts",
    "react": "https://esm.sh/v117/react@18.2.0",
    "react-dom": "https://esm.sh/v117/react-dom@18.2.0",
    "react-dom/": "https://esm.sh/v117/react-dom@18.2.0/"
  },
  "scopes": {}
}
```

各ファイルでの呼び出し方例

```
import type { FormEvent } from "react";
import { Head, useData } from "aleph/react";

```

## NPMのライブラリが全て使えないわけではない

動画では過去のNPMライブラリとの互換性は無くなるとの話をしていた

それはすごく爽快なことだろうとは思うけれど
全部使えないとなると一から開発しなおす必要があるし
プロジェクトにすぐに導入を検討することができない

なので、現在すでにNPMのライブラリを読み込むための仕組みが
導入されいてる様子だった

https://deno.com/manual@v1.32.0/node

`nom:`とつけて実行すると
グローバルにNPMライブラリがインストールされて
以降はそこから使用されるようになる

とはいえ、もしそのライブラリがnode-modulesから利用されることを想定している場合は
個別にnode-modulesディレクトリを作ることもできる様子

# Denoのフレームワーク

試してみたのは以下二つ
インストールの方法はどちらもとても簡単

- [Aleph.js](https://alephjs.org/)
  - Unocss
  - bootstrap
  - React/Vue/SolidJS/Yew/
- [Fresh](https://fresh.deno.dev/)
  - twind
  - preact

## Aleph.js

https://github.com/alephjs/aleph.js

インストールはGitに書いてる一行で完結

![Aleph.js_install.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/645692/67510091-9c26-4105-6448-8bb2deac46f2.png)
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/645692/9008d97d-eaf1-22b5-aa0e-e1c7de100b1b.png" width="300" />

サンプルのコードにはTODOリストが実装されている

![alephjs_demo1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/645692/5a53f1fa-850e-268f-3df1-953026fd4e1b.png)
![alephjs_demo2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/645692/734fec4f-4d03-8b6b-a11f-de3f7e37d3b7.png)
![alephjs_demo3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/645692/a70038db-8169-ceaf-0d06-411a264ac12e.png)

**補足： Vueのサンプルはダウンロードできなかった（2023/04/26)**

https://alephjs.org/docs/framework/vue

公式をみるとvue templateもインストールできるかのような記述があるが
2023/04/26現在は以下のような警告が出てダウンロードできなかった
公式サイトの情報はGitの更新に追いついてなくて古いかもしれない

```
! Invalid template name vue, must be one of [react,react-mdx,yew,leptos,api]
```

## fresh

https://github.com/denoland/fresh

![fresh_install.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/645692/52b4a410-2ab1-4d8c-a878-c51707a86493.png)

こちらはもとからVueのサンプルはなかったので
TypeScriptで手順通りインストール

![fresh_home.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/645692/d332c366-d0a8-e4e6-bffe-526687b37854.png)


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/645692/c703c36f-1e57-711c-d481-259c8fe8925d.png" width="300" />


# DenoでAPIを呼び出す（TODOアプリを作ってみる）

先に開発されて人気なのはAleph.jsのようだが
freshの方がディレクトリ構造が面白そうだったので
freshフレームワークを使いつつTODOアプリを作ってみた

## Fetch

https://deno.land/x/deno_api_server@v0.6.2

- DenoではFetchがdefaultで利用できる
- fetchで直接GQLquery(GET)を送る
  - 参考：https://zenn.dev/mayo_dev/articles/fetch-graphql-api
- Freshではislandsディレクトリで定義したイベントのみがクレイアント側で実行できる
  - これに気づかないままずっと ルート直下の index.tsx で実行して右往左往していた
  - https://github.com/denoland/fresh/discussions/716
  - https://fresh.deno.dev/docs/getting-started/adding-interactivity


~/islands/GqlTable.tsx
```tsx
import { useState } from "preact/hooks";
import { Button } from "../components/Button.tsx";

const CONTENT_API_URL = "あなたのContent API Endpoint URL";
const AUTH_TOKENS = "あなたのトークン";

// クエリはご自身で作ったテーブルにあわせて作成してください
const params = new URLSearchParams({
  query: `{
    tasks(orderBy: id_ASC) {
      id
      name
      description
      createdAt
    }
  }`,
});

export default function GqlTable() {
  const [data, setData] = useState({data: {tasks: []}});
  const [isLoading, setIsLoading] = useState(false);
  const [err, setErr] = useState('');
  const handleClick = async () => {
    setIsLoading(true);
  
    try {
      const response = await fetch(`${CONTENT_API_URL}?${params}`, {
        method: 'GET',
        headers: {
          Accept: 'application/json',
          Authorization: `Bearer ${AUTH_TOKENS}`,
        },
      });
  
      if (!response.ok) {
        throw new Error(`Error! status: ${response.status}`);
      }
  
      const result = await response.json();
  
      setData(result);
    } catch (err) {
      setErr(err.message);
    } finally {
      setIsLoading(false);
    }
  };
  return (
    <div>
      <div class="flex gap-2 w-full">
        <Button onClick={handleClick}>Fetch data</Button>
      </div>
      <div>
        {err && <h2>{err}</h2>}

        <div class="relative overflow-x-auto my-8">
            <table class="w-full text-sm text-left text-gray-500 dark:text-gray-400">
              <thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
                <tr>
                  <th scope="col" class="px-6 py-3">name</th>
                  <th scope="col" class="px-6 py-3">description</th>
                  <th scope="col" class="px-6 py-3">created_at</th>
                </tr>
              </thead>
              <tbody class="bg-white dark:bg-slate-800">

                {!isLoading && data.data.tasks.length==0 && <tr class="text-center bg-white border-b dark:bg-gray-800 dark:border-gray-700">
                  <td colspan="3" class="py-2">Not data</td></tr>}
                {isLoading && <tr class="text-center bg-white border-b dark:bg-gray-800 dark:border-gray-700">
                  <td colspan="3" class="py-2">Loading...</td></tr>}
                {data.data.tasks.map(person => {
                  return (
                    <tr key={person.id} class="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
                      <td class="py-2">{person.name}</td>
                      <td class="py-2">{person.description}</td>
                      <td class="py-2">{person.createdAt}</td>
                    </tr>
                  );
                })}
              </tbody>
            </table>
        </div>
      </div>
    </div>
  );
}
```

~/routes/index.tsx
```typescript
import { Head } from "$fresh/runtime.ts";

import GqlTable from "../islands/GqlTable.tsx";

export default function Home() {
  return (
    <>
      <Head>
        <title>Fresh App</title>
      </Head>
      <div class="p-4 mx-auto max-w-screen-md">
        <img
          src="/logo.svg"
          class="w-32 h-32"
          alt="the fresh logo: a sliced lemon dripping with juice"
        />
        <p class="my-6">
          Welcome to `fresh`. Try updating this message in the
          ./routes/index.tsx file, and refresh.
        </p>
        <GqlTable />
      </div>
    </>
  );
}

```

![gql.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/645692/3e01abe1-853b-f09b-76da-b0791351cfbc.png)


# その他の情報

## .envはブラウザ側で？使えない

https://deno.com/manual@v1.34.2/basics/env_variables

環境変数を定義するのに.envを使いたかったのだが
NUXTのような動的インポートをサポートしていないらしく
サーバー側でしか使えなかった

なので、deno実行時に直接環境変数を追加するしかない

https://github.com/denoland/fresh/issues/774

## GQLのライブラリはまだ使えない

https://deno.land/x/gql@1.2.3

サンプル通りに実行してもエラーがでるので
こちらのライブラリの利用はやめた

まだ解決はしてない様子(2023/06/20)

https://github.com/deno-libs/gql/issues/18

```
Error: Expected { __validationErrors: undefined, description: undefined, extensions: {}, astNode: { kind: "SchemaDefinition", operationTypes: [Array] }, extensionASTNodes: [], _queryType: Query, _mutationType: undefined, _subscriptionType: undefined, _directives: [@include, @skip, @deprecated, @specifiedBy], _typeMap: { Query: Query, String: String, Boolean: Boolean, __Schema: __Schema, __Type: __Type, __TypeKind: __TypeKind, __Field: __Field, __InputValue: __InputValue, __EnumValue: __EnumValue, __Directive: __Directive, __DirectiveLocation: __DirectiveLocation }, _subTypeMap: {}, _implementationsMap: {} } to be a GraphQL schema.
    at Q (https://esm.sh/v117/graphql@16.6.0/deno/type/schema.js:2:843)
    at W (https://esm.sh/v117/graphql@16.6.0/deno/type/validate.js:2:891)
    at a (https://esm.sh/v117/graphql@16.6.0/deno/graphql.js:2:863)
    at https://esm.sh/v117/graphql@16.6.0/deno/graphql.js:2:483
    at new Promise (<anonymous>)
    at R (https://esm.sh/v117/graphql@16.6.0/deno/graphql.js:2:466)
    at runHttpQuery (https://deno.land/x/gql@1.2.3/common.ts:28:16)
    at eventLoopTick (ext:core/01_core.js:165:11)
    at async https://deno.land/x/gql@1.2.3/http.ts:91:22
    at async handleGraphQL (~/edge/supabase/functions/graphql/index.ts:37:15)
```

# 所感

> 既存プロジェクトをDenoに移行できるか

しようと思えばできるかもしれないが
Nodeのプロジェクトで使っているプラグインが全て使えるわけではないし
その代変えとなるプラグインを探したり実装するのにかなり時間がかかる

Denoは基本がTypeScriptということも踏まえると
フロントエンド的にも学習コストが高め

プライベートで色々開発してテストする分には楽しい
仕事で導入を検討するとなるとフロントエンドだけではなく
チーム全体で学習しなければいけないコストもあり

Denoがとても速いので導入て開発をやってみたいけれど
そこまでの恩恵を受けれる仕組みのプロジェクトはあるのか
と言われると難しい

やはりNodeの情報量が勝る現在
Denoで開発していくことは茨の道になりそうで
二の足を踏んでしまうのが正直なところだ