本文详细解析如何使用原生JavaScript从零开发一个自动完成插件。文章涵盖了input与propertychange事件的浏览器兼容性处理、通过AJAX获取后端数据、动态创建下拉列表及选项,并利用匿名闭包封装为可复用对象。旨在帮助开发者理解核心逻辑,提升原生JS编码能力。
原生JS自动完成插件开发概述
在实际项目开发中,优先使用现成的插件库以节省时间成本是常见做法,特别是在项目周期紧张且需兼顾多浏览器兼容性的场景下。然而,从技术成长的角度出发,利用业余时间尝试使用原生JavaScript自行编写插件是一个极佳的学习途径。通过梳理个人思路并动手实现,能够有效加深对底层机制的理解,从而提升整体技术水平。
Input事件兼容性处理
AddEvt:function(ele, evt, fn) {
if (document.addEventListener) {
ele.addEventListener(evt, fn, false);
} else if (document.attachEvent) {
ele.attachEvent('on' + (evt == "input" ? "propertychange" : evt), fn);
} else {
ele['on' + (evt == "input" ? "propertychange" : evt)] = fn;
}
}
input事件与其他事件不同,低版本IE不支持该事件,需使用propertychange事件;高版本IE及W3C标准浏览器支持input事件。 2.在输入事件触发时获取数据 数据存在两种形式:直接设置的对象数组,或通过ajax请求返回的数据。 此时需要一个ajax请求函数,此处编写了一个get请求
AJAX数据请求封装
get: function(url, paraobj, fn, timeout) {
var xhr = null;
try {
////兼容firefox,chrom
if (window.XMLHttpRequest) {
xhr = new XMLHttpRequest();
}
//////兼容IE
else if (Window.ActiveXObject) {
xhr = new ActiveXObject("Msxml2.Xmlhttp");
}
} catch (e) {
//TODO handle the exception
xhr = new ActiveXObject('Microsoft.Xmlhttp');
}
xhr.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
fn.call(this, this.responseText);
} else {
setTimeout(function() {
xhr.abort();
}, timeout);
}
};
var parastr = '';
parastr += "?";
for (var prop in paraobj) {
parastr += prop + "=" + paraobj[prop] + "&";
}
xhr.open('get', parastr != "?" ? (url + parastr) : url, true);
xhr.send();
}
实现自动完成的第一步是为输入框绑定input事件。需要注意的是,该事件的浏览器兼容性存在差异:低版本IE浏览器不支持标准的input事件,必须回退使用propertychange事件;而高版本IE及遵循W3C标准的现代浏览器均支持input事件。具体的兼容性处理代码如下所示。
下拉列表与选项动态创建
createShowDiv: function() {
///如果下拉div已存在,删除掉
var parentNode = this.autoElement.parentNode || this.autoElement.parentElement;
var childNodes = parentNode.childNodes;
var showDiv = document.getElementById(this.config.showdivId);
if (showDiv) {
parentNode.removeChild(showDiv);
}
//创建下拉Div
var div = document.createElement('div');
div.id = this.config.showdivId;
//设置下拉div样式
var style = this.config.style || {
width: '200px',
height: 'auto',
backgroundColor: '#1c5683',
cursor: 'pointer',
display: 'block'
};<br>
for (var prop in style) {
div.style[prop] = style[prop];
}
this.showdiv = div;
}
当输入事件被触发时,系统需要获取对应的数据。数据来源通常分为两种情况:一是直接配置的对象数组,二是通过AJAX异步请求后端接口返回的数据。为此,我们需要封装一个通用的AJAX请求函数,此处以GET请求为例进行说明,相关代码如下。
appendChild: function(data) {
var self = this;
var data = data;
var fragment = document.createDocumentFragment();
for (var i = 0; i < data.length; i++) {
var obj = data[i];
var child = document.createElement('div');
child.style.width = self.showdiv.style.width;
child.style.border = '1px';
child.style.borderStyle = 'solid';
child.style.borderTopColor = 'white';
child.setAttribute('key', obj[self.config.valueFiled]);
child.innerHTML = obj[self.config.textFiled];
fragment.appendChild(child);
}
self.showdiv.appendChild(fragment);
self.util.insertAfter(self.showdiv, self.autoElement);
//为下拉框添加点击事件
self.util.AddEvt(self.showdiv, 'click', function(e) {
var evt = e || window.event;
var target = evt.srcElement || evt.target;
var key = target.getAttribute("key");
var val = target.innerHTML;
self.autoElement.value = val;
self.closeDiv();
self.config.select.call(self, key, val);
});
}
在AJAX请求成功且返回有效数据后,下一步是创建下拉显示区域并在其中追加对应的选项。首先需处理下拉Div的创建逻辑,如果已存在旧的下拉框,则先移除,再动态生成新的Div元素并应用样式。创建下拉Div的核心代码如下所示。
插件对象封装与闭包应用
(function(win) {
var autocomplete= function() {
this.Init.apply(this, arguments);
}
autocomplete.prototype = {
////添加相关操作代码
Init: {}, ///初始化参数
Render: {},
createShowDiv: {}, ///创建下拉div
appendChild: {}, ///在下拉div里面追加显示项
closeDiv: {}, ////关闭下拉框
//////工具对象,事件,请求,还有dom节点操作的函数
util: {
AddEvt: {}, ///添加事件
insertAfter: {}, ///在某元素后面追加元素
get: {} //// Ajax get请求
}
}
win.Autocomplete= function(paraobj) {
new autocomplete(paraobj).Render();
}
})(window)
接着,将获取到的数据遍历并追加到下拉Div中,形成可视化的选项列表。这一步涉及创建文档片段以优化性能,并为每个选项绑定点击事件,以便用户选择后回填输入框并触发回调函数。追加选项的具体实现代码如下。
(function(win) {
var autocomplete = function () {
this.Init.apply(this, arguments);
}
autocomplete.prototype = {
Init: function() {
var args = Array.prototype.slice.call(arguments);
if (args && args.length > 0) {
var config = args[0];
var getType = Object.prototype.toString;
if (config && getType.call(config) == "[object Object]") {
// this.config = config;
this.config = config || {
id: '', //控件id
data: [], //数据
textFiled: '', //显示的文字的属性名
valueFiled: '', //获取value的属性名
style: {}, //显示的下拉div的样式设置
url: '', //ajax请求的url
paraName:'name',//ajax请求的参数
select: function() {}, //选择选项时触发的事件,
showdivId: '' //下拉选择区域的id
};
}
}
},
Render: function() {
var self = this;
if (self.config) {
var autoElement = document.getElementById(self.config.id);
this.autoElement = autoElement;
if (autoElement) {
self.util.AddEvt(this.autoElement, 'input', function() {
try {
if (autoElement.value) {
////ajax请求获取数据的方式
if (self.config.url && !self.config.data) {
var paraobj = {};
paraobj[self.config.paraName] = autoElement.value;
self.util.get(self.config.url, paraobj, function (data) {
self.createShowDiv();
self.appendChild(eval('(' + data + ')'));
}, 10000);
}
////直接设置对象数组的形式
else if
(!self.config.url && self.config.data) {
self.createShowDiv();
self.appendChild(self.config.data);
}
} else {
self.closeDiv();
}
} catch (e) {
//TODO handle the exception
alert(e);
}
});
}
}
},
////创建下拉Div
createShowDiv: function() {
///如果下拉div已存在,删除掉
var parentNode = this.autoElement.parentNode || this.autoElement.parentElement;
var childNodes = parentNode.childNodes;
var showDiv = document.getElementById(this.config.showdivId);
if (showDiv) {
parentNode.removeChild(showDiv);
}
//创建下拉Div
var div = document.createElement('div');
div.id = this.config.showdivId;
//设置下拉div样式
var style = this.config.style || {
width: '200px',
height: 'auto',
backgroundColor: '#1c5683',
cursor: 'pointer',
display: 'block'
};
for (var prop in style) {
div.style[prop] = style[prop];
}
this.showdiv = div;
},
///在下拉div里面追加显示项
appendChild: function(data) {
var self = this;
var data = data;
var fragment = document.createDocumentFragment();
for (var i = 0; i < data.length; i++) {
var obj = data[i];
var child = document.createElement('div');
child.style.width = self.showdiv.style.width;
child.style.border = '1px';
child.style.borderStyle = 'solid';
child.style.borderTopColor = 'white';
child.setAttribute('key', obj[self.config.valueFiled]);
child.innerHTML = obj[self.config.textFiled];
fragment.appendChild(child);
}
self.showdiv.appendChild(fragment);
self.util.insertAfter(self.showdiv, self.autoElement);
//为下拉框添加点击事件
self.util.AddEvt(self.showdiv, 'click', function(e) {
var evt = e || window.event;
var target = evt.srcElement || evt.target;
var key = target.getAttribute("key");
var val = target.innerHTML;
self.autoElement.value = val;
self.closeDiv();
self.config.select.call(self, key, val);
});
},
////关闭下拉框
closeDiv: function () {
if (this.showdiv) {
this.showdiv.style.display = 'none';
}
}
,
util: {
///添加事件
AddEvt: function(ele, evt, fn) {
if (document.addEventListener) {
ele.addEventListener(evt, fn, false);
} else if (document.attachEvent) {
ele.attachEvent('on' + (evt == "input" ? "propertychange" : evt), fn);
} else {
ele['on' + (evt == "input" ? "propertychange" : evt)] = fn;
}
},
///在某元素后面追加元素
insertAfter: function(ele, targetELe) {
var parentnode = targetELe.parentNode || targetELe.parentElement;
if (parentnode.lastChild == targetELe) {
parentnode.appendChild(ele);
} else {
parentnode.insertBefore(ele, targetELe.nextSibling);
}
},
///Get请求
get: function(url, paraobj, fn, timeout) {
var xhr = null;
try {
if (window.XMLHttpRequest) {
xhr = new XMLHttpRequest();
} else if (Window.ActiveXObject) {
xhr = new ActiveXObject("Msxml2.Xmlhttp");
}
} catch (e) {
//TODO handle the exception
xhr = new ActiveXObject('Microsoft.Xmlhttp');
}
xhr.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
fn.call(this, this.responseText);
} else {
setTimeout(function() {
xhr.abort();
}, timeout);
}
};
var parastr = '';
parastr += "?";
for (var prop in paraobj) {
parastr += prop + "=" + paraobj[prop] + "&";
}
xhr.open('get', parastr != "?" ? (url + parastr) : url, true);
xhr.send();
}
}
}
win.AutoComplete = function (paraobj) {
new autocomplete(paraobj).Render();
}
})(window)
上述步骤构成了自动完成功能的主要逻辑。为了使其成为一个可复用的插件,我们需要将这些代码封装到一个对象中。这里采用匿名闭包的模式来封装核心逻辑,通过原型链定义方法,从而避免全局变量污染并实现模块化。封装后的核心结构代码如下所示。
在封装好核心逻辑后,我们可以展示完整的插件主体代码。这部分代码整合了初始化、渲染、事件绑定及数据请求等所有功能,构成了插件的完整实现。完整的主体代码如下所示。
插件调用与后台接口配合
window.onload = function () {
AutoComplete({
id: 'txtTest', //控件id
url: '/Home/Test4', //数据
paraName:'name',
textFiled: 'name', //显示的文字的属性名
valueFiled: 'id', //获取value的属性名
// style: {}, //显示的下拉div的样式设置
// url: '', //ajax请求的url
select: function (val, text) {
alert(val + '---' + text);
}, //选择选项时触发的事件,
showdivId: 'showDIv' //下拉选择区域的id});
});
}
接下来展示如何调用该插件。用户只需实例化插件对象并传入配置参数,即可在页面上启用自动完成功能。具体的调用示例代码如下所示。
public JsonResult Test4(string name)
{
var list=new List<Student>();
list.Add(new Student { id="1",name="aaaaa"});
list.Add(new Student { id = "2", name = "aacc" });
list.Add(new Student { id = "3", name = "aabb" });
list.Add(new Student { id = "4", name = "bbcc" });
if (!string.IsNullOrEmpty(name))
{
list = list.Where(p => p.name.Contains(name)).ToList();
}
return Json(list,JsonRequestBehavior.AllowGet);
}
页面端的调用方式如上所述,而后台接口则需要配合返回JSON格式的数据。本文示例中后台采用MVC架构进行开发,具体的后端处理代码如下所示。
至此,自动完成插件的基本功能实现与调用流程已讲解完毕。从初始构思到最终代码落地,整个过程涉及多个步骤,每个方法都需逐步实现,且未引用任何第三方库,因此必须充分考虑各浏览器的兼容性问题。