Rust WASM ile Tarayıcı DOM API Kullanımı

Sistem yöneticisi olarak sunucu tarafında Rust kullanmaya alışkınsınız, ama Rust’ın WebAssembly dünyasındaki gücü artık tarayıcıya da taşınıyor. DOM manipülasyonu, event handling, asenkron işlemler… Bunların hepsini artık Rust ile yapabiliyorsunuz. Bu yazıda, Rust WASM ile tarayıcı DOM API’sini nasıl kullanacağınızı, gerçek dünya senaryolarıyla birlikte ele alacağız.

Neden Rust ile DOM Manipülasyonu?

JavaScript zaten DOM ile konuşabiliyor, neden Rust kullanalım diye sorabilirsiniz. Birkaç somut neden var:

  • Performans kritik işlemler: Büyük veri tabloları, gerçek zamanlı grafikler veya karmaşık hesaplamalar içeren UI bileşenleri
  • Tip güvenliği: Runtime hatalarını derleme zamanında yakalamak
  • Kod paylaşımı: Sunucu tarafında yazdığınız Rust kodunu tarayıcıya taşımak
  • Memory güvenliği: Garbage collector olmadan verimli bellek yönetimi

Rust’ın wasm-bindgen ekosistemi, DOM API’lerine oldukça temiz bir arayüz sunuyor. web-sys crate’i ise W3C web API’lerini neredeyse bire bir Rust’a çeviriyor.

Ortam Kurulumu

Önce gerekli araçları kuralım. Bu kısımı atlamayın, eksik kurulumlar ilerleyen adımlarda sinir bozucu hatalar üretiyor.

# Rust kurulu değilse
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source $HOME/.cargo/env

# WASM target ekle
rustup target add wasm32-unknown-unknown

# wasm-pack kur
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh

# trunk kur (geliştirme sunucusu olarak kullanacağız)
cargo install trunk

# cargo-generate ile proje şablonu kullanmak istersen
cargo install cargo-generate

Yeni bir proje oluşturalım:

# Boş proje oluştur
cargo new --lib rust-dom-demo
cd rust-dom-demo

# Proje yapısına bakalım
ls -la

Cargo.toml Yapılandırması

Cargo.toml dosyası bu projenin kalbi. web-sys feature’larını dikkatli seçmek gerekiyor, çünkü her feature derleme süresini ve binary boyutunu etkiliyor.

cat > Cargo.toml << 'EOF'
[package]
name = "rust-dom-demo"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"
js-sys = "0.3"
serde = { version = "1.0", features = ["derive"] }
serde-wasm-bindgen = "0.6"

[dependencies.web-sys]
version = "0.3"
features = [
  "Window",
  "Document",
  "Element",
  "HtmlElement",
  "HtmlButtonElement",
  "HtmlInputElement",
  "HtmlDivElement",
  "Node",
  "NodeList",
  "EventTarget",
  "Event",
  "MouseEvent",
  "KeyboardEvent",
  "console",
  "CssStyleDeclaration",
  "Location",
  "Storage",
]

[profile.release]
opt-level = "s"
EOF

Temel DOM Erişimi

İlk kod örneğimizde temel DOM işlemlerini görelim. src/lib.rs dosyasını oluşturalım:

cat > src/lib.rs << 'EOF'
use wasm_bindgen::prelude::*;
use web_sys::{window, Document, Element, HtmlElement};

// Tarayıcı konsoluna log basmak için yardımcı makro
macro_rules! console_log {
    ($($t:tt)*) => (web_sys::console::log_1(&format!($($t)*).into()))
}

// Document nesnesine güvenli erişim
fn get_document() -> Document {
    window()
        .expect("Window bulunamadi")
        .document()
        .expect("Document bulunamadi")
}

#[wasm_bindgen]
pub fn initialize() {
    // Panic hook - hata ayıklamayı kolaylaştırır
    console_error_panic_hook::set_once();
    console_log!("Rust WASM baslatildi!");
}

