• 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.小册总结

这节我们来实现下事件绑定。

现在看下 amis 里事件绑定的流程:

选中组件,在事件面板会列出可以绑定的事件。

选中某个事件之后,可以添加动作:

你可以添加自定义执行的 JS 代码。

或者执行一些内置的动作,比如跳转链接。

还可以调用别的组件的方法,比如修改某个组件的显示隐藏:

这节我们就实现下。

首先,不同组件可绑定的事件是不同的:

这明显也是需要配置的。

我们在 componentConfig 里加上这个配置:

export interface ComponentEvent {
    name: string;
    label: string;
}

export interface ComponentConfig {
    name: string;
    defaultProps: Record<string, any>;
    desc: string;
    setter?: ComponentSetter[];
    stylesSetter?: ComponentSetter[];
    events?: ComponentEvent[];
    dev: any;
    prod: any;
}

然后给 Button 组件配置一下:

events: [
    {
        name: 'onClick',
        label: '点击事件',
    },
    {
        name: 'onDoubleClick',
        label: '双击事件'
    },
],

改下 Setting/ComponentEvent.tsx 组件,把事件渲染出来:

import { Collapse, Input, Select, CollapseProps } from "antd";
import { useComponetsStore } from "../../stores/components";
import { useComponentConfigStore } from "../../stores/component-config";

export function ComponentEvent() {
    const { curComponentId, curComponent, updateComponentProps } =
        useComponetsStore();
    const { componentConfig } = useComponentConfigStore();

    if (!curComponent) return null;

    const items: CollapseProps["items"] = (
        componentConfig[curComponent.name].events || []
    ).map((event) => {
        return {
            key: event.name,
            label: event.label,
            children: (
                <div>
                    <div className="flex items-center">
                        <div>动作:</div>
                        <Select
                            className="w-[160px]"
                            options={[
                                { label: "显示提示", value: "showMessage" },
                                { label: "跳转链接", value: "goToLink" },
                            ]}
                            value={curComponent?.props?.[event.name]?.type}
                        />
                    </div>
                </div>
            ),
        };
    });

    return (
        <div className="px-[10px]">
            <Collapse className="mb-[10px]" items={items} />
        </div>
    );
}

根据 curComponent 从 componentConfig 取出对应组件的 events 配置。

用 antd 的 Collapse 组件渲染。

这样选中按钮组件的时候,就会渲染出它可以绑定的事件。

内置了两个动作:显示提示、跳转链接

当选择某个动作的时候,我们把它保存到 store 里。

比如 onClick 选择了 gotoLink 的动作,那就会在 component.props 上添加这样一个属性:

onClick: {
    type: "gotoLink";
}

onChange={(value) => { selectAction(event.name, value) }}
function selectAction(eventName: string, value: string) {
    if (!curComponentId) return;

    updateComponentProps(curComponentId, { [eventName]: { type: value } });
}

然后当切换到不同 action 的时候,显示对应的表单:

import { Collapse, Input, Select, CollapseProps } from "antd";
import { useComponetsStore } from "../../stores/components";
import { useComponentConfigStore } from "../../stores/component-config";

export function ComponentEvent() {
    const { curComponentId, curComponent, updateComponentProps } =
        useComponetsStore();
    const { componentConfig } = useComponentConfigStore();

    if (!curComponent) return null;

    function selectAction(eventName: string, value: string) {
        if (!curComponentId) return;

        updateComponentProps(curComponentId, { [eventName]: { type: value } });
    }

    function urlChange(eventName: string, value: string) {
        if (!curComponentId) return;

        updateComponentProps(curComponentId, {
            [eventName]: {
                ...curComponent?.props?.[eventName],
                url: value,
            },
        });
    }

    const items: CollapseProps["items"] = (
        componentConfig[curComponent.name].events || []
    ).map((event) => {
        return {
            key: event.name,
            label: event.label,
            children: (
                <div>
                    <div className="flex items-center">
                        <div>动作:</div>
                        <Select
                            className="w-[160px]"
                            options={[
                                { label: "显示提示", value: "showMessage" },
                                { label: "跳转链接", value: "goToLink" },
                            ]}
                            onChange={(value) => {
                                selectAction(event.name, value);
                            }}
                            value={curComponent?.props?.[event.name]?.type}
                        />
                    </div>
                    {curComponent?.props?.[event.name]?.type === "goToLink" && (
                        <div className="mt-[10px]">
                            <div className="flex items-center gap-[10px]">
                                <div>链接</div>
                                <div>
                                    <Input
                                        onChange={(e) => {
                                            urlChange(
                                                event.name,
                                                e.target.value
                                            );
                                        }}
                                        value={
                                            curComponent?.props?.[event.name]
                                                ?.url
                                        }
                                    />
                                </div>
                            </div>
                        </div>
                    )}
                </div>
            ),
        };
    });

    return (
        <div className="px-[10px]">
            <Collapse className="mb-[10px]" items={items} />
        </div>
    );
}

测试下:

当切换动作为跳转链接的时候,就会显示 url 的输入框。

输入 url 后,可以在 json 里看到这个信息:

那渲染的时候根据这个绑定 click 事件就好了。

改下 Preview 组件:

根据 componentConfig 里的事件类型给组件绑定事件。

如果有 components.props 里如果有 goToLink 的配置,就跳转链接。

import React from "react";
import { useComponentConfigStore } from "../../stores/component-config";
import { Component, useComponetsStore } from "../../stores/components";

