自定义指令

组件上不推荐使用自定义指令

自定义指令种类

  1. 注册全局指令

    const app = createApp({})
    
    // 使 v-focus 在所有组件中都可用
    app.directive('focus', {
      /* ... */
    })
    
  2. 注册局部指令(很少使用)

    const focus = {
      mounted: (el) => el.focus()
    }
    
    export default {
      directives: {
        // 在模板中启用 v-focus
        focus
      }
    }
    

钩子函数

  1. 一个指令的定义对象可以提供几种钩子函数 (都是可选的):

    const myDirective = {
      // 在绑定元素的 attribute 前
      // 或事件监听器应用前调用
      created(el, binding, vnode, prevVnode) { },
    
      // 在元素被插入到 DOM 前调用
      beforeMount(el, binding, vnode, prevVnode) { },
    
      // 在绑定元素的父组件
      // 及他自己的所有子节点都挂载完成后调用
      mounted(el, binding, vnode, prevVnode) { },
    
      // 绑定元素的父组件更新前调用
      beforeUpdate(el, binding, vnode, prevVnode) { },
    
      // 在绑定元素的父组件
      // 及他自己的所有子节点都更新后调用
      updated(el, binding, vnode, prevVnode) { },
    
      // 绑定元素的父组件卸载前调用
      beforeUnmount(el, binding, vnode, prevVnode) { },
    
      // 绑定元素的父组件卸载后调用
      unmounted(el, binding, vnode, prevVnode) { }
    }
    
  2. 钩子函数参数:

    • el:指令所绑定的元素,可以用来直接操作 DOM;
    • binding:一个对象,包含以下 property:
      • value:指令的绑定值,例如:v-my-directive=“1 + 1” 中,绑定值为 2;
      • oldValue:指令绑定的前一个值,仅在 update 和 componentUpdated 钩子中可用,无论值是否改变都可用;
      • arg:传给指令的参数,可选,例如 v-my-directive:foo 中,参数为 “foo”。
      • modifiers:一个包含修饰符的对象,例如:v-my-directive.foo.bar 中,修饰符对象为 { foo: true, bar: true };
      • instance:使用该指令的组件实例;
      • dir:指令的定义对象;
      • vnode:Vue 编译生成的虚拟节点;
    • prevNode:之前的渲染中代表指令所绑定元素的 VNode,仅在 update 和 componentUpdated 钩子中可用;
  3. 举例:

    <!-- 像下面这样使用指令 -->
    <div v-example:foo.bar="baz">
    
    // binding  参数会是一个这样的对象
    {
      arg: 'foo',
      modifiers: { bar: true },
      value: /* `baz` 的值 */,
      oldValue: /* 上一次更新时 `baz` 的值 */
    }
    

批量挂载自定义指令

  1. 手动方式

    JavaScript
    JavaScript
    // main.js
    import { createApp } from 'vue';
    import App from '@/App';
    import Directives from '/directives/index.ts';
    const app = createApp(App);
    app.use(Directives);
    app.mount('#app');
    
    // directives/index.js
    import copy from './copy';
    import screenfull from './screenfull';
    import ellipsis from './ellipsis';
    import backtop from './backtop';
    import resize from './resize';
    import debounce from './debounce';
    import permission from './permission';
    import waterMarker from './waterMarker';
    
    // 自定义指令
    const directives = {
      copy,
      screenfull,
      ellipsis,
      backtop,
      resize,
      debounce,
      permission,
      waterMarker,
    };
    
    export default {
      install(Vue) {
        Object.keys(directives).forEach((key) => {
          Vue.directive(key, directives[key]);
        });
      },
    };
    
  2. 自动方式

案例

点击外部指令 v-click-outside

import type { Directive, DirectiveBinding } from "vue";

interface ClickOutsideBinding extends DirectiveBinding {
  value: (e: Event) => void;
}

