🦄

Elysia.jsを始める【Context】

2024/09/14に公開

※ 主に https://elysiajs.com/essential/context の日本語訳をしつつ、多少わかりやすい表現に直していたり、独自で検証していたりしたものになっているので、詳細は公式ページを参照してください

Elysia.jsのContextは各リクエストごとにユニークになっている。
基本、他のリクエストと共有されない。
ただし、storeはグローバルな可変状態として共有されるようになっている。

ElysiaのContextの構成要素

path

リクエストのパス名を表す。
ex)/user/123のようなURLパスがそれにあたる

body

HTTPメッセージのbody部分。フォームデータやファイルアップロードの内容を含むことがある。

query

クエリ文字列をJavaScriptのオブジェクトとして扱う。

params

ElysiaのパスパラメータをJavaScriptオブジェクトとして扱う。
ex)/user/:idのようなパスから取得されるパラメータの:idなどがそれにあたる。

headers

HTTPヘッダー。リクエストに関する追加情報(User-Agent, Content-Type, Cache-Controlなど)が含まれる。

request

Web標準のRequestオブジェクトを表す。リクエスト全体の詳細を含む。

redirect

レスポンスを別のURLにリダイレクトするための関数。

store

Elysiaインスタンス全体で共有されるグローバルな可変ストア。

Cookieとのやり取りを行うためのグローバルな可変シグナルストア。Cookieの取得や設定が可能。

set

レスポンスに適用するプロパティを設定。

status

set.statusでHTTPステータスコードを設定する。デフォルトは200。

headers

set.headersでレスポンスヘッダーを設定できる。。

redirect

set.redirectでリダイレクト先のパスを設定できる。

error

カスタムステータスコードを返すための関数。

Contextの拡張

Elysiaでは、特定のニーズに合わせてContextをカスタマイズできるようになっている。
公式のカスタマイズ例としては、

  • ユーザーIDを変数として抽出
  • 共通パターンのリポジトリを注入する
  • データベースの接続情報を追加する

としている。

ContextカスタマイズのためのAPI

| state | Context.storeにグローバルな可変オブジェクトを作成 |
| decorate | Contextに追加の関数やプロパティを割り当てたい時に利用 |
| derive / resolve | 既存のプロパティに基づいて、リクエストごとに一意の追加プロパティを作成 |

State

グローバルな変更可能オブジェクトを作成したい時に利用する。
ReactやVueなどにおけるグローバルステートの管理に似たような機能と考えると良さそう。

import { Elysia } from 'elysia'

const app = new Elysia()
  .state('belonging', 'hololive')
  .get('/sakura_miko', ({ store: belonging }) => belonging)
  .get('/oozora_subaru', ({ store }) => store)
  .get('/shirakami_fubuki', () => 'konkon fox!!')
  .listen(3000)

上記例だと、

path response
/sakura_miko {"belonging":"hololive"}
/oozora_subaru {"belonging":"hololive"}
/shirakami_fubuki konkon fox!!

といった、感じのレスポンスになる。

state()より前にstoreから呼び出した場合は?

import { Elysia } from 'elysia'

const app = new Elysia()
  .get('/sakura_miko', ({ store }) => store.belonging)
  .state('belonging', 'hololive')
  .get('/oozora_subaru', ({ store }) => store)
  .get('/shirakami_fubuki', () => 'konkon fox!!')
  .listen(3000)

上記のように、/sakura_mikoのパスのようなstate()より前に定義したルーティングで、storeを呼び出した場合、
IDE上では、「プロパティ 'belonging' は型 '{}' に存在しません。」のようにエラーメッセージが出る。

ただし、「動かないか?」でいうと動く。

/sakura_mikoでも、ブラウザにはhololiveと表示される。

Decorate

daecorate(デコレート)は、直接Contextにプロパティを追加することができる。
Stateとの違いは、値が読み取り専用で後で再割り当てされないことにある。
用いるシーンとしては、すべてのハンドラーに、決まった動作のする関数を定義したり、不変なプロパティを割り当てるなど...。

import { Elysia } from 'elysia'

class Logger {
  log(value: string) {
    console.log(value)
  }
}

const app = new Elysia()
  .decorate('logger', new Logger())
  .get('/sakura_miko', ({ logger }) => {
    logger.log('Nyahello〜!!')
    return 'Nyahello〜!!'
  })
  .get('/oozora_subaru', ({ logger }) => {
    logger.log('Ajimaruyo〜')
    return 'Ajimaruyo〜'
  })
  .listen(3000)

