• Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • React 通关秘籍

    • 1.关于本小册
    • 2.一网打尽组件常用Hook
    • 3.Hook的闭包陷阱的成因和解决方案
    • 4.React组件如何写TypeScript类型
    • 5.React组件如何调试
    • 6.受控模式VS非受控模式
    • 7.组件实战:迷你Calendar
    • 8.组件实战:Calendar日历组件(上)
    • 9.组件实战:Calendar日历组件(下)
    • 10.快速掌握Storybook
    • 11.React组件如何写单测
    • 12.深入理解Suspense和ErrorBoundary
    • 13.组件实战:Icon图标组件
    • 14.组件实战:Space间距组件
    • 15.React.Children和它的两种替代方案
    • 16.三个简单组件的封装
    • 17.浏览器的5种Observer
    • 18.组件实战:Watermark防删除水印组件
    • 19.手写react-lazyload
    • 20.图解网页的各种距离
    • 21.自定义hook练习
    • 22.自定义hook练习(二)
    • 23.用react-spring做弹簧动画
    • 24.react-spring结合use-gesture手势库实现交互动画
    • 25.用react-transition-group和react-spring做过渡动画
    • 26.快速掌握Tailwind:最流行的原子化CSS框架
    • 27.用CSSModules避免样式冲突
    • 28.CSSInJS:快速掌握styled-components
    • 29.react-spring实现滑入滑出的转场动画
    • 30.组件实战:Message全局提示组件
    • 31.组件实战:Popover气泡卡片组件
    • 32.项目里如何快速定位组件源码
    • 33.一次超爽的React调试体验
    • 34.组件实战:ColorPicker颜色选择器(一)
    • 35.组件实战:ColorPicker颜色选择器(二)
    • 36.组件实战:onBoarding漫游式引导组件
    • 37.组件实战:Upload拖拽上传
    • 38.组件实战:Form表单组件
    • 39.React组件库都是怎么构建的
    • 40.组件库实战:构建esm和cjs产物,发布到npm
    • 41.组件库实战:构建umd产物,通过unpkg访问
    • 42.数据不可变:immutable和immer
    • 43.基于ReactRouter实现keepalive
    • 44.Historyapi和ReactRouter实现原理
    • 45.ReactContext的实现原理和在antd里的应用
    • 46.ReactContext的性能缺点和解决方案
    • 47.手写一个Zustand
    • 48.原子化状态管理库Jotai
    • 49.用react-intl实现国际化
    • 50.国际化资源包如何通过Excel和GoogleSheet分享给产品经理
    • 51.基于react-dnd实现拖拽排序
    • 52.react-dnd实战:拖拽版TodoList
    • 53.ReactPlayground项目实战:需求分析、实现原理
    • 54.ReactPlayground项目实战:布局、代码编辑器
    • 55.ReactPlayground项目实战:多文件切换
    • 56.ReactPlayground项目实战:babel编译、iframe预览
    • 57.ReactPlayground项目实战:文件增删改
    • 58.ReactPlayground项目实战:错误显示、主题切换
    • 59.ReactPlayground项目实战:链接分享、代码下载
    • 60.ReactPlayground项目实战:WebWorker性能优化
    • 61.ReactPlayground项目实战:总结
    • 62.手写MiniReact:思路分析
    • 63.手写MiniReact:代码实现
    • 64.手写MiniReact:和真实React源码的对比
    • 65.React18的并发机制是怎么实现的
    • 66.Ref的实现原理
    • 67.低代码编辑器:核心数据结构、全局store
    • 68.低代码编辑器:拖拽组件到画布、拖拽编辑json
    • 69.低代码编辑器:画布区hover展示高亮框
    • 70.低代码编辑器:画布区click展示编辑框
    • 71.低代码编辑器:组件属性、样式编辑
    • 72.低代码编辑器:预览、大纲
    • 73.低代码编辑器:事件绑定
    • 74.低代码编辑器:动作弹窗
    • 75.低代码编辑器:自定义JS
    • 76.低代码编辑器:组件联动
    • 77.低代码编辑器:拖拽优化、Table组件
    • 78.低代码编辑器:Form组件、store持久化
    • 79.低代码编辑器:项目总结
    • 80.快速掌握ReactFlow画流程图
    • 81.ReactFlow振荡器调音:项目介绍
    • 82.ReactFlow振荡器调音:流程图绘制
    • 83.ReactFlow振荡器调音:合成声音
    • 84.AudioContext实现在线钢琴
    • 85.React服务端渲染:从SSR到hydrate
    • 86.小册总结

