把第三方的 UI 组件封装为 Vue 组件,以Frappe Gantt为例 想要在Vue中使用甘特图组件,参考vue-echart的实现

转化的步骤

  1. 在on Mount的hook中,初始化,并且将回调转换为事件emit
  2. 实现 v-model 双向绑定
    1. 初始化,然后在js库回调中emit事件
    2. 将js库所有更改传递到vue组件(在回调事件emit函数中修改传入的props)
    3. 外部vue组件的改变传递到js库(用watch来实现,当vue组件状态改变时候,同步修改js库的ui)
    4. 最后处理对于插槽(选项列表)内部内容的所有变更。(用updated 生命周期hook)

什么是Web Components

https://developer.mozilla.org/zh-CN/docs/Web/API/Web_components/Using_custom_elements 需要将封装为一个web component

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Custom Element Demo</title>
</head>

<body>
    <my-custom-element color="blue" size="20px">Click me!</my-custom-element>
    <script>
        class MyCustomElement extends HTMLElement {
            static observedAttributes = ["color", "size"];
            constructor() {
                super();
                this.attachShadow({ mode: "open" }); // 使用 Shadow DOM
                this.shadowRoot.innerHTML = `
                    <style>
                        :host {
                            display: inline-block;
                            padding: 10px;
                            border: 2px solid black;
                            cursor: pointer;
                        }
                    </style>
                    <slot></slot>
                `;
            }

            connectedCallback() {
                console.log("自定义元素添加至页面。");
                this.updateStyle(); // 初始化样式
                this.addEventListener("click", this.handleClick);
            }

            disconnectedCallback() {
                console.log("自定义元素从页面中移除。");
                this.removeEventListener("click", this.handleClick);
            }

            attributeChangedCallback(name, oldValue, newValue) {
                console.log(`属性 ${name} 已变更:从 "${oldValue}" 变为 "${newValue}"`);
                this.updateStyle(); // 属性变化时更新样式
            }

            updateStyle() {
                const color = this.getAttribute("color") || "black";
                const size = this.getAttribute("size") || "16px";
                this.style.color = color;
                this.style.fontSize = size;
            }

            handleClick() {
                alert("自定义元素被点击!");
            }
        }

        customElements.define("my-custom-element", MyCustomElement);

        // 动态更改属性
        setTimeout(() => {
            const customElement = document.querySelector("my-custom-element");
            customElement.setAttribute("color", "red");
            customElement.setAttribute("size", "24px");
        }, 2000);
    </script>
</body>
</html>

遇到的问题

  1. 第三方组件gantt在内部拖拽时更新了tasks,导致watcheffet循环响应了。循环,导致无限更新
1. 外部的tasks更改后,要同步更新js组件库的ui
2. 而可以通过js组件库的ui更改tasks。也要同步修改到外部的vue的tasks中

分为两种情况:

  • 如果js组件只读数据,就可以用

export default defineComponent({
  props: {
    tasks: Array as PropType<Tasks>,
    options: Object,
  },
  emits: ["date-change", "update:modelValue"],
  setup(props, context) {
    const root = shallowRef<GanttElement>();
    const gantt = shallowRef<Gantt>();

    function init() {
      if (!root.value) {
        console.error("root is ", root.value);
        return;
      }

      const instance = (gantt.value = new Gantt(root.value, props.tasks, {
        ...props.options,
        on_date_change: (task) => {
          // 在这里可以获取最新的任务数据
          context.emit("date-change", task);
          // 修改props.tasks,通过事件返回
          // context.emit("update:modelValue",)
        },
      }));

      if (instance) {
        console.log("Gantt chart initialized successfully.");
      } else {
        console.error("Failed to initialize Gantt chart.");
      }
    }

    onMounted(() => {
      init();
    });

    watchEffect(()=> {
      if (props.tasks && gantt.value){
        console.log("tasks update")
        gantt.value.refresh(props.tasks);
      }
    })

    return {
      root,
    };
  },
  render() {
    const attrs = {} as any;
    attrs.ref = "root";
    return h(TAG_NAME, attrs);
  },
});
  • 如果js组件会修改数据,就不能像上面那样,因为修改点有两个(vue的响应式,js内部组件改变数据)
      • 明确数据单向流动:
        • Vue 外部更新 tasks,传递给 JS 组件库,触发 UI 更新。
        • JS 组件库通过事件回调通知 Vue 修改 tasks,由 Vue 统一管理数据。
    • 避免直接操作响应式数据:
      • 通过本地 ref 副本中转 tasks,避免直接修改 props.tasks

watch的新、旧值输出一样

https://stackoverflow.com/questions/62729380/vue-watch-outputs-same-oldvalue-and-newvalue

参考资料

https://www.smashingmagazine.com/2019/02/vue-framework-third-party-javascript/#components-and-component-libraries

https://codepen.io/smashing-magazine/pen/QoLJJV