Для чего нужна данная статья? :
- Найти альтернативу между фреймворками и 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
);