上記例だと、

path ブラウザ コンソール画面
/sakura_miko Nyahello〜!! Nyahello〜!!
/oozora_subaru Ajimaruyo〜 Ajimaruyo〜

といったような表示+出力になる。

実際のブラウザ画像コンソール画面

/sakura_mikoの場合

oozora_subaruの場合

Derive

リクエストが発生する際に、decorateのようにContextにプロパティを割り当てることができる。
decorateと異なるのは、リクエストヘッダーや、リクエストボディ、クエリにアクセスできるため、リクエストヘッダーから情報を取得して利用することなどができる。

const app = new Elysia()
  .state('NyaHelloCounter', () => {
    console.log('exec state of NyaHelloCounter')
    return 0
  })
  .derive(({ headers }) => {
    const powerdBy = headers['x-powerd-by']

    return {
      powerdBy: powerdBy
    }
  })
  .get('/sakura_miko', ({ sotre, powerdBy }) => {
    return `x-powerd-by: ${powerdBy} NyaHello count: ${sotre.NyaHelloCounter++}`
  })
  .listen(3000)

上記の例だと、まず、Postmanなどのツールで、リクエストヘッダーにx-powerd-byの設定をする(今回はx-powerd-byの値をNya-Helloにする)。
その後、/sakura_mikoをツールを実行してみると、レスポンスでNya-Helloが取得できる。

derive、state、decorateの違い

ざっくりだが、derive、state、decorateの違いは以下

特徴 derive state decorate
実行タイミング 各リクエストの処理時 アプリケーション起動時 アプリケーション初期化時
スコープ リクエスト固有 グローバル グローバル
リクエスト間の値の共有 共有されない 全てのリクエストで共有可能 全てのリクエストで共有可能
状態の保持 - グローバルで状態の保持が可能 -
リクエスト情報にアクセス可能か? 可能(headers, query, body) 不可 不可

簡単なコードで簡易的に比べてみる。

const app = new Elysia()
  .state('NyaHelloCounterState', 0)
  .decorate('NyaHelloCounterDecorate', 0) // stateとの比較のため、counterとしてあえて定義
  .derive(({ headers }) => {
    const powerdBy = headers['x-powerd-by']

    return {
      powerdBy: powerdBy
    }
  })
  .get('/sakura_miko', ({ store, NyaHelloCounterDecorate, powerdBy }) => {
    store.NyaHelloCounterState++    
    NyaHelloCounterDecorate++    

    return `x-powerd-by: ${powerdBy} stateCounter: ${store.NyaHelloCounterState} decorateCounter: ${NyaHelloCounterDecorate}`
  })
  .listen(3000)

上記のコードをPostmanで3回実行してみると、

上の画像のような結果となった。
画像を見てわかる通り、stateで定義したカウンターとdecorateでそれっぽく定義したカウンターの出力結果を見ると、
stateの方はカウントアップしていくが、decorateの方はカウントアップせず、1のままの出力となっていた。

Contextのプロパティ追加のパターン

ElysiaのContextの設定パターンは3種類ある

  • key-valueパターン
  • objectパターン
  • remapパターン

statedecorateは、key-valueパターンobjectパターンremapパターン全てのパターンでプロパティを設定・追加できる。
deriveは、remapパターンのみ利用できる。

key-valueパターン

stateobjectで使う。
名前の通り、キーと値のペアでContextにプロパティを追加する。

const app = new Elysia()
  .state('NyaHelloCounter', 0)
  .decorate('NyaHelloLogger', new Logger())

objectパターン

複数のプロパティを一度に設定したい時に利用する。

import { Elysia } from 'elysia'

class Logger {
  log(value: string) {
    console.log(value)
  }
}

const app = new Elysia()
  .state('NyaHelloCounterState', 0)
  .decorate({
    NyaHelloLogger: new Logger(),
    greeting: 'Nya Hello ~'
  })
  .get('/sakura_miko', ({ NyaHelloLogger, greeting }) => {
    NyaHelloLogger.log('Nya Hello ~~~')
    return greeting
  })
  .listen(3000)

remapパターン

関数を使って既存の値から新しい値を作成したり、プロパティの名前を変更したりする方法。
ただし、Vue.jsやReactで提供されている、リアクティブシステムはremapでは提供されず、初期値の割り当てのみを行う方法となる。そのため、remapパターンでContextのプロパティ設定をした場合、動的な値の更新はできない。

import { Elysia } from 'elysia'