const vClickOutside: Directive = {
  beforeMount(el: HTMLElement, binding: ClickOutsideBinding) {
    el._clickOutsideHandler = (event: Event) => {
      if (!(el === event.target || el.contains(event.target as Node))) {
        binding.value(event);
      }
    };
    document.addEventListener("click", el._clickOutsideHandler);
  },
  unmounted(el: HTMLElement) {
    if (el._clickOutsideHandler) {
      document.removeEventListener("click", el._clickOutsideHandler);
    }
  },
};

declare global {
  interface HTMLElement {
    _clickOutsideHandler?: (e: Event) => void;
  }
}

export default vClickOutside;

响应缩放指令 v-resize

可以监听绑定了 v-resize 指令的元素的尺寸

import type { Directive, DirectiveBinding } from "vue";

interface ResizeObserverEntry {
  target: Element;
  contentRect: DOMRectReadOnly;
}
interface ResizeDirectiveBinding extends DirectiveBinding {
  value: (entries: ResizeObserverEntry[]) => void;
}

let lastWidth = 0,
  lastHeight = 0;

function checkResize(currentWidth, currentHeight) {
  let flag = true;

  if (currentWidth == lastWidth && currentHeight == lastHeight) {
    flag = true;
  } else {
    lastWidth = currentWidth;
    lastHeight = currentHeight;
    flag = false;
  }

  return flag;
}

function handleResize(currentWidth, currentHeight, cb) {
  if (!checkResize(currentWidth, currentHeight) && typeof cb == "function") {
    cb();
  }
}

const vResize: Directive = {
  mounted(el: HTMLElement, binding: ResizeDirectiveBinding) {
    const observer = new ResizeObserver((entries) => {
      for (let entry of entries) {
        console.log(
          "新尺寸:",
          entry.contentRect.width,
          entry.contentRect.height
        );
        handleResize(
          entry.contentRect.width,
          entry.contentRect.height,
          binding.value
        );
      }
    });
    observer.observe(el as Element);
    el._resizeObserver = observer;
  },
  unmounted(el: HTMLElement) {
    // el._resizeObserver?.unobserve(el); // 适用于只需要停止监听某个特定的元素时
    el._resizeObserver?.disconnect(); // 适用于需要完全停止所有元素的监听时
  },
};

declare global {
  interface HTMLElement {
    _resizeObserver?: ResizeObserver;
  }
}

export default vResize;

拖拽指令 (v-draggable)

import type { Directive } from "vue";

interface DraggableOptions {
  axis?: "x" | "y" | "both";
  handle?: HTMLElement | null;
}

const vDraggable: Directive<HTMLElement, DraggableOptions | undefined> = {
  mounted(el, binding) {
    const options = binding.value || {};
    const handle = options.handle || el;

    let startX = 0;
    let startY = 0;
    let initialX = 0;
    let initialY = 0;

    handle.style.cursor = "move";

    const onMouseDown = (e: MouseEvent) => {
      e.preventDefault();
      startX = e.clientX;
      startY = e.clientY;
      initialX = el.offsetLeft;
      initialY = el.offsetTop;

      document.addEventListener("mousemove", onMouseMove);
      document.addEventListener("mouseup", onMouseUp);
    };

    const onMouseMove = (e: MouseEvent) => {
      const dx = e.clientX - startX;
      const dy = e.clientY - startY;

      if (options.axis !== "y") {
        el.style.left = `${initialX + dx}px`;
      }
      if (options.axis !== "x") {
        el.style.top = `${initialY + dy}px`;
      }
    };

    const onMouseUp = () => {
      document.removeEventListener("mousemove", onMouseMove);
      document.removeEventListener("mouseup", onMouseUp);
    };

    handle.addEventListener("mousedown", onMouseDown);

    el._draggableCleanup = () => {
      handle.removeEventListener("mousedown", onMouseDown);
    };
  },
  unmounted(el) {
    el._draggableCleanup?.();
  },
};

declare global {
  interface HTMLElement {
    _draggableCleanup?: () => void;
  }
}

export default vDraggable;

权限控制指令 v-permission

import { useAccount } from "@/pinia/modules/account";
import type { Directive, DirectiveBinding } from "vue";

