そろそろ本気でブックマークレットを作る

はじめに

本気で作ろうと思います。

ブックマークレットとは

(知っている方は読み飛ばしていただいて構いません)。

ブックマークレットというのは…

<a href="javascript:void(0)">ボタン</a>  

このhrefのやつの進化系の応用です。

もっと詳しく

hrefには普通、URLを記述します。
URLというのはロケーターとしてリソースの場所を指し示す識別子のことです。

Uniform Resource Locator(ユニフォーム リソース ロケータ、URL)または、統一資源位置指定子(とういつしげんいちしていし)とは、インターネット上のリソース(資源)を特定するための形式的な記号の並び。WWWをはじめとするインターネットアプリケーションにおいて提供されるリソースを、主にその所在を表記することで特定する。
Uniform Resource Locator - Wikipedia

javascript:から始まるものはURIの一種で、URLのお友達です。

Uniform Resource Identifier(ユニフォーム リソース アイデンティファイア、URI)または統一資源識別子(とういつしげんしきべつし)は、一定の書式によってリソース(資源)を指し示す識別子。1998年8月に RFC 2396 として規定され、2005年1月に RFC 3986 として改定された。URI はUniform Resource Locator (URL) の考え方を拡張したものである。
(略)
URIスキームはIANAによって登録されたものが公式なものとされている。その一方で、 javascript のように未登録ではあるが広く使われているスキームも存在する。
Uniform Resource Identifier - Wikipedia

  • URI
    • URL
    • JavaScript URI
    • その他諸々(aboutとかchromeとか)

何ができるの?

javascript:に続いて書かれたスクリプトを実行します。
上のやつならばvoid(0)が実行されます。つまり何も起きません。

試しに、以下のようなHTMLファイルを作ってみてください。

<!DOCTYPE html>  
<meta charset="UTF-8">  
<title>JavaScript URI Test</title>  
<a href="javascript:alert('Hello, World!')">Click me!</a>  

リンクをクリックすると、alertが表示されるはずです。

応用する

そのリンクをブックマークしてください(ブックマークバーにドラッグ・アンド・ドロップするなり、右クリックメニューからブックマークに追加するなり)。
そして、そのブックマークを開いてください。
alertが表示されるはずです。

任意のJavaScriptを任意のタイミングで任意のWebページで実行することができます。

基本的な作り方

voidを使う

javascript: から始まる URI をサポートしたブラウザに於いて、それは、URI 内のコードを評価し、戻り値が undefined でなければ、返された値にページコンテンツを置き換えます。
void 演算子 - JavaScript | MDN

気をつけてください!
javascript:confirm()みたいなブックマークレットを実行すると、Webページの内容が書き換えられてしまいます。

そんなわけで実際に作る際には以下のようにします。

javascript:void(confirm())  

即時関数を使う

ブックマークレットは、Webページに含まれるスクリプトと同じ領域で実行されます。
つまり、変数名のバッティングが十分にありえるということです。

しかし大した問題ではありません。即時関数を使えばいいだけですから。

javascript:void((function(){var $ = 'Hey!'; console.log($)})())  

これで、jQueryの使われているWebページでも、Webページを壊すことなく好きなようにできます。

ついでに、私はvarを見ると吐きそうになるので書き換えます。

javascript:void((() => {const $ = 'Hey!'; console.log($)})())  

はい。いいですね。

Strictモードを使う

任意ですが、Strictモードを使うことをおすすめします。
厳格なモードでスクリプトが実行されるようになります。

'use strict';みたいなもの、見たことありませんか? あれです。

  • ミスがエラーとして出力されるようになったり
  • 非推奨なものが使えなくなったり
  • 将来のJavaScriptで追加されるであろう識別子を「予約語」として使えなくしたり

します。ついでに実行速度が速くなるらしいです。

詳しくは: Strict モード - JavaScript | MDN

誤解を解く

ブックマークレットに関して、誤解や古い情報が溢れているのでここで一旦整理します。

1行で書かなければいけないという誤解

先程から、JavaScript URIを1行で書いていることに気づかれたかもしれません。
JavaScript URIとてURIなので、URLのように1行で書く必要があるのです。

しかし待ってください。
きっとあなたは、改行などを含む文字列をURI用に1行にする術をすでに持っているはずです。

encodeURIComponent('文字列')  

書いたスクリプトをエンコードし、出力された1行の文字列の先頭にjavascript:と書き足せばよいのです。

Node.jsなどを活用しましょう。

#!/usr/bin/env node  

const fs = require('fs');  

const f = fs.readFileSync(process.argv[2], { encoding: 'utf-8' });  
const encoded = encodeURIComponent(f);  

console.log(`javascript:${encoded}`);  

文字数制限がきついという誤解

「ブックマークレットは文字数制限がきつい」といわれたりすることが多いです。

本当にそうでしょうか?

#!/usr/bin/env node  

const f = '// comment\n'.repeat(3000) + 'alert(\'どう?\');'  
const encoded = encodeURIComponent(`void((()=>{${f}})())`);  

console.log(`javascript:${encoded}`);  

手元のVSCodeで見たところ、57068文字ありました。
これをブックマークのURL欄に入力し、そのブックマークを実行してみるとどうでしょうか?
少なくとも私の環境では、Google Chrome(Canary)でもFirefox(Developer Edition)でも実行できました。

