Chrome Extensionでomnibox apiを使ってみる

Chromeでアドレスバーに検索文字列を入力してエンターを押すと現在開いているタブ上に検索結果が表示される。この動作が非常に気に食わなかったのでExtensionを作って新しいタブ上で検索結果が表示されるようにしてみようとやってみた。結果としては、そんなことはできないみたいだけどExtensionの基本的な作り方がわかった気がするので備忘録がてら書く。

サンプルを実行するまでの道のり

  • https://developer.chrome.com/extensions/samples#omniboxからサンプルをダウンロード&解凍
  • [設定]→[拡張機能]で拡張機能画面を開いて、画面右上の[デベロッパーモード]のチェックを付ける。
  • [パッケージ化されていない拡張機能を読み込む]で解凍したフォルダを選択

サンプルの中身(manifest.json)

{
  "name": "Create NewTab Enter Adress Bar",
  "version": "0.0.1",
  "description": "Create NewTab Enter Adress Bar",
  "background": {
    "scripts": ["background.js"]
  },
  "omnibox": { "keyword" : "omnix" },
  "manifest_version": 2
}

気になるところだけを抜粋。大体名前でわかる。

omnibox
omniboxを使うという宣言。アドレスバーに「omnix」と記載すると拡張機能が立ち上がる
manifest_version
どのバージョンのマニフェストファイルかを表してる模様。2で良い

サンプルの中身(background.js)

// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// This event is fired each time the user updates the text in the omnibox,
// as long as the extension's keyword mode is still active.
chrome.omnibox.onInputChanged.addListener(
  function(text, suggest) {
    console.log('inputChanged: ' + text);
    suggest([
      {content: text + " one", description: "the first one"},
      {content: text + " number two", description: "the second entry"}
    ]);
  });

// This event is fired with the user accepts the input in the omnibox.
chrome.omnibox.onInputEntered.addListener(
  function(text) {
    console.log('inputEntered: ' + text);
    alert('You just typed "' + text + '"');
  });

イベントに関数を設定してイベントが発生した時に呼んでくれる。onInputChangedは文字入力時、onInputEnteredはエンターキーをおした時。なので、「omnix」という文字列をアドレスバーに入力した後に何か文字を入れるとsuggestで登録したものが表示され、エンターキーを押すと入力した文字列がアラートで表示される。他にもAPIはあるみたいなので、詳しく知りたい方は以下のURLを参照のこと。

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

まとめ

つまりは、アドレスバーにkeywordで登録した文字列を入力しないと拡張機能が立ち上がらないので「検索文字列を入力してエンターを押すと検索結果を新しいタブで表示する」を実現するためにはkeywordで登録した文字列を一回入力する必要がある。「g」だったらGoogleで検索した結果を・・・とかも考えたけど、「g」という文字列を毎回押すぐらいならショートカットキーで新しいタブを開いて検索したほうが早いのであまり意味が無い。ChromeのExtensionを書くのが簡単だというのがわかっただけでもよしとしよう。

angular.jsのdirectiveでlistにactiveをつける

bootstrapでよくある縦並びのメニューでactiveクラスをつけるdirectiveをつくってみる

directiveの指定種別

directiveはattribute、element、classの3つで指定できる。デフォルトはattributeなのでclassに変更する必要がある

restrict: 'A' // Attribute <div ng-custom></div>
restrict: 'E' // Element <ng-custom></ng-custom>
restrict: 'C' // Class <div class="ng-custom"></div>

directive名について

directive名はlowercaseで指定してcamelcaseで指定する

directive('ngCustom', function() {
    // これはng-customのdirectiveになる
});

link/compile

詳細な動作はlink/compileで記載。今回はactiveを付けたいのでlink内で判定を記載する

pushstate時のhandling

link/compileで記載するとリロード時には動作するが、pushstateだと動かない。イベントをScopeでWatchする必要がある

できあがり

