{T}
项目内容
课程名称未提供
当前章节极验 GT4 验证组件封装为 Vue 组件
知识领域Vue 组件封装 / 前端安全 / 行为验证 / 工程化复用
学习目标掌握如何将极验 GT4 封装为可复用的 Vue 组件,并处理脚本动态加载、实例暴露、销毁和重建等问题
整理时间2026-03-29
资料校对结合极验 GT4 官方 Web API 文档修正实例方法、事件名称和展示模式说明

一、为什么要把极验封装成 Vue 组件 必须掌握

1.1 组件封装的意义

概念说明

如果每个页面都手写一遍极验初始化、事件绑定、实例销毁和脚本加载逻辑,会带来明显的重复劳动,也更容易在不同业务里出现不一致的实现。

把极验 GT4 封装成一个 Vue 组件的价值主要有:

  • 统一接入方式,减少重复代码
  • 对外暴露一致的调用方法,例如 showCaptcharesetdestroy
  • 把脚本动态加载、生命周期清理等细节隐藏在组件内部
  • 便于后续在登录、注册、短信发送等多个业务场景中复用

语法/用法

封装目标通常包括两层:

  1. UI 层:提供一个可直接挂载的验证区域
  2. 能力层:对外暴露实例方法、事件回调和初始化参数

代码示例

ts
type CaptchaExpose = {
  getInstance: () => unknown;
  showCaptcha: () => void;
  reset: () => void;
  destroy: () => void;
};

注意事项

  • 组件封装的核心不是“把代码塞进一个文件里”,而是抽象稳定的输入和输出。
  • 对外暴露的方法应该尽量少而明确,避免把内部实现细节全部透出。

二、封装前先明确输入输出 必须掌握

2.1 组件应该接收什么

概念说明

一个可复用验证码组件,首先要定义清楚 props。老师原文里提到的 proms,实际应为 props

语法/用法

建议的组件输入包括:

  • captchaId:极验产品 ID,必填
  • product:验证展示模式,如 popupfloatbind
  • language:界面语言
  • scriptSrc:SDK 地址,默认指向官方 gt4.js
  • options:其他初始化配置项

代码示例

ts
type GeetestProduct = "popup" | "float" | "bind";

interface GeetestCaptchaProps {
  captchaId: string;
  product?: GeetestProduct;
  language?: string;
  scriptSrc?: string;
  options?: Record<string, unknown>;
}

注意事项

  • captchaId 是业务方在极验后台申请到的标识,必须由外部传入。
  • 如果组件要保持通用性,不要把登录、注册等业务参数硬编码进组件内部。

2.2 组件应该向外暴露什么

概念说明

老师在课程中通过 testRef.value 看到了一组方法,比如 getInstanceresetdestroy。这一步本质上就是“定义组件暴露的公共 API”。

语法/用法

一个合理的对外暴露集合通常包括:

  • getInstance():获取底层极验实例
  • showCaptcha():触发验证,主要用于 bind 模式
  • reset():重置验证状态
  • destroy():销毁实例与 UI

代码示例

ts
defineExpose({
  getInstance,
  showCaptcha,
  reset,
  destroy,
});

注意事项

  • getInstance 可以暴露,但应优先鼓励业务通过组件事件完成交互,而不是直接操作底层实例。
  • destroy 调用后,如果还要继续使用组件,通常需要重新初始化。

三、极验实例与官方 API 对应关系 重要

3.1 哪些是官方实例方法,哪些可能是自定义封装方法

概念说明

课程里提到的 destroyreset 等能力,的确对应极验实例方法。但像 getInstancegetReady 更可能是组件封装层自己定义的方法,而不是极验官方原生 API。

语法/用法

根据极验 GT4 官方 Web API,常见实例能力包括:

  • appendTo(position)
  • showCaptcha()
  • getValidate()
  • reset()
  • destroy()
  • onReady(callback)
  • onNextReady(callback)
  • onSuccess(callback)
  • onError(callback)
  • onClose(callback)

