2018年8月13日月曜日

[Unity]GetComponentsInChildrenとinterfaceを組み合わせる

hierarchy上の全てのオブジェクトに特定の処理を適用したい!という要望に応えてくれるのがGameObject.GetCoponentsInChildrenです。
GameObjectの再生成をせずに動的にUIの内容を差し替えたいというときや子要素の全てのコライダーを無効にしたりするなどに使えます。
image.png (120.2 kB)
例として、上のような階層構造を持つuGUIコンポーネントに対して以下のようなインターフェースを持つキャラクターを適用したいときの実装を考えます。1

Character.cs
public interface ICharacter {
    string Name { get; }
    int Hp { get; }
    int Mp { get; }
    Sprite Sprite { get; }
}

実装

各TextやImageのコンポーネントが実装すべきインターフェースの定義です。

CharacterSettable.cs
public interface ICharacterSettable {
    void SetCharacter(ICharacter character);
}

各コンポーネントはICharacter型を引数にコンポーネントの値を変更します。

NameText.cs
using UnityEngine;
using UnityEngine.UI;

 // キャラクター名
[RequireComponent(typeof(Text))]
public class NameText : MonoBehaviour, ICharacterSettable {

    public void SetCharacter(ICharacter character) {
        GetComponent<Text>().text = character.Name;
    }
}
CharacterImage.cs
using UnityEngine;
using UnityEngine.UI;

 // キャラクター画像
[RequireComponent(typeof(Image))]
public class CharacterImage : MonoBehaviour, ICharacterSettable {

    public void SetCharacter(ICharacter character) {
        GetComponent<Image>().sprite = character.Sprite;
    }
}

(スクリプト略)
...
それぞれのコンポーネント別にスクリプトを作成し、オブジェクトにスクリプトをアタッチしていきます。
image.png (13.4 kB)

親側のスクリプトでは、GetComponentsInChildrenを呼び出して、子オブジェクトを操作します。

CharacterPanel.cs
using System;
using System.Collections.Generic;
using UnityEngine;

public class CharacterPanel : MonoBehaviour{

    public void Hoge(ICharacter character) {

        //  自分と自分の子要素にアタッチされている
        //  ICharacterSettableを実装する全てのコンポーネントのSetCharacterを実行する
        GetComponentsInChildren<ICharacterSettable>().ForEach(x => {
            x.SetCharacter(character);
        });
    }
}

//  IEnumerableでForEach使えるようにする拡張
public static partial class IEnumerableExtensions {

    public static void ForEach<T>(this IEnumerable<T> e, Action<T> action) {
        foreach (T item in e) {
            action(item);
        }
    }
}

よいタイミングでHogeを呼び出すと各コンポーネントにキャラクターの情報がセットされる寸法です。

何が嬉しいか

親側のクラスが各コンポーネントのクラスや、hierarchy上の構造を知る必要が無くなります。これはとても良いことです。
GetComponentsInChildrenを利用しない方法としては以下のように親側が操作対象のコンポーネントの参照を保持しておく方法があります。

CharacterPanel.cs
public class CharacterPanel : MonoBehaviour{

    public Image characterImage;
    public Text nameText;
    public Text hpText;
    public Text mpText;

    ...

image.png (9.9 kB)

この方法はGetComponentsInChildrenを使用する場合と異なり、親側がどのコンポーネントに何を設定するかを全て知っている必要があります。この方法は階層構造の規模が大きくなるにつれて徐々にスクリプトが複雑化しやすいです。
一方でGetComponentsInChildrenを使用する方法は親側のスクリプトは成長しませんし、新しいコンポーネントを増やす際も新しいスクリプトを書くことになりますので、適切なスクリプト分割が自然となされる点が健康的で魅力的です。

何が悲しいか

スクリプトが分散します。
InspectorでオブジェクトにアタッチするMonoBehaviourは1クラス1ファイルになってしまうので、やたらとスクリプトファイルが増殖します。また、それぞれのコンポーネントクラスはコード上からは参照されないクラスになるので、使用されているスクリプトを探すときに煩わしいこともあるでしょう。
私はスクリプトファイルが増えることには特に抵抗はありませんが、増殖を嫌う人も居るようです。

今回例に挙げたuGUIの操作以外にもGetComponentsInChildrenとinterfaceの組み合わせは様々に利用することが可能です。親コンポーネントが子コンポーネントのことをよく知っているようなスクリプトがある場合は、GetComponentsInChildrenを検討してみてはいかがでしょうか。


