riina-k.net

音楽・小説・プログラミングを手がけるリイナの拠点。

カテゴリー ‘ プログラミング

github で rk. tv を管理

私が主宰している同人音楽サークル「rk. tv : あーけーどてぃーゔいー」の公式サイトのソースコードを github で管理するようにしました。
https://github.com/RiinaKw/riina-k.tv

本番環境へのデプロイがめっちゃ楽!(いまさら)
だって

$ git pull

これ叩くだけでいいんだもん!(だからいまさら)

コミット履歴を見ると分かりますが、初期ソースが原型を留めないほどリファクタリングしまくった結果、どこかの燃料っぽい独自フレームワークになってました。
composer 使って、クラスのオートロード使って、ルーティングテーブル定義して、やってること完全に燃料じゃん

ローカルリポジトリの管理に GUI ツールを導入することも検討しましたが、いまいち使い勝手が悪いので、結局コンソールからコマンド打ってます。これが一番楽かもしれない。

riina-k.tv に SSL(HTTPS) を導入してみた

同人サークルサイトを大規模リニューアル, mod_rewrite と初めて格闘してみた に続くリニューアル第3弾。

https 通信にしないと Google から警告を食らうという噂が!
Google Chrome、HTTPSサイトの「保護された通信」を非表示に 「デフォルトで安全が前提」 – ITmedia エンタープライズ
いよいよGoogleが本気。Chrome 68から全HTTPサイトに警告! | 常時SSL Lab. by Zenlogicのファーストサーバ株式会社

そこで riina-k.tv も https を導入してみました。
普通は SSL 証明書の発行にはかなりの金額がかかるものですが、なんと無料で使える神認証局「Let’s Encryptがありました。
インフラエンジニアの友人の助けを受けながら、これを使って SSL を導入。

続きを読む

mod_rewrite と初めて格闘してみた

WordPress や FuelPHP を使っていると、勝手に設定してくれる Apache の mod_rewrite
実際のファイル名とは違う URL を指定できるのがポイントです。

今までは自動生成してくれたデータを .htaccess にコピペするくらいしかしてませんでしたが、
同人サークルサイトを大規模リニューアル に関して「ハッシュタグ(・A・)カコワルイ!!」と思ったのと、各曲ごとに og タグを出力できないことを考えて、思い切って導入しました。

<IfModule mod_rewrite.c>
    RewriteEngine on

    RewriteBase /
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    #RewriteCond %{REQUEST_FILENAME} ^(.*)$
    RewriteRule ^(.*)$ %1.php
</IfModule>

今回やったのは「/music」と拡張子抜きで「/music.php」にアクセスできるようにするだけ。
これに加えてプログラム側でハッシュタグをパス形式に変更して

http://riina-k.tv/music/anthurium

こんな URL でアクセスできるようになりました。
たったこれだけの設定に何時間もかかりました……やっぱりフレームワークは偉大だなぁ。

同人サークルサイトを大規模リニューアル

私が主催する同人音楽サークル「rk. tv」の公式サイトを大幅にリニューアルしました。
フル Ajax+レスポンシブデザイン。HTML5 + CSS3 + jQuery で構築しており、AngularJS や Bootstrap などのフレームワークは一切利用していませんここ自慢できるポイント

リニューアルのきっかけは、音楽の試聴に SoundCloud を使い始めたため。
30曲近いトラックのすべてを iframe で読み込んでいたため、ページがめっちゃ重たい状態になっていました。
それを打破するため、「曲情報をクリックして初めて SoundCloud を読み込む」という体制に変更しようとしたらフルAjaxになってました(?)
もともとメニューの追従や背景画像のエフェクトなど簡単な JavaScript を使っていましたが、これを期に大幅に書き加え、さらに「せっかくならスマホ対応しようぜ」とやっていたら軽く1週間経ってました言い換えればこの程度のウェブサイトであれば1週間の納期でできるってこった

iOS 端末が手元に無いのと、持ってるタブレットが全滅してるため、レスポンシブの完全なデバッグができていない状態ですが、バグが見つかり次第潰していく予定です。

カテゴリごとにTwitterウィジェットを切り替えるプラグイン作ってみた

ごぶさたーーーー!

