Hacker News Nổi bật (buzzing.cc bản dịch tiếng Trung)
Điểm AI 44/100

Sản phẩm

Topcoat v0.9 ra mắt: Khung làm việc Rust toàn năng bổ sung tính năng phản hồi phía client và streaming UI

(giờ Việt Nam)

Tóm tắt AI

Topcoat v0.9 nâng cấp khả năng phản hồi phía client thông qua signal và tối ưu hóa cập nhật DOM mà không mất trạng thái. Phiên bản này còn hỗ trợ streaming SSR và suspense, cho phép đẩy cập nhật giao diện trực tiếp từ server.

Chính văn · Bản dịch AI

Hai tháng trước, chúng tôi (Julien và tôi) đã công bố Topcoat, một framework Rust full-stack "tất cả trong một". Nó bao gồm các view, component, mailer, một ORM (Toasty) và nhiều tính năng khác. Topcoat hướng tới mục tiêu giúp việc xây dựng ứng dụng web bằng Rust đạt hiệu suất cao như bất kỳ ngôn ngữ nào khác. Chúng tôi đã làm việc chăm chỉ để phát hành các tính năng mới, vì vậy đây là thời điểm thích hợp để chia sẻ về những cập nhật này.

Tôi bắt đầu sự nghiệp của mình với tư cách là một lập trình viên phần mềm chuyên nghiệp, xây dựng các ứng dụng web bằng Ruby on Rails. Vào thời điểm đó, video “xây dựng blog trong 15 phút” thực sự là một bước ngoặt. Hồi đó, việc xây dựng phần mềm rất tẻ nhạt: phải viết code boilerplate thay vì tập trung vào các tính năng. Ruby on Rails đã thách thức điều đó và chứng minh rằng bạn có thể làm việc hiệu quả và việc xây dựng phần mềm cũng có thể rất thú vị. Tôi đã yêu thích Ruby on Rails, làm việc trong đội ngũ nòng cốt vài năm và hiện vẫn nằm trong top 50 người đóng góp nhiều nhất mọi thời đại cho Ruby on Rails. Đã trải qua giai đoạn đó, thật khó để nói hết tầm ảnh hưởng của triết lý Ruby on Rails đối với sự phát triển phần mềm nói chung.

Kể từ đó, tôi đã dành khoảng 13 năm qua để xây dựng hệ sinh thái mạng của Rust. Mặc dù Rust đã đạt được sự chấp nhận rộng rãi ở cấp độ hạ tầng, tôi vẫn luôn hướng tới việc phát triển ứng dụng ở cấp độ cao hơn, bao gồm cả không gian mà Ruby on Rails đang chiếm lĩnh. Tôi muốn nắm bắt một phần sự kỳ diệu mà tôi từng cảm nhận khi xây dựng các ứng dụng Ruby on Rails, nhưng với Rust (ai mà không thích những ứng dụng cực kỳ nhanh và chỉ tốn ~20MB RAM chứ?).

Tôi sẽ là người đầu tiên thừa nhận rằng Rust không thanh thoát bằng một số ngôn ngữ hiện đại khác, nhưng tôi chắc chắn thà làm việc với Rust còn hơn là phải chịu đựng sự khó chịu. Ngoài ra, hầu hết các ứng dụng cấp cao đều có thể hoạt động tốt mà không cần sử dụng quá nhiều lifetimes và generics. Rust rất giàu tính biểu đạt. Các ứng dụng được xây dựng bằng Rust và các thư viện được thiết kế tốt có thể trông rất tuyệt vời.

Rust là ngôn ngữ đa mục đích tốt nhất cho thế giới mới của sự phát triển dựa trên AI. Điều đó bao gồm việc xây dựng các ứng dụng web hoặc bất kỳ ứng dụng máy chủ nào. Vai trò của các thư viện và framework trong thế giới mới này vẫn chưa được định hình rõ ràng. Chi phí để từ bỏ một thư viện hoặc framework và sử dụng giải pháp tùy chỉnh đã giảm xuống, nhưng chưa hoàn toàn biến mất. Chúng sẽ tiếp tục đóng một vai trò quan trọng, nhưng với một cách thức hơi khác biệt. Các quy ước và trừu tượng hóa được xác định rõ ràng sẽ giúp LLM làm việc nhanh hơn, với ít token hơn và ít lỗi hơn. Đó chính là lý do cơ bản tại sao tôi vẫn thúc đẩy một framework "tất cả trong một" cho Rust.

