TypeScript + Vue.js で Chrome拡張機能を開発する

前回とほぼ同じ話です。 Chrome拡張機能の開発ではアイコンクリック時に表示されるポップアップと、開発者ツールに追加するタブでHTMLページを利用します。 このHTMLページの開発でVue.jsを利用したい、というのがモチベーションです。

Vue.jsでの開発ではよくvue-cliが利用されると思いますが、何をしているかよくわからない感じがあまり好きじゃないのでvue-cliは利用せずに行きたいと思います。

参考 :

github.com

まずはnpmのプロジェクトを作成します

$ cd my-extension
$ npm init
This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sensible defaults.
See `npm help init` for definitive documentation on these fields
and exactly what they do.
Use `npm install <pkg>` afterwards to install a package and
save it as a dependency in the package.json file.
Press ^C at any time to quit.
package name: (chrome-extension-vue-typescript)
version: (1.0.0)
description:
entry point: (index.js)
test command:
git repository: (https://github.com/TakahashiShuuhei/chrome-extension-vue-typescript.                                         git)
keywords:
author:
license: (ISC)
About to write to C:\Users\shuhei.takahashi\dev\chrome-extension-vue-typescript\packa                                         ge.json:
{
  "name": "chrome-extension-vue-typescript",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/TakahashiShuuhei/chrome-extension-vue-typescript.g                                         it"
  },
  "author": "",
  "license": "ISC",
  "bugs": {
    "url": "https://github.com/TakahashiShuuhei/chrome-extension-vue-typescript/issue                                         s"
  },
  "homepage": "https://github.com/TakahashiShuuhei/chrome-extension-vue-typescript#re                                         adme"
}
Is this OK? (yes)

とりあえず全部デフォルトでやってます。

次に、必要なライブラリをインストールしていきます。

$ npm install --save-dev @babel/core @babel/preset-env babel-loader clean-webpack-plugin copy-webpack-plugin css-loader html-webpack-plugin postcss-loader vue vue-loader vue-style-loader vue-template-compiler webpack webpack-cli

この時点でpackage.jsonは下記のようになりました。(ビルド用に scripts に build を追加しています)

{
  "name": "chrome-extension-vue-typescript",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack --mode production"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/TakahashiShuuhei/chrome-extension-vue-typescript.git"
  },
  "author": "",
  "license": "ISC",
  "bugs": {
    "url": "https://github.com/TakahashiShuuhei/chrome-extension-vue-typescript/issues"
  },
  "homepage": "https://github.com/TakahashiShuuhei/chrome-extension-vue-typescript#readme",
  "devDependencies": {
    "@babel/core": "^7.12.10",
    "@babel/preset-env": "^7.12.11",
    "babel-loader": "^8.2.2",
    "clean-webpack-plugin": "^3.0.0",
    "copy-webpack-plugin": "^7.0.0",
    "css-loader": "^5.0.1",
    "postcss-loader": "^4.1.0",
    "vue": "^2.6.12",
    "vue-loader": "^15.9.6",
    "vue-style-loader": "^4.1.2",
    "vue-template-compiler": "^2.6.12",
    "webpack": "^5.11.1",
    "webpack-cli": "^4.3.1"
  }
}

次にwebpack.config.jsを追加します。

const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

module.exports = {
  context: path.resolve(__dirname),
  entry: {
    popup: './src/popup/main.js',
    devtools: './src/devtools/main.js',
    background: './src/background/main.js',
    content: './src/content/main.js'
  },
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].js'
  },
  resolve: {
    alias: {
      vue$: 'vue/dist/vue.esm.js',
      '@': path.resolve(__dirname, 'src'),
      '~': path.resolve(__dirname)
    }
  },
  devServer: {
    open: true,
    hot: true
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: 'babel-loader'
      },
      {
        test: /\.vue$/,
        use: 'vue-loader'
      },
      {
        test: /\.css$/,
        use: [
          'vue-style-loader',
          'css-loader',
          'postcss-loader'
        ]
      }
    ]
  },
  plugins: [
    new CleanWebpackPlugin(),
    new VueLoaderPlugin(),
    new webpack.HotModuleReplacementPlugin(),
    new CopyWebpackPlugin({
      patterns: [
        {
          from: '**/*',
          context: 'public'
        }
      ]
    })
  ]
};

src 配下に popup や devtools 等のそれぞれのディレクトリを用意しておいて、その下の main.js をそれぞれのエントリーポイントにすることにしました。 コンパイル結果のファイルは ./dist/popup.js や ./dist/devtools.js のような形で出力されます。

ここからはソースコードを書いていきます。

public/popup.html

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Popup</title>
</head>
<body>
  <div id="app"></div>
  <script src="./popup.js"></script>