ここ最近、TwitterクライアントとかRuby on RailsとかWordPressとかいろんなものを触る機会が増えた。
触るだけで成果が見えないのももったいないので、軽くWordPressプラグインを作ってみた。

TwitterPerCategory

名前のまんま。
私のようにジャンルごとにTwitterアカウントを使い分けてる人が多いか少ないかは分からないけど、
私の場合
 プログラミング → @Riina_Kw_pg
 音楽 → @Riina_Kw_m
 小説 → @Riina_Kw_nv
と、3ジャンルに分けてて、このWordPressも同様のカテゴリ分けをしてる。

ここで困ったことが。
世に出回ってるTwitterプラグインは複垢に対応してない!

てことで、自分で作ってみた。(地産地消)

現時点では「とりあえず公式ウィジェットの貼り付け」だけだけど、いずれはカテゴリごとのデザイン変更とか該当カテゴリへの投稿時にTwitterに自動投稿とかも実装したい。

05/09 追記:
データの持ち方にバグがあるようで、いまウィジェットが表示されてません。もう一度調べなおしてみます。

Javascript の prototype を学び始めた

既に存在するオブジェクトの振る舞いを変えてしまう prototype。
どう上手く扱えばいいのか、少しずつ勉強を始めてみた。

まず書いてみたのが、textarea から入力文字列を受け取る際、改行コードを統一する関数。

String.prototype.replaceAll = function (org, dest)
{
	return this.split(org).join(dest);
}

String.prototype.uniformLF = function()
{
	return this.replaceAll("\r\n", "\n").replaceAll("\r", "\n");
}

正規表現を必要としない簡単な全置換が定義されていないため、replaceAll 関数を定義したのち、それを利用して改行コードを全置換。

これがうまく行ったので、以前の記事個人的に「これは組込み関数として必要だろ」っていうjavascript関数で作った関数を改造してみた。

Object.prototype.merge = function(arg)
{
	for ( var idx in arg ) {
		if ( arg.hasOwnProperty(idx) && typeof arg[idx] != 'undefined' && typeof arg[idx] != 'function' ) {
			this[idx] = arg[idx];
		}
	}
	return this;
}

Object.prototype.sliceKeys = function(keys)
{
	var result = {};
	for ( var idx in keys ) {
		var key = keys[idx];
		var item = this[key];
		if ( typeof item != 'undefined' && typeof item != 'function' ) {
			result[key] = ( this.hasOwnProperty(key) ? item : null );
		}
	}
	return result;
}

Object.prototype.walk = function(cb, param)
{
	if ( !cb ) {
		return;
	}
	for ( var idx in this ) {
		if ( this.hasOwnProperty(idx) ) {
			var item = this[idx];
			if ( typeof item != 'undefined' && typeof item != 'function' ) {
				cb(item, idx, param);
			}
		}
	}
}

for( … in …… ) の構文だと、プロパティやメソッド、prototype で指定したメソッドまで現れてしまうので、this.hasOwnProperty() 関数を多用するハメに。

しかしこれのおかげで、前の記事で書いたサンプルコードがこんな感じに豹変。

// 元のデータを保持するオブジェクトを生成
var obj = {
	id: 123,
	title: 'rk.tv',
	link: [
		'http://riina-k.net/',
		'http://riina-k.tv'
	]
};

function printHTML(item, key, jq)
{
	alert( '<dt>' + key + '</dt>' + '<dd>' + item + '</dd>' );
}
$('body').append('<dl id="output"/>');
	
// そこにプロパティを追加、切り出したプロパティそれぞれにコールバック関数を適用し、dlタグを出力
obj.merge({ author: 'Riina K.', param: 'foooo' }).sliceKeys(['title', 'author']).walk( printHTML, $('dl#output') );

追加したメソッドの戻り値のおかげで、メソッドチェーンを使って複雑な処理も1行で書けるようになった! これは便利。

遅延ロードのライブラリをクロスブラウザ対応

先日の記事「遅延ロードをライブラリ化してみた」がIEで動作しなかった原因が分かったので、一部書き直し。

delayload.js

