Найти в Дзене
Один Rust не п...Rust

SSR & Rust

Что это?
SSR — это способ создания веб-страниц, при котором весь HTML-код страницы формируется на сервере и отправляется в браузер пользователя в готовом виде. Простым языком:
Представьте, что вы заказываете пиццу. Если это SSR, то вам привозят уже готовую, горячую пиццу (страницу), которую можно сразу есть (просматривать). Вам не нужно ничего готовить (ждать, пока браузер соберёт страницу из кусочков). Если кратко: SSR — это когда сервер сам собирает страницу и отправляет её вам целиком, как готовую пиццу. - Найти альтернативу между фреймворками и SSR, - Использовать ML. Научиться использовать API для получения данных и рендеринга HTML с помощью шаблонизатора. Server-side rendering (SSR) в Rust можно реализовать с использованием нескольких различных подходов и библиотек. Вот основные варианты: Actix-web — web-фреймворк на Rust. Для SSR можно использовать шаблонизаторы вроде Tera или Askama для генерации HTML на сервере. Rocket — web-фреймворк для Rust. Поддержка SSR возможна с испол
Оглавление
GitHub - nicktretyakov/SSR

SSR — Server-Side Rendering (Рендеринг на стороне сервера)

Что это?
SSR — это способ создания веб-страниц, при котором
весь HTML-код страницы формируется на сервере и отправляется в браузер пользователя в готовом виде.

Простым языком:
Представьте, что вы заказываете пиццу. Если это SSR, то вам привозят уже готовую, горячую пиццу (страницу), которую можно сразу есть (просматривать). Вам не нужно ничего готовить (ждать, пока браузер соберёт страницу из кусочков).

Как это работает?

  1. Вы открываете страницу (например, новостной сайт).
  2. Сервер (компьютер, где хранится сайт) собирает всю информацию: текст, картинки, ссылки.
  3. Сервер формирует готовую HTML-страницу и отправляет её вам.
  4. Ваш браузер просто показывает её — никаких дополнительных загрузок или ожиданий.

Пример

  • Без SSR (например, SPA — Single Page Application):
    Вы открываете сайт, видите пустой экран, потом постепенно появляются заголовки, текст, картинки. Это как если бы вам привезли только тесто, а всё остальное вы должны были добавить сами.
  • С SSR:
    Вы открываете сайт — и сразу видите всю страницу, как в газете. Так работают многие новостные сайты, интернет-магазины (например, Wildberries, Яндекс.Маркет).

Зачем это нужно?

  • Быстрота: Пользователь сразу видит контент, не ждёт загрузки.
  • SEO: Поисковые системы (Google, Яндекс) лучше понимают и индексируют такие страницы.
  • Удобство: Работает даже на слабых устройствах или при медленном интернете.

Минусы

  • Нагрузка на сервер: Серверу приходится больше работать, особенно если много посетителей.
  • Меньше интерактивности: Если нужно много динамики (например, соцсети), часто комбинируют SSR с другими технологиями.

Если кратко: SSR — это когда сервер сам собирает страницу и отправляет её вам целиком, как готовую пиццу.

Для чего нужна данная статья? :

- Найти альтернативу между фреймворками и SSR,

- Использовать ML.

Зачем Вам это уметь? :

Научиться использовать API для получения данных и рендеринга HTML с помощью шаблонизатора.

Server-side rendering (SSR) в Rust можно реализовать с использованием нескольких различных подходов и библиотек.

Вот основные варианты:

Actix-web — web-фреймворк на Rust. Для SSR можно использовать шаблонизаторы вроде Tera или Askama для генерации HTML на сервере.

Rocket — web-фреймворк для Rust. Поддержка SSR возможна с использованием шаблонизаторов или рендеринга с использованием библиотек вроде askama или handlebars.

Askama: Rust-ориентированный шаблонизатор, который похож на Jinja2 в Python. С его помощью можно генерировать HTML на сервере для отправки клиенту.

Tera: Шаблонизатор, вдохновленный Jinja2, который можно использовать для SSR в приложениях на Rust.

Handlebars-rust: Rust-версия популярного шаблонизатора Handlebars, который можно использовать для серверной генерации HTML.

Yew + ssr: фреймворк для создания фронтенд-приложений на Rust, который поддерживает SSR. Можно использовать для генерации статических страниц на сервере с последующей отправкой их клиенту.

Leptos: фронтенд-фреймворк на Rust, который поддерживает SSR и позволяет создавать веб-приложения с рендерингом на стороне сервера и клиента.

Zola: Статический генератор сайтов на Rust, который можно использовать для предгенерации HTML страниц, что является одной из форм SSR.

Cobalt: статический генератор сайтов на Rust, который можно использовать для создания сайтов с предгенерацией контента.

Maud: шаблонизатор на Rust, который позволяет интегрировать HTML и Rust-код.

horrorshow: Шаблонизатор, ориентированный на высокую производительность, который поддерживает SSR.

