TypeScript/Rollup/Vercelでサクッとブックマークレットを作ってみよう

こんにちは。エンジニアブログ運営の梅津です。
普段はエージェンシー事業部でリードアプリケーションエンジニアとして働いています。

エンジニアブログ運営としてブログの質を向上させるために、これまでのブログの情報を集めたりもするのですが、これを逐一手作業で行うのは大変です。
ある程度の作業は自動化したい。そういったときはブックマークレットを作ると便利ですよね。

今回はそんなブックマークレットの作り方をまとめてみました。
「ブックマークレット?よく知らないな」「聞いたことあるけど作り方とか気にしたことなかった」という人がいれば是非一緒に試してみてください!

筆者の開発環境やこのブログで利用する主な技術のバージョンは次のとおりです。

  • macOS
  • Node.js 18.12.1
  • TypeScript 5.0.4
  • Rollup 3.23.0
  • Vercel CLI 29.4.0

ブックマークレットとは

ブックマークレットとはJavaScriptのコードをブラウザのブックマークとして保存し、クリックすることで実行するものです。UIは持ちません。
利用シーンとしては次のようなものがあります。

  1. ウェブページの表示内容をカスタマイズ
    • 広告や特定の要素を非表示にする
    • スタイルを変更する
  2. 作業の効率化
    • 特定のウェブサービスのフォームを自動入力する
    • 一括操作を行う
  3. ページ内の情報の抽出や処理
    • ページ内のテキストを自動的に翻訳する
    • 特定のパターンのリンクを一括で取得する

他にもさまざまな利用シーンが考えられますが、ざっくりいうと「こうなったら便利なのになー」を手軽に実現できるのがブックマークレットということです。

似たようなものとしてブラウザの拡張機能があります。
こちらはより高度なことを実現できますが、利用するには拡張機能のストアを経由する必要があります。

自分自身や周りの人にだけ使って欲しい機能であればブックマークレットとして開発するほうが簡単です。

この記事では、サンプルとして、アクセスしたWebページのタイトルとURLを取得するブックマークレットを作成します。 まず単純なブックマークレットを作成し、最終的には共有が簡単なブックマークレットをステップを踏んで作成していきます。

  • TypeScriptを使ってブックマークレットを作る
  • Rollupを使って複数のファイルをまとめる
  • JavaScriptファイルを外部ファイルとして読み込むようにして、ブックマークレット更新の手間を少なくする

TypeScriptを使ってブックマークレットを作る

それではブックマークレットを作っていきます。
生のJavaScriptで書いていってもいいのですが、私は型のないコードに触れていると発狂してしまう病に冒されているのでTypeScriptで書いていきます。
また、今回作成するブックマークレットは古いブラウザでの実行は考慮しません。最近のブラウザで動けばよいということにしましょう。

プロジェクトの作成

まずはコードを置いていくためのプロジェクトフォルダを作りましょう。今回は sample-bookmarklet とします。
sample-bookmarklet フォルダを作ったら、そのフォルダへ移動して npm init -y を実行し package.json を作成します。

mkdir sample-bookmarklet
cd sample-bookmarklet
npm init -y

TypeScriptのインストール

続いてTypeScriptのインストールと tsconfig.json の作成を行います。

npm install --save-dev typescript
npx tsc --init

npx tsc --init によって tsconfig.json が作られます。
コメントアウトされている部分の削除、設定の追加などをして最終的に次のようにします。

{
  "compilerOptions": {
    "target": "es2022",
    "module": "es2022",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true,
    "outDir": "dist"
  },
  "include": [
    "src/**/*.ts"
  ]
}

ブックマークレットは最終的に単一のJavaScriptファイルにしないといけないので、モジュールの読み込みというのは基本的に発生しないのですが、あとで使うRollupのために module も変えておきます。

トランスパイルの設定

トランスパイルするためのnpm scriptを package.json に追加します。

{
  "scripts": {
    "build": "tsc"
  }
}

これでTypeScriptでブックマークレットを作成する準備が整いました。
ソースコードを src フォルダに追加し、npm run build を実行すると、トランスパイルされたJavaScriptファイルが dist フォルダに出力されます。

