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,
},
}
}
)
}