interface PermissionBinding extends DirectiveBinding {
  value: string | string[];
}

const vPermission: Directive = {
  beforeMount(el: HTMLElement, binding: PermissionBinding) {
    const { permissionList } = useAccount();
    const permissions = Array.isArray(binding.value)
      ? binding.value
      : [binding.value];

    const hasPermission = permissions.some((permission) =>
      permissionList.includes(permission)
    );

    if (!hasPermission) {
      el.style.display = "none";
      el._permissionOriginalDisplay = el.style.display;
    }
  },
  updated(el: HTMLElement, binding: PermissionBinding) {
    const { permissionList } = useAccount();
    const permissions = Array.isArray(binding.value)
      ? binding.value
      : [binding.value];

    const hasPermission = permissions.some((permission) =>
      permissionList.includes(permission)
    );

    if (hasPermission) {
      el.style.display = el._permissionOriginalDisplay || "";
    } else {
      el.style.display = "none";
    }
  },
};

declare global {
  interface HTMLElement {
    _permissionOriginalDisplay?: string;
  }
}

export default vPermission;

复制文本指令 v-copy

import { ElMessage } from "element-plus";
import type { Directive, DirectiveBinding } from "vue";

interface CopyBinding extends DirectiveBinding {
  value: string | (() => string);
}

const vCopy: Directive = {
  mounted(el: HTMLElement, binding: CopyBinding) {
    el._copyHandler = () => {
      const text =
        typeof binding.value === "function" ? binding.value() : binding.value;

      navigator.clipboard
        .writeText(text)
        .then(() => {
          ElMessage.success("复制成功!");
        })
        .catch((err) => {
          console.error("复制失败:", err);
        });
    };

    el.addEventListener("click", el._copyHandler);
    el.style.cursor = "copy";
  },
  unmounted(el: HTMLElement) {
    if (el._copyHandler) {
      el.removeEventListener("click", el._copyHandler);
    }
  },
};

declare global {
  interface HTMLElement {
    _copyHandler?: () => void;
  }
}

export default vCopy;

防抖指令 v-debounce

import type { Directive, DirectiveBinding } from "vue";

interface DebounceOptions {
  event: string;
  delay?: number;
}

const vDebounce: Directive = {
  mounted(el: HTMLElement, binding: DirectiveBinding<Function>) {
    const defaultEvent = "click";
    const defaultDelay = 500;
    const options: DebounceOptions = {
      event: binding.arg || defaultEvent,
      delay: binding.arg ? parseInt(binding.arg.split("_")[1]) : defaultDelay,
    };

    let timeout: number;
    el._debounceHandler = (e: Event) => {
      clearTimeout(timeout);
      timeout = setTimeout(() => {
        binding.value(e);
      }, options.delay);
    };
    el.addEventListener(options.event, el._debounceHandler);
  },
  unmounted(el: HTMLElement) {
    if (el._debounceHandler) {
      const event = el._debounceHandler?.event || "click";
      el.removeEventListener(event, el._debounceHandler);
    }
  },
};

declare global {
  interface HTMLElement {
    _debounceHandler?: ((e: Event) => void) & { event?: string };
  }
}

export default vDebounce;

聚焦指令 v-focus

import type { Directive, DirectiveBinding } from "vue";

const vFocus: Directive = {
  mounted(el: HTMLElement) {
    el.focus();
  },
  updated(el: HTMLElement, binding: DirectiveBinding<boolean>) {
    if (binding.value) {
      el.focus();
    }
  },
};

export default vFocus;

无限滚动指令 v-infinite-scroll

import type { Directive, DirectiveBinding } from "vue";

interface InfiniteScrollOptions {
  distance: number;
  disabled?: boolean;
  delay?: number;
}

