🥸

Re: ブラウザの中でスタンド使いになってみた。

2022/12/20に公開

はじめに

今年もこの季節がやってきて年末だと改めて感じますが皆様いかがお過ごしでしょうか?

今回はクソアプリアドベントカレンダーに参加するにあたり昔作ったChrome拡張に急遽、季節感を出す拡張テーマ機能を追加し、よりキータイプを阻害するwアプリに仕上げました。

そもそもどんな拡張よ?という方はこちらの過去記事をどうぞ。

https://zenn.dev/temori/articles/qiita-20190627-c6979d6dae55599bb4c0

少しでもご興味ある方是非インストール&アンスト。(正直鬱陶しいです)

https://chrome.google.com/webstore/detail/jojo-experience/foinkfknclhpkpbomacdfjfbaklgifpk

https://github.com/temori1919/jojo_experience

何を変更したのか

仕組みは単純で現在日が設定ファイルに記載された日付範囲なら対象のテーマファイルをimportする、というものです。
随時季節によってテーマを追加できるようにしてあります。

クリスマスなので雪のエフェクトが入ります。
jojo.gif

エフェクトはsvgにアニメーションをつけて対応しています。
この辺はサラッと流します。

  const tags = `
    <svg id="svg-out-jojo" width="100%" height="100vh">
      <radialGradient id="tama" cx="60%" cy="40%" r="80%">
      <stop offset="0" style="stop-color:#fff"/>
      <stop offset="0.5" style="stop-color:#eee"/>
      </radialGradient>

      <symbol id="jojo-symbol">
      <circle cx="30%" cy="10%" r="10" fill="url(#tama)"/>
      </symbol>

      <use id="snow" href="#jojo-symbol"/>
    </svg>
  `;

  $("body").prepend(tags);

  $("#svg-out-jojo").css({
    display: "block",
    position: "fixed",
    overflow: "visible",
    "z-index": 1000000,
    "pointer-events": "none",
  });

  $("#snow").css({
    animation: "snow 4s linear forwards",
    filter: "blur(3px)",
  });

  $.keyframe.define([
    {
      name: "snow",
      from: { transform: "translateY(-20vh)" },
      to: { transform: "translateY(120vh)" },
    },
  ]);

  const svgOutJojo = $("#svg-out-jojo");
  const snow = $("#snow");

  let count = 0;

  const reproduction = () => {
    let clone = snow.clone(true);
    clone.on("animationend", function () {
      clone.remove();
    });
    let iti = String(Math.random() * 200 - 100) + "%";
    clone.attr("x", iti);
    svgOutJojo.append(clone);
    count++;
  };

  setInterval(function () {
    reproduction();
  }, 150);

今回の変更で対応したこと

manifest_versionをV2 -> V3に

V2は段階的に廃止予定とのことなのでこのタイミングでV3に変更。

https://developer.chrome.com/docs/extensions/mv3/mv2-sunset/

V3に変更するにあたって、manifest.jsonのweb_accessible_resourcesの書き方が若干変わっていたので変更。

manifest.jsonのV2とV3の比較
- "web_accessible_resources": [
-   "images/*"
- ],
+ "web_accessible_resources": [{
+   "resources": [
+     "images/*"    
+   ],
+   "matches" : ["<all_urls>"]
+ }],

リソースはresourcesキーとして指定、また対象のマッチするURLをmatchesとして指定する必要がありました。

https://developer.chrome.com/docs/extensions/mv3/manifest/web_accessible_resources/#manifest-declaration

テーマ用の設定ファイルはmoduleモードで読み込み

import、export構文を使いたかったのでいつものようにファイル冒頭に

configファイルの一例
import config from "./config.js";

const resolveDate = async () => {
  const today = new Date();

  const match = config.seasons.find((season) => {
    const from = new Date(season.from);
    const to = new Date(season.to);

    return today >= from && today <= to;
  });

  return match.file || false;
};

を記述したかったのですが、モジュールモードではないため以下のようなエラーが発生してしまいます。
いつもはバンドルツールありきなのでめんどくせー。

Uncaught SyntaxError: Cannot use import statement outside a module

なので動的インポートを使用します。

const resolveDate = async () => {
  const config = await import("./config.js");
  const today = new Date();

  const match = config.seasons.find((season) => {
    const from = new Date(season.from);
    const to = new Date(season.to);

    return today >= from && today <= to;
  });

  return match.file || false;
};

<details><summary>抜粋だとわかりずらいかと思うのでテーマ関連の全処理を書いておきます。</summary>

config.js
export const seasons = [
  {
    from: "2022-12-01",
    to: "2022-12-25",
    file: "christmas",
  },
];
extensionMethods.js
/**
 * 現在日付から設定済みのテーマファイル名を取得する
 * @returns
 */
const resolveDate = async () => {
  const config = await import("./config.js");
  const today = new Date();

  const match = config.seasons.find((season) => {
    const from = new Date(season.from);
    const to = new Date(season.to);

    return today >= from && today <= to;
  });

  return match.file || false;
};

/**
 * 拡張テーマのrenderを実行
 * @returns
 */