const app = new Elysia()
  .state('greeting', 'Nya Hello ~~~')
  .state(({ greeting, ...store }) => ({
    ...store,
    newGreeting: 'Ajimaru !!'
  }))
  .get('/oozora_subaru', ({ store }) => {  
    return store.newGreeting
  })
  .get('/sakura_miko', ({ store }) => {  
    console.log(store.greeting)
    return store.greeting
  })
  .listen(3000)

上記例だと、IDEで/sakura_mikostore.greetingの部分でエラーが出るかと思われる。
ただ、IDE上でエラーとなるだけで動作はする。
console.log()の出力は、undefinedとなる。

Affix

プラグインとして追加するプロパティを効率的にリマップするための機能。
利用シーンとしては、

  • 多くのプロパティを持つプロパティの名前衝突の回避
  • 一度に複数のプロパティのリマッピング

あたりになる。

prefixパターンとsuffixパターンがある。

prefixでリマップ

import { Elysia } from 'elysia'

const greetingSetup = new Elysia({ name: 'greeting'})
  .decorate({
    subaru: 'Ajimaru! Ajimaru!',
    miko: 'Nya Hello ~~~',
    fubuki: 'Kon Kon Fox!!'
  })

const app = new Elysia()
  .use(
    greetingSetup.prefix('decorator', 'greeting')
  )
  .get('/oozora_subaru', ({ greetingSubaru }) => {
    return greetingSubaru
  })
  .get('/sakura_miko', ({ greetingMiko }) => {  
    return greetingMiko
  })
  .get('/shirakami_fubuki', ({ greetingFubuki }) => {
    return greetingFubuki
  })
  .listen(3000)

上記例では、prefixメソッドを使ってプロパティ名にprefixを付与してリマッピングしている。

decoratorキーワードとは、なんだ?

prefix()を利用した時の第一引数である、decoratorというキーワード。
これは、decorate()で追加されたプロパティのみを離マップの対象とすることを示している。

state()追加されたプロパティをリマップするときは、stateキーワードを使う。
derive()は対象とならないので、利用できるキーワードはない。

全てを対象としたい場合は、allというキーワードを使うこともできる。

その他、prefix()の第一引数として利用できるものは、errormodelとなっている。

キーワード 対象となるメソッド
decorator decorate()
state state()
all 全て

errormodelに関してはここでは入れていない。

suffixでリマップ

公式では、prefix()の例でしか説明がないが、suffix()でもリマップできる。

import { Elysia } from 'elysia'

const greetingSetup = new Elysia({ name: 'greeting'})
  .decorate(
    {
      subaru: 'Ajimaru! Ajimaru!',
      miko: 'Nya Hello ~~~',
      fubuki: 'Kon Kon Fox!!'
    }
  )

const app = new Elysia()
  .use(
    greetingSetup.suffix('decorator', 'Greeting')
  )
  .get('/oozora_subaru', ({ subaruGreeting }) => {
    return subaruGreeting
  })
  .get('/sakura_miko', ({ mikoGreeting }) => {  
    return mikoGreeting
  })
  .get('/shirakami_fubuki', ({ fubukiGreeting }) => {
    return fubukiGreeting
  })
  .listen(3000)

prefix()と利用の仕方はほぼ変わらず、suffix()の第一引数にキーワード、第二引数にsuffixとして使いたいキーワードを入れる。
なお、第二引数のsuffixとして利用したいキーワードの頭文字は大文字でなくてもOK。
greetingSetup.suffix('decorator', 'greeting')であっても、subaruGreetingのようにElysiaの方で頭文字を大文字として変換してくれる(subarugreetingでは機能しない)。

Reference and Value(参照と値)

Elysiaでは、stateの変更の際は、参照を使用して変更をすることを推奨している。

import { Elysia } from 'elysia'


const app = new Elysia()
  .state('NyahelloCounter', 0)
  .get('/sakura_miko', ({ store }) => {
    return store.NyahelloCounter++
  })
  .get('/miko_da_nye', ({ store: { NyahelloCounter } }) => {  
    return NyahelloCounter++
  })
  .listen(3000)

/sakura_mikoでブラウザアクセスすると、最初に0が表示される。

その後、リロードするとカウントアップしていく。

リロードで値が3になったら、/miko_da_nyeにアクセスしてみると...

return NyahelloCounter++になっているので4になるが、そのあと/miko_da_nyeでリロードしてもカウントアップしないのが確認できる。

このあたりは、JavaScriptの「参照値渡し」に紐つく話なので、詳細はそちらに譲ります。

Discussion