見出し画像

Deno の Web フレームワーク Fresh を試してみた

はじめに

こんにちは!SHIFT DAAE 開発グループ所属の白木です。

JavaScript/TypeScript ランタイム Deno の Web フレームワーク Fresh がリリースされたので、Deno のインストールから Fresh のデプロイまで試してみました!

Deno とは

Deno は JavaScript/TypeScript ランタイムで、標準で TypeScript をサポートしています。

Node.js の開発者であるRyan Dahl氏を中心にNode.js での設計ミスを修正するために開発されました。

Node.js で TypeScript を実行するには ts-node を使うか tsc でコンパイルする必要がありますが、Deno は TypeScript をそのまま実行できます。(もちろん JavaScript も)

また周辺ツール(テストランナー、タスクランナー、フォーマッター)などが標準に組み込まれているため、Deno だけで開発がはじめられるのも良いところです。

Deno - A modern runtime for JavaScript and TypeScript

Fresh とは

そんな Deno で動く Web フレームワーク Fresh がリリースされました。
Fresh には以下のような特徴があります。

  • デフォルトではクライアントに JavaScript は送信しない

  • Islands Architecture という仕組みで必要な分だけ JavaScript を送信する

  • ビルドステップ不要、設定も不要

  • TypeScript サポート

fresh - The next-gen web framework.

Fresh のセットアップ

さっそく Fresh を試していきます!

実行環境

この記事の内容は M1 Mac の macOS 11.6 で実行しています。
ことなる環境の場合は読み替えをお願いします。

Deno のインストール

まずは brew で Deno をインストールします。

$ brew install deno

$ deno --version
deno 1.17.2 (release, x86_64-apple-darwin)
v8 9.7.106.15
typescript 4.5.2

Visual Studio Code ユーザーなので Deno for Visual Studio Code も入れておきます。

Fresh のプロジェクト作成

つづいてfresh-demo-appという名前で Fresh のプロジェクトを作成します。

$ deno run -A -r https://fresh.deno.dev fresh-demo-app
~~~
Check https://fresh.deno.dev/
error: TS2339 [ERROR]: Property 'writable' does not exist on type 'Writer & Closer'.
  await raw.pipeTo(proc.stdin.writable);
                              ~~~~~~~~
    at https://deno.land/x/fresh@1.0.1/src/dev/mod.ts:127:31

おっと、エラーが出ました。

Install Deno CLI version 1.23.0 or higher.

Fresh は Deno CLI 1.23.0 以上に対応しているとのことで、アップデートします。

$ deno upgrade
~~~
Upgraded successfully

$ deno --version
deno 1.23.3 (release, x86_64-apple-darwin)
v8 10.4.132.8
typescript 4.7.4

Deno 1.23.3 になったので、あらためて Fresh のプロジェクトを作成します。

$ deno run -A -r https://fresh.deno.dev fresh-demo-app
~~~
Project created!
`cd fresh-demo-app` to enter to the project directory.
Run `deno task start` to start the development server.

成功です!

開発サーバーの起動

Deno のタスクランナーである deno task コマンドを使ってサーバーを起動します。

deno.json の tasks (Node.js でいう package.json の scripts )に定義されている start コマンドを実行します。

$ cd fresh-demo-app
$ deno task start
~~~
Server listening on http://localhost:8000

http://localhost:8000 にアクセスすると、ページが表示されました!

Fresh のデプロイ

つくった Fresh プロジェクトを Deno Deploy にデプロイします。

Deno DeployDeno の開発元である Deno Land 社が提供しているホスティングサービスです。

GitHub アカウントでサインアップをして、New Project からプロジェクトを作成します。

Deno Deploy は GitHub のリポジトリ or Playground 上で書いたコードからデプロイができます。

今回は Fresh のコードを Push した GitHub リポジトリ(fresh-demo-app)と接続して、以下の設定をします。

  • リポジトリはfresh-demo-appを選択

  • ブランチはmainを選択

  • エントリーファイルは main.tsを選択

  • プロジェクト名(デフォルトはリポジトリ名が入る)を入力

Link をクリックするとデプロイが開始され、しばらく待って View からページが確認できたらデプロイ完了です!

Deno Deploy から割り当てられる URL は https://${PROJECT_NAME}.deno.dev になります。
そのためプロジェクト名は一意な名前になっている必要があり、重複している場合は Link 実行時に This project name is already in use. エラーが発生します。その場合は別のプロジェクト名に変更してください。

デプロイができたので、Fresh の特徴である Islands Architecture について紹介します。

Islands Architecture とは

Islands Architecture はブラウザが読み込む JavaScript のファイルサイズ肥大化に伴うパフォーマンスの課題を解決するためのアプローチで、静的な HTML に対して動的な要素(JavaScript)の必要な分だけを小さく分けて提供する仕組みです。

HTML を海、その上にある小さな動的な要素(JavaScript)を島と見たてて、Islands Architecture と呼ばれています。

ブラウザはバンドルされた大きな JavaScript ファイルを読み込むのではなく、必要な分だけの JavaScript ファイルを読み込むことでページ表示にかかる時間が短くなります。