  1. characterという名前は良くないですね。文字を意味するcharacterと被るので紛らわしくて良くないです。個人的にはキャラクターを表す命名にはactorをよく使います。 

2018年5月14日月曜日

Gulp導入手順書とファーストインプレッション

この文書作成時のソフトウェアバージョンは以下です。

  • Windows 10 Home バージョン1709
  • Chocolatey v0.10.8
  • Node.js v10.0.0
  • npm 5.6.0
  • Gulp.js CLI version 3.9.1

Gulp is 何

GulpはNode.jsのモジュールとして提供されるタスクランナー。だと思う。MavenとかGradleのWebフロントで使われる版。
GRUNTっていうタスクランナーが居たんだけど、不満があったので解消のために作られたのがGulp。
似たようなやつにGRUNTとかBroccoliとかがいる。
流れの早いWebフロントにおいて、この手のタスクランナーへの依存は負の遺産になるから依存を減らしてnpmでシェルスクリプトをごりごり書こうという向きもあるようだ。

導入手順書

Node.jsのインストール

Windows環境ではChocolateyを使うのがお手軽。

PowerShell
> cinst nodejs.install

インストール確認は以下

PowerShell
> node -v

Gulpのインストール

ここからnpmを使う。npmはNode.jsのパッケージマネージャでNode.jsに含まれている。
グローバル環境にGulpをインストールする方法もあるようだが、ローカルとグローバルで多重管理するのは嫌な予感しかしないのでローカルにのみインストールする。
まずは対象プロジェクトのルートディレクトリをnpm管理下に置く。

PowerShell
> npm init

初期情報は適当に入力する。
package.jsonやらnode_modulesディレクトリやらができる。VCS管理下に置いている場合はnode_modulesディレクトリは管理対象外にしておく。

次にGulpをインストールする

PowerShell
> npm install gulp --save-dev

package.jsonのdevDependenciesにGulpのバージョン情報が記載される。
グローバルにはGulpをインストールしないので、このままではgulpコマンドは使えない。
ので、npmコマンドから実行できるようにする。
package.jsonのscriptsに以下を書き加える。

package.json
{
    ...
    "scripts": {
        "gulp": "gulp"
    },
    ...
}

以上で> npm run gulp タスク名コマンドでタスクを走らせられる。コマンド長ぇな。

タスクを書く

ルート直下におもむろにgulpfile.jsを新規作成する。

gulpfile.js
var gulp = require('gulp');

gulp.task('hello', function() {
    console.log('Hello Gulp!!');
});

これで> npm run gulp helloするとfunctionの中身が実行される。

所感

ファイルのコピーとかjsonのマージとかやってみたけど、まあ、ビルドスクリプトって感じ。
まだ使っていないけど、ファイルを監視するwatchとか使い始めたら本番じゃないだろうか?
jsで書ける点とタスク間の依存関係を少なくできそうな雰囲気があるのは良い感じ。でもやっぱり使い倒してみないと本領や欠点はわからないね。

2018年5月9日水曜日

ChromeExtensionとWebExtensionsのコードを共通化したい(追記)

この文書作成時のソフトウェアバージョンは以下です。

