视图

Loco 中,Web 请求的处理被划分为控制器(controller)、模型(model)和视图(view)三部分。

  • 控制器(Controller) 负责处理请求,解析载荷(payload),然后将控制权交给模型。
  • 模型(Model) 主要负责与数据库通信,并在需要时执行 CRUD 操作。以及对所有业务和领域逻辑及操作进行建模。
  • 视图(View) 承担组装和渲染最终响应的责任,并将其发送回客户端。

您可以选择使用 JSON 视图(JSON responses),即 JSON 响应,或者 模板视图(Template views),它由模板视图引擎驱动,最终生成 HTML 响应。 您也可以将两者结合使用。

这在精神上类似于 Rails 的 `jbuilder` 视图(JSON)和常规视图(HTML),只是在 LOCO 中我们专注于 JSON 优先。

JSON 视图

例如,我们有一个处理用户登录的端点(endpoint)。当用户有效时,我们可以将 user 模型传递到 LoginResponse 视图(这是一个 JSON 视图)以返回响应。

有 3 个步骤:

  1. 解析,接受请求
  2. 创建领域对象:模型
  3. 将领域模型交给视图对象,该对象塑造最终的响应

以下 Rust 代码表示一个控制器,它负责处理用户登录请求,并将响应的 塑造 工作交给 LoginResponse

use crate::{views::auth::LoginResponse};
async fn login(
    State(ctx): State<AppContext>,
    Json(params): Json<LoginParams>,
) -> Result<Response> {
    // 使用请求的参数获取用户模型
    // let user = users::Model::find_by_email(&ctx.db, &params.email).await?;

    // 使用 LoginResponse 视图格式化 JSON 响应
    format::json(LoginResponse::new(&user, &token))
}

另一方面,LoginResponse 是一个响应塑造视图,它由 serde 驱动:

use serde::{Deserialize, Serialize};

use crate::models::_entities::users;

#[derive(Debug, Deserialize, Serialize)]
pub struct LoginResponse {
    pub token: String,
    pub pid: String,
    pub name: String,
}

impl LoginResponse {
    #[must_use]
    pub fn new(user: &users::Model, token: &String) -> Self {
        Self {
            token: token.to_string(),
            pid: user.pid.to_string(),
            name: user.name.clone(),
        }
    }
}

模板视图

当您想要向用户返回 HTML 时,您可以使用服务器端模板。 这类似于 Ruby 的 erb、Node 的 ejs 或 PHP 的工作方式。

对于服务器端模板渲染,我们提供了内置的 TeraView 引擎,它基于流行的 Tera 模板引擎。

要使用此引擎,您需要验证在 `initializers/view_engine.rs` 中是否有一个 ViewEngineInitializer,并且它也在您的 `app.rs` 中指定。 如果您使用了 SaaS Starter,这应该已经为您配置好了。

Tera 视图引擎从新的 assets/ 文件夹中获取资源。 这是一个示例结构:

assets/
├── i18n
│   ├── de-DE
│   │   └── main.ftl
│   ├── en-US
│   │   └── main.ftl
│   └── shared.ftl
├── static
│   ├── 404.html
│   └── image.png
└── views
    └── home
        └── hello.html
config/
:
src/
├── controllers/
├── models/
:
└── views/

创建新视图

首先,创建一个模板。 在本例中,我们在 assets/views/home/hello.html 中添加一个 Tera 模板。 请注意,assets/ 位于您的项目根目录中(与 src/config/ 同级)。

<html><body>
<code>assets/views/home/hello.html</code> 找到这个 tera 模板:
<br/>
<br/>
{{ /* t(key="hello-world", lang="en-US") */ }},
<br/>
{{ /* t(key="hello-world", lang="de-DE") */ }}

</body></html>

现在创建一个强类型的 view,将其封装在 src/views/dashboard.rs 中的模板中:

// src/views/dashboard.rs
use loco_rs::prelude::*;

pub fn home(v: impl ViewRenderer) -> Result<impl IntoResponse> {
    format::render().view(&v, "home/hello.html", data!({}))
}

并将其添加到 src/views/mod.rs 中:

pub mod dashboard;

接下来,转到您的控制器并使用该视图:

// src/controllers/dashboard.rs
use loco_rs::prelude::*;

use crate::views;

pub async fn render_home(ViewEngine(v): ViewEngine<TeraView>) -> Result<impl IntoResponse> {
    views::dashboard::home(v)
}

pub fn routes() -> Routes {
    Routes::new().prefix("home").add("/", get(render_home))
}

