必要なものは作っちゃえ!Chromeの拡張機能をAngular 2で書いてみた

2016/06/15

Michaela Lehr

0

Articles in this issue reproduced from SitePoint
Copyright © 2016, All rights reserved. SitePoint Pty Ltd. www.sitepoint.com. Translation copyright © 2016, KADOKAWA CorporationJapanese syndication rights arranged with SitePoint Pty Ltd, Collingwood, Victoria,Australia through Tuttle-Mori Agency, Inc., Tokyo

ブラウザーに必要な機能を追加できる、Google Chromeの機能拡張(エクステンション)。でもちょっとほしいものがないんだよな……。そんなときは、自分で作ってみては? Angular 2とTypeScriptで書く、モダンな機能拡張の作り方。

本記事はPanayiotis VelisarakosTim SeverienDan Princeが査読を担当しています。最高のコンテンツに仕上げるために尽力してくれたSitePointの査読担当者の皆さんに感謝します

Chrome拡張機能は、グーグルChromeブラウザーに機能を追加できるちょっとしたWebアプリです。ブラウザーの動作や開発ツール、新規タブページを拡張し、カスタマイズできるようになります。拡張機能のダウンロードはChrome Web Storeからできます。

記事では、WebサイトのURLを保存して新規タブで表示するChrome拡張機能のサンプルを作成します。ブックマークを新規タブに直接取り込み、見栄えを調整します。

プロジェクトコードの完全版はGitHubにあります。サンプルとして作成した拡張機能の現行バージョン(より多くの機能付き)のインストールもできます。

何を作る?

作成するサンプルの概要を簡単に説明します。次の画面は、ブックマークが増減するリストです。ブックマークは、クリックされたときに対応したURLが開くリンク集のことです。

01

各ブックマークには、タイトルとURLという2つの情報が必要です。情報の編集とブックマークの削除がオプションです。ブックマークを編集するには、2つの入力欄と送信ボタンのついたフォームが必要です。

02

ユーザーが入力したデータを処理してリストにするには、TypeScriptで開発されたのAngular 2を利用します。Angular 2はクライアント側アプリケーションを作成するのに最適で、JavaScriptのスーパーセットの1つであるTypeScriptと相性が良いのです。Angular 2とTypeScriptの導入から始めたい人には、こちらの記事をお勧めします。

本記事のサンプルを作成するには、テキストエディターとNode Package Manager (npm)だけで大丈夫ですが、拡張機能を公開するにはGoogleデベロッパーアカウントが必要です。こちらから作成してください。

設定と構造

それでは、サンプル作成を始めます。新規プロジェクトフォルダーを作成します。

mkdir sitepoint-extension && cd sitepoint-extension

TypeScript Config

tsconfig.jsonファイルをプロジェクトフォルダーに追加します。tsconfig.jsonファイルは、.tsファイルのコンパイル方法をTypeScriptコンパイラーへ指示します。

{
  "compilerOptions": {
    "target": "ES5",
    "module": "system",
    "moduleResolution": "node",
    "sourceMap": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "removeComments": false,
    "noImplicitAny": false
  },
  "exclude": [
    "node_modules"
  ]
}

重要な設定はcompilerOptionsです。ECMAScriptターゲットバージョンはES5とし、モジュールコードジェネレーションはSystemJS("module": "system")の使用を指定します。

"sourceMap": trueを使って、ソースマップファイルが作成されます。.mapファイルは、ブラウザーがコンパイルされたES5コードをTypeScriptコードにマップする際に利用するもので、デバッグに重要です。

本記事では、tsconfig.jsonファイルについての知識はこれ以上必要ありません。

Package.json

必要なパッケージのインストールにはnpmを、デベロップメントタスクとビルドタスクの作成にはnpmスクリプトを使います。メインディレクトリにpackage.jsonを追加します。

現在、Angular 2はベータ版です。この記事ではβ版バージョン7を使いました。もちろんもっと新しいバーションも使えますが、フレームワークが変わってしまうことがあるので、すべてスムーズに動作するか保証しかねます。

