建议先将amis文档从头到尾,仔细看一遍。
参考:amis - 低代码前端框架
amis 的渲染过程是将 json 转成对应的 React 组件。先通过 json 的 type 找到对应的 Component,然后把其他属性作为 props 传递过去完成渲染。
import * as React from 'react';
import {Renderer} from 'amis-core';
@Renderer({ // amis-core/src/factory.tsx里的Renderer方法,主要作用识别json格式的type交给对应react组件来处理(现在可以识别{"type": "page", "title": "自定义组件示例"} )。
type: 'page'
// ... 其他信息隐藏了
})
export class PageRenderer extends React.Component {
// ... 其他信息隐藏了
render() {
const { title, body, render /*用来渲染孩子节点,如果当前是叶子节点则可以忽略。*/ } = this.props;
return (
<div className="page">
<h1>{title}</h1>
<div className="body-container">
{render('body', body,{
// 这里的信息会作为 props 传递给子组件,一般情况下都不需要这个
}) /*渲染孩子节点*/}
</div>
</div>
);
}
}
// 如果不支持 Decorators 语法也可以使用如下写法
export Renderer({
type: 'page'
})(class PageRenderer extends React.Component {
render() {
// ...同上
}
})
React注册自定义组件:
1.比如:注册一个 React 组件,当节点的 type 是 my-renderer 时,交给当前组件来完成渲染。
import * as React from 'react';
import {Renderer} from 'amis';
@Renderer({
type: 'my-renderer',
autoVar: true // amis 1.8 之后新增的功能,自动解析出参数里的变量
})
class CustomRenderer extends React.Component {
render() {
const {tip} = this.props;
return <div>这是自定义组件:{tip}</div>;
}
}
有了以上这段代码后,就可以这样使用了:
{
"type": "page",
"title": "自定义组件示例",
"body": {
"type": "my-renderer",
"tip": "简单示例"
}
}
如果这个组件还能通过 children 属性添加子节点,则需从props中获取body, render处理(参考上面page组件)。
render(region, node, props) 方法,这个方法就是专门用来渲染子节点的。来看下参数说明:
* region 区域名称,你有可能有多个区域可以作为容器,请不要重复。
* node 子节点。
* props 可选,可以通过此对象跟子节点通信等。
属性支持变量
因为配置了 autoVar: true,使得所有组件参数将自动支持变量,在组件内拿到的将是解析后的值(ps: 1.8.0 及以上版本新增配置,之前版本需要调用 amis 里的 resolveVariableAndFilter 方法)
2.表单项FormItem的扩展(amis-core/src/renderes/Item)
以上是普通渲染器的注册方式,如果是表单项,为了更简单的扩充,请使用 FormItem 注解,而不是 Renderer。 原因是如果用 FormItem 是不用关心:label 怎么摆,表单验证器怎么实现,如何适配表单的 3 种展现方式(水平、上下和内联模式),而只用关心:有了值后如何回显,响应用户交互设置新值。
import * as React from 'react';
import {FormItem} from 'amis';
@FormItem({
type: 'custom'
})
class MyFormItem extends React.Component {
render() {
const {value, onChange} = this.props;
return (
<div>
<p>这个是个自定义组件</p>
<p>当前值:{value}</p>
<a
className="btn btn-default"
onClick={() => onChange(Math.round(Math.random() * 10000))}
>
随机修改
</a>
</div>
);
}
}
有了以上这段代码后,就可以这样使用了:
{
"type": "page",
"title": "自定义组件示例",
"body": {
"type": "form",
"body": [
{
"type": "custom",
"label": "随机值",
"name": "random"
}
]
}
}
注意: 使用 FormItem 默认是严格模式,即只有必要的属性变化才会重新渲染,有可能满足不了你的需求,如果忽略性能问题,可以传入 strictMode: false 来关闭。
表单项开发主要关心两件事。
1.呈现当前值。如以上例子,通过 this.props.value 判定如果勾选了则显示已勾选,否则显示请勾选。
2.接收用户交互,通过 this.props.onChange 修改表单项值。如以上例子,当用户点击按钮时,切换当前选中的值。
至于其他功能如:label/description 的展示、表单验证功能、表单布局(常规、左右或者内联)等等,只要是通过 FormItem 注册进去的都无需自己实现。
需要注意,获取或者修改的是什么值跟配置中 type 并列的 name 属性有关,也就是说直接关联某个变量,自定义中直接通过 props 下发了某个指定变量的值和修改的方法。如果你想获取其他数据,或者设置其他数据可以看下以下说明:
* 获取其他数据 可以通过 this.props.data 查看,作用域中所有的数据都在这了。
* 设置其他数据 可以通过 this.props.onBulkChange, 比如: this.props.onBulkChange({a: 1, b: 2}) 等于同时设置了两个值。当做数据填充的时候,这个方法很有用。
3.其它高级定制
——自定义验证器
如果 amis 自带的验证能满足需求了,则不需要关心。组件可以有自己的验证逻辑。
@FormItem({ type: 'custom-checkbox' })
export default class CustomCheckbox extends React.Component {
validate() {
// 通过 this.props.value 可以知道当前值。
return isValid ? '' : '不合法,说明不合法原因。';
}
// ... 其他省略了
}
上面的例子只是简单说明,另外可以做异步验证,validate 方法可以返回一个 promise。
——OptionsControl (amis-core/src/renderes/Options)
如果你的表单组件性质和 amis 的 Select、Checkboxes、List 差不多,用户配置配置 source 可通过 API 拉取选项,你可以用 OptionsControl 取代 FormItem 这个注解。
用法是一样,功能方面主要多了以下功能。
* 可以配置 options,options 支持配置 visibleOn hiddenOn 等表达式
* 可以配置 source 换成动态拉取 options 的功能,source 中有变量依赖会自动重新拉取。
* 下发了这些 props,可以更方便选项。
* options 不管是用户配置的静态 options 还是配置 source 拉取的,下发到组件已经是最终的选项了。
* selectedOptions 数组类型,当前用户选中的选项。
* loading 当前选项是否在加载
* onToggle 切换一个选项的值
* onToggleAll 切换所有选项的值,类似于全选。
4.组件间通信
关于组件间通信,amis 中有个机制就是,把需要被引用的组件设置一个 name 值,然后其他组件就可以通过这个 name 与其通信,比如这个例子。其实内部是依赖于内部的一个 Scoped Context。你的组件希望可以被别的组件引用,你需要把自己注册进去,默认自定义的非表单类组件并没有把自己注册进去,可以参考以下代码做添加:
import * as React from 'react';
import {Renderer, ScopedContext} from 'amis';
@Renderer({ type: 'my-renderer'})
export class CustomRenderer extends React.Component {
static contextType = ScopedContext;
constructor() {
const scoped = this.context;
scoped.registerComponent(this);
}
componentWillUnmount() {
const scoped = this.context;
scoped.unRegisterComponent(this);
}
// 其他部分省略了。
}
把自己注册进去了,其他组件就能引用到了。同时,如果你想找别的组件,也同样是通过 scoped 这个 context,如: scoped.getComponentByName("xxxName") 这样就能拿到目标组件的实例了(前提是目标组件已经配置了 name 为 xxxName)。
5.自定义组件接入事件动作
需求场景主要是想要自定义组件的内部事件暴露出去,能够通过对事件的监听来执行所需动作,并希望自定义组件自身的动作能够被其他组件调用。接入方法是通过`props.dispatchEvent`派发自身的各种事件,使其具备更灵活的交互设计能力;
通过重写`doAction`方法实现其他组件对其专属动作的调用,需要注意的是,此处依赖内部的 `Scoped Context`来实现自身的注册
amis/src/renderers中不同的组件可重写自己的doAction方法(实现自己的组件专属动作)
可以直接调某一组件的doAction方法:comp.doAction()触发组件特有动作。 const values = await form.doAction( { type: 'submit' }, form.props.data, true );
也可以通过onEvent配置组件特有动作(CmptAction)去触发对应组件的特有动作
自定义的渲染器 props 会下发一个非常有用的 env 对象。这个 env 有以下功能方法:
* env.fetcher 可以用来做 ajax 请求如: this.props.env.fetcher('xxxAPi', this.props.data).then((result) => console.log(result))
* env.confirm 确认框,返回一个 promise 等待用户确认如: this.props.env.confirm('你确定要这么做?').then((confirmed) => console.log(confirmed))
* env.alert 用 Modal 实现的弹框,个人觉得更美观。
* env.notify toast 某个消息 如: this.props.env.notify("error", "出错了")
* env.jumpTo 页面跳转。
大部分组件都是直接继承 RendererProps,里面包含渲染组件所需的常用属性. 例如:export interface PageProps extends RendererProps
amis-editor注册自定义组件
比如antd按钮组件:
方法一:这里'amis-widget'的registerAmisEditorPlugin, registerRendererByType分别注册plugin插件和renderer渲染器。
src/plugins/AntdButton.tsx:
import type {BaseEventContext, RendererPluginEvent} from 'amis-editor-core';
import {getSchemaTpl} from 'amis-editor-core';
import {getEventControlConfig} from 'amis-editor/lib/renderer/event-control/helper';
import {Button, ButtonProps} from 'antd';
import React from 'react';
export class AntdButtonPlugin {
rendererName = 'antd-button';
$schema = '/schemas/UnkownSchema.json';
name = '按钮';
description = 'Ant Design按钮预设模板';
tags = ['Ant Design'];
icon = 'fa fa-square';
scaffold = {
type: 'antd-button',
content: 'Antd 按钮',
block: false,
danger: false,
disabled: false,
ghost: false,
shape: 'default',
size: 'middle',
buttonType: 'primary'
};
previewSchema = {
...this.scaffold
};
panelTitle = '按钮';
events: RendererPluginEvent[] = [
{
eventName: 'onClick',
eventLabel: '按钮点击',
description: '按钮点击时触发',
defaultShow: true
}
];
panelBodyCreator = (context: BaseEventContext) => {
const id = context.id;
const manager = (window as any).store.editorManager;
return getSchemaTpl('tabs', [
{
title: '基础',
body: [
{
type: 'input-text',
name: 'content',
label: '按钮内容',
value: 'Antd 按钮'
},
{
type: 'switch',
name: 'block',
label: '将按钮宽度调整为其父宽度的选项',
value: false
},
{
type: 'switch',
name: 'danger',
label: '危险按钮',
value: false
},
{
type: 'switch',
name: 'disabled',
label: '禁用按钮',
value: false
},
{
type: 'switch',
name: 'ghost',
label: '幽灵属性',
value: false
},
{
type: 'input-text',
name: 'href',
label: '点击跳转的地址',
value: undefined
},
{
type: 'select',
name: 'shape',
label: '按钮形状',
value: 'default',
options: [
{
label: '默认',
value: 'default'
},
{
label: '圆形',
value: 'circle'
},
{
label: '圆弧',
value: 'round'
}
]
},
{
type: 'select',
name: 'size',
label: '按钮大小',
value: 'middle',
options: [
{
label: 'large',
value: 'large'
},
{
label: 'middle',
value: 'middle'
},
{
label: 'small',
value: 'small'
}
]
},
{
type: 'select',
name: 'buttonType',
label: '按钮类型',
value: 'primary',
options: [
{
label: '主要按钮',
value: 'primary'
},
{
label: '虚线按钮',
value: 'dashed'
},
{
label: '链接按钮',
value: 'link'
},
{
label: '文本按钮',
value: 'text'
},
{
label: '默认按钮',
value: 'default'
}
]
}
]
},
{
title: '事件',
className: 'p-none',
body: [
getSchemaTpl('eventControl', {
name: 'onEvent',
...getEventControlConfig(manager, context)
})
]
}
]);
};
}
/**
onClick={
onClick
? e => new Function(`return ${onClick}`)()(e)
: function onClick(e) {
console.log('click');
}
}
*/
export function AntdButton({
content,
block,
danger,
disabled,
ghost,
href,
shape,
size,
buttonType,
onClick
}: ButtonProps & {buttonType: ButtonProps['type']; onClick: string}) {
const type = buttonType;
return (
<Button
danger={danger || false}
disabled={disabled || false}
type={type || 'primary'}
block={block || false}
ghost={ghost || false}
href={href || undefined}
shape={shape || 'default'}
size={size || 'middle'}
>
{content || 'Antd 按钮'}
</Button>
);
}
src/plugins/index.ts中进行plugin注册:
//@ts-ignore
import {registerAmisEditorPlugin, registerRendererByType} from 'amis-widget';
// import {registerEditorPlugin} from 'amis-editor';
// import {AntdCalendarPlugin, AntdCalendar} from './AntdCalendar';
// registerEditorPlugin(AntdCalendarPlugin)
import './AntdCalendar';
import {AntdButtonPlugin, AntdButton} from './AntdButton';
import {AntdDropdownPlugin, AntdDropdown} from './AntdDropdown';
import {ProCRUDPlugin, ProCRUD} from './ProCRUD';
import {ChartPiePlugin, ChartPie} from './ChartPie';
import {ChartScatterPlugin, ChartScatter} from './ChartScatter';
import {ChartMapPlugin, ChartMap} from './ChartMap';
enum Usage {
renderer = 'renderer',
formitem = 'formitem',
options = 'options'
}
enum Framework {
react = 'react',
vue2 = 'vue2',
vue3 = 'vue3',
jquery = 'jquery'
}
const plugins = [
{
type: 'antd-button',
plugin: AntdButtonPlugin,
component: AntdButton
},
{
type: 'antd-dropdown',
plugin: AntdDropdownPlugin,
component: AntdDropdown
},
{
type: 'pro-crud',
plugin: ProCRUDPlugin,
component: ProCRUD
},
{
type: 'chart-pie',
plugin: ChartPiePlugin,
component: ChartPie
},
{
type: 'chart-scatter',
plugin: ChartScatterPlugin,
component: ChartScatter
},
{
type: 'chart-map',
plugin: ChartMapPlugin,
component: ChartMap
},
];
export default () => {
plugins.forEach(({type, plugin, component}) => {
registerAmisEditorPlugin(plugin);
registerRendererByType(component, {
type,
usage: Usage.renderer,
weight: 99,
framework: Framework.react
});
});
};
方法二:采用amis-editor的registerEditorPlugin注册plugin插件。 amis的@Renderer 注册renderer渲染器
src/plugins/AntdCalendar.tsx:
import {Calendar, CalendarProps} from 'antd';
import React from 'react';
import {Renderer, RendererProps} from 'amis';
import {BasePlugin, registerEditorPlugin} from 'amis-editor';
export class AntdCalendarPlugin extends BasePlugin{
rendererName = 'antd-calendar';
$schema = '/schemas/UnkownSchema.json';
name = '日历';
description = 'Ant Design日历预设模板';
tags = ['Ant Design'];
icon = 'fa fa-calendar';
scaffold = {
type: 'antd-calendar',
fullscreen: false
};
previewSchema = {
...this.scaffold
};
panelTitle = '日历';
panelControls = [
{
type: 'switch',
name: 'fullscreen',
label: '是否全屏',
value: false
}
];
}
// @Renderer({
// type: 'antd-calendar',
// name: 'antd-calendar',
// autoVar: true
// })
// export class AntdCalendar extends React.Component<RendererProps> {
// render() {
// const {fullscreen} = this.props;
// return <Calendar fullscreen={fullscreen || false} />;
// }
// }
export function AntdCalendar({fullscreen}: RendererProps) {
return <Calendar fullscreen={fullscreen || false} />;
}
Renderer({
type: 'antd-calendar',
name: 'antd-calendar',
autoVar: true
})(AntdCalendar);
registerEditorPlugin(AntdCalendarPlugin);
amis-sdk中注册自定义组件
react组件注册,这里以antd为例:
首先如果要使用React hook函数,必须满足hook规范:
1.在 React 的函数组件中调用 Hook;
2. 在自定义 Hook 中调用其他 Hook。
3.为了使 Hook 正常工作,你应用代码中react 依赖以及 react-dom 的 package 内部使用的 react 依赖,必须解析为同一个模块。
其次:antd组件库是默认排除React依赖的(没有合并React的代码到打包后的js中),如果是npm install使用(是通过import/require加载的父模块本地install的React依赖)。如果是cdn引入的话,则是通过全局变量获取的React依赖(root["React"])
方式一、配置external和cdn引入antd,保证与amis是同一个React. 推荐!!!
1.vue.config.js中配置external,使用全局变量React和antd:
configureWebpack: config => {
config.externals = {
'react': 'React',
'^/antd/.*': 'antd',
}
},
2.index.html中 将amis的React挂载到全局变量上:
<!-- cdn引入antd,加载全局变量中的React(root["React"]) 保证与amis依赖于同一个React-->
<script src="https://cdnjs.cloudflare.com/ajax/libs/antd/4.18.2/antd.js"></script>
window.React = amisRequire('react');
3.配置src/jsx/Calendar.jsx:
import React from 'react';//或者不配react的external,直接const React = amisRequire('react'); 也行
import {Calendar, CalendarProps} from 'antd';
const { Renderer } = amisRequire('amis');
export function AntdCalendar({fullscreen}) {
//配置了@babel/preset-react后也可以使用jsx语法
return React.createElement(Calendar, { fullscreen: fullscreen || false });
}
Renderer({
type: 'antd-calendar',
name: 'antd-calendar',
autoVar: true
})(AntdCalendar);
若要使用jsx语法(vue-cli项目中):
yarn add -D @babel/preset-react //解析jsx语法为React.createElement
配置babel.config.js:
{ "presets": [['@vue/app', { useBuiltIns: 'entry' }], "@babel/preset-react"] }
然后改为 return <Calendar fullscreen={fullscreen || false }/> 即可
4.main.js中导入即可:
import './jsx/Calendar.jsx';
方式二、npm install使用(amis和antd依赖于俩个不同的React) 不推荐【建议还是都依赖于同一React】
1.yarn add react react-dom antd
2.配置src/jsx/Calendar.jsx:
import {Calendar, CalendarProps} from 'antd';
const { Renderer } = amisRequire('amis');
import * as ReactDOM from 'react-dom/client'; //与antd中(import)的主项目React保持一致,保证antd的hook正常执行 此处用的是 18 版本
const React = amisRequire('react'); //使用hook函数 要与amis中render的React保持一致
export function AntdCalendar({fullscreen}) {
let dom = React.useRef(null);
React.useEffect(function () { //组件挂载时和更新都会执行
const root = ReactDOM.createRoot(dom.current);
root.render(React.createElement(Calendar, { fullscreen: fullscreen || false }))
});
return React.createElement('div', {
ref: dom
});
}
Renderer({
type: 'antd-calendar',
name: 'antd-calendar',
autoVar: true
})(AntdCalendar);
3.main.js中导入即可:
import './jsx/Calendar.jsx';