ブックマークレットのコードを書いていく

準備が整ったのでブックマークレットを作っていきましょう。
ブックマークレットを実行したときに、開いていたウェブページのタイトルとURLをクリップボードにコピーする、という内容にしてみます。

src フォルダに index.ts を追加します。中身は次のようにします。

// src/index.ts
(async function () {
  async function copyToClipboard(text: string): Promise<void> {
    return navigator.clipboard.writeText(text);
  }

  async function bookmarklet(): Promise<void> {
    const title = document.title;
    const url = window.location.href;

    await copyToClipboard(`Title: ${title}\nURL: ${url}`);
  }

  bookmarklet()
    .then(() => console.log("タイトルとURLをコピーしました"))
    .catch(error => console.error("コピーできませんでした: ", error));
})();

ブックマークレットを作成するときは即時実行関数式(IIFE)の形で定義してあげます。
こうすることで自分で定義した変数や関数がグローバルな名前空間を汚さないようにします。

ブックマークレットの動作確認

ブックマークレットを動かすために、まずはTypeScriptのコードをトランスパイルします。
先程追加したトランスパイルのnpm scriptを実行しましょう。

npm run build

dist フォルダに次のようなJavaScriptファイルができていると思います。

"use strict";
(async function () {
    async function copyToClipboard(text) {
        return navigator.clipboard.writeText(text);
    }
    async function bookmarklet() {
        const title = document.title;
        const url = window.location.href;
        await copyToClipboard(`Title: ${title}\nURL: ${url}`);
    }
    bookmarklet()
        .then(() => console.log("タイトルとURLをコピーしました"))
        .catch(error => console.error("コピーできませんでした: ", error));
})();