注意事项

  • 做笔记时要区分“官方 API”和“你自己组件暴露的方法”,不要混成一套。
  • 如果你对外暴露 getReady() 这类方法,请在组件内部自己定义其含义,比如“是否已初始化完成”。

四、脚本动态加载是封装的关键 必须掌握

4.1 为什么不能假设业务方已经全局引入 gt4.js

概念说明

老师在课程里提到一个很实际的问题:如果使用组件的页面没有事先在全局引入极验核心 JS,会导致组件初始化时直接报 initGeetest4 is not defined

因此,一个真正可复用的组件,不应该把“业务已经在全局手动引入脚本”当成前提。

语法/用法

动态加载脚本的核心要求:

  • 如果页面已经加载过同一个脚本,则直接复用
  • 如果还没加载,则动态插入 <script>
  • 只有在脚本加载完成之后,才能执行初始化逻辑

代码示例

ts
export function loadScript(src: string, attrs: Record<string, string> = {}) {
  return new Promise<void>((resolve, reject) => {
    const existed = document.querySelector(`script[src="${src}"]`) as HTMLScriptElement | null;

    if (existed) {
      if ((existed as HTMLScriptElement).dataset.loaded === "true") {
        resolve();
        return;
      }

      existed.addEventListener("load", () => resolve(), { once: true });
      existed.addEventListener("error", () => reject(new Error(`load script failed: ${src}`)), { once: true });
      return;
    }

    const script = document.createElement("script");
    script.src = src;

    Object.entries(attrs).forEach(([key, value]) => {
      script.setAttribute(key, value);
    });

    script.addEventListener(
      "load",
      () => {
        script.dataset.loaded = "true";
        resolve();
      },
      { once: true }
    );

    script.addEventListener(
      "error",
      () => reject(new Error(`load script failed: ${src}`)),
      { once: true }
    );

    document.head.appendChild(script);
  });
}

注意事项

  • 课程中老师使用“定时器轮询 initGeetest4 是否存在”的方式可以工作,但不是最稳妥的工程方案。
  • 更推荐让 loadScript 返回 Promise,并在 await 成功之后再初始化。
  • 如果使用 querySelector 查找 script[src="..."],属性选择器中的 URL 必须加引号。

4.2 原始轮询方案的问题与改进

概念说明

原文中的思路是:先插入 <script>,再用 setInterval 每 100ms 检查一次 initGeetest4 是否存在,存在后就停止定时器并执行初始化。

这个方案的优点是直观,缺点也很明显:

  • 多一个轮询开销
  • 容易忘记清理定时器
  • 脚本加载失败时不容易感知
  • 代码可维护性一般

代码示例

ts
// 可以工作,但更适合作为过渡方案
const timer = window.setInterval(() => {
  if (typeof window.initGeetest4 === "function") {
    window.clearInterval(timer);
    initCaptcha();
  }
}, 100);

注意事项

  • 如果已经能控制 loadScript 的实现,就优先使用 Promise,而不是轮询。
  • 轮询方案更像是“兜底技巧”,不应成为默认设计。

五、Vue 组件的完整封装思路 必须掌握

5.1 生命周期管理

概念说明

一个健壮的 Vue 验证组件,至少要处理这几个生命周期动作:

  • mounted:加载脚本并初始化实例
  • unmounted:销毁实例,清理资源
  • props 变化:必要时重建实例

语法/用法

推荐逻辑:

  1. 组件挂载后执行 loadScript
  2. 脚本加载完成后执行 initGeetest4
  3. 把实例保存到 ref
  4. 注册 onReadyonSuccessonError 等事件
  5. 组件卸载时执行 destroy

注意事项

  • 不要在每次渲染时都初始化一次极验实例。
  • 销毁实例时要把本地保存的实例引用一并清空。

5.2 推荐的 Vue 3 封装示例

代码示例

Vue SFC
<script setup lang="ts">
import { nextTick, onMounted, onUnmounted, ref, watch } from "vue";

type GeetestProduct = "popup" | "float" | "bind";

interface GeetestCaptchaProps {
  captchaId: string;
  product?: GeetestProduct;
  language?: string;
  scriptSrc?: string;
  options?: Record<string, unknown>;
}

