LDRがなくなるとのことがなくなったけど俺用フィードリーダー作った
mizchiさん(LDRがなくなるとのことで俺用フィードリーダー作った - mizchi's blog)のパクリです。はい。
実際LDRがなくなると、いずれは他のフィードリーダーも終わっていくんじゃないの? という不安がある。
なので、ローカルで動くものを持っておくのも仕方がないことなのかもしれないとモヤモヤしていたところで、mizchiさんのオレオレフィードリーダーが出てきた。
ごちゃごちゃ悩んでないでとりあえず作るべき。
パクる
LDRに準拠したキーバインドとかマウス操作に対応させないとか、僕にとって重要でパクれるところはパクった。
既読管理の方法は違っていて、インメモリではなくleveldbにアウトソースしてる(下記参照)
ブラウザ上では未読/既読の管理はしてない
localStorageではLDRでいうところのピンのような機能を持たせることに。npmライブラリの localstorage-down 使ってみた。
localstorage-down は ユーザが触れるapiのところがleveldbのapiと同じlevelupを使うのでバックエンドとフロントエンドで同じapiでいい感じ。
leveldbはポータブルに使えるので今回の要件にはマッチしてたので使ってみた。
実装の方針
opmlファイルの読み込みからフィードエントリーをクライアントへ届けるまでStreamのパイプで直列化できるんじゃないの? という発想から後先考えずに mkdir -p feedr/{lib,node_modules,t} したので、ストリームインターフェイスのモジュールを書く。機能毎に小分けにしたストリームを書く。
Streamについて
わりと手間がかかる
でもStream1より実装者がコードを書く量が少ないのはいい
.onpipe時にwritableに例外が投げられると.unpipeされるので例外処理重要!
Tatsumaki::HTTPClient で Pixivにログインする
メモ的に書きます。ので、やっつけです。
AnyEvent::HTTP で pixivにログインしようとする際に recurse を指定しておかないと、/login.php にデータをポストした後、自動的に Loation ヘッダが示すURLに移動しようとします。
が、Content-Lenghtなど を保持したまま移動しようしているようで、そのままでは上手く移動できません。なので、recurse => 0 指定して、レスポンスが帰って来たら Locationが示すURLに移動するなどします。
が、Tatsumaki::HTTPClient では recurse 指定する方法がありません。なので Tatsumaki::HTTPClient を拡張して使えるようにしてみます。
package Tatsumaki::HTTPClient::Custom; use Tatsumaki; use AnyEvent::HTTP; use Moo; use MooX::late; extends qw(Tatsumaki::HTTPClient); has jar => (is => 'rw', isa => 'HashRef', default => sub { +{version => 1} }); has recurse => (is => 'rw', isa => 'Num', default => $AnyEvent::HTTP::MAX_RECURSE); has agent => (is => 'rw', isa => 'Str', default => sub { join '/', __PACKAGE__, $Tatsumaki::VERSION }); sub request { my($self, $request, $cb) = @_; my $jar = $self->jar; # add my $headers = $request->headers; $headers->{'user-agent'} = $self->agent; ### delete $headers->{'content-length'}; ### my %options = ( timeout => $self->timeout, headers => $headers, body => $request->content, cookie_jar => $jar, # add recurse => $self->recurse, #add ); AnyEvent::HTTP::http_request $request->method, $request->uri, %options, sub { my($body, $header) = @_; my $res = HTTP::Response->new($header->{Status}, $header->{Reason}, [ %$header ], $body); $self->jar($jar); # add $cb->($res); }; } package main; use strict; use warnings; use AE; my $login_php = 'http://www.pixiv.net/login.php'; my %query = (mode => 'login', pixiv_id => 'foo', pass => 'bar'); my $client = Tatsumaki::HTTPClient::Custom->new(recurse => 0); my $cv = AE::cv; $client->post($login_php => \%query, sub { my $response = shift; ### Tatsumaki::HTTPClient::Custom::request で delete $headers->{'content-length'} しておくと ### 以降のリクエストは AnyEvent::HTTP がめんどうみてくれる $client->get($response->header('location'), sub { my $response = shift; # some work ... $cv->send; }); }); $cv->recv;
ただ、これだとたるいので、Tatsumaki::HTTPClient::Custom::request の $headers->{'user-agent'} = $self->agent; の後に delete $headers->{'content-length'} しておけば、recurse => 0 する必要も、再度 get リクエストする必要もないので、たるい時にはそれでいいかもしれないです。
引数にオブジェクト(ハッシュ)を想定していて、想定していない型の値を渡してもエラーが投げられないこともある。
function F (opt) { this.a = opt.a } function test (opt) { try { console.log(new F(opt)) } catch (err) { return console.error(err) } } test() // [TypeError: Cannot read property 'a' of undefined] test(null) // [TypeError: Cannot read property 'a' of null] test('foo') // {a: undefined} test(1) // {a: undefined} test(function () {}) // {a: undefined} test(false) // {a: undefined} test([]) // {a: undefined} test({a: 'abc'}) // {a: 'abc'}
文字列、数値、boolean、関数 でもエラーを投げないので気が置けない
filed使うとスタティックファイルを送るサーバが簡単に準備できるのでいいですね
※ 追記しました (2013.09.01)
WebWorker とか HTML5のFile API のテストをしたいときに使います
https://github.com/ishiduca/node-static-server
$ npm install https://github.com/ishiduca/node-static-server/tarball/master -g
すると `static-server` コマンドができるので
$ ROOT=$PWD/public PORT=3030 static-server
$ static-server --root=$PWD/public --port=3030
とかすると $PWD/public/ 以下のファイルを読み込むサーバが localhost:3030 に立ち上がります。
追記 2013.09.01
時たま XMMLHttpRequest などでリクエストをサーバー側に投げて返ってくるレスポンスを確認したいというような場合があるので、ミドルウェアの形でアプリを載せられるようにしてみました
$ static-server -p 3000 -r $PWD/public -m $PWD/xhr.js
上記の例だと、port が 3000 ルートディレクトリが $PWD/public ミドルウェアが $PWD/xhr.js になります
ミドルウェアはこんな感じで書きます。
xhr.js
module.exports = function () { var url = require('url') return function responseXHR (req, res, next) { if (url.parse(req.url).pathname !== '/xhr') return next() // 次のミドルウェア か sendStaticFile に任せる var data = '' req.on('data', function (chunk) { data += chunk }) req.on('end', function () { var parsed try { parsed = JSON.parse(data) } catch (err) { res.writeHead(500, {'content-type': 'application/json'}) res.end(JSON.stringify({error: 1, message: err.message}) return console.error(err) } var responseData = something( parsed ) res.writeHead(200, {'content-type': 'application/json'}) res.end(JSON.stringify(responseData) }) } }
ミドルウェアは複数指定できますが、指定した順に実行されるので、指定する順番を考えて指定します
new (stream.Transform) してみた
夏コミが終わったので、node.js のバージョンを v0.10.15 にした。
ので、Stream2 を使えるようになりました。
なので、早速(というか今更)Stream2 を触り始めてる
stream.Transform
stream.Readable と stream.Writable は Stream1 からあるので(API変わってるけど)、今回は Transformを使ってみる
まず簡単に
- ReadableStream: 100ミリ秒毎に 10 > 9 ... 0 とカウントダウンしていく
- WritableStream: 標準ストリームにプリントする
- TransformStream: ReadableStream が発行するデータ(カウント)を (10 - カウント)に変換して出力する
を実装する
(transf01.js)
var util = require('util') var stream = require('stream') var rs = stream.Readable() rs.count = 10 rs._read = function () { this.count >= 0 ? setTimeout(this.countdown.bind(this), 100) : this.push(null) } rs.countdown = function () { this.push(String(this.count--)) } function Transf () { stream.Transform.call(this) } util.inherits(Transf, stream.Transform) Transf.prototype._transform = function (chunk, enc, done) { this.push(String((10 - Number(chunk)) + '\n')) done() } process.stdout.on('error', process.exit) process.on('exit', function () { console.error('process.exit') }) //rs.pipe(process.stdout) rs.pipe(new Transf).pipe(process.stdout)
結果
0 1 2 3 4 5 6 7 8 9 10 process.exit
この例だと MyTransform.ptototpe._flush を使ってないので、_flush を使ってみる。
- ReadableStream: http.ServerRequest
- WritableStream: http.ServerResponse
- TransformStream: リクエストヘッダとリクエストデータをパースし、JSON形式に変換するストリーム
(transf02.js)
var util = require('util') var stream = require('stream') function Transf () { stream.Transform.call(this) this.body = '' this.headers = null this.once('pipe', this.oncePipe.bind(this)) } util.inherits(Transf, stream.Transform) Transf.prototype. class="synIdentifier">function (req) { req.setEncoding('utf8') this.headers = req.headers } Transf.prototype._transform = function (chunk, enc, done) { this.body += chunk done() } Transf.prototype._flush = function (done) { // try { this.push(JSON.stringify({ headers: this.headers , body: JSON.parse(this.body) }, null, 4)) } catch (err) { console.error(err) this.emit('error', err) } done() } var http = require('http') http.createServer(function (req, res) { var transf = new Transf req.pipe(transf).pipe(res) transf.on('error', function (err) { res.statusCode = 500 res.end(err.name + ': ' + err.message) }) }).listen(1337)
結果
$ curl localhost:1337 -d '{"foo": "bar", "hoge": {"hello": "world"}}' ... { "headers": { "user-agent": "curl/7.21.2 (x86_64-apple-darwin10.6.0) libcurl/7.21.2 OpenSSL/1.0.0d zlib/1.2.5 libidn/1.20", "host": "localhost:1337", "accept": "*/*", "content-length": "42", "content-type": "application/x-www-form-urlencoded" }, "body": { "foo": "bar", "hoge": { "hello": "world" } } } # 文法違反のリクエスト送ってみる $ curl localhost:1337 -d '["foo": "bar"]' ... SyntaxError: Unexpected token :
"_transform は writable.write の直前にフックする"、"_flush は writable.end の直前にフックする" とイメージしてみたけど、あってるんだろうか?
もうちょっとドキュメント読んでみないとまずい印象
console.logの出力結果のテストってどうやってるんだろう?
実際にはテストしやすいように出力する部分と出力する内容を分離すればいいんだけど、直接出力する場合ってどうやってるんだろうか?
var QUnit = require('path/to/qunit-helper').QUnit; var response = {}; response.log = function log () { console.log( (new Date).toUTCString() , this.method.toUpperCase() , this.pathname , this.statusCode.toString() ); }; QUnit.module('response.log()', { setup: function () { this.test = function (method, pathname, statusCode) { console.log = function () { var arg = arguments; equal(arg[0], (new Date).toUTCString(), arg[0]); equal(arg[1], method.toUpperCase(), arg[1]); equal(arg[2], pathname, arg[2]); equal(arg[3], statusCode.toString(), arg[3]); }; response.method = method; response.pathname = pathname; response.statusCode = statusCode; response.log(); } } }); test('response.log', function () { this.test('get', '/log', 200); });