この内容をブラウザのブックマークとして登録します。
ブラウザによって手順や文言は変わる場合がありますが、基本的には次のようにして登録します。

  1. ブックマーク追加のボタンを押す
  2. URL欄が無い場合は「その他」や「詳細」といったボタンを押してURL欄が出るようにする
  3. 「名前」欄にブックマークレットの名前を入力する
    • 例: sample-bookmarklet
  4. 「URL」欄にJavaScriptのコードを貼り付ける
    • ただし javascript: で始める必要があります
    • 例: javascript:"use strict";(async function () { async function copyToClipboard(text) ...以下略
  5. 「保存」ボタンを押してブックマークレットを登録する

登録したブックマークレットをクリックすることでJavaScriptのコードが実行されます。
実行してみて、開発者コンソールに タイトルとURLをコピーしました と出ていれば成功です。
DOMException: Document is not focused. と出た場合は一度ページ内をクリックしてから再度実行してみてください。

ブックマークレットを実行するもうひとつの方法

ブラウザのURLバーに javascript: から始まるコードを直接入力して実行することもできます。
開発途中などコードが頻繁に変わるタイミングではこちらの実行方法のほうが簡単です。
※ 今回のサンプルコードでは Clipboard.writeText を使っているため、URLバーからの実行では DOMException: Document is not focused. エラーが出てしまいます。

Rollupを使って複数のファイルをまとめる

ブックマークレットのコード量が大きくなってくるとファイルを分割したくなります。
しかし、ブックマークレットとして実行するには1つのJavaScriptにまとめる必要があるので、分割したファイルをそのまま読み込むような方法は取れません。
そこでバンドラを利用することで、ファイルを分割したまま開発し、最終的なJavaScriptのファイルは1つにまとめるようにしましょう。

バンドラの候補としてはいくつかありますが、今回は次の理由からRollupを使ってみたいと思います。

  • Rollupはライブラリやモジュールのバンドリングなどでよく使われる
  • ツリーシェイキングのサポートがあり、小さなバンドルを作成できる
  • 設定ファイルを自分で書けるので、困ったことがあったときになんとかしやすい
  • 設定はWebpackなどに比べるとシンプル
  • デフォルトで出力時のフォーマットに即時実行関数式(IIFE)を選べる
  • プラグインを使って色々とカスタマイズする事もできる

今回は試せませんでしたが、その他にも似たような条件を満たせるバンドラはありそうです。他のバンドラについては、また機会があれば紹介したいと思います。

Rollupのインストール

それではRollupと関連するプラグインなどを入れていきます。

npm install --save-dev rollup @rollup/plugin-typescript
npm install tslib

Rollupの設定

続いてRollupの設定ファイルを書いていきます。
プロジェクトのルートフォルダ直下に rollup.config.ts を作成ください。

format: 'iife' を指定するとバンドル後のJavaScriptファイルを即時実行関数式にしてくれます。

import typescript from '@rollup/plugin-typescript';
import { RollupOptions } from 'rollup';

const config: RollupOptions = {
  input: 'src/index.ts',
  output: {
    file: 'dist/index.js',
    format: 'iife',
  },
  plugins: [
    typescript(),
  ],
};

export default config;

次にRollupを使ってトランスパイルするようにnpm scriptを変更します。
--configPlugin オプションを指定することで設定ファイルをTypeScriptで書けるようにします。

{
  "scripts": {
-   "build": "tsc"
+   "build": "rollup --config rollup.config.ts --configPlugin typescript"
  },
}

rollup.config.ts も型のチェックができるように tsconfig.jsoninclude に追加します。
バンドラを利用するようにしたので "moduleResolution": "Bundler" も追加します。

{
  "compilerOptions": {
    "target": "es2022",
    "module": "es2022",
    ...
+   "moduleResolution": "Bundler"
  },
  "include": [
    "src/**/*.ts",
+   "rollup.config.ts"
  ]
}

これでRollupの設定が整いました。

ファイルを分割する

お目当てのファイル分割をしていきます。

これまで src/index.ts に全て書いていたコードを src/bookmarklet.tssrc/utils.ts に分けます。
src/index.ts からはブックマークレットのロジックとなる関数を呼び出すようにしてみます。

// src/index.ts
import { bookmarklet } from "./bookmarklet";

bookmarklet()
  .then(() => console.log("タイトルとURLをコピーしました"))
  .catch(error => console.error("コピーできませんでした: ", error));
// src/bookmarklet.ts
import { copyToClipboard } from "./utils";

export async function bookmarklet(): Promise<void> {
  const title = document.title;
  const url = window.location.href;

  await copyToClipboard(`Title: ${title}\nURL: ${url}`);
}
// src/utils.ts
export async function copyToClipboard(text: string): Promise<void> {
  return navigator.clipboard.writeText(text);
}

この状態で npm run build をすると dist/index.js が吐き出されます。
中身は次のようになっているでしょう。

(function () {
    'use strict';

    async function copyToClipboard(text) {
        return navigator.clipboard.writeText(text);
    }

    async function bookmarklet() {
        const title = document.title;
        const url = window.location.href;
        await copyToClipboard(`Title: ${title}\nURL: ${url}`);
    }

    bookmarklet()
        .then(() => console.log("タイトルとURLをコピーしました"))
        .catch(error => console.error("コピーできませんでした: ", error));

})();

分割していたファイルが1つのJavaScriptファイルとしてまとまりましたね!
これでコード量が多少大きくなっても怖くありません。

バンドル後のファイルサイズをもう少し小さくしたければ @rollup/plugin-terser を使って minify することもできます。
詳しくは上記のリンクを参照してください。

JavaScriptファイルを外部ファイルとして読み込むようにして、ブックマークレット更新の手間を少なくする

ここまででブックマークレットの作成は問題なくできるようになりました。
しかし、ブックマークレットを継続的に開発していくような場合を考えると、更新があるたびにブックマークレットの登録作業をするのは大変ですね。
加えて作成したブックマークレットを自分だけでなく他の人にも共有することを考えると、この登録し直す手間が跳ね上がります。

こういった手間を少なくするために、ブックマークレットのコードを外部のJavaScriptファイルとしてどこかに公開し、それを読み込むようにしてみましょう。
そうすればブックマークレットのコードを修正するたびにブックマークを再登録する必要はなくなります。
また、修正したコードを他の人と共有するためには単にJavaScriptファイルを更新するだけで良くなります。

JavaScriptファイルを公開する先はどこでもいいのですが、今回はサンプルということで、比較的使うのが簡単なVercelにファイルを置いてみます。

Rollupの設定を変更

これまでは直接ブックマークレットとして登録するために即時実行関数式にしてきましたが、ロジック部分は外部ファイルとして置くことになるのでこの設定はいらなくなります。 format には esmcommonjs を指定しておけば良いと思います。

// rollup.config.ts

const config: RollupOptions = {
  output: {
    file: 'dist/index.js',
-   format: 'iife',
+   format: 'esm',
  }
};

export default config;

Vercelを使うための準備

Vercelのアカウントを持っていない場合はこちらのリンクからVercelのアカウントを作成します

続いて Vercel CLI をインストールします。
グローバルにインストールするか、プロジェクトのローカルにインストールするかはどちらでも問題ないです。
vercel --version を実行して結果が返ってくるようにしましょう。

Vercelへのログイン

次のコマンドを実行して、Vercel CLIからVercelへログインします。
アカウントの作成方法に合わせてログインの仕方を選択してください。

vercel login

VercelへJavaScriptファイルをデプロイする

プロジェクトのルートフォルダ直下に vercel.json を作成します。
outputDirectory を追加して、どのフォルダ内のファイルを生成物として公開するかを指定します。

{
  "outputDirectory": "dist"
}

Vercel CLIを使ってVercelへデプロイするには次の方法があります。

  1. vercel コマンドを使う
  2. vercel buildvercel deploy --prebuilt を使う

1の方法ですとプロジェクト内のソースコードやファイルをまるっとVercel側に渡してビルドやデプロイを行ってもらいます。
2の方法ではローカルやCI環境でプロジェクトをビルドし、 .vercel/output に出力されたものをデプロイします。こちらの方法ではVercelへソースコードを共有するようなことはありません。

今回はせっかくなので2の方法を試してみましょう。

次のコマンドでビルドを行います。

vercel pull --yes --environment=production
vercel build --prod

このときnpm scriptに用意した build コマンドも実行されるので、事前に npm run build を実行しておく必要はありません。

.vercel/output フォルダに static/index.js というのができているのを確認します。

続いてデプロイ用のコマンドも実行します。

vercel deploy --prebuilt --prod

実行結果に https://${Vercelプロジェクト名}.vercel.app のようなURLが出ていればデプロイは成功です。

公開したJavaScriptファイルを読み込むようにブックマークレットを変更する

ブックマークレットを次のように変更します。src に先程デプロイしたファイルのURLを入力してください。

javascript:(function () {
    document.body
        .appendChild(document.createElement('script'))
        .src = "https://${Vercelプロジェクト名}.vercel.app";
})();

ブックマークレットを実行して、これまでと同じ動作をしていることを確認します。

今後はJavaScriptファイルをデプロイし直すだけでロジックの更新ができます。
他の人へ共有するときも上記のブックマークレットの内容を一度登録してもらうだけとなります。
ブックマークレットの更新・共有が格段にやりやすくなりましたね。

おまけ: Vercel使ったローカル開発の体験向上

Vercel CLIには vercel dev というコマンドがあります。
これを利用すると静的ファイルのホスティングをローカルで試せます(加えてServerless Functionsもローカルで実行可能)。 細かい内容は割愛しますが、次のものを組み合わせることでローカルの開発体験がより向上しますので試してみてください。

  1. vercel dev
  2. RollupのWatchモード
  3. concurrently
    • vercel dev と Watchモードでのトランスパイルを同時実行
  4. @rollup/plugin-replace
    • ローカル、プレビュー、本番といった環境に合わせて読み込み先のURLを置き換える

さいごに

いかがだったでしょうか。
ブックマークレット特有の事情がときどきあるものの、基本的にはNode.jsアプリケーションを作るのと似たような感覚だったかと思います。

「こうなったら便利なのになー」ということを思いついたら、どんどんブックマークレットを作ってみましょう!

それでは、また。

参考リンク