在我上一篇文章中,我向您展示了如何通过语义输入类型(例如,<input type="email">
)和验证属性(例如 required
和 pattern
)的组合来使用原生浏览器表单验证。
虽然这种方法非常简单且轻量级,但它也有一些缺点。
- 您可以使用
:invalid
伪选择器来设置包含错误的字段的样式,但无法设置错误消息本身的样式。 - 浏览器之间的行为也不一致。
Christian Holst 和 Luke Wroblewski(分别)进行的用户研究发现,当用户离开字段时显示错误,并在问题得到解决之前保持错误持续存在,可以提供最佳和最快的用户体验。
不幸的是,没有一个浏览器原生支持这种行为。但是,有一种方法可以在不依赖大型 JavaScript 表单验证库的情况下获得这种行为。
文章系列
- HTML 中的约束验证
- JavaScript 中的约束验证 API(您当前所在位置!)
- 有效性状态 API polyfill
- 验证 MailChimp 订阅表单
约束验证 API
除了 HTML 属性外,浏览器原生约束验证还提供了一个 JavaScript API,我们可以使用它来自定义表单验证行为。
API 公开了几种不同的方法,但最强大的方法,有效性状态,允许我们在脚本中使用浏览器的字段验证算法,而不是编写我们自己的算法。
在本文中,我将向您展示如何使用有效性状态来自定义表单验证错误消息的行为、外观和内容。
有效性状态
validity
属性提供了一组关于表单字段的信息,以布尔值(true
/false
)的形式呈现。
var myField = document.querySelector('input[type="text"]');
var validityState = myField.validity;
返回的对象包含以下属性
valid
– 当字段通过验证时为true
。valueMissing
– 当字段为空但为必填字段时为true
。typeMismatch
– 当字段type
为email
或url
但输入的value
不是正确的类型时为true
。tooShort
– 当字段包含minLength
属性且输入的value
短于该长度时为true
。tooLong
– 当字段包含maxLength
属性且输入的value
长于该长度时为true
。patternMismatch
– 当字段包含pattern
属性且输入的value
与模式不匹配时为true
。badInput
– 当输入type
为number
且输入的value
不是数字时为true
。stepMismatch
– 当字段具有step
属性且输入的value
不符合步长值时为true
。rangeOverflow
– 当字段具有max
属性且输入的数字value
大于最大值时为true
。rangeUnderflow
– 当字段具有min
属性且输入的数字value
小于最小值时为true
。
通过将 validity
属性与我们的输入类型和 HTML 验证属性结合使用,我们可以构建一个健壮的表单验证脚本,该脚本可以使用相对少量的 JavaScript 提供出色的用户体验。
让我们开始吧!
禁用原生表单验证
由于我们正在编写验证脚本,因此我们希望通过向表单添加 novalidate
属性来禁用原生浏览器验证。我们仍然可以使用约束验证 API——我们只是想阻止原生错误消息显示。
作为最佳实践,我们应该使用 JavaScript 添加此属性,以便如果我们的脚本出现错误或加载失败,原生浏览器表单验证仍然可以工作。
// Add the novalidate attribute when the JS loads
var forms = document.querySelectorAll('form');
for (var i = 0; i < forms.length; i++) {
forms[i].setAttribute('novalidate', true);
}
可能有一些表单您不希望进行验证(例如,每个页面上显示的搜索表单)。与其将我们的验证脚本应用于所有表单,不如将其仅应用于具有 .validate
类的表单。
// Add the novalidate attribute when the JS loads
var forms = document.querySelectorAll('.validate');
for (var i = 0; i < forms.length; i++) {
forms[i].setAttribute('novalidate', true);
}
查看 CodePen 上 Chris Ferdinandi (@cferdinandi) 编写的 表单验证:以编程方式添加 `novalidate`。
用户离开字段时检查有效性
每当用户离开字段时,我们都希望检查它是否有效。为此,我们将设置一个事件监听器。
与其向每个表单字段添加监听器,不如使用称为事件冒泡(或事件传播)的技术来监听所有 blur
事件。
// Listen to all blur events
document.addEventListener('blur', function (event) {
// Do something on blur...
}, true);
您会注意到 addEventListener
中的最后一个参数设置为 true
。此参数称为 useCapture
,通常设置为 false
。blur
事件不会像 click
等事件那样冒泡。将此参数设置为 true 允许我们捕获所有 blur
事件,而不仅仅是我们正在监听的元素上发生的事件。
接下来,我们要确保模糊的元素是具有 .validate
类的表单中的字段。我们可以使用 event.target
获取模糊的元素,并通过调用 event.target.form
获取其父表单。然后,我们将使用 classList
检查表单是否具有验证类。
如果它有,我们可以检查字段有效性。
// Listen to all blur events
document.addEventListener('blur', function (event) {
// Only run if the field is in a form to be validated
if (!event.target.form.classList.contains('validate')) return;
// Validate the field
var error = event.target.validity;
console.log(error);
}, true);
如果 error
为 true
,则字段有效。否则,存在错误。
查看 CodePen 上 Chris Ferdinandi (@cferdinandi) 编写的 表单验证:在失去焦点时验证。
获取错误
一旦我们知道存在错误,了解错误的实际内容就很有帮助。我们可以使用其他有效性状态属性来获取该信息。
由于我们需要检查每个属性,因此此代码可能会变得有点长。让我们为此设置一个单独的函数,并将我们的字段传递给它。
// Validate the field
var hasError = function (field) {
// Get the error
};
// Listen to all blur events
document.addEventListner('blur', function (event) {
// Only run if the field is in a form to be validated
if (!event.target.form.classList.contains('validate')) return;
// Validate the field
var error = hasError(event.target);
}, true);
有一些字段类型我们希望忽略:禁用的字段、file
和 reset
输入,以及 submit
输入和按钮。如果字段不是其中之一,让我们获取其有效性。
// Validate the field
var hasError = function (field) {
// Don't validate submits, buttons, file and reset inputs, and disabled fields
if (field.disabled || field.type === 'file' || field.type === 'reset' || field.type === 'submit' || field.type === 'button') return;
// Get validity
var validity = field.validity;
};
如果没有错误,我们将返回 null
。否则,我们将检查每个有效性状态属性,直到找到错误。
当我们找到匹配项时,我们将返回一个包含错误的字符串。如果没有任何属性为 true
但 validity
为 false,我们将返回一个通用的“兜底”错误消息(我无法想象这种情况会发生,但做好计划以防意外是好的)。
// Validate the field
var hasError = function (field) {
// Don't validate submits, buttons, file and reset inputs, and disabled fields
if (field.disabled || field.type === 'file' || field.type === 'reset' || field.type === 'submit' || field.type === 'button') return;
// Get validity
var validity = field.validity;
// If valid, return null
if (validity.valid) return;
// If field is required and empty
if (validity.valueMissing) return 'Please fill out this field.';
// If not the right type
if (validity.typeMismatch) return 'Please use the correct input type.';
// If too short
if (validity.tooShort) return 'Please lengthen this text.';
// If too long
if (validity.tooLong) return 'Please shorten this text.';
// If number input isn't a number
if (validity.badInput) return 'Please enter a number.';
// If a number value doesn't match the step interval
if (validity.stepMismatch) return 'Please select a valid value.';
// If a number field is over the max
if (validity.rangeOverflow) return 'Please select a smaller value.';
// If a number field is below the min
if (validity.rangeUnderflow) return 'Please select a larger value.';
// If pattern doesn't match
if (validity.patternMismatch) return 'Please match the requested format.';
// If all else fails, return a generic catchall error
return 'The value you entered for this field is invalid.';
};
这是一个良好的开端,但我们可以进行一些额外的解析以使我们的一些错误更有用。对于 typeMismatch
,我们可以检查它是否应该是 email
或 url
并相应地自定义错误。
// If not the right type
if (validity.typeMismatch) {
// Email
if (field.type === 'email') return 'Please enter an email address.';
// URL
if (field.type === 'url') return 'Please enter a URL.';
}
如果字段值过长或过短,我们可以找出它应该有多长或多短以及它实际有多长或多短。然后,我们可以将这些信息包含在错误中。
// If too short
if (validity.tooShort) return 'Please lengthen this text to ' + field.getAttribute('minLength') + ' characters or more. You are currently using ' + field.value.length + ' characters.';
// If too long
if (validity.tooLong) return 'Please short this text to no more than ' + field.getAttribute('maxLength') + ' characters. You are currently using ' + field.value.length + ' characters.';
如果数字字段超过或低于允许的范围,我们可以将该最小或最大允许值包含在我们的错误中。
// If a number field is over the max
if (validity.rangeOverflow) return 'Please select a value that is no more than ' + field.getAttribute('max') + '.';
// If a number field is below the min
if (validity.rangeUnderflow) return 'Please select a value that is no less than ' + field.getAttribute('min') + '.';
如果存在 pattern
不匹配且字段具有 title
,我们可以将其用作我们的错误,就像原生浏览器行为一样。
// If pattern doesn't match
if (validity.patternMismatch) {
// If pattern info is included, return custom error
if (field.hasAttribute('title')) return field.getAttribute('title');
// Otherwise, generic error
return 'Please match the requested format.';
}
这是我们 hasError()
函数的完整代码。
// Validate the field
var hasError = function (field) {
// Don't validate submits, buttons, file and reset inputs, and disabled fields
if (field.disabled || field.type === 'file' || field.type === 'reset' || field.type === 'submit' || field.type === 'button') return;
// Get validity
var validity = field.validity;
// If valid, return null
if (validity.valid) return;
// If field is required and empty
if (validity.valueMissing) return 'Please fill out this field.';
// If not the right type
if (validity.typeMismatch) {
// Email
if (field.type === 'email') return 'Please enter an email address.';
// URL
if (field.type === 'url') return 'Please enter a URL.';
}
// If too short
if (validity.tooShort) return 'Please lengthen this text to ' + field.getAttribute('minLength') + ' characters or more. You are currently using ' + field.value.length + ' characters.';
// If too long
if (validity.tooLong) return 'Please shorten this text to no more than ' + field.getAttribute('maxLength') + ' characters. You are currently using ' + field.value.length + ' characters.';
// If number input isn't a number
if (validity.badInput) return 'Please enter a number.';
// If a number value doesn't match the step interval
if (validity.stepMismatch) return 'Please select a valid value.';
// If a number field is over the max
if (validity.rangeOverflow) return 'Please select a value that is no more than ' + field.getAttribute('max') + '.';
// If a number field is below the min
if (validity.rangeUnderflow) return 'Please select a value that is no less than ' + field.getAttribute('min') + '.';
// If pattern doesn't match
if (validity.patternMismatch) {
// If pattern info is included, return custom error
if (field.hasAttribute('title')) return field.getAttribute('title');
// Otherwise, generic error
return 'Please match the requested format.';
}
// If all else fails, return a generic catchall error
return 'The value you entered for this field is invalid.';
};
在下面的 CodePen 中自己尝试一下。
查看 CodePen 上 Chris Ferdinandi (@cferdinandi) 编写的 表单验证:获取错误。
显示错误消息
一旦我们得到错误,我们可以在字段下方显示它。我们将创建一个showError()
函数来处理此问题,并将我们的字段和错误作为参数传递。然后,我们将在我们的事件监听器中调用它。
// Show the error message
var showError = function (field, error) {
// Show the error message...
};
// Listen to all blur events
document.addEventListener('blur', function (event) {
// Only run if the field is in a form to be validated
if (!event.target.form.classList.contains('validate')) return;
// Validate the field
var error = hasError(event.target);
// If there's an error, show it
if (error) {
showError(event.target, error);
}
}, true);
在我们的showError
函数中,我们将执行以下几件事
- 我们将向带有错误的字段添加一个类,以便我们可以对其进行样式设置。
- 如果错误消息已存在,我们将使用新文本更新它。
- 否则,我们将创建一个消息并将其注入到字段之后的 DOM 中。
我们还将使用字段 ID 为消息创建唯一的 ID,以便我们以后可以再次找到它(如果不存在 ID,则回退到字段name
)。
var showError = function (field, error) {
// Add error class to field
field.classList.add('error');
// Get field id or name
var id = field.id || field.name;
if (!id) return;
// Check if error message field already exists
// If not, create one
var message = field.form.querySelector('.error-message#error-for-' + id );
if (!message) {
message = document.createElement('div');
message.className = 'error-message';
message.id = 'error-for-' + id;
field.parentNode.insertBefore( message, field.nextSibling );
}
// Update error message
message.innerHTML = error;
// Show error message
message.style.display = 'block';
message.style.visibility = 'visible';
};
为了确保屏幕阅读器和其他辅助技术知道我们的错误消息与我们的字段相关联,我们还需要添加aria-describedby
角色。
var showError = function (field, error) {
// Add error class to field
field.classList.add('error');
// Get field id or name
var id = field.id || field.name;
if (!id) return;
// Check if error message field already exists
// If not, create one
var message = field.form.querySelector('.error-message#error-for-' + id );
if (!message) {
message = document.createElement('div');
message.className = 'error-message';
message.id = 'error-for-' + id;
field.parentNode.insertBefore( message, field.nextSibling );
}
// Add ARIA role to the field
field.setAttribute('aria-describedby', 'error-for-' + id);
// Update error message
message.innerHTML = error;
// Show error message
message.style.display = 'block';
message.style.visibility = 'visible';
};
设置错误消息的样式
我们可以使用.error
和.error-message
类来设置表单字段和错误消息的样式。
作为一个简单的示例,您可能希望在带有错误的字段周围显示红色边框,并将错误消息设置为红色和斜体。
.error {
border-color: red;
}
.error-message {
color: red;
font-style: italic;
}
查看 Chris Ferdinandi 在 CodePen 上的笔 表单验证:显示错误 (@cferdinandi)。
隐藏错误消息
一旦我们显示错误,您的访客将(希望)修复它。一旦字段验证通过,我们需要删除错误消息。让我们创建另一个函数removeError()
,并将字段作为参数传递。我们也将从事件监听器中调用此函数。
// Remove the error message
var removeError = function (field) {
// Remove the error message...
};
// Listen to all blur events
document.addEventListener('blur', function (event) {
// Only run if the field is in a form to be validated
if (!event.target.form.classList.contains('validate')) return;
// Validate the field
var error = event.target.validity;
// If there's an error, show it
if (error) {
showError(event.target, error);
return;
}
// Otherwise, remove any existing error message
removeError(event.target);
}, true);
在removeError()
中,我们希望
- 从我们的字段中删除错误类。
- 从字段中删除
aria-describedby
角色。 - 隐藏 DOM 中任何可见的错误消息。
因为我们可能在一个页面上有多个表单,并且这些表单有可能具有相同名称或 ID 的字段(即使这无效,但确实会发生),我们将使用querySelector
限制我们对表单中字段所在表单的错误消息的搜索,而不是整个文档。
// Remove the error message
var removeError = function (field) {
// Remove error class to field
field.classList.remove('error');
// Remove ARIA role from the field
field.removeAttribute('aria-describedby');
// Get field id or name
var id = field.id || field.name;
if (!id) return;
// Check if an error message is in the DOM
var message = field.form.querySelector('.error-message#error-for-' + id + '');
if (!message) return;
// If so, hide it
message.innerHTML = '';
message.style.display = 'none';
message.style.visibility = 'hidden';
};
查看 Chris Ferdinandi 在 CodePen 上的笔 表单验证:修复后删除错误 (@cferdinandi)。
如果字段是单选按钮或复选框,我们需要更改我们向 DOM 添加错误消息的方式。
对于这些类型的输入,字段标签通常出现在字段之后,或者完全包含它。此外,如果单选按钮是某个组的一部分,我们希望错误出现在该组之后,而不是仅出现在单选按钮之后。
查看 Chris Ferdinandi 在 CodePen 上的笔 表单验证:单选按钮和复选框的问题 (@cferdinandi)。
首先,我们需要修改我们的showError()
方法。如果字段type
是radio
并且它具有name
,我们希望获取所有具有相同name
的单选按钮(即组中的所有其他单选按钮),并将我们的field
变量重置为组中的最后一个。
// Show the error message
var showError = function (field, error) {
// Add error class to field
field.classList.add('error');
// If the field is a radio button and part of a group, error all and get the last item in the group
if (field.type === 'radio' && field.name) {
var group = document.getElementsByName(field.name);
if (group.length > 0) {
for (var i = 0; i < group.length; i++) {
// Only check fields in current form
if (group[i].form !== field.form) continue;
group[i].classList.add('error');
}
field = group[group.length - 1];
}
}
...
};
当我们要将消息注入到 DOM 中时,我们首先要检查字段类型是否为radio
或checkbox
。如果是,我们希望获取字段标签,并将我们的消息注入到它的后面,而不是字段本身的后面。
// Show the error message
var showError = function (field, error) {
...
// Check if error message field already exists
// If not, create one
var message = field.form.querySelector('.error-message#error-for-' + id );
if (!message) {
message = document.createElement('div');
message.className = 'error-message';
message.id = 'error-for-' + id;
// If the field is a radio button or checkbox, insert error after the label
var label;
if (field.type === 'radio' || field.type ==='checkbox') {
label = field.form.querySelector('label[for="' + id + '"]') || field.parentNode;
if (label) {
label.parentNode.insertBefore( message, label.nextSibling );
}
}
// Otherwise, insert it after the field
if (!label) {
field.parentNode.insertBefore( message, field.nextSibling );
}
}
...
};
当我们要删除错误时,我们也需要类似地检查字段是否为某个组的一部分的单选按钮,如果是,则使用该组中的最后一个单选按钮来获取错误消息的 ID。
// Remove the error message
var removeError = function (field) {
// Remove error class to field
field.classList.remove('error');
// If the field is a radio button and part of a group, remove error from all and get the last item in the group
if (field.type === 'radio' && field.name) {
var group = document.getElementsByName(field.name);
if (group.length > 0) {
for (var i = 0; i < group.length; i++) {
// Only check fields in current form
if (group[i].form !== field.form) continue;
group[i].classList.remove('error');
}
field = group[group.length - 1];
}
}
...
};
查看 Chris Ferdinandi 在 CodePen 上的笔 表单验证:修复单选按钮和复选框 (@cferdinandi)。
提交时检查所有字段
当访客提交我们的表单时,我们应该首先验证表单中的每个字段,并在任何无效字段上显示错误消息。我们还应该将第一个带有错误的字段聚焦,以便访客可以立即采取措施进行更正。
我们将通过为submit
事件添加一个监听器来做到这一点。
// Check all fields on submit
document.addEventListener('submit', function (event) {
// Validate all fields...
}, false);
如果表单具有.validate
类,我们将获取每个字段,循环遍历每个字段,并检查错误。我们将找到的第一个无效字段存储到一个变量中,并在完成后将其聚焦。如果未找到错误,则表单可以正常提交。
// Check all fields on submit
document.addEventListener('submit', function (event) {
// Only run on forms flagged for validation
if (!event.target.classList.contains('validate')) return;
// Get all of the form elements
var fields = event.target.elements;
// Validate each field
// Store the first field with an error to a variable so we can bring it into focus later
var error, hasErrors;
for (var i = 0; i < fields.length; i++) {
error = hasError(fields[i]);
if (error) {
showError(fields[i], error);
if (!hasErrors) {
hasErrors = fields[i];
}
}
}
// If there are errrors, don't submit form and focus on first element with error
if (hasErrors) {
event.preventDefault();
hasErrors.focus();
}
// Otherwise, let the form submit normally
// You could also bolt in an Ajax form submit process here
}, false);
查看 Chris Ferdinandi 在 CodePen 上的笔 表单验证:提交时验证 (@cferdinandi)。
整合所有内容
我们完成的脚本仅重 6kb(缩小后为 2.7kb)。您可以在 GitHub 上下载插件版本。
它适用于所有现代浏览器,并提供对 IE10 及更高版本的支持。但是,有一些浏览器问题……
- 因为我们不能拥有美好的事物,并非每个浏览器都支持每个 Validity State 属性。
- 当然,Internet Explorer 是主要的违规者,尽管 Edge 缺乏对
tooLong
的支持,即使 IE10+ 支持它。真是难以理解。
好消息是:使用轻量级 polyfill(5kb,缩小后为 2.7kb),我们可以将浏览器支持扩展到 IE9,并将缺失的属性添加到部分支持的浏览器,而无需触及任何核心代码。
IE9 支持有一个例外:单选按钮。IE9 不支持 CSS3 选择器(如[name="' + field.name + '"]
)。我们使用它来确保在组中至少选择了一个单选按钮。IE9 将始终返回错误。
我将在下一篇文章中向您展示如何创建此 polyfill。
文章系列
- HTML 中的约束验证
- JavaScript 中的约束验证 API(您当前所在位置!)
- 有效性状态 API polyfill
- 验证 MailChimp 订阅表单
很棒的文章!尤其是在我目前正在重新设计我公司应用程序页面,并且高层领导特别要求使用活动表单验证的情况下。
不过有一点,当您将鼠标悬停在电子邮件或 URL 框中的错误文本上时,它会显示替代文本,说明浏览器似乎认为当前文本哪里出错了(例如“缺少 .com”或“@ 后的文本无效”)。
除了文本似乎总是错误的。它不断告诉用户修复不存在的问题,这只会让任何不幸地将鼠标悬停在上面一秒钟的人感到困惑。
您知道如何在这些情况下禁用替代文本,或者可能导致它给出这些错误消息的原因吗?
这确实很烦人,不是吗?这让我觉得是糟糕的规范设计,但我们也可以将模式错误重写为类似
您的 URL 必须包含 TLD(例如 .com)
的内容。可能仍然有点令人困惑。
我们可以使用的另一种方法,尽管我不一定推荐它,是通过编程方式将所有
title
属性转换为数据属性。类似这样然后在我们的验证脚本中,我们将使用
data-title
而不是title
。您可能想知道为什么我不一开始就使用
data-title
,而是用 JS 替换它。如果我们的 JavaScript 失败并且本机 HTML 约束验证接管了工作,我们仍然希望我们的title
属性在那里。哇!如此多的有用信息!感谢您整理这些内容!
不客气!
我很好奇这将如何与只能在服务器端检查的错误很好地配合使用,例如注册的唯一电子邮件或无效的邮政编码。您是否会内联显示这些错误,并希望用户在第一次“失焦”之前解决它们,或者全部集中在表单上方的通知框中。
其次,我觉得这对于不太复杂的表单来说非常棒,但我更常需要一个验证库来处理字段之间的依赖关系,比如“如果选中该复选框,则此项为必填”或“此日期需要早于那个日期”。我之前成功地使用过 Nette Forms 以及配套的 nette-forms.js。它在前端也相当小巧,并且独立于 jQuery,解决了字段间依赖性的问题。由于我不总是使用 PHP,所以我想了解一下其他的替代方案。
我认为它增强了这些类型的错误。在一个理想的世界里,你可能能够在失去焦点时向服务器发出 Ajax 请求来检查它们,并在适用时显示错误。
替代方法
不过,当有人进行更正但错误没有消失时,后一种选择可能会让人感到困惑。
对于您描述的更复杂的情况,您可以编写自己的小型增强函数来处理它(例如,这里是如何使用 JS 检查日期有效性,包括考虑闰年)。或者,使用更强大的库是完全合理的。
非常感谢 Chris。非常有用的文章
谢谢 Louis!
如果我比较懒,只想显示默认的错误消息,但在失去焦点时而不是在提交时显示,最简单的方法是什么?
validate.init({selector: 'form'})
将其应用于所有表单。如果你想使用原生样式显示原生错误消息,那么……可以使用
reportValidity()
方法,但它会将焦点返回到字段,从而造成一种“加州旅馆”式的困境,让你困在任何带有错误的字段上,所以……这不是一个好的选择。在
showError
函数中单选按钮代码片段中,而不是使用你可能可以使用
然后去掉
if
条件在循环中。
我最初使用的是那个,但在下一篇文章中使用 polyfill 将支持回退到 IE9 时,它失败了。我切换到
getElementsByName
+ 表单检查以获得更好的支持。嗯?!非常奇怪!据我所知,IE9 支持 querySelectorAll。
它确实支持,IE8 也支持。我确定这是一些简单而愚蠢的事情,但我认为在截止日期前多一行代码比调试要好。=)
如何与 ajax 结合使用?提前感谢
嗨,Dimitrian!Ajax 表单提交在不同的表单之间差异很大,具体取决于接收表单数据的服务器的设置方式。不过,举个例子,请查看第 4 部分,我介绍了使用 Ajax 提交 MailChimp 订阅表单。
如果之前已经涵盖过,我深感抱歉,但是否有更强大的方法来验证电子邮件地址以确保电子邮件地址确实存在?
我有一个表单,其中包含一个蜜罐来避免被机器人发送电子邮件,但令人惊讶的是,我实际上收到了很多来自使用虚假电子邮件地址的人类的电子邮件。
我非常喜欢这篇关于表单验证的文章!谢谢!
我实际上并不觉得这有多么令人惊讶(可能是因为我自己也遇到过这种情况)。
为此有一些选择
如果是我,我会选择选项 1,或者认为这只是做生意的一部分。=(
感谢您对选项的详细分解。我会研究一下,看看我能想出什么。我明白要让某些东西(库或插件)搜索数百万个电子邮件地址以查看正在使用的地址是否真实是多么困难。然而,现在几乎所有东西都有一个应用程序,所以我认为这并非不可能。