前面实现了内置的几个动作,这节来实现下自定义 JS。

比如 amis:

它就支持通过代码来自定义动作。

而且自定义 JS 可以拿到 doAction 方法来执行其他动作:

可以通过 context 拿到组件信息。

我们也来实现下。

创建 Setting/actions/CustomJS.tsx

import { useState } from "react";
import { useComponetsStore } from "../../../stores/components";
import MonacoEditor, { OnMount } from "@monaco-editor/react";

export interface CustomJSConfig {
    type: "customJS";
    code: string;
}

export interface CustomJSProps {
    defaultValue?: string;
    onChange?: (config: CustomJSConfig) => void;
}

export function CustomJS(props: CustomJSProps) {
    const { defaultValue, onChange } = props;

    const { curComponentId } = useComponetsStore();
    const [value, setValue] = useState(defaultValue);

    function codeChange(value?: string) {
        if (!curComponentId) return;

        setValue(value);

        onChange?.({
            type: "customJS",
            code: value!,
        });
    }

    const handleEditorMount: OnMount = (editor, monaco) => {
        editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyJ, () => {
            editor.getAction("editor.action.formatDocument")?.run();
        });
    };

    return (
        <div className="mt-[40px]">
            <div className="flex items-start gap-[20px]">
                <div>自定义 JS</div>
                <div>
                    <MonacoEditor
                        width={"600px"}
                        height={"400px"}
                        path="action.js"
                        language="javascript"
                        onMount={handleEditorMount}
                        onChange={codeChange}
                        value={value}
                        options={{
                            fontSize: 14,
                            scrollBeyondLastLine: false,
                            minimap: {
                                enabled: false,
                            },
                            scrollbar: {
                                verticalScrollbarSize: 6,
                                horizontalScrollbarSize: 6,
                            },
                        }}
                    />
                </div>
            </div>
        </div>
    );
}

和其他动作表单不同的是这里用 monaco editor。

然后在 ActionModal 里用一下:

切换自定义 JS 的 tab 时,渲染 CustomJS 组件。

顺便把类型也改一下,加上 CustomJSConfig 的类型

import { Modal, Segmented } from "antd";
import { useState } from "react";
import { GoToLink, GoToLinkConfig } from "./actions/GoToLink";
import { ShowMessage, ShowMessageConfig } from "./actions/ShowMessage";
import { CustomJS, CustomJSConfig } from "./actions/CustomJS";

export interface ActionModalProps {
    visible: boolean;
    handleOk: (config?: ActionConfig) => void;
    handleCancel: () => void;
}

export type ActionConfig = GoToLinkConfig | ShowMessageConfig | CustomJSConfig;

export function ActionModal(props: ActionModalProps) {
    const { visible, handleOk, handleCancel } = props;

    const [key, setKey] = useState<string>("访问链接");
    const [curConfig, setCurConfig] = useState<ActionConfig>();

    return (
        <Modal
            title="事件动作配置"
            width={800}
            open={visible}
            okText="确认"
            cancelText="取消"
            onOk={() => handleOk(curConfig)}
            onCancel={handleCancel}>
            <div className="h-[500px]">
                <Segmented
                    value={key}
                    onChange={setKey}
                    block
                    options={["访问链接", "消息提示", "自定义 JS"]}
                />
                {key === "访问链接" && (
                    <GoToLink
                        onChange={(config) => {
                            setCurConfig(config);
                        }}
                    />
                )}
                {key === "消息提示" && (
                    <ShowMessage
                        onChange={(config) => {
                            setCurConfig(config);
                        }}
                    />
                )}
                {key === "自定义 JS" && (
                    <CustomJS
                        onChange={(config) => {
                            setCurConfig(config);
                        }}
                    />
                )}
            </div>
        </Modal>
    );
}

