导读
现在面试题会问到很多的场景题,例如【如何设计一个input框,输入内容请求数据,你会如何设计】。通常情况下可能只想到防抖功能,但却忘记了请求数据必定会请求接口,而基于用户侧来说,请求接口是异步的,用户并不清楚程序在后面干了什么;如果请求成功了最好,如果请求失败了呢?还有用户可能输入一些无效的或者不安全的内容,如果空白字符也就没有了请求接口的意义,如果是一些HTML片段,那么也容易构成XSS攻击。本文接下来从用户体验、性能优化(面向浏览器渲染、面向服务端请求)、前端安全、兼容性等四个大的维度展开。
用户体验
用户体验的核心是流畅无卡顿、且有友好的反馈、错误提示等。
确保用户操作及时响应
如果系统中除了该input框的交互以外,有其他任务执行,且任务涉及复杂计算、渲染的,有可能造成用户交互卡顿,因此尽量不在JS处理复杂计算和渲染;如确有必要,请使用Web Workers
、requestIdleCallback
和OffscreenCanvas
等方案。
即时反馈
用户输入后势必需要调用接口从服务端拿到数据,这个过程必定是个耗时过程,接口应该做到尽快响应;除此之外,在输入时,给用户一个下拉选择的输入提示或是请求中在input框的右侧显示loading,皆在给用户提供人性化操作和请求期待。
错误提示
无论是用户输入不规范的错误信息或者调用接口失败时,都应该向用户弹出一个提示框(警告或者错误级别),提示用户输入信息有误或者请求失败。但需要注意的是,请求失败次数一旦过多,将会降低用户的使用意愿,因此前后端必须紧密配合,确保请求尽快兑现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| inputEl.addEventListener("input", function(e) { const value = e.target.value.trim(); if (value) { fetch("/api", { method: "POST", body: JSON.stringify({ query: value }) }).then((res) => { if (!res.ok) { throw new Error("请求失败"); } return res.json(); }).then((data) => { }).catch((err) => { console.error(err); alert("请求失败,请稍后重试"); }); } });
|
历史搜索
可以使用LocalStorage
存入用户输入的内容,并采取LRU算法按热度排序显示。用户此次输入内容在历史搜索记录中,这样,用户直接鼠标或者手指触发就可以一键输入想输入的内容,节省用户的输入耗时。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| const HISTORY_KEY = "search_history"; const MAX_HISTORY_ITEMS = 10;
function saveSearchHistory(query) { let history = JSON.parse(localStorage.getItem(HISTORY_KEY)) || []; history = history.filter(item => item !== query); history.unshift(query); if (history.length > MAX_HISTORY_ITEMS) { history.pop(); } localStorage.setItem(HISTORY_KEY, JSON.stringify(history)); }
function getSearchHistory() { return JSON.parse(localStorage.getItem(HISTORY_KEY)) || []; }
|
性能优化
防抖
用户的输入常会快于渲染时间+请求时间,为了避免频繁的请求,需要做防抖处理,只在用户输入的最后一个字符后发送请求,而不是每输入一个字符就发送一个请求。
例如,用户搜索“达拉崩吧”,不能每输入一个字就请求一次(输入建议除外)搜索结果,必定是设置大约50ms左右的防抖以减少非用户期望的请求。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| function debounce(func, delay) { let timer; return function(...args) { clearTimeout(timer); timer = setTimeout(() => { func.apply(this, args); }, delay); }; }
inputEl.addEventListener("input", debounce(function(e) { const value = e.target.value.trim(); if (value) { fetch("/api", { method: "POST", body: JSON.stringify({ query: value }) }).then((res) => { }).catch((err) => { }); } }, 300));
|
取消请求
上述所说的防抖也不一定就能取消所有非用户期望的请求,且用户在第一次搜索结果还没响应就又搜索了一次,此时需要废弃用户之前的无效请求,于是需要取消请求。取消请求通常可以使用AbortController
实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| let abortController;
inputEl.addEventListener("input", debounce(function(e) { const value = e.target.value.trim(); if (value) { if (abortController) { abortController.abort(); } abortController = new AbortController(); fetch("/api", { method: "POST", body: JSON.stringify({ query: value }), signal: abortController.signal }).then((res) => { }).catch((err) => { if (err.name !== "AbortError") { console.error(err); alert("请求失败,请稍后重试"); } }); } }, 300));
|
节流
在某些场景下,可能需要节流代替防抖。例如在指定时间内只允许搜索一次,例如付费搜索或者会员等级限定搜索次数、或是防止爬虫工具等在规定时间内限制搜索次数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| function throttle(func, limit) { let inThrottle; return function(...args) { if (!inThrottle) { func.apply(this, args); inThrottle = true; setTimeout(() => (inThrottle = false), limit); } }; }
inputEl.addEventListener("input", throttle(function(e) { const value = e.target.value.trim(); if (value) { fetch("/api", { method: "POST", body: JSON.stringify({ query: value }) }).then((res) => { }).catch((err) => { }); } }, 1000));
|
输入验证
用户可能输入空字符或者无意义的标点符号,又或者输入长度不符合要求,可以在输入后验证输入,如果输入无意义将跳过请求。
1 2 3 4 5 6 7 8 9 10 11 12 13
| inputEl.addEventListener("input", function(e) { const value = e.target.value.trim(); if (value.length > 3) { fetch("/api", { method: "POST", body: JSON.stringify({ query: value }) }).then((res) => { }).catch((err) => { }); } });
|
缓存
用户可能在同一时间内多次搜索相同的内容,可以设置一个key-value
对象,key为用户输入的内容,value为之前返回的结果数据。一旦用户再次输入后,可以直接从缓存中拿出数据,减少请求次数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| const cache = new Map();
inputEl.addEventListener("input", function(e) { const value = e.target.value.trim(); if (value) { if (cache.has(value)) { const data = cache.get(value); } else { fetch("/api", { method: "POST", body: JSON.stringify({ query: value }) }).then((res) => { return res.json(); }).then((data) => { cache.set(value, data); }).catch((err) => { }); } } });
|
前端安全
前端安全有一条准则:永远不要相信用户的输入。有些目的不纯粹的用户可能会输入一些HTML文本或者片段,并以此发起XSS攻击。因此,在正式调用接口前,需要净化用户的输入,例如使用dompurify
这个库。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import DOMPurify from "dompurify";
inputEl.addEventListener("input", function(e) { const value = DOMPurify.sanitize(e.target.value.trim()); if (value) { fetch("/api", { method: "POST", body: JSON.stringify({ query: value }) }).then((res) => { }).catch((err) => { }); } });
|
兼容性
用户的输入框可能是在移动端页面,或者同时兼容PC和移动端设备,那么就需要响应式布局或者媒体查询提供不同的样式,以保证input正常显示且符合用户的直观感受。
1 2 3 4 5 6 7 8 9 10 11 12
| input { width: 100%; padding: 10px; font-size: 16px; }
@media (min-width: 768px) { input { width: 50%; } }
|
总结
本文从【如何设计一个input框,输入内容请求数据,你会如何设计】出发,介绍了设计场景面试题中需要注意的问题。总的来说,需要分别从用户体验、性能优化、前端安全和兼容性四大方面。涉及到接口请求,务必考虑到loading
的交互提示、取消请求、结果缓存、防抖和节流、接口错误提示等;而对于用户的输入,要考虑到两点:是否有意义的输入?默认用户输入内容不安全。基于以上角度,设计类场景题就能很好地应对了。当然,这还有补充一点,如果涉及到渲染性能,特别是渲染帧的问题,还可能涉及到具体的常见的性能指标、指标的测量和具体用户措施上。