### 背景 在項目中有集成低代碼平臺的想法,經過多方對比最後選擇了 amis,主要是需要通過 amis 進行頁面配置,導出 json 供移動端和 PC 端進行渲染,所以接下來講一下近兩周研究 amis 的新的以及一些簡單經驗,供大家參考. ### 什麼是 amis amis 是一個低代碼前端框架, ...
背景
在項目中有集成低代碼平臺的想法,經過多方對比最後選擇了 amis,主要是需要通過 amis 進行頁面配置,導出 json 供移動端和 PC 端進行渲染,所以接下來講一下近兩周研究 amis 的新的以及一些簡單經驗,供大家參考.
什麼是 amis
amis 是一個低代碼前端框架,它使用 JSON 配置來生成頁面,可以減少頁面開發工作量,極大提升效率。
如何使用 amis
在 amis 官網提供了兩種使用 amis 的方式分別是
- JSSDK 可以在任意頁面使用
- React 可以在 React 項目中使用
博主是在 umi 框架下結合 React 使用 amis,所以本文主要著重介紹第二種方法
在使用時需要對 amis 進行安裝,項目中也需要使用 amis-editor 進行頁面配置所以需要同時安裝如下兩個包
{
"amis": "^3.1.1",
"amis-editor": "^5.4.1"
}
amis
首先介紹 amis,amis 提供了 render 方法來對 amis-editor 生成的 JSON 對象頁面配置進行渲染,如下,在使用是 render 主要作用就是進行渲染
import { render as renderAmis } from "amis";
const App = () => {
return (
<div>
{renderAmis({
type: "button",
label: "保存",
level: "primary",
onClick: function () {
console.log("TEST");
},
})}
</div>
);
};
export default App;
amis-editor
amis-editor 提供了一個編譯器組件 <Editor />
import { useState } from "react";
import { Editor, setSchemaTpl } from "amis-editor";
import type { SchemaObject } from "amis";
import { render as renderAmis } from "amis";
import type { Schema } from "amis/lib/types";
// 以下樣式均生效
import "amis/lib/themes/default.css";
import "amis/lib/helper.css";
import "amis/sdk/iconfont.css";
import "amis-editor-core/lib/style.css";
import "amis-ui/lib/themes/antd.css";
type Props = {
defaultPageConfig?: Schema;
codeGenHandler?: (codeObject: Schema) => void;
pageChangeHandler?: (codeObject: Schema) => void;
};
export function Amis(props: Props) {
const [mobile, setMobile] = useState(false);
const [preview, setPreview] = useState(false);
const [defaultPageConfig] = useState<Schema>(props.defaultPageConfig); // 傳入配置
const defaultSchema: Schema | SchemaObject = defaultPageConfig || {
type: "page",
body: "",
title: "標題",
regions: ["body"],
};
const [schema] = useState(defaultSchema);
let pageJsonObj: Schema = defaultSchema;
const onChange = (value: Schema) => {
pageJsonObj = value;
props.pageChangeHandler && props.pageChangeHandler(value);
};
const onSave = () => {
props.codeGenHandler && props.codeGenHandler(pageJsonObj);
};
return (
<>
{renderAmis({
type: "form",
mode: "inline",
title: "",
body: [
{
type: "switch",
option: "預覽",
name: "preview",
onChange: function (v: any) {
setPreview(v);
},
},
{
type: "switch",
option: "移動端",
name: "mobile",
onChange: function (v: any) {
setMobile(v);
},
},
{
type: "button",
label: "保存",
level: "primary",
onClick: function () {
onSave();
},
},
{
type: "button",
label: "退出",
level: "danger",
onClick: function () {
// if (!window.confirm('確定退出?')) return;
if (props.cancleGenHandler) props.cancleGenHandler();
},
},
],
})}
<Editor
preview={preview}
isMobile={mobile}
onChange={onChange}
value={schema as SchemaObject}
theme={"antd"}
onSave={onSave}
/>
</>
);
}
export default Amis;
在 amis 中提供了兩套組件樣式供我們選擇,分別是 cxd(雲舍)和 antd(仿 Antd),我們可以通過設置Editor
組件中 theme 屬性來進行主題的選擇,同時需要引入對應的組件樣式在以上代碼中,我們對Editor
組件進行了二次封裝,暴露出了defaultPageConfig
(進入編譯器預設頁面 JSON 配置)屬性和codeGenHandler
(代碼生成保存方法),cancleGenHandler
(退出頁面編輯器方法),pageChangeHandler
(頁面改變方法)供外部使用
自定義組件
在 amis-editor 中使用的組件可以是我們的自定義組件.在編寫自定義組件時特別需要主義的是它的 plugin 配置接下來以MyButton
為例來進行自定義組件的介紹
首先來介紹以下組件結構
├─MyButton
│ ├─comp.tsx # 組件本體
│ ├─index.tsx # 整體導出
│ ├─plugin.tsx # 右側panel配置
在comp.tsx
中主要進行組件的開發
import React from "react";
import type { Schema } from "amis/lib/types";
import { Button } from "antd";
const MyButtonRender = React.forwardRef((props: Schema, ref: any) => {
// const props = this.props
return (
<Button
{...props}
ref={ref}
type={props.level || "primary"}
name={props.name}
>
{props.label}
</Button>
);
});
class MyButtonRender2 extends React.Component<any, any> {
handleClick = (nativeEvent: React.MouseEvent<any>) => {
const { dispatchEvent, onClick } = this.props;
// const params = this.getResolvedEventParams();
dispatchEvent(nativeEvent, {});
onClick?.({});
};
handleMouseEnter = (e: React.MouseEvent<any>) => {
const { dispatchEvent } = this.props;
// const params = this.getResolvedEventParams();
dispatchEvent(e, {});
};
handleMouseLeave = (e: React.MouseEvent<any>) => {
const { dispatchEvent } = this.props;
// const params = this.getResolvedEventParams();
dispatchEvent(e, {});
};
render() {
return (
<MyButtonRender
onClick={this.handleClick}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
>
{this.props.label}2
</MyButtonRender>
);
}
}
export default MyButtonRender2;
在上述代碼中MyButtonRender
簡單的對Button
組件進行了簡單的封裝,MyButtonRender2
對 amis 中組件的事件進行了簡單的處理並暴露出去
在plugin.tsx
中主要對MyButtonRender
組件進行渲染器註冊以及對組件的 plugin 進行配置,註冊渲染器是為了將自定義組件拖入中間預覽區域是可以正常的顯示,這一操作與 amis 的工作原理有關(amis 的渲染過程是將 json 轉成對應的 React 組件。先通過 json 的 type 找到對應的 Component,然後把其他屬性作為 props 傳遞過去完成渲染。工作原理
)
在plugin.tsx
中進行 panel 配置
import { Renderer } from "amis";
import MyButtonRender from "./comp";
import type { BaseEventContext } from "amis-editor-core";
import { BasePlugin } from "amis-editor-core";
import { getSchemaTpl } from "amis-editor-core";
import type { RendererPluginAction, RendererPluginEvent } from "amis-editor";
import { getEventControlConfig } from "amis-editor/lib/renderer/event-control/helper";
// 渲染器註冊
Renderer({
type: "my-button",
autoVar: true,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
})(MyButtonRender);
export class MyButton extends BasePlugin {
// 關聯渲染器名字
rendererName = "my-button";
$schema = "/schemas/ActionSchema.json";
order = -400;
// 組件名稱
name = "MyButton";
isBaseComponent = true;
description =
"用來展示一個按鈕,你可以配置不同的展示樣式,配置不同的點擊行為。";
docLink = "/amis/zh-CN/components/button";
tags = ["自定義"];
icon = "fa fa-square";
pluginIcon = "button-plugin";
scaffold = {
type: "my-button",
label: "MyButton",
wrapperBody: true,
};
previewSchema: any = {
type: "my-button",
label: "MyButton",
wrapperBody: true,
};
panelTitle = "MyButton";
// 事件定義
events: RendererPluginEvent[] = [
{
eventName: "click",
eventLabel: "點擊",
description: "點擊時觸發",
defaultShow: true,
dataSchema: [
{
type: "object",
properties: {
nativeEvent: {
type: "object",
title: "滑鼠事件對象",
},
},
},
],
},
{
eventName: "mouseenter",
eventLabel: "滑鼠移入",
description: "滑鼠移入時觸發",
dataSchema: [
{
type: "object",
properties: {
nativeEvent: {
type: "object",
title: "滑鼠事件對象",
},
},
},
],
},
{
eventName: "mouseleave",
eventLabel: "滑鼠移出",
description: "滑鼠移出時觸發",
dataSchema: [
{
type: "object",
properties: {
nativeEvent: {
type: "object",
title: "滑鼠事件對象",
},
},
},
],
},
];
// 動作定義
actions: RendererPluginAction[] = [];
panelJustify = true;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
panelBodyCreator = (context: BaseEventContext) => {
return getSchemaTpl("tabs", [
{
title: "屬性",
body: [
getSchemaTpl("label", {
label: "按鈕名稱",
}),
{
type: "input-text",
label: "欄位名稱",
name: "name",
},
{
type: "select",
label: "按鈕類型",
name: "level",
options: [
{
label: "預設",
value: "primary",
},
{
label: "危險",
value: "danger",
},
{
label: "警告",
value: "warn",
},
{
label: "成功",
value: "success",
},
{
label: "淺色",
value: "default",
},
],
multiple: false,
selectFirst: false,
},
{
type: "input-text",
label: "按鈕圖標",
name: "icon",
// 提示
labelRemark: {
icon: 'icon-close',
trigger: ['hover'],
className: 'Remark--warning',
title: '提示',
content: '圖標請從My Iconfont庫中選擇 部分圖標需要加icon-首碼 如close -> icon-close',
// },
},
],
},
{
title: "樣式",
body: [
getSchemaTpl("buttonLevel", {
label: "高亮樣式",
name: "activeLevel",
visibleOn: "data.active",
}),
getSchemaTpl("switch", {
name: "block",
label: "塊狀顯示",
}),
getSchemaTpl("size", {
label: "尺寸",
}),
],
},
{
title: "事件",
className: "p-none",
body:
!!context.schema.actionType ||
["submit", "reset"].includes(context.schema.type)
? [
getSchemaTpl("eventControl", {
name: "onEvent",
...getEventControlConfig(this.manager, context),
}),
// getOldActionSchema(this.manager, context)
]
: [
getSchemaTpl("eventControl", {
name: "onEvent",
...getEventControlConfig(this.manager, context),
}),
],
},
]);
};
}
當點選某個組件的時候,編輯器內部會觸發麵板構建動作,每個插件都可以通過實現 buildEditorPanel
來插入右側面板。通常右側面板都是表單配置,使用 amis 配置就可以完成。所以推薦的做法是,直接在這個插件上面定義 panelBody
或者 panelBodyCreator
即可。
具體配置可以參考上述代碼,其中需要註意的是getSchemaTpl
這一方法,該方法通過獲取預先通過setSchemaTpl
設置的模板來進行渲染某些元素組件,一下部分源碼可進行參考,tpl 部分全部源碼可參考這裡
export function getSchemaTpl(
name: string,
patch?: object,
rendererSchema?: any
): any {
const tpl = tpls[name] || {};
let schema = null;
if (typeof tpl === "function") {
schema = tpl(patch, rendererSchema);
} else {
schema = patch
? {
...tpl,
...patch,
}
: tpl;
}
return schema;
}
export function setSchemaTpl(name: string, value: any) {
tpls[name] = value;
}
在index.tsx
中主要進行自定義組件插件的註冊以及導出
import { registerEditorPlugin } from "amis-editor";
import { MyButton } from "./plugin";
registerEditorPlugin(MyButton);
其他
在拖拽組件生成頁面時,amis-editor 可選擇的組件有很多,如果我們想使用自己組建的同時忽略隱藏原有組件可以通過disabledRendererPlugin
來對原生組件進行隱藏
import { registerEditorPlugin, BasePlugin } from "amis-editor";
import type {
RendererEventContext,
SubRendererInfo,
BasicSubRenderInfo,
} from "amis-editor";
/**
* 用於隱藏一些不需要的Editor組件
* 備註: 如果不知道當前Editor中有哪些預置組件,可以在這裡設置一個斷點,console.log 看一下 renderers。
*/
// 需要在組件面板中隱藏的組件
const disabledRenderers = [
// 'flex',
"crud2",
"crud2",
"crud2",
// 'crud',
// 'input-text',
"input-email",
"input-password",
"input-url",
// "button",
"reset",
"submit",
"tpl",
"grid",
"container",
// 'flex',
// 'flex',
"collapse-group",
"panel",
"tabs",
// 'form',
"service",
"textarea",
"input-number",
// 'select',
"nested-select",
"chained-select",
"dropdown-button",
"checkboxes",
"radios",
"checkbox",
"input-date",
"input-date-range",
"input-file",
"input-image",
"input-excel",
"input-tree",
"input-tag",
"list-select",
"button-group-select",
"button-toolbar",
"picker",
"switch",
"input-range",
"input-rating",
"input-city",
"transfer",
"tabs-transfer",
"input-color",
"condition-builder",
"fieldset",
"combo",
"input-group",
"input-table",
"matrix-checkboxes",
"input-rich-text",
"diff-editor",
"editor",
"search-box",
"input-kv",
"input-repeat",
"uuid",
"location-picker",
"input-sub-form",
"hidden",
"button-group",
"nav",
"anchor-nav",
"tooltip-wrapper",
"alert",
"wizard",
"table-view",
"web-component",
"audio",
"video",
"custom",
"tasks",
"each",
"property",
"iframe",
"qrcode",
"icon",
"link",
"list",
"mapping",
"avatar",
"card",
"card2",
"cards",
"table",
"table2",
"chart",
"sparkline",
"carousel",
"image",
"images",
"date",
"time",
"datetime",
"tag",
"json",
"progress",
"status",
"steps",
"timeline",
"divider",
"code",
"markdown",
"collapse",
"log",
"input-array",
"control",
"input-datetime",
"input-datetime-range",
"formula",
"group",
"input-month",
"input-month-range",
"input-quarter",
"input-quarter-range",
"static",
"input-time",
"input-time-range",
"tree-select",
"input-year",
"input-year-range",
"breadcrumb",
"custom",
"hbox",
"page",
"pagination",
"plain",
"wrapper",
"column-toggler",
];
export class ManagerEditorPlugin extends BasePlugin {
order = 9999;
buildSubRenderers(
context: RendererEventContext,
renderers: SubRendererInfo[]
): BasicSubRenderInfo | BasicSubRenderInfo[] | void {
// 更新NPM自定義組件排序和分類
// console.log(renderers);
for (let index = 0, size = renderers.length; index < size; index++) {
// 判斷是否需要隱藏 Editor預置組件
const pluginRendererName = renderers[index].rendererName;
if (
pluginRendererName &&
disabledRenderers.indexOf(pluginRendererName) > -1
) {
renderers[index].disabledRendererPlugin = true; // 更新狀態
}
}
}
}
registerEditorPlugin(ManagerEditorPlugin);
寫在最後
一個階段的結束伴隨著另一個階段的開始,在新的階段中會繼續學習繼續進步