ComponentEvent 里渲染的时候也支持 customJS,并改下 ts 类型:

import { Collapse, Input, Select, CollapseProps, Button } from "antd";
import { useComponetsStore } from "../../stores/components";
import { useComponentConfigStore } from "../../stores/component-config";
import type { ComponentEvent } from "../../stores/component-config";
import { ActionConfig, ActionModal } from "./ActionModal";
import { useState } from "react";
import { DeleteOutlined } from "@ant-design/icons";

export function ComponentEvent() {
    const { curComponentId, curComponent, updateComponentProps } =
        useComponetsStore();
    const { componentConfig } = useComponentConfigStore();
    const [actionModalOpen, setActionModalOpen] = useState(false);
    const [curEvent, setCurEvent] = useState<ComponentEvent>();

    if (!curComponent) return null;

    function deleteAction(event: ComponentEvent, index: number) {
        if (!curComponent) {
            return;
        }

        const actions = curComponent.props[event.name]?.actions;

        actions.splice(index, 1);

        updateComponentProps(curComponent.id, {
            [event.name]: {
                actions: actions,
            },
        });
    }

    const items: CollapseProps["items"] = (
        componentConfig[curComponent.name].events || []
    ).map((event) => {
        return {
            key: event.name,
            label: (
                <div className="flex justify-between leading-[30px]">
                    {event.label}
                    <Button
                        type="primary"
                        onClick={(e) => {
                            e.stopPropagation();

                            setCurEvent(event);
                            setActionModalOpen(true);
                        }}>
                        添加动作
                    </Button>
                </div>
            ),
            children: (
                <div>
                    {(curComponent.props[event.name]?.actions || []).map(
                        (item: ActionConfig, index: number) => {
                            return (
                                <div>
                                    {item.type === "goToLink" ? (
                                        <div
                                            key="goToLink"
                                            className="border border-[#aaa] m-[10px] p-[10px] relative">
                                            <div className="text-[blue]">
                                                跳转链接
                                            </div>
                                            <div>{item.url}</div>
                                            <div
                                                style={{
                                                    position: "absolute",
                                                    top: 10,
                                                    right: 10,
                                                    cursor: "pointer",
                                                }}
                                                onClick={() =>
                                                    deleteAction(event, index)
                                                }>
                                                <DeleteOutlined />
                                            </div>
                                        </div>
                                    ) : null}
                                    {item.type === "showMessage" ? (
                                        <div
                                            key="showMessage"
                                            className="border border-[#aaa] m-[10px] p-[10px] relative">
                                            <div className="text-[blue]">
                                                消息弹窗
                                            </div>
                                            <div>{item.config.type}</div>
                                            <div>{item.config.text}</div>
                                            <div
                                                style={{
                                                    position: "absolute",
                                                    top: 10,
                                                    right: 10,
                                                    cursor: "pointer",
                                                }}
                                                onClick={() =>
                                                    deleteAction(event, index)
                                                }>
                                                <DeleteOutlined />
                                            </div>
                                        </div>
                                    ) : null}
                                    {item.type === "customJS" ? (
                                        <div
                                            key="customJS"
                                            className="border border-[#aaa] m-[10px] p-[10px] relative">
                                            <div className="text-[blue]">
                                                自定义 JS
                                            </div>
                                            <div
                                                style={{
                                                    position: "absolute",
                                                    top: 10,
                                                    right: 10,
                                                    cursor: "pointer",
                                                }}
                                                onClick={() =>
                                                    deleteAction(event, index)
                                                }>
                                                <DeleteOutlined />
                                            </div>
                                        </div>
                                    ) : null}
                                </div>
                            );
                        }
                    )}
                </div>
            ),
        };
    });

    function handleModalOk(config?: ActionConfig) {
        if (!config || !curEvent || !curComponent) {
            return;
        }

        updateComponentProps(curComponent.id, {
            [curEvent.name]: {
                actions: [
                    ...(curComponent.props[curEvent.name]?.actions || []),
                    config,
                ],
            },
        });

        setActionModalOpen(false);
    }

    return (
        <div className="px-[10px]">
            <Collapse
                className="mb-[10px]"
                items={items}
                defaultActiveKey={componentConfig[
                    curComponent.name
                ].events?.map((item) => item.name)}
            />
            <ActionModal
                visible={actionModalOpen}
                handleOk={handleModalOk}
                handleCancel={() => {
                    setActionModalOpen(false);
                }}
            />
        </div>
    );
}