{
  "name": "SitePointBookmarkExtension",
  "description": "A Chrome Extension for Bookmarks",
  "version": "1.0.0",
  "scripts": {
    "lite": "lite-server",
    "tsc": "tsc",
    "tsc:w": "tsc -w",
    "start": "concurrently \"npm run tsc:w\" \"npm run lite\""
  },
  "dependencies": {
    "angular2": "2.0.0-beta.7",
    "systemjs": "0.19.22",
    "es6-promise": "^3.0.2",
    "es6-shim": "^0.33.3",
    "reflect-metadata": "0.1.2",
    "rxjs": "5.0.0-beta.2",
    "zone.js": "0.5.15"
  },
  "devDependencies": {
    "concurrently": "^2.0.0",
    "lite-server": "^2.1.0",
    "typescript": "^1.7.5"
  }
}

次のツールを使ってパッケージをインストールします。

npm install

既存のnpmスクリプトのうち、npm run [script name]で実行するものもあるので注意してください。TypeScriptファイルをコンパイルしてデベロップメントサーバーを作成するスクリプトは4つあります。

Manifest.json

アプリを作成する前に、manifest.jsonという別の.jsonファイルを追加しておかなければなりません。このファイルには、Web Storeとブラウザーが拡張機能を取扱う方法が記述されており、Chrome拡張機能には常に必要なものです。

ファイルを完成するのはあとにして、必要な推奨プロパティを追加します。

{
    "manifest_version": 2,
    "name": "SitePoint Bookmark Extension",
    "short_name": "Make the most of a new tab",
    "description": "This extension helps you save your favorite webpages.",
    "version": "1.0.0",
    "author": "Michaela Lehr @fischaelameer"
}

ブックマーク・コンポーネント

Angular 2はコンポーネントベースのフレームワークです。最初のコンポーネントとして、単独のブックマークを1つ作成します。このあとブックマークを格納するリストを親コンポーネントとして作成するので、作成したコンポーネントは子コンポーネントになります。

03

scriptsという新規フォルダーを作成し、その中にbookmark.component.tsという名称のファイルを作成します。

// To create a component, we need Angular's "Component" function.
// It can be imported from the "angular2/core" module.
import { Component } from 'angular2/core';

// A component decorator tells Angular that the "BookmarkComponent" class
// is a component and adds its meta data: the selector and the template.
@Component({
    selector: 'sp-bookmark',
    template: '<h1>Bookmark</h1>'
})

// The "BookmarkComponent" module exports the "BookmarkComponent" class,
// because we will need it in other modules,
// e.g. to create the bookmark list.
export class BookmarkComponent { }

BookmarkComponentコンポーネントを起動するには、次のような別ファイルを追加しなければなりません。ここでは、boot.tsと呼びます。

// We need to reference a type definition (or 'typings') file 
// to let TypeScript recognize the Angular "promise" function
// (we'll need this later on) otherwise we'll get compile errors.
/// <reference path="../node_modules/angular2/typings/browser.d.ts" />

// Angular's "bootstrap" function can be imported 
// from the angular2/platform/browser module.
// Since we want to bootstrap the "BookmarkComponent",
// we have to import it, too.
import { bootstrap }    from 'angular2/platform/browser'
import { BookmarkComponent } from './bookmark.component'

// We can now bootstrap the "BookmarkComponent" as the root component.
bootstrap( BookmarkComponent );

system.config.jsという別の新規ファイルは、SystemJSモジュールローダーを設定します。上で作成したboot.tsファイルをロードします。

// SystemJS is the module loader for the application. 
// It loads the libraries and our modules and then catches and logs errors, 
// that may occur during the app launch.
System.config({
  packages: {
    scripts: {
      format: 'register',
      defaultExtension: 'js'
    }
  }
});
System.import('scripts/boot')
  .then(null, console.error.bind(console));

サンプルをブラウザーで確認するには、あとindex.htmlだけが必要です。次のファイルをプロジェクトディレクトリのルート部分の、.jsonファイルと同レベルのところに入れます。

<html>
  <head>

    <title>SitePoint Bookmark Extension</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <!-- We load the libraries we need directly from the "node_modules" folder.
    In a more complex project, we would use a different approach here, 
    e.g. working with a build tool like gulp.js or Angular-CLI. -->
    <script src="node_modules/angular2/bundles/angular2-polyfills.js"></script>
    <script src="node_modules/systemjs/dist/system.src.js"></script>
    <script src="node_modules/rxjs/bundles/Rx.js"></script>
    <script src="node_modules/angular2/bundles/angular2.dev.js"></script>

    <!-- Load the SystemJS config -->
    <script src="scripts/system.config.js"></script>

  </head>

  <body>
    <!-- Here we are using the selector "sp-bookmark", 
    which we defined as component meta data in the "BookmarkComponent" decorator. 
    Everything inside the element tag will only be seen 
    until our application is loaded. -->
    <sp-bookmark>Loading bookmarks...</sp-bookmark>
  </body>