const vInfiniteScroll: Directive = {
  mounted(el: HTMLElement, binding: DirectiveBinding<() => void>) {
    const options: InfiniteScrollOptions = {
      distance: 50,
      disabled: false,
      delay: 200,
      ...(binding.value as any),
    };

    let isLoading = false;

    const onScroll = () => {
      if (options.disabled || isLoading) return;

      const scrollTop = el.scrollTop;
      const scrollHeight = el.scrollHeight;
      const clientHeight = el.clientHeight;

      if (scrollHeight - (scrollTop + clientHeight) <= options.distance) {
        isLoading = true;
        setTimeout(() => {
          binding.value();
          isLoading = false;
        }, options.delay);
      }
    };

    el.addEventListener("scroll", onScroll);
    el._infiniteScrollCleanup = () => {
      el.removeEventListener("scroll", onScroll);
    };
  },
  unmounted(el: HTMLElement) {
    el._infiniteScrollCleanup?.();
  },
};

declare global {
  interface HTMLElement {
    _infiniteScrollCleanup?: () => void;
  }
}

export default vInfiniteScroll;

工具提示指令 v-tooltip

import type { Directive, DirectiveBinding } from "vue";

interface TooltipOptions {
  content: string;
  placement?: "top" | "bottom" | "left" | "right";
  trigger?: "hover" | "click";
}

const vTooltip: Directive = {
  mounted(el: HTMLElement, binding: DirectiveBinding<TooltipOptions | string>) {
    const options =
      typeof binding.value === "string"
        ? { content: binding.value }
        : binding.value;

    const tooltip = document.createElement("div");
    tooltip.className = "vue-tooltip";
    tooltip.textContent = options.content;

    Object.assign(tooltip.style, {
      position: "absolute",
      background: "#333",
      color: "#fff",
      padding: "5px 10px",
      borderRadius: "4px",
      fontSize: "12px",
      zIndex: "9999",
      display: "none",
    });

    document.body.appendChild(tooltip);

    const positionTooltip = () => {
      const rect = el.getBoundingClientRect();
      const tooltipRect = tooltip.getBoundingClientRect();

      let top = 0;
      let left = 0;

      switch (options.placement || "top") {
        case "top":
          top = rect.top - tooltipRect.height - 5;
          left = rect.left + rect.width / 2 - tooltipRect.width / 2;
          break;
        case "bottom":
          top = rect.bottom + 5;
          left = rect.left + rect.width / 2 - tooltipRect.width / 2;
          break;
        case "left":
          top = rect.top + rect.height / 2 - tooltipRect.height / 2;
          left = rect.left - tooltipRect.width - 5;
          break;
        case "right":
          top = rect.top + rect.height / 2 - tooltipRect.height / 2;
          left = rect.right + 5;
          break;
      }

      tooltip.style.top = `${top + window.scrollY}px`;
      tooltip.style.left = `${left + window.scrollX}px`;
    };

    const showTooltip = () => {
      positionTooltip();
      tooltip.style.display = "block";
    };

    const hideTooltip = () => {
      tooltip.style.display = "none";
    };

    if ((options.trigger || "hover") === "hover") {
      el.addEventListener("mouseenter", showTooltip);
      el.addEventListener("mouseleave", hideTooltip);
      tooltip.addEventListener("mouseenter", showTooltip);
      tooltip.addEventListener("mouseleave", hideTooltip);
    } else {
      el.addEventListener("click", () => {
        if (tooltip.style.display === "block") {
          hideTooltip();
        } else {
          showTooltip();
        }
      });
    }

    el._tooltipCleanup = () => {
      document.body.removeChild(tooltip);
      el.removeEventListener("mouseenter", showTooltip);
      el.removeEventListener("mouseleave", hideTooltip);
      el.removeEventListener("click", showTooltip);
    };
  },
  unmounted(el: HTMLElement) {
    el._tooltipCleanup?.();
  },
};

declare global {
  interface HTMLElement {
    _tooltipCleanup?: () => void;
  }
}

export default vTooltip;

图片懒加载指令 v-lazy

import type { Directive, DirectiveBinding } from "vue";

