337 lines
9.9 KiB
Markdown
337 lines
9.9 KiB
Markdown
---
|
||
title: 如何定制
|
||
---
|
||
|
||
开始定制之前,请先仔细阅读工作原理。
|
||
|
||
## 工作原理
|
||
|
||
amis 的渲染过程就是将 `json` 转成对应的 React 组件。先通过 `json` 的 type 找到对应的 `Component` 然后,然后把其他属性作为 `props` 传递过去完成渲染。
|
||
|
||
拿一个表单页面来说,如果用React组件调用大概是这样。
|
||
|
||
```jsx
|
||
<Page
|
||
title="页面标题"
|
||
subTitle="副标题"
|
||
>
|
||
<Form
|
||
title="用户登录"
|
||
controls={[
|
||
{
|
||
type: 'text',
|
||
name: 'username',
|
||
label: '用户名'
|
||
}
|
||
]}
|
||
/>
|
||
</Page>
|
||
```
|
||
|
||
把以上配置方式换成 amis JSON, 则是:
|
||
|
||
```json
|
||
{
|
||
"type": "page",
|
||
"title": "页面标题",
|
||
"subTitle": "副标题",
|
||
"body": {
|
||
"type": "form",
|
||
"title": "用户登录",
|
||
"controls": [
|
||
{
|
||
"type": "text",
|
||
"name": "username",
|
||
"label": "用户名"
|
||
}
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
那么,amis 是如何将 JSON 转成组件的呢?直接根据节点的 type 去跟组件一一对应?似乎很可能会重名比如在表格里面展示的类型 `text` 跟表单里面的 `text`是完全不一样的,一个负责展示,一个却负责输入。所以说一个节点要被什么组件渲染,还需要携带上下文(context)信息。
|
||
|
||
如何去携带上下文(context)信息?amis 中直接是用节点的路径(path)来作为上下文信息。从上面的例子来看,一共有三个节点,path 信息分别是。
|
||
|
||
* `page` 页面节点
|
||
* `page/body/form` 表单节点
|
||
* `page/body/form/controls/0/text` 文本框节点。
|
||
|
||
根据 path 的信息就能很容易注册组件跟节点对应了。
|
||
|
||
Page 组件的示例代码
|
||
|
||
```jsx
|
||
@Renderer({
|
||
test: /^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)/*渲染孩子节点*/}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
}
|
||
```
|
||
|
||
Form 组件的示例代码
|
||
|
||
```jsx
|
||
@Renderer({
|
||
test: /(^|\/)form$/,
|
||
// ... 其他信息隐藏了
|
||
})
|
||
export class FormRenderer extends React.Component {
|
||
// ... 其他信息隐藏了
|
||
render() {
|
||
const {
|
||
title,
|
||
controls,
|
||
render // 用来渲染孩子节点,如果当前是叶子节点则可以忽略。
|
||
} = this.props;
|
||
return (
|
||
<form className="form">
|
||
{controls.map((control, index) => (
|
||
<div className="form-item" key={index}>
|
||
{render(`${index}/control`, control)}
|
||
</div>
|
||
))}
|
||
</form>
|
||
);
|
||
}
|
||
}
|
||
```
|
||
|
||
Text 组件的示例代码
|
||
|
||
```jsx
|
||
@Renderer({
|
||
test: /(^|\/)form(?:\/\d+)?\/control(?\/\d+)?\/text$/
|
||
// ... 其他信息隐藏了
|
||
})
|
||
export class FormItemTextRenderer extends React.Component {
|
||
// ... 其他信息隐藏了
|
||
render() {
|
||
const {
|
||
label,
|
||
name,
|
||
onChange
|
||
} = this.props;
|
||
return (
|
||
<div className="form-group">
|
||
<label>{label}<label>
|
||
<input type="text" onChange={(e) => onChange(e.currentTarget.value)} />
|
||
</div>
|
||
);
|
||
}
|
||
}
|
||
```
|
||
|
||
那么渲染过程就是根据节点 path 信息,跟组件池中的组件 `test` (检测) 信息做匹配,如果命中,则把当前节点转给对应组件渲染,节点中其他属性将作为目标组件的 props。需要注意的是,如果是容器组件,比如以上例子中的 `page` 组件,从 props 中拿到的 `body` 是一个子节点,由于节点类型是不固定,由使用者决定,所以不能直接完成渲染,所以交给属性中下发的 `render` 方法去完成渲染,`{render('body', body)}`,他的工作就是拿子节点的 path 信息去组件池里面找到对应的渲染器,然后交给对应组件去完成渲染。
|
||
|
||
## 自定义组件
|
||
|
||
如果 amis 中组件不能满足你的需求,同时你又会 React 组件开发,那么就自己定制一个吧。
|
||
|
||
先来看个简单的例子
|
||
|
||
```jsx
|
||
import * as React from 'react';
|
||
import {
|
||
Renderer
|
||
} from 'amis';
|
||
|
||
@Renderer({
|
||
test: /(^|\/)my\-renderer$/,
|
||
})
|
||
class CustomRenderer extends React.Component {
|
||
render() {
|
||
const {tip} = this.props;
|
||
return (
|
||
<div>这是自定义组件:{tip}</div>
|
||
);
|
||
}
|
||
}
|
||
```
|
||
|
||
有了以上这段代码后,就可以这样使用了。
|
||
|
||
```json
|
||
{
|
||
"type": "page",
|
||
"title": "自定义组件示例",
|
||
"body": {
|
||
"type": "my-renderer",
|
||
"tip": "简单示例"
|
||
}
|
||
}
|
||
```
|
||
|
||
如果你看了[amis工作原理](#工作原理)应该不难理解,这里注册一个 React 组件,当节点的 path 信息是 `my-renderer` 结尾时,交给当前组件来完成渲染。
|
||
如果你只写叶子节点的渲染器,已经可以不用看了,如果你的渲染器中有容器需要可以放置其他节点,那么接着看以下这段代码。
|
||
|
||
```jsx
|
||
import * as React from 'react';
|
||
import {
|
||
Renderer
|
||
} from 'amis';
|
||
|
||
@Renderer({
|
||
test: /(^|\/)my\-renderer2$/,
|
||
})
|
||
class CustomRenderer extends React.Component {
|
||
render() {
|
||
const {
|
||
tip,
|
||
body,
|
||
render
|
||
} = this.props;
|
||
return (
|
||
<div>
|
||
<p>这是自定义组件:{tip}</p>
|
||
{body ? (
|
||
<div className="container">
|
||
{render('body', body, {
|
||
// 这里的信息会作为 props 传递给子组件,一般情况下都不需要这个
|
||
})}
|
||
</div>
|
||
) : null}
|
||
</div>
|
||
);
|
||
}
|
||
}
|
||
```
|
||
|
||
有了以上这段代码后,就可以这样使用了。
|
||
|
||
```json
|
||
{
|
||
"type": "page",
|
||
"title": "自定义组件示例",
|
||
"body": {
|
||
"type": "my-renderer2",
|
||
"tip": "简单示例",
|
||
"body": {
|
||
"type": "form",
|
||
"controls": [
|
||
{
|
||
"type": "text",
|
||
"label": "用户名",
|
||
"name": "usename"
|
||
}
|
||
]
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
跟第一个列子不同的地方是,这里多了个 `render` 方法,这个方法就是专门用来渲染子节点的。来看下参数说明:
|
||
|
||
* `region` 区域名称,你有可能有多个区域可以作为容器,请不要重复。
|
||
* `node` 子节点。
|
||
* `props` 可选,可以通过此对象跟子节点通信等。
|
||
|
||
以上是普通渲染器的注册方式,如果是表单项,为了更简单的扩充,请使用 `FormItem` 注解,而不是 `Renderer`。 原因是如果用 `FormItem` 是不用关心:label怎么摆,表单验证器怎么实现,如何适配表单的3中展现方式(水平、上下和内联模式),而只用关心:有了值后如何回显,响应用户交互设置新值。
|
||
|
||
```jsx
|
||
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>
|
||
);
|
||
}
|
||
}
|
||
```
|
||
|
||
有了以上这段代码后,就可以这样使用了。
|
||
|
||
```json
|
||
{
|
||
"type": "page",
|
||
"title": "自定义组件示例",
|
||
"body": {
|
||
"type": "form",
|
||
"controls": [
|
||
{
|
||
"type": "text",
|
||
"label": "用户名",
|
||
"name": "usename"
|
||
},
|
||
|
||
{
|
||
"type": "custom",
|
||
"label": "随机值",
|
||
"name": "random"
|
||
}
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
> 注意: 使用 FormItem 默认是严格模式,即只有必要的属性变化才会重新渲染,有可能满足不了你的需求,如果忽略性能问题,可以传入 `strictMode`: `false` 来关闭。
|
||
|
||
以上的例子都是需要先注册组件,然后再使用的,如果你在自己项目中使用,还有更简单的用法,以下示例直接无需注册。
|
||
|
||
```jsx
|
||
{
|
||
"type": "page",
|
||
"title": "自定义组件示例",
|
||
"body": {
|
||
"type": "form",
|
||
"controls": [
|
||
{
|
||
"type": "text",
|
||
"label": "用户名",
|
||
"name": "usename"
|
||
},
|
||
|
||
{
|
||
"name": "a",
|
||
"children": ({
|
||
value,
|
||
onChange
|
||
}) => (
|
||
<div>
|
||
<p>这个是个自定义组件</p>
|
||
<p>当前值:{value}</p>
|
||
<a className="btn btn-default" onClick={() => onChange(Math.round(Math.random() * 10000))}>随机修改</a>
|
||
</div>
|
||
)
|
||
}
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
即:通过 `children` 实现一个自定义渲染方法,返回 React.ReactNode 节点。
|
||
任何节点如果包含 `children` 这个属性,则都会把当前节点交给 `children` 来处理,跳过了从 amis 渲染器池子中选择渲染器的过程。`children` 属性其实更应该叫 `render` 属性,但是历史原因不能改了。与之类似的还有个 `component` 属性,这个属性可以传入 React Component,如果想用 React.hooks,请通过 `component` 传递,而不是 `children`。
|