</body>
</html>

public/devtools.html

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Devtools</title>
</head>
<body>
  <div id="app"></div>
  <script src="./devtools.js"></script>
</body>
</html>

ポップアップと開発者ツールそれぞれに表示するページです。

src/popup/main.js

import Vue from 'vue';
import App from './App.vue';

new Vue({
  el: '#app',
  render: h => h(App)
});

src/popup/App.vue

<template>
  <div>
    <h1>This Is Popup! {{ now }} </h1>
  </div>
</template>

<script>
export default {
  computed: {
    now: () => {
      return new Date();
    }
  }
}
</script>

src/devtools/main.js

import Vue from 'vue';
import App from './App.vue';

new Vue({
  el: '#app',
  render: h => h(App)
});

src/devtools/App.vue

<template>
  <div>
    <h1>This Is Devtools! {{ now }} </h1>
  </div>
</template>

<script>
export default {
  computed: {
    now: () => {
      return new Date();
    }
  }
}
</script>

src/background/main.js src/content/main.js

console.log('hello');

backgroudとcontentは画面が無いので今回は適当にしておきます。

ここまでくるとビルドが可能になります

$ npm run build

dist配下にdevtools.htmlやpopup.htmlが出力されているのでブラウザで開くと "This Is Devtools! Fri Jan 08 2021 17:52:25 GMT+0900 (日本標準時)" のように表示されると思います。

これでひとまずポップアップと開発者ツールそれぞれのVue.jsアプリのビルドが可能になりました。

今度はTypeScriptへ対応していきます。

$ npm install --save-dev typescript ts-loader

webpack.config.js の module.rules に以下を追加します

      {
        test: /\.ts$/,
        loader: 'ts-loader',
        exclude: /node_modules/,
        options: {
          appendTsSuffixTo: [/\.vue$/],
        }
      }

あと resolve に以下を追加しておきます

    extensions: ['.ts', '.js', '.vue', '.json'],

entryの各main.js を main.ts に変更しておきます

  entry: {
    popup: './src/popup/main.ts',
    devtools: './src/devtools/main.ts',
    background: './src/background/main.ts',
    content: './src/content/main.ts'
  },

あとtsconfig.jsonを追加しておきます

{
  "compilerOptions": {
      "outDir": "./dist/",
      "sourceMap": true,
      "strict": true,
      "module": "es2015",
      "moduleResolution": "node",
      "target": "es5",
      "skipLibCheck": true,
      "esModuleInterop": true,
      "experimentalDecorators": true
  },
  "include": [
      "./src/**/*"
  ]
}

あと src に 以下の内容のvue-shims.d.tsをおいておきます 参考 : github.com

declare module "*.vue" {
  import Vue from "vue";
  export default Vue;
}

次にjsファイルをtsに変えていきます。 各 main.js のファイル名を main.ts に変更します。 App.vue の script を <script lang="ts"> に変更しておきます。

これでTypeScript化できたと思います。npm run build でビルドできることを確認します。

ここまでできればほぼ完了です。

あとは、Chrome拡張機能用のプロジェクトなのでpublicに必要なファイルを置いていきます。

public/manifest.json

{
  "manifest_version": 2,
  "name": "my chrome extension",
  "version": "0.1.0",
  "description": "My Chrome Extension",
  "icons": {
    "16": "icons/icon_16.png"
  },
  "background": {
    "scripts": [
      "background.js"
    ],
    "persistent": false
  },
  "browser_action": {
    "default_title": "popup title",
    "default_popup": "popup.html"
  },
  "permissions": [
    "storage"
  ],
  "content_scripts": [
    {
      "matches": [
        "<all_urls>"
      ],
      "js": [
        "content.js"
      ]
    }
  ],
  "devtools_page": "panelcreate.html"
}

アイコンも適当に public/icons においておいて下さい。

開発者ツールのパネルは devtools_page に指定した html が直接表示される訳ではなく、chrome.devtools.panel.create で追加しなければならないのでこれを実行してくれるファイルを用意しておきます。 public/panelcreate.js

chrome.devtools.panels.create('My Extension', null, 'devtools.html');

public/panelcreate.html

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Panel Create</title>
  <script type="text/javascript" src="./panelcreate.js"></script>
</head>
<body>
  
</body>
</html>

chrome.devtools.panels.create の第一引数はタブのタイトルになります。

npm run build で dist にファイルが生成されます。 Chrome でアドレスバーに chrome://extensions を入力して、デベロッパーモードをオンにして、"パッケージ化されていない拡張機能を読み込む"からdistを選択するとVue.js + TypeScriptで作成した拡張機能を読み込むことができると思います。

今回のコードは下記で確認できます。

github.com