以上を加味して記載するとこんな感じになるはず。list-group-itemのhrefを引っ張ってきて、それを現在のlocation.hrefと比較してactiveクラスをつけたり消したりしている。

	angular.module('myApp').directive('listGroupItem', function() {
			return {
				restrict:'C',
				link: function($scope, element, attrs) {
					$scope.$on('$routeChangeStart', function() {
						if(attrs.href == location.href) {
							element.addClass('active');
						} else {
							element.removeClass('active');
						}
					});
				}
			};
		}
	);

まとめ

$routeChangeStartのイベントをwatchするということがわかるまで時間がかかったけど、directiveを使って色々できそう。でもこれってカスタマイズし過ぎたら可読性が落ちる諸刃の剣だと思う。決まったところに決まったものを出すだけであれば使えばいいんじゃないかな。

APIのテストにはSuperTestが便利

node.js + expressでのテスト環境を探していて見つかったんだけど、これは便利

概要

色々なhttpリクエスト投げて、結果をMochaで表示できる。

リンク先

https://github.com/visionmedia/supertest

http://visionmedia.github.io/mocha/

導入

両方共グローバルにしてもいいかも。SuperTestはグローバルにする必要もないかと思ってしてない。

npm install supertest npm install -g mocha

使い方

以下のテストコードを走らせるとhttp://localhost:3000につないでrequest.get()などで接続したホストに対してリクエストを投げて、レスポンスをテストすることができる。

/**
 * テスト雛形
 * hostにつないで、各メソッド事のテストを書く
 * @author sato_shinichiro
 */ 
var request = require('supertest');
var host = 'http://localhost:3000';

describe('テスト種別1', function () {

	before(function() {
		console.log('テスト前に動く');
	});

	after(function() {
		console.log('テスト後に動く');
	});

	it('テストする内容(ステータスエラー)', function (done) {
		var status = 300;
		var res = {
			meta: {
				code: 200,
				message: "ok"
			},
			data: null
		};
		request(host)
			.get('/test')
			.expect(status, res, function(err) {
				console.log('テスト1終了');
				done(err);
			});
	});
	it('テストする内容(レスポンス内容エラー)', function (done) {
		var status = 200;
		var res = {
			meta: {
				code: 300,
				message: "ok"
			},
			data: null
		};
		request(host)
			.get('/test')
			.expect(status, res, function(err) {
				console.log('テスト2終了');
				done(err);
			});
	});
});

describe('テスト種別2', function () {
	it('テストする内容(成功)', function (done) {
		var status = 200;
		var res = {
			meta: {
				code: 200,
				message: "ok"
			},
			data: null
		};
		request(host)
			.get('/test')
			.expect(status, res, function(err) {
				console.log('テスト3終了');
				done(err);
			});
	});
});

テストを走らせるには以下のコマンド。./testにテスト用のjsがいる。mochaはフォルダを指定すると配下のjsファイルを実行してくれる。

mocha test

テスト結果はこんな感じ。

 テスト前に動く
テスト1終了
․テスト2終了
․テスト後に動く
テスト3終了
․

  ✖ 2 of 3 tests failed:

  1) テスト種別1 テストする内容(ステータスエラー):
     Error: expected 300 "Multiple Choices", got 200 "OK"
      stacktrace...

  2) テスト種別1 テストする内容(レスポンス内容エラー):
      
      actual expected
      
      1 | {
      2 |   "meta": {
      3 |     "code": 300200,
      4 |     "message": "ok"
      5 |   },
      6 |   "data": null
      7 | }

      stacktrace...

stacktraceって書いてる部分はパスが書いてあるから削った。本来はちゃんとstacktraceが出る。

もし、サーバを立てずにexpressだけで動かしたかったら以下の様なコードを書いてexportしたappをhostの代わりに指定してあげれば動く

var express = require('express');
var app = express.createServer();
exports.module = app;

まとめ

最初Express用のテストフレームワークかと思ったけど、一般的なサーバのAPIならこれで全て叩いてテストできるんじゃなかろうか。SuperTestは便利。

mongooseで配列を連想配列でネストした時につく_idを消したい

