ラベル JavaScript の投稿を表示しています。 すべての投稿を表示
ラベル JavaScript の投稿を表示しています。 すべての投稿を表示

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年3月28日水曜日

[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の引数にファイル名を指定してダウンロードできるようにすれば、一旦は完成!

2018年3月23日金曜日

[ChromeExtension]右クリックからコンテンツをダウンロード

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

  • 63.0.3239.132 (64bit)

関連記事

成果物

やりたいこと

twitterタイムライン上の画像のオリジナルイメージを右クリックからダウンロード可能にする。
以下のAPIを使う

  • chrome.contextMenus
  • chrome.downloads

pbs.twing.comソースのときだけメニューを表示する

contextMenus.createのプロパティを書く。
contextsをimageに絞り、画像の上だけでメニューを表示するように、
documentUrlPatternsで、twitter.comのみでメニューを表示するように、
targetUrlPatternsで、画像のソースがpbs.twimg.comの場合のみにメニューを表示するように変更。

background.js

chrome.runtime.onInstalled.addListener(function(){
    chrome.contextMenus.create({
        type: 'normal',
        id: 'downloadTwitterImage',
        title: 'download original image',
        contexts: [
            'image'
        ],
        documentUrlPatterns: [
            '*://twitter.com/*'
        ],
        targetUrlPatterns: [
            '*://pbs.twimg.com/media/*'
        ]
    });
});

URLを指定してダウンロード

downloads.downloadのAPIでソースのURLに:origを付加してDLする。

background.js
function downloadImage(info, tab){
    console.log(info);
    chrome.downloads.download({
        url: (info.srcUrl + ':orig'),
        saveAs: false
    }, function(id){downloadId = id;});
}

function renameDownloads(downloadItem, suggest){
    console.log(downloadItem);
    if(downloadItem.id !== downloadId) return;
    suggest({
        filename: downloadItem.filename.replace('_orig', '')
    });
}

chrome.downloads.onDeterminingFilename.addListener(renameDownloads);
chrome.contextMenus.onClicked.addListener(downloadImage);

saveAsプロパティをtrueにすると、ダウンロード時に「名前を付けて保存」ダイアログが展開するようになる。

ファイル名を変更する

そのままDLすると拡張子がjpg_origとなり使い勝手が悪いので、downloads.onDeterminingFilenameイベントを使用して_origを取り除く。
downloads.downloadのコールバック引数でダウンロードIDが取得できるので、DownloadItem.Idと比較することで、今回ダウンロードしたもののみ変更することができる。

permissionを更新する

downloads APIを利用するので、manifestのpermissionにdownloadsを追加する。

manifest.json
{
  ...
  "permissions" : [
    "contextMenus",
    "downloads"
  ]
  ...
}

以上でオリジナル画像のダウンロードが可能になった。
次はコンテキスト間を通信して、ユーザー名とツイートIDをDOMから取得する。

2018年3月19日月曜日

はじめてのChromeExtension

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

  • 63.0.3239.132 (64bit)

関連記事

成果物

作りたいもの

世の中には絵がうまい人が溢れている。
のちのち資料として利用するために画像を保存するわけだけど、twitterに投稿されたオリジナルの画像をローカルに保存するのは結構手間。

  1. 画像を右クリックして画像アドレス(pbs.twing.com/media/xxxxxxxxxxx)をコピー
  2. 画像アドレスの末尾に「:orig」を付加してブラウザで表示
  3. 名前を付けて画像を保存で保存する

既存のものとしてはコンテキストメニューからオリジナル画像をDLするChrome拡張がいくつか公開されている。

これらの拡張は便利なのだが、デフォルトファイル名がいまいちで利用しにくい。
そこでユーザー名とツイートIDでリネームするChromeExtensionを作成しようとした。

はじめてのChromeExtension

以下は公式ドキュメントにだいたい書いてあることです。

ChromeExtension概要

ChromeExtensionはChromeに機能を追加するソフトウェアで、JavaScriptで動作する。
ChromeExtensionは主に以下のコンテキストから構成されているようだ。

  • Browser Action (Page Action)
  • Content Script
  • Event Page

これらのコンテキストをマニフェストファイルに記載することでChromeがこれを解釈できるようなる。
他にもOptionPageなどがあるようだが、メインは上記の3種類のコンテキスト。

Browser Action (Page Action)

ChromeExtension独自のHTMLページを表示する。
インストールすると右上に表示されるアイコンをクリックするとHTMLページがポップアップする。
Browser Actionは全てのページで利用することが可能で、Page Actionは特定ページでのみ利用できる。
Page Actionが利用できないときは、アイコンはグレーアウトする。1

manifestファイルにはBrowser Action か Page Actionのどちらか、または利用しない場合はどちらも記載しない。

manifest.json
{
  "name": "Extension Name",
  ...
  "browser_action": {
    "default_icon": {
      "19": "icons/icon19.png",
    },
    "default_title": "Title",
    "default_popup": "popup.html"
  },
  ...
}

Content Script

特定のページにスクリプトとスタイルシートを埋め込む。
埋め込んだページのDOM操作が可能。
ページ内のスクリプトとは独立して動作するので、ページが持っている関数などは利用できない。

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

Event Page

バックグラウンドでスクリプトを動作させる。
目に見えないWebページが存在してそこにスクリプトが埋め込まれるイメージ。

manifest.json
{
  "name": "Extension Name",
  ...
  "background" : {
    "scripts" : ["scripts/background.js"],
    "persistent" : false
  },
  ...
}

persistentをtrueにすると Backgroud PageとなりChromeに常駐する。(常にメモリを食うので非推奨)

右クリックコンテキストメニューから Hello World !!

右クリックからこんにちは世界!

ディレクトリ構成

プロジェクトルートディレクトリにmanifest.jsonを配置する。
残りの配置は自由。今回は以下の配置とした。
アイコンはIncscapeで適当に準備した。(慣れてないので1時間かかった)

TwitterImageDL
┣ icon
┃ ┣ icon16.png
┃ ┣ icon19.png
┃ ┣ icon48.png
┃ ┗ icon128.png
┣ scripts
┃ ┗ background.js
┗ manifest.json

マニフェストファイルの記述

公式ドキュメントを読みながら必要そうなものを追加していく。

manifest.json
{
  "manifest_version" : 2,
  "name" : "Twitter Image DL",
  "short_name" : "Twitter Image DL",
  "version" : "0.0.0",
  "description" : "Download and rename original image in twitter.com.",
  "icons" : {
    "16" : "icon/icon16.png",
    "48" : "icon/icon48.png",
    "128" : "icon/icon128.png"
  },
  "background" : {
    "scripts" : ["scripts/background.js"],
    "persistent" : false
  },
  "permissions" : [
    "contextMenus"
  ]
}

スクリプト

スクリプトを書く。公式サンプルのContext Menus Sample (with Event Page)を参考に書く。Context Menus Sampleの方はEvent Pageに対応していないので、こちらを参考にすると詰まる。2
chrome.runtime.onInstalledイベントでコンテキストメニューを作成する。
イベントページ起動のタイミングでコンテキストメニューのonClickedイベントに世界とこんにちはを登録する。

background.js
function helloWorld(){
    console.log('Hello World !!');
}

chrome.runtime.onInstalled.addListener(function(){
    chrome.contextMenus.create({
        type: 'normal',
        id: 'helloWorld',
        title: 'hello world !!',
        contexts: [
            'all'
        ]
    });
});

chrome.contextMenus.onClicked.addListener(helloWorld);

デバッグ実行

chrome://extensionsにディレクトリをまるごとドラッグアンドドロップすると、インストールが完了する。
右上のチェックボックス「デベロッパーモード」を有効にして、「バックグラウンド ページ」をクリックするとイベントページを確認することができる。
拡張機能 - Google Chrome 2018-02-19 15.03.57.png (37.8 kB)

コンテキストメニューに作成したメニューが追加されていることを確認
image.png (17.3 kB)

実行するとConsoleが世界とこんにちはすることを確認
image.png (10.1 kB)

できた!
次はpbs.twing.comから画像をダウンロードする。


  1. 利用可能なら青バッジが付く仕様だったときもあったようだ。UI的にはアイコンはモノクロでないほうがよさそうだ。 

  2. chrome.contextMenus.createの引数プロパティのonClickがEvent Pageでは利用できないようだ。ドキュメントに書いてある。