Найти тему
Один Rust не п...Rust

SSR & Rust

Оглавление

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

- Найти альтернативу между фреймворками и 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

);