悩んだので覚書

ユーザが書いたコメントを参照として引っ張ってきて欲しくて、それと同時に記載した時間も取りたいっていう場合を想定。

単純にスキーマを書くとこうなる。

var userSchema = {
	name: String,
	comments : [{
		comment: {type: Number, ref: 'comment'},
		updated: {type: Date, default: Date.now}
	}]
};

そうするとなぜか、commentsの要素たちに_idが勝手に入ってしまう。

非常に_idが邪魔

var userSchema = {
	name: String,
	comments : [Schema.Types.Mixed]
};

こう書くとと、commentsに_idは入らないのでmongooseが勝手に挿入してるっぽい。

調べるとどうも連想配列を新しいスキーマとして認識してるから_idが勝手に入るよう。

なので、スキーマとして_idを消すような処理を入れる

var userSchema = {
	name: String,
	comments : [new Schema({
		comment: {type: Schema.Types.ObjectId, ref: 'comment'},
		updated: {type: Date, default: Date.now}
	}, {_id: false})]
};

こうすると、_idが消える。めでたしめでたし。

Canvas操作の基本

Canvas操作の基本をメモる

線を引く

beginPath()で開始。moveTo()で開始位置まで動いて、線を引く場所までlineTo()で動く。stroke()で実際に惹かれる。色を替える場合はstroke()の前なら大丈夫。strokeStyleで線の色を設定。

	// 線を引く
	ctx.beginPath();
	ctx.moveTo(xStart, yStart);
	ctx.lineTo(xEnd, yEnd);
	ctx.strokeStyle = color;
	ctx.stroke();

長方形を書く

fillRect()でいける。width/heightを引数に渡すことを注意。塗りつぶしの色はfillStyleで設定。

	// 長方形を書く
	ctx.fillStyle = color;
	ctx.fillRect(xStart, yStart, width, height);

不規則な形

lineTo()で適当に線を引きまくって、closePath()を呼び出せば最初と最後がくっつく。塗りつぶす場合はfill()でいける。

	// 不規則な形(三角形)
	ctx.beginPath();
	ctx.moveTo(xStart, yStart);
	ctx.lineTo(x1, y1);
	ctx.lineTo(x2, y2);
	ctx.strokeStyle = color;
	ctx.closePath();

グラデーション

createLinearGradient()は開始位置と終了位置で色の変わり方を設定、start/endは0-1の値を設定する。縦にかけたければX軸を、横にかけたければY軸を同じ値にする。

	// グラデーション(縦方向)
	var grad  = ctx.createLinearGradient(startX, startY, endX, endY);
	grad.addColorStop(start, colorStart);
	grad.addColorStop(end, colorEnd);
	ctx.fillStyle = grad;

アニメーション

案外力わざ。setInterval()でクルクル回して少しづつ伸ばしていくみたい。これならCSSアニメーションの方が綺麗だったりしないのかな。

	// 横長くなるアニメーション
	var w = 0;
	var animId = setInterval(function() {
		ctx.fillStyle = color;
		ctx.fillRect(xStart, yStart, w, height);
		w++;
		if(w > width) {
			clearInterval(animId);
		}
	}, 100);

まとめ

以上。ざっくりと使える分を書いてみた。

ネーミングに困ったので適当に名前を出す奴を作った

名前つけるのって難しい。なんか適当に文字をくっつけてとかやっても語感が悪いの嫌いだから語感だけで決めたかった。ということで、語感が良くなる法則を適当に考えてみると繰り返すとなんか良くなると思う。「まぐまぐ」とか「ぱるる」とかそんな感じね。これを適当な文字列で出力してインスピレーションをわかすために適当に作った。

var str = 'あいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほまみむめもやゆよらりるれろわをんがぎぐげござじずぜぞだぢづでどばびぶべぼぱぴぷぺぽ';

// random
var base = '';
for(i=0 ; i< 4 ; i++) {
	var start = Math.floor(Math.random() * str.length);
	base = base + str.substring(start, start + 1);
}
console.log(base);

