用React实现Flutter的可视化布局的一次尝试

535 阅读5分钟

前言

使用Flutter也有段时间了,相较于iOS写UI布局,效率提升了不少;但是widget嵌套过多,感觉不利于维护,当然,实际开发中,肯定会对其进行拆解,维护各种小组件,再去组合成大组件,所以就有个想法:Flutter是否也可以像iOS的xib布局那样可视化编辑布局,直接生成dart代码?

项目概况

先给大家看一下操作面板:
(由于第一个版本只是尝试如何去实现功能,所以交互ui都很不友好)

WechatIMG68.jpeg

WechatIMG72.png 目前分为四个面板:

  1. widget信息面板:在这里设置生成的widget类名
  2. widget布局预览面板:这里就是整个的widget的实时布局预览,并且可以在预览面板上选中各个子widget。
  3. widget节点操作面板:这个区域会展示的选中的widget可操作选项,比如当前节点的删除,属性设置;多个子节点的顺序调整等;
  4. 节点层级面板:这个区域显示整个widget树的层级,并可以在这里点击切换到各个节点,然后再操作面板进行操作
    操作面板介绍完了,给大家看一下生成的dart代码:

class HBCell extends StatelessWidget {
  final String name;
  final Function followTap;
  final Function collectionTap;
  final Function moreTap;

  HBCell({
    this.name,
    this.followTap,
    this.collectionTap,
    this.moreTap,
  }) : super();

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.only(left: 10, right: 10, top: 10, bottom: 10),
      margin: EdgeInsets.only(left: 0, right: 0, top: 0, bottom: 0),
      decoration: BoxDecoration(),
      child: Column(
        mainAxisAlignment: MainAxisAlignment.start,
        crossAxisAlignment: CrossAxisAlignment.start,
        mainAxisSize: MainAxisSize.max,
        children: [
          Container(
            padding: EdgeInsets.only(left: 0, right: 0, top: 0, bottom: 0),
            margin: EdgeInsets.only(left: 0, right: 0, top: 5, bottom: 5),
            decoration: BoxDecoration(),
            child: Text(
              "这是一个标题",
            ),
          ),
          Row(
            mainAxisAlignment: MainAxisAlignment.start,
            crossAxisAlignment: CrossAxisAlignment.start,
            mainAxisSize: MainAxisSize.max,
            children: [
              Container(
                padding: EdgeInsets.only(left: 0, right: 0, top: 0, bottom: 0),
                margin: EdgeInsets.only(left: 0, right: 5, top: 0, bottom: 0),
                width: 20,
                height: 20,
                decoration: BoxDecoration(),
              ),
              Text(name)
            ],
          ),
          Row(
            mainAxisAlignment: MainAxisAlignment.start,
            crossAxisAlignment: CrossAxisAlignment.center,
            mainAxisSize: MainAxisSize.max,
            children: [
              Expanded(
                child: Container(
                  padding:
                      EdgeInsets.only(left: 0, right: 0, top: 0, bottom: 0),
                  margin: EdgeInsets.only(left: 0, right: 0, top: 0, bottom: 0),
                  decoration: BoxDecoration(),
                  child: Text(
                    "这是内容描述这是内容描述这是内容描述这是内容描述这是内容描述这是内容描述这是内容描述这是内容描述这是内容描述",
                  ),
                ),
              ),
              Container(
                padding: EdgeInsets.only(left: 5, right: 5, top: 0, bottom: 0),
                margin: EdgeInsets.only(left: 0, right: 0, top: 0, bottom: 0),
                decoration: BoxDecoration(),
                child: Container(
                  padding:
                      EdgeInsets.only(left: 0, right: 0, top: 0, bottom: 0),
                  margin: EdgeInsets.only(left: 0, right: 0, top: 0, bottom: 0),
                  width: 50,
                  height: 50,
                  decoration: BoxDecoration(),
                ),
              )
            ],
          ),
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            crossAxisAlignment: CrossAxisAlignment.center,
            mainAxisSize: MainAxisSize.max,
            children: [
              GestureDetector(
                onTap: () {
                  if (this.followTap != null) {
                    this.followTap();
                  }
                },
                child: Text(
                  "点赞",
                  style: TextStyle(
                    color: Color(0xff999999),
                    fontSize: 10,
                  ),
                ),
              ),
              GestureDetector(
                onTap: () {
                  if (this.collectionTap != null) {
                    this.collectionTap();
                  }
                },
                child: Text(
                  "收藏",
                  style: TextStyle(
                    color: Color(0xff999999),
                    fontSize: 10,
                  ),
                ),
              ),
              GestureDetector(
                onTap: () {
                  if (this.moreTap != null) {
                    this.moreTap();
                  }
                },
                child: Text(
                  "更多",
                  style: TextStyle(
                    color: Color(0xff999999),
                    fontSize: 10,
                  ),
                ),
              )
            ],
          )
        ],
      ),
    );
  }
}

项目实现

技术选型

技术上选择的React+TS+Nodejs
用nodejs是因为浏览器不能直接i/o操作,所以通过接口请求本地的服务器来实现

实现

为了方便维护和扩展,以后可以很方便的添加各种widget,所以需要把widget对应的React布局,属性配置,以及各种操作分离,所以借鉴了flutter的widget的实现方式,先看一下最基础的数据模型:

  Single = 1,
  Mulit = 2,
  None = 3,
}

export abstract class Widget {
  // 当前是否被选中
  abstract selected: boolean;

  abstract type: WidgetType;

  // 生成布局配置信息
  abstract toJsonConfig(): WidgetJson;

  // 布局渲染
  abstract renderComponent(): any;
  // 添加子节点
  abstract addChild(child: Widget): any;

  // 节点深度
  deep?: number;

  // 父节点
  parent?: Widget;

  // 配置文件,各个节点的属性
  configs: DecorationItem[] = [];
  ...
}

先把flutter的组件分成了三类:1:只有一个child的widget,比如container等;2:有多个child的widget,比如row,column等;3:没有子节点的,比如text等
然后定义一个基础的抽象类:Widget,这里定义了子类必须实现的属性和方法,以及一些公共方法的具体实现。
接下来看看三种类型的基类:
SingleChildWidget:

class SingleChildWidget extends Widget {
  type = WidgetType.Single;
  selected = false;
  // 子节点
  child?: Widget;

  widgetName: string = '';

  addChild = (child?: Widget) => {
    if (!child) {
      return;
    }
    ...
    this.child = child;
    child.parent = this;
    ...
    this.forceUpdate();
  };

  renderComponent(): any {
    return null;
  }

  ...

  toJsonConfig(): WidgetJson {
    var json: WidgetJson = {
      widgetName: this.constructor.name,
      widgetType: this.type,
    };

    if (this.child) {
      json.child = this.child.toJsonConfig();
    }
    return json;
  }
}

MulitChildWidget:

class MulitChildWidget extends Widget {
  type = WidgetType.Mulit;
  selected = false;
  // 子节点
  children: Widget[] = [];

  widgetName: string = '';

  addChild(child: Widget) {
    if (!child) {
      return;
    }
    ...
    this.children?.push(child);
    child.parent = this;
    ...
    this.forceUpdate();
  }

  renderComponent(): any {
    return null;
  }

  ...

  toJsonConfig(): WidgetJson {
    let json: WidgetJson = {
      widgetName: this.constructor.name,
      widgetType: this.type,
    };
    if (this.children && this.children.length > 0) {
      json.children = [];
      this.children.forEach((item) => {
        json.children?.push(item.toJsonConfig());
      });
    }
    return json;
  }
}

NoneChildWidget:

class NoneChildWidget extends Widget {
  widgetName: string = '';
  type = WidgetType.None;
  selected = false;
  renderComponent(): any {
    return null;
  }
  addChild(child: Widget) {
    // 不需要实现
    return null;
  }

  ...

  toJsonConfig(): WidgetJson {
    return {
      widgetName: this.constructor.name,
      widgetType: this.type,
    };
  }
}

这个三个基类的主要区别就是如何去管理子节点,比如多节点的,就是用数组去实现;
最后就是各种不同的组件按需从这三者中集成,然后实现渲染函数,配置属性等;接下来用Container组件来举例:

export interface IContainerProps {
    widget:ContainerWidget;
}

class Container extends React.Component<IContainerProps> {

    render() {
        const {children, widget} = this.props
        var style:CSSProperties = {
            backgroundColor:widget.backgroundColor?.value || '#fff',
            ...
            }
        ...
        return (
            <SelectbaleDiv style={style} selected={widget.selected || false} onClick={widget.selectAction}>
                {children}
            </SelectbaleDiv>
        );
    }
}

class ContainerWidget extends SingleChildWidget {

  backgroundColor?: DecorationItem;
  topPadding?: DecorationItem;
  ...

  constructor() {
    super();
    this.backgroundColor = new DecorationItem(
      DecorationType.Input,
      '背景颜色',
      null,
    );
    this.width = new DecorationItem(DecorationType.Input, '宽度', null);
    this.height = new DecorationItem(DecorationType.Input, '高度', null);

    ...

    this.configs = [
      this.backgroundColor,
      ...
    ]
  }

  renderComponent() {
    return 
    <Container widget={this}>
        {this.child?.renderComponent()}
    </Container>;
  }

  toJsonConfig(): WidgetJson {
    var json = super.toJsonConfig();
    ...
    return json;
  }
}

注意一下函数renderComponent,会调用子类的渲染函数,这样就可以生成整个树形结构了
完成布局以后,会调用根节点的toJsonConfig生成整个布局的配置信息,然后用axios发送给本地服务器,由服务器去完成json到dart的映射,并且也把这个json文件保存在本地,方便以后从json到React组件的映射。
整个实现基本就是这些了,总结一下就是节点的配置信息和具体的布局信息分离,通过递归生成节点树.

外部属性和点击事件如何实现

我们在封装一个widget的时候,会给外部留状态属性以及点击回调。为了实现这个需求,我是通过一个固定的字符串解析来实现:"propName:propType",如果配置信息是这种格式的,就会解析并放入属性数组。

WechatIMG69.png

组件的多态

还有一个问题就是:一个组件在不同的状态下,会有不一样的ui表现,目前我是通过一个visiable组件来实现的,visiable会设置一个默认值,然后设置控制属性的keyName和类型,如果匹配上了,就显示对应的组件

WechatIMG71.png

WechatIMG70.png

最后

目前只支持stless的生成,staful就不考虑了;由于是验证自己的一个想法,时间关系,有时间再慢慢补充widget;后期也会加一些项目的管理在里面,比如图片资源管理,版本信息等统一修改等。