</html>

TypeScriptファイルをコンパイルし、サーバーを起動してテストします。

npm run start

すべてが正常に動作していれば、図のようなページを表示する前に、ブラウザーが新規タブを開いて「Loading bookmarks…」という文が表示されます。
04

ブックマーク・テンプレート

いまのところ、ブックマーク・テンプレートは静止したヘッドライン1つだけですが、本来の目的ではありません。ブックマークをすべて表示するため、bookmark.htmlと呼ばれる別のhtmlを参照します。

プロジェクトのルートに、templatesという新規フォルダーを作成し、新しいブックマーク・テンプレートを作ります。

<div class="bookmark">
  <!-- We are using the interpolation template syntax 
  to bind the component properties "bookmark.name" 
  and "bookmark.url" to our template. -->
  <a href="{{bookmark.url}}" class="bookmark__link">{{bookmark.name}}</a>
  <!-- Every bookmark has two buttons, to let users edit and delete a bookmark.-->
  <span class="bookmark__button-wrapper">
    <!-- The edit button has an event binding "(click)", 
    which sets the component variable "submitted" to true. 
    It also has a property binding "[hidden]",
    which hides the button, when the variable "submitted" is true. -->
    <button class="bookmark__button" (click)="submitted=true" [hidden]="submitted">
      Edit
    </button>
    <!-- The delete button uses an event binding "(click)", 
    that calls the component function "onDelete()", 
    when a user clicks it. -->
    <button class="bookmark__button" (click)="onDelete(bookmark)">Delete</button>
  </span>
  <!-- To edit a bookmark, we show a form 
  if the value of the property "submitted" is false. -->
  <div class="bookmark__form-wrapper" [hidden]="!submitted">
    <!-- The form has a submit button, 
    which allows us to use the Angular directive "ngSubmit".
    It calls another component function "onSubmit()". -->
    <form class="bookmark__form" (ngSubmit)="onSubmit()">
      <label class="bookmark__form__label">Name: </label>
      <!-- There are two input fields for the two properties 
      "bookmark.name" and "bookmark.url". 
      Both use the two-way data binding template syntax, 
      to change the property values. -->
      <input class="bookmark__form__input" [(ngModel)]="bookmark.name" 
        placeholder="Name"/>
      <label class="bookmark__form__label">URL: </label>
      <input class="bookmark__form__input" [(ngModel)]="bookmark.url" 
        placeholder="URL"/>
      <button class="bookmark__form__button" type="submit">Done</button>
    </form>
  </div>
</div>

テンプレートを参照するtemplateUrlは、次のようにBookmarkComponentデコレーター内のtemplateメタデータを置換します。

@Component({
    selector: 'sp-bookmark',
    templateUrl: './templates/bookmark.html'
})

ブラウザーには、編集、削除という2つのボタンがフォームに表示されます。bookmark.namebookmark.urlのプロパティが指定されていないので、欄は空のままです。

BookmarkComponentに足りないプロパティを追加します。後でプリセットやlocalStorageのダイナミックデータも利用できますが、引き続きハードコード化したブックマークを使って進めます。

import { Component } from 'angular2/core';

// We are using an interface to represent a bookmark.
// A single bookmark is now strongly typed:
// it has to have two properties "name" and "url",
// which both must be a string.
interface Bookmark {
  name : string,
  url : string
}

@Component({
    selector: 'sp-bookmark',
    templateUrl: './templates/bookmark.html'
})

export class BookmarkComponent {

  // The bookmark property is of the type "Bookmark",
  // defined in the interface above.
  bookmark : Bookmark = {
   name : 'SitePoint',
   url : 'https://sitepoint.com'
  }

  // Setting the default value for the "submitted" property.
  submitted = false;

}