export function Preview() {
    const { components } = useComponetsStore();
    const { componentConfig } = useComponentConfigStore();

    function handleEvent(component: Component) {
        const props: Record<string, any> = {};

        componentConfig[component.name].events?.forEach((event) => {
            const eventConfig = component.props[event.name];

            if (eventConfig) {
                const { type } = eventConfig;

                props[event.name] = () => {
                    if (type === "goToLink" && eventConfig.url) {
                        window.location.href = eventConfig.url;
                    }
                };
            }
        });
        return props;
    }

    function renderComponents(components: Component[]): React.ReactNode {
        return components.map((component: Component) => {
            const config = componentConfig?.[component.name];

            if (!config?.prod) {
                return null;
            }

            return React.createElement(
                config.prod,
                {
                    key: component.id,
                    id: component.id,
                    name: component.name,
                    styles: component.styles,
                    ...config.defaultProps,
                    ...component.props,
                    ...handleEvent(component),
                },
                renderComponents(component.children || [])
            );
        });
    }

    return <div>{renderComponents(components)}</div>;
}

然后组件里接收这个参数:

测试下:

这样,我们第一个动作就完成了。

对比下 amis 里的实现:

没跳转是因为 amis 在预览模式下禁止了跳转:

虽然交互有点区别,但流程是一样的。

看下 amis 的 json:

也是把动作信息记录在 json 里,渲染的时候用这些来绑定事件。

动作后面会越来越多,所以最好抽成组件:

新建 Setting/actions/GoToLink.tsx

import { Input } from "antd";
import { ComponentEvent } from "../../../stores/component-config";
import { useComponetsStore } from "../../../stores/components";

export function GoToLink(props: { event: ComponentEvent }) {
    const { event } = props;

    const { curComponentId, curComponent, updateComponentProps } =
        useComponetsStore();

    function urlChange(eventName: string, value: string) {
        if (!curComponentId) return;

        updateComponentProps(curComponentId, {
            [eventName]: {
                ...curComponent?.props?.[eventName],
                url: value,
            },
        });
    }

    return (
        <div className="mt-[10px]">
            <div className="flex items-center gap-[10px]">
                <div>链接</div>
                <div>
                    <Input
                        onChange={(e) => {
                            urlChange(event.name, e.target.value);
                        }}
                        value={curComponent?.props?.[event.name]?.url}
                    />
                </div>
            </div>
        </div>
    );
}

把跳转链接的表单抽离到这里:

然后我们再实现一个动作:

Setting/actions/ShowMessage.tsx

import { Input, Select } from "antd";
import { ComponentEvent } from "../../../stores/component-config";
import { useComponetsStore } from "../../../stores/components";

export function ShowMessage(props: { event: ComponentEvent }) {
    const { event } = props;

    const { curComponentId, curComponent, updateComponentProps } =
        useComponetsStore();

    function messageTypeChange(eventName: string, value: string) {
        if (!curComponentId) return;

        updateComponentProps(curComponentId, {
            [eventName]: {
                ...curComponent?.props?.[eventName],
                config: {
                    ...curComponent?.props?.[eventName]?.config,
                    type: value,
                },
            },
        });
    }

    function messageTextChange(eventName: string, value: string) {
        if (!curComponentId) return;

        updateComponentProps(curComponentId, {
            [eventName]: {
                ...curComponent?.props?.[eventName],
                config: {
                    ...curComponent?.props?.[eventName]?.config,
                    text: value,
                },
            },
        });
    }

    return (
        <div className="mt-[10px]">
            <div className="flex items-center gap-[10px]">
                <div>类型:</div>
                <div>
                    <Select
                        style={{ width: 160 }}
                        options={[
                            { label: "成功", value: "success" },
                            { label: "失败", value: "error" },
                        ]}
                        onChange={(value) => {
                            messageTypeChange(event.name, value);
                        }}
                        value={curComponent?.props?.[event.name]?.config?.type}
                    />
                </div>
            </div>
            <div className="flex items-center gap-[10px] mt-[10px]">
                <div>文本:</div>
                <div>
                    <Input
                        onChange={(e) => {
                            messageTextChange(event.name, e.target.value);
                        }}
                        value={curComponent?.props?.[event.name]?.config?.text}
                    />
                </div>
            </div>
        </div>
    );
}

和 GoToLink 差不多,只不过现在多了一个 Select 表单。

用一下:

{
    curComponent?.props?.[event.name]?.type === "showMessage" && (
        <ShowMessage event={event} />
    );
}

渲染的时候做下处理:

props[event.name] = () => {
    if (type === "goToLink" && eventConfig.url) {
        window.location.href = eventConfig.url;
    } else if (type === "showMessage" && eventConfig.config) {
        if (eventConfig.config.type === "success") {
            message.success(eventConfig.config.text);
        } else if (eventConfig.config.type === "error") {
            message.error(eventConfig.config.text);
        }
    }
};

试一下效果:

这样我们就实现了 showMessage 的动作:

试下 amis 里的:

一样。

当然,amis 里是支持绑定多个动作的:

它的 actions 是个数组:

我们目前只支持绑定一个 action。

这个也很简单,就是把存储结构改为数组,然后界面支持添加多个动作,大家可以自己完善。

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

git reset --hard 4fd81d180f8369efb4142876944b0c70a6f4cd6c

总结

这节我们实现了事件绑定。

我们先实现了内置动作的方式。

在 comonentConfig 里配置组件可以绑定的事件,然后在 Setting 区事件面板里展示。

可以选择绑定的动作,比如跳转链接,显示提示,输入一些参数之后,就会保存到 json 里。

然后渲染 Preview 的时候根据这些信息来绑定事件。

我们对比了下和 amis 的区别,内置动作这些的实现一样的。

当然,事件绑定还有别的方式,下节我们继续完善。

上次更新: 6/21/25, 9:42 AM
贡献者: YNight
Prev
72.低代码编辑器:预览、大纲
Next
74.低代码编辑器:动作弹窗