変更点は2箇所。ひとつは「オブジェクトのプロパティとして class という名前は使用できない」という制約があるため、これを「target」というプロパティ名に変更。
もう一つは、img要素を生成する際、先にwidth, heightを設定してからsrcを設定してしまうと、IEでは先に設定したサイズを無視してオリジナルサイズでwidth, heightを上書きしてしまうため、srcを設定してからwidth, heightを設定という順番に変更。

var DelayLoad = {
	target: "delay",
	error: "error",
	retry: 1,
	init: function(params)
	{
		if ( params && params.hasOwnProperty("target") ) {
			this.target = params.target;
		}
		if ( params && params.hasOwnProperty("retry") ) {
			this.retry = params.retry;
		}
		if ( params && params.hasOwnProperty("error") ) {
			this.error = params.error;
		}
		
		// 対象のjQueryオブジェクトにパラメータを設定
		var jqTarget = jQuery( "*[class^='" + this.target + "']" );
		for ( i=0; i<jqTarget.length; ++i ) {
			jQuery.data( jqTarget.get(i), "retry", this.retry );
			jQuery.data( jqTarget.get(i), "error", this.error );
		}
		jqTarget.each(function(){
			// 引数をパース
			var paramsString = this.className.split(";")[1];
			var arrParams = paramsString.match(/^params=\{(.*?)\}$/)[1].match(/([a-z]+):('(.*?)'|[0-9]+)/g);
			var objParams = {};
			for ( i=0; i<arrParams.length; ++i ) {
				var tmp = arrParams[i].split(":");
				if ( tmp[1].match(/^'(.*?)'$/) ) {
					objParams[ tmp[0] ] = RegExp.$1;
				} else {
					objParams[ tmp[0] ] = tmp[1];
				}
			}
			if ( !objParams.hasOwnProperty("url") ) {
				throw "DelayLoad: url required";
			}
			if ( !objParams.hasOwnProperty("title") ) {
				objParams.title = "";
			}
			if ( !objParams.hasOwnProperty("retry") ) {
				objParams.retry = jQuery.data( this, "retry" );
			}
			objParams.error = jQuery.data( this, "error" );
			
			var jqImg = jQuery("<img />");
			// エラー時のイベントハンドラ
			jqImg.bind("error", function(e){
				// 再試行回数だけリロード
				var retry = jQuery.data( this, "retry" );
				if ( --retry > 0 ) {
					jQuery.data( this, "retry", retry );
					this.src = this.src;
				} else {
					// 試行回数切れ
					var target = jQuery.data( this, "target" );
					var error = jQuery.data( this, "error" );
					target.replaceWith(error);
					// 後始末
					jQuery.removeData( this, "retry" );
					jQuery.removeData( this, "error" );
					jQuery.removeData( this, "target" );
				}
			});
			// ロード時のイベントハンドラ
			jqImg.bind("load", function(e){
				var target = jQuery.data( this, "target" );
				target.replaceWith( jQuery(this) );
				// 後始末
				jQuery.removeData( this, "retry" );
				jQuery.removeData( this, "error" );
				jQuery.removeData( this, "target" );
			});
			// 再試行回数を設定
			jQuery.data( jqImg.get(0), "retry", objParams.retry );
			// エラーHTMLを設定
			jQuery.data( jqImg.get(0), "error", objParams.error );
			// 画像置換対象
			jQuery.data( jqImg.get(0), "target", jQuery(this) );
			// 読み込み
			jqImg.attr("src", objParams.url);
			if ( objParams.hasOwnProperty("width") ) {
				jqImg.attr("width", objParams.width);
			}
			if ( objParams.hasOwnProperty("height") ) {
				jqImg.attr("height", objParams.height);
			}
			if ( objParams.hasOwnProperty("title") ) {
				jqImg.attr("alt", objParams.title);
			}
		});
	}
};

DelayLoad デモ

現時点では、以下のブラウザで動作を確認。

  • [Windows XP] IE 8.0
  • [Windows XP] Firefox 13.0.1
  • [Windows XP] Opera 11.64
  • [Windows 7] Opera 23.0

クロスブラウザ対応がとりあえずできたということで、今回をもって「DelayLoad ver 1.0」としますか。
前回書いたものはβ版ってことで。

デモファイルは http://riina-k.net/delayload/1.0/ 以下にあるので、欲しい人はソース漁ってください 要望があればzipにまとめてDLできるようにしますかねー。

