AngularでChrome Extensionの開発を完結させる

Angular Advent Calendar 2019 12/13の記事です。誠に勝手ながらQiitaではなく個人ブログに執筆させていただきます。

誠に勝手ながら自己紹介しておくと、22歳の大学院生やりつつ、LCNEMのCEO,CTOです。Angularが大好きでフロントに多用しつつ、Firebaseでサクッと開発したり、Golangでブロックチェーン「そのもの」を開発したり、案件をいただいたりもしています。ブロックチェーンに限らず、開発の依頼や、採用についてお気軽にご連絡ください。

私個人の是非Twitterもぜひご覧ください。

今回のテーマは、AngularでChrome拡張の開発を完結させる、です。

「Orbit」という、SLIP44の規格をもとに、ブロックチェーンに利用する秘密鍵をチョー簡単に管理するChrome拡張を開発中です(というか完成)。(もともとCosmos用になにか作ろうとしていてOrbit(軌道の意)としましたけどもSLIP44規格によってBitcoinやNEMにも使えるめちゃ便利なものができたので…)

本記事はOrbitの開発過程をもとに執筆します。もちろん記事の内容はブロックチェーン関係なくAngularの記事ですのでご安心ください。

Chrome拡張の構造

chrome拡張を説明するにあたって、まずmanifest.jsonを解説します。

重厚な解説が以下のページにありますのでこちらも参考にしてください。

https://developer.chrome.com/extensions/manifest

以下が、Orbitの

{
  "name": "Orbit",
  "manifest_version": 2,
  "version": "0.1.0",
  "browser_action": {
    "default_icon": "favicon.png",
    "default_popup": "index.html"
  },
  "content_scripts": [
    {
      "matches": [
        "file://*/*",
        "http://*/*",
        "https://*/*"
      ],
      "js": [
        "extension/content-script.js"
      ],
      "run_at": "document_start",
      "all_frames": true
    }
  ],
  "background": {
    "page": "index.html#/background"
  },
  "permissions": [
    "storage",
    "tabs"
  ],
  "web_accessible_resources": [
    "extension/injection.js"
  ]
}

えーまずフォルダ構造としては、

$ ng new orbit
$ cd orbit
$ mkdir extension
$ ng g component background
$ ng g component home
$ ng g component request

をして作られる構造になっています。

  • orbit
    • extension
      • content-script.ts
      • injection.ts
    • src
      • app
        • background
        • home
        • request

こんな感じですね。

省略

name, manifest_version, version, permissionsは省略します。

content_scripts

content scriptsでは、originが条件とマッチしたときに、ページに仕込むjsファイルを指定できます。

ただし、content_scriptsで指定したjsファイルにおいては、windowオブジェクトの中に関数を仕込む操作などはできないようになっているようです。おそらくセキュリティの都合上。

しかしながら、これを回避する方法があります。

web_accessible_resources

injection.jsという別ファイルをChrome拡張からアクセスできるようにするために、指定しておきます。

このinjection.jsを、ブラウザが開いている一般的なウェブページに仕込むことができます。やり方は、content-script.jsにおいてDOM操作でscriptタグを仕込むだけです。

こうするとなんと、injection.jsにおいてwindowオブジェクトに中身を注入できるようになります。これのメリットは次節説明します。

background

ブラウザの裏で常時起動してるスクリプトを指定できます。

なんとここにはjsファイルだけでなく、scriptタグ持ちのhtmlファイルを指定することができます。

ここでは、content_scriptとの間で、chrome.runtime.sendMessageメソッドを利用することで、情報交換を行うことができます。

npm i -s @types/chrome

によってTypeScript型定義が手に入りますのでこれを使いましょう。

injection.jsのメリット

さて、先程バレバレの伏線を貼っておいた本件ですが、解説します。injection.jsのメリットは、手短に言うと、

  • ウェブサイトの開発者↔injection.jsをwindowに仕込んだ関数で情報交換
  • injection.js ↔ content_script.jsをwindow.postMessageで情報交換
  • content_script.js ↔ backgroundをchrome.runtime.sendMessageで情報交換

とすることで、拡張機能と連動するサイトを作れるようになるのです。これはinjection.jsなしではwindowオブジェクトに関数を仕込むことができないので、1行目が実現できません。

秘密鍵の管理のような面倒なことを拡張機能にまかせて、ウェブアプリを開発するといった使い方ができます。

browser_action

よく理解できない人は、lighthouseやchrome remote desktopをchromeにインストールしてください。chromeの右上にアイコンが出てきて、そこクリックするとどうなるかを指定できます。ここでdefault_popupを指定することで、ポップアップの中に表示するページを指定することができます。

UseHash

popup用のページと、background用のページも当然ながらAngularで開発することができます。

まず、background用のページを認識させるために、AngularのRoutingのuseHashをtrueにします。

@NgModule({
  imports: [RouterModule.forRoot(routes, { useHash: true })],
  exports: [RouterModule]
})
export class AppRoutingModule {}

こうすることで、index.html/#backgroundというパスでbackground用のコンポーネントのコードを実行してくれます。Angularだけで開発が完結できる感出てきました。

Webpack

問題は、jsしか指定できないcontent-script.jsとinjection.jsをどのようにビルドするか、です。

以下のコマンドしましょう。

$ cd extension
$ npm init
$ npm i -D webpack webpack-cli typescript ts-loader

で、extension/webpack.config.jsを以下のような内容で作成します。

const path = require('path');

module.exports = {
  mode: "development",
  entry: {
    "content-script": "./content-script.ts",
    "injection": "./injection.ts"
  },
  output: {
    path: path.resolve(__dirname, "../dist/orbit/extension/")
  },
  module: {
    rules: [
      {
        test: /\.ts$/,
        loader: 'ts-loader'
      }
    ]
  }
}

そしてextension/package.jsonを

"scripts": {
  "build": "webpack",
  "build-prod": "webpack --mode=production"
},

とし、

$ cd ..

としてpackage.jsonを

"scripts": {
  ...,
  "i": "npm i && cd extension && npm i",
  "build-aot": "ng build --aot && cd extension && npm run build",
  "build-prod": "ng build --prod && cd extension && npm run build-prod"
},

とすると、

$ npm run i
$ npm run build-aot
$ npm run build-prod

これらのコマンドだけで完結するようになります。

以上です!