Tôi đã hợp tác với Julien, người đang dẫn dắt thiết kế front-end của Topcoat trong khi tôi chủ yếu tập trung vào Toasty và tầng DB. Chúng tôi vẫn đang tìm hiểu chính xác framework mới này nên trông như thế nào, nhưng nó đang dần hình thành một diện mạo rất ấn tượng.

Với những điều đó, có gì mới ở Topcoat và Toasty?

Tính phản ứng phía client trong Topcoat 0.9 trở đi

Hôm nay, chúng tôi đã phát hành Topcoat v0.9.

Khi bắt đầu xây dựng một framework web, bạn thường chọn xây dựng trình render phía trình duyệt hoặc phía máy chủ. Nếu bắt đầu với render trình duyệt, thách thức sẽ là lấy dữ liệu từ máy chủ về trình duyệt và render trang ban đầu nhanh nhất có thể. Nếu bắt đầu ở máy chủ, việc truy cập dữ liệu và tải trang ban đầu trở nên dễ dàng, nhưng độ trễ và khả năng phản hồi phía client lại trở thành nút thắt cổ chai. Dù bắt đầu từ đâu, bạn thường sẽ tiến dần về phía trung tâm để đạt được lợi ích tốt nhất từ cả hai thế giới. Chúng tôi tin rằng server-side rendering là mặc định tốt nhất cho hầu hết các ứng dụng web, nhưng chúng tôi muốn đảm bảo rằng bạn có thể chuyển sang các giao diện tương tác hoàn toàn, không độ trễ khi cần thiết.

Theo dõi các tín hiệu client trên máy chủ

Topcoat bắt đầu với các tín hiệu (signals) và một cú pháp biểu thức runtime đặc biệt bên trong macro view!: một tập con của Rust được kiểm tra kiểu đầy đủ, có thể được chuyển đổi sang JavaScript và chạy lại trong trình duyệt. Ý tưởng là giữ phần lớn logic render và logic nghiệp vụ trên máy chủ, và chỉ thêm các biểu thức runtime để thu hẹp khoảng cách độ trễ, ví dụ như hiển thị vòng quay tải hoặc thay đổi một số thuộc tính class. Với một hệ thống như thế này, bạn có thể xây dựng tính tương tác cơ bản mà không cần phải gửi yêu cầu qua lại với máy chủ:

Mã
#[page]
pub async fn page(cx: &Cx) -> Result<impl View> {
    // This state variable is initialized on the server, but lives in the browser.
    let count = signal(cx, || 0i32);

    Ok(view! {
        <button @click=$(|_e| count.increment())>"increment"</button>
        <button @click=$(|_e| count.decrement())>"decrement"</button>

        // The count updates when clicking the buttons. No server roundtrip.
        <div>$(count.get())</div>
    })
}

Tuy nhiên, bản thân các biểu thức runtime của Topcoat vẫn chưa đủ khi bạn cần thay đổi cấu trúc của chính mã đánh dấu (markup). Để khắc phục điều này, chúng tôi đã thêm các shard, một loại component đặc biệt có thể được render lại trên máy chủ bất cứ khi nào các đối số của chúng thay đổi:

Mã
#[component]
async fn search(cx: &Cx) -> Result<impl View> {
    let query = signal(cx, String::new);

    Ok(view! {
        <input @input=$(|e: Event| query.set(e.target.value))>

        // Search results are updated as the input changes.
        search_results(query: $(query.get()))
    })
}

#[shard]
async fn search_results(cx: &Cx, query: String) -> Result<impl View> {
    // This markup is rendered on the server and can access the database.
    Ok(view! {
        <ul>
            for product in search_products(cx, &query).await? {
                <li>(product.name)</li>
            }
        </ul>
    })
}