const props = withDefaults(defineProps<GeetestCaptchaProps>(), {
  product: "popup",
  language: "zho",
  scriptSrc: "https://static.geetest.com/v4/gt4.js",
  options: () => ({}),
});

const emit = defineEmits<{
  ready: [];
  nextReady: [];
  success: [result: Record<string, string>];
  error: [error: unknown];
  close: [];
}>();

const rootRef = ref<HTMLElement | null>(null);
const instanceRef = ref<any>(null);
const initializedRef = ref(false);

function getInstance() {
  return instanceRef.value;
}

function showCaptcha() {
  instanceRef.value?.showCaptcha?.();
}

function reset() {
  instanceRef.value?.reset?.();
}

function destroy() {
  instanceRef.value?.destroy?.();
  instanceRef.value = null;
  initializedRef.value = false;

  if (rootRef.value) {
    rootRef.value.innerHTML = "";
  }
}

async function initCaptcha() {
  await nextTick();

  if (!rootRef.value || typeof window.initGeetest4 !== "function") {
    return;
  }

  destroy();

  await new Promise<void>((resolve) => {
    window.initGeetest4(
      {
        captchaId: props.captchaId,
        product: props.product,
        language: props.language,
        ...props.options,
      },
      (captchaObj: any) => {
        instanceRef.value = captchaObj;

        if (props.product !== "bind") {
          captchaObj.appendTo(rootRef.value);
        }

        captchaObj
          .onReady(() => {
            initializedRef.value = true;
            emit("ready");
            resolve();
          })
          .onNextReady(() => {
            emit("nextReady");
          })
          .onSuccess(() => {
            const result = captchaObj.getValidate();
            emit("success", result);
          })
          .onError((error: unknown) => {
            emit("error", error);
          })
          .onClose(() => {
            emit("close");
          });
      }
    );
  });
}

defineExpose({
  getInstance,
  showCaptcha,
  reset,
  destroy,
});

onMounted(async () => {
  await loadScript(props.scriptSrc, { defer: "true" });
  await initCaptcha();
});

watch(
  () => [props.captchaId, props.product, props.language, props.options],
  async () => {
    await initCaptcha();
  },
  { deep: true }
);

onUnmounted(() => {
  destroy();
});
</script>

<template>
  <div ref="rootRef"></div>
</template>

注意事项

  • bind 模式下,官方文档说明 appendTo 无效,应通过 showCaptcha() 主动触发验证。
  • onSuccess 中通过 getValidate() 获取结果,再交给父组件处理业务请求。
  • 如果监听 props.options,需要留意对象引用变化是否会导致不必要的重建。

六、事件透传与父组件交互 重要

6.1 为什么要把官方事件包装成 Vue 事件

概念说明

老师在原文里提到 onReadyonNextReady 这些事件没有逐个测试。对于组件封装来说,更合理的做法是把这些底层事件包装为 Vue 组件事件,然后交给父组件按需监听。

语法/用法

建议映射关系:

  • captchaObj.onReady -> emit("ready")
  • captchaObj.onNextReady -> emit("nextReady")
  • captchaObj.onSuccess -> emit("success", result)
  • captchaObj.onError -> emit("error", error)
  • captchaObj.onClose -> emit("close")

代码示例

Vue SFC
<GeetestCaptcha
  captcha-id="demo"
  product="bind"
  @ready="handleReady"
  @success="handleSuccess"
  @error="handleError"
/>

注意事项

  • 不建议让父组件直接操作极验底层 API 作为默认交互方式。
  • 默认使用事件驱动,defineExpose 只作为补充能力。

七、props 变化后是否需要重新初始化 重要

7.1 为什么需要 watch

概念说明

课程最后提到:如果组件接收到的 props 发生变化,可以重新初始化整个实例。这个建议是合理的,因为极验初始化参数通常在实例创建后不会自动同步更新。

语法/用法

适合触发重建的参数通常包括:

  • captchaId
  • product
  • language
  • 其他影响初始化行为的配置项

代码示例

ts
watch(
  () => [props.captchaId, props.product, props.language],
  async () => {
    await initCaptcha();
  }
);

