/d2c-dsl

Primary LanguageTypeScript

D2C 平台的 DSL 标准定义

标准里本质是定义了一些标签节点, 将这些标签进行组合、嵌套即可描述任意 UI 界面。 具体标签的 DSL 描述里, 大部分属性的值都可以绑定数据, 所有标签都可以使用条件语法。

以下是具体的类型定义:

标签类型

以下是标签的通用类型定义, 相当于基类, 实际并不会使用这些通用标签类型,而是会使用后面“标签定义”部分定义的具体标签。

注意, 后续凡是提到“标签名”时, 指的都是这个标签的 DSL 定义里的 type 字段。

基础标签

所有的标签的 DSL 描述都需要符合以下 Node 类型。

interface Node {
    /**
     * 标签名,取值唯一,不能重复
     */
    type: string;

    /**
     * 样式属性,具体参考后面的 Style 类型定义
     * 注意: 每个标签可能仅支持 Style 类型里部分特定属性, 具体参考后面每个具体标签的定义
     */
    style?: Style;

    /**
     * 条件判断,目前支持 if、show、for, 具体参考后面 Condition 类型定义
     */
    condition?: Condition;

    /**
     * 子节点,可以有多个
     */
    children?: Node[];
}

容器标签

容器标签可以包含其他子节点, 比较常用, 我们专门为它定义了一个类型:

interface ContainerNode extends Node {
    // 容器标签一定有 children 字段, 类型必须是一个数组
    children: Node[];
}

叶子标签

叶子标签不能包含其他子节点, 比较常用, 我们专门为它定义了一个类型:

type LeafNode = Omit<Node, 'children' | 'text'>;

样式

支持的样式属性列表

标签支持的样式属性是有限的, 下面 Style 类型里的属性就是所有支持的样式属性。

注意: DSL 描述里的 style 字段里, 只能使用 Style 类型里列出来的属性

interface Style {
    // 展示策略
    visibility?: Display;
    // 宽度。单位为px的数值
    width?: number;

    // 高度,单位为px的数值
    height?: number;

    // 外边距,值必须是包含四部分,依次代表上、右、下、左四个方向的间距,单位为px, 示例: margin: '10px 10px 8px 4px'
    margin?: string;

    // 内边距,值必须是包含四部分,依次代表上、右、下、左四个方向的间距,单位为px, 示例: padding: '10px 10px 8px 4px'
    padding?: string;
    // 矩形边框宽度。单位为px
    borderWidth?: number;
    // 矩形边框颜色。取值同css中border-color
    borderColor?: string;
    // 圆角边框,单位为px
    borderRadius?: string;
    // 背景颜色,仅支持 RGB 格式的颜色值, 示例: bgColor: '#fefefe'
    bgColor?: string;
    // 用于为一个元素设置一个背景图像。值为图片地址
    bgImg?: string;
    // 设置背景图片大小
    scaleType?: ImgScaleType;
    // 设置了元素溢出时所需的行为
    overflow?: Overflow;
    // 属性指定了内部元素是如何在 flex 容器中布局的,定义了主轴的方向 (正方向或反方向),flex 容器中必须包括这个值
    flexDirection?: FlexDirection;
    // 属性指定 flex 元素单行显示还是多行显示。如果允许换行,这个属性允许你控制行的堆叠方向
    flexWrap?: FlexWrap;
    // 定义 flex 直接子元素在交叉轴上如何对齐
    alignItems?: AlignItems;
    // 定义 flex 直接子元素在主轴上的对齐方式
    justifyContent?: JustifyContent;
    // 布局方位(linear/frame直接子节点应用)
    gravity?: Gravity;
    // 类似css的z-index
    weight?: number;
    // 定义项目的放大比例(flex直接子元素应用)
    flexGrow?: FlexGrow;
    // 定义了项目的缩小比例(flex直接子元素应用)
    flexShrink?: FlexShrink;
    // 允许 flex 的直接子元素有与其他直接子元素不一样的对齐方式,可覆盖alignItems属性
    alignSelf?: AlignSelf;
    // 图片或者lottie地址
    src: string;
    placeHolder?: string;
    loopTime?: string;
    scale?: string;
    // linear元素的排列方向
    orientation?: Orientation;
    weightSum?: string;
    gap?: string;
    showNum?: string;
    repeat?: string;
    resizeMode?: LottieResizeMode;
    // 文本字体大小k。单位为px。
    fontSize: number;
    // 文本颜色。仅支持 RGB 格式的颜色值, 示例: color: '#fefefe'
    color: string;
    // 文本截断位置
    ellipsis: Ellipsis;
    // 文本粗细程度
    fontWeight: FontWeight;
    strokeWidth?: string;
    // 文本行高。单位为px。
    lineHeight?: string;
    // 文本修饰线
    decoration?: TextDecoration;
}

各个属性值对应的类型如下:

样式属性的值类型定义

以下是上面样式属性里用到的各个值类型的定义

// 展示策略
const enum Display {
    // 隐藏,不占位
    None = 'none',
    // 隐藏,占位
    Invisible = 'invisible',
    // 正常展示
    Visible = 'visible'
}

// 超出展示
const enum Overflow {
    // 自动
    Auto = 'auto',
    // 显示
    Visible = 'visible',
    // 隐藏
    Hidden = 'hidden'
}

// linear排列方向
const enum Orientation {
    // 垂直
    V = 'v',
    // 水平
    H = 'h'
}

// 布局方位(linear/frame直接子节点应用)
const enum Gravity {
    Top = 'top',
    Bottom = 'bottom',
    Left = 'left',
    Right = 'right',
    CenterV = 'center_vertical',
    CenterH = 'center_horizontal',
    Center = 'center',
    TopLeft = 'top|left',
    LeftTop = 'left|top',
    TopRight = 'top|right',
    RightTop = 'right|top',
    TopCenterH = 'top|center_horizontal',
    CenterHTop = 'center_horizontal|top',
    LeftCenterV = 'left|center_vertical',
    CenterVLeft = 'center_vertical|left',
    RightCenterV = 'right|center_vertical',
    CenterVRight = 'center_vertical|right',
    BottomLeft = 'bottom|left',
    LeftBottom = 'left|bottom',
    BottomRight = 'bottom|right',
    RightBottom = 'right|bottom',
    BottomCenterH = 'bottom|center_horizontal',
    CenterHBottom = 'center_horizontal|bottom'
}

// 主轴的方向
const enum FlexDirection {
    // 水平
    Row = 'row',
    // 垂直
    Col = 'column'
}

// 是否换行
const enum FlexWrap {
    // flex 元素 被打断到多个行中
    Wrap = 'wrap',
    // flex 的元素被摆放到到一行
    NoWrap = 'nowrap'
}

// 定义项目的放大比例(flex直接子节点应用)
const enum FlexGrow {
    // 等分剩余空间
    Yes = '1',
    // 默认为0,即如果存在剩余空间,也不放大
    No = '0'
}

// 定义了项目的缩小比例(flex直接子节点应用)
const enum FlexShrink {
    // 默认为1,即如果空间不足,该项目将缩小
    Yes = '1',
    // 如果一个项目为0,其他项目都为1,则空间不足时,前者不缩小
    No = '0'
}

// 定义了项目在主轴上的对齐方式
const enum JustifyContent {
    // 左对齐(默认值)
    FlexStart = 'flex-start',
    // 右对齐
    FlexEnd = 'flex-end',
    // 居中
    Center = 'center',
    // 两端对齐,项目之间的间隔都相等
    SpaceBetween = 'space-between',
    // 每个项目两侧的间隔相等。所以,项目之间的间隔比项目与边框的间隔大一倍
    SpaceAround = 'space-around'
}

// 定义项目在交叉轴上如何对齐
const enum AlignItems {
    // 交叉轴的起点对齐
    FlexStart = 'flex-start',
    // 交叉轴的终点对齐
    FlexEnd = 'flex-end',
    // 交叉轴的中点对齐
    Center = 'center',
    // 如果项目未设置高度或设为auto,将占满整个容器的高度(默认值)
    Stretch = 'stretch'
}

// 允许单个项目有与其他项目不一样的对齐方式,可覆盖align-items属性
const enum AlignSelf {
    // 继承父元素
    Auto = 'auto',
    // 交叉轴的起点对齐
    FlexStart = 'flex-start',
    // end:交叉轴的终点对齐
    FlexEnd = 'flex-end',
    // 交叉轴的中点对齐
    Center = 'center',
    // 如果项目未设置高度或设为auto,将占满整个容器的高度(默认值)
    Stretch = 'stretch'
}

// 图片填充方式
const enum ImgScaleType {
    // 不保持宽高比,拉伸填满
    FitXY = 'fitXY',
    // 保持宽高比,居中铺满,多余裁剪
    CenterCrop = 'centerCrop',
    // 保持宽高比,短边填满,剩余空白
    FitCenter = 'fitCenter'
}

// Lottie填充方式
const enum LottieResizeMode {
    // 保持宽高比,全部容纳
    Contain = 'contain'
}

// 文本粗细程度
const enum FontWeight {
    // 正常不加粗
    Normal = 'normal',
    // 500
    Mid = '500',
    // 加粗
    Bold = 'bold'
}

// 文本截断位置
const enum Ellipsis {
    // 开头
    Start = 'start',
    // 中间
    Center = 'center',
    // 结尾
    End = 'end'
}

// 文本修饰线
const enum TextDecoration {
    // 无
    None = 'none',
    // 删除线
    Through = 'line-through',
    // 下划线
    Under = 'underline'
}

// 是否循环播放
const enum LottieRepeat {
    Yse = '1',
    No = '0'
}

通用样式定义

以下样式属性集合比较常用, 我们单独定义一个类型, 方便后面定义标签时复用。

/**
 * 通用样式属性,属性值类型和 Style 类型一致, 属于 Style 类型的一个子集
 */
interface GeneralStyle {
    visibility?: Display;
    width?: string;
    height?: string;
    margin?: string;
    padding?: string;
    borderWidth?: string;
    borderColor?: string;
    borderRadius?: string;
    bgColor?: string;
}

绑定数据

DSL 里大部分字段的值,可以绑定数据里特定的字段, 格式为 '${字段名}' , 其中字段名支持多级,数据整体本身命名固定为 data

比如, style 下的 color 属性值需要绑定数据里的 x 字段, 则 DSL 里的对应 color 属性的描述为 color: '${data.x}'

条件语法

DSL 里也可以使用条件语法,在标签节点的 condition 字段里声明即可, 支持 mfor mif show 三种语法。

// 条件类型
interface Condition {
    // 是否渲染
    mif?: string;
    // 是否展示
    show?: string;
    // 循环渲染
    mfor?: ConditionFor;
}

mfor

表示将一个节点循环输出,类似 vue.js 里的 v-for 指令,一般在遍历数组或对象时用来,具体定义如下:

interface ConditionFor {
    // 循环的列表变量,支持数组和对象, 值可以是数组或对象常量,也可以绑定数据
    list: string;

    item: string; // 遍历列表项的变量名,代表该项的值
    index: string; // 遍历列表项的索引变量名, 代表该项的索引
}

以下是使用 mfor 的示例:

遍历数组:

{
    type: 'span',
    condition: {
        mfor: {
            list: '${data.x}', // 变量 x 是个数组
            item: 'item', // 值就是对应单个列表项的变量名, 可以指定任意合法的变量名
            index: 'i' // 对应单个列表项在数组中的索引, 可以指定任意合法的变量名
        },
    },
    text: '${ i } - ${ item }' // 可以使用 mfor 条件里定义的变量名
}

遍历对象:

{
    type: 'span',
    condition: {
        mfor: {
            list: '${data.x}', // 变量 x 是个对象
            item: 'val', // 值可以指定任意合法的变量名, 对应对象里单个键值对的值
            index: 'key' // 值可以指定任意合法的变量名, 对应对象里单个键值对的健
        },
    },
    text: '${ key } - ${ val }' // 可以使用 mfor 条件里定义的变量名
}

mifshow

这两个条件比较类似,一个表示是否渲染, 一个表示渲染后是否展示, 值的格式都一样,是一个表达式, 返回值会被强制转为 布尔值。

以下是几个示例:

  • mif: '${x} > 1'
  • show: '${isShow}'

标签定义

以下是标准里支持的所有标签的具体定义, 生成 DSL 时只应该使用以下定义的标签:

flex 标签

最常用的布局标签之一:弹性布局, 类型定义如下:

/**
 * flex 标签支持的样式属性, flex 标签只能使用这些样式属性
 * 属性值类型和 Style 类型一致, 属于 Style 类型的一个子集
 */
interface FlexStyle extends GeneralStyle {
    flexDirection: FlexDirection;
    bgImg?: string;
    scaleType?: ImgScaleType;
    overflow?: Overflow;
    flexWrap?: FlexWrap;
    alignItems?: AlignItems;
    justifyContent?: JustifyContent;
    gravity?: Gravity;
    weight?: string;
    flexGrow?: FlexGrow;
    flexShrink?: FlexShrink;
    alignSelf?: AlignSelf;
}

// flex 标签的定义
interface FlexNode extends ContainerNode {
    // 标签名固定为 'flex'
    type: 'flex';
    style: FlexStyle;
}

frameLayout 标签

较常用的布局标签之一: 层叠布局,类似 CSS 里 position: absolute 的效果,只用在合适的场景里, 类型定义如下:

/**
 * frameLayout 标签支持的样式属性, frameLayout 标签只能使用这些样式属性
 * 属性值类型和 Style 类型一致, 属于 Style 类型的一个子集
 */
interface FrameStyle extends GeneralStyle {
    bgImg?: string;
    scaleType?: ImgScaleType;
    overflow?: Overflow;
    gravity?: Gravity;
    weight?: string;
    flexGrow?: FlexGrow;
    flexShrink?: FlexShrink;
    alignSelf?: AlignSelf;
}

// frameLayout 标签的定义
interface FrameNode extends ContainerNode {
    // 标签名固定为 'frameLayout'
    type: 'frameLayout';
    style: FrameStyle;
}

linearLayout 标签

常用的布局标签之一: 线性布局,类似 Android 里的 linearLayout, 类型定义如下:

/**
 * linearLayout 标签支持的样式属性, linearLayout 标签只能使用这些样式属性
 * 属性值类型和 Style 类型一致, 属于 Style 类型的一个子集
 */
interface LinearStyle extends GeneralStyle {
    bgImg?: string;
    scaleType?: ImgScaleType;
    orientation?: Orientation;
    weightSum?: string;
    gap?: string;
    showNum?: string;
    gravity?: Gravity;
    weight?: string;
    flexGrow?: FlexGrow;
    flexShrink?: FlexShrink;
    alignSelf?: AlignSelf;
}

// linearLayout 标签的定义
interface LinearNode extends ContainerNode {
    // 标签名固定为 'linearLayout'
    type: 'linearLayout';
    style: LinearStyle;
}

scroll 标签

较常用的布局标签之一: 滚动布局,内部的子元素高度或宽度超出容器尺寸后, 支持滚动,只用在合适的场景里, 类型定义如下:

/**
 * scroll 标签支持的样式属性, scroll 标签只能使用这些样式属性
 * 属性值类型和 Style 类型一致, 属于 Style 类型的一个子集
 */
interface ScrollStyle extends GeneralStyle {
    orientation: Orientation;
    gravity?: Gravity;
    weight?: string;
    flexGrow?: FlexGrow;
    flexShrink?: FlexShrink;
    alignSelf?: AlignSelf;
}

// scroll 标签的定义
interface ScrollNode extends ContainerNode {
    // 标签名固定为 'scroll'
    type: 'scroll';
    style: ScrollStyle;
}

span 标签

常用的内容标签之一: 纯文本标签, 用来渲染文字, 所有文字必须包含在一个 span 标签里, 类型定义如下:

/**
 * span 标签支持的样式属性, span 标签只能使用这些样式属性
 * 属性值类型和 Style 类型一致, 属于 Style 类型的一个子集
 */
interface SpanStyle {
    visibility?: Display;
    fontSize: string;
    color: string;
    ellipsis: Ellipsis;
    fontWeight: FontWeight;
    strokeWidth?: string;
    lineHeight?: string;
    decoration?: TextDecoration;
}

// span 标签的定义
interface SpanNode extends LeafNode {
    // 标签名固定为 'span'
    type: 'span';
    style: SpanStyle;

    /**
     * 文本内容
     */
    text: string;
}

img 标签

常用的内容标签之一: 图片标签,常用来显示图片, 类型定义如下:

/**
 * img 标签支持的样式属性, img 标签只能使用这些样式属性
 * 属性值类型和 Style 类型一致, 属于 Style 类型的一个子集
 */
interface ImgStyle extends GeneralStyle {
    // 图片地址,一个 URL
    src: string;
    scaleType: ImgScaleType;
    placeHolder?: string;
    gravity?: Gravity;
    weight?: string;
    flexGrow?: FlexGrow;
    flexShrink?: FlexShrink;
    alignSelf?: AlignSelf;
}

// img 标签的定义
interface ImgNode extends LeafNode {
    // 标签名固定为 'img'
    type: 'img';
    style: ImgStyle;
}

lottie 标签

内容标签之一: lottie 标签, 语法类似 img 标签,但只能用来显示 lottie 资源, 类型定义如下:

/**
 * lottie 标签支持的样式属性, lottie 标签只能使用这些样式属性
 * 属性值类型和 Style 类型一致, 属于 Style 类型的一个子集
 */
interface LottieStyle extends GeneralStyle {
    // lottie 资源地址, 一个 URL
    src: string;
    scaleType: ImgScaleType;
    placeHolder?: string;
    loopTime?: string;
    scale?: string;
    repeat?: string;
    resizeMode?: LottieResizeMode;
    gravity?: Gravity;
    weight?: string;
    flexGrow?: FlexGrow;
    flexShrink?: FlexShrink;
    alignSelf?: AlignSelf;
}

// lottie 标签的定义
interface LottieNode extends Node {
    // 标签名固定为 'lottie'
    type: 'lottie';
    style: LottieStyle;
}

最后

一个合法的 DSL 描述必须符合以下类型:

type DSLNode = FlexNode | FrameNode | LinearNode | ScrollNode | SpanNode | ImgNode | LottieNode;

即:

  • DSL 描述里只能使用 DSLNode 里定义的这些标签, type 字段的值绝对不能出现其他未定义的标签名
  • 各个标签里只能使用该标签类型定义里声明的样式属性, style 字段下绝对不能出现该标签类型定义里未声明的属性名(注意定义里使用了 typescript 的 继承语法)

以下是一个基于特定 mock 数据的 DSL 示例:

示例

下面是一个比较完整的示例, 包括了 mock 数据, 以及绑定了 mock 数据的 DSL 描述。

json 格式的 mock 数据如下:

{
    "title": "xxx",
    "person": {
        "name": "Jack"
    },
    "x": ["a", "b"],
    "y": {
        "a": 1,
        "b": 2
    }
}

DSL 描述:

注意:为了方便描述,DSL 里省略了大部分的样式属性, 同时没有使用 JSON 格式, 而是 JS 里的对象格式

// 需要严格符合 DSLNode 类型
{
    type: 'flex';
    style: {
        flexDirection: 'column',
    },
    children: [
        {
            type: 'flex',
            style: {
                flexDirection: 'row',
            },
            children: [
                {
                    type: 'span',
                    text: '${ data.title }' // 使用 title 字段
                }
                {
                    type: 'span',
                    text: '${ data.person.name }' // 使用 person.name 字段
                }
            ]
        },
        {
            type: 'flex',
            style: {
                flexDirection: 'row',
            },
            children: [
                {
                    type: 'span',
                    condition: {
                        // 使用 mfor 语法来遍历数组, 以便精简 DSL 体积
                        mfor: {
                            list: '${data.x}', // 变量 x 是个数组
                            item: 'item', // 值就是对应单个列表项的变量名, 可以指定任意合法的变量名
                            index: 'i' // 对应单个列表项在数组中的索引, 可以指定任意合法的变量名
                        },
                    },
                    text: '${ i } - ${ item }' // 可以使用 mfor 条件里定义的变量名
                }
            ]
        },
        {

            type: 'flex',
            style: {
                flexDirection: 'row',
            },
            children: [
                {
                    type: 'flex',
                    style: {
                        flexDirection: 'row',
                    },
                    condition: {
                        // 使用 mfor 语法来遍历对象, 以便精简 DSL 体积
                        mfor: {
                            list: '${data.y}', // 也支持对 对象使用 mfor 条件语法
                            item: 'val', // 值可以指定任意合法的变量名, 对应对象里单个键值对的值
                            index: 'key' // 值可以指定任意合法的变量名, 对应对象里单个键值对的健
                        },
                    },
                    children: [
                        {
                            type: 'img',
                            style: {
                                src: '${ val.imgUrl }' // 使用 for 条件里定义的变量名
                            }
                        },
                        {
                            type: 'span',
                            text: '${ key } - ${ val.text }' // 使用 for 条件里定义的变量名
                        }
                    ]
                }
            ]
        }
    ]
}