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(¬ification).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-sysfeature’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-packbunu otomatik yapar amatrunkiç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_intobaşarısız: Yanlış tip cast deniyorsunuz.dyn_refile type check yapın önceforget()unutmak: Closure’lar drop edilince event listener çalışmaz,forget()şartwasm-bindgenversiyon uyumsuzluğu:Cargo.lockdosyasını versiyonla birlikte commit edinInvalidStateError: DOM hazır olmadan erişim,DOMContentLoadedsonrası çalıştırın- Büyük binary boyutu: Kullanılmayan
web-sysfeature’larınıCargo.toml‘dan kaldırın - Async/await sorunları:
spawn_localyerine doğrudanasync fnkullanmayı 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.