Warp — web-фреймворк на Rust, который можно использовать для создания API, поддерживающих SSR. Можно интегрировать с любым из вышеупомянутых шаблонизаторов.

Пример на базе Actix-web с использованием API для получения данных и рендеринга HTML с помощью шаблонизатора.

Создайте новый проект на Rust:

cargo new actix_ssr_api

cd actix_ssr_api

Обновите Cargo.toml, добавив зависимости для actix-web, reqwest (для работы с API) и askama (для рендеринга шаблонов):

[package]

name = "actix_ssr_api"

version = "0.1.0"

edition = "2021"

[dependencies]

actix-web = "4.0"

askama = "0.11"

reqwest = "0.11"

serde = { version = "1.0", features = ["derive"] }

serde_json = "1.0"

Создайте папку templates в корневой директории проекта и добавьте файл index.html.askama:

<!DOCTYPE html>

<html>

<head>

<title>{{ title }}</title>

</head>

<body>

<h1>{{ title }}</h1>

<ul>

{% for item in items %}

<li>{{ item }}</li>

{% endfor %}

</ul>

</body>

</html>

В src/main.rs создайте структуру для работы с API и шаблонизатором:

use actix_web::{web, App, HttpServer, Responder, HttpResponse};

use askama::Template;
use reqwest;

use serde::Deserialize;

#[derive(Template)]

#[template(path = "index.html.askama")]

struct IndexTemplate {

title: String,
items: Vec<String>,

}

#[derive(Deserialize)]

struct ApiResponse {

items: Vec<String>,

}
async fn fetch_data() -> Result<Vec<String>, Box<dyn std::error::Error>> {

let resp = reqwest::get("https://api.example.com/data")

.await?

.json::<ApiResponse>()

.await?;

Ok(resp.items)

}

