はじめに
Day 5では、FastAPIによるAPIハブの設計とインスタンス化の方針について書きました。
今回はフロントエンドの技術選定と、クラスベースのコンポーネント設計について書きます。なぜReactを使わなかったのか、共通コンポーネントをどのように設計したか、バックエンドとの連携方法の判断を記録します。
なお、機能コンポーネント実装については全容をまとめた別記事として後日公開する予定のため、今回は取り上げていません。
Reactを使用しない理由
今回のツールの要件を見れば、Reactを使用する方が学習面からも機能面からも優れていると考えています。それでも使わない理由は3つあります。
1. バイナリサイズの問題
Reactをバイナリに含めた場合、容量が増加し軽量なツール作成という目的から外れてしまいます。
2. 学習コストの問題
フロントエンドの開発は今回が初挑戦です。Reactを使用するとTypeScriptの学習とフレームワークの学習を同時に行うことになり、学習コストが高くなると判断しました。
3. 要件に対して過剰である
今回のツールはVS CodeのUIをモデルにしています。サイドバー・ヘッダー・メインエリアという構成を再現することが要件であり、この目的に対してReactは過剰と判断しました。クラスベースで責務ごとに明示的に管理する方針の方が、今回の規模には適しています。
共通コンポーネントの責務
Day 5で触れた通り、今回のツールは共通コンポーネントと機能コンポーネントに分かれています。
共通コンポーネントは以下の6つで構成されています。
| コンポーネント | 役割 |
|---|---|
| Header | 画面レイアウトの調整・選択中の機能を表示 |
| Sidebar | 機能を登録し選択する起点 |
| MainArea | 選択した機能コンポーネントを配置する領域 |
| Footer | バックエンドのログを出力 |
| StatisticsArea | NN機能で演算した統計データを表示(予定) |
| GraphArea | 進捗グラフを表示(予定) |
StatisticsAreaとGraphAreaは現時点では未実装ですが、NN機能の開発完了後に組み込む想定でレイアウト上の領域として確保しています。
各コンポーネントはクラスとして定義し、render()メソッドでDOMへの追加を明示的に管理しています。Headerクラスの実装は以下の通りです。
import { MenuBar } from "./menu";
export class Header{
header: HTMLHeadElement;
functionName?: HTMLDivElement;
menubar?: HTMLElement;
constructor(functionName: string){
// header
this.header = document.createElement('header');
this.header.classList.add('header');
// childElements
if(this.header){
// functionName
this.functionName = document.createElement('div')
this.functionName.classList.add('function-name')
this.functionName.textContent = functionName;
this.functionName.textContent = functionName;
this.functionName.style.textAlign = 'center'
// menubar
this.menubar = document.createElement('ul');
this.menubar.classList.add('menu')
this.menubar.style.alignItems = 'left'
this.menubar.textContent = 'test'
}
}
render(){
const container = document.querySelector('.app-container');
const menu = new MenuBar()
this.menubar?.appendChild(menu.build())
if(this.menubar){
this.header?.append(this.menubar);
}
if(this.functionName){
this.header?.append(this.functionName);
}
container?.append(this.header)
}
};
コンストラクタでDOM要素を生成し、render()を呼び出した時点で画面に反映される設計です。各コンポーネントが自身のDOM操作を完結して持つため、他のコンポーネントに影響を与えません。
API連携方法
バックエンドとの通信にはSwaggerのAPI自動生成を使用しています。
当初はfetchを直接記述する方法で実装していましたが、バックエンドの変更がそのままフロントエンドに影響するという独立したツールの性質上、手動でのfetch実装では人的ミスが発生しやすい状況でした。
FastAPIが自動生成するSwagger定義からswagger-typescript-apiを用いてTypeScriptのAPIクライアントを生成することで、エンドポイントの変更をフロント側に確実に反映できるようになりました。
生成されたファイルの冒頭は以下の通りです。
/* eslint-disable */
/* tslint:disable */
// @ts-nocheck
/*
* ---------------------------------------------------------------
* ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ##
* ## ##
* ## AUTHOR: acacode ##
* ## SOURCE: https://github.com/acacode/swagger-typescript-api ##
* ---------------------------------------------------------------
*/
実際の呼び出しはエンドポイントごとにメソッドとして生成されます。timerの例は以下の通りです。
timer = {
timerStartTimerStartGet: (params: RequestParams = {}) =>
this.request<any, any>({
path: `/timer/start`,
method: "GET",
format: "json",
...params,
}),
timerEndTimerStopPost: (params: RequestParams = {}) =>
this.request<any, any>({
path: `/timer/stop`,
method: "POST",
format: "json",
...params,
}),
};
手動でfetchを書く場合と比べ、パスの誤記やメソッドの取り違えといったミスを防ぐことができます。
おわりに
今日はフロントエンドの技術選定とコンポーネント設計について書きました。
Reactを使わない選択は制約ではなく、今回のツールの規模と目的に照らした判断です。クラスベースで責務を明示的に分けることで、各コンポーネントの役割が把握しやすい構成になっています。
Day 7では1週間の振り返りと今後の開発方針について書く予定です。連載を通じて気づいたこと、これから実装する機能、完全ローカル・自力実装を続ける意味を記録します。
この記事は連載「クラウドに依存しないマイルストーン管理ツール開発記」のDay 6です。
- Day 1 - なぜ自作するのか
- Day 2 - 技術スタックとその選定理由
- Day 3 - Pythonで時間記録機能を作る
- Day 4 - タスク管理のデータ構造
- Day 5 - FastAPIによるAPIハブ設計
- Day 7 - 1週間の振り返りと今後の方針(予定)