遅延ロードをライブラリ化してみた

先ほどのエントリー「画像ロード時のエラー制御で試行錯誤」で組んだjavascript、エラー制御というより「読み込み中」的な意味で使い回しができそうなので汎用性を持たせてライブラリ化。

delayload.js

DelayLoad = {
	class: "delay",
	error: "error",
	retry: 1,
	init: function(params)
	{
		if ( params && params.hasOwnProperty("class") ) {
			this.class = params.class;
		}
		if ( params && params.hasOwnProperty("retry") ) {
			this.retry = params.retry;
		}
		if ( params && params.hasOwnProperty("error") ) {
			this.error = params.error;
		}
		
		// 対象のjQueryオブジェクトにパラメータを設定
		var jqTarget = jQuery( "*[class^='" + this.class + "']" );
		for ( i=0; i<jqTarget.length; ++i ) {
			jQuery.data( jqTarget.get(i), "retry", this.retry );
			jQuery.data( jqTarget.get(i), "error", this.error );
		}
		jqTarget.each(function(){
			// 引数をパース
			var paramsString = this.className.split(";")[1];
			var arrParams = paramsString.match(/^params=\{(.*?)\}$/)[1].match(/([a-z]+):('(.*?)'|[0-9]+)/g);
			var objParams = {};
			for ( i=0; i<arrParams.length; ++i ) {
				var tmp = arrParams[i].split(":");
				if ( tmp[1].match(/^'(.*?)'$/) ) {
					objParams[ tmp[0] ] = RegExp.$1;
				} else {
					objParams[ tmp[0] ] = tmp[1];
				}
			}
			if ( !objParams.hasOwnProperty("url") ) {
				throw "DelayLoad: url required";
			}
			if ( !objParams.hasOwnProperty("title") ) {
				objParams.title = "";
			}
			if ( !objParams.hasOwnProperty("retry") ) {
				objParams.retry = jQuery.data( this, "retry" );
			}
			objParams.error = jQuery.data( this, "error" );
			
			var jqImg = jQuery("<img />");
			// エラー時のイベントハンドラ
			jqImg.bind("error", function(e){
				// 再試行回数だけリロード
				var retry = jQuery.data( this, "retry" );
				if ( --retry > 0 ) {
					jQuery.data( this, "retry", retry );
					this.src = this.src;
				} else {
					// 試行回数切れ
					var target = jQuery.data( this, "target" );
					var error = jQuery.data( this, "error" );
					target.replaceWith(error);
					// 後始末
					jQuery.removeData( this, "retry" );
					jQuery.removeData( this, "error" );
					jQuery.removeData( this, "target" );
				}
			});
			// ロード時のイベントハンドラ
			jqImg.bind("load", function(e){
				var target = jQuery.data( this, "target" );
				target.replaceWith( jQuery(this) );
				// 後始末
				jQuery.removeData( this, "retry" );
				jQuery.removeData( this, "error" );
				jQuery.removeData( this, "target" );
			});
			// 再試行回数を設定
			jQuery.data( jqImg.get(0), "retry", objParams.retry );
			// エラーHTMLを設定
			jQuery.data( jqImg.get(0), "error", objParams.error );
			// 画像置換対象
			jQuery.data( jqImg.get(0), "target", jQuery(this) );
			// 読み込み
			if ( objParams.hasOwnProperty("width") ) {
				jqImg.attr("width", objParams.width);
			}
			if ( objParams.hasOwnProperty("height") ) {
				jqImg.attr("height", objParams.height);
			}
			if ( objParams.hasOwnProperty("title") ) {
				jqImg.attr("alt", objParams.title);
			}
			jqImg.attr("src", objParams.url);
		});
	}
};

使い方はこんな感じ。

<script type="text/javascript" src="js/jquery.js"></script>
<script type="text/javascript" src="js/delayload.js"></script>
<script type="text/javascript">
	jQuery(function(){
		DelayLoad.init({
			error: "<strong style='color:red;'>読み込みに失敗しました</strong>"
		});
	});