  • Google Chrome 66.0.3359.139 (64bit)
  • Firefox Quantum 59.0.3 (64 ビット)

結論から言うと、manifestをプラットフォームごとに分岐させ、js上はchrome.runtime.idの値で分岐させれば多重管理は防げる。

ChromeExtensionとWebExtensionsには互換性がある、しかし

ブラウザ拡張に用いるAPIはブラウザごとに違う。
OperaとChromeで採用されているのはextension API
Firefoxで採用されているのはWebExtensions API
MDNによれば、

WebExtension API で開発する拡張機能は、ブラウザー間で互換性が維持されるように設計されており、大半は Google Chrome や Opera でサポートされている extension API とコード互換性があります。これらのブラウザー向けに書かれた拡張機能はほとんどの場合、少しの変更を加えるだけで Firefox でも動くようになります。1

とのことなので、ひとつのコードベースでChromeもFirefoxもサポートしたいところ。
しかし、非互換API2があったり、互換とされるAPIであっても微妙に動きが異なったりする。

例えば、EventPageでコンテキストメニューに項目を追加するにはChromeExtensionでは以下のコードが推奨されている3
が、これをFirefoxで動かすとブラウザを閉じたときにコンテキストメニューはなくなり、再度インストールするまでコンテキストメニューに項目が追加されなくなる。(しかもパッケージ化していない拡張機能はブラウザが閉じるまでしか有効にならないのでテストできない! なんてこった)

background.js
chrome.runtime.onInstalled.addListener(function(){
    chrome.contextMenus.create({
        ...
    });
});

UserAgentからブラウザを判別して処理を分岐する方法

とりあえず思いついたのがUserAgent文字列をパースしてブラウザを判別する方法。
パーサは勿論既存のものを使う。
今回はこちらを拝借https://github.com/faisalman/ua-parser-js
で、分岐したい部分を愚直に分岐する。

background.js
var parser = new UAParser();
if(parser.getResult().browser.name === 'Chrome'){
    chrome.runtime.onInstalled.addListener(createContextMenus);
}
else{
    createContextMenus();
}

(今回はChromeとFirefoxのことしか考えていない)

他にいい方法があれば教えてください

ください!!


ここから追記
教えていただきました! ありがとうございます!

chrome.runtime.id

Chromeではchrome.runtime.idの値は拡張機能一覧に表示されるIDと一致する。
image.png (20.3 kB)

Firefoxではmanifestに記載したIDと一致する

manifest.json
{
    ...
    "applications" :{
        "gecko" : {
            "id" : "test@example.com"
        }
    },
    ...
}

Chromeではmanifestにapplicationsキーが書かれていると

Unrecognized manifest key 'applications'.

とエラーを吐かれてしまうので、なんとかする必要がある。

ビルド時にmanifestを分岐する

結局分岐させた。
firefox用のmanifestを記載したjsonを作っておき、ビルド時にマージする。今回はGulpを使ってみた。

manifest-firefox.json
{
    "applications" :{
        "gecko" : {
            "id" : "test@example.com"
        }
    }
}
gulpfile.js
var gulp = require('gulp');
var mergeJson = require('gulp-merge-json');
var rimraf = require('rimraf')

gulp.task('clean', function(callback){
    rimraf('release/firefox', callback)
});

gulp.task('releaseForFirefox', ['clean'], function(){
    gulp.src(['manifest.json', 'manifest-firefox.json'])
    .pipe(mergeJson({
        fileName: 'manifest.json',
    }))
    .pipe(gulp.dest('release/firefox'));

    // ソースのコピーとかアーカイブとか
    ...
});

gulp.task('releaseForChrome', ['clean'], function(){
    gulp.src('manifest.json').pipe(gulp.dest('release/chrome'));

    // ソースのコピーとかアーカイブとか
    ...
});

結局manifestは分岐することになったけど多重管理を防ぐことはできたので満足。
jsのソースも一本化できた。

background.js
if(chrome.runtime.id === 'test@example.com'){
    createContextMenus();
}
else{
    chrome.runtime.onInstalled.addListener(createContextMenus);
}

2018年4月13日金曜日

[Android]暗黙的Intentを受け取ることができるActivityの存在確認

この文書作成時の環境は以下です。

  • Kotlin 1.2.31
  • Android SDK 27

Intent#resolveActivity()

暗黙的Intentを用いてstartActivityすると、受け取れるActivityがないとき、ActivityNotFoundExceptionが発生するので、存在チェックをしなければならない。
一般的なインテント | Android Developersにあるとおり、Intent#resolveActivity()を使用する。
存在チェックはstartActivityのタイミングでするのではなく、Activity#onCreate()などでチェックして、存在しないならば機能を封印してしまうのが良い。

MainActivity.kt
...

fun Intent.existsImplicitReceiver(pm: PackageManager) : Boolean{
    return this.resolveActivity(pm) != null
}

class MainActivity : AppCompatActivity() {

    val cameraRequestCode:Int = 1

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        setSupportActionBar(toolbar)

        val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
         //  resolveActivityの結果がnullならば機能を封印する
        if(intent.resolveActivity(packageManager) == null){
            button_camera.isEnabled = false
        }

        button_camera.setOnClickListener {
            startActivityForResult(intent, cameraRequestCode)
        }
    }
}

2018年4月11日水曜日

Hello Kotlin for Android

この文書作成時の環境は以下です。

  • Windows 10 Home バージョン1709
  • Android Studio 3.1.1
  • Gradle 4.4
  • Gradle Android Plugin 3.1.1
  • Kotlin 1.2.31

Kotlin is 何