测试下:

动作添加成功。

在 json 里可以看到这个配置:

接下来只要 Preview 的时候实现这种 action 的执行就好了。

支持 customJS 的 action 执行,顺便改下类型。

props[event.name] = () => {
    eventConfig?.actions?.forEach((action: ActionConfig) => {
        if (action.type === "goToLink") {
            window.location.href = action.url;
        } else if (action.type === "showMessage") {
            if (action.config.type === "success") {
                message.success(action.config.text);
            } else if (action.config.type === "error") {
                message.error(action.config.text);
            }
        } else if (action.type === "customJS") {
            const func = new Function(action.code);
            func();
        }
    });
};

测试下:

这样就实现了自定义 JS 的执行。

然后给执行的函数加上一些参数:

new Function 可以传入任意个参数,最后一个是函数体,前面都会作为函数参数的名字。

然后调用的时候传入参数。

我们这里只传入了当前组件的 name、props 还有一个方法。

const func = new Function("context", action.code);
func({
    name: component.name,
    props: component.props,
    showMessage(content: string) {
        message.success(content);
    },
});

测试下:

这样,自定义 JS 的功能就完成了。

但现在有个问题:

我们上节做了动作的新增、删除,并没有做编辑。

这对于跳转链接、消息弹窗这种动作还好,参数比较简单。

但是对于自定义 JS,写一段 JS 成本还是挺高的,删了再重写体验不好,所以我们得支持下编辑。

改下 ComponentEvent 组件:

<div
    style={{ position: "absolute", top: 10, right: 30, cursor: "pointer" }}
    onClick={() => editAction(item)}>
    <EditOutlined />
</div>

加一个绝对定位的 icon。

点击的时候打开弹窗:

function editAction(config: ActionConfig) {
    if (!curComponent) {
        return;
    }

    setActionModalOpen(true);
}

测试下:

能打开弹窗,但是还没回显内容。

在 ActionModal 传入 action 来回显:

import { Modal, Segmented } from "antd";
import { useEffect, useState } from "react";
import { GoToLink, GoToLinkConfig } from "./actions/GoToLink";
import { ShowMessage, ShowMessageConfig } from "./actions/ShowMessage";
import { CustomJS, CustomJSConfig } from "./actions/CustomJS";

export type ActionConfig = GoToLinkConfig | ShowMessageConfig | CustomJSConfig;

export interface ActionModalProps {
    visible: boolean;
    action?: ActionConfig;
    handleOk: (config?: ActionConfig) => void;
    handleCancel: () => void;
}

export function ActionModal(props: ActionModalProps) {
    const { visible, action, handleOk, handleCancel } = props;

    const map = {
        goToLink: "访问链接",
        showMessage: "消息提示",
        customJS: "自定义 JS",
    };

    const [key, setKey] = useState<string>("访问链接");
    const [curConfig, setCurConfig] = useState<ActionConfig>();

    useEffect(() => {
        if (action?.type) {
            setKey(map[action.type]);
        }
    }, [action]);

    return (
        <Modal
            title="事件动作配置"
            width={800}
            open={visible}
            okText="确认"
            cancelText="取消"
            onOk={() => handleOk(curConfig)}
            onCancel={handleCancel}>
            <div className="h-[500px]">
                <Segmented
                    value={key}
                    onChange={setKey}
                    block
                    options={["访问链接", "消息提示", "自定义 JS"]}
                />
                {key === "访问链接" && (
                    <GoToLink
                        key="goToLink"
                        defaultValue={
                            action?.type === "goToLink" ? action.url : ""
                        }
                        onChange={(config) => {
                            setCurConfig(config);
                        }}
                    />
                )}
                {key === "消息提示" && (
                    <ShowMessage
                        key="showMessage"
                        value={
                            action?.type === "showMessage"
                                ? action.config
                                : undefined
                        }
                        onChange={(config) => {
                            setCurConfig(config);
                        }}
                    />
                )}
                {key === "自定义 JS" && (
                    <CustomJS
                        key="customJS"
                        defaultValue={
                            action?.type === "customJS" ? action.code : ""
                        }
                        onChange={(config) => {
                            setCurConfig(config);
                        }}
                    />
                )}
            </div>
        </Modal>
    );
}

