Nguon: Microsoft Learn · .NET 8.0

ASP.NET Core SignalR JavaScript client (Máy khách JavaScript)

Nguồn: ASP.NET Core SignalR JavaScript client

Thư viện máy khách JavaScript (JavaScript client library) của ASP.NET Core SignalR cho phép lập trình viên gọi code hub (trung tâm) phía máy chủ. Bài viết này mô tả cách sử dụng các API để kết nối với SignalR hub và gọi các phương thức hub và client JavaScript.

Xem hoặc tải code mẫu

Cài đặt gói SignalR client

Thư viện JavaScript client của SignalR được phân phối dưới dạng gói npm. Có một số cách để cài đặt thư viện client:

Cài đặt bằng npm

Visual Studio: Chạy các lệnh npm sau trong cửa sổ Package Manager Console:

bash
npm init -y
npm install @microsoft/signalr

Visual Studio Code: Mở terminal tích hợp và chạy các lệnh npm sau:

bash
npm init -y
npm install @microsoft/signalr

npm cài đặt nội dung gói vào thư mục node\_modules\@microsoft\signalr\dist\browser.

Hoàn tất thiết lập:

  1. Tạo thư mục wwwroot/lib/signalr.
  2. Sao chép tệp signalr.js vào thư mục wwwroot/lib/signalr.

Bây giờ bạn có thể tham chiếu đến SignalR JavaScript client đã cài đặt trong phần tử <script>. Ví dụ:

html
<script src="~/lib/signalr/signalr.js"></script>

Sử dụng CDN

Để sử dụng thư viện client mà không cần npm, tham chiếu bản sao thư viện client trên CDN. Ví dụ:

html
<script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/6.0.1/signalr.js"></script>

Ví dụ trên chỉ định phiên bản 6.0.1. Để lấy phiên bản thư viện client mới nhất, chọn một trong các CDN sau:

Cài đặt bằng LibMan

Một cách khác là sử dụng LibMan để chỉ cài đặt các tệp thư viện client cụ thể từ thư viện client trên CDN. Ví dụ, bạn có thể chỉ thêm tệp JavaScript đã được rút gọn (minified) vào dự án.

Chi tiết về cách này, xem Add the SignalR client library.

Kết nối đến hub

Code sau tạo và bắt đầu kết nối. Tên hub không phân biệt hoa thường:

javascript
const connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub")
    .configureLogging(signalR.LogLevel.Information)
    .build();

async function start() {
    try {
        await connection.start();
        console.log("SignalR Connected.");
    } catch (err) {
        console.log(err);
        setTimeout(start, 5000);
    }
};

connection.onclose(async () => {
    await start();
});

// Start the connection.
start();

Kết nối cross-origin (CORS)

Thông thường, trình duyệt tải kết nối từ cùng domain với trang được yêu cầu. Tuy nhiên, đôi khi cần kết nối đến domain khác.

Đối với yêu cầu cross domain, code client phải sử dụng URL tuyệt đối thay vì URL tương đối. Khi sử dụng yêu cầu cross domain, thay đổi .withUrl("/chathub") thành .withUrl("https://{tên domain ứng dụng}/chathub").

Để ngăn một trang web độc hại đọc dữ liệu nhạy cảm từ trang khác, kết nối cross-origin bị tắt theo mặc định. Để cho phép yêu cầu cross-origin, bật CORS:

csharp
using SignalRChat.Hubs;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddSignalR();

builder.Services.AddCors(options =>
{
    options.AddDefaultPolicy(
        builder =>
        {
            builder.WithOrigins("https://example.com")
                .AllowAnyHeader()
                .WithMethods("GET", "POST")
                .AllowCredentials();
        });
});

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

// UseCors must be called before MapHub.
app.UseCors();

app.MapRazorPages();
app.MapHub<ChatHub>("/chatHub");

app.Run();

Phương thức UseCors phải được gọi trước khi gọi phương thức MapHub.

