对象显示

在Sui上,对象的结构和行为是明确的,可以以易于理解的方式显示。然而,为了支持客户机的丰富元数据,定义了一种标准且高效的方式来“描述”对象,即Sui框架中的Display对象。

背景

历史上曾有不同的尝试来达成对象结构的标准,以便可以在用户界面中显示对象。其中一种方法是定义对象结构中的某些字段,当这些字段存在时,会在UI中使用。然而,这种方法不够灵活,需要开发人员在每个对象中定义相同的字段,有时这些字段对对象来说没有意义。

/// An attempt to standardize the object structure for display.
public struct CounterWithDisplay has key {
    id: UID,
    /// If this field is present it will be displayed in the UI as `name`.
    name: String,
    /// If this field is present it will be displayed in the UI as `description`.
    description: String,
    // ...
    image: String,
    /// Actual fields of the object.
    counter: u64,
    // ...
}

如果字段包含静态数据,每个对象中都会重复这些数据。而且,由于Move没有接口,无法知道一个对象是否有特定字段,这使得客户端的获取过程更复杂。

对象显示

为了解决这些问题,Sui引入了一种标准方式来描述对象的显示。与其在对象结构中定义字段,不如将显示元数据存储在一个单独的对象中,并与类型关联。这样,显示元数据不会重复,且易于扩展和维护。

Sui Display的另一个重要特性是能够定义模板并在这些模板中使用对象字段。这不仅允许更灵活的显示,还解放了开发人员不必在每个对象中定义相同的字段和类型。

Sui Fullnode本机支持对象显示,如果对象类型关联了Display,客户端可以获取显示元数据。

module book::arena {
    use std::string::String;
    use sui::package;
    use sui::display;

    /// The One Time Witness to claim the `Publisher` object.
    public struct ARENA has drop {}

    /// Some object which will be displayed.
    public struct Hero has key {
        id: UID,
        class: String,
        level: u64,
    }

    /// In the module initializer we create the `Publisher` object, and then
    /// the Display for the `Hero` type.
    fun init(otw: ARENA, ctx: &mut TxContext) {
        let publisher = package::claim(otw, ctx);
        let mut display = display::new<Hero>(&publisher, ctx);

        display.add(
            b"name".to_string(),
            b"{class} (lvl. {level})".to_string()
        );

        display.add(
            b"description".to_string(),
            b"One of the greatest heroes of all time. Join us!".to_string()
        );

        display.add(
            b"link".to_string(),
            b"https://example.com/hero/{id}".to_string()
        );

        display.add(
            b"image_url".to_string(),
            b"https://example.com/hero/{class}.jpg".to_string()
        );

        // Update the display with the new data.
        // Must be called to apply changes.
        display.update_version();

        transfer::public_transfer(publisher, ctx.sender());
        transfer::public_transfer(display, ctx.sender());
    }
}

创建者特权

虽然对象可以由账户拥有并可能属于真正的所有权范畴,但Display可以由对象的创建者拥有。这样,创建者可以更新显示元数据,并全局应用更改而不需要更新每个对象。创建者还可以将Display转移给其他账户,甚至围绕对象构建一个应用程序来管理元数据。

标准字段

最广泛支持的字段包括:

  • name - 对象的名称。当用户查看对象时显示。
  • description - 对象的描述。当用户查看对象时显示。
  • link - 在应用程序中使用的对象链接。
  • image_url - 对象的图像的URL或Blob。
  • thumbnail_url - 在钱包、浏览器和其他产品中用作预览的小图像的URL。
  • project_url - 与对象或创建者相关的网站链接。
  • creator - 表示对象创建者的字符串。

请参考Sui文档获取最新支持的字段列表。

尽管有一套标准字段,Display对象并不强制执行这些字段。开发人员可以定义他们需要的任何字段,客户端可以根据需要使用它们。一些应用程序可能需要额外的字段,而忽略其他字段,Display足够灵活以支持这些需求。

使用Display

Display对象在sui::display模块中定义。它是一个泛型结构,接受一个虚拟类型作为参数。虚拟类型用于将Display对象与其描述的类型关联。Display对象的fields是键值对的VecMap,其中键是字段名,值是字段值。version字段用于显示元数据的版本,并在update_display调用时更新。

文件:sui-framework/sources/display.move

struct Display<phantom T: key> has key, store {
    id: UID,
    /// 包含显示字段。目前支持的字段有:name、link、image和description。
    fields: VecMap<String, String>,
    /// 版本只能由发布者手动更新。
    version: u16
}

Publisher对象需要一个新的Display,因为它作为类型的所有权证明。

模板语法

目前,Display支持简单的字符串插值,并可以在其模板中使用结构字段(和路径)。语法很简单 - {path}替换为该路径字段的值。路径是一个以点分隔的字段名列表,针对嵌套字段从根对象开始。

/// Some common metadata for objects.
public struct Metadata has store {
    name: String,
    description: String,
    published_at: u64
}

/// The type with nested Metadata field.
public struct LittlePony has key, store {
    id: UID,
    image_url: String,
    metadata: Metadata
}

上述LittlePony类型的Display可以定义如下:

{
  "name": "Just a pony",
  "image_url": "{image_url}",
  "description": "{metadata.description}"
}

多个Display对象

对于特定的T类型,可以创建任意数量的Display<T>对象。然而,fullnode将使用最近更新的Display<T>

进一步阅读