#[wasm_bindgen]
pub fn manipulate_dom() {
    let document = get_document();
    
    // Var olan elementi seç
    let body = document.body().expect("Body bulunamadi");
    
    // Yeni div oluştur
    let div = document
        .create_element("div")
        .expect("Div olusturulamadi");
    
    // Attribute ekle
    div.set_attribute("id", "rust-container").unwrap();
    div.set_attribute("class", "rust-panel").unwrap();
    
    // İç içerik ekle
    div.set_inner_html("<h2>Rust'tan merhaba!</h2><p>Bu eleman Rust WASM tarafından oluşturuldu.</p>");
    
    // CSS style ekle
    if let Some(html_el) = div.dyn_ref::<HtmlElement>() {
        html_el.style().set_property("background", "#1a1a2e").unwrap();
        html_el.style().set_property("color", "#eee").unwrap();
        html_el.style().set_property("padding", "20px").unwrap();
        html_el.style().set_property("border-radius", "8px").unwrap();
    }
    
    // DOM'a ekle
    body.append_child(&div).unwrap();
    
    console_log!("DOM manipülasyonu tamamlandi");
}

#[wasm_bindgen]
pub fn query_selector_demo() {
    let document = get_document();
    
    // querySelector kullanımı
    if let Some(element) = document.query_selector("#main-content").unwrap() {
        element.set_inner_html("<p>Rust tarafından güncellendi!</p>");
        console_log!("Element güncellendi: {}", element.id());
    }
    
    // querySelectorAll kullanımı
    let node_list = document
        .query_selector_all(".item-card")
        .expect("QuerySelectorAll basarisiz");
    
    console_log!("Bulunan eleman sayisi: {}", node_list.length());
    
    for i in 0..node_list.length() {
        if let Some(node) = node_list.item(i) {
            if let Some(el) = node.dyn_ref::<Element>() {
                el.set_attribute("data-processed", "true").unwrap();
            }
        }
    }
}
EOF

Event Listener Ekleme

Event handling, DOM API kullanımının en kritik kısmı. Rust’ta closure’lar ve lifetime’lar burada devreye giriyor:

cat > src/events.rs << 'EOF'
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use web_sys::{
    window, Document, Event, HtmlButtonElement, 
    HtmlInputElement, KeyboardEvent, MouseEvent
};

fn get_document() -> Document {
    window().unwrap().document().unwrap()
}

#[wasm_bindgen]
pub fn setup_button_handler() {
    let document = get_document();
    
    // Butonu bul
    let button = document
        .get_element_by_id("action-button")
        .expect("Buton bulunamadi")
        .dyn_into::<HtmlButtonElement>()
        .expect("HtmlButtonElement'e cast basarisiz");
    
    // Click handler oluştur - Box::new ile heap'e al
    let handler = Closure::<dyn FnMut(MouseEvent)>::new(|event: MouseEvent| {
        // Koordinatları al
        let x = event.client_x();
        let y = event.client_y();
        
        web_sys::console::log_1(
            &format!("Tiklanma koordinatlari: ({}, {})", x, y).into()
        );
        
        // Tıklama sayacını güncelle
        let doc = window().unwrap().document().unwrap();
        if let Some(counter) = doc.get_element_by_id("click-counter") {
            let current: i32 = counter.inner_html().parse().unwrap_or(0);
            counter.set_inner_html(&(current + 1).to_string());
        }
    });
    
    // Event listener ekle
    button
        .add_event_listener_with_callback("click", handler.as_ref().unchecked_ref())
        .expect("Event listener eklenemedi");
    
    // Handler'ı düşürme (WASM çalışırken yaşamalı)
    handler.forget();
}

#[wasm_bindgen]
pub fn setup_keyboard_handler() {
    let document = get_document();
    
    let input = document
        .get_element_by_id("search-input")
        .unwrap()
        .dyn_into::<HtmlInputElement>()
        .unwrap();
    
    let handler = Closure::<dyn FnMut(KeyboardEvent)>::new(|event: KeyboardEvent| {
        // Enter tuşuna basıldığında arama yap
        if event.key() == "Enter" {
            let doc = window().unwrap().document().unwrap();
            let input = doc
                .get_element_by_id("search-input")
                .unwrap()
                .dyn_into::<HtmlInputElement>()
                .unwrap();
            
            let query = input.value();
            web_sys::console::log_1(&format!("Arama: {}", query).into());
            
            // Sonuç alanını güncelle
            if let Some(results) = doc.get_element_by_id("search-results") {
                results.set_inner_html(
                    &format!("<p>'{}' için sonuçlar yükleniyor...</p>", query)
                );
            }
        }
    });
    
    input
        .add_event_listener_with_callback("keydown", handler.as_ref().unchecked_ref())
        .unwrap();
    
    handler.forget();
}
EOF

LocalStorage ile State Yönetimi

