做业务系统时,用户输入单引号(
')会直接导致 SQL 报错甚至注入风险。后端当然要做转义,但前端拦截能让体验更好、更安全。本文分享一套 Vue3 的完整解决方案:自定义指令 精准控制 + 全局插件一键覆盖,两种方式按需选用。
方案一:自定义指令 v-input-no-single-quote
适合需要精准控制某些输入框的场景。
创建指令文件
src/directives/inputNoSingleQuote.js
js
const globalNoSingleQuoteConfig = {
defaultEnabled: true,
processedInputs: new WeakSet(),
disabledInputs: new WeakSet()
};
export default {
mounted(el, binding) {
const inputElement = el.querySelector('.el-input__inner')
|| el.querySelector('input')
|| el.querySelector('textarea');
if (!inputElement) return;
const config = binding.value || {};
const enabled = config.enabled !== false;
if (!enabled) {
globalNoSingleQuoteConfig.disabledInputs.add(el);
return;
}
if (globalNoSingleQuoteConfig.processedInputs.has(el)) return;
globalNoSingleQuoteConfig.processedInputs.add(el);
let isComposing = false;
const removeSingleQuote = () => {
if (isComposing || !inputElement.value) return;
const oldValue = inputElement.value;
const newValue = oldValue.replace(/'/g, '');
if (oldValue !== newValue) {
const start = inputElement.selectionStart;
inputElement.value = newValue;
const newPos = start - (oldValue.substring(0, start).match(/'/g) || []).length;
inputElement.setSelectionRange(newPos, newPos);
inputElement.dispatchEvent(new Event('input', { bubbles: true }));
}
};
inputElement.addEventListener('input', removeSingleQuote);
inputElement.addEventListener('blur', removeSingleQuote);
inputElement.addEventListener('paste', () => setTimeout(removeSingleQuote, 10));
inputElement.addEventListener('compositionstart', () => { isComposing = true; });
inputElement.addEventListener('compositionend', () => {
isComposing = false;
setTimeout(removeSingleQuote, 10);
});
el._removeSingleQuote = removeSingleQuote;
el._inputElement = inputElement;
},
beforeUnmount(el) {
if (el._inputElement && el._removeSingleQuote) {
el._inputElement.removeEventListener('input', el._removeSingleQuote);
el._inputElement.removeEventListener('blur', el._removeSingleQuote);
}
globalNoSingleQuoteConfig.processedInputs.delete(el);
}
};
export { globalNoSingleQuoteConfig };
注册指令
src/main.js
js
import noSingleQuote from './directives/inputNoSingleQuote.js'
app.directive('input-no-single-quote', noSingleQuote)
使用
vue
<!-- 启用(默认) -->
<el-input v-model="name" v-input-no-single-quote />
<!-- 明确禁用(某些特殊输入框不需要限制) -->
<el-input v-model="sql" v-input-no-single-quote="{ enabled: false }" />
就这么简单,加一个指令,单引号自动消失。
方案二:全局插件(推荐)
不想每个输入框都加指令?用插件一次配置,全局生效。
创建插件文件
src/plugins/globalNoSingleQuote.js
js
import { globalNoSingleQuoteConfig } from '../directives/inputNoSingleQuote.js';
// 需要排除的页面路径(这些页面允许输入单引号)
const globalNoSingleQuotePluginConfig = {
excludedPaths: ['/qp/zjjc', '/qp/tableModuSet']
};
export const GlobalNoSingleQuotePlugin = {
install(app) {
app.mixin({
mounted() {
this.$nextTick(() => {
if (!this.$el || typeof this.$el.querySelectorAll !== 'function') return;
// 排除指定页面
const currentPath = window.location.hash || window.location.pathname;
if (globalNoSingleQuotePluginConfig.excludedPaths.some(p => currentPath.includes(p))) return;
const inputs = this.$el.querySelectorAll('.el-input, input, textarea');
inputs.forEach(inputEl => {
if (globalNoSingleQuoteConfig.processedInputs.has(inputEl) ||
globalNoSingleQuoteConfig.disabledInputs.has(inputEl)) return;
const actualInput = inputEl.querySelector('.el-input__inner')
|| inputEl.querySelector('input')
|| inputEl.querySelector('textarea')
|| (['INPUT', 'TEXTAREA'].includes(inputEl.tagName) ? inputEl : null);
if (!actualInput) return;
let isComposing = false;
const removeSingleQuote = () => {
if (isComposing || !actualInput.value) return;
const oldValue = actualInput.value;
const newValue = oldValue.replace(/'/g, '');
if (oldValue !== newValue) {
const start = actualInput.selectionStart;
actualInput.value = newValue;
const newPos = start - (oldValue.substring(0, start).match(/'/g) || []).length;
actualInput.setSelectionRange(newPos, newPos);
actualInput.dispatchEvent(new Event('input', { bubbles: true }));
}
};
actualInput.addEventListener('input', removeSingleQuote);
actualInput.addEventListener('blur', removeSingleQuote);
actualInput.addEventListener('paste', () => setTimeout(removeSingleQuote, 10));
actualInput.addEventListener('compositionstart', () => { isComposing = true; });
actualInput.addEventListener('compositionend', () => {
isComposing = false;
setTimeout(removeSingleQuote, 10);
});
globalNoSingleQuoteConfig.processedInputs.add(inputEl);
inputEl._removeSingleQuote = removeSingleQuote;
inputEl._inputElement = actualInput;
});
});
},
beforeUnmount() {
if (!this.$el || typeof this.$el.querySelectorAll !== 'function') return;
const inputs = this.$el.querySelectorAll('.el-input, input, textarea');
inputs.forEach(inputEl => {
if (inputEl._inputElement && inputEl._removeSingleQuote) {
inputEl._inputElement.removeEventListener('input', inputEl._removeSingleQuote);
inputEl._inputElement.removeEventListener('blur', inputEl._removeSingleQuote);
}
globalNoSingleQuoteConfig.processedInputs.delete(inputEl);
});
}
});
}
};
export { globalNoSingleQuotePluginConfig };
注册插件
src/main.js
js
import { GlobalNoSingleQuotePlugin } from './plugins/globalNoSingleQuote.js'
app.use(GlobalNoSingleQuotePlugin)
完成! 全站所有输入框自动禁止单引号,无需改任何业务代码。
两种方案对比
| 自定义指令 | 全局插件 | |
|---|---|---|
| 使用方式 | 每个输入框加 v-input-no-single-quote |
注册一次,全局生效 |
| 控制粒度 | 精准,逐个控制 | 全局,按页面路径排除 |
| 适用场景 | 部分页面需要限制 | 整个项目都需要限制 |
| 排除方式 | { enabled: false } |
配置 excludedPaths |
几个细节值得关注
1. 中文输入法兼容
用 compositionstart / compositionend 事件判断是否在拼音输入中,避免打字过程中误删拼音里的 ' 符号(比如 don't 的输入过程)。
2. 光标位置修正
删除单引号后,光标位置会偏移。代码里计算了光标前被删除的字符数,精准还原光标位置,用户无感知。
3. 粘贴处理
粘贴事件用 setTimeout(fn, 10) 延迟处理,确保粘贴内容已写入 DOM 再进行清理。
4. WeakSet 防重复
用 WeakSet 记录已处理的元素,避免 mixin 在组件更新时重复绑定事件,也不会造成内存泄漏。
5. 触发 Vue 响应式更新
直接修改 inputElement.value 不会触发 Vue 的响应式,需要手动 dispatchEvent(new Event('input', { bubbles: true })) 通知 Vue 更新绑定的数据。
总结
- 需要精准控制:用自定义指令,哪里需要加哪里
- 需要全局覆盖:用插件,注册一次搞定全站
- 两者可以同时使用:插件全局兜底,指令局部排除