然后在 ComponentEvent 里传入这个参数:

const [curAction, setCurAction] = useState<ActionConfig>();

测试下:

这样,回显就完成了。

然后保存的时候也要处理下:

记录下当前编辑的 action 的 index。

保存的时候如果有 curAction,就是修改,没有的话才是新增。

import { Collapse, Input, Select, CollapseProps, Button } from "antd";
import { useComponetsStore } from "../../stores/components";
import { useComponentConfigStore } from "../../stores/component-config";
import type { ComponentEvent } from "../../stores/component-config";
import { ActionConfig, ActionModal } from "./ActionModal";
import { useState } from "react";
import { DeleteOutlined, EditOutlined } from "@ant-design/icons";

export function ComponentEvent() {
    const { curComponentId, curComponent, updateComponentProps } =
        useComponetsStore();
    const { componentConfig } = useComponentConfigStore();
    const [actionModalOpen, setActionModalOpen] = useState(false);
    const [curEvent, setCurEvent] = useState<ComponentEvent>();
    const [curAction, setCurAction] = useState<ActionConfig>();
    const [curActionIndex, setCurActionIndex] = useState<number>();

    if (!curComponent) return null;

    function deleteAction(event: ComponentEvent, index: number) {
        if (!curComponent) {
            return;
        }

        const actions = curComponent.props[event.name]?.actions;

        actions.splice(index, 1);

        updateComponentProps(curComponent.id, {
            [event.name]: {
                actions: actions,
            },
        });
    }

    function editAction(config: ActionConfig, index: number) {
        if (!curComponent) {
            return;
        }
        setCurAction(config);
        setCurActionIndex(index);

        setActionModalOpen(true);
    }

    const items: CollapseProps["items"] = (
        componentConfig[curComponent.name].events || []
    ).map((event) => {
        return {
            key: event.name,
            label: (
                <div className="flex justify-between leading-[30px]">
                    {event.label}
                    <Button
                        type="primary"
                        onClick={(e) => {
                            e.stopPropagation();

                            setCurEvent(event);
                            setActionModalOpen(true);
                        }}>
                        添加动作
                    </Button>
                </div>
            ),
            children: (
                <div>
                    {(curComponent.props[event.name]?.actions || []).map(
                        (item: ActionConfig, index: number) => {
                            return (
                                <div>
                                    {item.type === "goToLink" ? (
                                        <div
                                            key="goToLink"
                                            className="border border-[#aaa] m-[10px] p-[10px] relative">
                                            <div className="text-[blue]">
                                                跳转链接
                                            </div>
                                            <div>{item.url}</div>
                                            <div
                                                style={{
                                                    position: "absolute",
                                                    top: 10,
                                                    right: 30,
                                                    cursor: "pointer",
                                                }}
                                                onClick={() =>
                                                    editAction(item, index)
                                                }>
                                                <EditOutlined />
                                            </div>
                                            <div
                                                style={{
                                                    position: "absolute",
                                                    top: 10,
                                                    right: 10,
                                                    cursor: "pointer",
                                                }}
                                                onClick={() =>
                                                    deleteAction(event, index)
                                                }>
                                                <DeleteOutlined />
                                            </div>
                                        </div>
                                    ) : null}
                                    {item.type === "showMessage" ? (
                                        <div
                                            key="showMessage"
                                            className="border border-[#aaa] m-[10px] p-[10px] relative">
                                            <div className="text-[blue]">
                                                消息弹窗
                                            </div>
                                            <div>{item.config.type}</div>
                                            <div>{item.config.text}</div>
                                            <div
                                                style={{
                                                    position: "absolute",
                                                    top: 10,
                                                    right: 30,
                                                    cursor: "pointer",
                                                }}
                                                onClick={() =>
                                                    editAction(item, index)
                                                }>
                                                <EditOutlined />
                                            </div>
                                            <div
                                                style={{
                                                    position: "absolute",
                                                    top: 10,
                                                    right: 10,
                                                    cursor: "pointer",
                                                }}
                                                onClick={() =>
                                                    deleteAction(event, index)
                                                }>
                                                <DeleteOutlined />
                                            </div>
                                        </div>
                                    ) : null}
                                    {item.type === "customJS" ? (
                                        <div
                                            key="customJS"
                                            className="border border-[#aaa] m-[10px] p-[10px] relative">
                                            <div className="text-[blue]">
                                                自定义 JS
                                            </div>
                                            <div
                                                style={{
                                                    position: "absolute",
                                                    top: 10,
                                                    right: 30,
                                                    cursor: "pointer",
                                                }}
                                                onClick={() =>
                                                    editAction(item, index)
                                                }>
                                                <EditOutlined />
                                            </div>
                                            <div
                                                style={{
                                                    position: "absolute",
                                                    top: 10,
                                                    right: 10,
                                                    cursor: "pointer",
                                                }}
                                                onClick={() =>
                                                    deleteAction(event, index)
                                                }>
                                                <DeleteOutlined />
                                            </div>
                                        </div>
                                    ) : null}
                                </div>
                            );
                        }
                    )}
                </div>
            ),
        };
    });

    function handleModalOk(config?: ActionConfig) {
        if (!config || !curEvent || !curComponent) {
            return;
        }

        if (curAction) {
            updateComponentProps(curComponent.id, {
                [curEvent.name]: {
                    actions: curComponent.props[curEvent.name]?.actions.map(
                        (item: ActionConfig, index: number) => {
                            return index === curActionIndex ? config : item;
                        }
                    ),
                },
            });
        } else {
            updateComponentProps(curComponent.id, {
                [curEvent.name]: {
                    actions: [
                        ...(curComponent.props[curEvent.name]?.actions || []),
                        config,
                    ],
                },
            });
        }

        setCurAction(undefined);

        setActionModalOpen(false);
    }

    return (
        <div className="px-[10px]">
            <Collapse
                className="mb-[10px]"
                items={items}
                defaultActiveKey={componentConfig[
                    curComponent.name
                ].events?.map((item) => item.name)}
            />
            <ActionModal
                visible={actionModalOpen}
                handleOk={handleModalOk}
                action={curAction}
                handleCancel={() => {
                    setCurAction(undefined);
                    setActionModalOpen(false);
                }}
            />
        </div>
    );
}