KotlinはJetBrains製の言語で、IDE開発でJavaの代用をするために作られたらしい。1 JVMで動く。
AndroidStudioがJetBrains製ということで、当初はAndroidStudioがサポートする言語という位置づけだったが、2017年5月にGoogleに正式採用された。2
KotlinはJavaと100%相互運用性がある3ので、JavaとKotlinをまぜまぜしながら開発できるらしい。
選択肢は多いにこしたことはないので、是非Kotlinも書けるようになっておきたい。

Hello World in Android

まずは新しいプロジェクトを作成する。Android Studio 3.0以降ならKotlinはプリインストールなのでInclude Kotlin supportのチェックボックスをonにする
image.png (39.6 kB)

ActivityはBasic Activityを追加しておく
image.png (47.8 kB)

エミュレータで実行するとHello Worldする。OK
image.png (25.1 kB)

自動生成されるKotlinファイルを読み解く

Kotlin supportをチェックしておくとMainActivity.javaの代わりにMainActivity.ktが自動生成される。Kotlinファイルを読んでみる

MainActivity.kt
package blue.hassaku.hellokotlin

import android.os.Bundle
import android.support.design.widget.Snackbar
import android.support.v7.app.AppCompatActivity
import android.view.Menu
import android.view.MenuItem

import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        setSupportActionBar(toolbar)

        fab.setOnClickListener { view ->
            Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
                    .setAction("Action", null).show()
        }
    }

    override fun onCreateOptionsMenu(menu: Menu): Boolean {
        // Inflate the menu; this adds items to the action bar if it is present.
        menuInflater.inflate(R.menu.menu_main, menu)
        return true
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        return when (item.itemId) {
            R.id.action_settings -> true
            else -> super.onOptionsItemSelected(item)
        }
    }
}

セミコロンの無い文、後置の型指定、各所のアロー演算子など気になるところはあれど、Androidマンならだいたい読めるのでは?

kotlin-android-extensionsによるViewの解決

MainActivity.kt
        ...
        fab.setOnClickListener { view ->
            Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
                    .setAction("Action", null).show()
        }
        ...

急に出てくるfab。これが定義されているのはこちら。

activity_main.xml
    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|end"
        android:layout_margin="@dimen/fab_margin"
        app:srcCompat="@android:drawable/ic_dialog_email" />

import kotlinx.android.synthetic.main.activity_main.*とすることで、id付きViewがid名に自動で解決される。すばらしい。
JavaのようにfindViewById()してキャストして4といった操作が必要ない。

式として解釈できるifとwhen