</script>
<body>
	<strong>画像直リン</strong>
	<img class="delay;params={url:'images/ongakuji.jpg',width:480,height:270,title:'音楽寺'}" src="images/loading.gif" width="16" height="16" />
	<strong>意図的に2秒待たせるPHP</strong>
	<img class="delay;params={url:'wait2seconds.php',width:480,height:270,title:'音楽寺(2秒遅れ)'}" src="images/loading.gif" width="16" height="16" />
	<strong>存在しないリンクを3回再試行</strong>
	<img class="delay;params={url:'no_longer_exists',width:480,height:270,title:'存在しないURL',retry:3}" src="images/loading.gif" width="16" height="16" />
</body>

実行結果はこんな感じ。DelayLoad デモ

軽く解説。
あらかじめjQueryがロードされていることが条件。
DOMのロードが終わった状態で、DelayLoad.init()関数を実行する。
このとき引数として置換対象クラスclass、エラー時に表示されるHTMLerror、再試行回数retryをパラメータとするオブジェクトを渡すことができる。
値を省略した場合、対象クラスは「delay」、エラーメッセージは「error」、再試行回数は1回。

DelayLoad.init({class:"mydelay",retry:3});

HTMLでは、置換パラメータをクラス指定の中に記述する。
最終的にはクラス指定した要素が画像で置き換わるので、クラス指定をするのはimgタグである必要はない。
パラメータの指定方法は 「init関数で指定したクラス名;params={……}」、前記のとおり、クラス名のデフォルトは「delay」。
paramsにはいくつかのパラメータを書くことができる。
url : 必須、読み込む画像のURL。
width : 画像を表示する横幅、省略した場合はimgタグでwidthを書かなかったときと同じ動作。
height : 画像を表示する縦幅、省略した場合はimgタグでheightを書かなかったときと同じ動作。
title : 画像のaltに設定される画像のタイトル。省略した場合はaltが出力されない。
retry : エラー時の再試行回数。init関数での指定よりも優先される。省略した場合はinit関数の設定またはデフォルト値。

<img class="delay;params={url:'images/ongakuji.jpg',width:480,height:270,title:'音楽寺'}" src="material/loading.gif" width="16" height="16" />

現時点ではIEには対応してません。
要望があればバージョンアップしたいなー。

画像ロード時のエラー制御で試行錯誤

開発中のCDデータベースを試しにこのサーバにアップロードしたところ、
開発環境であるローカルホストでは発生しなかった「画像へのリクエストで500エラー」という状況が発生。

実はCDのジャケット画像をサーバ内部の見えないところでキャッシュしておいて、パラメータで出力する画像を選択、キャッシュが存在すればそのまま出力、存在しなければ404を返すというPHPを組んでいた。

404エラーと500エラーの双方に対応するため、まずAjaxで500エラーを捕捉するよう、次のように書いてみた。

PHP

まずはAjaxでジャケット画像の縦横サイズを取得できるよう、独自ヘッダでサイズを出力。

header( 'Content-type: ' . $this->mime );
header( 'Content-Length: ' . $this->bytes );
// 独自ヘッダ
header( 'X-Width: ' . $this->width );
header( 'X-Height: ' . $this->height );

HTML

ジャケット画像が正常に読み込まれるまではローディング中を示す例のくるくる回るやつを表示しておいて、alt属性に読み込みたいURLを書いておく。
※ {{ …… }} はSmartyのタグ、$pkgはパッケージ情報のインスタンス、$imgは画像情報のインスタンス。

<tr id="{{$pkg->htmlid|escape}}">
	<td class="jacket">
		<img src="material/loading.gif" width="16" height="16" alt="{{$img->pathWeb|escape}}" title="{{$title_artist}}" />
	</td>
</tr>

javascript

コールバック関数内で、altに記述されたURLを読み込み、ステータスが200になったらsrcにジャケット画像のURLを設定。

function loadJacketImage(jqImage, count)
{
	$.ajax(
		{
			jq: jqImage,	// 対象となるimgタグのjQueryオブジェクト
			count: count,	// 再試行回数
			url: jqImage.attr("alt"),
			success: function(data, status, xhr){
				var width = xhr.getResponseHeader("X-Width");
				var height = xhr.getResponseHeader("X-Height");
				this.jq.attr("src", this.url).attr("alt", "").attr("width", width).attr("height", height);
			},
			error: function(xhr, status, error){
				if ( xhr.status == 404 ) {
					this.jq.replaceWith("no image");
				} else {
					if ( this.count > 0 ) {
						// 再試行
						loadJacketImage(this.jq, this.count-1);
					} else {
						this.jq.replaceWith("error");
					}
				}
			}
		}
	);
}