ブラウザーには、2つのボタンがあるハイパーリンクが表示されています。submittedのプロパティを「偽」に設定してあるので、フォームは今のところは隠れて見えません。ブックマークの編集と削除は、設定していないので機能しません。

06

ブックマーク・リスト

ブックマークのリストを作成しデータを取り込むため、list.component.tsという親コンポーネントを作成します。

import { Component } from 'angular2/core';
import { Bookmark } from './bookmark.component';
import { BookmarkComponent } from './bookmark.component';

// The ListComponent metadata defines the component's selector,
// the url of the template and the directives used in this template.
@Component({
    selector: 'sp-list',
    templateUrl: './templates/list.html',
    directives: [ BookmarkComponent ]
})

export class ListComponent { }

boot.tsファイルで説明したコンポーネントと、index.htmlで使用した要素を変更しなければなりません。アプリがListComponentをロードし、次にBookmarkComponentをロードするようにします。

/// <reference path="../node_modules/angular2/typings/browser.d.ts" />

import { bootstrap }    from 'angular2/platform/browser';
import { ListComponent } from './list.component';

bootstrap( ListComponent );
<body>
  <sp-list>Loading bookmarks...</sp-list>
</body>

デフォルトデータ

この状態ではデフォルトデータが不足しているので、新規ユーザーが使う場合、ブックマークリストは空になっています。しかし、はじめて利用するユーザーでもブックマークを見ることはありますから、次のようにlist.data.constant.tsという名称で新規ファイルにデフォルトのブックマークデータを少し作成しておきます。

// We are using a constant here,
// because we do not want to change the default data.
export const BOOKMARKS = [
  { 'name': 'Twitter', 'url': 'https://twitter.com' },
  { 'name': 'Github', 'url': 'https://github.com' },
  { 'name': 'Sitepoint', 'url': 'https://sitepoint.com' },
  { 'name': 'Codepen', 'url': 'https://codepen.com' }
];

リストサービス

デフォルトデータかlocalStorageに保存されたデータを使用するかどうかにかかわらずListComponentを使うのではなく、list.service.tsと呼ばれる新しいファイルで、データのインポートを処理します。

import { BookmarkComponent } from './bookmark.component';
import { BOOKMARKS } from './list.data.constant';

// Importing the "Injectable" function from the angular2/core module
// and adding the "@Injectable" decorator lets us use dependency injection
// in this service.
import { Injectable } from 'angular2/core';

@Injectable()

export class ListService {

  // We create three variables: 
  // one for possible data in the localStorage,
  // one for our default data and
  // one for the data our service should return.
  bookmarksLocalStorage = JSON.parse(  localStorage.getItem('sp-bookmarklist') );
  bookmarksDefaultData = BOOKMARKS;
  bookmarksToReturn = this.bookmarksDefaultData;

  // The "getBookmarks()" function checks if there is data in the local storage.
  // If there is, we return this data,
  // if there isn't we return the default data.
  getBookmarks() {
    if ( this.bookmarksLocalStorage !== null ) {
      this.bookmarksToReturn = this.bookmarksLocalStorage;
    }
    return Promise.resolve( this.bookmarksToReturn );
  }

  // A "setBookmarks()" function saves new data in the local storage.
  setBookmarks( bookmarks : Object ) {
    localStorage.setItem( 'sp-bookmarklist', JSON.stringify( bookmarks ) );
  }

}

ListComponentでこのサービスを使ってみましょう。サービスをインポートし、それをプロバイダーとしてコンポーネントに追加して、コンストラクター関数でプライベート変数に代入します。

また、OnInitライフサイクルフックも追加しなければなりませんが、ListComponentがアクティブになるとすぐに呼び出されます。この関数はListServiceを使ってブックマークリストを取得します。非同期的にブックマークを取得するので、ES2015 promisesとアロー関数を使用します。

import { Component } from 'angular2/core';
import { OnInit } from 'angular2/core';
import { Bookmark } from './bookmark.component';
import { BookmarkComponent } from './bookmark.component';
import { ListService } from './list.service';

@Component({
    selector: 'sp-list',
    templateUrl: './templates/list.html',
    directives: [ BookmarkComponent ],
    providers: [ ListService ]
})

export class ListComponent implements OnInit {

  public bookmarks : Object;

  constructor( private listService : ListService ) {}