Gọi phương thức hub từ client

JavaScript client gọi các phương thức public trên hub thông qua phương thức invoke của HubConnection. Phương thức invoke nhận:

Trong ví dụ sau, tên phương thức trên hub là SendMessage. Đối số thứ hai và thứ ba truyền vào invoke ánh xạ tới các đối số usermessage của phương thức hub:

javascript
try {
    await connection.invoke("SendMessage", user, message);
} catch (err) {
    console.error(err);
}

Việc gọi phương thức hub từ client chỉ được hỗ trợ khi sử dụng Azure SignalR Service ở chế độ Default. Để biết thêm thông tin, xem Frequently Asked Questions (azure-signalr GitHub repository).

Phương thức invoke trả về một đối tượng JavaScript Promise. Đối tượng Promise được giải quyết (resolved) với giá trị trả về (nếu có) khi phương thức trên máy chủ trả về. Nếu phương thức trên máy chủ ném lỗi, đối tượng Promise bị từ chối (rejected) với thông báo lỗi. Sử dụng async/await hoặc các phương thức then/catch của đối tượng Promise để xử lý các trường hợp này.

JavaScript client cũng có thể gọi các phương thức public trên hub thông qua phương thức send của HubConnection. Khác với phương thức invoke, phương thức send không chờ phản hồi từ máy chủ. Phương thức send trả về một đối tượng JavaScript Promise. Promise được giải quyết khi tin nhắn được gửi đến máy chủ. Nếu có lỗi khi gửi tin nhắn, Promise bị từ chối với thông báo lỗi.

Sử dụng send không chờ đợi cho đến khi máy chủ nhận được tin nhắn, vì vậy không thể trả về dữ liệu hay lỗi từ máy chủ.

Gọi phương thức client từ hub

Để nhận tin nhắn từ hub, định nghĩa một phương thức bằng phương thức on của HubConnection. Phương thức on nhận:

Trong ví dụ sau, tên phương thức là ReceiveMessage. Tên đối số là usermessage:

javascript
connection.on("ReceiveMessage", (user, message) => {
    const li = document.createElement("li");
    li.textContent = `${user}: ${message}`;
    document.getElementById("messageList").appendChild(li);
});

Code trong connection.on chạy khi code phía máy chủ gọi nó bằng phương thức SendAsync:

csharp
using Microsoft.AspNetCore.SignalR;
namespace SignalRChat.Hubs;

public class ChatHub : Hub
{
    public async Task SendMessage(string user, string message)
    {
        await Clients.All.SendAsync("ReceiveMessage", user, message);
    }
}

SignalR xác định phương thức client nào cần gọi bằng cách khớp tên phương thức và các đối số được định nghĩa trong SendAsyncconnection.on.

Thực hành tốt nhất là gọi phương thức start trên HubConnection sau on. Cách này đảm bảo các trình xử lý (handlers) được đăng ký trước khi nhận bất kỳ tin nhắn nào.

Xử lý lỗi và ghi log

Sử dụng console.error để xuất lỗi ra console của trình duyệt khi client không thể kết nối hoặc gửi tin nhắn:

javascript
try {
    await connection.invoke("SendMessage", user, message);
} catch (err) {
    console.error(err);
}

Thiết lập theo dõi log (log tracing) phía client bằng cách truyền logger và loại sự kiện cần log khi kết nối được tạo. Tin nhắn được ghi log ở mức log được chỉ định và cao hơn. Các mức log có sẵn:

Mức logDữ liệu được ghiMô tả
signalR.LogLevel.ErrorTin nhắn lỗiChỉ ghi log tin nhắn Error.
signalR.LogLevel.WarningTin nhắn cảnh báo về lỗi tiềm ẩnGhi log tin nhắn WarningError.
signalR.LogLevel.InformationTin nhắn trạng thái không có lỗiGhi log tin nhắn Information, WarningError.
signalR.LogLevel.TraceTin nhắn traceGhi log tất cả mọi thứ, bao gồm dữ liệu được truyền giữa hub và client.

