Google 搜尋框、電商關鍵字聯想皆為經典「自動完成」體驗。本文實作一個具備防抖、快取、鍵盤選取、滑鼠點選、多輸入框共存的完整元件,後端以 JSON 回傳資料,前端原生 JS + jQuery 渲染。
HTML 結構
<input type="text" id="brandName" autocomplete="off">
<input type="hidden" id="brandId">
<div class="autocomplete-wrap" id="brandList">
<ul class="autocomplete-list" id="brandListUl"></ul>
</div>
CSS 關鍵:.autocomplete-wrap { position:absolute; display:none; border:1px solid #ccc; background:#fff; z-index:999; },ul { max-height:200px; overflow-y:auto; }。
JavaScript 核心邏輯
var AC = (function($) {
var cache = {}, xhrPool = {};
function fetch(listId, inputId, hiddenId, apiUrl, kw) {
if (xhrPool[listId]) xhrPool[listId].abort(); // 取消前一次請求
if (cache[kw]) return render(listId, inputId, hiddenId, cache[kw]);
xhrPool[listId] = $.getJSON(apiUrl, { keyword: kw, _: Date.now() }, function(data) {
cache[kw] = data;
render(listId, inputId, hiddenId, data);
});
}
function render(listId, inputId, hiddenId, data) {
var $ul = $('#' + listId + 'Ul').empty();
$.each(data, function(i, item) {
$('').text(item.name)
.data('id', item.id)
.on('mousedown', function() {
$('#' + inputId).val(item.name);
$('#' + hiddenId).val(item.id);
hide(listId);
})
.appendTo($ul);
});
show(listId);
}
function show(id) { $('#' + id).show(); }
function hide(id) { $('#' + id).hide(); }
return {
bind: function(inputId, hiddenId, listId, apiUrl, delay) {
var $inp = $('#' + inputId), timer;
$inp.on('input', function() {
clearTimeout(timer);
var kw = $.trim(this.value);
timer = setTimeout(function() {
if (kw) fetch(listId, inputId, hiddenId, apiUrl, kw);
else hide(listId);
}, delay || 200);
});
$inp.on('focus', function() { if ($.trim(this.value)) fetch(listId, inputId, hiddenId, apiUrl, $.trim(this.value)); });
$(document).on('mousedown', function(e) {
if (!$(e.target).closest('#' + listId + ', #' + inputId).length) hide(listId);
});
// 鍵盤上下選取、Enter 確認、Esc 關閉
$inp.on('keydown', function(e) {
var $ul = $('#' + listId + 'Ul'), $lis = $ul.children('li'), $sel = $ul.children('.active');
if (e.keyCode === 40) { // Down
e.preventDefault();
var $next = $sel.length ? $sel.next() : $lis.first();
$sel.removeClass('active'); $next.addClass('active');
} else if (e.keyCode === 38) { // Up
e.preventDefault();
var $prev = $sel.length ? $sel.prev() : $lis.last();
$sel.removeClass('active'); $prev.addClass('active');
} else if (e.keyCode === 13) { // Enter
$sel.mousedown();
} else if (e.keyCode === 27) { // Esc
hide(listId);
}
});
}
};
})(jQuery);
// 使用範例
$(function() {
AC.bind('brandName', 'brandId', 'brandList', '/api/brands', 200);
AC.bind('seriesName', 'seriesId', 'seriesList', '/api/series', 200);
});
後端 API 規範
請求:GET /api/brands?keyword=寶馬&_=1623456789
回應:[{"id":"1","name":"寶馬"},{"id":"2","name":"寶馬X5"}]
效能與體驗優化清單
- 防抖:
setTimeout 延遲 200ms 送請求,避免每按鍵即發送。
- 請求取消:新請求前
abort() 舊請求,避免競態條件導致舊資料覆蓋新資料。
- 前端快取:同關鍵字二次輸入直接讀
cache[kw],零延遲。
- 鍵盤無障礙:上下鍵選取、Enter 確認、Esc 關閉,符合 WAI-ARIA 建議(可再加
role="listbox", aria-activedescendant)。
- 點擊外部關閉:
document.mousedown 判斷目標是否在下拉區或輸入框內。
- 多實例隔離:模組化封裝,同頁可掛載多組自動完成互不干擾。