如何建立一个元数据驱动的用户界面
一个元数据驱动的用户界面通过调用一个单一的数据端点为项目团队提供了元素的一致性。下面是如何建立你自己的元数据驱动的用户界面。
元数据驱动的UI方法在后端或DBA能力强的项目团队中特别有用,而不是UI。一般来说,它通过调用一个端点来提供元素对齐,该端点提供了所有需要的数据,如cardinality、语言、字体大小和字体本身。
该库本身旨在提供一个可配置的元数据引擎和一组端点。同时,UI应该从头开始编写,考虑到相应的用例的具体情况,以便能够正确地处理元数据并在此基础上构建自己。
开始使用
要开始使用元数据提供者,只需在主程序中添加以下maven依赖项。
XML
<dependency>
<groupId>io.github.sergeivisotsky.metadata</groupId>
<artifactId>metadata-selector</artifactId>
</dependency>
并将以下依赖关系添加到部署应用程序中。
XML
<dependency>
<groupId>io.github.sergeivisotsky.metadata</groupId>
<artifactId>metadata-deploy</artifactId>
</dependency>
为了获得这两个依赖的兼容版本,我们还强烈建议在父POM的dependencyManagement 部分添加一个库启动器。
XML
<dependency>
<groupId>io.github.sergeivisotsky.metadata</groupId>
<artifactId>metadata-provider-bom</artifactId>
<version>0.0.7</version>
<scope>import</scope>
<type>pom</type>
</dependency>
它将从Maven中心加载。
扩展
让我们设想一下,我们有以下预配置的表单元数据提供者,它是由以下预配置的模板制作的。
爪哇
@Component
public class FormMetadataMapper implements MetadataMapper<FormMetadata> {
@Override
public String getSql() {
return "SELECT fm.id,\n" +
" fm.form_name,\n" +
" fm.cardinality,\n" +
" fm.language,\n" +
" fm.offset,\n" +
" fm.padding,\n" +
" fm.font,\n" +
" fm.font_size,\n" +
" fm.description,\n" +
" fm.facet,\n" +
" vf.enabled_by_default,\n" +
" vf.ui_control\n" +
"FROM form_metadata fm\n" +
" LEFT JOIN view_field vf on fm.id = vf.form_metadata_id\n" +
"WHERE fm.form_name = :formName\n" +
" AND fm.language = :lang";
}
@Override
public ExtendedFormMetadata map(ResultSet rs) {
try
ExtendedFormMetadata metadata = new ExtendedFormMetadata();
metadata.setFormName(rs.getString("form_name"));
metadata.setCardinality(rs.getString("cardinality"));
metadata.setLang(Language.valueOf(rs.getString("language")
.toUpperCase(Locale.ROOT)));
metadata.setOffset(rs.getInt("offset"));
metadata.setPadding(rs.getInt("padding"));
metadata.setFont(rs.getString("font"));
metadata.setFontSize(rs.getInt("font_size"));
metadata.setDescription(rs.getString("description"));
ViewField viewField = new ViewField();
viewField.setEnabledByDefault(rs.getInt("enabled_by_default"));
viewField.setUiControl(rs.getString("ui_control"));
metadata.setViewField(viewField);
metadata.setFacet(rs.getString("facet"));
return metadata;
} catch (SQLException e) {
throw new RuntimeException("Unable to get value from ResultSet for Mapper: {}" +
FormMetadataMapper.class.getSimpleName(), e);
}
}
}
从第一眼看,这已经绰绰有余了。然而,对于一个交付项目的特定需求,有必要添加一个额外的结构,它将代表一些神秘的页脚数据。
这需要采取以下步骤。
- 通过调整部署Liquibase脚本,创建一个相应的数据库表/新字段。
- 在预先配置的领域模型中添加一个新的结构,如`ExtendedFormMetadata`或创建一个全新的结构,它将成为表单元数据的一部分。
- 调整`FormMetadataMapper`或创建一个全新的映射器以满足新的需求。
然而,让我们转到我们的例子,一个神秘的页脚。
我们有一个要求。
- 网页页脚应该由元数据生成。
- 它应该在OOTBS元数据端点的响应中被撞出来。
第一步
创建一个新的部署Liquibase脚本。
在我们的案例中,它只是被称为db.changelog-12-09-2021.xml
XML
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.0.xsd">
<changeSet id="1" author="svisockis">
<createTable tableName="footer">
<column name="id" type="java.sql.Types.BIGINT" autoIncrement="true">
<constraints nullable="false" primaryKey="true"/>
</column>
<column name="resizable" type="java.sql.Types.BOOLEAN"/>
<column name="displayable" type="java.sql.Types.BOOLEAN"/>
<column name="defaultText" type="java.sql.Types.VARCHAR(150)"/>
<column name="form_metadata_id" type="java.sql.Types.BIGINT"/>
</createTable>
<addForeignKeyConstraint baseTableName="footer" baseColumnNames="form_metadata_id"
constraintName="footer_form_view_metadata_fk"
referencedTableName="form_metadata"
referencedColumnNames="id"/>
</changeSet>
</databaseChangeLog>
我们的页脚元数据应该持有关于页脚是否可调整大小和可显示的信息,以及用户在页面生成后将看到的默认文本和元数据基表的外键。
第二步
创建一个相应的POJO类。
爪哇
public class Footer {
private Long id;
private Boolean displayable;
private Boolean resizable;
private String defaultText;
// Constructor, getter and setters omitted
}
像这样添加一个对父POJO的引用。
Java
public class ExtendedFormMetadata extends FormMetadata {
private String facet;
private Footer footer;
// Constructor, getters and setters omitted
}
第三步
调整一个相应的映射器--在我们的例子中是`FormMetadataMapper`。
- SQL应该被调整。
- 应调整结果集的提取。
爪哇
@Component
public class FormMetadataMapper implements MetadataMapper<FormMetadata> {
@Override
public String getSql() {
return "SELECT fm.id,\n" +
" fm.form_name,\n" +
" fm.cardinality,\n" +
" fm.language,\n" +
" fm.offset,\n" +
" fm.padding,\n" +
" fm.font,\n" +
" fm.font_size,\n" +
" fm.description,\n" +
" fm.facet,\n" +
" vf.enabled_by_default,\n" +
" vf.ui_control,\n" +
" ft.displayable,\n" + // new
" ft.resizable,\n" + // new
" ft.default_Text\n" + // new
"FROM form_metadata fm\n" +
" LEFT JOIN view_field vf on fm.id = vf.form_metadata_id\n" +
" LEFT JOIN footer ft on fm.id = ft.form_metadata_id\n" + // new
"WHERE fm.form_name = :formName\n" +
" AND fm.language = :lang";
}
@Override
public ExtendedFormMetadata map(ResultSet rs) {
try {
ExtendedFormMetadata metadata = new ExtendedFormMetadata();
metadata.setFormName(rs.getString("form_name"));
metadata.setCardinality(rs.getString("cardinality"));
metadata.setLang(Language.valueOf(rs.getString("language")
.toUpperCase(Locale.ROOT)));
metadata.setOffset(rs.getInt("offset"));
metadata.setPadding(rs.getInt("padding"));
metadata.setFont(rs.getString("font"));
metadata.setFontSize(rs.getInt("font_size"));
metadata.setDescription(rs.getString("description"));
ViewField viewField = new ViewField();
viewField.setEnabledByDefault(rs.getInt("enabled_by_default"));
viewField.setUiControl(rs.getString("ui_control"));
metadata.setViewField(viewField);
metadata.setFacet(rs.getString("facet"));
// --- New block ---
Footer footer = new Footer();
footer.setResizable(rs.getBoolean("resizable"));
footer.setDisplayable(rs.getBoolean("displayable"));
footer.setDefaultText(rs.getString("default_text"));
metadata.setFooter(footer);
// --- End new block ---
return metadata;
} catch (SQLException e) {
throw new RuntimeException("Unable to get value from ResultSet for Mapper: {}" +
FormMetadataMapper.class.getSimpleName(), e);
}
}
}
第四步
运行deployer应用程序,更新数据库模式和应用程序本身。
结果
在结果中,你可以看到元数据端点中的以下新部分。
JSON
}
// ...
"footer": {
"id": null,
"displayable": true,
"resizable": false,
"defaultText": "This is some footer needed to fulfill our business requirements"
}
// ...
}
这个演示的源代码可以在以下资源库中找到。
OOTB(开箱即用)使用示例
下面的页面描述了一个OOTB(Out-of-the-Box)组合框元数据功能。
对于一个组合框的样式和值,元数据也被使用。作为一个例子。
JSON
[
{
"id": 1,
"codifier": "CD_001",
"font": "Times New Roman",
"fontSize": 12,
"weight": 300,
"height": 20,
"displayable": true,
"immutable": false,
"comboContent": [
{
"key": "initial",
"defaultValue": "Some initial value",
"comboId": 1
},
{
"key": "secondary",
"defaultValue": "Some secondary value",
"comboId": 1
},
{
"key": "someThird",
"defaultValue": "Some third value",
"comboId": 1
}
]
}
]
主部分包含组合框的一般属性,如重量、高度、字体和字体大小。
一个comboContent 子部分包含了组合框的内容,包括所有可能的默认值。
当UI调用一个元数据端点时,它首先应该构建页面本身,然后它应该解析一个组合框的例子。
在React中的例子。
JavaScript
class SampleCombo extends Component {
state = {
metadata: null,
}
// process metadata
componentDidMount() {
const viewName = 'main';
const self = this;
axios.all([getMetadata(viewName), getMessageHeader(viewName)])
.then(axios.spread((metadata, header) => {
let formattedMetadata = formatMetadata(metadata);
formattedMetadata = populateFields(header, formattedMetadata);
self.setState({metadata: formattedMetadata, activeTab: formattedMetadata.sections.get('comboContent')});
}));
}
// renders component
render() {
const {metadata, activeTab} = this.state;
if (!metadata) return <Loader/>;
const {
codifier,
font,
fontSize,
weight,
height,
displayable,
immutable,
} = metadata;
return (
<div id={uiName} className="klp-page">
<select id="sample" name="sample" style="font={font};fontSize={fontSize};weight={weight};height={height}">
<option value="{key}">{defaultValue}</option>
</select>
</div>
);
}
}
注意:这个例子并不理想,但它显示了主要的想法。
数据库模式
库提供OOTB数据库模式表,其目标是为所有可能的UI提供通用的基础元数据。它由以下表格组成。
- form_metadata
- 布局
- 视图_字段
- 查看者
- 检索_元数据
- combo_box
- combo_box_content
- combo_box_and_content_relation(组合框和内容)。
数据库扩展
可以扩展一个数据库模式。为了扩展目的和数据库版本管理的目的,使用了Liquibase。一个开箱即用的解决方案是用XML表示的,然而,YAML表示也可以根据每个特定案例的愿望/要求来接受。