在这次的讨论中,与大家分析一个使用js实现的拼音搜索程序,这个功能,相信在很多项目中都会用到。其实思路和原理应该都差不多,只是在算法上,会有性能之分。在今天要分享的程序中,主要还是讲解一些实现的思路,在算法上,相信还有更多更好的实现,小弟只是在此起个抛砖引玉的作用。希望能有更多的人加入到讨论中来。下面,我们就开始吧。
一.概述。
我们首先来看下程序运行的效果。
这是页面的初始效果。当我们在文本框中输入拼音或者汉字时,会对数据进行筛选,如下:
功能非常简单,下面我们就来分析下,在界面背后,程序到底做了什么?
二.思路
1. 拼音的数据哪里来的?
通过上面程序的运行我们可以看到,输入拼音就能显示匹配的汉字,那么,必定会有一个字典数据集合,保存了大部分品应所对应的汉字。就像下面这样:
window.PINYIN_DATA = { a:"啊阿嗄吖锕...", ai:"哎爱唉艾挨...", an:"案按安俺暗岸鞍...", ....}
就如同上面看到的,这个字段数据中保存了每种拼音对应的汉字,当然,它的数据量取决于你需要保存多少量的汉字与其对应的拼音。因为这个程序是纯js的,所以特别用了一个js文件
来保存上述的数据,名字的话就叫做PinYinData.js,我们使用的数据结构是对象字面量,可以以key/value的方式获取。
另外,上面页面中显示的学校的数据,同样保存到一个js文件中,叫做StudentData.js,它的数据结构如下:
var StudentData = [ { "id":1,"name":"北京大学"}, { "id":1,"name":"北京外国语大学"}, { "id":1,"name":"北京中央戏剧学院"}, ...];
可以看到,它的结构稍微和拼音数据的那个结构不一样,使用的是一个数组,然后里面每个元素是一个对象常量。
了解了数据的来源之后,我们就可以一个一个的流程进行分析了。
2.程序实现思路整体概述
在进行每个具体的代码流程讲解前,有必要把一个大概的思路与大家分享,形成一个整体性的认识。
首先,页面加载的时候,会去读取学校的数据,将学校名字提取多出来,显示在网页上。
然后,回去读取拼音数据,读取进来后,需要做一个转换的操作,要将{拼音:"汉字串"}这样的数据结构,转换为{"某个汉字",[拼音]},如何转?为什么要转?拼音为什么变为一个数组了?这个,会在接下来的详细分析中介绍。将转换后的结构保存起来。
最后,需要将所有的学校汉字串对应的拼音串保存起来,就像下面这样:
var hanToPinyin = { tags: "北京大学 beijingdaxue beijingdaixue" content: {id:1,name:"北京大学"}}_cache.push(hanToPinyin);
做完上面的工作,页面加载的工作就完成了。
当用户在文本框中输入拼音或者汉字的时候,就会去遍历_cache里面保存的对象的tags属性,看看有没有符合的,有的话,就将content部分取出来,将name显示在页面上。
以上,就是程序的一个整体,其实功能并不复杂也不多,它牵涉到如下几个文件。
下面我们就来具体分析。
三.详细代码分析
1.如何加载学校的数据并显示。
(1)首先,学校数据所在的js文件是肯定要导入的,如下:
在HTML文件中,定义了一个匿名函数,专门用于加载并显示学校数据,它的声明如下:
var loadSchool = function(callback)
它接收一个函数作为参数,这个callback的作用就是当我们把学校数据都提取出来并形成一个HTML串后,用来将这个HTML串赋值给某个div的innerHTML属性。
接下来,看看这个函数的主体,代码如下:
/** * 加载学校数据 */ var loadSchool = function(callback) { txt = []; //遍历学校数据 for (var i in studentsData) { txt.push("
代码pinyinEngine.setCache我们先忽略掉。在这里,最关键的就是那个for循环。也许有人会问,那个suidentData变量哪来的呢?回忆前面学校数据的定义:
var StudentData = [ { "id":1,"name":"北京大学"}, { "id":1,"name":"北京外国语大学"}, { "id":1,"name":"北京中央戏剧学院"}, ...];
它作为全局变量被定义在了studentData.js文件中。我们在这里遍历它,分别取出id和name部分,并拼上<li><a>的HTML元素。另外,在这里,我们拼接字符串使用了txt[]数组,在js中,如果要拼接很多字符串,使用数组比直接+=效率要高。
拼好以后,使用数组的join("")方法,将之转换为一个字符串,然后调用callback对这个HTML串进行处理。在这里,只是简要的显示在页面中,调用如下:
/** * 加载学校数据 * @parame {String} 包含学校数据的HTML代码 */ loadSchool(function(html) { $unisContent.innerHTML = html; });
$unisContent是一个div元素。
嗯,这一步完成了,而且也非常容易,接着,再来看看加载拼音数据并转换结构的代码。
(2)加载拼音数据并转换结构
在上面的分析中,我们忽略掉的那行代码,就是实现这个功能的。
//设置拼音引擎的缓存 pinyinEngine.setCache([studentsData[i].name],false,studentsData[i]);
pinyinEngine这个对象在pinyinEngine这个文件中。定义如下:
var pinyinEngine = function(my) { .......}(pinyinEngine || {})
使用这种方式在一个文件里定义一个对象,可以防止其他文件覆盖掉你的定义。比如,你在一个HTML文件中引用了两个外部js文件,其中后面的那个定义了一个对象,与前面那个js文件里定义的对象名字一样,后面的会覆盖掉前面的。使用这种方式定义,就不会。在多人开发时,推荐使用这种方式,特别是一个对象由不同人实现,又放在不同文件中。或者是引入了两个版本不同的库等等。
对象pinyinEngine的结构如下:
首先,它定义了两个变量:
//缓存所有的学校名称以及对应的拼音 var _cache = []; //保存历史筛选的记录 var _history = {};
其中_history用于保存历史搜索记录,以加快搜索速度。
两个内部需要使用到的函数
/** * 将拼音对应的汉字串转换为每个汉字对应的拼音* @return {Array} 转换后的结果*/function convertHanToPinyin() { ..... }/*** 利用笛卡尔乘机处理一字多读的情况* @parame {Array} 作为被乘数的拼音* @parame {Array} 作为乘数的拼音* @return 返回乘积的结果*/function product(arr1, arr2, sp) { ....}
以及四个可供外部调用的函数
/*** 根据关键字对数据进行筛选* @param {String} 关键字* @param {function} 一个回调函数,用于操作每次筛选的结果* @return {Array} 筛选的返回结果*/my.search = function(keyword, callback) { ......}/*** 将可供查询的内容设置到缓存中* @parame {Array} 一个包含汉字的数组* @parame {Any} 一个结构为id/content的对象*/my.setCache = function(tags, single, content, sp) { ......}/*** 重置缓存和历史记录*/my.resetCache = function() { ......}/*** 将指定的汉字转换为拼音* @parame {String} 汉字串* @parame {boolean} 是否只提取拼音的第一个字母* @parame {String} 提取的内容的分隔符*/my.toPinyin = function(text, single, sp) { ......}
首先关注的就是页面加载时调用的函数 - setCache,这个函数实现了两个功能,分别是转换结构和缓存所有学校名对应的拼音。通过上面的注释可知道,它有四个参数,分别是:
tags:包含汉字串的数字(在这里,就是学校名字,每取出一个学校名,就传给这个参数)
single:是否只取出每个汉字对应的拼音的首个字母
content:一个学校的数据结构{id...,name:....}
sp:汉字与拼音,拼音与拼音之间的分隔符(有些汉字存在一字多读,可能对应多个拼音)
弄清楚几个参数的作用后,就要深入它的代码了。如下:
var keys = tags, strKeys = ""; //循环遍历每个汉字串,取得汉字串的拼音 for (var i = 0, imax = tags.length; i < imax; i++) { keys.push(my.toPinyin(tags[i], single, sp || "\u0001")); } //将汉字串和对应的拼音展开为字符串 strKeys = keys.join(sp || "\u0001"); var obj = { tags: strKeys, content: content } _cache.push(obj);
关键是那个循环,它遍历tags数组中每个汉字串,然后调用my.toPinyin函数获得这个汉字串对应的拼音,并加入keys数组。my.toPinyin这个函数等下再来分析。最后,把上述字符串放到keys数组中,然后把keys数组展开成字符串,保存到对象obj中,然后把obj保存到_cache缓存中。例如,假如我们向该函数传入["北京大学"]这样一个汉字串,通过for循环后,则会得到如下一个字符串:
"北京大学 beijingdaxue beijingdaixue"
注意,"大"有两种读法,所以得到了两个拼音字符串。然后把这个字符串假如keys数组,因为值传递了一个汉字串,循环执行一次,然后把keys数组转换成字符串。使用如下形式保存下来
var obj = { tags:"北京大学 beijingdaxue beijingdaixue", content:{id:1,name:"北京大学"}}
最后,把该obj保存到缓存中。有多少个学校名字,就会得到多少个这种汉字对应拼音的串,所以,_cache就保存了所有的学校汉字和拼音串。我们搜索时,就到这个缓存中来搜索,找到匹配的就可以了。
那么,my.toPinyin这个函数,是如何分析出汉字串所对应的拼音的呢?
首先,在toPnyin函数中,需要转换拼音数据的结构,也就是说,把如下的结构:
window.PINYIN_DATA = { a:"啊阿嗄锕吖", .....}
转换成如下的结构:
var cache = { 啊:[a], 阿:[a], ... 大:[da,dai], ....}
这个功能,是由内部使用的函数convertHanToPinyin完成的:
/** * 将拼音对应的汉字串转换为每个汉字对应的拼音 * @return {Array} 转换后的结果 */ function convertHanToPinyin() { //获取拼音汉字表 datas = window.PINYIN_DATA || {}; var cache = {}; for (var i in datas) { var hans = datas[i];//获得该拼音下对应的所有汉字 var han = ""; //遍历汉字串的每个汉字 for (var j = 0, max = hans.length; j < max; j++) { han = hans.charAt(j); if (!cache[han]) { cache[han] = []; } //保存该汉字对应的拼音 cache[han].push(i); } } return cache; }
它首先获得拼音字典,然后遍历里面的每个拼音,取出每个拼音对应的汉字串。在第二个循环里面,取出这个汉字串里面的每个汉字,以这个汉字作为cache的key值,value值则是这个汉字的拼音。这样,当循环执行完毕后,cache就是程序需要的转换后的结构了。这样转换后,就能找到某个汉字对应的拼音。当然,也可以不进行转换,直接在拼音数据中查找。那就需要找到汉字所在的汉字串,然后取出这个汉字串所对应的拼音。
将汉字拼音的数据结构转换完成后,就是找出给出的汉字串对应的拼音了。在toPinyin中有如下代码:
if (len === 0) return text; else if (len === 1) { //如果只有一个汉字 py = cache[text]; if (single) return (py && py[0] ? py[0] : text); return py || [text]; } else { //多个汉字 for (var i = 0; i < len; i++) { py = cache[text.charAt(i)];//取得该汉字对应的拼音 if (py) { //如果存在对应的拼音 //是否只提取单个字符 pys[pys.length] = single ? py[0] : py; } else { pys[pys.length] = single ? text.charAt[i] : [text.charAt[i]]; } } //如果只返回汉字对应的第一个拼音字母,则不需要处理一字多读的情况 if (single) { return sp == null ? pys : pys.join(sp || ""); } //处理一字多读的情况 var arr1 = pys[0];//第一个拼音 var tmpArr = []; for (var k = 1, kmax = pys.length; k < kmax; k++) { tmpArr = product(arr1, pys[k], sp); arr1 = tmpArr.array; } return (sp == null ? arr1 : tmpArr.string); }
len就是给出的汉字串的长度。我们一步步看。第一个if就不用说了,汉字串中没有汉字,那就什么都不做,原样返回。第二个if,如果汉字串中只有一个汉字,那么就以这个汉字为key,到转换后的汉字拼音结构中去找(在回顾下上面给出的数据结构,一个汉字对应它的拼音),single是表示是否返回拼音的首字母。else才是关键的代码了。它包括两部分:
1.查找汉字串中每个汉字的拼音
2.处理一字多读
第1步应该都没什么问题,只是多了个循环。关键是如何处理一字多读的呢。程序使用的是笛卡尔乘积的方式,把原理说下:
假设汉字对应的拼音已经提取出来,其结构如下:
[[bei],[jing],[da,dai],[xue]]
它是一个数组结构,在出现一字多读的情况下,会出现一个嵌套的数组。程序的笛卡尔按照如下方式处理:
1.取出数组中第一个和第二个,即[bei],[jing]
2.将两个元素拼成一个元素的数组返回,即[beijing]
3.重复第一步,这时候,参数变成[beijing],[da,tai]
4.因为第二个参数是一个有两个元素的数组,按照笛卡尔乘机,会变成如下数组返回:[beijingda,beijingtai]
5.重复第一步,这时候,参数变成[beijingda,beijingdai],[xue]
6.同样按照笛卡尔乘机,会变成如下数组返回:[beijingdaxue,beijingtaixue]。并结束处理。明白了这个过程,大家再结合给出的代码仔细走一遍流程,就会明白了。
要注意,这个toPinyin函数会被调用多次,具体多少次,取决于有多少个学校名字,因为它需要找到所有的学校名字对应的拼音,最后会把学校名字连同对应的拼音一起存放到cache中。当程序进行搜索时,则是直接到这个cache中去搜索。代码如下:
/** * 根据关键字对数据进行筛选 * @param {String} 关键字 * @param {function} 一个回调函数,用于操作每次筛选的结果 * @return {Array} 筛选的返回结果 */ my.search = function(keyword, callback) { var cache = _cache; var history = _history; var values = [], number = 0; //如果这次所搜的关键词在上次已经搜索过,则只需在历史记录中搜索 if (history.keyword && history.keyword === keyword) { cache = history.value; } //在cache中进行筛选 for (var i = 0, max = cache.length; i < max; i++) { if (cache[i].tags.indexOf(keyword) !== -1) { number++; values.push(cache[i]); callback(cache[i].content); } } _history = { keyword: keyword, value: values, count: number } return values; }
keyword就是用户在界面上输入的拼音或者汉字。程序会首先到历史记录中搜索,如果在历史记录中有,则不会再去整个缓存中搜索了。这段代码不复杂,就交给大家自己分析了。
大家可以自己尝试做一个输入简体转换成繁体的程序,其原理应该是一样的。好了,今天就到这里吧。谢谢大叫,完整的源代码在这里。