Sử dụng phương thức configureLogging trên HubConnectionBuilder để cấu hình mức log. Tin nhắn được ghi log ra console của trình duyệt:

javascript
const connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub")
    .configureLogging(signalR.LogLevel.Information)
    .build();

Kết nối lại client

Để kết nối lại với client, bạn có thể thiết lập kết nối lại tự động hoặc cấu hình kết nối thủ công.

Tự động kết nối lại

JavaScript client cho SignalR có thể được cấu hình để tự động kết nối lại bằng cách sử dụng phương thức WithAutomaticReconnect trên HubConnectionBuilder. Theo mặc định, nó không tự động kết nối lại.

javascript
const connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub")
    .withAutomaticReconnect()
    .build();

Khi không có tham số, WithAutomaticReconnect cấu hình client để chờ 0, 2, 10 và 30 giây tương ứng trước khi thử mỗi lần kết nối lại. Sau bốn lần thất bại, nó ngừng thử kết nối lại.

Trước khi bắt đầu bất kỳ lần kết nối lại nào, HubConnection:

Cách tiếp cận kết nối lại này cung cấp cơ hội để cảnh báo người dùng rằng kết nối bị mất và vô hiệu hóa các phần tử UI.

javascript
connection.onreconnecting(error => {
    console.assert(connection.state === signalR.HubConnectionState.Reconnecting);

    document.getElementById("messageInput").disabled = true;

    const li = document.createElement("li");
    li.textContent = `Connection lost due to error "${error}". Reconnecting.`;
    document.getElementById("messageList").appendChild(li);
});

Nếu client kết nối lại thành công trong bốn lần thử đầu tiên, HubConnection chuyển trở lại trạng thái Connected và kích hoạt các callback onreconnected. Điều này cung cấp cơ hội để thông báo cho người dùng rằng kết nối đã được thiết lập lại.

Vì kết nối trông hoàn toàn mới đối với máy chủ, một connectionId mới được cung cấp cho callback onreconnected.

Tham số connectionId của callback onreconnectedundefined nếu HubConnection được cấu hình để bỏ qua đàm phán.

javascript
connection.onreconnected(connectionId => {
    console.assert(connection.state === signalR.HubConnectionState.Connected);

    document.getElementById("messageInput").disabled = false;

    const li = document.createElement("li");
    li.textContent = `Connection reestablished. Connected with connectionId "${connectionId}".`;
    document.getElementById("messageList").appendChild(li);
});

withAutomaticReconnect không cấu hình HubConnection để thử lại các lỗi khởi động ban đầu, vì vậy các lỗi khởi động cần được xử lý thủ công:

javascript
async function start() {
    try {
        await connection.start();
        console.assert(connection.state === signalR.HubConnectionState.Connected);
        console.log("SignalR Connected.");
    } catch (err) {
        console.assert(connection.state === signalR.HubConnectionState.Disconnected);
        console.log(err);
        setTimeout(() => start(), 5000);
    }
};

Nếu client không kết nối lại thành công trong bốn lần thử đầu tiên, HubConnection chuyển sang trạng thái Disconnected và kích hoạt các callback onclose. Điều này cung cấp cơ hội để thông báo cho người dùng rằng kết nối đã bị mất vĩnh viễn và đề xuất làm mới trang:

javascript
connection.onclose(error => {
    console.assert(connection.state === signalR.HubConnectionState.Disconnected);

    document.getElementById("messageInput").disabled = true;

    const li = document.createElement("li");
    li.textContent = `Connection closed due to error "${error}". Try refreshing this page to restart the connection.`;
    document.getElementById("messageList").appendChild(li);
});

