在 react 中单独使用 kityformula-editor

准备(使用kityformula-editor 必要的文件)

kityformula-editor

官网:fex-team.github.io/kityformula... (没什么参考价值,可以参考这个

仓库地址:github.com/fex-team/kf...

  1. s: 文件 /lib 中的 所有文件;

注意:需要额外添加 jquery.js

  1. 静态资源:css、img等

demo 放在下面了,可以参考

字体文件:

resource.zip

KityFormula-react-demo.zip

在 react 中引入 kityformula

在项目的根目录下,创建public/kityformula ,将上述的资源文件放进去

使用

动态引入资源 js 文件,注意引入顺序

  • 注意,一定引入 jQuery 库

kitygraph -> kity-formula-render -> kity-formula-parser -> kityformula-editor

创建工具函数,用于按照顺序动态引入 文件

挂载编辑器 将 创建 编辑器所需的方法 挂载与 window中

如: kity、kf

javascript 复制代码
// 等待条件成立的工具函数
const waitFor = (conditionFn, interval = 50) =>
  new Promise((resolve) => {
    const timer = setInterval(() => {
      if (conditionFn()) {
        clearInterval(timer);
        resolve();
      }
    }, interval);
  });

export default async () => {
  // 动态加载脚本的工具函数
  const loadScript = (src) =>
    new Promise((resolve, reject) => {
      const script = document.createElement("script");
      script.src = src;
      script.async = false;
      script.defer = false;
      script.onload = resolve;
      script.onerror = reject;
      document.head.appendChild(script);
    });

  try {
    // 1. 加载 jQuery
    if (!window.jQuery) {
      await loadScript("/kityformula/js/jquery.min.js");
      console.log("jQuery 加载完毕");
    }

    // 2. 加载 kitygraph
    await loadScript("/kityformula/js/kitygraph.all.js");
    console.log("kitygraph 加载完毕");
    await waitFor(() => window.kity);

    // 3. 加载 render 和 parser
    await loadScript("/kityformula/js/kity-formula-render.all.js");
    await loadScript("/kityformula/js/kity-formula-parser.all.min.js");
    console.log("render 和 parser 加载完毕");

    // 4. 加载 editor
    await loadScript("/kityformula/js/kityformula-editor.all.min.js");
    console.log("editor 加载完毕");

    // 5. 动态引入样式
    const styles = [
      "/kityformula/assets/styles/base.css",
      "/kityformula/assets/styles/page.css",
      "/kityformula/assets/styles/ui.css",
      "/kityformula/assets/styles/dialogbase.css",
    ];
    styles.forEach((href) => {
      const link = document.createElement("link");
      link.rel = "stylesheet";
      link.href = href;
      document.head.appendChild(link);
    });

    await waitFor(() => window.kf && window.kf.EditorFactory);

    // ✅ 修复: 防止空公式状态下 objTree 为 null 报错
    if (window.kf?.SyntaxComponenet?.prototype?.hasRootplaceholder) {
      const original = window.kf.SyntaxComponenet.prototype.hasRootplaceholder;
      window.kf.SyntaxComponenet.prototype.hasRootplaceholder = function (
        ...args
      ) {
        if (!this.objTree || !this.objTree.mapping) {
          return false; // 跳过
        }
        return original.apply(this, args);
      };
      console.log("✅ KityFormula Patch: hasRootplaceholder 防空指针修复");
    }
  } catch (err) {
    console.error("初始化失败", err);
  }
};

创建 KityFormula 组件

  1. 创建 kityformula-editor 编辑器 初始化 方法 (用户 给编辑器 赋 初始值 以及 点击 进入编辑状态)
javascript 复制代码
// editorRef 为 kityformula-editor 编辑器 的实例 , 见下文

  const init_editor = () => {
    // 延迟尝试创建编辑器,确保 DOM 和 kf 已就绪
    const tryCreateEditor = () => {
      if (
        !editorRef.current &&
        window.kf?.EditorFactory &&
        containerRef.current
      ) {
        editorRef.current = window.kf.EditorFactory.create(
          containerRef.current,
          {
            render: {
              fontsize: 24,
            },
            resource: {
              path: "/kityformula/resource/", // 加载字体资源
            },
          }
        );

        setTimeout(() => {
          const editor = editorRef.current?.editor; // KFEditor 实例
          if (editor && editor.execCommand) {
            // 公式回填  value 为传入 该组件的 公式: string 用于公式回填,编辑公式
            editor.execCommand("render", value ? value : "\placeholder");
            editor.execCommand("focus");
            console.log("✅ 插入占位公式并聚焦");
          } else {
            console.warn("⚠️ KFEditor 未准备好");
          }
        }, 200);

      } else if (!editorRef.current) {
        setTimeout(tryCreateEditor, 50);
      }
    };

    tryCreateEditor();
  };

组件初始化时 调用工具函数,引入 kityformula-editor。

javascript 复制代码
//三方
import { useEffect } from 'React'

// utils
import init_kityformula_editor from './utils'

const KityFormula = props => {

  
  const containerRef = useRef(null);  // 渲染 编辑器 的DOM,编辑器创建好之后,或将其渲染到该 DOM 中
  const editorRef = useRef(null);  // 用于绑定 编辑器 的实例,编辑器的 各种方法属性 都在该实例中


  useEffect(() => {
    // 延时调用, 确保 DOM 已经挂载
    const timer = setTimeout(() => {
      // 挂载 编辑器
      init_kityformula();

      // 初始化 编辑器
      init_editor();
    }, 100);

    // 组件卸载 卸载实例
    return () => {
      clearTimeout(timer);
      if (editorRef.current?.dispose) editorRef.current.dispose();
    };
  }, [])

  return (
    <div style={{ padding: 20 }}>
      <div
        ref={containerRef}
        className="my-kf-editor"
      />
    </div>
  )
}

最重要的一点,向外暴漏(创建)导出图片,公式 string 的方法

kityformula-editor 中所有的(大部分这个逻辑)方法调用都是这个逻辑

kityformula-editor 暴露的方法 中的 execFn为一个 对象方法, 其本身是一个 方法, 同时还包括一个属性方法 apply(正式使用时调用的方法)

其参数 thisArg 为一个必填的,kityformula-editor方法对象中 (cmd)中的 executor

typescript 复制代码
// 从实例对象中找到所需的方法
// 此处 editorRef.current 为实例对象
const cmds =
  editorRef.current.commands ||
  editorRef.current.editor?.commands ||
  null;


// cmd 中的 execFn为一个 对象方法, 其本身是一个 方法, 同时还包括一个属性方法,apply
type TCmd<TArgs = any, TResult = any> = {
  execFn: {
    (...args: TArgs[]): TResult
    apply(thisArg: any, args: TArgs[]): TResult
  }
  executor: object // 调用 apply 串的第一个参数
}

const cmd: TCmd= cmds['具体方法名']

这里我使用的是 react 中父组件调用 子组件的方法 所以用到了 useImperativeHandle、forwardRef

javascript 复制代码
import { useImperativeHandle, forwardRef } from "react";


const KityFormula = forwardRef((props, ref) => {

  /**
  * kityformula 创建的公式 最终形式 是一个 字符串,kity 会将这个字符串转为 公式,
  * 所以 公式的保存形式为 字符串;显示形式为 图片。
  */
  
  // 向父组件暴露方法(替换原有 useImperativeHandle 块)
  useImperativeHandle(ref, () => ({
    // 返回 公式 字符串
    getFormulaString: () => {
      if (!editorRef.current) return "";

      // cmds 中存储的 是具体的导出方案,但是因为方法入参需要传入一个 对象,所以现将其提取出来

      // editorRef.current 可能是 KFEditor,也可能是一个 wrapper(带 .editor)
      const cmds =
        editorRef.current.commands ||
        editorRef.current.editor?.commands ||
        null;

      // 下面为逻辑判断,避免报错
      if (!cmds) {
        console.warn("找不到 commands:", editorRef.current);
        return "";
      }

      // 导出字符串方法  get.source
      const cmd = cmds["get.source"] || cmds["getSource"] || null;
      if (!cmd) {
        console.warn("命令 get.source 不存在,available:", Object.keys(cmds));
        return "";
      }

      // 最终导出 公式字符串
      return cmd.execFn.apply(cmd.executor);
    },
    // 返回 Promise,resolve 为 base64 图片字符串或空串
    getFormulaImage: () => {
      if (!editorRef.current) return "";

      //
      const strFoormula = ref.current.getFormulaString()
      if (!strFoormula || strFoormula === "placeholder" || strFoormula === "\placeholder ") {
        console.warn('未输入公式!');
        return
      }


      // editorRef.current 可能是 KFEditor,也可能是一个 wrapper(带 .editor)
      const cmds =
        editorRef.current.commands ||
        editorRef.current.editor?.commands ||
        null;

      if (!cmds) {
        console.warn("找不到 commands:", editorRef.current);
        return "";
      }

      const cmd = cmds["get.image.data"] || cmds["getImageData"] || null;
      if (!cmd) {
        console.warn("命令 get.image.data 不存在,available:", Object.keys(cmds));
        return "";
      }

      try {
        return new Promise((resolve) => {
          // 1) 先尝试以 callback 形式(大多数实现会走 callback)
          let finished = false;

          try {
            const maybeReturn = cmd.execFn.apply(cmd.executor, [
              (data) => {
                finished = true;
                console.log("get.image.data callback ->", data);
                resolve(data?.img || "");
              },
            ]);

            // 2) 如果 execFn 同步返回了对象(某些实现),则直接用返回值
            if (!finished && maybeReturn && typeof maybeReturn === "object") {
              finished = true;
              console.log("get.image.data returned sync ->", maybeReturn);
              resolve(maybeReturn.img || "");
            }

            // 3) 兜底:如果既没回调也没返回(极少见),等待短时间后返回空串
            setTimeout(() => {
              if (!finished) {
                console.warn("get.image.data 未在回调或返回中给出结果,返回空字符串。");
                resolve("");
              }
            }, 200); // 200ms 兜底
          } catch (e) {
            console.error("调用 get.image.data 出错:", e);
            // resolve("");
          }
        });
      } catch (e) {
        console.error("获取公式图片失败:", e);
        return "";
      }
    },
  }));

  return (
    <div style={{ padding: 20 }}>
      <div
        ref={containerRef}
        className="my-kf-editor"
      />
    </div>
  );
});

export default KityFormula;

完整组件

javascript 复制代码
import { useEffect, useRef, useImperativeHandle, forwardRef } from "react";
import "./index.less";

import { Button } from 'antd'

import init_kityformula from './utils/kityformula.js'

const KityFormula = forwardRef((props, ref) => {
  const { data: value = null } = props

  const containerRef = useRef(null);
  const editorRef = useRef(null);

  const init_editor = () => {
    // 延迟尝试创建编辑器,确保 DOM 和 kf 已就绪
    const tryCreateEditor = () => {
      if (
        !editorRef.current &&
        window.kf?.EditorFactory &&
        containerRef.current
      ) {
        editorRef.current = window.kf.EditorFactory.create(
          containerRef.current,
          {
            render: {
              fontsize: 24,
            },
            resource: {
              path: "/kityformula/assets/",
            },
          }
        );

        setTimeout(() => {
          const editor = editorRef.current.editor; // KFEditor 实例
          if (editor && editor.execCommand) {
            // 公式回填
            editor.execCommand("render", value ? value : "\placeholder");
            editor.execCommand("focus");
            console.log("✅ 插入占位公式并聚焦");
          } else {
            console.warn("⚠️ KFEditor 未准备好");
          }
        }, 200);

      } else if (!editorRef.current) {
        setTimeout(tryCreateEditor, 50);
      }
    };

    tryCreateEditor();
  };


  // 向父组件暴露方法(替换原有 useImperativeHandle 块)
  useImperativeHandle(ref, () => ({
    // 返回 Promise,resolve 为 base64 图片字符串或空串
    getFormulaImage: () => {
      if (!editorRef.current) return "";

      const strFoormula = ref.current.getFormulaString()
      if (!strFoormula || strFoormula === "placeholder" || strFoormula === "\placeholder ") {
        console.warn('未输入公式!');
        return
      }


      // editorRef.current 可能是 KFEditor,也可能是一个 wrapper(带 .editor)
      const cmds =
        editorRef.current.commands ||
        editorRef.current.editor?.commands ||
        null;

      if (!cmds) {
        console.warn("找不到 commands:", editorRef.current);
        return "";
      }

      const cmd = cmds["get.image.data"] || cmds["getImageData"] || null;
      if (!cmd) {
        console.warn("命令 get.image.data 不存在,available:", Object.keys(cmds));
        return "";
      }

      try {
        return new Promise((resolve) => {
          // 1) 先尝试以 callback 形式(大多数实现会走 callback)
          let finished = false;

          try {
            const maybeReturn = cmd.execFn.apply(cmd.executor, [
              (data) => {
                finished = true;
                console.log("get.image.data callback ->", data);
                resolve(data?.img || "");
              },
            ]);

            // 2) 如果 execFn 同步返回了对象(某些实现),则直接用返回值
            if (!finished && maybeReturn && typeof maybeReturn === "object") {
              finished = true;
              console.log("get.image.data returned sync ->", maybeReturn);
              resolve(maybeReturn.img || "");
            }

            // 3) 兜底:如果既没回调也没返回(极少见),等待短时间后返回空串
            setTimeout(() => {
              if (!finished) {
                console.warn("get.image.data 未在回调或返回中给出结果,返回空字符串。");
                resolve("");
              }
            }, 200); // 200ms 兜底
          } catch (e) {
            console.error("调用 get.image.data 出错:", e);
            // resolve("");
          }
        });
      } catch (e) {
        console.error("获取公式图片失败:", e);
        return "";
      }
    },
    getFormulaString: () => {
      if (!editorRef.current) return "";


      // editorRef.current 可能是 KFEditor,也可能是一个 wrapper(带 .editor)
      const cmds =
        editorRef.current.commands ||
        editorRef.current.editor?.commands ||
        null;

      if (!cmds) {
        console.warn("找不到 commands:", editorRef.current);
        return "";
      }

      const cmd = cmds["get.source"] || cmds["getSource"] || null;
      if (!cmd) {
        console.warn("命令 get.source 不存在,available:", Object.keys(cmds));
        return "";
      }

      return cmd.execFn.apply(cmd.executor);
    },
  }));


  useEffect(() => {
    const timer = setTimeout(() => {
      init_kityformula();

      init_editor();
    }, 100);

    return () => {
      clearTimeout(timer);
      if (editorRef.current?.dispose) editorRef.current.dispose();
    };
  }, []);




  return (
    <div style={{ padding: 20 }}>
      <div
        ref={containerRef}
        className="my-kf-editor"
      />
    </div>
  );
});

export default KityFormula;

总结

大概就是这样,这东西也没有一个文档,只能找看源码,看别人的 demo 打断点,看传参,结合 AI 辅助,也是看了 两天。

相关推荐
来恩100334 分钟前
jQuery选择器
前端·javascript·jquery
前端繁华如梦36 分钟前
树上挂苹果还是挂玻璃球?Three.js 程序化果实的完整实现指南
前端·javascript
CDwenhuohuo1 小时前
优惠券组件直接用 uview plus
前端·javascript·vue.js
川冰ICE2 小时前
TypeScript装饰器与元编程实战
前端·javascript·typescript
AI砖家2 小时前
Vue3组件传参大全,各种传参方式的对比
前端·javascript·vue.js
希望永不加班2 小时前
var局部变量类型推断的利弊
java·服务器·前端·javascript·html
threelab2 小时前
Three.js 3D 地图可视化 | 三维可视化 / AI 提示词
前端·javascript·人工智能·3d·着色器
失眠的咕噜3 小时前
PDA 安卓设备上传多张图片
android·前端·javascript
掰头战士3 小时前
深入了解JS原型及原型继承链机制
javascript
一只叁木Meow4 小时前
电商 SKU 选择器:用算法实现优雅的用户交互
前端·javascript·算法