第1章:Webアプリケーションの進化
はじめに
ft_transcendenceは、42 Common Coreの集大成プロジェクトです。フルスタックWebアプリケーションとして、リアルタイムPongゲーム、ユーザー認証、チャット機能を実装します。本章では、Web技術の歴史からモダンSPAアーキテクチャまでを学びます。
---
1. Webの歴史
1.1 World Wide Webの誕生
1989年、CERN:
Tim Berners-Lee が WWW を提案
"Information Management: A Proposal"
目的:
- 研究者間の情報共有
- ハイパーテキストによるリンク
- 分散型ドキュメントシステム
構成要素:
1. HTML (HyperText Markup Language)
2. HTTP (HyperText Transfer Protocol)
3. URL (Uniform Resource Locator)
4. ブラウザ (WorldWideWeb, 後の Nexus)
1.2 Web 1.0 — 静的Webの時代
1990年代:
特徴:
- 静的HTMLページ
- 読み取り専用
- CGIによる動的生成
- テーブルレイアウト
技術スタック:
- HTML 2.0 / 3.2 / 4.0
- CGI (Perl, C)
- Apache HTTP Server
- GIF アニメーション
典型的なサイト:
- 企業ホームページ
- 掲示板 (BBS)
- ディレクトリ (Yahoo!)
1.3 Web 2.0 — インタラクティブWebへ
2000年代:
特徴:
- ユーザー生成コンテンツ
- 双方向コミュニケーション
- AJAX (Asynchronous JavaScript and XML)
- リッチなUI
技術革新:
2004: Gmail (AJAX活用)
2005: Google Maps (動的地図)
2005: YouTube
2006: Twitter
2006: jQuery
AJAXの衝撃:
XMLHttpRequest (XHR) により
ページ遷移なしでサーバー通信可能に
var xhr = new XMLHttpRequest();
xhr.open('GET', '/api/data');
xhr.onload = function() {
console.log(xhr.responseText);
};
xhr.send();
1.4 モダンWeb — SPA時代
2010年代以降:
特徴:
- Single Page Application (SPA)
- コンポーネントベース開発
- 仮想DOM
- 状態管理
主要フレームワーク:
2010: Backbone.js
2010: AngularJS
2013: React (Facebook)
2014: Vue.js
2016: Angular 2+
2020+: Svelte, SolidJS
SPAの原理:
1. 初回: HTML + JavaScript バンドルをロード
2. 以降: JavaScript がページを動的に更新
3. サーバーとは API で通信
---
2. モダンWebアーキテクチャ
2.1 フロントエンドとバックエンドの分離
従来(モノリシック):
+---------------------------+
| サーバー |
| +---------------------+ |
| | テンプレート生成 | |
| | (PHP, Rails, etc.) | |
| +---------------------+ |
| | ビジネスロジック | |
| +---------------------+ |
| | データベース | |
| +---------------------+ |
+---------------------------+
↓
HTML ページ
モダン(分離型):
+-------------+ +-------------+
| フロント | ←API→ | バックエンド |
| (SPA) | | (REST/GraphQL)|
+-------------+ +-------------+
↓ ↓
ブラウザ データベース
利点:
- 独立した開発・デプロイ
- スケーラビリティ
- モバイルアプリと同じAPIを共有
2.2 RESTful API
REST (Representational State Transfer):
Roy Fielding 博士の論文 (2000年)
原則:
1. ステートレス
2. リソース指向
3. 統一インターフェース
4. クライアント-サーバー分離
HTTPメソッドとCRUD:
+--------+---------+------------------+
| Method | CRUD | Example |
+--------+---------+------------------+
| GET | Read | GET /users/1 |
| POST | Create | POST /users |
| PUT | Update | PUT /users/1 |
| PATCH | Partial | PATCH /users/1 |
| DELETE | Delete | DELETE /users/1 |
+--------+---------+------------------+
レスポンス例:
GET /api/users/1
{
"id": 1,
"username": "player1",
"email": "player1@example.com",
"avatar": "/avatars/1.png",
"wins": 10,
"losses": 5,
"created_at": "2024-01-15T10:30:00Z"
}
2.3 WebSocket
HTTP vs WebSocket:
HTTP:
クライアント → リクエスト → サーバー
クライアント ← レスポンス ← サーバー
(毎回接続を確立)
WebSocket:
クライアント ← → サーバー
(永続的な双方向通信)
ハンドシェイク:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
用途:
- リアルタイムチャット
- オンラインゲーム
- ライブ通知
- 株価フィード
2.4 OAuth 2.0
OAuth 2.0:
認可のための業界標準プロトコル
ロール:
1. Resource Owner: ユーザー
2. Client: アプリケーション
3. Authorization Server: 認可サーバー (42 API)
4. Resource Server: リソースサーバー
Authorization Code Flow:
+--------+ +---------------+
| |--(A)- Authorization Request ->| Resource |
| | | Owner |
| |<-(B)-- Authorization Grant ---| |
| | +---------------+
| |
| | +---------------+
| |--(C)-- Authorization Grant -->| Authorization |
| Client | | Server |
| |<-(D)----- Access Token -------| |
| | +---------------+
| |
| | +---------------+
| |--(E)----- Access Token ------>| Resource |
| | | Server |
| |<-(F)--- Protected Resource ---| |
+--------+ +---------------+
42 APIの例:
1. ユーザーを https://api.intra.42.fr/oauth/authorize にリダイレクト
2. ユーザーが承認
3. コールバックURLに認可コードが返される
4. 認可コードをアクセストークンに交換
5. アクセストークンでユーザー情報を取得
---
3. ft_transcendence要件
3.1 必須機能
1. ユーザー管理:
- OAuth 2.0 認証 (42 API)
- プロフィール
- アバター
- 二要素認証 (2FA)
- フレンド機能
- ステータス (オンライン/オフライン)
2. チャット:
- チャンネル (公開/非公開/パスワード保護)
- ダイレクトメッセージ
- ブロック機能
- 管理者権限
3. Pongゲーム:
- リアルタイムマルチプレイヤー
- マッチメイキング
- カスタマイズオプション
4. インフラ:
- Docker Compose で全サービス構築
- HTTPS (TLS)
- SPA (Single Page Application)
3.2 技術選択
バックエンド:
- NestJS (TypeScript)
- Django (Python)
- Ruby on Rails
- 任意のフレームワーク
フロントエンド:
- 純粋JavaScript/TypeScript
- フレームワーク任意
データベース:
- PostgreSQL (必須)
その他:
- Docker / Docker Compose
- WebSocket
- OAuth 2.0
3.3 ディレクトリ構造
ft_transcendence/
├── docker-compose.yml
├── .env
├── Makefile
├── backend/
│ ├── Dockerfile
│ ├── package.json (or requirements.txt)
│ ├── src/
│ │ ├── main.ts
│ │ ├── app.module.ts
│ │ ├── auth/
│ │ ├── users/
│ │ ├── chat/
│ │ ├── game/
│ │ └── common/
│ └── test/
├── frontend/
│ ├── Dockerfile
│ ├── package.json
│ ├── src/
│ │ ├── index.html
│ │ ├── main.ts
│ │ ├── components/
│ │ ├── views/
│ │ ├── services/
│ │ └── styles/
│ └── public/
├── database/
│ ├── Dockerfile (optional)
│ └── init.sql
└── nginx/
├── Dockerfile
└── nginx.conf
---
4. 技術スタック詳解
4.1 TypeScript
// JavaScriptに型を追加
// Anders Hejlsberg (C# 設計者) による開発
// 型安全性
interface User {
id: number;
username: string;
email: string;
avatar?: string; // オプショナル
}
// 型チェック
function getUser(id: number): User {
// コンパイル時に型エラーを検出
return {
id: id,
username: "player1",
email: "player1@example.com"
};
}
// ジェネリクス
interface ApiResponse<T> {
data: T;
status: number;
message: string;
}
// 使用例
const response: ApiResponse<User[]> = await fetchUsers();
4.2 NestJS
// Node.js のエンタープライズフレームワーク
// Angular に着想を得たアーキテクチャ
// モジュール
@Module({
imports: [TypeOrmModule.forFeature([User])],
controllers: [UsersController],
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}
// コントローラー
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get()
findAll(): Promise<User[]> {
return this.usersService.findAll();
}
@Get(':id')
findOne(@Param('id') id: string): Promise<User> {
return this.usersService.findOne(+id);
}
@Post()
create(@Body() createUserDto: CreateUserDto): Promise<User> {
return this.usersService.create(createUserDto);
}
}
// サービス
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private usersRepository: Repository<User>,
) {}
findAll(): Promise<User[]> {
return this.usersRepository.find();
}
}
4.3 PostgreSQL
-- リレーショナルデータベース
-- ACID 準拠
-- JSON サポート
-- ユーザーテーブル
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
intra_id INTEGER UNIQUE,
avatar_url VARCHAR(255),
two_factor_enabled BOOLEAN DEFAULT FALSE,
two_factor_secret VARCHAR(255),
status VARCHAR(20) DEFAULT 'offline',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- ゲーム履歴
CREATE TABLE game_history (
id SERIAL PRIMARY KEY,
player1_id INTEGER REFERENCES users(id),
player2_id INTEGER REFERENCES users(id),
player1_score INTEGER NOT NULL,
player2_score INTEGER NOT NULL,
winner_id INTEGER REFERENCES users(id),
played_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- フレンド関係
CREATE TABLE friendships (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id),
friend_id INTEGER REFERENCES users(id),
status VARCHAR(20) DEFAULT 'pending',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(user_id, friend_id)
);
-- チャットメッセージ
CREATE TABLE messages (
id SERIAL PRIMARY KEY,
channel_id INTEGER REFERENCES channels(id),
sender_id INTEGER REFERENCES users(id),
content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
4.4 WebSocket (Socket.io)
// サーバーサイド (NestJS)
@WebSocketGateway({
cors: {
origin: '*',
},
})
export class GameGateway {
@WebSocketServer()
server: Server;
@SubscribeMessage('joinGame')
handleJoinGame(
@MessageBody() data: { gameId: string },
@ConnectedSocket() client: Socket,
) {
client.join(data.gameId);
this.server.to(data.gameId).emit('playerJoined', {
playerId: client.id,
});
}
@SubscribeMessage('paddleMove')
handlePaddleMove(
@MessageBody() data: { gameId: string; y: number },
@ConnectedSocket() client: Socket,
) {
client.to(data.gameId).emit('opponentMove', { y: data.y });
}
}
// クライアントサイド
const socket = io('http://localhost:3000');
socket.on('connect', () => {
console.log('Connected to server');
socket.emit('joinGame', { gameId: 'room-123' });
});
socket.on('playerJoined', (data) => {
console.log('Player joined:', data.playerId);
});
socket.on('opponentMove', (data) => {
updateOpponentPaddle(data.y);
});
---
5. 開発環境セットアップ
5.1 プロジェクト初期化
# プロジェクトディレクトリ作成
mkdir ft_transcendence
cd ft_transcendence
# バックエンド (NestJS)
npm i -g @nestjs/cli
nest new backend
cd backend
npm install @nestjs/websockets @nestjs/platform-socket.io
npm install @nestjs/typeorm typeorm pg
npm install @nestjs/passport passport passport-42
# フロントエンド
mkdir frontend
cd frontend
npm init -y
npm install typescript webpack webpack-cli webpack-dev-server
npm install socket.io-client
5.2 docker-compose.yml
services:
nginx:
build: ./nginx
ports:
- "443:443"
depends_on:
- backend
- frontend
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
backend:
build: ./backend
expose:
- "3000"
depends_on:
- db
environment:
- DATABASE_URL=postgresql://user:pass@db:5432/transcendence
- INTRA_CLIENT_ID=${INTRA_CLIENT_ID}
- INTRA_CLIENT_SECRET=${INTRA_CLIENT_SECRET}
- JWT_SECRET=${JWT_SECRET}
frontend:
build: ./frontend
expose:
- "80"
db:
image: postgres:15-alpine
volumes:
- db_data:/var/lib/postgresql/data
- ./database/init.sql:/docker-entrypoint-initdb.d/init.sql
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=pass
- POSTGRES_DB=transcendence
volumes:
db_data:
networks:
default:
name: transcendence
5.3 Makefile
NAME = transcendence
all: build up
build:
docker-compose build
up:
docker-compose up -d
down:
docker-compose down
logs:
docker-compose logs -f
shell-backend:
docker-compose exec backend sh
shell-db:
docker-compose exec db psql -U user -d transcendence
clean: down
docker system prune -af
fclean: clean
docker volume rm -f $(docker volume ls -q) 2>/dev/null || true
re: fclean all
.PHONY: all build up down logs shell-backend shell-db clean fclean re
---
6. SPAの基礎
6.1 ルーティング
// クライアントサイドルーティング
// History API を使用
class Router {
constructor() {
this.routes = new Map();
window.addEventListener('popstate', () => this.handleRoute());
}
addRoute(path, handler) {
this.routes.set(path, handler);
}
navigate(path) {
history.pushState(null, '', path);
this.handleRoute();
}
handleRoute() {
const path = window.location.pathname;
const handler = this.routes.get(path) || this.routes.get('/404');
handler();
}
}
// 使用例
const router = new Router();
router.addRoute('/', () => renderHome());
router.addRoute('/game', () => renderGame());
router.addRoute('/profile', () => renderProfile());
router.addRoute('/chat', () => renderChat());
router.addRoute('/404', () => render404());
// ナビゲーション
document.querySelectorAll('a').forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
router.navigate(link.getAttribute('href'));
});
});
6.2 コンポーネント設計
// シンプルなコンポーネントシステム
class Component {
constructor(props = {}) {
this.props = props;
this.state = {};
}
setState(newState) {
this.state = { ...this.state, ...newState };
this.render();
}
render() {
throw new Error('render() must be implemented');
}
}
// 例: ユーザーカード
class UserCard extends Component {
constructor(props) {
super(props);
this.state = { loading: true };
}
async connectedCallback() {
const user = await fetchUser(this.props.userId);
this.setState({ loading: false, user });
}
render() {
if (this.state.loading) {
return '<div class="loading">Loading...</div>';
}
const { user } = this.state;
return `
<div class="user-card">
<img src="${user.avatar}" alt="${user.username}">
<h3>${user.username}</h3>
<p>Wins: ${user.wins} | Losses: ${user.losses}</p>
<span class="status ${user.status}">${user.status}</span>
</div>
`;
}
}
6.3 状態管理
// シンプルな状態管理 (Redux風)
class Store {
constructor(reducer, initialState = {}) {
this.reducer = reducer;
this.state = initialState;
this.listeners = [];
}
getState() {
return this.state;
}
dispatch(action) {
this.state = this.reducer(this.state, action);
this.listeners.forEach(listener => listener(this.state));
}
subscribe(listener) {
this.listeners.push(listener);
return () => {
this.listeners = this.listeners.filter(l => l !== listener);
};
}
}
// リデューサー
function appReducer(state, action) {
switch (action.type) {
case 'SET_USER':
return { ...state, user: action.payload };
case 'UPDATE_GAME_STATE':
return { ...state, game: action.payload };
case 'ADD_MESSAGE':
return {
...state,
messages: [...state.messages, action.payload]
};
default:
return state;
}
}
// 使用
const store = new Store(appReducer, {
user: null,
game: null,
messages: []
});
store.subscribe((state) => {
console.log('State updated:', state);
renderApp(state);
});
store.dispatch({ type: 'SET_USER', payload: { id: 1, name: 'Player1' } });
---
まとめ
本章で学んだこと:
- Webの歴史: Web 1.0 から SPA へ
- モダンアーキテクチャ: フロント/バック分離
- REST API: リソース指向設計
- WebSocket: リアルタイム双方向通信
- OAuth 2.0: 認可フロー
- 技術スタック: TypeScript, NestJS, PostgreSQL
- SPA基礎: ルーティング、コンポーネント、状態管理
次章では、システムアーキテクチャの詳細設計を学びます。