最后,在 src/app.rs 中注册您的新控制器的路由

pub struct App;
#[async_trait]
impl Hooks for App {
    // 为了简洁而省略

    fn routes(_ctx: &AppContext) -> AppRoutes {
        AppRoutes::with_default_routes()
            .add_route(controllers::auth::routes())
            // 在这里包含您的控制器路由
            .add_route(controllers::dashboard::routes())
    }

完成以上所有步骤后,您应该能够在运行 cargo loco routes 时看到您的新路由

$ cargo loco routes
[GET] /_health
[GET] /_ping
[POST] /api/auth/forgot
[POST] /api/auth/login
[POST] /api/auth/register
[POST] /api/auth/reset
[POST] /api/auth/verify
[GET] /api/auth/current
[GET] /home              <-- 我们新视图对应的 URL

它是如何工作的?

  • ViewEngine 是一个提取器(extractor),您可以通过 loco_rs::prelude::* 获得
  • TeraView 是我们随 Loco 提供的 Tera 视图引擎,也可以通过 loco_rs::prelude::* 获得
  • 控制器需要处理获取请求,调用一些模型逻辑,然后为视图提供模型和其他数据,而无需关心视图如何工作
  • views::dashboard::home 是一个不透明的调用,它隐藏了视图如何工作或字节如何进入浏览器的细节,这是一件 好事
  • 如果您想更换视图引擎,这里的封装就像魔法一样有效。 您可以更改提取器类型:ViewEngine<Foobar>,一切都会正常工作,因为 v 最终只是一个 ViewRenderer trait

静态资源

如果您想提供静态资源并在视图模板中引用这些资源,您可以使用 静态中间件(Static Middleware),并按以下方式配置它:

static:
  enable: true
  must_exist: true
  precompressed: false
  folder:
    uri: "/static"
    path: "assets/static"
  fallback: "assets/static/404.html"

在您的模板中,您可以这样引用静态资源:

<img src="/static/image.png"/>

自定义 Tera 视图引擎

Tera 视图引擎带有以下配置:

  • 模板加载和位置:assets/**/*.html
  • 国际化 (i18n) 配置到 Tera 视图引擎中,您可以使用翻译函数:t(..) 在您的模板中使用

如果您想更改 i18n 库的任何配置细节,您可以编辑 src/initializers/view_engine.rs

通过编辑初始化器,您可以:

  • 添加自定义 Tera 函数
  • 删除 i18n
  • 更改 Tera 或 i18n 库的配置
  • 提供新的或自定义的 Tera 实例(可能是不同的版本)

使用您自己的视图引擎

如果您不喜欢 Tera 作为视图引擎,或者想使用 Handlebars 或其他引擎,您可以非常轻松地创建自己的自定义视图引擎。

这是一个虚拟的 "Hello" 视图引擎的示例。 它是一个始终返回单词 hello 的视图引擎。

// src/initializers/hello_view_engine.rs
use axum::{Extension, Router as AxumRouter};
use async_trait::async_trait;
use loco_rs::{
    app::{AppContext, Initializer},
    controller::views::{ViewEngine, ViewRenderer},
    Result,
};
use serde::Serialize;

#[derive(Clone)]
pub struct HelloView;
impl ViewRenderer for HelloView {
    fn render<S: Serialize>(&self, _key: &str, _data: S) -> Result<String> {
        Ok("hello".to_string())
    }
}

pub struct HelloViewEngineInitializer;
#[async_trait]
impl Initializer for HelloViewEngineInitializer {
    fn name(&self) -> String {
        "custom-view-engine".to_string()
    }

    async fn after_routes(&self, router: AxumRouter, _ctx: &AppContext) -> Result<AxumRouter> {
        Ok(router.layer(Extension(ViewEngine::from(HelloView))))
    }
}

要使用它,您需要将其添加到 src/app.rs 的 hooks 中:

// src/app.rs
// 在 `initializers(..)` hook 中添加您的自定义 "hello" 视图引擎
impl Hooks for App {
    // ...
    async fn initializers(_ctx: &AppContext) -> Result<Vec<Box<dyn Initializer>>> {
        Ok(vec![
            // ,.----- 在这里添加它
            Box::new(initializers::hello_view_engine::HelloViewEngineInitializer),
        ])
    }
    // ...

Tera 内置函数

Loco 的 Tera 包含了其 内置函数。 此外,Loco 还引入了以下自定义内置函数:

要查看 Loco 内置函数:


请检查翻译结果是否符合您的要求。如果您有任何修改意见或需要进一步润色,请随时提出。