Gerçek dünya uygulamalarında state’i saklamak gerekiyor. LocalStorage ile Rust arasındaki köprüyü kuralım:

cat > src/storage.rs << 'EOF'
use wasm_bindgen::prelude::*;
use web_sys::window;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct AppState {
    pub theme: String,
    pub language: String,
    pub user_preferences: Vec<String>,
    pub visit_count: u32,
}

impl Default for AppState {
    fn default() -> Self {
        AppState {
            theme: "dark".to_string(),
            language: "tr".to_string(),
            user_preferences: vec![],
            visit_count: 0,
        }
    }
}

fn get_storage() -> web_sys::Storage {
    window()
        .expect("Window yok")
        .local_storage()
        .expect("LocalStorage alinamadi")
        .expect("LocalStorage mevcut degil")
}

#[wasm_bindgen]
pub fn save_state(state_json: &str) -> Result<(), JsValue> {
    let storage = get_storage();
    storage.set_item("app_state", state_json)
        .map_err(|e| {
            web_sys::console::error_1(&format!("State kaydedilemedi: {:?}", e).into());
            e
        })
}

#[wasm_bindgen]
pub fn load_state() -> String {
    let storage = get_storage();
    
    match storage.get_item("app_state") {
        Ok(Some(state)) => state,
        Ok(None) => {
            // Varsayılan state döndür
            let default_state = AppState::default();
            serde_json::to_string(&default_state).unwrap_or_default()
        }
        Err(e) => {
            web_sys::console::error_1(&format!("State yuklenemedi: {:?}", e).into());
            String::new()
        }
    }
}

#[wasm_bindgen]
pub fn increment_visit_count() {
    let storage = get_storage();
    
    let count: u32 = storage
        .get_item("visit_count")
        .unwrap_or(None)
        .and_then(|s| s.parse().ok())
        .unwrap_or(0);
    
    let new_count = count + 1;
    storage.set_item("visit_count", &new_count.to_string()).unwrap();
    
    // DOM'u güncelle
    let doc = window().unwrap().document().unwrap();
    if let Some(el) = doc.get_element_by_id("visit-counter") {
        el.set_inner_html(&format!("Ziyaret sayisi: {}", new_count));
    }
}
EOF

Asenkron DOM İşlemleri

Modern web uygulamalarında async işlemler kaçınılmaz. wasm-bindgen-futures ile Rust Future’larını JavaScript Promise’lara dönüştürebilirsiniz:

cat > src/async_ops.rs << 'EOF'
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::JsFuture;
use web_sys::{window, Request, RequestInit, RequestMode, Response};
use js_sys::Promise;

// JavaScript'teki setTimeout'u simüle et
async fn sleep(ms: i32) {
    let promise = Promise::new(&mut |resolve, _| {
        window()
            .unwrap()
            .set_timeout_with_callback_and_timeout_and_arguments_0(
                &resolve, ms
            )
            .unwrap();
    });
    JsFuture::from(promise).await.unwrap();
}

