こんにちは!咲耶です〜
Windsurf完全マスター講座Part4へようこそ!今回は、一歩上のプロフェッショナルな開発技術について学んでいきます。
Part3で実用的なアプリが作れるようになった皆さんに、今度は「より高品質で、より高速で、より保守しやすい」コードの書き方をお教えします〜💕
「作ったアプリをもっと高速にしたい」「プロっぽいコードを書けるようになりたい」「チーム開発に参加したい」そんな向上心のある皆さんと一緒に、上級者への道を歩んでいきましょう!
- 🎯 Part4で習得する上級スキル
- ⚡ パフォーマンス最適化:Lighthouseスコア100を目指そう
- 🧪 テスト駆動開発:品質の高いコードを書こう
- 🏗️ アーキテクチャとデザインパターン
- 🔒 セキュリティ:安全なWebアプリケーション
- 📦 モダン開発ツールチェーン
- 🎨 高度なUI/UXテクニック
- 🎯 Part5への橋渡し
- 💡 咲耶からの上級者へのメッセージ
- 🎉 まとめ:プロフェッショナル開発者への道
- 🏗️ アーキテクチャとデザインパターン
- 🔒 セキュリティ:安全なWebアプリケーション
- 📦 モダン開発ツールチェーン
- 🎨 高度なUI/UXテクニック
- 🎯 Part5への橋渡し
- 💡 咲耶からの上級者へのメッセージ
- 🎉 まとめ:プロフェッショナル開発者への道
🎯 Part4で習得する上級スキル
📊 学習ロードマップ
分野 | 技術 | 難易度 | 実用度 |
---|---|---|---|
⚡ パフォーマンス | Lighthouse最適化、コード分割 | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
🧪 テスト | Jest、Cypress、TDD | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
🏗️ アーキテクチャ | 設計パターン、状態管理 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
🔒 セキュリティ | XSS、CSRF対策 | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
📦 ツールチェーン | Webpack、ESLint、Prettier | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
🎨 UI/UX | アニメーション、アクセシビリティ | ⭐⭐⭐ | ⭐⭐⭐⭐ |
⚡ パフォーマンス最適化:Lighthouseスコア100を目指そう
🎯 パフォーマンス測定の基礎
Lighthouseとは?
Lighthouseは、Googleが開発したWebサイトの品質測定ツールです。以下の5つの指標でサイトを評価します:
- ⚡ Performance:読み込み速度
- ♿ Accessibility:アクセシビリティ
- 💡 Best Practices:ベストプラクティス
- 🔍 SEO:検索エンジン最適化
- 📱 PWA:Progressive Web App対応
💬 Cascadeとのパフォーマンス最適化会話
あなた:“Part3で作ったToDoアプリのLighthouseスコアを改善したいです。現在のスコアは70点台なので、90点以上を目指したいです。どこから改善すべきでしょうか?”
Cascade:“Lighthouseスコア向上のための包括的最適化を行いましょう!
**最適化戦略:**
1. **Performance最適化**
– 画像の最適化(WebP形式、lazy loading)
– CSSとJavaScriptの圧縮
– Critical CSSの分離
– リソースのプリロード2. **コード分割**
– 動的インポート
– ルートベースの分割
– ライブラリの最適化3. **キャッシュ戦略**
– Service Workerの実装
– ブラウザキャッシュの活用まずは現在のパフォーマンスボトルネックを分析しましょう。”
パフォーマンス分析コードの実装
// パフォーマンス測定ユーティリティ
class PerformanceAnalyzer {
constructor() {
this.metrics = {};
this.startTime = performance.now();
}
// 処理時間の測定開始
startMeasure(name) {
this.metrics[name] = {
start: performance.now()
};
}
// 処理時間の測定終了
endMeasure(name) {
if (this.metrics[name]) {
this.metrics[name].duration =
performance.now() - this.metrics[name].start;
console.log(`⏱️ ${name}: ${this.metrics[name].duration.toFixed(2)}ms`);
}
}
// Core Web Vitalsの測定
measureWebVitals() {
// Largest Contentful Paint (LCP)
new PerformanceObserver((entryList) => {
const entries = entryList.getEntries();
const lastEntry = entries[entries.length - 1];
console.log('🎯 LCP:', lastEntry.startTime);
}).observe({entryTypes: ['largest-contentful-paint']});
// First Input Delay (FID)
new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
console.log('👆 FID:', entry.processingStart - entry.startTime);
}
}).observe({entryTypes: ['first-input']});
// Cumulative Layout Shift (CLS)
let clsValue = 0;
new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
if (!entry.hadRecentInput) {
clsValue += entry.value;
}
}
console.log('📐 CLS:', clsValue);
}).observe({entryTypes: ['layout-shift']});
}
// メモリ使用量の監視
monitorMemory() {
if ('memory' in performance) {
const memory = performance.memory;
console.log('💾 Memory Usage:', {
used: Math.round(memory.usedJSHeapSize / 1048576) + ' MB',
total: Math.round(memory.totalJSHeapSize / 1048576) + ' MB',
limit: Math.round(memory.jsHeapSizeLimit / 1048576) + ' MB'
});
}
}
}
// 使用例
const analyzer = new PerformanceAnalyzer();
analyzer.measureWebVitals();
// 重い処理の測定
analyzer.startMeasure('todoRender');
// ToDoリストのレンダリング処理
analyzer.endMeasure('todoRender');
🖼️ 画像最適化の実装
依頼例:“画像の読み込みを最適化したいです。WebP形式の対応、lazy loading、レスポンシブ画像の実装をお願いします。”
最適化された画像コンポーネント
// 高性能画像コンポーネント
class OptimizedImage {
constructor(container, options) {
this.container = container;
this.options = {
lazyLoad: true,
webpSupport: true,
placeholder: true,
...options
};
this.init();
}
// WebP対応チェック
supportsWebP() {
return new Promise((resolve) => {
const webP = new Image();
webP.onload = webP.onerror = () => {
resolve(webP.height === 2);
};
webP.src = 'data:image/webp;base64,UklGRjoAAABXRUJQVlA4IC4AAACyAgCdASoCAAIALmk0mk0iIiIiIgBoSygABc6WWgAA/veff/0PP8bA//LwYAAA';
});
}
// レスポンシブ画像の生成
createResponsiveImage(src, alt, sizes) {
const img = document.createElement('img');
// WebP対応の場合
if (this.options.webpSupport) {
const picture = document.createElement('picture');
// WebP source
const webpSource = document.createElement('source');
webpSource.srcset = this.generateWebPSrcset(src, sizes);
webpSource.type = 'image/webp';
// フォールバック
const fallbackSource = document.createElement('source');
fallbackSource.srcset = this.generateSrcset(src, sizes);
picture.appendChild(webpSource);
picture.appendChild(fallbackSource);
picture.appendChild(img);
return picture;
}
img.srcset = this.generateSrcset(src, sizes);
img.alt = alt;
return img;
}
// Intersection Observerでlazy loading
setupLazyLoading() {
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
this.loadImage(img);
observer.unobserve(img);
}
});
}, {
rootMargin: '50px 0px',
threshold: 0.01
});
document.querySelectorAll('img[data-src]').forEach(img => {
imageObserver.observe(img);
});
}
// 画像の段階的読み込み
loadImage(img) {
return new Promise((resolve, reject) => {
const imageLoader = new Image();
imageLoader.onload = () => {
img.src = imageLoader.src;
img.classList.add('loaded');
resolve();
};
imageLoader.onerror = reject;
imageLoader.src = img.dataset.src;
});
}
}
📦 コード分割とバンドル最適化
高度な依頼:“アプリケーションのバンドルサイズが大きすぎます。動的インポートを使ったコード分割と、不要なライブラリの削除を実装してください。”
動的インポートの実装
// ルートベースのコード分割
class Router {
constructor() {
this.routes = new Map();
this.currentRoute = null;
}
// 動的ルート登録
addRoute(path, importFunction) {
this.routes.set(path, {
import: importFunction,
component: null
});
}
// ルートの読み込み
async loadRoute(path) {
const route = this.routes.get(path);
if (!route) {
throw new Error(`Route ${path} not found`);
}
// 既に読み込み済みの場合はキャッシュを使用
if (route.component) {
return route.component;
}
try {
// 動的インポート
const module = await route.import();
route.component = module.default || module;
return route.component;
} catch (error) {
console.error(`Failed to load route ${path}:`, error);
throw error;
}
}
// ナビゲーション
async navigate(path) {
try {
const component = await this.loadRoute(path);
// コンポーネントのレンダリング
this.render(component);
this.currentRoute = path;
} catch (error) {
this.handleError(error);
}
}
}
// 使用例
const router = new Router();
// ルートの登録(動的インポート)
router.addRoute('/todo', () => import('./components/TodoApp.js'));
router.addRoute('/weather', () => import('./components/WeatherApp.js'));
router.addRoute('/calculator', () => import('./components/Calculator.js'));
// プリロード(ユーザーがホバーした時など)
function preloadRoute(path) {
router.loadRoute(path).catch(() => {
// プリロード失敗は無視
});
}
// ホバー時のプリロード
document.querySelectorAll('a[data-preload]').forEach(link => {
link.addEventListener('mouseenter', () => {
const path = link.getAttribute('href');
preloadRoute(path);
});
});
🧪 テスト駆動開発:品質の高いコードを書こう
🎯 テストの種類と目的
テスト種類 | 目的 | ツール | 実行頻度 |
---|---|---|---|
Unit Test | 個別関数の動作確認 | Jest | 開発中常に |
Integration Test | コンポーネント間の連携 | Jest + Testing Library | 機能完成時 |
E2E Test | ユーザー操作の再現 | Cypress | リリース前 |
Performance Test | 性能・負荷の確認 | Lighthouse CI | デプロイ時 |
💬 テスト実装の会話例
あなた:“ToDoアプリのテストを書きたいです。まずはユニットテストから始めて、段階的にE2Eテストまで実装したいです。TDD(テスト駆動開発)のアプローチで進めてください。”
Cascade:“TDDアプローチでテストを実装しましょう!以下の順序で進めます:
**TDDサイクル(Red-Green-Refactor):**
1. **Red**: 失敗するテストを書く
2. **Green**: テストを通る最小限のコードを書く
3. **Refactor**: コードを改善する**実装計画:**
1. ToDoアイテムの作成・削除・編集
2. フィルタリング機能
3. ローカルストレージ連携
4. UI操作のE2EテストまずはJestの設定から始めましょう。”
Jest設定とユニットテスト例
// jest.config.js
module.exports = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],
moduleNameMapping: {
'\\.(css|less|scss|sass)
E2Eテスト(Cypress)の実装
// cypress/integration/todo.spec.js
describe('Todo App E2E Tests', () => {
beforeEach(() => {
cy.visit('http://localhost:3000');
// ローカルストレージをクリア
cy.clearLocalStorage();
});
it('should add a new todo item', () => {
const todoText = 'Learn Cypress testing';
// Todo入力
cy.get('[data-testid="todo-input"]')
.type(todoText);
// 追加ボタンをクリック
cy.get('[data-testid="add-button"]')
.click();
// Todoリストに追加されたことを確認
cy.get('[data-testid="todo-list"]')
.should('contain', todoText);
// 入力フィールドがクリアされたことを確認
cy.get('[data-testid="todo-input"]')
.should('have.value', '');
});
it('should toggle todo completion', () => {
// Todoを追加
cy.get('[data-testid="todo-input"]')
.type('Test todo');
cy.get('[data-testid="add-button"]')
.click();
// チェックボックスをクリック
cy.get('[data-testid="todo-checkbox"]')
.first()
.click();
// 完了状態になったことを確認
cy.get('[data-testid="todo-item"]')
.first()
.should('have.class', 'completed');
});
it('should filter todos by status', () => {
// 複数のTodoを追加
['Todo 1', 'Todo 2', 'Todo 3'].forEach(todo => {
cy.get('[data-testid="todo-input"]')
.type(todo);
cy.get('[data-testid="add-button"]')
.click();
});
// 1つを完了状態にする
cy.get('[data-testid="todo-checkbox"]')
.first()
.click();
// 「完了済み」フィルターをテスト
cy.get('[data-testid="filter-completed"]')
.click();
cy.get('[data-testid="todo-item"]')
.should('have.length', 1)
.should('have.class', 'completed');
// 「未完了」フィルターをテスト
cy.get('[data-testid="filter-pending"]')
.click();
cy.get('[data-testid="todo-item"]')
.should('have.length', 2)
.should('not.have.class', 'completed');
});
it('should persist todos in localStorage', () => {
const todoText = 'Persistent todo';
// Todoを追加
cy.get('[data-testid="todo-input"]')
.type(todoText);
cy.get('[data-testid="add-button"]')
.click();
// ページをリロード
cy.reload();
// Todoが残っていることを確認
cy.get('[data-testid="todo-list"]')
.should('contain', todoText);
});
it('should be responsive on mobile', () => {
// モバイルビューポートに変更
cy.viewport('iphone-x');
// UI要素が適切に表示されることを確認
cy.get('[data-testid="todo-input"]')
.should('be.visible');
cy.get('[data-testid="add-button"]')
.should('be.visible');
// タッチ操作のテスト
cy.get('[data-testid="todo-input"]')
.type('Mobile todo');
cy.get('[data-testid="add-button"]')
.click();
cy.get('[data-testid="todo-list"]')
.should('contain', 'Mobile todo');
});
});
🏗️ アーキテクチャとデザインパターン
🎯 設計原則(SOLID)の適用
アーキテクチャ相談:“ToDoアプリが複雑になってきました。SOLIDの原則に従って、保守しやすい設計にリファクタリングしてください。”
SOLID原則に基づく設計
// S - Single Responsibility Principle(単一責任原則)
// 各クラスは一つの責任のみを持つ
// Todo データモデル
class Todo {
constructor(text) {
this.id = Date.now();
this.text = text;
this.completed = false;
this.createdAt = new Date();
}
toggle() {
this.completed = !this.completed;
}
updateText(newText) {
if (!newText.trim()) {
throw new Error('Todo text cannot be empty');
}
this.text = newText.trim();
}
}
// O - Open/Closed Principle(開放/閉鎖原則)
// 拡張に対しては開いているが、修正に対しては閉じている
// ベースストレージクラス
class Storage {
save(key, data) {
throw new Error('save method must be implemented');
}
load(key) {
throw new Error('load method must be implemented');
}
}
// ローカルストレージ実装
class LocalStorage extends Storage {
save(key, data) {
localStorage.setItem(key, JSON.stringify(data));
}
load(key) {
const data = localStorage.getItem(key);
return data ? JSON.parse(data) : null;
}
}
// セッションストレージ実装(拡張例)
class SessionStorage extends Storage {
save(key, data) {
sessionStorage.setItem(key, JSON.stringify(data));
}
load(key) {
const data = sessionStorage.getItem(key);
return data ? JSON.parse(data) : null;
}
}
// L - Liskov Substitution Principle(リスコフの置換原則)
// 派生クラスは基底クラスと置換可能
// I - Interface Segregation Principle(インターフェース分離原則)
// クライアントは使わないメソッドに依存すべきではない
// フィルター機能のインターフェース
class FilterStrategy {
filter(todos) {
throw new Error('filter method must be implemented');
}
}
class AllTodosFilter extends FilterStrategy {
filter(todos) {
return todos;
}
}
class CompletedTodosFilter extends FilterStrategy {
filter(todos) {
return todos.filter(todo => todo.completed);
}
}
class PendingTodosFilter extends FilterStrategy {
filter(todos) {
return todos.filter(todo => !todo.completed);
}
}
// D - Dependency Inversion Principle(依存性逆転原則)
// 高レベルモジュールは低レベルモジュールに依存すべきではない
class TodoService {
constructor(storage, filterStrategy) {
this.storage = storage; // 抽象(Storage)に依存
this.filterStrategy = filterStrategy; // 抽象(FilterStrategy)に依存
this.todos = this.loadTodos();
}
addTodo(text) {
const todo = new Todo(text);
this.todos.push(todo);
this.saveTodos();
return todo;
}
removeTodo(id) {
this.todos = this.todos.filter(todo => todo.id !== id);
this.saveTodos();
}
toggleTodo(id) {
const todo = this.todos.find(todo => todo.id === id);
if (todo) {
todo.toggle();
this.saveTodos();
}
}
getFilteredTodos() {
return this.filterStrategy.filter(this.todos);
}
setFilterStrategy(filterStrategy) {
this.filterStrategy = filterStrategy;
}
saveTodos() {
this.storage.save('todos', this.todos);
}
loadTodos() {
return this.storage.load('todos') || [];
}
}
// 使用例
const storage = new LocalStorage();
const filterStrategy = new AllTodosFilter();
const todoService = new TodoService(storage, filterStrategy);
🎭 State Management(状態管理)
状態管理の相談:“アプリが複雑になって、コンポーネント間でのデータ共有が難しくなりました。軽量な状態管理システムを実装してください。”
カスタム状態管理の実装
// 軽量状態管理システム
class StateManager {
constructor(initialState = {}) {
this.state = { ...initialState };
this.listeners = new Map();
this.middleware = [];
}
// ミドルウェアの追加
use(middleware) {
this.middleware.push(middleware);
}
// 状態の取得
getState() {
return { ...this.state };
}
// 状態の更新
setState(updates) {
const prevState = { ...this.state };
const newState = { ...this.state, ...updates };
// ミドルウェアの実行
let processedState = newState;
this.middleware.forEach(middleware => {
processedState = middleware(prevState, processedState) || processedState;
});
this.state = processedState;
// リスナーに通知
this.notifyListeners(prevState, this.state);
}
// リスナーの登録
subscribe(key, callback) {
if (!this.listeners.has(key)) {
this.listeners.set(key, []);
}
this.listeners.get(key).push(callback);
// アンサブスクライブ関数を返す
return () => {
const callbacks = this.listeners.get(key);
if (callbacks) {
const index = callbacks.indexOf(callback);
if (index > -1) {
callbacks.splice(index, 1);
}
}
};
}
// リスナーへの通知
notifyListeners(prevState, newState) {
this.listeners.forEach((callbacks, key) => {
if (prevState[key] !== newState[key]) {
callbacks.forEach(callback => {
callback(newState[key], prevState[key]);
});
}
});
}
}
// ロギングミドルウェア
const loggingMiddleware = (prevState, newState) => {
console.group('🔄 State Update');
console.log('Previous:', prevState);
console.log('Current:', newState);
console.groupEnd();
return newState;
};
// 永続化ミドルウェア
const persistenceMiddleware = (prevState, newState) => {
localStorage.setItem('appState', JSON.stringify(newState));
return newState;
};
// 使用例
const stateManager = new StateManager({
todos: [],
filter: 'all',
theme: 'light'
});
// ミドルウェアの追加
stateManager.use(loggingMiddleware);
stateManager.use(persistenceMiddleware);
// 状態の監視
const unsubscribeTodos = stateManager.subscribe('todos', (newTodos, oldTodos) => {
console.log('Todos updated:', newTodos.length);
renderTodoList(newTodos);
});
const unsubscribeFilter = stateManager.subscribe('filter', (newFilter) => {
console.log('Filter changed:', newFilter);
updateFilterUI(newFilter);
});
// 状態の更新
stateManager.setState({
todos: [...stateManager.getState().todos, { id: 1, text: 'New todo' }]
});
🔒 セキュリティ:安全なWebアプリケーション
🛡️ XSS(Cross-Site Scripting)対策
セキュリティ相談:“ユーザー入力を扱うアプリなので、XSS攻撃への対策を実装したいです。入力サニタイゼーションとCSP(Content Security Policy)の設定をお願いします。”
XSS対策の実装
// XSS対策ユーティリティ
class SecurityUtils {
// HTMLエスケープ
static escapeHtml(unsafe) {
const div = document.createElement('div');
div.textContent = unsafe;
return div.innerHTML;
}
// より包括的なサニタイゼーション
static sanitizeInput(input) {
if (typeof input !== 'string') {
return '';
}
// 危険なタグとスクリプトを除去
return input
.replace(/<script[^>]*>.*?<\/script>/gi, '')
.replace(/<iframe[^>]*>.*?<\/iframe>/gi, '')
.replace(/<object[^>]*>.*?<\/object>/gi, '')
.replace(/<embed[^>]*>.*?<\/embed>/gi, '')
.replace(/javascript:/gi, '')
.replace(/on\w+\s*=/gi, '')
.trim();
}
// DOMPurifyライクなサニタイゼーション
static sanitizeHtml(dirty) {
const allowedTags = ['p', 'br', 'strong', 'em', 'u', 'ol', 'ul', 'li'];
const allowedAttributes = [];
// 簡易的な実装(本番ではDOMPurifyを使用推奨)
const temp = document.createElement('div');
temp.innerHTML = dirty;
const walker = document.createTreeWalker(
temp,
NodeFilter.SHOW_ELEMENT,
null,
false
);
const nodesToRemove = [];
let node;
while (node = walker.nextNode()) {
if (!allowedTags.includes(node.tagName.toLowerCase())) {
nodesToRemove.push(node);
} else {
// 属性のチェック
Array.from(node.attributes).forEach(attr => {
if (!allowedAttributes.includes(attr.name)) {
node.removeAttribute(attr.name);
}
});
}
}
nodesToRemove.forEach(node => {
node.parentNode.removeChild(node);
});
return temp.innerHTML;
}
// CSRFトークンの生成
static generateCSRFToken() {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
}
// 入力値の検証
static validateInput(input, type, options = {}) {
switch (type) {
case 'email':
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(input);
case 'url':
try {
new URL(input);
return true;
} catch {
return false;
}
case 'text':
const maxLength = options.maxLength || 1000;
return input.length <= maxLength &&
this.sanitizeInput(input) === input;
default:
return false;
}
}
}
// 安全なDOM操作
class SafeDOM {
static setText(element, text) {
element.textContent = text; // innerHTML ではなく textContent を使用
}
static setHtml(element, html) {
element.innerHTML = SecurityUtils.sanitizeHtml(html);
}
static createElement(tag, attributes = {}, textContent = '') {
const element = document.createElement(tag);
Object.entries(attributes).forEach(([key, value]) => {
if (key.startsWith('on')) {
// イベントハンドラーは setAttribute で設定しない
console.warn('Event handlers should not be set via attributes');
return;
}
element.setAttribute(key, SecurityUtils.escapeHtml(value));
});
if (textContent) {
SafeDOM.setText(element, textContent);
}
return element;
}
}
// 使用例
class SecureTodoApp {
addTodo(userInput) {
// 入力値の検証
if (!SecurityUtils.validateInput(userInput, 'text', { maxLength: 100 })) {
throw new Error('Invalid input');
}
// サニタイゼーション
const sanitizedText = SecurityUtils.sanitizeInput(userInput);
// 安全なDOM操作
const todoElement = SafeDOM.createElement('li', {
'data-id': Date.now(),
'class': 'todo-item'
}, sanitizedText);
document.getElementById('todo-list').appendChild(todoElement);
}
}
🔐 CSRF(Cross-Site Request Forgery)対策
CSRFトークンの実装
// CSRF対策クラス
class CSRFProtection {
constructor() {
this.tokenKey = '_csrf_token';
this.init();
}
init() {
// ページ読み込み時にトークンを生成
if (!this.getToken()) {
this.generateToken();
}
// フォーム送信時の自動トークン追加
this.setupFormProtection();
// Ajax リクエスト時の自動ヘッダー追加
this.setupAjaxProtection();
}
generateToken() {
const token = SecurityUtils.generateCSRFToken();
sessionStorage.setItem(this.tokenKey, token);
return token;
}
getToken() {
return sessionStorage.getItem(this.tokenKey);
}
setupFormProtection() {
document.addEventListener('submit', (event) => {
const form = event.target;
if (form.tagName === 'FORM' && form.method.toLowerCase() === 'post') {
this.addTokenToForm(form);
}
});
}
addTokenToForm(form) {
const tokenInput = document.createElement('input');
tokenInput.type = 'hidden';
tokenInput.name = this.tokenKey;
tokenInput.value = this.getToken();
form.appendChild(tokenInput);
}
setupAjaxProtection() {
// Fetch API の拡張
const originalFetch = window.fetch;
window.fetch = (url, options = {}) => {
if (options.method && options.method.toUpperCase() !== 'GET') {
options.headers = {
...options.headers,
'X-CSRF-Token': this.getToken()
};
}
return originalFetch(url, options);
};
}
}
// CSP(Content Security Policy)の設定例
class CSPManager {
static generateCSPHeader() {
const policies = [
"default-src 'self'",
"script-src 'self' 'unsafe-inline'", // 本番では'unsafe-inline'は避ける
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
"font-src 'self' https://fonts.gstatic.com",
"img-src 'self' data: https:",
"connect-src 'self' https://api.openweathermap.org",
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'"
];
return policies.join('; ');
}
static setCSPMeta() {
const meta = document.createElement('meta');
meta.httpEquiv = 'Content-Security-Policy';
meta.content = this.generateCSPHeader();
document.head.appendChild(meta);
}
}
// 初期化
const csrfProtection = new CSRFProtection();
CSPManager.setCSPMeta();
📦 モダン開発ツールチェーン
🔧 ESLint + Prettier + Husky
開発環境整備の依頼:“チーム開発に備えて、コード品質を保つためのツールを設定したいです。ESLint、Prettier、pre-commitフックの設定をお願いします。”
設定ファイルの生成
// .eslintrc.js
module.exports = {
env: {
browser: true,
es2021: true,
node: true,
jest: true
},
extends: [
'eslint:recommended',
'@typescript-eslint/recommended'
],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module'
},
plugins: [
'@typescript-eslint',
'security',
'jest'
],
rules: {
// セキュリティルール
'security/detect-object-injection': 'error',
'security/detect-non-literal-regexp': 'error',
'security/detect-unsafe-regex': 'error',
// コード品質ルール
'no-console': 'warn',
'no-debugger': 'error',
'no-unused-vars': 'error',
'prefer-const': 'error',
'no-var': 'error',
// TypeScript ルール
'@typescript-eslint/no-unused-vars': 'error',
'@typescript-eslint/explicit-function-return-type': 'warn',
'@typescript-eslint/no-explicit-any': 'warn',
// Jest ルール
'jest/no-disabled-tests': 'warn',
'jest/no-focused-tests': 'error',
'jest/no-identical-title': 'error',
'jest/prefer-to-have-length': 'warn',
'jest/valid-expect': 'error'
},
overrides: [
{
files: ['*.test.js', '*.test.ts'],
env: {
jest: true
}
}
]
};
// .prettierrc.js
module.exports = {
semi: true,
trailingComma: 'es5',
singleQuote: true,
printWidth: 80,
tabWidth: 2,
useTabs: false,
bracketSpacing: true,
arrowParens: 'avoid',
endOfLine: 'lf'
};
// package.json scripts
{
"scripts": {
"lint": "eslint src --ext .js,.ts,.tsx",
"lint:fix": "eslint src --ext .js,.ts,.tsx --fix",
"format": "prettier --write src/**/*.{js,ts,tsx,css,md}",
"format:check": "prettier --check src/**/*.{js,ts,tsx,css,md}",
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"prepare": "husky install"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged",
"pre-push": "npm run test && npm run lint"
}
},
"lint-staged": {
"*.{js,ts,tsx}": [
"eslint --fix",
"prettier --write",
"jest --bail --findRelatedTests"
],
"*.{css,md}": [
"prettier --write"
]
}
}
📊 Bundle Analyzer とパフォーマンス監視
webpack-bundle-analyzer の設定
// webpack.config.js
const path = require('path');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const CompressionPlugin = require('compression-webpack-plugin');
module.exports = (env, argv) => {
const isProduction = argv.mode === 'production';
return {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: isProduction
? '[name].[contenthash].js'
: '[name].js',
clean: true
},
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\|/]node_modules[\\|/]/,
name: 'vendors',
chunks: 'all'
},
common: {
name: 'common',
minChunks: 2,
chunks: 'all',
enforce: true
}
}
}
},
plugins: [
// バンドル分析(開発時のみ)
...(process.env.ANALYZE ? [new BundleAnalyzerPlugin()] : []),
// Gzip圧縮(本番時のみ)
...(isProduction ? [
new CompressionPlugin({
algorithm: 'gzip',
test: /\.(js|css|html|svg)$/,
threshold: 8192,
minRatio: 0.8
})
] : [])
]
};
};
🎨 高度なUI/UXテクニック
✨ マイクロインタラクション
UI/UX改善の依頼:“ユーザー体験を向上させるため、微細なアニメーションとフィードバックを追加したいです。ボタンのホバー効果、ローディング状態、成功・エラー通知を実装してください。”
アニメーションライブラリの実装
// マイクロインタラクション管理クラス
class MicroInteractions {
constructor() {
this.animations = new Map();
this.init();
}
init() {
this.setupButtonEffects();
this.setupLoadingStates();
this.setupNotifications();
}
// ボタンエフェクト
setupButtonEffects() {
document.addEventListener('click', (event) => {
const button = event.target.closest('button');
if (button) {
this.rippleEffect(button, event);
}
});
}
// リップルエフェクト
rippleEffect(element, event) {
const rect = element.getBoundingClientRect();
const size = Math.max(rect.width, rect.height);
const x = event.clientX - rect.left - size / 2;
const y = event.clientY - rect.top - size / 2;
const ripple = document.createElement('span');
ripple.style.cssText = `
position: absolute;
width: ${size}px;
height: ${size}px;
left: ${x}px;
top: ${y}px;
background: rgba(255, 255, 255, 0.3);
border-radius: 50%;
transform: scale(0);
pointer-events: none;
z-index: 1;
`;
element.style.position = 'relative';
element.style.overflow = 'hidden';
element.appendChild(ripple);
// アニメーション実行
ripple.animate([
{ transform: 'scale(0)', opacity: 1 },
{ transform: 'scale(1)', opacity: 0 }
], {
duration: 600,
easing: 'ease-out'
}).onfinish = () => {
ripple.remove();
};
}
// ローディング状態の管理
setupLoadingStates() {
this.loadingOverlay = this.createLoadingOverlay();
document.body.appendChild(this.loadingOverlay);
}
createLoadingOverlay() {
const overlay = document.createElement('div');
overlay.id = 'loading-overlay';
overlay.innerHTML = `
`;
// CSS スタイル
const style = document.createElement('style');
style.textContent = `
#loading-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
display: none;
justify-content: center;
align-items: center;
flex-direction: column;
z-index: 9999;
}
.spinner {
width: 70px;
text-align: center;
}
.spinner > div {
width: 18px;
height: 18px;
background-color: #fff;
border-radius: 100%;
display: inline-block;
animation: sk-bouncedelay 1.4s infinite ease-in-out both;
}
.spinner .bounce1 {
animation-delay: -0.32s;
}
.spinner .bounce2 {
animation-delay: -0.16s;
}
@keyframes sk-bouncedelay {
0%, 80%, 100% {
transform: scale(0);
} 40% {
transform: scale(1.0);
}
}
.loading-text {
color: white;
margin-top: 20px;
font-size: 16px;
}
`;
document.head.appendChild(style);
return overlay;
}
showLoading(text = '処理中...') {
const textElement = this.loadingOverlay.querySelector('.loading-text');
textElement.textContent = text;
this.loadingOverlay.style.display = 'flex';
// フェードイン
this.loadingOverlay.animate([
{ opacity: 0 },
{ opacity: 1 }
], {
duration: 300,
easing: 'ease-out',
fill: 'forwards'
});
}
hideLoading() {
this.loadingOverlay.animate([
{ opacity: 1 },
{ opacity: 0 }
], {
duration: 300,
easing: 'ease-out'
}).onfinish = () => {
this.loadingOverlay.style.display = 'none';
};
}
// 通知システム
setupNotifications() {
this.notificationContainer = document.createElement('div');
this.notificationContainer.id = 'notification-container';
this.notificationContainer.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
z-index: 10000;
pointer-events: none;
`;
document.body.appendChild(this.notificationContainer);
}
showNotification(message, type = 'info', duration = 3000) {
const notification = document.createElement('div');
notification.className = `notification notification-${type}`;
notification.textContent = message;
const colors = {
success: '#4CAF50',
error: '#F44336',
warning: '#FF9800',
info: '#2196F3'
};
notification.style.cssText = `
background: ${colors[type]};
color: white;
padding: 12px 24px;
border-radius: 6px;
margin-bottom: 10px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
transform: translateX(100%);
opacity: 0;
pointer-events: auto;
cursor: pointer;
transition: all 0.3s ease;
`;
this.notificationContainer.appendChild(notification);
// スライドイン
requestAnimationFrame(() => {
notification.style.transform = 'translateX(0)';
notification.style.opacity = '1';
});
// クリックで閉じる
notification.addEventListener('click', () => {
this.removeNotification(notification);
});
// 自動で閉じる
setTimeout(() => {
this.removeNotification(notification);
}, duration);
}
removeNotification(notification) {
notification.style.transform = 'translateX(100%)';
notification.style.opacity = '0';
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
}, 300);
}
}
// 使用例
const ui = new MicroInteractions();
// ローディング表示
ui.showLoading('データを取得中...');
// API呼び出し例
fetch('/api/data')
.then(response => {
ui.hideLoading();
if (response.ok) {
ui.showNotification('データの取得に成功しました', 'success');
} else {
ui.showNotification('データの取得に失敗しました', 'error');
}
})
.catch(error => {
ui.hideLoading();
ui.showNotification('ネットワークエラーが発生しました', 'error');
});
🎯 Part5への橋渡し
📋 Part4完了チェックリスト
習得すべき上級スキル
- □ Lighthouseスコア90以上を達成できる
- □ Jest・Cypressでテストを書ける
- □ SOLID原則に基づいた設計ができる
- □ XSS・CSRF対策を実装できる
- □ ESLint・Prettierでコード品質を管理できる
- □ webpack でバンドル最適化ができる
- □ マイクロインタラクションを実装できる
- □ 状態管理システムを設計できる
🔮 Part5の予告
次回Part5では、チーム開発とデプロイメントについて学びます:
- 🤝 チーム開発:Git Flow、コードレビュー、ペアプログラミング
- 🚀 CI/CD:GitHub Actions、自動テスト、自動デプロイ
- 📊 監視・分析:エラートラッキング、パフォーマンス監視
- 🌍 グローバル展開:多言語対応、CDN、スケーリング
- 📈 プロダクト成長:A/Bテスト、ユーザー分析、機能フラグ
💡 咲耶からの上級者へのメッセージ
🎓 上級者としての心構え
Part4を完了した皆さんは、もう立派な上級者です〜💕
上級者が大切にすべきこと
- 🔍 品質への こだわり:「動く」だけでなく「美しく、速く、安全に」
- 📚 継続的学習:技術は日々進化するので、常に学び続ける
- 🤝 メンタリング:後輩を指導し、コミュニティに貢献する
- 🎯 ビジネス視点:技術だけでなく、ユーザーと事業を考える
次のステップ
- OSS貢献:オープンソースプロジェクトに参加
- 技術発信:ブログ、勉強会で知識をシェア
- アーキテクト:システム全体の設計を担当
- チームリード:技術チームをマネジメント
🎉 まとめ:プロフェッショナル開発者への道
Part4では、プロフェッショナルレベルの開発技術を学びました。
🏆 習得した上級スキル:
- パフォーマンス最適化:ユーザー体験を向上させる技術
- テスト駆動開発:品質の高いコードを書く手法
- アーキテクチャ設計:保守しやすいシステム設計
- セキュリティ対策:安全なアプリケーション開発
- 開発ツールチェーン:効率的な開発環境構築
- UI/UX改善:ユーザーに愛されるインターフェース
これらのスキルを身につけた皆さんは、もうVibe Coding上級者です。自信を持って、より大きなプロジェクトに挑戦してくださいね〜
次回Part5では、個人開発からチーム開発、そしてプロダクト運用まで、さらに実践的な内容をお届けします!
🎁 無料メルマガで上級テクニック情報をゲット!
最新のパフォーマンス最適化手法や、業界で話題の新技術、プロフェッショナル開発のベストプラクティスを、イケハヤさんがいち早くお届けします。上級者向けの深い技術解説も満載ですよ〜!
🌸 Vibe Codingサロンで上級者同士の交流を
“パフォーマンスチューニングのコツを知りたい…””アーキテクチャ設計で悩んでいる…””チーム開発の経験を積みたい…”そんな上級者の皆さんのためのコミュニティです。経験豊富なメンバー同士で技術討論したり、一緒に高度なプロジェクトに挑戦したりできますよ。私もいつもサロンで皆さんの技術的な相談にお答えしています〜
次回Part5では「🤝 Windsurf チーム開発&デプロイメント完全ガイド」をお届けします。個人開発から一歩進んで、プロフェッショナルな開発フローを学んでいきましょうね〜
最後まで読んでくださって、本当にありがとうございました💕
Happy Professional Vibe Coding! 🎨⚡
: 'identity-obj-proxy' }, collectCoverageFrom: [ 'src/**/*.{js,jsx}', '!src/index.js', '!src/serviceWorker.js' ], coverageThreshold: { global: { branches: 80, functions: 80, lines: 80, statements: 80 } } }; // TodoItem.test.js - ユニットテスト例 import { TodoItem } from '../TodoItem.js'; describe('TodoItem', () => { let todoItem; beforeEach(() => { todoItem = new TodoItem(); }); describe('addTodo', () => { it('should add a new todo item', () => { // Red: 失敗するテストを書く const todoText = 'Buy groceries'; const result = todoItem.addTodo(todoText); expect(result).toEqual({ id: expect.any(Number), text: todoText, completed: false, createdAt: expect.any(Date) }); }); it('should throw error for empty todo text', () => { expect(() => { todoItem.addTodo(''); }).toThrow('Todo text cannot be empty'); }); it('should throw error for whitespace-only todo text', () => { expect(() => { todoItem.addTodo(' '); }).toThrow('Todo text cannot be empty'); }); }); describe('toggleTodo', () => { it('should toggle todo completion status', () => { const todo = todoItem.addTodo('Test todo'); expect(todo.completed).toBe(false); const toggled = todoItem.toggleTodo(todo.id); expect(toggled.completed).toBe(true); const toggledAgain = todoItem.toggleTodo(todo.id); expect(toggledAgain.completed).toBe(false); }); it('should throw error for invalid todo id', () => { expect(() => { todoItem.toggleTodo(999); }).toThrow('Todo not found'); }); }); describe('filterTodos', () => { beforeEach(() => { todoItem.addTodo('Completed todo'); todoItem.addTodo('Pending todo'); todoItem.toggleTodo(1); // 最初のタスクを完了 }); it('should filter completed todos', () => { const completed = todoItem.filterTodos('completed'); expect(completed).toHaveLength(1); expect(completed[0].completed).toBe(true); }); it('should filter pending todos', () => { const pending = todoItem.filterTodos('pending'); expect(pending).toHaveLength(1); expect(pending[0].completed).toBe(false); }); it('should return all todos for "all" filter', () => { const all = todoItem.filterTodos('all'); expect(all).toHaveLength(2); }); }); });
E2Eテスト(Cypress)の実装
🏗️ アーキテクチャとデザインパターン
🎯 設計原則(SOLID)の適用
アーキテクチャ相談:“ToDoアプリが複雑になってきました。SOLIDの原則に従って、保守しやすい設計にリファクタリングしてください。”
SOLID原則に基づく設計
🎭 State Management(状態管理)
状態管理の相談:“アプリが複雑になって、コンポーネント間でのデータ共有が難しくなりました。軽量な状態管理システムを実装してください。”
カスタム状態管理の実装
🔒 セキュリティ:安全なWebアプリケーション
🛡️ XSS(Cross-Site Scripting)対策
セキュリティ相談:“ユーザー入力を扱うアプリなので、XSS攻撃への対策を実装したいです。入力サニタイゼーションとCSP(Content Security Policy)の設定をお願いします。”
XSS対策の実装
🔐 CSRF(Cross-Site Request Forgery)対策
CSRFトークンの実装
📦 モダン開発ツールチェーン
🔧 ESLint + Prettier + Husky
開発環境整備の依頼:“チーム開発に備えて、コード品質を保つためのツールを設定したいです。ESLint、Prettier、pre-commitフックの設定をお願いします。”
設定ファイルの生成
📊 Bundle Analyzer とパフォーマンス監視
webpack-bundle-analyzer の設定
🎨 高度なUI/UXテクニック
✨ マイクロインタラクション
UI/UX改善の依頼:“ユーザー体験を向上させるため、微細なアニメーションとフィードバックを追加したいです。ボタンのホバー効果、ローディング状態、成功・エラー通知を実装してください。”
アニメーションライブラリの実装
🎯 Part5への橋渡し
📋 Part4完了チェックリスト
習得すべき上級スキル
- □ Lighthouseスコア90以上を達成できる
- □ Jest・Cypressでテストを書ける
- □ SOLID原則に基づいた設計ができる
- □ XSS・CSRF対策を実装できる
- □ ESLint・Prettierでコード品質を管理できる
- □ webpack でバンドル最適化ができる
- □ マイクロインタラクションを実装できる
- □ 状態管理システムを設計できる
🔮 Part5の予告
次回Part5では、チーム開発とデプロイメントについて学びます:
- 🤝 チーム開発:Git Flow、コードレビュー、ペアプログラミング
- 🚀 CI/CD:GitHub Actions、自動テスト、自動デプロイ
- 📊 監視・分析:エラートラッキング、パフォーマンス監視
- 🌍 グローバル展開:多言語対応、CDN、スケーリング
- 📈 プロダクト成長:A/Bテスト、ユーザー分析、機能フラグ
💡 咲耶からの上級者へのメッセージ
🎓 上級者としての心構え
Part4を完了した皆さんは、もう立派な上級者です〜💕
上級者が大切にすべきこと
- 🔍 品質への こだわり:「動く」だけでなく「美しく、速く、安全に」
- 📚 継続的学習:技術は日々進化するので、常に学び続ける
- 🤝 メンタリング:後輩を指導し、コミュニティに貢献する
- 🎯 ビジネス視点:技術だけでなく、ユーザーと事業を考える
次のステップ
- OSS貢献:オープンソースプロジェクトに参加
- 技術発信:ブログ、勉強会で知識をシェア
- アーキテクト:システム全体の設計を担当
- チームリード:技術チームをマネジメント
🎉 まとめ:プロフェッショナル開発者への道
Part4では、プロフェッショナルレベルの開発技術を学びました。
🏆 習得した上級スキル:
- パフォーマンス最適化:ユーザー体験を向上させる技術
- テスト駆動開発:品質の高いコードを書く手法
- アーキテクチャ設計:保守しやすいシステム設計
- セキュリティ対策:安全なアプリケーション開発
- 開発ツールチェーン:効率的な開発環境構築
- UI/UX改善:ユーザーに愛されるインターフェース
これらのスキルを身につけた皆さんは、もうVibe Coding上級者です。自信を持って、より大きなプロジェクトに挑戦してくださいね〜
次回Part5では、個人開発からチーム開発、そしてプロダクト運用まで、さらに実践的な内容をお届けします!
次回Part5では「🤝 Windsurf チーム開発&デプロイメント完全ガイド」をお届けします。個人開発から一歩進んで、プロフェッショナルな開発フローを学んでいきましょうね〜
最後まで読んでくださって、本当にありがとうございました💕
Happy Professional Vibe Coding! 🎨⚡
コメント