注意事项

  • 不是所有 props 变化都要重建,避免因为无关属性变化导致频繁销毁和重建。
  • 如果 options 是大对象,建议由业务层保证引用稳定,或者在内部做更细粒度比较。

八、联调中的典型问题 重要

8.1 initGeetest4 is not defined

概念说明

这通常说明 SDK 还没加载完成,你就开始初始化了。

解决思路

  • 确认脚本是否真的插入到页面
  • 确认脚本请求是否成功
  • 确认初始化逻辑是否等待脚本加载完成

8.2 动态插入脚本后还是找不到

概念说明

如果 querySelector 的选择器写错,例如少了 URL 引号,脚本判断逻辑就可能失效,导致重复插入或错误判断。

代码示例

ts
// 推荐
document.querySelector(`script[src="${src}"]`);

8.3 销毁后界面残留

概念说明

只调用实例的 destroy() 有时还不够,组件自身保存的引用和宿主容器内容也要一起清理。

注意事项

  • 销毁后应将 instanceRef.value = null
  • 必要时清空宿主节点内容

代码实战案例

需求描述

将极验 GT4 封装成一个可复用的 Vue 3 组件,要求支持:

  • 自动加载 gt4.js
  • 初始化极验实例
  • 对外暴露 getInstanceshowCaptcharesetdestroy
  • 将底层事件包装成 Vue 事件
  • 在组件卸载时自动销毁实例

完整实现代码

脚本加载工具:

ts
// utils/loadScript.ts
export function loadScript(
  src: string,
  attrs: Record<string, string> = {}
) {
  return new Promise<void>((resolve, reject) => {
    const existed = document.querySelector(
      `script[src="${src}"]`
    ) as HTMLScriptElement | null;

    if (existed) {
      if (existed.dataset.loaded === "true") {
        resolve();
        return;
      }

      existed.addEventListener("load", () => resolve(), { once: true });
      existed.addEventListener(
        "error",
        () => reject(new Error(`load script failed: ${src}`)),
        { once: true }
      );
      return;
    }

    const script = document.createElement("script");
    script.src = src;

    Object.entries(attrs).forEach(([key, value]) => {
      script.setAttribute(key, value);
    });

    script.addEventListener(
      "load",
      () => {
        script.dataset.loaded = "true";
        resolve();
      },
      { once: true }
    );

    script.addEventListener(
      "error",
      () => reject(new Error(`load script failed: ${src}`)),
      { once: true }
    );

    document.head.appendChild(script);
  });
}

Vue 组件:

Vue SFC
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from "vue";
import { loadScript } from "@/utils/loadScript";

const props = withDefaults(
  defineProps<{
    captchaId: string;
    product?: "popup" | "float" | "bind";
  }>(),
  {
    product: "popup",
  }
);

const emit = defineEmits<{
  success: [result: Record<string, string>];
  error: [error: unknown];
}>();

const rootRef = ref<HTMLElement | null>(null);
const instanceRef = ref<any>(null);

function getInstance() {
  return instanceRef.value;
}

function showCaptcha() {
  instanceRef.value?.showCaptcha?.();
}

function reset() {
  instanceRef.value?.reset?.();
}

function destroy() {
  instanceRef.value?.destroy?.();
  instanceRef.value = null;
  if (rootRef.value) rootRef.value.innerHTML = "";
}

async function initCaptcha() {
  await loadScript("https://static.geetest.com/v4/gt4.js", {
    defer: "true",
  });

  if (!rootRef.value || typeof window.initGeetest4 !== "function") {
    return;
  }

  window.initGeetest4(
    {
      captchaId: props.captchaId,
      product: props.product,
    },
    (captchaObj: any) => {
      instanceRef.value = captchaObj;

      if (props.product !== "bind") {
        captchaObj.appendTo(rootRef.value);
      }

      captchaObj
        .onSuccess(() => {
          emit("success", captchaObj.getValidate());
        })
        .onError((error: unknown) => {
          emit("error", error);
        });
    }
  );
}

defineExpose({
  getInstance,
  showCaptcha,
  reset,
  destroy,
});

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