  // The function "getBookmarkLists" requests the bookmarks asynchronously.
  // When the promise is resolved, the callback function assigns
  // the bookmarks to the component's bookmarks property.
  getBookmarkLists() {
    this.listService.getBookmarks().then( bookmarks => this.bookmarks = bookmarks );
  }

  // The "ngOnInit" function gets called, when the component gets activated.
  ngOnInit() {
    this.getBookmarkLists();
  }

}

リスト・テンプレート

足りないのはlist.htmlですから、作成してtemplatesフォルダーに入れておきます。list.htmlには、順不同のリストがある<section>要素が1つしか入っていません。リスト要素は、Angular付属の構造指示文*ngForを使って繰り返し使用されます。リスト要素内では、<sp-bookmark>というBookmarkComponentコンポーネントのセレクターが使われます。

<section class="bookmarklist-container bookmarklist-container--blue-dark">
  <ul class="bookmarklist__sublist">
    <!-- Angular's built-in structural directive "*ngFor" 
    instantiates a list element for each bookmark. 
    The hash prefix means, that the private variables 
    "bookmark" and "i" are created. 
    They can be used on the list element's child elements.-->
    <li *ngFor="#bookmark of bookmarks; #i = index">
      <!-- The template property binding "[bookmark]" 
      sets the value to the component property "bookmark". 
      In addition there are two custom component event bindings 
      "(bookmarkChanged)" and "(bookmarkDeleted)". 
      Whenever one of these events were raised, 
      their respective functions will be executed. -->
      <sp-bookmark [bookmark]="bookmark" (bookmarkChanged)="setBookmarks()" 
        (bookmarkDeleted)="deleteBookmark(bookmark, i)"></sp-bookmark>
    </li>
  </ul>
</section>

きちんとすべてが機能するように、bookmark.component.tsファイルに少しだけ変更を加えます。テンプレートプロパティバインディングの[bookmark]を使用しており、プロパティをコンポーネントデコレーターでインプットプロパティとして設定しなければなりません。

@Component({
    selector: 'sp-bookmark',
    templateUrl: './templates/bookmark.html',
    inputs : ['bookmark']
})

ブラウザーにデフォルトデータの入ったリストが表示されるようになりました。

07

イベント

あと必要なのは、ブックマークの編集機能と削除機能だけです。ブックマークの編集・削除ボタンのクリックはbookmarkという子コンポーネントで発生するので、親コンポーネントはイベントに反応しなければなりません。子コンポーネントから親コンポーネントへの伝達手段が必要なので、カスタムイベントを利用して作成します。

すでに、bookmark.htmlに両ボタン用のクリックハンドラーを用意し、list.html(bookmarkChanged)(bookmarkDeleted)という2つのイベントリスナーを追加してあります。次のように、bookmark.component.tsにイベントエミッターを追加します。

import { Component } from 'angular2/core';
import { Output } from 'angular2/core';
import { EventEmitter } from 'angular2/core';

// [...] I left some code out of the example to save space.

export class BookmarkComponent {

  bookmark : Bookmark;
  submitted = false;

  // Events flow outside the child component and therefor need an output decorator.
  @Output() bookmarkChanged : EventEmitter<any> = new EventEmitter();
  @Output() bookmarkDeleted : EventEmitter<any> = new EventEmitter();

  // Whenever a user clicks on "Done" after editing a bookmark,
  // an event is fired, which indicates that the bookmark was changed.
  // To hide the form, the "submitted" property is set to false again.
  onSubmit( bookmark : Bookmark ) {
    this.submitted = false;
    this.bookmarkChanged.emit( bookmark );
  }

  // When the "Delete" button is clicked, the event "bookmarkDeleted" 
  // will be fired.
  onDelete( bookmark : Bookmark ) {
    this.bookmarkDeleted.emit( bookmark );
  }

}

setList()deleteBookmark()という2つの関数で、ListComponent内のイベントに対応します。

// [...]

export class ListComponent implements OnInit {

  public bookmarks : Array< Object >;

  constructor( private listService : ListService ) {}

  getBookmarkLists() {
    this.listService.getBookmarks().then( bookmarks => this.bookmarks = bookmarks );
  }

  ngOnInit() {
    this.getBookmarkLists();
  }

  // setList uses the "ListService" to save the complete list.
  setList() {
    this.listService.setBookmarks( this.bookmarks );
  }