// 繰り返し
var r = '';
for(i=0 ; i<2 ; i++) {
	r = r + base.substring(i, i+1);
}
for(i=0 ; i<2 ; i++) {
	r = r + base.substring(i, i+1);
}
console.log(r);

var r = '';
for(i=1 ; i<3 ; i++) {
	r = r + base.substring(i, i+1);
}
for(i=1 ; i<3 ; i++) {
	r = r + base.substring(i, i+1);
}
console.log(r);

var r = '';
for(i=2 ; i<4 ; i++) {
	r = r + base.substring(i, i+1);
}
for(i=2 ; i<4 ; i++) {
	r = r + base.substring(i, i+1);
}
console.log(r);

// 続き
var r = '';
for(i=0 ; i<4 ; i++) {
	if(i<2) {
		r = r + base.substring(0, 1);
	} else {
		r = r + base.substring(i, i+1);
	}
}
console.log(r);

var r = '';
for(i=0 ; i<4 ; i++) {
	if(i>0 && i<3) {
		r = r + base.substring(1, 2);
	} else {
		r = r + base.substring(i, i+1);
	}
}
console.log(r);

var r = '';
for(i=0 ; i<4 ; i++) {
	if(i>1 && i<4) {
		r = r + base.substring(2, 3);
	} else {
		r = r + base.substring(i, i+1);
	}
}
console.log(r);

気が向いたらもうちょっと改良するかも。

location.hrefでの画面遷移はSEO的にどうなの?

色々調べてみたがGoogleさんがどういう解析をしているのかがわからないので結論は出ない。

http://www.suzukikenichi.com/blog/google-can-read-javascript-and-pass-pagerank-and-anchor-text/

Googleは、Javascriptのリンクも通常のリンクと同じように取り扱い、PageRankも渡すし、アンカーテキストも評価するようになりました。

ただし同一ページ内にスクリプトのコードが存在している場合で、スクリプトファイルを外部化している場合は対象になりません。

という記事が2009年に出ているので、これよりもクロールしなくなっているということは無いんだと想像できて、同一ページにlocation.hrefでアンカーを貼った場合は評価してくれることになる。別の言い方をすると、別ページにlocation.hrefによるページ遷移を書いた場合はアンカーテキストとして認識されず、それ以降のリンクをクロールしてくれないってことになる。

最近動的にページを作るサイトが増えているけど、少なくともアンカータグで記載している静的サイトに比べて動的に作成するコードが別ソースに書かれているサイトはSEO的に弱くなるんだろう。ajaxでデータ受信して動的に作成するサイトの場合、「#!」でハッシュ値を教えてあげてそれに対応した静的ページを作れば問題ないらしい。なんでそんな手間なことをやらないといけないんだって気がしなくはないんだが、JSの中身まで全部解析しろっていうのも刻な話しなんだろう。

結論としてはツールとして使われるようなサイトの場合は、TOPページがインデックスされれば問題ないという割り切りをして使いやすさを求めてajaxで動的に作成するっていうのはありだけど、Googleからの流入を狙っているようなサイト(ブログとか)でajaxで動的にメニュー作ってみようとかはやらないほうがいいんだろうなぁ。


Google Closure Compilerでフォルダ配下のjsファイルを1ファイルにまとめる

やるならシェル書いたほうがいい。

#!/bin/bash # 環境変数 rootPath="../html/js" outputFileName="all.js" compileLevel="SIMPLE_OPTIMIZATIONS" # jsファイル一覧取得 paths="" separate=" –js " for file in `find $rootPath -name \*.js` do paths=${paths}${separate}${file} done # コンパイル java -jar compiler.jar ${paths} –language_in ECMASCRIPT5 –compilation_level ${compileLevel} –js_output_file ${outputFileName}

それだけ。dirが指定出来ればそれでいいのに・・・。

はてブをシンプルに表示するSimpleHatebuを作った