const vLazy: Directive = {
  mounted(el: HTMLImageElement, binding: DirectiveBinding<string>) {
    const observer = new IntersectionObserver(
      (entries) => {
        entries.forEach((entry) => {
          if (entry.isIntersecting) {
            el.src = binding.value;
            observer.unobserve(el);
          }
        });
      },
      {
        rootMargin: "0px",
        threshold: 0.1,
      }
    );

    observer.observe(el);
    el._lazyObserver = observer;
  },
  unmounted(el: HTMLImageElement) {
    el._lazyObserver?.unobserve(el);
  },
};

declare global {
  interface HTMLElement {
    _lazyObserver?: IntersectionObserver;
  }
}

export default vLazy;

全屏指令 v-screenfull

import type { Directive, DirectiveBinding } from "vue";

interface ScreenfullElement extends HTMLElement {
  _screenfullHandler?: () => void;
}

const vScreenfull: Directive = {
  mounted(el: ScreenfullElement, binding: DirectiveBinding<boolean | undefined>) {
    const isFullscreen = binding.value ?? true;

    const toggleFullscreen = () => {
      if (!document.fullscreenElement) {
        el.requestFullscreen().catch((err) => {
          console.error("全屏失败:", err);
        });
      } else {
        if (document.exitFullscreen) {
          document.exitFullscreen();
        }
      }
    };

    el._screenfullHandler = toggleFullscreen;
    el.addEventListener("click", el._screenfullHandler);

    if (isFullscreen && !document.fullscreenElement) {
      el.requestFullscreen().catch((err) => {
        console.error("初始全屏失败:", err);
      });
    }
  },
  unmounted(el: ScreenfullElement) {
    if (el._screenfullHandler) {
      el.removeEventListener("click", el._screenfullHandler);
    }
  },
};

export default vScreenfull;

文字超出省略指令 v-ellipsis

import type { Directive } from "vue";

interface EllipsisOptions {
  line?: number; // 行数,默认1行
  content?: string; // 自定义内容
}

const vEllipsis: Directive<HTMLElement, EllipsisOptions | string | undefined> = {
    mounted(el, binding) {
      const options: EllipsisOptions =
        typeof binding.value === "object"
          ? binding.value
          : { content: binding.value };

      const line = options.line || 1;
      const content = options.content || el.textContent || "";

      el.style.overflow = "hidden";
      el.style.textOverflow = "ellipsis";
      el.style.display = "-webkit-box";
      el.style.webkitLineClamp = String(line);
      el.style.webkitBoxOrient = "vertical";

      if (options.content) {
        el.textContent = content;
      }
    },
    updated(el, binding) {
      const options: EllipsisOptions =
        typeof binding.value === "object"
          ? binding.value
          : { content: binding.value };

      if (options.content) {
        el.textContent = options.content;
      }
    },
  };

export default vEllipsis;

回到顶部指令 v-backtop

import type { Directive } from "vue";

interface BacktopOptions {
  visibilityHeight?: number; // 滚动多少距离显示按钮
  right?: string; // 距离右侧距离
  bottom?: string; // 距离底部距离
  transition?: string; // 过渡效果
  scrollContainer?: HTMLElement | string; // 滚动条容器
}

interface BacktopElement extends HTMLElement {
  _backtopHandler?: () => void;
  _backtopScrollHandler?: () => void;
  _scrollContainer: HTMLElement;
}