  // The function deletes the bookmark and saves the complete list.
  deleteBookmark( bookmark : Bookmark, i : number ) {
    this.bookmarks.splice( i, 1 );
    this.setList();
  }

}

これですべてきちんと動作するようになりました。ブックマークを編集・削除して、機能性をチェックしてみてください。

スタイリング

サンプルの拡張機能を追加する前に、ここでちょっとCSSを追加してみましょう。もうclassのマークアップはできているので、新規CSSフォルダーにCSSファイルを1つ追加して、index.htmlで参照するだけです。CSSはここからダウンロードできます。

<html>
  <head>

    <title>SitePoint Bookmark Extension</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <link rel="stylesheet" href="css/main.css">

    <!-- [...] -->

</html>

次のように表示されます。

08

Chrome拡張機能

サンプルの拡張機能を追加する準備が整いました。Chromeのツールバーにアイコンを1つ追加して、クリックされたらブックマークリストに現在表示中のページを保存するようにします。新規タブは、Chromeのデフォルト新規ページの代わりにAngular 2のブックマークリストを開くようになります。Chrome JavaScript APIを使って現在表示中のWebページをリストに保存します。

Event Page

scriptsフォルダーに1つスクリプトを追加します。このタスクではEvent Pagesを使用していますので、この新規スクリプトの名称はeventPage.tsにします。ユーザーがツールバーのアイコンをクリックすると実行されるものは、Chrome.browserAction.onClicked listenerを利用します。現在表示中のタブのタイトルとURLを取得するには、Chrome.tabs APIが必要です。

///<reference path="chrome/chrome.d.ts" />

import { Injectable } from 'angular2/core';
import { ListService } from './list.service';

@Injectable()

export class EventPage {

  // The event listener should be set when the "EventPage" class is initialized.
  // Therefore we are using the constructor for adding it to the "Chrome.browserAction".
  // To set and get the bookmarkLists, we are using the "ListService".
  constructor ( listService : ListService ) {

    let bookmarkLists : Array< Object >;

    // The "Chrome.browserAction" object throws an error,
    // when it is not available in development mode.
    // This is why we are only logging a message,
    // if it is undefined.
    if (typeof chrome.browserAction !== 'undefined') {
      // The Chrome "browserAction" is responsible for the icon in the Chrome toolbar.
      // This is when we are get the latest list of bookmarks from the "ListService"
      // and call the function "getSelectedTab" after the promise is resolved.
      chrome.browserAction.onClicked.addListener( function ( tab ) {
        listService.getBookmarks().then( bookmarkLists => {
          bookmarkLists = bookmarkLists;
          getSelectedTab( bookmarkLists );
        });
      });
    } else {
      console.log( 'EventPage initialized' );
    }

    // The Chrome tabs API gives us access to the current tab,
    // its title, and its url, which we are using to add a new bookmark
    // and save the list of bookmarks again with the "ListService".
    function getSelectedTab( bookmarkLists ) {
      chrome.tabs.getSelected( null, function ( tab ) {
        let newBookmark : Object = {
          name : tab.title,
          url : tab.url
        };
        bookmarkLists.push( newBookmark );
        listService.setBookmarks( bookmarkLists );
      });
    }
  }

}

1行目については少し説明が必要で、スクリプトをコンパイルするためのファイルがもっと必要だということです。Chrome JavaScript APIの機能をTypeScriptで使うには、TypeScriptへAPIのグローバルオブジェクトに関する指示を出さなければなりません。そのためには、///<reference path="chrome/chrome.d.ts" />というリファレンスパスをスクリプトに、そしてTypeScriptの定義ファイル(.d.ts)をプロジェクトのscriptフォルダーにそれぞれ追加します。必要なファイルはここからダウンロードできます。

Manifest.json

すべてmanifest.jsonファイルにまとめられました。必要なプロパティを1つずつ加えていきましょう。最初に、拡張機能アイコンのリファレンスとツールチップ付きツールバーアイコンを次のように追加します。

"icons": {
    "19": "Icon-19.png",
    "38": "Icon-38.png",
    "48": "Icon-48.png",
    "128": "Icon-128.png"
},

"browser_action": {
    "default_icon": {
        "19": "Icon-19.png",
        "38": "Icon-38.png"
    },
    "default_title": "Open a new tab to view your bookmarks."
}

