GitHubPagesで汎用JSをブックマークレットに与える
はじめに
みなさんこんにちは、ALH開発事業部のREIYAです。
今回はGitHubPagesを用いてAPIを構えて
ブックマークレットを便利にするアイデアをまとめました。
紹介すること
GitHub PagesでAPI用意
GitHub Pagesでは、レポジトリのファイルをWebページとして公開できます。
自分でサーバを立てなくても静的コンテンツを公開できるため、フロントサーバの代替として用いたうえで
別途バックエンドサーバを建て、APIをフロントから呼べるような構造が多いです。
GitHub Pagesでdocsを公開し
docs/test/index.html
docs/index.html
とファイルがある場合、以下のようなアクセスをしますが
https://ユーザ名.github.io/レポジトリ名/test
https://ユーザ名.github.io/レポジトリ名/
これはindex.htmlというファイル内部の文字列を、MIME Typeがtext/htmlで返却する処理になります。
htmlファイルの中身がちゃんとHTMLであればページがちゃんとレンダリングされますが
これをちょっと変な使い方にして、HTMLファイルの中身にJSを書くことで
アクセス結果文字列をJSとして扱う工夫になります。
具体的には以下のレポジトリに実装しております。
こちらのREADMEにシーケンス図でイメージを書いていますが
ようはJS文字列をレポジトリ側で作っておき、HTTPリクエストを通して呼び出せるようにしよう!ということです。
こういったhtmlファイルを用意します。これは明らかにJSですが、「index.html」として作成します。
const test = "Hello World!";
console.log(test);
これにより、以下のように取得します。
// あくまでHTMLを返却するため、MIMEタイプに注意すること
const res = await fetch("https://xxxx...url").then(v => v.text())
// 変数スコープを絞るため、windowオブジェクトに登録するjsがおすすめです。
// このようなJS「だけ」を「index.html」の中に書きます。
window.someObject = {
func1: v => console.log(v),
func2: () => console.log("")
};
window.someObject.func1("some"); // このように呼べます
ただしこちらの記事のように
「jsファイルなどを用意し、GitHub Actionsでindex.htmlファイルを作成してPagesで公開」
するような工夫もおすすめです。
編集中にファイル拡張子がずっとhtmlだと、シンタックスハイライト等で困りますので。
ブックマークレットでにひと工夫
さて、これでJSを用意できたので、ブックマークレットなどで呼び出すことができます。
以下のようにeval関数を使います。
javascript: (async () => {
/** ページ取得後、javascript文字列を取得 */
const res = await fetch("https://ユーザ名.github.io/レポジトリ名/")
.then(v => v.text())
.catch(e => console.error(e));
console.log(res);
/** evalで実行する */
eval(res);
})();
eval関数は、文字列をJavaScriptコードだとして実行するもので
任意コード実行の脆弱性に大いにつながるため、とても注意して扱いましょう。
サンプル: どんなページでもメッセージ表示できるブックマークレット
先ほどのレポジトリに、以下のようなものを用意しています。
window.utilfunc = {};
window.utilfunc._showMsg = async (msg, level = "info") => {
const $ = v => document.querySelector(v);
const $$ = (e = "div") => document.createElement(e);
const DOM = str => new DOMParser().parseFromString(str, "text/html").body.firstElementChild;
const attr = (e, att) => Object.entries(att).map(([k, v]) => e[k] = v);
const css = (e, sty) => Object.entries(sty).map(([k, v]) => e.style[k] = v);
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const st = $$("style");
attr(st, {"media": "screen", "type": "text/css"});
st.appendChild(document.createTextNode(
`@keyframes anime-fadein-right {${[
"0% { opacity: 0; transform: translateX(10px); }",
"100% { opacity: 1; transform: translateY(0); }"
].join(' ')}}`
));
st.appendChild(document.createTextNode(
`@keyframes anime-fadeout-up {${[
"0% { opacity: 1; transform: translateY(0); }",
"100% { opacity: 0; transform: translateY(-100px); }"
].join(' ')}}`
));
st.appendChild(document.createTextNode(
`.fadeout-up {
animation: anime-fadeout-up 0.3s ease-out;
}`
));
document.head.appendChild(st);
const link = $$("link");
attr(link, {
"rel": "stylesheet",
"href": "https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0"
})
document.head.appendChild(link);
const color = {
"info": "green",
"warn": "yellow",
"error": "red",
};
const wrap = $$();
css(wrap, {
"position": "fixed",
"inset": "400px 0px 1px 200px",
"margin": "auto",
"width": "300px",
"height": "70px",
"padding": "10px",
"borderRadius": "7px",
"background": "gray",
"display": "flex",
"justifyContent": "center",
"alignItems": "center",
"zIndex": "1000",
"animation": "anime-fadein-right 0.5s 1 ease-out",
});
const inner = $$();
css(inner, {
"width": "100%",
"height": "100%",
"borderRadius": "7px",
"background": "black",
"display": "grid",
"gridTemplateColumns": "20% 80%",
});
const iconWrap = $$();
const span = DOM(`
<span class="material-symbols-outlined">
${level == "info" ? "task_alt" : "error"}
</span>
`);
css(span, {
"width": "80%",
"height": "80%",
"marin": "10px",
"fontSize": "50px",
"color": color[level],
});
const msgBox = $$();
css(msgBox, {
"width": "90%",
"height": "80%",
"display": "flex",
"justifyContent": "left",
"alignItems": "center",
});
const text = $$();
css(text, {
"fontSize": "40px",
"color": color[level],
});
attr(text, {
"innerText": msg
});
wrap.appendChild(inner);
inner.appendChild(iconWrap);
inner.appendChild(msgBox);
iconWrap.appendChild(span);
msgBox.appendChild(text);
$("body").appendChild(wrap);
await sleep(1000).then(_ => wrap.style.animation = "anime-fadeout-up 0.3s ease-out");
await sleep(300).then(_ => [st, link, wrap].forEach(v => v.remove()));
};
これはページの右下にメッセージをぽわぁっと表示するJSコードを
windowに追加するブックマークレットになります。
しかし、コードは長いですね!
ブックマークレットに登録できるJSコードは文字数制限があり、Google Chrome 3.0〜で6138文字となっています。
リッチなCSSやJSを付けたくても文字数制限があり、長いコードを管理するのも大変なので
レポジトリに置いておくことができ、いつでもPages経由で呼び出せるのはかなり便利ではないでしょうか!
これをどんなページでもいいので呼び出したあと
このように関数を呼び出すことで
window.utilfunc._showMsg("test");
window.utilfunc._showMsg("test", "warn");
window.utilfunc._showMsg("test", "error");
このようになります。cssでアニメーションもします。いいですね。
以下はGoogleで適当に検索したあとで表示したものになります。
※注意
ページによってはCSPを設定しており、そもそもgithub.ioへのfetchアクセスや
evalを禁止する設定を入れている場合があります。
その場合は以下のようなエラーになり、実行できません。
Refused to connect to 'https://ユーザ名.github.io/レポジトリ名/' because it violates the document's Content Security Policy.
流石に汎用といっても、脆弱性につながるやり方なので
間違えて変なコードを実行しないよう、eval時の確認や変数スコープであったりに注意しましょう。
最後に
今回はGitHubPagesを用いてAPIを構えて汎用的に呼び出したい処理をあらかじめ用意し
ブックマークレットを便利にするアイデアを紹介しました。
サンプルとして紹介したレポジトリは公開しておきますので、フォークなりご自由にどうぞ。
ALHについて知る
↓ ↓ ↓ 採用サイトはこちら ↓ ↓ ↓
↓ ↓ ↓ コーポレートサイトはこちら ↓ ↓ ↓
↓ ↓ ↓ もっとALHについて知りたい? ↓ ↓ ↓