Khi tín hiệu truy vấn thay đổi, trình duyệt sẽ gửi giá trị hiện tại của nó đến máy chủ, máy chủ sẽ chạy lại shard và phản hồi bằng HTML đã cập nhật.

Trong Topcoat 0.8, việc phản ứng với các thay đổi tín hiệu đã trở nên dễ dàng hơn. Giờ đây, bạn có thể đọc một tín hiệu trong khi render UI trên máy chủ. Vì kết quả phụ thuộc vào giá trị của các tín hiệu, Topcoat sẽ chỉ tải lại những phần trang của bạn có theo dõi giá trị tín hiệu đó:

Mã
#[shard]
pub async fn search(cx: &Cx) -> Result<impl View> {
    let query = signal(cx, String::new);
    // The signal is read here, meaning the shard will re-run
    // on the server whenever the browser-side state changes.
    let products = search_products(cx, &query.get()).await?;

    Ok(view! {
        // The input event handler still runs in the browser.
        <input @input=$(|e: Event| query.set(e.target.value))>

        <ul>
            for product in products {
                <li>(product.name)</li>
            }
        </ul>
    })
}

Các phần tử HTML được cập nhật sẽ được biến đổi (morph) để tránh mất tiêu điểm hoặc trạng thái nhập liệu. Ngay cả toàn bộ trang cũng có thể được render lại để phản hồi với thay đổi tín hiệu, mang lại khả năng biểu đạt cao hơn đáng kể.

Streaming các thay đổi UI từ máy chủ

Phản hồi với các thay đổi trạng thái client trên máy chủ là rất tốt, nhưng nếu bạn muốn cập nhật UI để phản hồi với thay đổi trạng thái phía máy chủ thì sao? Đối với trường hợp này, Topcoat cung cấp các macro live! và emit!. Một live! view là một loại view! đặc biệt có thể phát ra các cập nhật UI không giới hạn.

Một ví dụ đơn giản là “streaming SSR” hoặc “suspense”. Mục tiêu là render một khung tải (loading skeleton) nhanh nhất có thể trong khi chờ dữ liệu. Khi dữ liệu đã sẵn sàng, bạn có thể thay thế bằng nội dung trang thực tế. Topcoat cung cấp sẵn các component suspense và error_boundary hoạt động tương tự như React và các framework web khác. Tuy nhiên, bạn có thể đạt được hiệu ứng tương tự với một live view:

Mã
#[page]
pub async fn page() -> Result<impl View> {
    Ok(live! {
        // First, emit a loading indicator.
        emit! { <p>"Loading..."</p> }?;

        // Then, load the data.
        let content = load_content().await;

        // Finally, swap in the full UI.
        emit! { <p>(content)</p> }
    })
}

Một trường hợp sử dụng nâng cao hơn là phát ra một chỉ báo tiến trình cập nhật nhiều lần khi trang đang tải:

Mã
#[page]
pub async fn page(cx: &Cx) -> Result<impl View> {
    Ok(live! {
        // Start at 0%.
        emit! { <p>"Working... 0%"</p> }?;

        while let Some(progress) = load_more_data(cx).await? {
            // Each time new data arrives, we update the progress indicator.
            emit! {
                <p>
                    "Working... "
                    (progress.percent)
                    "%"
                </p>
            }?;
        }

        emit! { <p>"Done!"</p> }
    })
}

Bắt đầu từ phiên bản 0.9, Topcoat cũng hỗ trợ server-push. Thay vì chỉ streaming cho lần tải trang đầu tiên, Topcoat có thể mở kết nối WebSocket từ trình duyệt đến máy chủ và đăng ký các thay đổi UI qua các kết nối tồn tại lâu dài. Điều này hữu ích, ví dụ, cho giao diện trò chuyện:

Mã
#[component]
pub async fn chat_messages(cx: &Cx) -> Result<impl View> {
    Ok(live! {
        let chat = app_context::<Chat>(cx);
        let mut changed = chat.subscribe();
        loop {
            // Render the current chat state.
            let token = emit! {
                <ul>
                    for message in chat.messages() {
                        <li>(message)</li>
                    }
                </ul>
            }?;

            // During the initial page load, only render once.
            if !connected(cx) {
                break Ok(token);
            }

            // Wait for changes, then re-render the chat box.
            changed.recv().await.ok();
        }
    })
}

Toasty, DB client của Topcoat

Toasty đã nhận được nhiều cập nhật nhỏ trong suốt các tháng qua. Tôi sẽ chỉ nêu bật một vài trong số đó một cách nhanh chóng.

Cập nhật giàu tính biểu đạt

Procedural macro là một trong những tính năng "sát thủ" của Rust. Một API dựa trên procedural macro có thể rất giàu tính biểu đạt trong khi vẫn đảm bảo an toàn kiểu dữ liệu. Toasty sử dụng chúng rất nhiều để giảm thiểu boilerplate khi truy vấn, tạo và cập nhật. Chúng tôi gần đây đã thêm macro update!:

Mã
#[derive(Model)]
struct User {
    #[key]
    #[auto]
    id: i64,

    name: String,
    login_count: i64,
}

// let mut user = ...;

toasty::update!(user {
    name: "Alicia",
    login_count.increment(),
})
.exec(&mut db)
.await?;

Lệnh này thực hiện một cập nhật duy nhất giúp thiết lập trường name và tăng login_count trong cơ sở dữ liệu (mà không cần tải nó trước, tương tự như SET login_count = login_count + 1).

Các trường tài liệu (Document fields)

Toasty không chỉ dành cho dữ liệu quan hệ. Các cơ sở dữ liệu dựa trên tài liệu rất phổ biến và hầu hết các cơ sở dữ liệu quan hệ (bao gồm cả PostgreSQL) đều hỗ trợ các kiểu tài liệu như JSON, bao gồm cả khả năng truy vấn các cột tài liệu. Toasty đang bổ sung hỗ trợ hạng nhất cho tính năng đó. Dưới đây là một ví dụ nhanh:

Mã
#[derive(Model)]
struct User {
    #[key]
    #[auto]
    id: i64,

    name: String,

    #[document]
    settings: Settings,
}

#[derive(Embed)]
struct Settings {
    theme: String,
    notifications: bool,
}

let users = User::filter(
    User::fields().settings().theme().eq("dark"),
)
.exec(&mut db)
.await?;

Trường cài đặt của người dùng được mã hóa dưới dạng JSONB trong PostgreSQL và bộ lọc sử dụng các khả năng lọc JSONB của PostgreSQL. Truy vấn bộ lọc trông như sau:

Mã
SELECT
    users.id,
    users.name,
    users.settings
FROM users
WHERE users.settings->>'theme' = $1;

Quan hệ đa hình (Polymorphic relations)

Quan hệ đa hình là các mối quan hệ mà mục tiêu có thể là một trong nhiều kiểu dữ liệu khác nhau. Tôi chưa bao giờ thích cách chúng hoạt động trong các ORM mà tôi từng sử dụng trước đây. Tuy nhiên, có những lý do chính đáng để cần đến chúng. Sau đó, tôi nhận ra rằng về cơ bản bạn có thể đạt được các quan hệ đa hình với Toasty bằng cách kết hợp enum với các quan hệ thông thường.

Bài gốc còn tiếp — xem tiếp tại bài gốc ↗

RustWeb DevelopmentTopcoatStreaming SSRFramework

Bài viết được AI dịch và tổng hợp tự động từ Hacker News Nổi bật (buzzing.cc bản dịch tiếng Trung). Liên kết bài gốc ở phía trên. Dữ liệu đồng bộ qua API công khai được ghi nguồn tại AI HOT (canonical) ↗. AIHOT.vn luôn dẫn nguồn đầy đủ — nếu bạn thấy điểm cần chỉnh sửa, hãy gửi ý kiến tại trang phản hồi.

Topcoat v0.9 ra mắt: Khung làm việc Rust toàn năng bổ sung tính năng phản hồi phía client và streaming UI | AIHOT.vn