本当に JavaScript を送信していないのか

Islands Architecture について解説した記事には

dedicated <script> emitted for it that loads the image carousel implementation

と書かれており、表示されるページに動的な要素(JavaScript)が必要な場合のみ script タグを生成して HTML に埋め込んでいると推測できます。

それを確認するために routes/index.tsx で動的なコンポーネントを含む/含まないの2パターンに実装を変更し、レスポンスの HTML が変わっているか検証してみます。

islands/Counter.tsx ではクリックで数値が増減する、動的な処理が実装されています。

// islands/Counter.tsx

import { h } from "preact";
import { useState } from "preact/hooks";
import { IS_BROWSER } from "$fresh/runtime.ts";

interface CounterProps {
  start: number;
}

export default function Counter(props: CounterProps) {
  const [count, setCount] = useState(props.start);
  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount(count - 1)} disabled={!IS_BROWSER}>
        -1
      </button>
      <button onClick={() => setCount(count + 1)} disabled={!IS_BROWSER}>
        +1
      </button>
    </div>
  );
}

動的なコンポーネント(Counter.tsx)を含むパターン

// routes/index.tsx
import { h } from "preact";
import { tw } from "@twind";
import Counter from "../islands/Counter.tsx";

export default function Home() {
  return (
    <div class={tw`p-4 mx-auto max-w-screen-md`}>
      <img
        src="/logo.svg"
        height="100px"
        alt="the fresh logo: a sliced lemon dripping with juice"
      />
      <p class={tw`my-6`}>
        Welcome to `fresh`. Try update this message in the ./routes/index.tsx
        file, and refresh.
      </p>
      <Counter start={3} />
    </div>
  );
}

レスポンスの HTML にはmain.jsとisland-counter.jsが含まれています。
island-counter.jsがCounter.tsx部分の JavaScript として読み込まれていますね。

// レスポンスで返ってくる HTML の head 部分

<head>
  <meta charset="UTF-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <script
    src="/_frsh/js/nej5fywrqbcg/main.js"
    nonce="3a75cc7920364ec39e7d95451f320260"
    type="module"
  ></script>
  <script
    src="/_frsh/js/nej5fywrqbcg/island-counter.js"
    nonce="08f1d3ac4cc343119ea7806d73cf2da2"
    type="module"
  ></script>
  <style id="__FRSH_STYLE">
  </style>
</head>

動的なコンポーネント(Counter.tsx)を含まないパターン

動的なコンポーネントであるCounter.tsxを削除します。

// routes/index.tsx
import { h } from "preact";
import { tw } from "@twind";
- import Counter from "../islands/Counter.tsx";

export default function Home() {
  return (
    <div class={tw`p-4 mx-auto max-w-screen-md`}>
      <img
        src="/logo.svg"
        height="100px"
        alt="the fresh logo: a sliced lemon dripping with juice"
      />
      <p class={tw`my-6`}>
        Welcome to `fresh`. Try update this message in the ./routes/index.tsx
        file, and refresh.
      </p>
-     <Counter start={3} />
    </div>
  );
}

レスポンスの HTML に script タグがなくなりました!

// レスポンスで返ってくる HTML の head 部分

<head>
  <meta charset="UTF-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <style id="__FRSH_STYLE">
  </style>
</head>

静的なページではレンダリングされる HTML から script タグが消え、JavaScript ファイルの読み込みは行われなくなりました!

同じ tsx をあつかう React はデフォルトでは静的なページでも JavaScript(main.js や chunk.js)が読み込まれます。
React でもコード分割をすることで必要な JavaScript のみを読み込むことは可能ですが、この挙動(Islands Architecture)をデフォルトでフレームワークが取り入れている点に Fresh の思想を感じました。

まとめ

Deno の Web フレームワークである Fresh を紹介しました。
Deno は標準で TypeScript の実行がサポートされているため、TypeScript に関する設定なしでプロジェクトが始められるのが良いですね!

おなじく JavaScript/TypeScript ランタイムで速さが売りのBun も v0.1 がリリースされランタイム界隈が盛り上がってきています。

すぐに本番投入とはいかないですが、プロトタイプの実装などで使えるチャンスを探しつつ情報を追いかけていきます。

\もっと身近にもっとリアルに!DAAE公式Twitter/


執筆者プロフィール: Shoya Shiraki
ソフトウェアエンジニアとしてソーシャルゲーム開発、スタートアップでのCTO経験を経てSHIFTに入社。 テクノロジーを人に最適化するをモットーに、日々楽しんで開発しています。

お問合せはお気軽に
https://service.shiftinc.jp/contact/

SHIFTについて(コーポレートサイト)
https://www.shiftinc.jp/

SHIFTのサービスについて(サービスサイト)
https://service.shiftinc.jp/

SHIFTの導入事例
https://service.shiftinc.jp/case/

お役立ち資料はこちら
https://service.shiftinc.jp/resources/

SHIFTの採用情報はこちら
https://recruit.shiftinc.jp/career/

みんなにも読んでほしいですか?

オススメした記事はフォロワーのタイムラインに表示されます!