async fn index() -> impl Responder {

let data = fetch_data().await.unwrap_or_else(|_| vec!["Error loading

data".to_string()]);

let tmpl = IndexTemplate {

title: "SSR with API".to_string(),

items: data,

};

HttpResponse::Ok().content_type("text/

html").body(tmpl.render().unwrap())

}
#[actix_web::main]

async fn main() -> std::io::Result<()> {

HttpServer::new(|| {

App::new()

.route("/", web::get().to(index))

})

.bind("127.0.0.1:8080")?

.run()

.await

}

Убедитесь, что ваше API доступно и возвращает данные в формате JSON, соответствующем структуре ApiResponse. Запустите сервер:

cargo run

Затем откройте браузер и перейдите по адресу http://localhost:8080, чтобы увидеть результат.

Пример отправки запроса с идентификатором (user_id) в модель ML для получения рекомендаций, извлечения рекомендованных элементов из базы данных, рендеринга HTML.

Установите зависимости:

[dependencies]
actix-web = "4"

tch = "0.14"

tera = "1"

sqlx = { version = "0.7", features = ["runtime-tokio", "postgres"] }

moka = { version = "0.12", features = ["future"] }

serde = { version = "1", features = ["derive"] }

tokio = { version = "1", features = ["full"] }

env_logger = "0.10"

  • Настройте PostgreSQL и создайте таблицы.
  • Создайте директорию templates с файлом recommendations.html.

use actix_web::{web, App, HttpServer, HttpResponse, Error, middleware::Logger};
use tch::{nn, nn::Module, Device, Tensor};

use tera::{Tera, Context};

use sqlx::{PgPool, Postgres, query_as};

use std::sync::{Arc, RwLock};

use moka::future::Cache;

use serde::{Serialize, Deserialize};

use tokio::task;

// Модель машинного обучения

struct RecommendationModel {

linear: nn::Linear,

}

impl RecommendationModel {

fn new(vs: &nn::Path) -> Self {

// Пример: линейная модель с входом 10 (признаки пользователя) и выходом 100 (оценки элементов)

let linear = nn::linear(vs / "linear", 10, 100, Default::default());

RecommendationModel { linear }

}

}

impl Module for RecommendationModel {

fn forward(&self, xs: &Tensor) -> Tensor {

self.linear.forward(xs)

}

}

// Структура данных приложения

struct AppState {

model: Arc<RwLock<RecommendationModel>>, // Модель с возможностью обновления

tera: Arc<Tera>, // Шаблонизатор

pool: Arc<PgPool>, // Пул соединений с базой данных

cache: Cache<i32, Vec<i32>>, // Кэш рекомендаций

}

// Структура для признаков пользователя

#[derive(Debug, sqlx::FromRow)]

struct UserFeatures {

features: Vec<f32>, // Пример: массив признаков

}

#[derive(Debug, Serialize)]

struct Item {

id: i32,

name: String,

description: String,

}

// Подготовка входных данных для модели

async fn prepare_input(user_id: i32, pool: &PgPool) -> Result<Tensor, Error> {

let features = query_as!(UserFeatures,

"SELECT features FROM user_features WHERE user_id = $1",

user_id

)

.fetch_optional(pool)

.await

.map_err(|e| actix_web::error::ErrorInternalServerError(e))?

.ok_or_else(|| actix_web::error::ErrorNotFound("User not found"))?;

Ok(Tensor::of_slice(&features.features).to_device(Device::Cpu))

}

// Обработка выхода модели

fn process_output(output: Tensor) -> Vec<i32> {

let scores = output.to_vec::<f32>().unwrap();

let mut indexed_scores: Vec<(usize, f32)> = scores.into_iter().enumerate().collect();

indexed_scores.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));

indexed_scores.into_iter().take(5).map(|(idx, _)| idx as i32).collect()

}

// Извлечение элементов из базы данных

async fn fetch_items(item_ids: Vec<i32>, pool: &PgPool) -> Result<Vec<Item>, Error> {

let items = query_as!(Item,

"SELECT id, name, description FROM items WHERE id = ANY($1)",

&item_ids[..]

)

.fetch_all(pool)

.await

.map_err(|e| actix_web::error::ErrorInternalServerError(e))?;

Ok(items)

}

// Обработчик запроса

async fn recommendations_handler(

data: web::Data<AppState>,

user_id: web::Query<i32>,

) -> Result<HttpResponse, Error> {

let user_id = user_id.into_inner();

// Проверка кэша

let recommendations = if let Some(recos) = data.cache.get(&user_id).await {

recos

} else {

let input = prepare_input(user_id, &data.pool).await?;

// Выполнение вывода модели в отдельном потоке, чтобы не блокировать

let model = data.model.read().map_err(|_| actix_web::error::ErrorInternalServerError("Model lock poisoned"))?;

let output = web::block(move || model.forward(&input)).await??;

let recos = process_output(output);

data.cache.insert(user_id, recos.clone()).await;

recos

};

// Извлечение деталей элементов

let items = fetch_items(recommendations, &data.pool).await?;

// Рендеринг шаблона

let mut context = Context::new();

context.insert("items", &items);

let html = data.tera

.render("recommendations.html", &context)

.map_err(|e| actix_web::error::ErrorInternalServerError(e))?;

Ok(HttpResponse::Ok().content_type("text/html").body(html))

}

// Функция для периодического обновления модели

async fn update_model_periodically(state: Arc<AppState>) {

loop {

tokio::time::sleep(tokio::time::Duration::from_secs(3600)).await; // Каждые 60 минут

println!("Обновление модели...");

let vs = nn::VarStore::new(Device::Cpu);

let new_model = RecommendationModel::new(&vs.root());

let mut model_lock = state.model.write().unwrap();

*model_lock = new_model;

println!("Модель обновлена.");

}

}

#[actix_web::main]

async fn main() -> std::io::Result<()> {

// Инициализация переменных окружения для логирования

std::env::set_var("RUST_LOG", "actix_web=info");

env_logger::init();

// Инициализация модели

let vs = nn::VarStore::new(Device::Cpu);

let model = RecommendationModel::new(&vs.root());

let model_arc = Arc::new(RwLock::new(model));

// Инициализация шаблонизатора

let tera = Tera::new("templates/**/*").unwrap();

let tera_arc = Arc::new(tera);

// Инициализация пула базы данных

let pool = PgPool::connect("postgres://user:password@localhost/db")

.await

.expect("Не удалось подключиться к базе данных");

let pool_arc = Arc::new(pool);

// Инициализация кэша

let cache = Cache::builder()

.time_to_live(std::time::Duration::from_secs(300)) // TTL 5 минут

.max_capacity(1000) // Максимум 1000 записей

.build();

// Создание состояния приложения

let app_state = Arc::new(AppState {

model: model_arc.clone(),

tera: tera_arc.clone(),

pool: pool_arc.clone(),

cache: cache.clone(),

});

// Запуск задачи обновления модели

let state_clone = app_state.clone();

tokio::spawn(update_model_periodically(state_clone));

// Запуск сервера

HttpServer::new(move || {

App::new()

.app_data(web::Data::new(app_state.clone()))

.wrap(Logger::default()) // Middleware для логирования

.route("/recommendations", web::get().to(recommendations_handler))

})

.workers(4) // Количество воркеров

.bind("127.0.0.1:8080")?

.run()

.await

}

Пример шаблона recommendations.html:

<!DOCTYPE html>

<html>

<head>

<title>Рекомендации</title>

</head>

<body>

<h1>Ваши рекомендации</h1>

<ul>

{% for item in items %}

<li>{{ item.name }} - {{ item.description }}</li>

{% endfor %}

</ul>

</body>

</html>

Предполагаемая схема:

CREATE TABLE user_features (

user_id INT PRIMARY KEY,

features FLOAT[] NOT NULL

);

CREATE TABLE items (

id INT PRIMARY KEY,

name TEXT NOT NULL,

description TEXT NOT NULL

);