Chrome Override Pageはユーザーが新規タブを開くたびにAngularアプリをロードします。

"chrome_url_overrides" : {
    "newtab": "index.html"
}

persistent: falseオブジェクトを持つbackgroundプロパティは、Event Pageスクリプト用のChromeイベントリスナーを追加します。

"background": {
    "page": "index.html",
    "persistent": false
}

content security policy (CSP)を設定します。"manifest_version": 2のデフォルトのコンテンツセキュリティ方針はscript-src 'self'; object-src 'self'ですが、ライブラリーのうちの1つが評価されたJavaScriptに依存するのでunsafe-evalを追加します。

"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'"

manifest.jsonに追加するのはpermissionsプロパティで、アクティブなタブに関する情報へのアクセスに必要なものです。

"permissions": ["activeTab"]

サンプルのテスト

サンプルをテストする準備が整いました。chrome://extensions/を閲覧すると、現在インストールされている拡張機能の概要や圧縮された拡張機能を展開してアップロードもできます。Chrome Web Storeに拡張機能をアップロードするには、.zip形式で圧縮しなければなりません。npmスクリプトをあと2つ追加して、必要なモジュールをlibフォルダーにコピーして全体を圧縮しましょう。

"copy-lib": "mkdir lib && cp node_modules/{angular2/bundles/angular2-polyfills.js,systemjs/dist/system.src.js,rxjs/bundles/Rx.js,angular2/bundles/angular2.dev.js} lib/",

"compress": "zip -r -X $npm_package_name-$npm_package_version.zip ./{templates/*,lib/*,css/*,scripts/*.js,*.html,manifest.json,*.png,*.ico}"

index.htmlファイル内にあるこのファイルのリファレンスを忘れずに変更しましょう。

<script src="lib/angular2-polyfills.js"></script>
<script src="lib/system.src.js"></script>
<script src="lib/Rx.js"></script>
<script src="lib/angular2.dev.js"></script>

次のようにnpmスクリプトを実行します。

npm run copy-lib
npm run compress

これで完成です! サンプルのテストは、chrome://extensions/を表示し、デベロッパーモードをアクティブ化し「Load unpacked extension」ボタンを使ってアンパックされていないzipフォルダーをアップロードしてください。新規タブにはブックマークアプリが表示され、ツールバーにある別のWebサイト上の新規拡張アイコンをクリックすれば、リストに新しいブックマークが追加されます。新規ブックマークを表示するため、拡張タブをリフレッシュしなければなりません。

付記eventPageスクリプトをデバックするには、chrome://extensions/のページからデバッグウィンドウを開くと、「Inspect views(表示の点検)」というバックグラウンドページへのハイパーリンクがあります。

次のステップは?

サンプルのブックマークアプリは、次のように改良できます。

  • カラースキームの変更やブックマークのインポートなど、追加できる機能はいろいろあります。
  • ユーザーによる新規ブックマークの追加完了後のフィードバックとしてポップアップするなど、ユーザーエクスペリエンスの向もできます。
  • 単体テストやエンドツーエンドテストをアプリに追加できます。Angular 2アプリのテストに関するデベロッパーガイドはここから参照できます。
  • 構築プロセスの向上や環境変数の利用は良いことです。npmスクリプトの代わりに、スキャフォールディングやローカルの開発サーバー、エンドツーエンドのテストなどのオプションがあるAngular-CLIを利用できます。
  • そしてもちろん、Webストアに自作のアプリは、Chrome Developer Dashboardで公開できます。

本記事が、Angular 2とTypeScriptを使ったChrome拡張機能の開発に対する第一印象やインスピレーションを得る役に立ったならうれしいです。このトピックについてもっと詳しく知りたい人は、次の資料を一読することをお勧めします。

(原文:Build Your Own Chrome Extension Using Angular 2 & TypeScript

[翻訳:Noriko O. Romano]
[編集:Livit

Copyright © 2016, Michaela Lehr All Rights Reserved.

Michaela Lehr

Michaela Lehr

ベルリン出身で、フロントエンド開発者およびUXデザイナーとして活躍するかたわら、開発スタジオ GeilDankeを共同設立しています。趣味はゲーム作りやヨガ、サーフィン、編み物。

Loading...