const matchMedhods = async () => {
  const matchFile = await resolveDate();

  if (matchFile) {
    const method = await import(`./apps/${matchFile}.js`);
    return method.render();
  } else {
    return false;
  }
};
app/christmas.js(このファイルが期間ごとの拡張テーマ用のファイルになる)
/**
 * 拡張テーマ関数は必ずrenderという命名にすること
 */
export const render = () => {
  const tags = `
    <svg id="svg-out-jojo" width="100%" height="100vh">
      <radialGradient id="tama" cx="60%" cy="40%" r="80%">
      <stop offset="0" style="stop-color:#fff"/>
      <stop offset="0.5" style="stop-color:#eee"/>
      </radialGradient>

      <symbol id="jojo-symbol">
      <circle cx="30%" cy="10%" r="10" fill="url(#tama)"/>
      </symbol>

      <use id="snow" href="#jojo-symbol"/>
    </svg>
  `;

  $("body").prepend(tags);

  $("#svg-out-jojo").css({
    display: "block",
    position: "fixed",
    overflow: "visible",
    "z-index": 1000000,
    "pointer-events": "none",
  });

  $("#snow").css({
    animation: "snow 4s linear forwards",
    filter: "blur(3px)",
  });

  $.keyframe.define([
    {
      name: "snow",
      from: { transform: "translateY(-20vh)" },
      to: { transform: "translateY(120vh)" },
    },
  ]);

  const svgOutJojo = $("#svg-out-jojo");
  const snow = $("#snow");

  let count = 0;

  const reproduction = () => {
    let clone = snow.clone(true);
    clone.on("animationend", function () {
      clone.remove();
    });
    let iti = String(Math.random() * 200 - 100) + "%";
    clone.attr("x", iti);
    svgOutJojo.append(clone);
    count++;
  };

  setInterval(function () {
    reproduction();
  }, 150);
};

soundEffect.mjs(アプリ本体となるファイル)
// 拡張テーマの読み込み
document.onready = async () => {
  await matchMedhods();
};
...

</details>

manifest.jsonの変更

extensionMethods.jsのmatchMedhodsを外部ファイルから実行するためにcontent_scriptsweb_accessible_resources.resourcesにファイルを指定します。

{
  "manifest_version": 3,
  "name": "Jojo experience",
  "version": "1.1.0",
  "description": "スタンド使いになれるChrome Extensionです",
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": [
      	"jquery.min.js",
      	"soundEffect.mjs",
      	"caretposition.js",
+       "extensions/extensionMethods.js"
      ]
    }
  ],
  "web_accessible_resources": [{
    "resources": [
      "images/*",
+     "extensions/*"
    ],
    "matches" : ["<all_urls>"]
}],
  "permissions": [
    "storage"
  ]
}

Github Actionsでstoreへ自動アップロード

手作業でアプリをアップロードするのも面倒なので、アップロード、app storeへの申請をGithub Actionsで行うよう設定しました。

release.yml
name: Publish

# リリースを公開したタイミングで発火
# https://docs.github.com/ja/actions/using-workflows/events-that-trigger-workflows#release
on:
  release:
    types: [published]

jobs:
  release-actions:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - uses: actions/setup-node@v3
        with:
          node-version: 16

      - name: Archive
        run: |
          # アプリ公開の際にはzipでアップロードする必要があるので圧縮
          zip -r extension.zip ./ -x "*.git*"

      - name: Publish
        run: |
          yarn global add chrome-webstore-upload-cli
          npx chrome-webstore-upload-cli@2 upload --source extension.zip --auto-publish
        env:
          EXTENSION_ID: ${{ secrets.EXTENSION_ID }}
          CLIENT_ID: ${{ secrets.CLIENT_ID }}
          CLIENT_SECRET: ${{ secrets.CLIENT_SECRET }}
          REFRESH_TOKEN: ${{ secrets.REFRESH_TOKEN }}

以下のnpmパッケージでアプリUPと申請を自動化しています。

https://github.com/fregante/chrome-webstore-upload-cli

  • アプリのID(EXTENSION_ID)
  • GCPでclientIDを発行(CLIENT_ID)
  • 同じくsercretを発行(CLIENT_SECRET)
  • refresh token(REFRESH_TOKEN)

が別途必要になるので、Githubのsercretsに設定します。

sercretやclient idなどの発行手順はパッケージのREADMEに記載があります。

いざデプロイ

早速Githubのreleasesを公開してworkflowを回してみます。

スクリーンショット 2022-12-20 1.08.29.png

あるぇー?
Googleアカウントの2要素認証を有効にしろ的なメッセージが。。

2要素認証を有効にして再度トライ。
スクリーンショット 2022-12-20 1.12.19.png

今度は連絡先メールアドレスを追加しないとだめっぽいので追加して再度トライ。

またまたダメだったのでdevconsoleを確認、公開までにクリアしないことが多々あったため(プライバシーへの取り組みの入力など)、諸々対応してもう一度回す。
スクリーンショット 2022-12-20 1.16.43.png

無事成功しました🎉🎉🎉
これでGoogleの審査が通れば晴れて公開となります。

最後に

何かしら邪魔が入った方が燃える、など特殊な性癖の方にはもってこいの拡張だと思うので気が向いたら追加してみてください。

https://chrome.google.com/webstore/detail/jojo-experience/foinkfknclhpkpbomacdfjfbaklgifpk

Discussion