このケーススタディを一緒に見ていきましょう。Next.jsサーバー アクションのパワーと精巧さを活用して、リアルタイム アプリケーションを構築する旅に乗り出します。あなたが経験豊富な開発者であっても、リアルタイム アプリの領域に足を踏み入れたばかりの人であっても、たくさんの洞察があなたを待っています。
それを大局的に理解するために、どこにでもあるいくつかの例を考えてみましょう。
インスタント メッセージング アプリ: WhatsAppや Telegram など、メッセージが遅延なく送信、受信、表示されるプラットフォーム。
共同作業ツール: Google ドキュメントを思い浮かべてください。複数のユーザーが同時にドキュメントを編集し、お互いの変更をリアルタイムで観察できます。
Live Stock Tickers : 市場の変動に応じて瞬時に更新される株価を表示するプラットフォーム。
オンライン マルチプレイヤー ゲーム: プレイヤーが遅延ゼロでお互いや環境と対話し、シームレスなゲーム体験を保証します。
では、なぜリアルタイム機能がこれほど求められているのでしょうか?
リアルタイム アプリケーションの構築には次のようなハードルがあります。
スケーラビリティの問題: リアルタイム アプリは多くの場合、多数の同時接続を処理する必要があるため、堅牢なインフラストラクチャが必要です。
データの整合性: さまざまなユーザー インターフェイス間でリアルタイム データの一貫性を確保することは、特に複数の同時編集や操作の場合に困難になる可能性があります。
遅延: リアルタイム アプリの性能は、最も遅いコンポーネントによって決まります。遅延を最小限に抑えるには、慎重な最適化とリソースの効率的な使用が必要です。
React エコシステムのアクションは、まだ実験段階ではありますが、開発者がユーザーの操作に応じて非同期コードを実行できるようにすることで、パラダイム シフトをもたらしました。
興味深いことに、これらは Next.js またはReactサーバー コンポーネントに限定されているわけではありませんが、Next.js を通じて使用するということは、React 実験チャネルに参加していることを意味します。
HTML フォームに慣れている人は、 action
プロパティに URL を渡すことを思い出したかもしれません。アクションを使用すると、関数を直接渡すことができるため、対話がより動的で統合されたものになります。
<button action={() => { /* async function logic here */ }}>Click me!</button>
フォーム アクションは、 React のアクションと標準の<form>
API を巧みに組み合わせたものです。これらは HTML の基本的なformaction
属性と共鳴し、開発者がすぐに使用できるプログレッシブ読み込み状態やその他の機能を強化できるようになります。
<!-- Traditional HTML approach --> <form action="/submit-url"> <!-- form elements --> </form> <!-- With Next.js 13.4 Form Actions --> <form action={asyncFunctionForSubmission}> <!-- form elements --> </form>
サーバー関数は基本的にサーバー側で動作する関数ですが、クライアントから呼び出すこともできます。これらにより、Next.js のサーバー側レンダリング機能がまったく新しいレベルに引き上げられます。
サーバー アクションに移行すると、サーバー関数として理解できますが、特にアクションとしてトリガーされるものです。特にaction
プロパティを介したフォーム要素との統合により、クライアント側の JavaScript が読み込まれる前でもフォームがインタラクティブなままであることが保証されます。これにより、React ハイドレーションがフォーム送信の前提条件ではなくなり、よりスムーズなユーザー エクスペリエンスが実現します。
// A simple Server Action in Next.js 13.4 <form action={serverActionFunction}> <!-- form elements --> </form>
最後に、サーバーアクションのサブセットであるサーバーミューテーションがあります。これらは、サーバー上のデータを変更してから、 redirect
、 revalidatePath
、またはrevalidateTag
などの特定の応答を実行する必要がある場合に特に強力です。
const serverMutationFunction = async () => { // Modify data logic here... // ... return { revalidatePath: '/updated-path' }; } <form action={serverMutationFunction}> <!-- form elements --> </form>
注:要約すると、Next.js 13.4 のサーバー アクション フレームワークは、アクション、フォーム アクション、サーバー関数、サーバー ミューテーションによって支えられ、リアルタイム Web アプリケーションへの革新的なアプローチを具体化しています。
ケーススタディを進めていくと、これらの機能がもたらす優れた機能を直接的目の当たりにすることになります。それでは、これからのエキサイティングな旅に備えて準備をしましょう。
まず、Next.js プロジェクトでサーバー アクションを有効にする必要があります。次のコードをnext.config.js
ファイルに追加するだけです。
module.exports = { experimental: { serverActions: true, }, }
サーバー コンポーネント内: サーバー アクションは、次のようにサーバー コンポーネント内で簡単に定義できます。
export default function ServerComponent() { async function myAction() { 'use server' // ... } }
クライアント コンポーネントの場合: クライアント コンポーネント内でサーバー アクションを使用する場合は、別のファイルにアクションを作成してインポートします。
// app/actions.js 'use server' export async function myAction() { // ... }
// app/client-component.js import { myAction } from './actions' export default function ClientComponent() { return ( <form action={myAction}> <button type="submit">Add to Cart</button> </form> ) }
カスタム呼び出し: startTransition
などのカスタム メソッドを使用して、フォーム、ボタン、入力の外部でサーバー アクションを呼び出すことができます。
// Example using startTransition 'use client' import { useTransition } from 'react' import { addItem } from '../actions' function ExampleClientComponent({ id }) { let [isPending, startTransition] = useTransition() return ( <button onClick={() => startTransition(() => addItem(id))}> Add To Cart </button> ) }
Next.js 13.4 では、Progressive Enhancement も提供されており、JavaScript なしで<form>
を機能させることができます。サーバー アクションは<form>
に直接渡すことができるため、JavaScript が無効になっている場合でもフォームをインタラクティブにすることができます。
// app/components/example-client-component.js 'use client' import { handleSubmit } from './actions.js' export default function ExampleClientComponent({ myAction }) { return ( <form action={handleSubmit}> {/* ... */} </form> ) }
サーバー アクションに送信される最大リクエスト本文は、デフォルトでは 1MB です。必要に応じて、 serverActionsBodySizeLimit
オプションを使用してこの制限を構成できます。
module.exports = { experimental: { serverActions: true, serverActionsBodySizeLimit: '2mb', }, }
npx create-next-app@latest my-real-time-app
my-real-time-app
プロジェクトの目的の名前に置き換えます。このコマンドは、デフォルト構成で新しい Next.js プロジェクトをセットアップします。
Next.js 13.4 の導入により、 App Router は開発者が共有レイアウト、ネストされたルーティング、エラー処理などを利用できるようにする重要な機能です。これは既存のpages
ディレクトリと連携して動作するように設計されていますが、 app
という名前の新しいディレクトリ内に格納されます。
プロジェクトのルートにapp
ディレクトリを作成します。
デフォルトでは、 app
ディレクトリ内のコンポーネントはサーバー コンポーネントであり、最適なパフォーマンスを提供し、開発者が簡単に導入できるようにします。
my-real-time-app/ │ ├── app/ # Main directory for App Router components │ ├── _error.js # Custom error page │ ├── _layout.js # Shared layout for the app │ │ │ ├── dashboard/ # Nested route example │ │ ├── index.js # Dashboard main view │ │ └── settings.js # Dashboard settings view │ │ │ ├── index.js # Landing/Home page │ ├── profile.js # User profile page │ ├── login.js # Login page │ └── register.js # Registration page │ ├── public/ # Static assets go here (images, fonts, etc.) │ ├── images/ │ └── favicon.ico │ ├── styles/ # Global styles or variables │ └── global.css │ ├── package.json # Dependencies and scripts ├── next.config.js # Next.js configuration └── README.md # Project documentation
サーバー コンポーネント: アプリケーションの非対話型部分に最適です。これらのコンポーネントはサーバー上でレンダリングされ、HTML としてクライアントに送信されます。ここでの利点は、パフォーマンスの向上、クライアント側の JavaScript の削減、およびデータのフェッチやバックエンド リソースへの直接アクセスができることです。
クライアント コンポーネント: インタラクティブな UI 要素に使用されます。これらはサーバー上で事前にレンダリングされ、クライアント上で「ハイドレート」されて対話性が追加されます。
これらのコンポーネントを区別するために、Next.js では"use client"
ディレクティブが導入されました。このディレクティブは、コンポーネントがクライアント コンポーネントとして扱われる必要があることを示します。これは、インポートの前に、コンポーネント ファイルの先頭に配置する必要があります。
たとえば、提供されたコードのように対話型カウンターがある場合は、 "use client"
ディレクティブを使用して、それがクライアント側コンポーネントであることを示します。
デフォルトでサーバー コンポーネントを使用します ( app
ディレクトリにあるため)。
インタラクティブ性の追加、ブラウザー専用 API の利用、状態またはブラウザーの機能に依存する React フックの活用など、特定のユースケースがある場合にのみ、クライアント コンポーネントを選択してください。
注:この構造とセットアップに従えば、Next.js 13.4 のサーバー アクションを使用してパフォーマンスの高いリアルタイム アプリケーションを構築できるようになります。
Next.js 13.4 のパワーは、リアルタイム バックエンド機能をプロジェクトに統合するときに輝きます。 my-real-time-app
に関連するコード例を使用して手順を見てみましょう。
my-real-time-app
の場合、サーバー アクションは、 frontendとbackendの間の主要なブリッジとして機能し、個別の API を必要とせずに効率的なデータ トランザクションを可能にします。
// my-real-time-app/app/actions/index.js export * from './auth-action'; export * from './chat-action';
my-real-time-app
では、サーバー アクションを活用して認証プロセスを合理化します。
// my-real-time-app/app/actions/auth-action.js export const login = async (credentials) => { // Logic for authenticating user with credentials // Return user details or error message }; export const logout = async (userId) => { // Logic for logging out the user // Return success or error message }; export const register = async (userInfo) => { // Logic for registering a new user // Store user in database and return success or error message };
// my-real-time-app/app/actions/chat-action.js export const sendMessage = async (messageDetails) => { // Logic to send a new message // Store message in database and inform other users via WebSocket or similar }; export const receiveMessage = async () => { // Logic to receive a message in real-time // Return the message details }; export const getRecentMessages = async (userId) => { // Logic to fetch recent messages for the user // Retrieve messages from the database };
// Initialize MongoDB connection const { MongoClient } = require('mongodb'); const client = new MongoClient(process.env.MONGODB_URI); await client.connect(); // Now, use this connection in server actions to interact with the database.
// my-real-time-app/app/actions/chat-action.js export const sendMessage = async (messageDetails) => { const messagesCollection = client.db('chatDB').collection('messages'); await messagesCollection.insertOne(messageDetails); // Inform other users via WebSocket or similar };
// Middleware for validating request data const validateRequest = (req) => { // Validation logic here return isValid; }; export const sendMessage = async (messageDetails) => { if (!validateRequest(messageDetails)) { throw new Error("Invalid request data"); } // Remaining logic... };
このセクションでは、 my-real-time-app
用の直感的で応答性の高いチャット インターフェイスを構築します。 Next.js 13.4 のサーバー コンポーネントの統合により、リアルタイムの更新が可能になり、スムーズなユーザー エクスペリエンスが実現します。
// my-real-time-app/app/chat-interface.js import { useEffect, useState } from 'react'; import { getRecentMessages } from './actions/chat-action'; export default function ChatInterface() { const [messages, setMessages] = useState([]); useEffect(() => { async function loadMessages() { const recentMessages = await getRecentMessages(); setMessages(recentMessages); } loadMessages(); }, []); return ( <div className="chatBox"> {messages.map(msg => ( <p key={msg.id}>{msg.content}</p> ))} </div> ); }
// my-real-time-app/app/chat-interface.js const [socket, setSocket] = useState(null); useEffect(() => { const ws = new WebSocket("ws://your-backend-url/ws"); ws.onmessage = (event) => { const newMessage = JSON.parse(event.data); setMessages(prevMessages => [...prevMessages, newMessage]); }; setSocket(ws); return () => { ws.close(); }; }, []);
// my-real-time-app/app/chat-interface.js useEffect(() => { if (messages.length && "Notification" in window && Notification.permission === "granted") { const lastMessage = messages[messages.length - 1]; new Notification(`New message from ${lastMessage.sender}: ${lastMessage.content}`); } }, [messages]);
const HeavyComponent = React.lazy(() => import('./HeavyComponent')); function Chat() { return ( <React.Suspense fallback={<div>Loading...</div>}> <HeavyComponent /> </React.Suspense> ); }
React Server Components
を使用してロジックを分割します。
リアルタイム アプリケーションのコア コンポーネントを適切に配置したら、それらが期待どおりに機能し、パフォーマンス、拡張性、堅牢性を確保することが重要です。このセクションではmy-real-time-app
のようなリアルタイム システムに合わせたさまざまなテスト アプローチに光を当てます。
// cypress/integration/chat.spec.js describe('Chat functionality', () => { it('should send and receive messages in real-time', () => { cy.visit('/chat'); cy.get('[data-cy=messageInput]').type('Hello, World!'); cy.get('[data-cy=sendButton]').click(); cy.contains('Hello, World!').should('exist'); }); });
# artillery-config.yml config: target: '//my-real-time-app.com' phases: - duration: 300 arrivalRate: 20 scenarios: - flow: - emit: channel: 'chat' payload: message: 'Hello, World!'
$ artillery run artillery-config.yml
Node.js にはプロファイリング用の組み込みツールが用意されており、Next.js 開発サーバーで--inspect
フラグを使用して Node.js インスペクターを有効にすることができます。 Chrome の DevTools を使用すると、パフォーマンスのボトルネックについての洞察を得ることができます。
クライアント側の場合、Chrome DevTools のPerformance
タブのようなツールは、レンダリングのボトルネックを特定するのに役立ちます。特にリアルタイム更新の場合は、不必要なレンダリングが発生しないようにしてください。
SWR を使用した例:
// my-real-time-app/app/chat-interface.js import useSWR from 'swr'; function ChatInterface() { const { data: messages } = useSWR('/api/messages', fetcher); // ... rest of the component }
注:リアルタイム アプリケーションをテストするには、標準的なソフトウェア テスト手法と、リアルタイム システムの課題や特性に合わせて特別に調整されたいくつかの手法を組み合わせる必要があります。 my-real-time-app
の厳格なテスト体制を確保することで、ユーザー トラフィックやデータ フローの規模に関係なく、スムーズで応答性の高いユーザー エクスペリエンスを保証できます。
リアルタイム アプリケーションの基本アーキテクチャがしっかりと確立されたので、今度はその機能とパフォーマンスを改良することに注意を向けます。ユーザー エクスペリエンスを向上させ、 my-real-time-app
を最適化するためのいくつかの戦略を次に示します。
// my-real-time-app/app/components/Message.js function Message({ content, status }) { return ( <div> <p>{content}</p> {status === 'read' && <span>✓ Read</span>} </div> ); }
// my-real-time-app/app/components/UserStatus.js function UserStatus({ isOnline }) { return ( <div> {isOnline ? <span className="online-indicator"></span> : <span className="offline-indicator"></span>} </div> ); }
// Example: Setting up compression with a WebSocket server const WebSocket = require('ws'); const wss = new WebSocket.Server({ perMessageDeflate: { zlibDeflateOptions: { // Add compression options here } } });
// Example: Simple retry logic with fetch let retries = 3; function fetchData(url) { fetch(url) .then(response => response.json()) .catch(error => { if (retries > 0) { retries--; fetchData(url); } else { console.error('Failed to fetch data after 3 retries'); } }); }
注: my-real-time-app
の継続的な成功は、そのコア機能だけでなく、摩擦のないユーザー エクスペリエンスを保証する微妙な機能強化と継続的な最適化にもかかっています。上記の戦略を組み込むことで、信頼性が高く楽しい優れたチャット エクスペリエンスをユーザーに提供する準備ができています。
my-real-time-app
での取り組みでは、Next.js 13.4 での初期セットアップから、サーバー アクションを使用したバックエンドの構築、シームレスなフロントエンド エクスペリエンスの設計、リアルタイム機能のテストと最適化の確認までを行いました。私たちはサーバーとクライアントのコンポーネントの微妙な違いを深く掘り下げ、サーバー側のレンダリングとクライアント側の対話性の間の効果的なバランスを確保しました。
my-real-time-app
長い道のりを歩んできましたが、将来の機能拡張の可能性は依然として膨大です。
まず最初に、 Next.js の世界のこの複雑な迷路を一緒に旅してくれて、本当にありがとうございます。ここまで進んだなら、おめでとうございます!もしあなたがいくつかの部分をざっと読んだとしても、私はあなたを責めません。私もそれらを書くのをスキップしたかったときがありました。
問題のデバッグに何時間も費やした後、セミコロンを見逃していたことに気づいたという経験はありませんか?あるいは、コードの重要な部分を誤って削除してしまい、Ctrl + Z があればいいのにと思ったときはありませんか?ああ、プログラミングの楽しさよ!
したがって、次回あなたのコードが連携を拒否したときは、深呼吸してコーヒー (またはお茶、私は判断しません。私自身マテコシドのファンです) を飲み、この問題に陥っているのはあなただけではないことを思い出してください。