1. 为什么你的Layui下拉框需要拼音搜索?
如果你正在开发一个面向国内用户的管理后台、CRM系统或者任何包含大量中文选项的表单,那你肯定遇到过这个头疼的问题:用户面对一个包含几百个“省份”、“城市”、“产品名称”或者“员工姓名”的下拉框,只能一个汉字一个汉字地输入来筛选,效率低得让人抓狂。
我自己就踩过这个坑。几年前做一个电商后台,商品分类下拉框里有上千个选项,运营同事每次找“连衣裙”都要打全这三个字,抱怨说“要是能打‘lyq’就出来多好”。那时候我就意识到,原生的Layui-select搜索虽然能用,但在中文场景下,它缺了点什么——缺的就是对拼音输入的支持。
Layui本身是个很棒的前端框架,特别是对于需要快速搭建后台的中小团队来说,它的简洁和易用性让人爱不释手。它的select组件自带search属性,开启后就能搜索,原理就是简单的字符串匹配。但问题在于,它只能匹配汉字本身。用户输入“beijing”,它不会匹配到“北京”;输入“zj”,它更不会联想到“浙江”。
这就是我们今天要解决的核心痛点:如何让Layui-select不仅能听懂汉字,还能听懂拼音,甚至是拼音的首字母缩写。想象一下,用户输入“sh”就能找到“上海”,输入“shanghai”也能匹配,输入“上海”当然更没问题。这种混合匹配的能力,能极大提升用户的操作体验,尤其是在数据量大的情况下。
实现这个功能,本质上是对Layui搜索逻辑的一次“增强手术”。我们不需要重写整个组件,而是巧妙地修改它核心的匹配函数,引入拼音转换能力。听起来有点技术含量?别担心,跟着我的步骤走,哪怕你是刚接触Layui不久的前端同学,也能在半小时内搞定。我会把每一步的原理、要修改的代码、可能遇到的坑都讲得明明白白。
2. 准备工作:找到正确的“手术刀”和“手术台”
在开始动代码之前,我们得把工具和环境准备好。这里有几个关键点,如果搞错了,后面会白费功夫。
首先,最重要的一步:获取Layui的源代码文件,而不是压缩文件。 很多新手会直接去Layui官网下载那个layui.all.js或者layui.js,然后试图在里面修改。我劝你千万别这么做!因为这些发布版是经过压缩和混淆的,所有变量名都被缩短了,代码挤在一行,根本没有可读性。你打开一看,绝对会“晕倒在电脑前”,根本找不到我们要修改的那个函数在哪里。
正确的做法是去Layui的GitHub仓库:https://github.com/sentsin/layui。在仓库的src/目录下,找到layui.js或者更具体的form.js(select组件的逻辑主要在form模块里)。这里的代码是未经压缩的源码,格式清晰,带注释,是我们进行修改的“手术台”。你可以把整个src目录下载到你的本地项目里。
其次,准备两把关键的“手术刀”:拼音转换插件。 我们需要两个轻量级的JavaScript库:
- pinyin.js:负责将汉字转换成完整的拼音字符串。比如“北京”转换成“beijing”。
- initials.js:负责提取汉字的首字母。比如“北京”转换成“BJ”。
这两个文件在网上有很多版本,我推荐使用经过社区验证的。你可以从一些开源仓库获取,确保它们支持多音字处理(比如“重庆”能正确转为“chongqing”而非“zhongqing”)。通常,你只需要在HTML页面中,在引入layui.js之前,用<script>标签引入这两个文件即可。
<!-- 引入拼音工具 -->
<script src="./lib/pinyin.js"></script>
<script src="./lib/initials.js"></script>
<!-- 引入Layui -->
<script src="./layui/layui.js"></script>
准备好这两样,我们的“手术室”就算布置完毕了。接下来,就是要找到Layui-select身体里那个负责判断“是否匹配”的“神经中枢”。
3. 深入Layui心脏:定位搜索匹配的核心函数
要修改一个开源组件,最怕的就是像无头苍蝇一样乱找。我的经验是,先理解它的运行机制。Layui的select组件并不是用原生的<select>标签实现的,而是用一套自定义的HTML结构(<dl>、<dd>标签)来模拟下拉列表。当你在输入框里打字时,它会遍历所有的<dd>选项,根据你输入的内容,决定哪些选项显示,哪些隐藏。
这个“决定”的过程,就发生在一个叫做notOption的函数里。我们的目标就是找到它。在你下载的Layui源码中(比如src/lay/modules/form.js),搜索notOption这个函数名。你应该会找到类似下面这样的一段代码:
//检测值是否不属于 select 项
var notOption = function(value, callback, origin){
var num = 0;
layui.each(dds, function(){
var othis = $(this)
,text = othis.text() // 获取选项的显示文本
,id = othis.attr('layui-value') // 获取选项的值
,not = text.indexOf(value) === -1; // 关键判断:文本中是否包含输入值
if(value === '' || (origin === 'blur') ? value !== text : not) num++;
origin === 'keyup' && othis[not ? 'addClass' : 'removeClass'](HIDE);
});
var none = num === dds.length;
return callback(none), none;
};
看第7行,not = text.indexOf(value) === -1。这就是原生搜索的全部逻辑!它仅仅检查选项的文本(text)里是否包含了输入的字符串(value)。如果包含,not为false,选项显示;如果不包含,not为true,选项隐藏。
我们的“手术”切入点就是这一行。我们要用一个更强大的自定义函数myFilter,替换掉这个简单的indexOf判断。这个自定义函数将具备汉字匹配、全拼匹配、首字母匹配的“混合动力”。
但在此之前,我们得先拿到选项的value(即layui-value属性),因为有时候用户可能也想通过值来搜索。所以修改的第一步,是在这个函数里,把id变量准备好,并传给我们的新函数。我们先把not那一行注释掉,改成调用新函数。
4. 打造混合动力引擎:编写myFilter过滤函数
现在,我们来打造这个强大的myFilter函数。这个函数接收三个参数:用户输入的值value、选项的文本text、选项的value值id。它的核心任务是判断:根据输入value,选项text是否应该被匹配上。
逻辑上,我们分两层判断:
- 如果输入的是汉字:这个最简单,直接用原生的
indexOf判断文本是否包含该汉字即可。我们可以用一个小技巧escape(value).indexOf("%u") != -1来检测字符串里是否包含Unicode字符(即汉字)。 - 如果输入的不是汉字(即拼音或字母):这里就是发挥我们“混合匹配”威力的地方了。我们需要同时进行四种匹配尝试:
- 全拼匹配:将选项文本转换成全拼,看其开头是否与输入值匹配。
- 首字母匹配:将选项文本转换成首字母并转小写,看其开头是否与输入值匹配。
- 原文本匹配:选项文本本身转小写后是否包含输入值(这是为了兼容可能存在的英文选项)。
- Value值匹配:选项的value值是否包含输入值(扩展功能,方便按值搜索)。
我们把myFilter函数写在notOption函数的上方,这样代码结构更清晰。
// 自定义搜索过滤器 - 核心引擎
var myFilter = function(value, text, id) {
var result = false; // 默认不匹配
var len = value.length;
// 情况1:输入的是汉字,直接进行文本包含匹配
if (escape(value).indexOf("%u") != -1) {
result = text.indexOf(value) > -1;
}
// 情况2:输入的是拼音或字母
else {
// 转换为小写,方便比较
var lowerValue = value.toLowerCase();
// 混合匹配:全拼 或 首字母 或 原文本 或 value值
result = ConvertPinyin(text).toLowerCase().indexOf(lowerValue) === 0 // 全拼开头匹配
|| makePy(text)[0].toLowerCase().indexOf(lowerValue) === 0 // 首字母开头匹配
|| text.toLowerCase().indexOf(lowerValue) > -1 // 原文本包含匹配
|| (id !== undefined && id.indexOf(lowerValue) > -1); // value值包含匹配
}
// 返回true表示匹配成功(选项应显示),false表示不匹配(选项应隐藏)
// 注意:这里返回的是匹配结果,与原逻辑的`not`含义相反
return result;
};
这里有几个关键点需要注意:
ConvertPinyin(text):这是我们引入的pinyin.js提供的全局函数,它把汉字文本转换成全拼字符串。makePy(text)[0]:这是我们引入的initials.js提供的全局函数,它返回一个数组,第一个元素就是首字母字符串。- 我使用了
.indexOf(lowerValue) === 0来判断“开头匹配”,这比原文的.substring(0, len) === value更直观,效果一样。意思是“转换后的字符串是否以输入值开头”。 - 我增加了
text.toLowerCase().indexOf(lowerValue) > -1,这是一个降级策略。如果某个选项文本本身就是英文“Apple”,用户输入“app”也能匹配上。 - 函数最终返回
true或false,表示是否匹配。但要注意,这个布尔值的含义将与后面notOption函数中的not变量相反。
5. 完成嫁接手术:修改notOption函数并测试
“引擎”造好了,现在要把它“安装”到Layui的“车身”上。回到我们之前找到的notOption函数,进行修改:
//检测值是否不属于 select 项
var notOption = function(value, callback, origin){
var num = 0;
layui.each(dds, function(){
var othis = $(this)
,text = othis.text()
,id = othis.attr('layui-value') // 确保获取到value
// 原来的简单匹配注释掉
// ,not = text.indexOf(value) === -1;
// 使用我们强大的混合过滤器
,isMatched = myFilter(value, text, id); // true表示匹配,false表示不匹配
,not = !isMatched; // not的含义是“是否不属于”,所以对结果取反
if(value === '' || (origin === 'blur') ? value !== text : not) num++;
origin === 'keyup' && othis[not ? 'addClass' : 'removeClass'](HIDE);
});
var none = num === dds.length;
return callback(none), none;
};
看,我们新增了isMatched变量来接收myFilter的结果,然后not = !isMatched。这样,当我们的过滤器说“匹配上了”(true),not就是false,选项就会显示,逻辑就通顺了。
大功告成! 现在,让我们来测试一下这个“混合动力”下拉框。创建一个简单的HTML页面,引入必要的JS文件,然后初始化一个带搜索的Layui-select。
<div class="layui-form">
<select name="city" lay-search="">
<option value="">请选择城市</option>
<option value="1">北京</option>
<option value="2">上海</option>
<option value="3">广州</option>
<option value="4">深圳</option>
<option value="5">杭州</option>
<option value="6">Hangzhou</option>
</select>
</div>
<script>
layui.use('form', function(){
var form = layui.form;
form.render('select');
});
</script>
测试场景:
- 输入“北”或“京” -> 匹配“北京”。(汉字匹配)
- 输入“bei” -> 匹配“北京”。(全拼开头匹配)
- 输入“bj” -> 匹配“北京”。(首字母匹配)
- 输入“sh” -> 匹配“上海”。(首字母匹配)
- 输入“hang” -> 同时匹配“杭州”和“Hangzhou”。(全拼匹配+原文本匹配)
- 输入“1” -> 匹配“北京”。(value值匹配)
如果一切顺利,你的下拉框已经变得非常智能了!用户无论习惯打汉字、打全拼还是打首字母,都能快速定位到目标选项。
6. 高级技巧与性能优化实战
基础功能实现了,但在实际项目中,我们还得考虑更多。比如,数据量特别大怎么办?用户输入“A”,可能匹配出几千个选项,前端会不会卡住?又或者,除了layui.form的select,我还用了像xm-select这样的多选下拉框,该怎么适配?
针对大数据量的性能考量:
我们的myFilter函数在遍历每个选项时,都会调用ConvertPinyin和makePy进行转换。如果选项有几千个,每次按键都计算一遍,确实可能成为性能瓶颈。这里有几个优化思路:
-
空间换时间,预计算拼音:如果下拉框的数据是静态的,或者加载后不变,我们可以在初始化时,就为每个选项计算好它的全拼和首字母,并作为
data-*属性存储在DOM元素上。// 假设在数据渲染时,我们可以提前处理 data.forEach(function(item){ item.pinyin = ConvertPinyin(item.name); item.initial = makePy(item.name)[0].toLowerCase(); }); // 渲染选项时,将拼音信息存入自定义属性 // <dd layui-value="1" data-pinyin="beijing" data-initial="bj">北京</dd>然后在
myFilter函数中,就不再需要实时转换,直接从othis.attr('data-pinyin')和othis.attr('data-initial')读取即可,速度会快很多。 -
引入防抖(Debounce):Layui原生的搜索是
keyup即时触发的。我们可以考虑对搜索输入事件进行防抖处理,比如用户连续输入时,只在停止输入300毫秒后才触发搜索,减少不必要的计算次数。但这需要修改Layui的事件绑定部分,侵入性较强。 -
后端辅助搜索:对于数据量极其庞大的场景(如十万级),前端搜索已经不现实。应该将拼音转换和搜索逻辑放到后端,前端select组件仅作为展示,搜索功能通过异步加载数据来实现。
适配xm-select等多选组件:
很多朋友喜欢用功能更强大的xm-select。好消息是,我们的核心逻辑myFilter函数完全可以复用!xm-select提供了filterMethod配置项,允许我们完全自定义过滤逻辑。
xmSelect.render({
el: '#test',
filterable: true,
filterMethod: function(val, item, index, prop){
// 直接使用我们写好的myFilter函数!
// item.name 相当于 text, item.value 相当于 id
return myFilter(val, item.name, item.value);
},
data: [...]
});
你看,核心的拼音转换和匹配逻辑是通用的,我们只需要根据不同的UI组件,调整一下调用方式即可。这体现了将业务逻辑(拼音匹配)与UI框架解耦的好处。
7. 避坑指南:我踩过的那些雷
在给多个项目实施这个优化方案后,我总结了一些常见的“坑”,希望能帮你一次成功。
坑1:插件引入顺序与全局变量冲突
确保pinyin.js和initials.js在layui.js之前引入。检查浏览器控制台,确认ConvertPinyin和makePy这两个全局函数是否存在。有时不同的拼音库全局变量名不同,需要根据你实际使用的库调整myFilter函数中的函数名。
坑2:多音字处理不准
这是拼音转换的老大难问题。“重庆”应该转成“chongqing”还是“zhongqing”?“行长”是“hangzhang”还是“xingzhang”?你使用的pinyin.js库的质量决定了这一点。在选择插件时,最好寻找那些带有多音字词典或能根据上下文(虽然在前端单字环境下很难)进行判断的库。对于大多数地名、人名场景,一个好的基础库足够用了。如果遇到特别重要的个别词汇转换错误,可以考虑在数据层预先处理好正确的拼音。
坑3:修改了压缩版的layui.all.js
再次强调,一定要修改源代码(src/lay/modules/form.js),然后用Layui的构建方式(或直接使用源码)运行。如果你修改了压缩版,下次更新Layui版本时,你的修改会全部丢失,而且调试起来如同噩梦。
坑4:忽略了大小写
用户在输入拼音时,可能大小写混用。我们的myFilter函数中已经通过.toLowerCase()统一转为小写进行比较,这一点很重要。
坑5:与Layui其他功能的兼容性 修改核心代码后,务必测试一下select组件的其他功能是否正常,比如:
- 动态赋值
form.val后,搜索是否正常。 - 使用
lay-search属性时,清空搜索框,所有选项是否都正确显示。 - 与Layui的
lay-filter、表单验证等功能联用是否正常。
最好的实践是,将你修改后的form.js模块单独保存为一个文件,例如form.extend.js,并在项目中注明这是自定义扩展版。这样既方便管理,也提醒其他同事这里包含了特殊逻辑。
8. 扩展思路:还能做得更好吗?
实现了基础的全拼/首字母混合搜索,我们的思维可以再发散一下。这个功能还有哪些可以打磨的细节?
1. 更灵活的匹配模式:
现在的匹配是“开头匹配”(indexOf(...) === 0)。但有些用户可能希望输入“jing”也能匹配到“北京”。我们可以增加一个“任意位置包含拼音”的匹配模式。这只需要将判断条件从=== 0改为> -1即可。你甚至可以在myFilter函数中增加一个配置参数,让开发者决定使用哪种匹配模式。
2. 拼音分词与模糊音支持: 对于长词组,如“内蒙古自治区”,用户可能输入“neimenggu”或“nmggzzq”。更高级的实现可以考虑对转换后的全拼进行“分词”匹配,或者加入模糊音支持(如“l”和“n”,“z”和“zh”),但这会显著增加复杂度。
3. 视觉反馈优化:
当用户输入拼音搜索时,我们可以在匹配的选项上,高亮显示出是哪个部分被匹配了(是汉字本身、还是全拼、还是首字母)。这需要修改渲染逻辑,在notOption函数中,不仅判断显示隐藏,还要给匹配的选项添加一个包含匹配类型信息的属性,然后在CSS中为其设置不同的样式。
4. 封装成Layui模块:
最优雅的方式,不是直接修改form.js源码,而是将我们的增强功能写成一个独立的Layui扩展模块。例如,可以创建一个layui.extend.js,在其中用Layui的模块扩展机制,重写form模块的notOption方法。这样完全无侵入,升级Layui版本时风险也最小。不过,这需要对Layui的模块化机制有更深的理解。
5. 拥抱现代前端框架:
如果你正在使用Vue、React等现代框架,那么可能不再直接使用Layui。但拼音搜索的需求依然存在。此时,你可以将myFilter这个核心匹配逻辑抽象成一个纯JavaScript函数,或者封装成一个自定义Hook(在React中)或Composable(在Vue 3中),然后在你选择的UI组件库(如Element Plus、Ant Design)的下拉框组件中应用它。这样,你的拼音搜索能力就变得与框架无关,可移植性更强。
技术的优化永无止境。从最初简单的字符串匹配,到引入拼音转换,再到考虑性能、兼容性和用户体验,每一步都是为了让工具更好地服务于人。当你看到运营同事不再抱怨,而是称赞这个搜索框“真智能”时,你就会觉得这些折腾都是值得的。
1万+



被折叠的 条评论
为什么被折叠?



