一场由股票提醒助手插件引发的血案——浅入浅出 jquery autocomplete

来源:互联网 发布:算法在web上的应用 编辑:程序博客网 时间:2024/05/22 05:27
 
我没有学过前端,所以这篇文章注定要班门弄斧了。
通常,需要用到什么技术什么语言时,我才去学,学了也不一定掌握,就是记不住!所以现在明白了,学习的时候,亦或是攻克难点的时候,一定要记录下来,并不一定非要呈现什么高大上的技术,但求复原自己的心路历程足以。
大家都知道,最近股市很火爆,当然这几天正在调整期呵呵,神车复牌都交出一天涨停一天跌停的答卷自降为灵车,相比于528垂直过山车,见怪不怪了。很多人虽然没投入大量精力炒股,但还是时刻关注着股市行情,上班族们总不能时刻盯着手机看吧,所以我就想到了股票插件,chrome浏览器(361极速浏览器也OK)中的股票插件https://chrome.google.com/webstore/detail/%E8%82%A1%E7%A5%A8%E6%8F%90%E9%86%92%E5%8A%A9%E6%89%8B/goiffchdhlcehhgdpdbocefkohlhmlom#,这个玩意可以添加自选股,并且几乎实时地通过新浪财经同步(就是看着数字就放心了),试用过后发现挺有意思,于是尝试获取源码,把这个插件的所有html/js/css/image文件都下载下来,并且按照chrome插件开发教程把文件结构、manifest都一步步搞定,现在已经在github开源:https://github.com/hustlbj/chrome-extensions仓库中的StockHelper。
简单介绍一下这个小插件:在浏览器插件栏中的图标是background.html,实时获取自选股的价格,然后刷新到图标和标题中;popup.html是点击图标以后弹出的详情页面,可以查看每支股票的价格折线图、日K线图、成交量之类的详情,选中某支股票,则该支股票的当前价会实时刷新到图标中;options.html是点击自选股后打开的页面,进行自选股的添加删除,输入名称、简写、代码都可以查找。它的股票代码是保存在一个stocks.js文件中的,我们在添加新股票输入部分字母时,会执行autocomplete来自动搜索匹配stock.js中的股票然后为我们呈现一个选项列表,这就是自动填充的功能,如下图:
那么问题来了,将候选数据保存在本地文件(变量)中,虽然检索速度快,但是新股IPO以后还得更新插件版本或者你自己把代码加到stocks.js中,虽说不难,但是我可替广大小白表示忍不了。。好嘛,既然源代码都搞定了,修复个小bug也是顺便的事,不修复不舒服斯基。

查看源码,options.html中引用的是
jquery 1.7
autocomplete 1.1(注意版本,新的autocomplete的参数列表可能不同,这里不做讨论)
默认参数:
我们看autocomplete是如何使用的(input代表输入框对象):
input.autocomplete(stockInfos, {
     省略很多参数设置, 
     formatItem: function(){}, 
     fomatMatch: function(){}, 
     formatResult: function(){}
}).result(function() {})。
很明显,stockInfos就是从本地文件中读入的候选数据源,format相关函数就是规定自动补全时如何来匹配,最后的result则是选中某项候选值后界面需要做的显示工作。

首先,需要解决如何用远程数据源替换掉本地的静态数据源stockInfos来作为autocomplete的候选数据源(本文不探讨本地变量作为数据源。。)。不懂怎么办,搜索之,参考http://www.cnblogs.com/weixing/archive/2013/06/06/3120535.html 远程数据源的调用方法,这里要注意的是jquery.autocomplete或者是jquery.UI版本不对的话,可能并不是这些参数设置,比如新版本的jquery.UI中的autocomplete http://api.jqueryui.com/autocomplete/,我们还是使用autocomplete 1.1,看源码的话最好不要用min版本。修改的方法就是,把上面调用autocomplete时第一个参数stockInfos替换成URL,同时设置dataType参数,当需要向远程传递参数时还需要extraParams参数,如果远程传回的数据格式不适合autocomplete,我们还需要自定义parse函数。然后我先是改成了如下(http://suggest3.sinajs.cn/suggest/key=你的查找,这是新浪提供的接口,类似的还有很多,这里不做详述):

在浏览器中运行,当在输入框中输入几个字后,autocomplete向远程url请求数据,在浏览器的控制台Network界面,发现有请求和响应数据,并且响应数据和我们直接在浏览器中访问该url的内容一样比如http://suggest3.sinajs.cn/suggest/?q=shang&limit=10&timestamp=1433840697966&key=shang,除了key字段是我们的远程URL查找必需的以外,autocomplete自动添加了q、limit、timestamp参数但是收到数据(浏览器控制台的Response中输出了var suggestvalue="shanghai ind h,31,00363,00363,上海实业控股,shsykg;shanghai tonva。。。是正确地)后并没有执行parse函数和后续的format相关函数

错误排查:
首先检查autocomplete源代码(不要看min.js,下载对应版本的autocomplete源码),打开这个文件发现,autocomplete的参数列表:delay、scroll、highlight、max、minChar、dataType、extraParams等参数都是有的,formatItem等函数、parse函数也都有,但是似乎并没有执行parse()。
autocomplete中原始的parse函数,我们做一些log调试:
 
我们在调用autocomplete时自定义的parse函数,因为浏览器控制台并未输出自定义parse的log,所以该函数并未执行。原始的parse()函数也并未执行。

再根据autocomplete源码进行溯源,调用parse的函数是request函数,也就是根据url参数来向远程请求,由于浏览器控制台中有请求和响应,所以request函数肯定执行了,分析其源代码,ajax发送请求后如果成功则执行success标签对应的处理程序这里并没有error对应的处理程序,猜测可能是收到的错误响应,所以并没有执行success,我们添加error处理函数:
在浏览器控制台观察其运行,如我所料执行到了error处理程序中:
果然,ajax请求收到的是返回被判定为错误类型,所以并没有执行success处理函数。请求响应Headers如下:
发现响应的Headers的Content-Type是text/html,而我们在调用autocomplete时设置的dataType类型是json,显然是冲突的(不能盲目照抄别人的代码!还是要根据自己的实际情况!),所以request函数中认为收到的信息是错误的,所以没有执行success处理,也就没有执行parse()函数进行解析。

解决方案:
调用autocomplete时,dataType参数根据自己的数据源类型来定,比如我的数据源返回的是text/html,并不是json,所以我这里改成text。再次执行:
成功执行了自定义的parse函数。
这里,我的parse函数解析还有bug,所以解析返回的数组是空,请不要在意,根据原始数据来做调整处理即可。
parse函数解析成功后,构造一个数组parsed返回,在这里parse函数返回的是[Object, Object, Object, ...]如下:

然后会执行formatItem函数:
控制台报出item是undefined,并且item.name也报出了Uncaught TypeError: Cannot read property 'name' of undefined错误。一个问题解决,又一个问题来了。。。

排查和解决parse()解析源数据(数据格式不对,undefined错误)的问题:
继续追踪autocomplete源代码,request函数在success处理程序中执行parse函数结果保存在parsed变量中,然后调用success(term, parsed)函数,success()函数是调用request函数时传入的函数见request声明function request(term, success, failure)。继续追踪request调用的地方有两处,一处使search中的request(value, findValueCallback, findValueCallback),另一处是onChange()函数中的request(currentValue, receiveData, hideResultsNow),在这两处分别打上console.log("search")和console.log("onChange"),重新在浏览器中运行,发现Console端输出了onChange,说明是onChange函数调用了request来发起请求、处理返回结果等等后续。所以,request在执行完parse函数以后,调用了传进来的reveiceData函数,继续追踪receiveData函数,在该函数中打上console.log("receiveData"),还可以输出一下data[0],重新运行后发现输出了receiveData 和data[0]并无差错。receiveData(q, data)中调用了select.display(data, q),display中也打上console.log("display")输出日志,里面调用了init()函数和fillList()函数,init中并无数据处理相关代码,所以我们关注fillList()。进入fillList()函数,打上console.log("fillList")输出日志作为函数跟踪。不难发现,fillList()函数对我们的parse后的data做了一些处理然后传给了formatItem函数,至此,整条路径基本完成。观察它是如何调用formatItem函数的:
var formatted = options.formatItem(data[i].data, i+1, max, data[i].value, term);
而我们的data格式为:
data是一个数组,data[i]索引到数组中的元素,而data[i].data却未定义!所以我们在最初调用autocomplete时的formatItem(item, i, max)接收了三个参数data[i].data、i+1、max,而data[i].data是未定义的,所以formatItem时报出了item.name undefined错误!!OK,排查阶段终于完了!问题出在了parse返回的数据data并不符合fullList()函数中对data的操作!
所以,修改自定义的parse函数:

再次在浏览器中运行,成功了!

最后,总结一下autocomplete()的执行流程:
你的js中调用$(你的输入框).autocomplete(本地数据源或者远程URL, {
     参数列表, 
     dataType: json或者text等等, 
     extraParams:{参数名: function(){ return 参数值如$(你的输入框).val()}}, 
     parse: function(){}, 
     format相关函数,  
     }).result(function(){});
然后,当你在输入框中输入时,autocomplete就调用onChange函数->request()函数向URL发送请求->parse()函数处理远程的数据,然后产出一个数组[{data:{ }, value{ }}, {...}, {...}, {...}, ...] -> receiverData() -> display() -> fillList() -> formatItem()。

不管怎样,感谢七一〇提供的股票提醒助手,想研究的同学到我的github下载即可https://github.com/hustlbj/chrome-extensions
0 0