見出し画像

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について知りたい? ↓ ↓ ↓