const vBacktop: Directive<BacktopElement, BacktopOptions | undefined> = {
  mounted(el, binding) {
    const options: BacktopOptions = binding.value || {};
    const {
      visibilityHeight = 200,
      right = "40px",
      bottom = "40px",
      transition = "all .3s",
    } = options;
    const scrollContainer =
      (typeof options.scrollContainer == "string"
        ? document.getElementById(options.scrollContainer)
        : options.scrollContainer) ?? window;

    // 设置按钮样式
    Object.assign(el.style, {
      position: "fixed",
      right,
      bottom,
      cursor: "pointer",
      transition,
      opacity: "0",
      visibility: "hidden",
    });

    // 滚动事件处理
    const scrollHandler = () => {
      const scrollTop = scrollContainer==window?(document.documentElement || document.body).scrollTop:(scrollContainer as HTMLElement).scrollTop;
      if (scrollTop > visibilityHeight) {
        el.style.opacity = "1";
        el.style.visibility = "visible";
      } else {
        el.style.opacity = "0";
        el.style.visibility = "hidden";
      }
    };

    // 点击返回顶部
    const clickHandler = () => {
      scrollContainer.scrollTo({
        top: 0,
        behavior: "smooth",
      });
    };

    el._scrollContainer = scrollContainer as HTMLElement;
    el._backtopScrollHandler = scrollHandler;
    el._backtopHandler = clickHandler;

    scrollContainer.addEventListener("scroll", scrollHandler);
    el.addEventListener("click", clickHandler);

    // 初始检查
    scrollHandler();
  },
  unmounted(el: BacktopElement) {
    if (el._backtopScrollHandler && el._scrollContainer) {
      el._scrollContainer.removeEventListener(
        "scroll",
        el._backtopScrollHandler
      );
    }
    if (el._backtopHandler) {
      el.removeEventListener("click", el._backtopHandler);
    }
  },
};

export default vBacktop;

水印指令 vue-waterMarker

import type { Directive, DirectiveBinding } from "vue";

interface WatermarkOptions {
  text?: string | string[]; // 水印文字
  font?: string; // 字体样式
  color?: string; // 字体颜色
  opacity?: number; // 透明度
  angle?: number; // 旋转角度
  gap?: [number, number]; // 水印间距 [水平, 垂直]
}

const vWatermark: Directive<HTMLElement, WatermarkOptions | string> = {
  mounted(el, binding) {
    createWatermark(el, binding);
  },
  updated(el, binding) {
    createWatermark(el, binding);
  },
};

function createWatermark(
  el: HTMLElement,
  binding: DirectiveBinding<WatermarkOptions | string>
) {
  // 清除现有水印
  const existingWatermark = el.querySelector(".vue-watermark");
  if (existingWatermark) {
    el.removeChild(existingWatermark);
  }

  const options: WatermarkOptions =
    typeof binding.value === "string"
      ? { text: binding.value }
      : binding.value || {};

  const {
    text = "Watermark",
    font = "16px Microsoft YaHei",
    color = "rgba(128, 128, 128, 0.3)",
    opacity = 0.3,
    angle = -20,
    gap = [100, 100],
  } = options;

  const canvas = document.createElement("canvas");
  const ctx = canvas.getContext("2d");
  if (!ctx) return;

  // 测量文本宽度
  ctx.font = font;
  const texts = Array.isArray(text) ? text : [text];
  let maxWidth = 0;
  texts.forEach((t) => {
    const metrics = ctx.measureText(t);
    maxWidth = Math.max(maxWidth, metrics.width);
  });

  const canvasSize = Math.max(maxWidth, 200) * 1.5;
  canvas.width = canvasSize + gap[0];
  canvas.height = canvasSize * texts.length + gap[1];

  // 绘制水印
  ctx.font = font;
  ctx.fillStyle = color;
  ctx.globalAlpha = opacity;
  ctx.rotate((angle * Math.PI) / 180);

  texts.forEach((t, i) => {
    ctx.fillText(t, 0, (i + 1) * (canvasSize / texts.length));
  });

  // 创建水印背景
  const watermark = document.createElement("div");
  watermark.className = "vue-watermark";
  watermark.style.position = "absolute";
  watermark.style.top = "0";
  watermark.style.left = "0";
  watermark.style.width = "100%";
  watermark.style.height = "100%";
  watermark.style.pointerEvents = "none";
  watermark.style.backgroundImage = `url(${canvas.toDataURL()})`;
  watermark.style.backgroundRepeat = "repeat";

  el.style.position = "relative";
  el.appendChild(watermark);
}

export default vWatermark;

快速引入外部页面指令 v-iframe

import type { Directive, DirectiveBinding } from "vue";

interface IframeOptions {
  src: string;
  width?: string;
  height?: string;
  loading?: "eager" | "lazy";
  sandbox?: string[];
  allow?: string[];
  onLoad?: (iframe: HTMLIFrameElement) => void;
  onError?: (error: Event) => void;
}