どんなWebページでも実行できるという誤解

Content Security Policyによっては実行できません。

例えばtwitter.comなんかでは、以下のようなエラーメッセージがコンソールにでます(Firefox)。

Content Security Policy: ページの設定により次のリソースの読み込みをブロックしました: inline (“script-src”)  

回避する方法としては、いっそのこと、Webブラウザの拡張機能を作ってしまうという手があります。
browser​Action APIを使ってツールバーにボタンを表示して押されたときに実行したり、omnibox APIを使ってアドレスバーに指定した文字列が入力された場合に実行したり、ブックマークレットではできないようなことも可能です。
(いくらJavaScriptとはいっても学習コスト低くはないけどね…)。

コンバートするサービスもあります。

ただし、拡張機能を実際に使う場合は「署名」をしなければならず、そのために拡張機能のストアで開発者登録をする必要があります。
ChromeのChromeウェブストアの場合、開発者登録は有料です。また、署名するためにはWebストアにて公開する必要があります。
FirefoxのAMOの場合、開発者登録は無料で、署名だけして公開しないという選択も可能です。
ご注意ください。

ちなみに、twitter.comではconsole.logが書き換えられているようなので、デバッグではconsole.error等を使ってください。

実践的な作り方

テンプレ

javascript: void ((async (  

) => {  
    'use strict';  

})(  

));  

即時関数の引数として設定データを渡すなんてこともできます。

javascript: void ((async (  
    config  
) => {  
    'use strict';  
    alert(config.message);  
})(  
    {  
        message: 'Hello, World!',  
    }  
));  

ただし、Strictモードなので以下のようにはできません。

javascript: void ((async (  
    config = {                    // エラー  
        message: 'Hello, World!', // エラー  
    }                             // エラー  
) => {  
    'use strict';  
    alert(config.message);  
})());  

TypeScriptは使うべきか?

使えるなら使ったほうがいい気がしますが、私にはわかりません。使えないので。
スプレッド構文でNodeListを展開しようとしたらコンパイラに怒られました。そして、Array.from()を使ってみようとしたらfromなんてものはないとコンパイラに怒られました。トラウマです。

Babelは使うべきか?

使ってもいい気がしますが、自分用のものならば自分の環境に合わせて作ったほうがいろいろ楽そうです。

大規模な開発はどうするか?

大規模なブックマークレット開発というのがちょっと想像できませんが…。
Git使いましょう。でもってGitHub Pagesを使ってJavaScriptファイルをホストしましょう。そしてブックマークレットからそのJavaScriptファイルを読み込みましょう。

javascript: void ((async (  
    srcUrl  
) => {  
    'use strict';  
    const scriptElem = document.createElement('script');  
    scriptElem.src = srcUrl;  
    document.body.appendChild(scriptElem);  
})(  
    'https://username.github.io/path/to/script.js'  
));  

ちなみに、リポジトリにindex.htmlがないと、srcとして読み込むときにMIME Typeがどうたらというエラーが出てしまいますのでご注意ください。

配布はどうするか?

もし多くの人に使ってもらいたいという場合は、先程のようにGitHub Pagesにホストする方法を用いましょう。

Gitがめんどくさい場合はどうするか?

個人での開発の場合はGitが煩わしいかもしれません。そんなときはCodePen使いましょう。

javascript: void ((async (  
    srcUrl  
) => {  
    'use strict';  
    const scriptElem = document.createElement('script');  
    scriptElem.src = srcUrl;  
    document.body.appendChild(scriptElem);  
})(  
    'https://codepen.io/username/pen/penId.js'  
));  

ナウい読み込みがしたい!

まぁ確かに、scriptタグを作って追加するのは汚いですよね。そんなときはDynamic Importしましょう。

// GitHubなら  
javascript: void (import('https://username.github.io/path/to/script.js').then(m => m.default()));  
// CodePenなら  
javascript: void (import('https://codepen.io/username/pen/penId.js').then(m => m.default()));  

Importすると自動的にStrictモードになります。即時関数で囲む必要もなくなるので、とても短くなります。

リンク先では以下のようにします。

export default () => {  
    alert('Hello, World!');  
};  

追記: 場合によってはもっと短くすることもできます。一番下をご覧ください。

Dynamic Importは、Chrome・Opera・Safariでは使えます。
Edgeでは使えませんが、まぁいずれChromiumベースになりますし。
IEは捨てましょう。
Firefoxは、about:configからjavascript.options.dynamicImporttrueに設定することで使えるようになります。バージョン67からデフォルトでtrueになるようです。ちなみに正式版の67のリリースは2019/05/14日の予定。

おわりに

私は、GitHub Pagesとして公開し、Dynamic Importする方法をとっています。

ぜひ、ブックマークレットを作って楽してください!

追記: めっちゃ短く

ブックマークレットは以下の通り。

javascript:import('https://codepen.io/username/pen/penid')  

リンク先は以下の通り。

alert('Hello, World!');  

import()undefinedではなくPromiseを返しますが、Webページが置き換えられることはなさそうです(よくわからない)。

exportしていないライブラリもimport()できるようです(これまたよくわからない)。
ただ、あらかじめ読み込んでおいて任意のタイミングで実行、というようなことはできないので適材適所ですかね。

追記(2019/04/19)
あと、一回しか実行されませんねこれ…だめじゃないですか…。