kotlinではifwhenは式として解釈される。(whenはJavaでいうところのswitch

MainActivity.kt
        ...
        return when (item.itemId) {
            R.id.action_settings -> true
            else -> super.onOptionsItemSelected(item)
        }
        ...

上記のコードは、item.itemIdの値がR.id.action_settingsと同値ならtrueを返し、それ以外ならsuper.onOptionsItemSelected(item)を返すという処理。

感想

今後本格的に学んでいくため、現時点ではKotlinの良さというのは判らない。ただ、Javaとの100%相互運用性があるため、少しずつでもKotlinを覚えていくと幸せになりそう。

2018年3月28日水曜日

Hello Python

この文書作成時の環境は以下です。

  • Windows 10 professional バージョン1703
  • Chocolatey v0.10.8
  • Python v3.6.4:d48eceb
  • PyCharm Community 2017.3.3

小規模なスクリプトにPythonを使いたい

ログとかをごちゃごちゃ弄るスクリプトは今までbatやPowerShellを主に使ってきた。
PowerShellはともかくbatは二度と書きたくないので、高級言語で簡単なスクリプトを書けるようになろうってんでやり玉に挙がったのがPython。なんか最近人気みたいだし。1 何よりparseargsっていうコマンドラインパーサが標準で用意されているらしい。これがとても魅力的だった。

チュートリアルを上から順番にやるってめんどくさいじゃん

この考えはあんまり良くない考え方だと思うけど、事情により「学習のための時間」が取れないことだってあるのだ。
Pythonをこれから長期的に使っていくとした場合、チュートリアルを上から実践していくのが、最終的にコストが小さくなるであろう。
時間があるならチュートリアルを上から順番にやればいいし、今後やるつもりだがすぐに動くスクリプトを要求されたので付け焼刃でPythonスクリプトを書いていく。

とりあえずPythonを使えるようにする

chocolatey2 使えば一発よ。chocolateyはwindowsの標準であるところのPackageManagement3 よりもパッケージが充実しているのでオススメ。

PowerShell(管理者)
> cinst python3

ついでにPythonのIDEであるPyCharmも入れとこう。JetBrains製のIDE好きよ。

PowerShell(管理者)
> cinst pycharm-community

PyCharmの起動は置いておいて、Pythonのインストールが完了したらおもむろにPythonインタプリタを起動

PowerShell
> python

インタプリタでHelloって言うとHelloって返してくれる。Pythonは挨拶ができるいいやつだ。

Pythonインタプリタ
>>> 'Hello'
'Hello'

Python版Hello World!

Hello.py
#! /usr/bin/env python
# -*- coding: utf-8 -*-

import sys

sys.stdout.write('Hello World!')

まあなんとなくわかる。

shebang行

#! /usr/bin/env python
shebang(シェバン)行と呼ばれるものらしい。
3. Windows で Python を使う — Python 3.6.4 ドキュメント
Linux環境で./Hello.pyと実行したときに/usr/bin/env pythonに解決されることを意味するようだ。
詳しくは呼んでいないがWindows環境でも書いておくと可搬性が上がるらしい。

マジックコメント

# -*- coding: utf-8 -*-
ソースコードの1行目または2行目にソースコードのエンコード形式を記載するらしい。
デフォルトはUTF-8なので冗長ではあるけども、もしかしたら今後デフォルトのエンコード形式が変更になるかもしれないのでおまじない的に書いておくのがいいのかな?
2. Python インタプリタを使う — Python 3.6.4 ドキュメント

標準モジュールの読み込みと利用

pythonの標準モジュールであるsysモジュールを読み込み、利用している。

sys モジュールは、全ての Python インタープリターにビルトインされています。
6. モジュール (module) — Python 3.6.4 ドキュメント

とのこと。
こちらを利用して標準出力にHello World!を出力している。

なんか作ってみる

(必要に迫られたので)ディレクトリ内のテキストを結合するツールを作成する。
Pythonを使わずとも、あるいはPythonでももっと簡単な方法があるだろうけど、あえて不慣れなPythonで。

できたものがこちら。

Merge.py
#! /usr/bin/env python
# -*- coding: utf-8 -*-

import sys
import argparse
import os

parser = argparse.ArgumentParser()
parser.add_argument('srcDir', help='Target directory contains txt', type=str)
parser.add_argument('-d', '--deleteSrc', help='delete merged source files')
args = parser.parse_args()

if not os.path.isdir(args.srcDir) :
    sys.stderr.write('argument [srcDir] is not directory.')
    sys.exit(1)

for filename in os.listdir(args.srcDir) :
    path = os.path.join(args.srcDir, filename)
    if(os.path.isdir(path)) : break
    with open(path, 'r', encoding='utf_8_sig') as f :
        for line in f:
            sys.stdout.write(line)

特徴的なのはオフサイドルール4に従ってブロックが記述されていることだろうか。

文、変数の代入、関数呼び出し

Merge.py
parser = argparse.ArgumentParser()
parser.add_argument('srcDir', help='Target directory contains txt', type=str)
parser.add_argument('-d', '--deleteSrc', help='delete merged source files')
args = parser.parse_args()

動的型付けで特に宣言なくいきなり変数に代入が可能な模様。(型ヒントは存在するようだ5
関数呼び出しはxxx(引数, 引数)という形式。文は改行で区切るのが普通らしい。

条件分岐

Merge.py
if not os.path.isdir(args.srcDir) :
    sys.stderr.write('argument [srcDir] is not directory.')
    sys.exit(1)

if文。条件式は()でくくらないのが普通らしい。
論理演算がnotandなどの英単語なのが読みやすくておしゃれ。
ちなみに、elseifではブロックが深くなっていって具合が悪いのでelifというキーワードを用いる模様。
また、Pythonで共通してブロックの開始は:でブロックの終了はインデントが戻るところ。

繰り返し

Merge.py
for filename in os.listdir(args.srcDir) :
    path = os.path.join(args.srcDir, filename)
    if(os.path.isdir(path)) : break
    with open(path, 'r', encoding='utf_8_sig') as f :
        for line in f:
            sys.stdout.write(line)

今回はリストのイテレーション。
for x in xxx :でxxxコレクションの中身をイテレーションするみたい。
withはC#のusing構文やJavaのtry-with-resource構文に相当するキーワード。ブロック終了で資源が回収されるらしい。

やってることは全部のファイルを全部のファイルごとに一行ずつ標準出力に書くとか頭の悪いことしてるので、もうちょっと頭良く処理する例を作ればよかった。

感想

ブラックボックスで構わないちょっとしたツールなんかを作るときは便利だな~って感じ。

  • 可読性に重きを置いた文法に思える
    • 現状読みなれていないので本当に読みやすいかどうかはわからない
  • 標準ライブラリが豊富
    • ライブラリの選定が必要ないのは大きな利点

[ChromeExtension]コンテキスト間メッセージ通信

この文書作成時のChromeバージョンは以下です。

  • 63.0.3239.132 (64bit)

関連記事

成果物

やりたいこと

Twitterタイムライン上の画像で右クリックコンテキストメニューしたときに以下を取得したい。

  • 画像投稿者のユーザー名
  • ツイートID
  • 複数画像の何枚目か(twitterは1ツイートに4枚まで画像を添付できる)

右クリックしたコンテキストのソースURLぐらいしか手掛かりがないので、表示中のTwitterのDOMを解析して解決する。
もちろん、TwitterのDOM構成が変わると動かなくなる。

Content Scripts を使ってtwitterにスクリプトを埋め込む

Content Scriptsは特定のページに埋め込まれ、ページのDOMにアクセス可能だ。今回はtwitter.comの中ならどこでも埋め込むようにする。jQueryも使うので埋め込んでおく。

manifest.json
{
  ...
  "content_scripts":[
     {
          "matches" : [
              "*://twitter.com/*"
          ],
          "js":[
              "scripts/getImageInfo.js",
              "scripts/jquery-3.3.1.min.js"
          ]
      }
  ],
  ...
}

content_scriptsが配列であることからわかるように、複数のページにスクリプトを埋め込むことができる。

コンテキスト間メッセージ通信

Browser Action (Page Action)によるポップアップ、Event Page、Content Scriptが埋め込まれたページ、これらは別コンテキストである。
他のコンテキストの処理を呼び出す場合には、Chrome経由でメッセージ通信を行う必要がある。

今回はコンテキストメニュークリック時にEvent PageからContent Scriptにメッセージを送信する。
また、Content ScriptでDOM解析した結果をEventPage側で同期的に処理する。
image.png (18.4 kB)

送信側

Content Script を呼び出す場合はchrome.tabs.sendMessage()を使う。
それ以外を呼び出す場合はchome.runtime.sendMessage()を使う。
注意点は以下

  • Content Scriptにメッセージを送信する場合とそれ以外に送信する場合で、APIが異なる
  • 一度も起動していないBrowser Action (Page Action)ポップアップは存在しないので、メッセージを送信できない
background.js
...
function downloadImage(srcUrl, response){
    ...
}

function messageToGetTweetInfo(info, tab){
    console.log(info);
    chrome.tabs.sendMessage(
        tab.id,
        {name: 'twitterImageDL', srcUrl: info.srcUrl},
        function(response) {downloadImage(info.srcUrl, response);}
    );
}

chrome.contextMenus.onClicked.addListener(messageToGetTweetInfo);

...

コンテキストメニューを開いたページにメッセージを送る場合は、contextMenus.onClicked.addListener()で登録した関数の
第2引数(tab)のidプロパティをtabs.sendMessage()の第1引数に入れるとよい。
tabs.sendMessageの第2引数には、受信側で行う処理を一意に特定できる識別子を入れるとよい。
同期メッセージの結果はコールバックの引数として取得できる。

ポップアップ起動時など、tabのidが不明な場合は、chrome.tabs.queryを用いてタブを取得する。

popup.js

chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
    chrome.tabs.sendMessage(tabs[0].id, {name:'message_name'}, function(response){
        ...
    });
});

受信側

chrome.runtime.onMessageイベントリスナーを登録する。
結果の返送は第3引数のハンドラを利用する。

getImageInfo.js
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse){
    console.log(request);
    if(request.name === 'twitterImageDL'){
        ...
        sendResponse({
            result : true,
            username : username,
            tweetId : tweetId,
            imageIndex : imageIndex
        });
    }
    return true;    //  sync
});

注意点は以下

  • trueを返却すると同期実行、trueを返却しないと非同期実行
  • 同じExtension内のメッセージイベントのみが発火する(他のアプリからのメッセージを受ける場合はonMessageExternal

DOMを読む

jQueryで泥臭く読む。twimg.comのURLをsrcに持つimgタグを検索するところからスタート。特に説明は割愛。

これでユーザー名、ツイートID、画像が何枚目かを取得できた。
chrome.downloads.downloadの引数にファイル名を指定してダウンロードできるようにすれば、一旦は完成!