$(function(){
	$(".jacket img").each(function(){
		loadJacketImage( $(this), 3 );
	});
});

しかしこの方法には大きな罠があり、「Ajaxで取得できた画像のURLをsrcに設定する」タイミングでもう一度リクエストが飛んでしまい、そのときに500エラーとなってしまうという状況が時々発生してしまう。
キャッシュしてくれないんかい……

そこでイベントハンドラを利用した以下のような方法を試してみた。

HTML

<tr id="{{$pkg->htmlid|escape}}">
	<td class="jacket">
		<img src="{{$img->pathWeb|escape}}" width="{{$img->width|escape}}" height="{{$img->height|escape}}" alt="{{$title_artist}}" />
	</td>
</tr>

javascript

img要素のイベントハンドラで、エラー時に単純にリロードさせるためにsrcを上書き。

$(function(){
	$(".jacket img").bind("error", function(e){
		// リロード
		this.src = this.src;
	});
});

今度はイベントハンドラで404と500の区別が付かない。
そもそもPHPのほうで404が返ってくるようなURLは最初から書かないように変更し、500エラーが返ってきてもリロードが働いて正常に表示されるようになった。
ただ、PHPが何らかの影響でエラーを吐き続けた場合、リロードが無限ループしてしまうので、Ajaxの時と同様に再試行回数を設定できるようにしたい。

あとやっぱり例のくるくる欲しいよね。
ってことでいじくり回して、最終的にこんな感じ。

HTML

ジャケット画像が存在しない場合はあらかじめ「no image」を表示、存在する場合は例のくるくるに対してalt指定でジャケット画像のURL、横幅、高さをカンマ区切りで設定。

<tr id="{{$pkg->htmlid|escape}}">
	<td class="jacket">{{strip}}
		{{if $img->exists }}
			<img src="material/loading.gif" width="16" height="16" alt="{{$img->pathWeb|escape}},{{$img->width|escape}},{{$img->height|escape}}" title="{{$title_artist}}" />
		{{else}}
			no image
		{{/if}}
	{{/strip}}</td>
</tr>

javascript

成功時に例のくるくるをジャケット画像で置き換え、エラー時には指定回数だけリロードを試み、一定回数(このサンプルの場合は3回)を超えると例のくるくるを「error」という文字列で置き換え。
コールバック関数内で置換対象のくるくると試行回数を見失ってしまうと意味が無いので、jQueryの内部APIで要素に関連付けておく。コールバック関数に引数を渡す方法がめっちゃめんどくさいの

$(function(){
	$(".jacket img").each(function(){
		var tmp = this.alt.split(",");
		var jqImg = $("<img />");
		
		// エラー時のイベントハンドラ
		jqImg.bind("error", function(e){
			// 再試行回数だけリロード
			var tryCount = jQuery.data( this, "tryCount" );
			if ( --tryCount > 0 ) {
				jQuery.data( this, "tryCount", tryCount );
				this.src = this.src;
			} else {
				// 試行回数切れ
				var id = jQuery.data( this, "target" );
				$("#" + id + " img").replaceWith("error");
			}
		});
		// ロード時のイベントハンドラ
		jqImg.bind("load", function(e){
			var id = jQuery.data( this, "target" );
			$("#" + id + " img").replaceWith( $(this) );
			// 後始末
			jQuery.removeData( this, "tryCount" );
			jQuery.removeData( this, "target" );
		});
		
		jQuery.data( jqImg.get(0), "tryCount", 3 );								// 再試行回数を設定
		jQuery.data( jqImg.get(0), "target", $(this).parents("tr").get(0).id );	// 画像置換対象のID
		jqImg.attr("src", tmp[0]).attr("width", tmp[1]).attr("height", tmp[2]).attr("alt", $(this).attr("title"));
	});
});

ここまでやってようやく気が済んだのでした。
そもそもPHPが500を返すほどサーバに負荷をかけてる事実をなんとかするべきじゃ?

return top