测试下:

action 的新增和修改正常。

这时候我发现虽然最终保存的是对的,回显的不对:

如上图,我修改下面的 action 的时候,回显的依然是之前的值,但保存是对的。

这是为什么呢?我们不是传了参数了么:

因为我们是用非受控模式写的,传的参数作为表单的默认值:

所以修改 defaultValue 并不会修改表单值。

有回显需求的表单,必须用受控模式来写。

我们改一下:

当传入 value 参数的时候,同步设置内部的 value

测试下:

这样就好了。

案例代码上传了小册仓库,可以切换到这个 commit 查看:

git reset --hard 29562eb568bdc05e4efbdd02ba4f817f47201279

总结

这节我们实现了自定义 JS。

通过 monaco editor 来输入代码,然后通过 new Function 来动态执行代码,执行的代码可以访问 context,传入一些属性方法。

然后我们实现了动作的编辑,点击编辑按钮会在弹窗回显 action,保存之后会修改 json。

主要回显的表单一定是受控模式,这样才可以随时 value,不然只能设置初始值 defaultValue

这样,内置动作、自定义 JS 的动作就都完成了。

上次更新: 6/21/25, 9:42 AM
贡献者: YNight
Prev
74.低代码编辑器:动作弹窗
Next
76.低代码编辑器:组件联动