onUnmounted(() => {
  destroy();
});
</script>

<template>
  <div ref="rootRef"></div>
</template>

父组件调用示例:

Vue SFC
<script setup lang="ts">
import { ref } from "vue";
import GeetestCaptcha from "./GeetestCaptcha.vue";

const captchaRef = ref<InstanceType<typeof GeetestCaptcha> | null>(null);

function handleSuccess(result: Record<string, string>) {
  console.log("验证码结果", result);
}

function handleDestroy() {
  captchaRef.value?.destroy();
}
</script>

<template>
  <GeetestCaptcha
    ref="captchaRef"
    captcha-id="你的-captchaId"
    @success="handleSuccess"
  />

  <button @click="handleDestroy">销毁实例</button>
</template>

代码逐行解析

  1. loadScript 用来保证 gt4.js 只加载一次,并且只有加载完成后才继续执行。
  2. 组件通过 rootRef 挂载极验 UI。
  3. instanceRef 用于保存底层极验实例,供重置、销毁和显式触发时复用。
  4. defineExposegetInstanceshowCaptcharesetdestroy 暴露给父组件。
  5. onMounted 中初始化极验,而不是在模块顶层直接执行。
  6. product !== "bind" 时才调用 appendTo,因为 bind 模式由 showCaptcha() 主动触发。
  7. onSuccess 里通过 getValidate() 获取校验结果,再抛给父组件处理。
  8. onUnmounted 里调用 destroy(),避免内存泄漏和残留 DOM。

常见问题与解决方案

问题原因分析解决方案
initGeetest4 is not defined脚本尚未加载完成,就开始初始化用 Promise 化的 loadScript,在 await 成功后再初始化
组件能渲染,但 bind 模式点了没反应bind 模式下没有调用 showCaptcha()由父组件或内部按钮在合适时机调用 showCaptcha()
销毁后页面上还有残留验证框只销毁了实例,没有清空引用和宿主节点destroy() 后同步清空 instanceRef 与容器内容
props 改了,但验证码没有变化初始化参数只在实例创建时生效,后续不会自动同步监听关键 props 并在变化后重建实例
动态脚本重复插入没有先判断页面中是否已存在同一 scriptloadScript 中先查找 script[src="..."]
组件对外暴露太多内部能力封装边界不清晰,父组件直接操纵底层实例优先事件驱动,只暴露少量必要方法

学习要点总结

  1. 把极验封装成 Vue 组件,核心是统一输入、输出和生命周期,而不是简单搬运代码。
  2. destroyreset 是极验实例能力,getInstance 通常是你自己组件额外暴露的方法。
  3. 动态脚本加载一定要处理“已加载复用”和“加载完成后再初始化”这两件事。
  4. 原文中的定时器轮询方案能用,但工程上更推荐 Promise 化脚本加载。
  5. bind 模式和 popupfloat 的接入方式不同,不能混用 appendTo 逻辑。

术语纠正与内容优化

  • 原文中的 VU 组件,结合上下文应为 Vue 组件
  • 原文中的 proms,规范写法应为 props
  • 原文中的 GS,这里应理解为 JS 或极验的 gt4.js 脚本。
  • 原文中的 unit gay test four,规范写法应为 initGeetest4
  • 原文中的 date reset,应理解为 reset
  • 原文中的 use interval,本质上是 setInterval 轮询思路。
  • 原文中的 already,结合上下文更可能是监听 onReady 一类的初始化完成事件,而不是正式 API 名称。
  • 原文里的轮询初始化方案可以工作,但更推荐改造成 Promise 式的脚本加载,以减少定时器和时序问题。

延伸学习资源

参考说明

本文结合课程原文整理,并对其中的口语化表述、实例方法名称和封装思路做了规范化修正。initGeetest4appendToshowCaptcha()getValidate()reset()destroy()onReady()onNextReady() 等信息,基于 2026-03-29 可访问的极验 GT4 官方 Web API 文档进行了校对;代码示例中的 Vue 组件实现属于工程化整理版本,重点体现更稳妥的封装思路,并非课程原文逐字转写。