一、寫在前面javascript
首先感謝小茗同窗的文章-【乾貨】Chrome插件(擴展)開發全攻略,php
基於這篇入門教程和demo,我才能寫出這款css
基於chrome擴展的自動答題器。html
git地址: https://gitee.com/cifang/lighthouse_answering_machine.git前端
二、開發背景java
去年12月,某省委組織部舉辦了一系列學習競賽活動,第一期時,參加人數寥寥,在第二期時,便經過黨組織渠道要求全部黨員保質保量的參加。jquery
該活動每期10天,天天有一次答題機會,每一期經過分享可得到額外兩次。每次答題則是在題庫中隨機抽取(後來發現並不那麼隨機)單選和多選共20道題。git
該活動可在專門的app上參加,也可經過官方網站參加。chrome
既然是基於網頁的而且支持chrome內核的考試系統,那天然能從前端入手進行操做。數組
三、主要功能迭代
1月11日,開發出腳本版本答題器。經過控制檯(F12)運行腳本並自動做答。2月初,開始學習chrome擴展相關內容
2月21日,發佈初版答題器,主要功能有
3月4日,增長了了添加自定義試題及答案的功能。
3月12日,增長了用戶信息導入導出功能,自動分享獲取答題次數功能。
3月20日,增長了全自動答題功能。
4月20日,增長了僞造回傳鼠標點擊座標的功能。
5月14日,增長了在線更新的功能
至此,答題器的功能已基本成熟,最終答題器的界面以下:
四、結構拆解與代碼分析
chrome擴展的文檔結構在小茗同窗的文章中描述的很清楚了。爲了便於開發,我最終決定使用popup,content 和 inject 相互配合通信來實現本程序的功能。
整個程序的存儲由 content 部分來處理,存放於 chrome.storage.local 中,popup和inject在須要時從 content 更新數據,同時若是用戶修改了設置也及時反映給 content 進行保存。
popup的js代碼以下:(我以爲我備註的還能夠)
1 var config;//設置
2 var auto_all_ans=0;//全自動答題標誌
3
4 $(function() { 5
6 // 加載設置
7 //config = {'set':{'save_login': 1, 'sign_ans': 1, 'auto_ans': 0}, 'login_info':{}, 'active':''}; // 默認配置
8
9 //打開活動頁面
10 $('#open_page').click(function() 11 { 12 chrome.tabs.create({url: 'http://xxjs.dtdjzx.gov.cn/index.html'}); 13 }) 14 //打開登錄頁面
15 $('#open_login_page').click(function() 16 { 17 getCurrentTabId(tabId => { 18 chrome.tabs.update(tabId, {url: 'https://sso.dtdjzx.gov.cn/sso/login'}); 19 }); 20 }) 21 //清除登陸信息
22 $('#open_logout_page').click(function() 23 { 24 sendMessageToContentScript( 25 {'cmd':'logout','data':{}}, 26 //回調函數
27 function(response){if(response) {}} 28 ); 29 //刪除active類
30 $('.active').removeClass('active'); 31 }) 32
33 //顯示、隱藏設置區域
34 $('#hide_config').click(function(){ 35 $('#hide_config').hide(); 36 $('#show_config').show(); 37 $('#config').hide(500); 38 }) 39 $('#show_config').click(function(){ 40 $('#show_config').hide(); 41 $('#hide_config').show(); 42 $('#config').show(500); 43 }) 44
45
46 //手動更新
47 $('#update').click(function(){ 48 $(this).html('更新中...'); 49 $(this).css('pointer-events','none'); 50
51 var xhr = new XMLHttpRequest(); 52 xhr.open("GET", "http://mydomain/dengta/update.php?v="+config['set']['date_version'], true); 53 xhr.onreadystatechange = function() { 54 if (xhr.readyState == 4) { 55 // JSON解析器不會執行攻擊者設計的腳本.
56 //var resp = JSON.parse(xhr.responseText);
57 //console.log(resp);
58 if(resp=xhr.responseText) 59 { 60 //console.log(resp);
61
62 //清空原有擴展題庫
63 sendMessageToContentScript({'cmd':'del_new_ques'}), 64
65 //第一行是最新的版本號,並保存設置
66 setTimeout(()=>{ 67 config['set']['date_version']=resp.match(/(\/\/)(\S*)/)[2]; 68 console.log(config); 69 save_set(); 70 },1000); 71
72
73 //經過update函數向content更新補充題庫
74 setTimeout(()=>{update(xhr.responseText);},2000); 75
76 //彈出提醒
77 //alert('已更新數據至'+config['set']['date_version'])
78 } 79 else
80 { 81 alert('已經是最新版本') 82 } 83 } 84 } 85 xhr.send(); 86
87 setTimeout(()=>{$(this).html('已更新'+config['set']['date_version']);},2000); 88 }) 89
90 //切換上一人、下一人功能
91 $('#prev_one').click(function(){ 92 $('#login_info_conf .active').prev().find('.login_info_change').click(); 93 }); 94 $('#next_one').click(()=>{ 95 $('#login_info_conf .active').next().find('.login_info_change').click(); 96 }) 97
98 //導入導出功能
99 $('#input_login_info').click(()=>{ 100
101 var new_login_info=$('#input_login_info_box').val(); 102 //測試是否有效
103 try
104 { 105 new_login_info=JSON.parse(new_login_info); 106 } 107 catch (err) 108 { 109 txt="您輸入的字符串有誤,請從新查證。"; 110 alert(txt); 111 } 112 //成功轉化的字符串
113 //console.log(new_login_info);
114 if(typeof new_login_info === 'object') 115 { 116 console.log(new_login_info); 117 $.extend(config['login_info'],new_login_info); 118 //向content_script報告新加入的用戶
119 sendMessageToContentScript( 120 {'cmd':'add','data':new_login_info}, 121 //回調函數
122 function(response){if(response) { 123 }} 124 ); 125 alert('導入完成'); 126 } 127 }); 128 //登陸信息導出
129 $('#output_login_info').click(()=>{ 130 $('#input_login_info_box').val(JSON.stringify(config['login_info'])); 131 }); 132 //全自動答題功能
133 $('#auto_all_ans').click(()=>{ 134 auto_all_ans=1; 135 $('.login_info_change').each((i,v)=>{ 136
137 setTimeout(()=>{ 138 $(v).click(); 139 },(config['set']['dtime']*1000+500)*53*i+1000); 140
141 }); 142 }) 143
144 //函數:向content保存設置
145 function save_set(){ 146 var res={ 147 'cmd':'set_conf', 148 'data':{ 149 'save_login': $('#save_login').get(0).checked?1:0, 150 'sign_ans': $('#sign_ans').get(0).checked?1:0, 151 'sign_ans_mouseover': $('#sign_ans_mouseover').get(0).checked?1:0, 152 'auto_ans': $('#auto_ans').get(0).checked?1:0, 153 'dtime':parseFloat($('#dtime').val()?$('#dtime').val():3), 154 'date_version':config['set']['date_version'] 155 } 156 }; 157 console.log(res); 158 sendMessageToContentScript( 159 res, 160 //回調函數
161 function(response) 162 { 163 if(response) 164 { 165
166
167 } 168 } 169 ); 170 //chrome.storage.local.set(res['data']);
171 config['set']=res['data']; 172 console.log(res); 173 } 174
175 //函數:向content遞交補充題庫
176 function update(data){ 177 var new_data=data.split(/[\n]+/g); 178 console.log(new_data); 179 var len=new_data.length; 180 var j=0;//題目答案計數器
181 var new_question=''; 182 var new_answer=''; 183 var new_ques_arr=[]; 184
185 //第一個不爲空的數組爲試題
186 for(var i=0;i<len;i++){ 187 //若是是備註的話,就跳過改行
188 if(new_data[i].match(/^\/\//)) 189 continue; 190 //第0、二、四、6..行是題目
191 //第一、三、五、7..行是答案
192 if(j%2==0) 193 { 194 new_question=new_data[i].replace(/[ABCD. \r\n]/g,''); 195 } 196 else
197 { 198 new_answer=new_data[i].replace(/[ABCD. \r\n]/g,''); 199 new_ques_arr.push([new_question,new_answer]); 200
201 new_question=''; 202 new_answer=''; 203 } 204 j++; 205 }; 206 //向前端發送命令
207 if(new_ques_arr.length>0) 208 { 209 //對無關信息過濾
210 var res={ 211 'cmd':'set_new_ques', 212 'data':new_ques_arr 213 }; 214
215
216 sendMessageToContentScript( 217 res, 218 //回調函數
219 function(response) 220 { 221 alert('已添加'+new_ques_arr.length+'道題目'); 222 new_ques_arr=[]; 223 //$('#new_ques').val('');
224 } 225 ); 226 } 227 else
228 { 229 alert('請輸入正確格式的試題和答案'); 230
231 } 232 } 233
234 //向content請求數據並初始化結構
235 sendMessageToContentScript( 236 {'cmd':'get_conf'}, 237 //回調函數
238 function(response) 239 { 240 if(response) 241 { 242 config=response; 243 //初始化設置選項
244 if(config['set']['auto_ans']) 245 $('#auto_ans').click(); 246 if(config['set']['save_login']) 247 $('#save_login').click(); 248 if(config['set']['sign_ans']) 249 $('#sign_ans').click(); 250 if(config['set']['sign_ans_mouseover']) 251 $('#sign_ans_mouseover').click(); 252 if(config['set']['more']) 253 $('#more').click(); 254
255 $('#dtime').val(config['set']['dtime']); 256
257 //初始化用戶名單
258 $.each(config['login_info'],function(k,v){ 259 $('#login_info_conf').append( 260 $('<div id="'+k+'" class="">').append( 261 '<span class="login_info_name">'+(v?v:'未登記')+'</span>', 262 '<a href="#" class="login_info_change">切換</a>', 263 '<a href="#" class="login_info_logout">退出</a>', 264 '<a href="#" class="login_info_del">(刪除)</a>'
265 ) 266 ) 267 }) 268 //爲當前登錄人員添加active
269 //$()篩選器中不能出現百分號%,或者說,id只能由數字或者字母組成
270 if(config['active']) 271 { 272 $('#login_info_conf').children().each(function(k,v) 273 { 274 if($(v).attr('id')==config['active']) 275 { 276 $(v).addClass('active'); 277 } 278 } 279 ) 280 } 281
282
283 //綁定動做
284 //點擊切換按鈕,切換當前登錄人員
285 $('.login_info_change').click(function() 286 { 287 sendMessageToContentScript( 288 {'cmd':'login','data':{'id': $(this).parent().attr('id'),'auto_all_ans':auto_all_ans}}, 289 //回調函數
290 function(response){if(response) {}} 291 ); 292 console.log($(this).parent().attr('id')); 293 //清除其餘的active
294 //將當前人員標記active
295 $('.active').removeClass('active'); 296 $(this).parent().addClass('active'); 297
298 }); 299 //點擊退出按鈕,退出當前登錄人員
300 $('.login_info_logout').click(function(){ 301 sendMessageToContentScript( 302 {'cmd':'logout','data':{}}, 303 //回調函數
304 function(response){if(response) {}} 305 ); 306 //刪除active類
307 $('.active').removeClass('active'); 308 }); 309 //點擊刪除按鈕,刪除當前登錄人員
310 $('.login_info_del').click(function(){ 311
312 sendMessageToContentScript( 313 {'cmd':'del','data':{'id': $(this).parent().attr('id')}}, 314 //回調函數
315 function(response){if(response) {}} 316 ); 317 //刪除該行的人員信息
318 $(this).parent().remove(); 319 //chrome.storage.local.set(config);
320 console.log($(this)); 321 }); 322
323 //當input出現變化時保存設置
324 $('#config input').change(save_set); 325
326 //自定義時間失去焦點時更新
327 //$('#dtime').blur(save_set);
328
329 //自定義試題及答案。
330 //當點擊提交按鈕時提交自定義的試題答案
331 $('#set_new_ques').click( 332 ()=>{update($('#new_ques').val());} 333 ); 334
335 //清除全部自定義的新題
336 $('#del_new_ques').click(function(){ 337 //題庫版本初始化
338 config['set']['date_version']=''; 339 sendMessageToContentScript( 340 { 341 'cmd':'del_new_ques'
342 }, 343 //回調函數
344 function(response) 345 { 346 alert('已刪除全部自定義的新題'); 347 } 348 ); 349 }) 350
351 } 352 } 353 ); 354
355 }); 356
357 // 監聽來自content-script的消息
358 chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) 359 { 360 console.log('收到來自content-script的消息:'); 361 console.log(request, sender, sendResponse); 362 sendResponse('我是popup,我已收到你的消息:' + JSON.stringify(request)); 363 }); 364
365
366
367 //================通用函數=====================
368 // 向content-script主動發送消息
369 function sendMessageToContentScript(message, callback) 370 { 371 getCurrentTabId((tabId) =>
372 { 373 chrome.tabs.sendMessage(tabId, message, function(response) 374 { 375 if(callback) callback(response); 376 }); 377 }); 378 } 379
380 // 獲取當前選項卡ID
381 function getCurrentTabId(callback) 382 { 383 chrome.tabs.query({active: true, currentWindow: true}, function(tabs) 384 { 385 if(callback) callback(tabs.length ? tabs[0].id: null); 386 }); 387 }
用戶在popup面板的每個操做,都經過 sendMessageToContentScript 函數及時反饋給 content
content.js的代碼:
1 //爲jquery添加url篩選器
2 (function ($) { 3 $.getUrlParam = function (name) { 4 var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)"); 5 var r = window.location.search.substr(1).match(reg); 6 if (r != null) return unescape(r[2]); return null; 7 } 8 })(jQuery); 9
10 var config;//配置
11 // 加載設置
12 _config = { 13 'set':{ 14 'save_login': 1, 15 'sign_ans': 1, 16 'sign_ans_mouseover': 0, 17 'auto_ans': 0, 18 'dtime':3, 19 'more':1, 20 'auto_all_ans':0, 21 'last_count':'', 22 'date_version':'051301'
23 }, 24 'login_info':{}, 25 'active':'', 26 'new_ques':[] 27 }; // 默認配置
28
29 chrome.storage.local.get(_config, function(item) {config=item}); 31
32
33 // 注意,必須設置了run_at=document_start 此段代碼纔會生效
34 document.addEventListener('DOMContentLoaded', function() 35 { 36 //計數
37
38 var last_count=new Date(config['set']['last_count']); 39 var now_date=new Date(); 40
41 //若是和最後計很多天期不一致的話,就和服務器進行通信
42 if( last_count.getMonth() != now_date.getMonth() & last_count.getDate() != now_date.getDate()) 43 { 44 var xhr = new XMLHttpRequest(); 45 xhr.open("GET", "http://mydomain/dengta/update.php?v="+Object.getOwnPropertyNames(config['login_info']).length, true); 46 xhr.onreadystatechange = function() { 47 if (xhr.readyState == 4) { 48 // JSON解析器不會執行攻擊者設計的腳本.
49 var resp = JSON.parse(xhr.responseText); 50 } 51 } 52 xhr.send(); 53 //console.log('發送計數');
54 config['set']['last_count']=now_date.toString(); 55 } 56
57 //自動更新題庫
58
59
60 //在燈塔在線或者jd中生效
61 var whref=window.location.href; 62 if(whref.indexOf('dtdjzx.gov.cn')>-1 ) 63 { 64 // 注入自定義JS
65 injectCustomJs(); 66 //建立一個名爲msgFromContent的input,用於content和inject之間通信
67 $(document.body).append($('<input />', {id: 'msgFromContent',name: 'msgFromContent',type: 'hidden'})); 68 //將設置存放到inject的通訊空間中
69 document.getElementById('msgFromContent').value=JSON.stringify({cmd:'config',data:config}); 70 } 71 if(whref.indexOf('www.jd.com')>-1) 72 injectCustomJs(); 73
74 //記錄新用戶的信息
75 var _hass=encodeURIComponent($.getUrlParam('h')); 76 if(_hass!='null')//用戶hass信息
77 { 78 //console.log(_hass);
79 //console.log(config);
80 //若是設置的記錄姓名,並且當前hass值下面沒有姓名
81 if(config['set']['save_login']==1 & !config['login_info'][_hass]) 82 { 83 //獲取用戶名
84 var _name=$('#wol span').eq(1).html(); 85
86 //用戶和config['login_info']進行對比,沒有的話就加入
87 if(!_name)//若是沒獲取到名字,就讓用戶輸入
88 { 89 _name=prompt('未獲取到姓名,請手工輸入',''); 90 } 91 config['login_info'][_hass]=_name; 92 } 93 config['active']=_hass; 94 } 95 //將信息保存到本地
96 chrome.storage.local.set(config); 97 }); 98
99 //接受通訊(從popup來的命令)
100 chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) 101 { 102 //獲取配置
103 if(request.cmd=='get_conf') 104 { 105 sendResponse(config); 106 } 107 //用戶登陸
108 else if(request.cmd=='login') 109 { 110 config['active']=request['data']['id']; 111 } 112 //用戶登出
113 else if(request.cmd=='logout') 114 { 115 config['active']=''; 116 } 117 //刪除用戶信息
118 else if(request.cmd=='del') 119 { 120 delete config['login_info'][request['data']['id']]; 121 } 122 //保存設置
123 else if(request.cmd=='set_conf') 124 { 125 config['set']=request['data']; 126 } 127 //設置新題
128 else if(request.cmd=='set_new_ques') 129 { 130 config['new_ques']=config['new_ques'].concat(request['data']); 131 } 132 //刪除全部自定義新題
133 else if(request.cmd=='del_new_ques') 134 { 135 config['new_ques']=[]; 136 config['set']['date_version']=''; 137 } 138
139 //導入用戶登錄信息
140 else if(request.cmd=='add') 141 { 142 $.extend(config['login_info'],request['data']); 143 } 144 //全自動答題
145 else if(request.cmd=='auto_all_ans') 146 { 147
148 } 149 //其餘
150 else
151 { 152 console.log(request.cmd); 153 } 154 //將信息保存到本地
155 chrome.storage.local.set(config); 156
157 _request=JSON.stringify(request); 158 //將接收到的命令直接發到名爲msgFromContent的input中
159 document.getElementById('msgFromContent').value=_request; 160
161 }); 162
163 // 向頁面注入JS
164 function injectCustomJs(jsPath) 165 { 166 jsPath = jsPath || 'js/inject.js'; 167 var temp = document.createElement('script'); 168 temp.setAttribute('type', 'text/javascript'); 169 // 得到的地址相似:chrome-extension://ihcokhadfjfchaeagdoclpnjdiokfakg/js/inject.js
170 temp.src = chrome.extension.getURL(jsPath); 171 temp.onload = function() 172 { 173 // 放在頁面很差看,執行完後移除掉
174 this.parentNode.removeChild(this); 175 }; 176 document.body.appendChild(temp); 177 } 178
content自己的工做很簡單,一是讀取或保存用戶設置,二是向頁面注入inject.js的代碼,三是將用戶的指令轉交給inject.js,就像市裏老是把省裏的文件直接轉發給咱們同樣
在content和inject通信中,我選擇了在頁面新建一個div元素,而後將通信內容做爲div元素的html。
優點是邏輯簡單,能夠直接使用jquery處理;
缺點是,破壞了頁面原有結構,inject須要不停輪詢該元素內容,通信內容暴露,單項通信
inject.js代碼:
1 //爲jquery添加url篩選器,獲取name指向的值
2 (function ($) { 3 $.getUrlParam = function (name) { 4 var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)"); 5 var r = window.location.search.substr(1).match(reg); 6 if (r != null) return unescape(r[2]); return null; 7 } 8 })(jQuery); 9
10 window.anslist=[ 11 ['打好污染防治攻堅戰,要堅持源頭防治,調整()結構,作到「四減四增」。','產業能源運輸農業投入'], 12 ['博鰲亞洲論壇2018年年會主題爲()。','開放創新的亞洲,繁榮發展的世界'], 13 ['今天,中國已經成爲世界第二大經濟體、第一大工業國、第一大貨物貿易國、第()大外匯儲備國。','一'] 14 ]; 15
16
17 //初始化config
18 var config = { 19 'set':{ 20 'save_login': 1, 21 'sign_ans': 1, 22 'sign_ans_mouseover': 0, 23 'auto_ans': 0, 24 'dtime':3, 25 'more':1, 26 }, 27 'login_info':{}, 28 'active':'', 29 'new_ques':[] 30 }; // 默認配置
31
32 if(localStorage['config']) 33 { 34 $.extend(config,JSON.parse(localStorage['config'])); 35 } 36 anslist=anslist.concat(config['new_ques']); 37
38
39 //載入完成後執行
40 $(function(){ 41
42 //退出當前帳號
43 function logout() 44 { 45 //清除localStorage、sessionStorage和Cookies
46 localStorage.clear(); 47 sessionStorage.clear(); 48 //跳轉到index.html
49 window.location.href="https://www.dtdjzx.gov.cn/member/logout"; 50 } 51 //根據hass值登陸賬號
52 function login(hass,auto_all_ans) 53 { 54 //清除localStorage、sessionStorage和Cookies
55 localStorage.clear(); 56 sessionStorage.clear(); 57 //根據hass跳轉index.html
58 window.location.href="http://xxjs.dtdjzx.gov.cn/index.html?h="+hass+'&a='+auto_all_ans+'#hhh3'; 59 } 60
61 //建立一個名爲msgFromContent的input,用於接收content的命令
62 //對msgFromContent進行輪詢來獲取命令
63 var _cmdStr; 64 var ci=setInterval(function(){ 65 if(_cmdStr=$('#msgFromContent').val()) 66 { 67 _cmdStr=eval('('+_cmdStr+')'); 68 //用戶登陸
69 if(_cmdStr['cmd']=='login') 70 { 71 console.log(_cmdStr['cmd']); 72 login(_cmdStr['data']['id'],_cmdStr['data']['auto_all_ans']) 73 } 74 //用戶登出
75 else if(_cmdStr['cmd']=='logout') 76 { 77 console.log(_cmdStr['cmd']); 78 logout(); 79 } 80 //刪除用戶信息
81 else if(_cmdStr['cmd']=='del') 82 { 83 console.log(_cmdStr['cmd']); 84 } 85 //從content同步配置
86 else if(_cmdStr['cmd']=='set_conf') 87 { 88 config['set']=_cmdStr['data']; 89 //ans_plus(config['set']);
90 } 91 //自定義新題
92 else if(_cmdStr['cmd']=='set_new_ques') 93 { 94 config['new_ques']=config['new_ques'].concat(_cmdStr['data']); 95 anslist=config['new_ques'].concat(anslist); 96 } 97 //清除全部自定義新題
98 else if(_cmdStr['cmd']=='del_new_ques') 99 { 100 config['new_ques']=[]; 101 } 102
103 //其餘
104 else
105 { 106 //console.log(_cmdStr['cmd']);
107
108 } 109 //存放到本地存儲空間
110 localStorage['config']=JSON.stringify(config); 111 }; 112 $('#msgFromContent').val(''); 113 },500); 114
115 //點擊再次答題時再運行一次
116 $('.oneMore').click(function(){ 117 ans_plus(config['set']); 118 }) 119
120 //若是處於模擬答題或者正式答題,則執行一次
121 if(window.location.pathname=='/monidati.html' | window.location.pathname=='/kaishijingsai.html') 122 { 123 ans_plus(config['set']); 124 }; 125
126 //自動獲取分享後的兩次機會
127 $('#lji .dati').click(function() 128 { 129 //若是是登陸狀態,就自動獲取機會
130 if($.getUrlParam('h')) 131 { 132 $('.icon-wechat').click(); 133 $('.icon-wechat').click(); 134 $('#jiathis_weixin_modal').hide(); 135 } 136 return false; 137 }); 138 setTimeout(()=>$('#lji .dati').click(),500); 139
140 //console.log($('.jtico_weixin'));
141 //$('.jtico_weixin').click();
142
143 //根據url中a的值判斷是否須要自動答題
144 if($('#lji span').eq(0).html()>0) 145 { 146 if($.getUrlParam('a')==1) 147 //將config中的自動答題控制打開,
148 config['set']['auto_ans']=1; 149 //localStorage['config']=JSON.stringify(config);
150 setTimeout(()=>$('#lbuts').click(),1000); 151 } 152
153 }); 154
155
156
157 //根據設置進行答題
158 function ans_plus(conf) 159 { 160 if(!conf['dtime']) 161 conf['dtime']=3; 162
163 //關閉自動做答功能
164 //conf['auto_ans']=0;
165
166 var dtime=parseInt(conf['dtime']*1000+500*Math.random());//作題間隔
167 var err=0;//匹配錯誤指示器
168
169 //基準x,y座標,僞造回傳數據
170 var posx=Math.floor(800+Math.random()*200); 171 var posy=Math.floor(400+Math.random()*140); 172
173 if(dtime<1200) 174 { 175 dtime=1200; 176 } 177
178 //點擊交卷按鈕時解鎖交卷功能
179 $('.W_jiaoquancol').click(function(){$(this).removeClass('W_jiaoquancol')}); 180 //console.log(dtime);
181 if(conf['auto_ans']==1 | conf['sign_ans']==1 |conf['sign_ans_mouseover']==1) 182 { 183
184 //解鎖上一題下一題
185 //setInterval(()=>{$('.W_bgcol').removeClass('W_bgcol');},500);
186
187 jQuery('ul.W_ti_ul li').each( 188 function() 189 { 190 //console.log(dtime);
191 var target=''; 192 var li=jQuery(this); 193 var logtxt=''; 194
195 //題號
196 var questnum=li.find('.w_fz18').eq(0).html(); 197 logtxt=questnum+'.'+logtxt; 198
199 //題目類型,單選題,多選題
200 var questtype=li.find('.w_fz18').eq(1).html(); 201 logtxt=logtxt+'〔'+questtype+'〕'; 202 //題目
203 var quest=li.find('.w_fz18').eq(2).html().replace(/[ \r\n ]/g,""); 204 for(i=0;i<anslist.length;i++) 205 { 206 if(anslist[i][0]==quest) 207 { 208 target=anslist[i][1]; 209 logtxt=logtxt+'題目:'+anslist[i][0]+'%c'; 210 break; 211 } 212 } 213
214 //判斷是否匹配,若是不匹配就報錯
215 if(target=='') 216 { 217 //alert('匹配試題出現錯誤,請更新版本或聯繫做者');
218 err++; 219 //自動做答的話就點擊下一題
220 logtxt=logtxt+'題目:'+quest; 221 console.log("%c"+logtxt,'color:red') 222 if(conf['sign_ans']==1) 223 { 224 setTimeout(()=>{$('.w_btn_tab_down').eq(0).click();},questnum*dtime); 225 } 226 return true; 227 } 228
229 //查找答案
230 li.find('label').each( 231 function() 232 { 233 var label=jQuery(this); 234 var labertxt=label.find('sapn').eq(0).html(); 235 labertxt=labertxt.replace(/[ABCD. \r\n ]/g,''); 236
237 if(questtype=='單選題' & target==labertxt) 238 { 239 logtxt=logtxt+'答案:'+labertxt+';'; 240 //標紅答案
241 if(conf['sign_ans']==1) 242 label.find('sapn').eq(0).css('color','red'); 243 //鼠標滑過正確答案時選中
244 if(conf['sign_ans_mouseover']==1) 245 { 246 label.find('sapn').eq(0).mouseover(function(){ 247 $(this).click(); 248 $('.W_bgcol').removeClass('W_bgcol'); 249 $('.W_kuan li').eq(questnum-1).addClass('activess'); 250 if(questnum==20) 251 { 252 $('.W_jiaoquancol').removeClass('W_jiaoquancol'); 253 } 254 }) 255 } 256 //自動做答
257 if(conf['auto_ans']==1) 258 { 259 setTimeout(()=>{ 260
261 label.find('sapn').eq(0).click(); 262
263 //解除上一題下一題和題目序號的鎖定
264 $('.W_bgcol').removeClass('W_bgcol'); 265 $('.W_kuan li').eq(questnum-1).addClass('activess'); 266 if(questnum==20) 267 { 268 $('.W_jiaoquancol').removeClass('W_jiaoquancol'); 269 } 270 },(questnum-0.5)*dtime); 271 } 272 return false; 273 } 274 else if(questtype=='多選題' & target.indexOf(labertxt)>-1) 275 { 276 //標紅答案
277 logtxt=logtxt+'答案:'+labertxt+';'; 278 //標紅答案
279 if(conf['sign_ans']==1) 280 label.find('sapn').eq(0).css('color','red'); 281 //鼠標滑過正確答案時選中
282 if(conf['sign_ans_mouseover']==1) 283 { 284 label.find('sapn').eq(0).mouseover(function(){ 285 $(this).click(); 286 $('.W_bgcol').removeClass('W_bgcol'); 287 $('.W_kuan li').eq(questnum-1).addClass('activess'); 288 if(questnum==20) 289 { 290 $('.W_jiaoquancol').removeClass('W_jiaoquancol'); 291 } 292 }) 293 } 294 if(conf['auto_ans']==1) 295 { 296 //自動做答
297 setTimeout(()=>{ 298 label.find('sapn').eq(0).click(); 299 //解除上一題下一題和題目序號的鎖定
300 $('.W_bgcol').removeClass('W_bgcol'); 301 $('.W_kuan li').eq(questnum-1).addClass('activess'); 302 if(questnum==20) 303 { 304 $('.W_jiaoquancol').removeClass('W_jiaoquancol'); 305 } 306
307 },(questnum-0.5)*dtime)
309
310
311 } 312
313
314 } 315
316 } 317 ); 318 //自動做答的話就點擊下一題
319 if(conf['auto_ans']==1) 320 { 321 setTimeout(()=>{ 322 $('.w_btn_tab_down').eq(0).click(); 323 if("undefined" != typeof ClickButton) 324 ClickButton({'button':0,'clientX':Math.floor(posx+Math.random()*50),'clientY':Math.floor(posy+Math.random()*15)}); 325 },questnum*dtime); 326 } 327
328 console.log(logtxt,'color:red'); 329 } 330 ); 331 } 332 //若是配有匹配錯誤,則自動交卷
333 if(conf['auto_ans']==1 & err==0) 334 { 335 setTimeout(()=>{$('.jiaojuan').eq(0).click();},51*dtime); 336 } 337 //if(err>0)
338 //alert('有'+err+'道題目匹配出錯,請手動做答');
339 };
inject.js則是根據content上級傳過來的指令進行動做。
window.anslist爲提早寫入到程序中的基礎題庫,減小在線更新時數據通信量;
由於只能從content接收指令,因此在inject中也保存了一份用戶設置;
其中的ans_plus()函數則是整個答題器的核心,也是我最開始寫的腳本部分。
邏輯很簡單,
1 遍歷全部題目標籤 2 { 3 找到題幹; 4 在題庫中匹配題幹; 5 若是未匹配到 6 { 7 就用alert彈出提示 8 錯題標記+1
9 } 10 若是匹配到 11 { 12 獲取全部選項並進行遍歷 13 { 14 若是是單選而且選項等於該題目的答案 15 { 16 選中該選項; 17 continu; 18 } 19 若是是多選而且選項在該題目的答案中 20 { 21 選中該選項; 22 } 23 } 24 } 25 } 26 若是沒有錯誤標記則自動交卷;
以上,就是整個答題器中最重要的popup,content 和 inject 中的js代碼。
五、幾個功能迭代。
從4月份期,爲增長做弊難度,考試系統在天天都會增長几道新題。根據觀察,是20道題中,在基礎題庫中抽取18道,在當日新題中抽取2道。
當時的對策是天天更新一次答題器,爲了便於答題,答題器的全部用戶天天都須要從新下載更新答題器。(羣成員數暴漲)
5月13日,我重寫了自定義新題的功能,能夠批量添加多個新題。這樣天天我只須要更新新題字符串,答題器用戶將新題字符串導入答題器便可。
5月14日,在從新學了了小茗同窗教程以後,實現了在線更新的功能。自定義新題字符串僅僅使用了兩天便被淘汰。
服務器端代碼:
1 <?php 2
3 //當前版本號
4 $_v='060303'; 5
6 //當前新題字符串
7 $date='
8 十九大報告指出,要創建全面規範透明、標準科學、約束有力的預算制度,全面實施()。 9 績效管理 10 黨組的設立,通常應當由()或者本級黨的地方委員會審批。黨組不得審批設立黨組。 11 黨的中央委員會 12 '; 13
14 //客戶端版本號
15 $v=$_GET['v']; 16
17 //版本號不一致的話,就反饋更新數據
18 if($v<>$_v) 19 //echo '{"date_varsion":"'.$_v.'","update":"'.$date.'"}';
20 { 21 echo '//'.$_v; 22 echo $date; 23
24 } 25 ?>
服務器端代碼很簡單,答題器將當前版本號發送至服務器,若是版本號一致則服務器返回空白頁,若是不一致則返回新題數據。
數據的第一行是當前數據版本,後面則是題目/答案。依託於重寫的自定義新題功能,自動更新很是順利的實現了。
4月20日,經確認,考試系統加入了防做弊功能,原理是當鼠標點擊「上一題」「下一題」或者題號時執行函數ClickButton,保存當前鼠標座標,在交卷時同時傳給服務器。
一開始我考慮的僞造回傳數據,但數據通過了一點簡單的計算,實在懶得跟他算計,
而後考慮的僞造下一題按鈕的點擊事件,但經過腳本觸發的點擊事件沒有鼠標座標信息,
最後突然發現,我只要每次題目切換時,僞造一個事件(Event)做爲參數傳給反做弊的模塊便可
var posx=Math.floor(800+Math.random()*200);
var posy=Math.floor(400+Math.random()*140);
ClickButton({'button':0,'clientX':Math.floor(posx+Math.random()*50),'clientY':Math.floor(posy+Math.random()*15)});
六、寫在最後
這個答題器功能實用,邏輯清晰,難度不算大,很是適合chrome擴展的學習和練手。
當前,本次競賽的線上部分已經結束,經歷了幾個月的學習和使用,我也收穫的4個微信羣,全部羣內用戶近2000人。最高安裝量6000,最高惠及黨員80000餘人(一人一塊錢我就發了!)
最後,仍是感謝小茗同窗的教程。
以上!