奥创协议

1,576 阅读6分钟

奥创简介

奥创

背景

我们开发B/S架构的系统时,一般都是由后端负责对数据进行整理,前端负责渲染。作为动态渲染的页面,通常前端也会有一些业务逻辑影响渲染结果,例如根据后端返回的数据,判断某个组件是否显示,样式应该是什么样的,内容上有什么不一样等等。

一般情况下,这样的设计可以满足大多数需求,但是在交互变化非常多的场景下,就不太合适了。

举一个简单的例子:在下单页中,增加某个商品购买的数量,当数量大于2时,触发优惠规则;当数量等于5时,触发限购规则。

我们当然可以在一开始就将优惠规则和限购规则给到前端去进行逻辑校验,但是如果再叠加一些其他因素呢?

比如打折促销活动,指定哪些店铺的哪些商品参与,甚至会指定哪些地区的买家可以参与,那就要再增加一些接口和条件判断了。

当很多的这样的因素同时对某一个或者某几个组件产生作用时,页面的渲染就会呈现出无数种变化。而且最关键的是,这种变化可能大部分是随时会产生的,各个商家的各种组合玩法也是层出不穷。这些可变点叠加到一起,是一个庞大的笛卡尔乘积。这时候,就需要奥创协议了。

奥创是什么

在描述奥创是什么之前,可以先看看奥创做了什么:

  1. 将一整个页面划分成一个个业务组件。 —— 模块化

  2. 组件定义了数据结构(后端)和渲染逻辑(前端),并且相互独立。 —— 复用性

  3. 组件有多种类型:normal、input 和 request。 normal 只做数据的展示,不参与交互;request 能触发交互行为。Tab 页的切换、选择条件的改变换,都可以作为交互动作的来源;input 负责将前端输入的内容(输入框、富文本)带到后端进行数据渲染同步 —— 多样性

  4. 只有 request 类型的组件才会触发页面交互,并且并且每次交互只会带上一个 request 组件和所有的 input。—— 单一来源

  5. 前后端用同一套协议。服务端告诉前端,页面的结构是什么样子的,结构中每个节点是使用什么的组件,当前端组件发生变化时,比如选中了一个地址,后端可以根据前端提交的的参数判断页面会发生什么样的变化,然后告知前端该如何重新渲染。—— 控制反转

综上,奥创实际上就是,由后端告诉前端如何去渲染;将页面组件化,每种组件具有相应的职责;具有组件可复用的能力的前后端统一的交互方式。

奥创使用场景

  1. 重展示、轻交互。

对于大量数据联动变化的场景非常适合,如下单页。但是对于弹窗、动画、拖拉拽等不太适合用奥创,最好由前端在组件内部去实现。

  1. 交互不复杂,但是需要模块化定制的场景。

当需要定制样式时,只需要前端重写对应的组件即可。

奥创协议后端设计说明

以一个简单的订单页举例,这个页面只有两个组件,一个 Order 组件,其下面有多个 Item 子组件。

Order 组件下有一个 title 属性,负责对所有的 Item 信息做一个汇总,同时也是一个可编辑的文本。Item 组件有一个下拉列表属性,随着选择项的改变,Item 展示的内容也会随之改变。

  1. 构造组件

先定义 OrderCO,需要去继承 BaseCO。


@Data

@EqualsAndHashCode(*callSuper* = *true*)

public class OrderCO extends BaseCO {

private static final long serialVersionUID = *6994555999689507655L*;

public OrderCO() {

this.setUrl("/workbench/designer/task/demo/adjust"); // 定义 adjust请求的 url,这样,当 request 组件发生变化时,会自动调用这个 url。

// 需要注意的是,如果没有组件定义这个 Url,那么相当于不会调用 adjust。可以根据需求决定。

this.setWithUrl(*true*); // 定义了 url 的同时一定要将 WithUrl 设置成 true , 不然框架识别不到。

this.setTag("order");

this.setInput(*true*); // 定义成 input 是由于 title 属性对应到前端是一个可编辑的文本。

}

// OrderCO 的属性,为了规范统一用 prop 进行包裹。

private OrderDTO prop;

@Data

public static class OrderDTO{

private String title;

}

}

定义 ItemCO


@Data

@EqualsAndHashCode(*callSuper* = *true*)

