将极验 GT4 封装为可复用的 Vue 3 组件,是工程化落地的关键一步。本文详解组件的输入输出设计、动态脚本加载方案、生命周期管理、事件透传机制,以及 props 变化后的实例重建策略。
<script setup>、defineProps、defineExpose)<script> 插入与 Promise 化加载Props(输入):
type GeetestProduct = "popup" | "float" | "bind";
interface GeetestCaptchaProps {
captchaId: string;
product?: GeetestProduct;
language?: string;
scriptSrc?: string;
options?: Record<string, unknown>;
}Emits(事件输出):
defineEmits<{
ready: [];
success: [result: Record<string, string>];
error: [error: unknown];
close: [];
}>();Expose(方法暴露):
defineExpose({
getInstance,
showCaptcha,
reset,
destroy,
});组件不应假设业务方已全局引入 gt4.js。如果页面未加载 SDK,初始化时会报 initGeetest4 is not defined。
export function loadScript(
src: string,
attrs: Record<string, string> = {}
): Promise<void> {
return new Promise((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);
});
}设计要点:
await 成功后再初始化<script setup lang="ts">
import { nextTick, onMounted, onUnmounted, ref, watch } from "vue";
import { loadScript } from "@/utils/loadScript";
type GeetestProduct = "popup" | "float" | "bind";
const props = withDefaults(
defineProps<{
captchaId: string;
product?: GeetestProduct;
language?: string;
scriptSrc?: string;
options?: Record<string, unknown>;
}>(),
{
product: "popup",
language: "zho",
scriptSrc: "https://static.geetest.com/v4/gt4.js",
options: () => ({}),
}
);
const emit = defineEmits<{
ready: [];
success: [result: Record<string, string>];
error: [error: unknown];
close: [];
}>();
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 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;
// bind 模式不需要 appendTo
if (props.product !== "bind") {
captchaObj.appendTo(rootRef.value);
}
captchaObj
.onReady(() => {
emit("ready");
resolve();
})
.onSuccess(() => {
emit("success", captchaObj.getValidate());
})
.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],
() => initCaptcha()
);
onUnmounted(() => destroy());
</script>
<template>
<div ref="rootRef"></div>
</template><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);
}
</script>
<template>
<GeetestCaptcha
ref="captchaRef"
captcha-id="你的-captchaId"
product="popup"
@success="handleSuccess"
/>
</template>| 模式 | appendTo | 触发方式 | 封装差异 |
|---|---|---|---|
| popup | 需要 | 自动弹出 | 挂载到 rootRef |
| bind | 不需要 | 手动 showCaptcha() | 父组件通过 ref 调用 |
| float | 需要 | 按钮附近浮层 | 关注容器布局 |
| 极验原生事件 | 组件事件 | 说明 |
|---|---|---|
| onReady | ready | 组件就绪 |
| onSuccess | success | 验证成功,附带结果 |
| onError | error | 验证错误 |
| onClose | close | 关闭验证框 |
| 问题 | 原因 | 解决方案 |
|---|---|---|
initGeetest4 is not defined | SDK 未加载完就初始化 | 用 Promise 化 loadScript,await 后再初始化 |
| bind 模式点按钮没反应 | 未调用 showCaptcha() | 父组件通过 ref 在合适时机调用 |
| 销毁后页面残留验证框 | 只销毁实例未清空容器 | destroy() 后同步清空 innerHTML 和引用 |
| props 改了验证码没变化 | 初始化参数只在创建时生效 | watch 关键 props 并重建实例 |
| 脚本重复插入 | 未判断页面是否已存在 | loadScript 中先查 script[src="..."] |
<script setup>:https://cn.vuejs.org/api/sfc-script-setup.html