Elysia.jsを始める【Context】
※ 主に 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とのやり取りを行うためのグローバルな可変シグナルストア。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パターン
state
、decorate
は、key-valueパターン
、objectパターン
、remapパターン
全てのパターンでプロパティを設定・追加できる。
derive
は、remapパターン
のみ利用できる。
key-valueパターン
state
とobject
で使う。
名前の通り、キーと値のペアで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_miko
のstore.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()
の第一引数として利用できるものは、error
、model
となっている。
キーワード | 対象となるメソッド |
---|---|
decorator | decorate() |
state | state() |
all | 全て |
※ error
、model
に関してはここでは入れていない。
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