Để cấu hình số lần kết nối lại tùy chỉnh trước khi ngắt kết nối hoặc thay đổi thời gian kết nối lại, withAutomaticReconnect nhận một mảng số đại diện cho thời gian trễ tính bằng mili giây để chờ trước khi bắt đầu mỗi lần kết nối lại.

javascript
const connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub")
    .withAutomaticReconnect([0, 0, 10000])
    .build();

    // .withAutomaticReconnect([0, 2000, 10000, 30000]) yields the default behavior

Ví dụ trên cấu hình HubConnection để bắt đầu thử kết nối lại ngay lập tức sau khi kết nối bị mất.

Để kiểm soát nhiều hơn về thời gian và số lần kết nối lại tự động, withAutomaticReconnect nhận một đối tượng triển khai interface IRetryPolicy, có một phương thức duy nhất tên là nextRetryDelayInMilliseconds. nextRetryDelayInMilliseconds nhận một đối số kiểu RetryContext. RetryContext có ba thuộc tính: previousRetryCount (kiểu number), elapsedMilliseconds (kiểu number), và retryReason (kiểu Error).

nextRetryDelayInMilliseconds phải trả về một số đại diện cho số mili giây cần chờ trước lần kết nối lại tiếp theo hoặc null nếu HubConnection nên ngừng kết nối lại.

javascript
const connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub")
    .withAutomaticReconnect({
        nextRetryDelayInMilliseconds: retryContext => {
            if (retryContext.elapsedMilliseconds < 60000) {
                // If we've been reconnecting for less than 60 seconds so far,
                // wait between 0 and 10 seconds before the next reconnect attempt.
                return Math.random() * 10000;
            } else {
                // If we've been reconnecting for more than 60 seconds so far, stop reconnecting.
                return null;
            }
        }
    })
    .build();

Ngoài ra, bạn có thể viết code để kết nối lại client thủ công như được minh họa trong phần tiếp theo.

Kết nối lại thủ công

Đoạn code sau minh họa cách tiếp cận kết nối lại thủ công điển hình:

  1. Để bắt đầu kết nối, tạo một hàm. Trong trường hợp này là hàm start.
  2. Gọi hàm start trong trình xử lý sự kiện onclose của kết nối.
javascript
async function start() {
    try {
        await connection.start();
        console.log("SignalR Connected.");
    } catch (err) {
        console.log(err);
        setTimeout(start, 5000);
    }
};

connection.onclose(async () => {
    await start();
});

Các triển khai trong môi trường production thường sử dụng chiến lược back-off theo cấp số nhân hoặc thử lại một số lần nhất định.

Tab ngủ của trình duyệt (Browser sleeping tab)

Một số trình duyệt có tính năng đóng băng tab (tab freezing) hoặc tab ngủ (tab sleeping) để giảm mức sử dụng tài nguyên máy tính cho các tab không hoạt động. Tuy nhiên, tính năng này có thể khiến các kết nối SignalR bị đóng và gây ra trải nghiệm người dùng không mong muốn.

Các trình duyệt sử dụng heuristics (phương pháp thử nghiệm) để xác định xem một tab có nên được đưa vào trạng thái ngủ hay không, chẳng hạn như:

Các heuristics có thể thay đổi theo thời gian và cũng khác nhau giữa các trình duyệt. Kiểm tra ma trận hỗ trợ và tìm ra phương pháp nào phù hợp nhất cho tình huống của bạn.

Để tránh ứng dụng bị đưa vào trạng thái ngủ, ứng dụng nên kích hoạt một trong các heuristics mà trình duyệt sử dụng.

Ví dụ code sau cho thấy cách sử dụng Web Lock để giữ tab thức và tránh đóng kết nối bất ngờ:

javascript
var lockResolver;
if (navigator && navigator.locks && navigator.locks.request) {
    const promise = new Promise((res) => {
        lockResolver = res;
    });

    navigator.locks.request('unique_lock_name', { mode: "shared" }, () => {
        return promise;
    });
}

Trong code trên: