結論
chrome.tabs.executeScriptはいいぞ
対象
chrome拡張がbrowser_action, background, content_scriptsからなることを知っている。
背景
gotoスパゲッティにキレた
システムを構成するツールの実行環境には、計算機ネイティブ, スマホアプリ, webアプリ, GAS, Excel, chrome拡張などの選択肢があります。半手動のスクレイピングがしたい場合などにchrome拡張は有力な選択肢となります。
半手動のスクレイピングツールをchrome拡張でつくることを考えます。まず、アイコンクリックでポップアップメニューを表示しGUIを提供し、GUIからの操作は全てbackground.jsにメッセージを送る。background.jsはスクレイピング対象ページを開き、ページ上でcontent scriptsを動かしメッセージングにより目的を達成する。こういう設計が思いつきます。background.jsとcontent scriptsの間のインターフェイスをメッセージングのみとすることで、DOM操作をcontent scriptsに隠蔽できるメリットがあります。
background.jsとcontent scripts間のメッセージング
background.jsがcontent scriptsを呼び出しcontent scriptsでsendMessageしてbackground.jsでonMessageで受け取る方向で書くと、content scriptsでは条件により様々なメッセージを送ることになります。background.jsではそのメッセージに基づいてswitch等で分岐します。結果として、C言語ならmain関数に書かれるだろうメインとなる大きな処理の流れがbackground.jsではswitch等で散らばることになります。
background.js:chrome.extension.onMessage.addListener(function(message, sender, sendResponse) {
background.js: sendResponse(bot_mode);
background.js: sendResponse('ok');
background.js: sendResponse(res);
default.js: chrome.extension.sendMessage({type: 'retry'});
default.js: chrome.extension.sendMessage({type: 'open_****'});
****.js: chrome.extension.sendMessage({type: 'bot_mode'}, function(bot_mode) {
****.js: chrome.extension.sendMessage({type: '****_info'}, function(res) {
****.js: chrome.extension.sendMessage({type: '****_send_ok'});
login.js: chrome.extension.sendMessage({type: 'stop_smile'});
menu.js: chrome.extension.sendMessage({type: 'start_bot'}, function(res) {
menu.js: chrome.extension.sendMessage({type: 'reserve_bot'}, function(res) {
menu.js: chrome.extension.sendMessage({type: 'stop_bot'}, function(res) {
menu.js: chrome.extension.sendMessage({type: 'setting_saved'});
menu.js: chrome.extension.sendMessage({type: 'open_****'}, function(res) {
menu.js: chrome.extension.sendMessage({type: 'open_****'}, function(res) {
a.js: chrome.extension.sendMessage({type: 'bot_mode'}, function(bot_mode) {
a.js: chrome.extension.sendMessage({type: 'go_next'});
a.js: chrome.extension.sendMessage({type: 'retry'});
a.js: chrome.extension.sendMessage({type: 'go_next'});
a.js: chrome.extension.sendMessage({type: 'go_next'});
a.js: chrome.extension.sendMessage({type: 'go_next'});
a.js: chrome.extension.sendMessage({type: 'go_next'});
a.js: chrome.extension.sendMessage({type: '****_rireki', rireki: rireki});
message_list.js: chrome.extension.sendMessage({type: 'bot_mode'}, function(bot_mode) {
message_list.js: chrome.extension.sendMessage({type: 'replylist', list: list});
no_watch_register.js: chrome.extension.sendMessage({type: 'bot_mode'}, function(bot_mode) {
no_watch_register.js: chrome.extension.sendMessage({type: 'go_next'});
b.js: chrome.extension.sendMessage({type: 'bot_mode'}, function(bot_mode) {
b.js: chrome.extension.sendMessage({type: 'go_next'});
b.js: chrome.extension.sendMessage({type: 'retry'});
b.js: chrome.extension.sendMessage({type: 'go_next'});
b.js: chrome.extension.sendMessage({type: '****_profile', profile: profile});
c.js: chrome.extension.sendMessage({type: 'bot_mode'}, function(bot_mode) {
c.js: chrome.extension.sendMessage({type: 'mail_skip'});
c.js: chrome.extension.sendMessage({type: 'mail_skip'});
c.js: chrome.extension.sendMessage({type: 'mail_skip'});
d.js: chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
d.js: chrome.extension.sendMessage({type: 'bot_mode'}, function(bot_mode) {
d.js: chrome.extension.sendMessage({type: 'attacklist', list: list});
top.js: chrome.extension.sendMessage({type: 'bot_mode'}, function(bot_mode) {
top.js: chrome.extension.sendMessage({type: 'retry'});
background.js:160: case 'open_****': //ok //ok
background.js:180: case 'open_****': //ok//ok
background.js:187: case 'bot_mode': //ok
background.js:191: case 'start_bot': //ok
background.js:216: case 'stop_bot': // ok
background.js:229: case 'reserve_bot': // ok
background.js:244: case 'setting_saved': // ok
background.js:250: case 'replylist': //ok
background.js:318: case 'attacklist':
background.js:374: case 'retry':
background.js:381: case 'go_next':
background.js:425: case '****_rireki':
background.js:430: case '****_profile':
background.js:482: case '****_info': //ok
background.js:490: case '****_send_ok': //ok
background.js:500: case 'mail_skip':
background.js:506: case 'mail_done': // ok
さらに、content scriptsの呼び出しはurlを開くことと等価であるので、ときにそれは明示的な呼び出しでなく変数に収まった文字列であったりし、どのcontent scriptsが呼び出されるかすら自明でないというスパゲッティコードを発生させます。これが私が出会ったスパゲッティコードでした。
background.js:314: open_****_page(list.shift(), calc_wait(1, 3));
background.js:370: open_****_page(list.shift(), calc_wait(1, 3));
background.js:378: open_****_page(last_url, calc_wait(3, 5));
background.js:421: open_****_page(list.shift(), wait);
background.js:534: open_****_page(
background.js:579: open_****_page(list.shift(), wait);
background.js:732: open_****_page(
background.js:751: open_****_page(
background.js:756: open_****_page(
background.js:764: open_****_page(
background.js:769: open_****_page(
background.js:775:function open_****_page(url, wait) {
ここからは改善方法の提案です。background.jsがcontent scriptsを呼び出すのは変えられないが、どのcontent scriptsが呼び出されるであろうことを示しておくのがひとつ。background.jsがsendMessageしてcontent scriptsがonMessageからのsendResponseする方向で書くと、background.jsではメインとなる処理が散らばることなく書き下すことができます。
content scriptsを使わない方法
そもそも、DOMから少し情報を取得する目的ならcontent scriptsをつくらずbackground.jsだけで可能です。chrome.tabs.executeScriptにタブIDと文字列のjsを与えるとページ上でjsを実行してくれます。
私はbackground.jsにこう書いた。
this.tab = new Tab();
const format = f => '(' + f.toString() + ')();';
await this.tab.open('https://example.com/');
await this.tab.loaded();
const h1 = await this.tab.exec(format(function(){
return document.querySelector('h1').innerText;
}));
class Tab {
open(url){
return new Promise((resolve, reject) => {
try{
console.log('open:' + url);
const p = {url: url, active: false};
const c = tab => { this.id = tab.id; resolve(this.id); };
if( ! this.id )
chrome.tabs.create(p, c);
else
chrome.tabs.get(this.id, () => {
if( chrome.runtime.lastError )
chrome.tabs.create(p, c);
else
chrome.tabs.update(this.id, p, c);
});
}catch(e){
reject(e);
}
});
}
close(){
if( ! this.id ) return;
try{
chrome.tabs.get(this.id, () => {
if( chrome.runtime.lastError ){
console.log(chrome.runtime.lastError);
return;
}
chrome.tabs.remove(this.id, () => { this.id = null; });
});
}catch(e){
console.log(e);
}
}
loaded(){
return new Promise(resolve => {
const fire = (tabId, changeInfo, tab) => {
if( tabId != this.id || ! this.id || changeInfo.status != 'complete' ) return;
resolve();
chrome.tabs.onUpdated.removeListener(fire);
};
chrome.tabs.onUpdated.addListener(fire);
});
}
exec(code){
if( ! this.id ) return;
return new Promise((resolve, reject) => {
try{
chrome.tabs.executeScript(this.id, {code: code}, results => {
resolve(results[0]);
});
}catch(e){
reject(e);
}
});
}
}