interface IframeElement extends HTMLElement {
  _iframe?: HTMLIFrameElement;
  _iframeResizeObserver?: ResizeObserver;
}

const vIframe: Directive<IframeElement, string | IframeOptions> = {
  mounted(el, binding) {
    createIframe(el, binding);
  },
  updated(el, binding) {
    createIframe(el, binding);
  },
  unmounted(el) {
    cleanupIframe(el);
  },
};

function createIframe(
  el: IframeElement,
  binding: DirectiveBinding<string | IframeOptions>
) {
  // 清理现有 iframe
  cleanupIframe(el);

  // 解析配置
  const options: IframeOptions =
    typeof binding.value === "string" ? { src: binding.value } : binding.value;

  if (!options.src) {
    console.error("v-iframe: src is required");
    return;
  }

  // 创建 iframe 元素
  const iframe = document.createElement("iframe");
  iframe.src = options.src;
  iframe.style.border = "none";
  iframe.style.width = options.width || "100%";
  iframe.style.height = options.height || "100%";
  iframe.loading = options.loading || "lazy";

  // 设置安全沙箱属性
  if (options.sandbox) {
    iframe.sandbox.add(...options.sandbox);
  } else {
    // 默认沙箱限制
    iframe.sandbox.add("allow-same-origin", "allow-scripts", "allow-popups");
  }

  // 设置允许权限
  if (options.allow) {
    iframe.allow = options.allow.join("; ");
  }

  // 事件处理
  iframe.onload = () => {
    options.onLoad?.(iframe);
    setupAutoHeight(el, iframe);
  };

  iframe.onerror = (error) => {
    options.onError?.(error);
    console.error("v-iframe: Failed to load", options.src, error);
  };

  el._iframe = iframe;
  el.appendChild(iframe);
}

function setupAutoHeight(el: IframeElement, iframe: HTMLIFrameElement) {
  try {
    // 尝试自动调整高度
    const resizeObserver = new ResizeObserver(() => {
      if (iframe.contentWindow?.document?.body) {
        iframe.style.height = `${iframe.contentWindow.document.body.scrollHeight}px`;
      }
    });

    resizeObserver.observe(iframe);
    el._iframeResizeObserver = resizeObserver;
  } catch (error) {
    console.warn(
      "v-iframe: Auto height adjustment failed due to cross-origin restrictions"
    );
  }
}

function cleanupIframe(el: IframeElement) {
  if (el._iframe) {
    el.removeChild(el._iframe);
    delete el._iframe;
  }

  if (el._iframeResizeObserver) {
    el._iframeResizeObserver.disconnect();
    delete el._iframeResizeObserver;
  }
}

export default vIframe;
打赏作者
您的打赏是我前进的动力
微信
支付宝
评论

中午好👏🏻,我是 ✍🏻   疯狂 codding 中...

粽子

这有关于前端开发的技术文档和你分享。

相信你可以在这里找到对你有用的知识和教程。

了解更多

目录

  1. 1. 自定义指令
    1. 1.1. 自定义指令种类
    2. 1.2. 钩子函数
    3. 1.3. 批量挂载自定义指令
  2. 2. 案例
    1. 2.1. 点击外部指令 v-click-outside
    2. 2.2. 响应缩放指令 v-resize
    3. 2.3. 拖拽指令 (v-draggable)
    4. 2.4. 权限控制指令 v-permission
    5. 2.5. 复制文本指令 v-copy
    6. 2.6. 防抖指令 v-debounce
    7. 2.7. 聚焦指令 v-focus
    8. 2.8. 无限滚动指令 v-infinite-scroll
    9. 2.9. 工具提示指令 v-tooltip
    10. 2.10. 图片懒加载指令 v-lazy
    11. 2.11. 全屏指令 v-screenfull
    12. 2.12. 文字超出省略指令 v-ellipsis
    13. 2.13. 回到顶部指令 v-backtop
    14. 2.14. 水印指令 vue-waterMarker
  3. 3. 快速引入外部页面指令 v-iframe