#[wasm_bindgen]
pub async fn fetch_and_render(url: String, target_id: String) -> Result<(), JsValue> {
    let doc = window().unwrap().document().unwrap();
    
    // Yükleniyor göster
    if let Some(target) = doc.get_element_by_id(&target_id) {
        target.set_inner_html(r#"
            <div class="loading-spinner">
                <p>Veri yükleniyor...</p>
            </div>
        "#);
    }
    
    // HTTP isteği hazırla
    let mut opts = RequestInit::new();
    opts.method("GET");
    opts.mode(RequestMode::Cors);
    
    let request = Request::new_with_str_and_init(&url, &opts)?;
    request.headers().set("Accept", "application/json")?;
    
    // Fetch yap
    let window = window().unwrap();
    let resp_value = JsFuture::from(window.fetch_with_request(&request)).await?;
    let resp: Response = resp_value.dyn_into()?;
    
    if !resp.ok() {
        return Err(JsValue::from_str(&format!(
            "HTTP hatasi: {}", resp.status()
        )));
    }
    
    // JSON parse et
    let json = JsFuture::from(resp.json()?).await?;
    
    // Veriyi işle ve DOM'a yaz
    let json_str = js_sys::JSON::stringify(&json)
        .unwrap()
        .as_string()
        .unwrap_or_default();
    
    if let Some(target) = doc.get_element_by_id(&target_id) {
        target.set_inner_html(&format!(
            r#"<pre class="json-output">{}</pre>"#,
            json_str
        ));
    }
    
    Ok(())
}

#[wasm_bindgen]
pub async fn delayed_notification(message: String, delay_ms: i32) {
    // Gecikme uygula
    sleep(delay_ms).await;
    
    let doc = window().unwrap().document().unwrap();
    
    // Bildirim elementi oluştur
    let notification = doc.create_element("div").unwrap();
    notification.set_attribute("class", "notification").unwrap();
    notification.set_inner_html(&format!(
        r#"<span>{}</span><button onclick="this.parentElement.remove()">X</button>"#,
        message
    ));
    
    doc.body().unwrap().append_child(&notification).unwrap();
    
    // 3 saniye sonra otomatik kaldır
    sleep(3000).await;
    notification.remove();
}
EOF

HTML ve Trunk ile Projeyi Ayağa Kaldırma

Trunk, WASM projelerini geliştirirken vazgeçilmez bir araç. Otomatik rebuild ve hot reload sunuyor:

# index.html oluştur
cat > index.html << 'EOF'
<!DOCTYPE html>
<html lang="tr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Rust DOM Demo</title>
    <style>
        body { font-family: system-ui; max-width: 800px; margin: 0 auto; padding: 20px; }
        .rust-panel { margin: 20px 0; }
        .notification { 
            position: fixed; top: 20px; right: 20px;
            background: #333; color: white; padding: 15px;
            border-radius: 8px; display: flex; gap: 10px;
        }
        .loading-spinner { text-align: center; padding: 40px; }
        .json-output { background: #f4f4f4; padding: 15px; overflow-x: auto; }
    </style>
</head>
<body>
    <h1>Rust WASM DOM Demo</h1>
    <div id="main-content">
        <button id="action-button">Tikla!</button>
        <span id="click-counter">0</span>
    </div>
    <input type="text" id="search-input" placeholder="Arama yap (Enter'a bas)">
    <div id="search-results"></div>
    <div id="api-data"></div>
    <div id="visit-counter"></div>
    
    <!-- Trunk bu satırı otomatik işler -->
    <link data-trunk rel="rust" />
</body>
</html>
EOF

# Geliştirme sunucusunu başlat
trunk serve --open

# Release build için
trunk build --release

Gerçek Dünya Senaryosu: Dashboard Bileşeni

Bir sistem monitöring dashboard’u için Rust WASM bileşeni yazalım. Bu senaryo, sunucu metrikleri için mükemmel bir örnek:

cat > src/dashboard.rs << 'EOF'
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::JsFuture;
use web_sys::{window, Element};
use js_sys::Promise;

#[derive(Debug)]
struct ServerMetric {
    name: String,
    value: f64,
    unit: String,
    status: MetricStatus,
}

#[derive(Debug)]
enum MetricStatus {
    Ok,
    Warning,
    Critical,
}

impl MetricStatus {
    fn css_class(&self) -> &str {
        match self {
            MetricStatus::Ok => "metric-ok",
            MetricStatus::Warning => "metric-warning",
            MetricStatus::Critical => "metric-critical",
        }
    }
    
    fn from_percentage(value: f64) -> Self {
        if value > 90.0 { MetricStatus::Critical }
        else if value > 75.0 { MetricStatus::Warning }
        else { MetricStatus::Ok }
    }
}

fn create_metric_card(doc: &web_sys::Document, metric: &ServerMetric) -> Element {
    let card = doc.create_element("div").unwrap();
    card.set_attribute("class", &format!("metric-card {}", metric.status.css_class())).unwrap();
    
    card.set_inner_html(&format!(
        r#"
        <h3>{}</h3>
        <div class="metric-value">{:.1}{}</div>
        <div class="metric-bar">
            <div class="metric-fill" style="width: {:.0}%"></div>
        </div>
        "#,
        metric.name,
        metric.value,
        metric.unit,
        metric.value
    ));
    
    card
}

#[wasm_bindgen]
pub async fn render_dashboard(container_id: String) {
    let doc = window().unwrap().document().unwrap();
    
    // API'den metrikleri çek (gerçek senaryoda)
    // Burada simüle ediyoruz
    let metrics = vec![
        ServerMetric {
            name: "CPU Kullanımı".to_string(),
            value: 67.5,
            unit: "%".to_string(),
            status: MetricStatus::from_percentage(67.5),
        },
        ServerMetric {
            name: "RAM Kullanımı".to_string(),
            value: 82.3,
            unit: "%".to_string(),
            status: MetricStatus::from_percentage(82.3),
        },
        ServerMetric {
            name: "Disk I/O".to_string(),
            value: 45.0,
            unit: "MB/s".to_string(),
            status: MetricStatus::Ok,
        },
        ServerMetric {
            name: "Network".to_string(),
            value: 91.2,
            unit: "Mbps".to_string(),
            status: MetricStatus::Critical,
        },
    ];
    
    if let Some(container) = doc.get_element_by_id(&container_id) {
        container.set_inner_html(r#"<h2>Sunucu Metrikleri</h2><div class="metrics-grid"></div>"#);
        
        if let Some(grid) = container.query_selector(".metrics-grid").unwrap() {
            for metric in &metrics {
                let card = create_metric_card(&doc, metric);
                grid.append_child(&card).unwrap();
            }
        }
    }
    
    // Her 30 saniyede bir yenile
    let promise = Promise::new(&mut |resolve, _| {
        window().unwrap()
            .set_interval_with_callback_and_timeout_and_arguments_0(&resolve, 30000)
            .unwrap();
    });
    JsFuture::from(promise).await.unwrap();
}
EOF

Derleme ve Optimizasyon İpuçları

# Debug build - geliştirme sırasında
trunk build

# Release build - production için
trunk build --release

# WASM binary boyutunu kontrol et
ls -lh dist/*.wasm

# wasm-opt ile daha fazla optimize et
wasm-opt -Oz -o dist/optimized.wasm dist/rust_dom_demo_bg.wasm
ls -lh dist/optimized.wasm

# wasm2wat ile WASM'ı insan okuyabilir forma dönüştür (debug)
wasm2wat dist/rust_dom_demo_bg.wasm -o output.wat
head -50 output.wat

# Bundle boyutunu analiz et
wasm-pack build --release --target web
du -sh pkg/

Projeniz büyüdükçe bazı optimizasyonlara dikkat edin:

  • feature seçimi: web-sys feature’larını minimumda tutun, her feature binary boyutunu artırır
  • opt-level = “s”: Boyut odaklı optimizasyon, sunucu performansı için değil indirme süresi için kritik
  • wasm-opt kullanımı: wasm-pack bunu otomatik yapar ama trunk için manuel gerekebilir
  • code splitting: Büyük uygulamalarda lazy loading için dinamik import düşünün
  • console_error_panic_hook: Sadece debug build’lerde aktif edin, release’de gereksiz

Sık Karşılaşılan Hatalar ve Çözümleri

  • dyn_into başarısız: Yanlış tip cast deniyorsunuz. dyn_ref ile type check yapın önce
  • forget() unutmak: Closure’lar drop edilince event listener çalışmaz, forget() şart
  • wasm-bindgen versiyon uyumsuzluğu: Cargo.lock dosyasını versiyonla birlikte commit edin
  • InvalidStateError: DOM hazır olmadan erişim, DOMContentLoaded sonrası çalıştırın
  • Büyük binary boyutu: Kullanılmayan web-sys feature’larını Cargo.toml‘dan kaldırın
  • Async/await sorunları: spawn_local yerine doğrudan async fn kullanmayı deneyin

Sonuç

Rust WASM ile DOM manipülasyonu başlangıçta alışılmadık gelebilir. JavaScript’te tek satırda yaptığınız şeyi Rust’ta birkaç satırda yapmanız gerekebiliyor. Ama bunun karşılığını alıyorsunuz: tip güvenliği, memory güvenliği ve JavaScript’e yakın ya da daha iyi performans.

Özellikle karmaşık hesaplama gerektiren UI bileşenleri, büyük veri setlerini işleyen tablolar ve sistem monitöring dashboard’ları gibi senaryolarda bu yaklaşım gerçekten değer katıyor. Sysadmin perspektifinden bakarsak, sunucu tarafında yazdığınız Rust kodunu istemci tarafına taşıyabilmek de ciddi bir avantaj. Aynı parsing mantığı, aynı veri yapıları, aynı dil.

Başlangıç için wasm-pack şablonuyla küçük bir proje açın, temel DOM işlemlerini deneyin ve oradan büyütün. web-sys dokümantasyonu başlangıçta bunaltıcı gelebilir ama MDN’deki JavaScript API’leriyle birebir eşleştiği için aslında oldukça öğrenilebilir bir yapısı var.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir