sula探索

675 阅读4分钟

sula 是什么?

  • 产品级配置,sula 通过行为链管理实现了渲染组件与用户行为的连接,实现了行为配置,极大扩展了配置范围。
  • 语义化,通过渲染插件与行为插件的组合,一段段配置可以语义化的描述UI呈现及用户行为动作。
  • 开发提效,sula 提供 4 大配置模板,可以满足80%以上的中台场景,提效明显。
  • 高扩展性,sula 通过 ctx 实现了插件与核心组件的通信,配置规则灵活且易于扩展。
  • 开箱即用,sula 内置请求插件与灵活扩展点、国际化,路由等能力,让开发者更加专注核心功能的开发。
  • 自动 loading,sula 不侵入式帮助用户完成按钮、超链接、图标点击过程的 loading 管理。

项目收获

项目中使用了sula封装组件 一点小收获

⚠️ 如果组件中不需要渲染其他的FieldPlugin,开发为受控组件可以单独用

注册

插件使用前需先注册

注册插件类型

说明:插件是全局作用的,所以建议在全局注册;

以field 插件为例 registerFieldPlugin 是 field 插件注册的内部方法

参数 pluginName 插件名 string

Component 组件 React.ComponentClass | React.FunctionComponent

hasSource 是否有数据源 boolean

hasCtx 是否在 props 中传入 ctx boolean

调用形式

registerFieldPlugin(pluginName)(Component, hasSource, hasCtx);
import EditableTable from './EditableTable';
registerFieldPlugin('EditableTable')(EditableTable);
export {
    Mentions,
    EditableTable,
};

开发组件

组件中需要渲染其他FieldPlugin时,要通过Field组件去渲染

示例

⚠️:

tableform 一开始使用triggerFieldPlugin 丢失一些filed能力

示例代码

_columns = columns.map(({ title = '', key = '', dataIndex = '', field = 'input' }) => {
            return {
                title,
                key,
                dataIndex,
                render(value, record) {
                    return triggerFieldPlugin(
                        {},
                        {
                            type: field,
                            props: {
                                value,
                                onChange: (e) => {
                                    handleFieldChange(e, dataIndex, record.key);
                                },
                            },
                        },
                    );
                },
            };
        });

以field插件为例,上下文ctx在form和mode基础上增加了disabled和source属性,便于让sula Form更方便控制表单组件。

而且Field和FieldGroup, Form都可以外层传 container容器。

使用了Filed的组件必须配合Form使用,不能单独使用;

Form: Form可以理解为是 antd form的载体,将antd form的配置属性放入React Context中,供Field与FieldGroup组件使用,施加在 Form 上的 container 和 Field 配置并不被 Form直接消费,而是转交给 FieldGroup 和 Field 组件消费

正确渲染

 _columns = _fields.map(({ name, label, ...rest }) => {
        return {
            title: label,
            dataIndex: name,
            render(_, __, index) {
                const props = {
                    ...rest,
                    name: [id, index, name],
                };
                return <Field {...props} />;
            },
        };
    });

注意:

  • 在定义组件props属性时,与FieldPlugin相关的属性需要与field配置字段保持一致,
  • 渲染Field时要传入正确的name(name path)和其余field参数。
  • 开发组件的时候,多使用sula内部封装好的能力,
  • 如果组件中没有其他的FieldPlugin,可以做成受控组件,

triggerFieldPlugin,triggerPlugin部分源码参考

源码:
/**
 * 1. lazyCtx -> ctx
 * 2. 方法 config.type 执行
 * 3. 配置属性转换
 * 4. 如果是render插件则做funcProps与props的合并
 * 5. 触发插件
 */
export const triggerPlugin = (
  name: PluginType,
  ctx: PluginCtx | null,
  config:
    | RenderPlugin
    | ActionPlugin
    | ValidatorPlugin
    | ConvertParamsPlugin
    | ConverterPlugin
    | DependencyPlugin,
  skipOptions?: SkipOptions,
  isRender?: boolean,
) => {
  // 1. normalize config
  const normalizedConfig = normalizeConfig(config);

  if (isFunction(normalizedConfig.type)) {
    return normalizedConfig.type(ctx);
  }

  // 2. render config transform
  let transedConfig = transformConfig(normalizedConfig, ctx, skipOptions);

  if (isRender) {
    const funcProps = transedConfig.funcProps;
    if (funcProps) {
      transedConfig.props = assign({}, transedConfig.props, funcProps);
    }
  }

  // TODO
  return sula[name](transedConfig.type, ctx, transedConfig);
};





/**
 * 表单插件
 * - skipFuncObjKeys
 *   - props
 * - skipSelector
 */
export const triggerFieldPlugin = (
  lazyCtx: LazyPluginCtx | PluginCtx,
  config: FieldPlugin,
  valuePropName?: string,
) => {
  const skipSelector = (curKey: string | number, parentKey: string | number) =>
    parentKey === 'props' && curKey === valuePropName;

  return triggerPlugin(
    'field',
    getLazyCtx(lazyCtx),
    config,
    {
      skipSelector,
      skipFuncObjKeys: ['props'],
    },
    true,
  );
};

其他

关于request请求

sula fetch 是基于 axios 封装的,为了 防止和 fetch 冲突,不再使用fetch这个名字,默认为get请求。

fetch有两个插件

  • convertParams 请求参数转换
  • converter 响应数据转换

convertParams

  • 类型:((ctx, config) => params) | ConvertParamsPlugin | Array<ConvertParamsPlugin>
  • 默认值: -
const config = {
  convertParams: (ctx) => {
    console.log('params: ', ctx.params);
    return {...ctx.params, seq: 1}
  }
}

converter

  • 类型:((ctx, config) => data) | ConverterPlugin | Array<ConverterPlugin>
  • 默认值: -
const config = {
  converter: (ctx) => {
    console.log('fetch data: ', ctx.data);
    return ctx.data;
  }
}

successMessage

  • 类型:boolean | string | undefined
  • 默认值:false

默认是 false 代表不出成功提示,true 代表使用后端返回message,string 代表自定义提示信息。

示例

const remoteDataSource = {
    url: 'https://www.mocky.io/v2/5ed7a9d33200009abc274abe',
    method: 'GET',
    convertParams({ params }) {
      return {
        results: 3,
        ...params,
      };
    },
    converter({ data }) {
      return {
        list: data.results.map((item, index) => {
          return {
            ...item,
            id: index,
            index,
          };
        }),
        total: 10,
      };
    },
  };

关于插件

插件可分为 UI展示插件 / 行为插件

插件规范

export type PluginType = 'render' | 'field' | 'action';

渲染插件

render-plugin

// 渲染插件
export type RenderPluginFunction = (ctx: RenderCtx) => React.ReactElement;

// 渲染插件配置规则
export type RenderPlugin = {
  type: string | RenderPluginFunction;
  props: Record<string, any>;
  functionProps: Record<string, (ctx: RenderCtx) => string>;
  action?: ActionPlugin | ActionPlugin[];
} | string | RenderPluginFunction;
import React from 'react';
import { Tag } from 'antd';
import sula from './';

sula.renderType('rfctag', (ctx, config) => {
  const color = ctx.getColor();
  return <Tag {...config.props} color={color} />;
})
export default () => {
  return sula.render('rfctag', 
    {
      getColor: () => 'blue',
    },
    {
      props: {
        children: 'sula'
      }
    }
  )
}

行为插件

// 行为插件
export type ActionPluginFunction = (ctx: ActionResultComboCtx) => Promise<any> | any | void;
export type ActionBeforeFunction = (ctx: ActionResultComboCtx) => Promise<boolean> | boolean | void;
export type ActionHookFunction = (ctx: ActionResultComboCtx) => void;
// 行为插件配置规则
export type ActionPlugin = {
  type: string | ActionPluginFunction;
  before?: ActionBeforeFunction;
  error?: ActionHookFunction;
  final?: ActionHookFunction;
  finish?: ActionPlugin | ActionPlugin[];
  [key: string]: any;
} | string | ActionPluginFunction;
import React from 'react';
import { Tag } from 'antd';
import sula from './';

sula.actionType('rfchello', (ctx, config) => {
  const name = ctx.getName();
  console.log(`${config.say} ${name}`);
})
export default () => {
  sula.action('rfchello', {
    getName: () => 'sula',
  }, {
    say: 'hello'
  });

  return <div>nothing</div>;
}

表单插件
// field插件
export type FieldPluginFunction = (ctx: FormCtx) => React.ReactElement;
export type FieldPlugin = {
  type: string | FieldPluginFunction;
  props: Record<string, any>;
  functionProps: Record<string, (ctx: FormCtx) => string>;
  action?: ActionPlugin | ActionPlugin[];
} | string | FieldPluginFunction;
import React from 'react';
import { Select } from 'antd';
import sula from './';

sula.fieldType('rfcselect', (ctx, config) => {
  const { source = [] } = ctx;
  return (
    <Select {...config.props}>
      {source.map((item) => {
        return <Select.Option key={item.value}>{item.text}</Select.Option>;
      })}
    </Select>
  );
})
export default () => {
  return sula.field('rfcselect', 
    {
      source: [{
        text: 'APPLE',
        value: 'apple'
      }, {
        text: 'PEACH',
        value: 'peach',
      }],
    },
    {
      props: {
        style: {
          width: 200,
        },
      }
    }
  )
}