はてブのタイル表示が非常に評判が悪くてシンプルにはてブを表示するページを作るのが流行ってるっぽい。ちょっと時期が過ぎたような気がしなくもないけど、便乗してSimpleHatebuってのを作ってみた

SimpleHatebu

どんなもの?

Yahooのニュースサイトのデザインを参考にカテゴリ別に表示する新着ページ。カテゴリを選択すればそのジャンルの新着が表示される。下の数ははてブ数の閾値を選択できる。至って単純。

開発の話

このページはすべてjavascriptで動いてる。どうやって値をとっているかというと、Google Feed APIを使ってる

Google Feed API  |  Google Developers

はてブってエントリーページにmode=rssのクエリパラメータを渡してあげるとrssで取得できるようにできてるから、そのRSSをGoogle Feed APIで取得してきてボタンが押されるたびに表示を入れ替えているっていう仕組み。Google Feed APIの使い方はすごい簡単。Google Feed APIの大本をもってきて、

        <script type="text/javascript" src="https://www.google.com/jsapi"></script>

こんなかんじで取れる

	google.load("feeds", "1");
	google.setOnLoadCallback(initialize);

	var initialize = function() {
		// URL指定してFeed作る
		var feed = new google.feeds.Feed('取得したいFeedのURL');
		// 取得するFormatを指定。JSONでも取れる
		feed.setResultFormat(google.feeds.Feed.XML_FORMAT);
		// 取得する件数を指定
		feed.setNumEntries(10);
		// 実際に取得
		feed.load(function(result) {
			// とれた値で好きに動かしましょー
		});
	};

で、再読み込みした時にカテゴリとかはてブの閾値とかが元に戻るのが気に食わなかったんでjQueryCookieを使って選択してるカテゴリとかをCookieにセットしてみた。こいつも使い方はすごい簡単。まず大本を持ってきて、

        <script type="text/javascript" src="./js/jquery.cookie.js"></script>

こんな感じで値の保存と取得ができる

	// 保存
	$.cookie('key', 'value');
	// 取得
	key = $.cookie('key');

本当はすべてのエントリーに対してはてブ数の閾値を設定できるようにしたかったんだけど、なぜかthresholdのクエリパラメータを設定してカテゴリのエントリーを取得すると最新のものではなく8月31日のデータがとれてしまう。はてな側が返答してる値が最新じゃないからこっちじゃどうしようもなくて本気だすとHTMLをGETしてパースして表示みたいなことをやるしかないみたいで、それをやるにはサーバ側のコードがどうしても必要だからしょうがなくカテゴリ別のものは新着だけしか取れないようにしてる。

まとめ

もともとはタイルでも見やすく作れるんじゃね?ってことでタイルっぽく作ろうとしてたんだけど、全然見やすくならなかった。タイルのいいところって情報をカテゴライズしたい場合にしか有効じゃないんだと思う。はてブみたいに文字データが基本でタイルでやろうとするとどうしても目の動きがきつくなるから、シンプルに文字だけで表示してカテゴリーごとにタイルにするっていうYahooニュースみたいな作りじゃないと見づらいと思う。それにしてもGoogle Feed APIはすごい便利。これ使えば簡単にiGoogleも再現できるんじゃないかなー。今度時間があったら作ってみようかな。

node.js + expressを調べてみた

触ってみたかったnode.jsを少しだけ触ってみた。

学んだことを箇条書きに

  • exports.hogehogeで設定したものはrequire()で変数(hugahuga)に指定してhugahuga.hogehogeみたいに使える
  • require()で指定するファイルは拡張子(.js)を省略できる。フォルダ名で指定した場合はindex.jsをdefaultで取得する模様
  • app.get()などでは正規表現でマッチするかを判別できる。数字だけならこんな感じ([0-9]+)
  • res.renderも同じように拡張子とか指定できる。
  • res.renderで取得するフォルダのデフォルトはapp.set(‘views’, hogehoge)で指定された場所
  • viewに指定するEngineはejs使ってるサンプルが多いけど僕はjadeが好き。

ぐらい。もうちょっと勉強が必要。