public class ItemCO extends BaseCO {

private static final long serialVersionUID = *4517824962948845192L*;

public ItemCO() {

this.setRequest(*true*); // 表明是 Request 组件,当 Item 组件中的下拉框选中项发生变化时,会调用 adjust 请求;

this.setTag("item");

this.setParentTag("order"); // 可以在 CO 中定义组件的层级关系,也可以在 Builder 中动态定义 ParentTag。如果 CO组件需要复用,推荐在 Builder 中定义。

}

public ItemDTO prop;

@Data

public static class ItemDTO {

private List<Option> options;

private String currentSelectedId; // 标记当前选中的选项 id

@Data

@Accessors(*chain* = *true*)

public static class Option {

private String value;

private String label;

private List<Option> children = Lists.newArrayList();

}

}

// 注意,一定要将前端 Item 组件选中的结果,缓存到 QueryParam 中去,不然后端框架就无法知道目前选中的是哪个 , 也就不能从统一的渲染流程中产生差异。

@Override

public void attachQueryParam(IQueryParam param) {

QueryParam queryParam = (QueryParam) param;

queryParam.getSelectedMap().put(getId(), getProp().currentSelectedId);

}

}

  1. 构造结构

// 只是一个 order 下 包含了多个 item

private static StructureConfig buildStructure() {

StructureConfig config = new StructureConfig();

config.setRoot(

StructureItem.of("order").addItem(

StructureItem.of("item")

)

);

return config;

}

  1. 解析组件

@AutoService(ComponentBuilderSpi.class)

public class OrderCOBuilder implements ComponentBuilderSpi<ShoppingResult, ShoppingResult, OrderCO> {

@Override

public boolean supportTag(String tag) {

return StringUtils.equals(tag, "order");

}

// 从 ShoppingResult 中提取出要 build 的组件,这里的 ShoppingResult 是我们自己定义的,需要实现 IPageRenderResult 接口。

@Override

public List<ShoppingResult> extract(String tag, ShoppingResult result, BaseCO parent) {

return Lists.newArrayList(result);

}

@Override

public OrderCO build(String tag, ShoppingResult result, ShoppingResult model, BaseCO parent) {

OrderCO orderCO = new OrderCO();

orderCO.setId("1");

QueryParam queryParam = result.getQueryParam();

List<String> selected = Lists.newArrayList();

for (ItemDTO itemDTO : model.getItemDTOS()) {

// 可以看到,在这里用到了 QueryParam 缓存的内容,来进行不同的展示

String currentSelectedId = result.getQueryParam().getSelectedMap().get(itemDTO.getId());

Option option = itemDTO.getOptions().stream().filter(a -> a.getValue().equals(currentSelectedId)).findFirst().orElse(*null*);

if (option != *null*) {

selected.add(option.getLabel());

}

}

OrderDTO orderDTO = new OrderDTO();

// 根据 item 的选择结果进行动态展示

if (CollectionUtils.isNotEmpty(selected)) {

orderDTO.setTitle("当前已选择:" + String.join(",", selected));

} else {

orderDTO.setTitle("当前未选择任何 item ");

}

orderCO.setProp(orderDTO);

return orderCO;

}

}

@AutoService(ComponentBuilderSpi.class)

public class ItemCOBuilder implements ComponentBuilderSpi<ShoppingResult, ItemDTO, ItemCO> {

@Override

public boolean supportTag(String tag) {

return StringUtils.equals(tag, "item");

}

@Override

public List<ItemDTO> extract(String tag, ShoppingResult result, BaseCO parent) {

return result.getItemDTOS();

}

@Override

public ItemCO build(String tag, ShoppingResult result, ItemDTO model, BaseCO parent) {

ItemCO itemCO = new ItemCO();

itemCO.setId(model.getId());

ItemCO.ItemDTO itemDTO = new ItemCO.ItemDTO();

itemDTO.setOptions(model.getOptions());

String currentSelectedId = result.getQueryParam().getSelectedMap().get(itemCO.getId());

if (model.getOptions().stream().anyMatch(a -> a.getValue().equals(currentSelectedId))) {

// 设置当前选择的结果

itemDTO.setCurrentSelectedId(currentSelectedId);

}

itemCO.setProp(itemDTO);

return itemCO;

}

}

  1. 异步渲染