Riot.jsはtag内でJavaScriptのロジックコードを書くことができます。
スクリプトタグのドキュメントではTypeScriptなど特別なコンパイラが必要なスクリプト以外の純粋なJavaScriptはそのままJavaScriptを書けるようになったようです。
https://riot.js.org/migration-guide/#the-script-tag
とても簡潔でわかりやすいのですがどうしてもTypeScriptでロジックコードは書きたいです。
昨今そこらへんの需要も多いのかちゃんとサンプルも用意されています。
https://github.com/riot/examples/tree/gh-pages/typescript
サンプルのrandom.riotを確認するとまさに私がやりたかったTypeScriptによるロジックの記述がありました。
<random>
..省略...
<script type="ts">
import Logs from '../logs/logs.riot'
import {RandomComponent} from './types'
function Random(): RandomComponent {
return {
state: {
number: null,
logs: []
},
...省略....
</script>
</random>
確かにこれはこれで悪くないのですが個人的にriot内にTypeScriptを書きたくないです。
VueやReactといったフレームワークでもtsxやvue内でTypeScriptを書くんですが、これらのフレームワークではEditor(VsCodeなど)のサポートがちゃんと受けられるような環境が整備されています。がRiot.jsでは少しそこのサポートが少なく、このままriotファイルにTypeScriptを書くのは少ししんどいところがあります。(もちろんコンパイルのチェック走るのでそのままJavaScriptを書く場合とは少し状況が違います。)
TypeScriptファイルとTagファイルを分離する
さてそんな状況もありRiot.jsユーザーの中にはTypeScriptとTagファイルを分けたいって人は他にもいるようでRiot@v3については以下のようなライブラリも作成されておりstarもそこそこ伸びているようです。
https://github.com/nippur72/RiotTS
ただこのライブラリはメンテされているとは言いがたい状況でなおかつRiot@v4ではRiot側が用意する内部的なAPIも大幅に変わっているため同じようには実装できません。
(余談ですが、私はCacooというサービスのフロントエンドの開発を行なっています。Cacooのコードはこのライブラリは使っていませんが、同じような処理でエディタの補助を最大限受けられるような環境を用意しています。)
俺の考えた最強の(まだまだ発展余地のある)Riot@v4でサンプルを作成する
さて上記のような状況なので上のriotの提供しているexampleプロジェクトを自分の思う構成で書き換えてみました。アドベントカレンダーの特性上時間が足りず最強まで考えを練れてないので、同じような問題意識をお持ちの方はぜひアイデアやご指摘いただけたら嬉しいです。
デコレータを作成する
まずはRiotTSのようにデコレータを作成します。デコレーター内で
const clazz: any = element;
const instance = new clazz();
register(
component.name || "",
Object.assign(component, {
exports: {
...instance.__proto__
}
})
);
element.prototype.update = (registered.get(
component.name || ""
) as any).update;
Riotのコンポーネントの登録時にexportsのプロパティに外部からの関数を入れられるのでそこにデコレーターでクラスの関数全てをあらかじめriot側に登録してしまいます。
riot側に登録してしまえばonMountontedなどのライフサイクル関数をTypeScriptファイル側で受けることができます(後述を参照)
全ての関数を登録してしまう理由はイベントハンドラーなどもriot側に登録しておきたかったからです。全て登録が良いのかどうかはまだ良く分かって無いです
(anyが多いのはまぁねぇ。。。)
クラスを用意する
classデコレーターを使いたいのでクラスベースでコンポーネントを作成することになります。以下のように
import { RiotComponent } from "riot";
import { template } from "../riot-ts";
import Random from "./random.riot";
@template(Random)
export class random {
onBeforeMount(
this: RiotComponent<RandomComponentProps, RandomComponentState>
) {
this.state = {
number: null,
logs: []
};
}
riotファイルは読み込んでそのままデコレーターに渡します。するとonBeforeMountがライフサイクルイベントとして呼ばれるので、クラス側にこの関数を用意してここでstateを初期化します。
イベントはどうハンドリングするかというと
clearLogs(this: RiotComponent<RandomComponentProps, RandomComponentState>) {
this.update({
logs: []
});
}
riot側にthisが渡ってくるのでここでthis.updateを呼び出しlogsを更新します。
(現状全てのメソッドのthisはriotになっていますがの読み違いが多発しそうなのでthisはclassのインスタンスを参照させたい)
クラスはあらかじめ読み込んでおく
const scripts = require.context("./", true, /^(?!.*\.d\.ts$).*\.ts$/);
scripts.keys().forEach(scripts);
TSがファイルが読み込まれないことにはデコレーターも何もできないので、main.tsで全てのTypeScriptはあらかじめ読み込んでしまいます。
今回のコードはこちらに置いてあります。
https://github.com/1984weed/riot-typescript-example
まとめ
一旦最初の目標は達成できましたが、まだまだだと思うのでもうちょっとブラッシュアップできたらと思います。