Next.js trên Vercel và serverless
Phát từ server action, đăng ký trong client component, không để socket nằm trong hàm.
Một ứng dụng Next.js trên Vercel, Netlify, Cloudflare hay AWS Lambda chạy mã máy chủ theo từng request. Một hàm chỉ sống vài trăm mili giây thì không thể giữ hàng nghìn kết nối WebSocket, nên "thêm cập nhật trực tiếp" thường kết thúc bằng polling hoặc một máy chủ riêng phải tự chạy. Với Hibira, route handler và server action của bạn phát bằng một lệnh gọi HTTPS ngắn, còn trình duyệt giữ kết nối với Hibira thay vì với các hàm của bạn.
Người dùng nhận được gì
- Trạng thái đơn hàng, tác vụ và thanh toán đổi trên màn hình ngay khi đổi trong cơ sở dữ liệu.
- Thông báo và bình luận xuất hiện ở mọi tab đang mở, kể cả tab mở từ một liên kết được chia sẻ.
- Trang vẫn nhanh: lần render đầu tiên vẫn đến từ server component của bạn.
Dùng những gì
pusher(thư viện máy chủ Node.js) trong route handler và server action.pusher-jstrong client component.- Kênh riêng tư (
private-), được cấp quyền bởi một route handler kiểm tra session của bạn. - Loại trừ người gửi (
socket_id), để người thực hiện thay đổi không nhận nó hai lần.
Bước 1: biến môi trường
PUSHER_APP_ID=YOUR_APP_ID
PUSHER_APP_SECRET=YOUR_APP_SECRET
NEXT_PUBLIC_PUSHER_KEY=YOUR_APP_KEY
NEXT_PUBLIC_PUSHER_HOST=ws.hibira.ioChỉ key và host có tiền tố NEXT_PUBLIC_, vì chúng được gửi tới trình duyệt. Secret thì không bao giờ.
Bước 2: client chỉ dùng ở máy chủ
// lib/pusher-server.ts
import 'server-only';
import Pusher from 'pusher';
export const pusher = new Pusher({
appId: process.env.PUSHER_APP_ID!,
key: process.env.NEXT_PUBLIC_PUSHER_KEY!,
secret: process.env.PUSHER_APP_SECRET!,
host: process.env.NEXT_PUBLIC_PUSHER_HOST!,
useTLS: true,
});Cài các thư viện bằng npm install pusher pusher-js server-only. Dòng import server-only khiến bản build thất bại nếu có client component nào import file này, nên secret không thể lọt vào bundle của trình duyệt.
Bước 3: phát từ server action
// app/orders/actions.ts
'use server';
import { pusher } from '@/lib/pusher-server';
export async function shipOrder(id: string, socketId?: string) {
const order = await db.orders.ship(id);
await pusher.trigger(`private-order-${id}`, 'status', { status: order.status }, { socket_id: socketId });
return order;
}Route handler, webhook từ nhà cung cấp thanh toán và cron job cũng phát theo cách đó. Luôn await lệnh gọi: nền tảng serverless có thể đóng băng hàm ngay khi phản hồi được gửi.
Bước 4: cấp quyền kênh riêng tư
// app/api/pusher/auth/route.ts
import { pusher } from '@/lib/pusher-server';
import { auth } from '@/auth';
export async function POST(req: Request) {
const session = await auth();
if (!session?.user) return new Response('Unauthorized', { status: 401 });
const form = await req.formData();
const socketId = String(form.get('socket_id'));
const channel = String(form.get('channel_name'));
const id = channel.replace(/^private-order-/, '');
if (!(await db.orders.belongsTo(id, session.user.id))) return new Response('Forbidden', { status: 403 });
return Response.json(pusher.authorizeChannel(socketId, channel));
}auth() ở đây là Auth.js; hãy dùng cách đọc session mà ứng dụng của bạn đang dùng.
Bước 5: đăng ký trong client component
// lib/pusher-client.ts
import Pusher from 'pusher-js';
let client: Pusher | undefined;
export function getPusher(): Pusher {
client ??= new Pusher(process.env.NEXT_PUBLIC_PUSHER_KEY!, {
cluster: 'ap',
wsHost: process.env.NEXT_PUBLIC_PUSHER_HOST!,
forceTLS: true,
enabledTransports: ['ws', 'wss'],
channelAuthorization: { endpoint: '/api/pusher/auth', transport: 'ajax' },
});
return client;
}// app/orders/[id]/order-status.tsx
'use client';
import { useEffect, useState } from 'react';
import { getPusher } from '@/lib/pusher-client';
export function OrderStatus({ id, initial }: { id: string; initial: string }) {
const [status, setStatus] = useState(initial);
useEffect(() => {
const name = `private-order-${id}`;
const channel = getPusher().subscribe(name);
const onStatus = (data: { status: string }) => setStatus(data.status);
channel.bind('status', onStatus);
return () => {
channel.unbind('status', onStatus);
getPusher().unsubscribe(name);
};
}, [id]);
return <p>{status}</p>;
}Server component render trạng thái hiện tại và truyền nó làm initial; client component giữ cho nó luôn mới. Client được tạo bên trong useEffect, nên không có kết nối nào trong lúc render ở máy chủ.
Mẹo
- Giữ route cấp quyền và mã phát trên runtime Node.js (mặc định): thư viện
pusherdùngcryptocủa Node. - Để bỏ qua người thực hiện thay đổi, truyền
getPusher().connection.socket_idvào server action, nhưshipOrderở trên. - Nếu nhiều component dùng chung một kênh, hãy đăng ký một lần ở component cha chung, hoặc đếm số người dùng trước khi hủy đăng ký, để một component bị unmount không cắt luôn các component khác.
- Để tái sử dụng server component thay vì gửi dữ liệu, gọi
router.refresh()khi event tới và để Next.js lấy bản render mới. - Sau khi đăng nhập, hướng dẫn Node.js và hướng dẫn JavaScript có sẵn các cài đặt này với giá trị của ứng dụng bạn.