document.querySelector

Reactコンポーネントをページに適用する際に、たったこれだけのためにjQueryを入れたくもなかったので、document.getElementByClassNameや〜Idでエレメント取得していましたが、単純名だけかい!という課題に私が知らなかっただけですでに高度な回答がありました。document.querySelector(CSSセレクター式)。

そもそもにCSSセレクターを叩くメソッドがあったのね。ブラウザ適用状況も、大体モダンなやつは行けてます。ただ残念なのはドキュメントの注釈に、getElementsByClassNameや〜Idの方が早いよーと明言されていること。結局は一周して元に戻る。

PassportによるFacebook認証(続いてDBへ保存)

Passportでの認証を実践的に進めてみます。認証情報を永続化するのにMongo DBを利用します。はじめだけMongo DBのJSドライバをそのまま使って書いてましたが、コードブロックをtry-finallyで囲って最後にDB接続を解放するような書き方など2000年ぐらいにJava出始めでJDBCで接続ガーと言ってる頃を思い出しました。懐かしいのは良いことばかりでもなく、何事もイージーにライトでやりたい現代のスピード感にそぐわないのですぐにMongoose通じてアクセスするように。Mongooseを選んだのは単に軽くググったらたくさん記事が出てきたのでメジャー感あったためです。アーキテクチャはいわゆるActive Record的なアレですね。機能は十分に豊富ですが、とは言えシンプルです。

var Mongoose = require('mongoose');
Mongoose.connect('mongodb://localhost:27017/buzzw');
var Account = Mongoose.model('accounts', new Mongoose.Schema({
    provider: String,
    id: String,
    displayName: String,
    accessToken: String,
    lastLogin: Date
}));

var Passport = require('passport');
var Facebook = require('passport-facebook');
Passport.use('fb', new Facebook.Strategy({
        clientID: '1513567018936647',
        clientSecret: '2596-xxxxxxxxxsecretxxxxxxx-d126',
        callbackURL: "http://localhost:8080/auth/facebook/callback"
    },
    function(accessToken, refreshToken, profile, done) {
        Account.findOne({id: profile.id}, function(err, account) {
            if(err) {
                return done(err);
            }
            if(!account) {
                // Active Record的なオブジェクトのコンストラクタにドメインオブジェクトを渡す
                account = new Account(profile);
            }
            account.accessToken = accessToken;
            account.lastLogin = new Date();
            account.save(function(err) {
                return done(err, account);
            });
        });
    }
));

Passport.serializeUser(function(user, done) {
    done(null, user._id);
});

Passport.deserializeUser(function(obj, done) {
    Account.findOne({_id: obj}, function(err, account) {
        done(err, account);
    });
});

var Express = require('express');
var app = Express();
app.disable('x-powered-by');
app.use(Passport.initialize());
app.use(Passport.session());
app.get('/auth/facebook', Passport.authenticate('fb'));
app.get('/auth/facebook/callback', Passport.authenticate('fb',
    {failureRedirect: '/index.html', successRedirect: '/success.html'}));
app.use(Express.static('public'));
app.listen(8080);

Active Record的なオブジェクトを作るのにSchemaを引数に渡しますが、こちらはnew付きの呼び出ししか対応していなかったので注意。最近の多くはnewをつけてもつけなくても同じ動きをするように一手間費やされているライブラリが多いのですが、ここにはそれがなく。 新規時にこのオブジェクトを作る時、つまりレコードをinsertする時にはFacebookから得られたドメインオブジェクトたるユーザー情報(profile)を渡してあげると、そのまま綺麗にDBへ格納してくれます。insertもupdateもコード的には区別なくいい感じ。

Mongooseでちょっと気持ち悪かったのは、connectで返すのがDBインスタンスではなくSocketで、インスタンスはMongoose.connectionというプロパティに入ってるってことかな。例外処理はDomain使った風に以下のように宣言します。

var db = Mongoose.connection;
db.on('error', function(err) {
    console.error(err);
});

APIユースケースとしては、connect呼び出し時にインスタンス返せばいいのにね。

x-powered-byを消す

悪意ある攻撃への対策として、Responseのヘッダから'x-powered-by'を削ると良いという。

Expressでは何もしなければ、サーバ実装を示すこのヘッダ値に'Express'と正直に返してしまいます。そうなると悪い人が「そうかNodeでExpress使ってるのね」とすぐわかってしまうので攻撃前の一手間が省かれてしまうという。いや、しかし、Node+Expressは一定以上使われてるメジャーなプラットフォームだと思うので悪意ある人の前では無駄な努力だとは思うけど...とりあえずおまじないとしてやっておく方がより良いということでしょう。

var app = express();
app.disable('x-powered-by');

これだけ。

そういえば、Node5.1.0にアップデートされていました。$sudo n stableではまだ対応されてなかったので$sudo n 5.1.0で上げました。npmは3.5.0です。

PassportによるFacebook認証(第一歩)

var Passport = require('passport');
var Facebook = require('passport-facebook');
// Facebook Strategyの設定を'fb'で登録。省略すると'facebook'で設定される
Passport.use('fb', new Facebook.Strategy({
        clientID: '1513567018936647',
        clientSecret: '2596-xxxxxxxxxsecretxxxxxxx-d126',
        callbackURL: "http://localhost:8080/auth/facebook/callback"
    },
    function(accessToken, refreshToken, profile, done) {
        // 2)Facebook認証のVerifyイベント
        console.log('Passport.verify');
        return done(null, profile);
    }
));
Passport.serializeUser(function(user, done) {
    // 3)セッションへ認証ユーザーを保存
    console.log('Passport.serializeUser');
    done(null, user);
});
Passport.deserializeUser(function(obj, done) {
    console.log('Passport.deserializeUser');
    done(null, obj);
});

var Express = require('express');
var app = Express();
app.use(Passport.initialize());
app.use(Passport.session());
// 1)認証始め
app.get('/auth/facebook', Passport.authenticate('fb'));
// 4)コールバック
app.get('/auth/facebook/callback',
    Passport.authenticate('fb', { failureRedirect: '/index.html'}),
    function(req, res) {
        console.log('success login');
        res.redirect('/success.html');
    }
);
app.use(Express.static('public'));
app.listen(8080);

Passportを利用してFacebookで認証するのに、ハマリどころは満載ですがコード自体はシンプルに書けます。シンプルにそぎ落としてみて最低限必要なことは以上の通りになりました。一見いらなそうなものも削るとエラー出ます。基本構造としては、1)'/auth/facebook'へのルーティングに仕込まれている、passport.authenticateミドルウェアで認証を始めます。引数に'fb'を渡すことで先に設定したFacebookStrategyを利用することを宣言しています。そしてFacebookに移動してそちらのログイン処理を行う最中に2)Strategyが持つVerifyイベントハンドラがコールされてAccess Tokenが手に入ります。その後に3)Strategyに設定するハンドラで認証ユーザーをセッションへ保存します。

ここまでの処理が成功していると、コールバックが戻ってきます。3)こちらも同じpassport.authenticateミドルウェアで、成功失敗の別に応じてリダイレクト。この後に取得したAccess Tokenを利用してFacebookAPIをコツコツ呼び出すことになります。

設定としてpassportのinitializeミドルウェアとsessionミドルウェアルーター登録するのが必須ですし、認証ユーザーのシリアライズがないとエラーを出しました。

f:id:masataka_k:20151117084100p:plain

Facebook側では、コールバックURLを登録する必要があります。localhost:8080をサンプルでは用いてるので、それはそれとしてFacebookアプリ設定ページにそのまま書いておかないと、指定コールバックURLが既知では無いということですぐに認証エラーを出す、いわば正しい動きをします。

オレオレHTTPSサーバを立てる

実際には証明書を買うとしても、開発時にはオレオレ証明書

$ openssl req -nodes -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 90
Generating a 2048 bit RSA private key
...........................+++
.........................+++
writing new private key to 'key.pem'
-----
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:
State or Province Name (full name) [Some-State]:
Locality Name (eg, city) []:
Organization Name (eg, company) [Internet Widgits Pty Ltd]:
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:
Email Address []:
$ ls
cert.pem    key.pem

opensslを使って証明書を作ってますが、パラメータの「-nodes」が必要です。これを付けないとhttpsを上げた時に「error:0906A068:PEM routines:PEM_do_header:bad password」とエラーが出ちゃう。最後の90は有効期間で、伸ばしたければ365とか。途中のopensslとの対話ではパスフレーズを4文字以上で適当に入力する以外、リターンキーで省略OK。

var fs = require('fs');
var https = require('https');
var app = express();
var privateKey  = fs.readFileSync('key.pem');
var certificate = fs.readFileSync('cert.pem');
app.use(express.static('public'));
https.createServer({key: privateKey, cert: certificate}, app).listen(443 function() {
    process.setuid('masataka_k');
});

平民で実行するとデカいポート番号(8443とか)にしなければ「listen EACCES 0.0.0.0:443」というようにエラーで上がらない。ルートで実行。

$ sudo node server.js

ルートで上げっぱだとセキュリティ上ナニなので、process.setuid()をするってNodeクックブックに書いてあった。が、これでいいのかな?psするとこんな状態。最後によくわかってなくておまじない状態。本番あげるまでには識者を見つけねば。

$ ps -ax | grep node
 4020 ttys000    0:00.01 sudo node server.js
 4021 ttys000    0:00.33 node server.js
 4037 ttys001    0:00.00 grep node

Nodeクックブック

Nodeクックブック

SuperAgentとMulterでファイルアップロード

ファイルアップロードの機能を作るのに、二日ハマりました。前提としてReactで作ったFormから投げ、Express4で書いたサーバで受けるというところでの通信部分だけです。まずガチなところとしてサーバを作ります。

var express = require('express');
var multer  = require('multer');
var server = express();
var upload = multer({ dest: "./uploads/" });
server.post("/confirm", upload.single("contact_attachment"), function(req, res) {
    console.log(req.headers);
    console.log(req.params);
    console.log(req.body);
    console.log(req.file);
    res.sendStatus(200);
});
server.use(express.static('public'));
server.listen(process.env.PORT || 8080);

multerはマルチパートのリクエストボディを処理してくれるExpressのMiddlewareで、npmでサクッとインストール。いろんな書き方がありますが、uploadというインスタンスを作っておいてExpressのルーターチェーンに差し込みます。メソッド引数に並べて渡すだけでチェーンできるのは発見だった。違う書き方としては、以下もあります。

server.use("/confirm", upload.single("contact_attachment"));
server.post("/confirm", function(req, res) {
    console.log(req.body);
    res.sendStatus(200);
});

ルーティングの書き方は同じことでもいろんな書き方できるから、ググってる時に混乱多い。みんなマニアックなテクニック使いまくってること多くて慣れるまでルーティングがどうなってるかなかなか掴めないじゃないか!

一方で、ドハマりしたクライアント。迷走の先に気がついたのは、簡単なものとしてプレーンHTMLさえ書けばOKなんでまずそれ作って、後でもっと難しいことをやる時に、それぞれヘッダやパラメータやボディなどに何を書かれているかを見比べることでした。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Test</title>
</head>
<body>
    <form method="post" enctype="multipart/form-data" action="/confirm">
        <div><input type="text" name="contact_name"></div>
        <div><input type="file" name="contact_attachment"></div>
        <div><button type="submit">UPLOAD</button></div>
    </form>
</body>
</html>

タグだけで書いたこれが動かないとしょうがない。だから一番初めにサーバがちゃんと動くことを確認すべきだった(私はやらなかったためサーバもクライアントも変更し続けて迷走が長引いた)。multerもたった数行のコードなのに、一発では動くものを書けなかったからねー。

クライアントはReactの中から、初めはjQueryの$.ajax()でやってたけど、どうにも動かなかった。普通にフォームPOSTするだけならすぐだったけど、ファイル添付されたmultipartができなかった。そんな中、ReactではDOMを直に触るの邪道っぽいこともあって通信にしか使わないので、ダメ元で通信専門のライブラリを持ってきました。SuperAgentです。

github.com

まあ、やることは$.ajax()と大差ない。

var React = require("react");
var SuperAgent = require("superagent");
var ContactForm = React.createClass({
    handleSubmit: function(event) {
        event.preventDefault();
        this.setState({
            valid_button: false
        });
  // var formData = new FormData() <==FormDataを使って迷走
        SuperAgent
            .post("/confirm")
            //.type("multipart/form-data") <==これをやるとboundaryが設定されなくなって迷走
            //.set({contact_name: this.state.contact_name}) <==set使って迷走
            .field("contact_name", this.state.contact_name)
            .field("contact_kana", this.state.contact_kana)
            .field("contact_email", this.state.contact_email)
            .field("contact_company", this.state.contact_company)
            .field("contact_message", this.state.contact_message)
            .attach("contact_attachment", this.state.contact_attachment)
            .end(function(err, res) {
                if(res.statusCode == 200) {
                    this.setState({
                        text_guidance: GUIDANCE_3,
                        style_form: {"display": "none"}
                    });
                } else {
                    this.setState({
                        valid_button: true,
                        text_guidance: GUIDANCE_4
                    });
                }
            }.bind(this));
        return false;
    },
   //以下、renderとか色々ごっそり省略
});

頭で"superagent"をrequireしてから、あとはfieldとattachでコツコツ積み上げます。multipartの場合は他のことをしちゃダメだった。より一般的なsetメソッドでパラメータを積んだりすると自動的にcontent-typeがjsonにされちゃう。また、typeで明示的にcontent-typeをmultipart/form-dataに設定すると続いてほしいboundaryのぐちゃぐちゃ文字列が設定されなくなるので死亡。

ライブラリのソース読んで理屈と癖がわかって、動くようになってみるとSuperAgentとMulterはなかなか良い。捗る。

ReactでinputのonChangeが効かなくて焦った話など

Reactでちょこちょこ書いていて、嵌ったところをメモ。ちなみにECMAScript6は、ExpressもReactもあまりメリットない割に、参考資料が少なくてどう書いていいのか悩む時もあるのでやめた。Babelでreact-presetだけ使ってます。

inputのonChangeが効かなくなった

var App = React.createClass({
    // getInitialStateとか、handleChangeとか、省略
    render: function() {
        return (
            <form>
                <input type="text" value={this.state.data} onChange={this.handleChange}/>
            </form>
        );
    }
});
ReactDom.render(<App/>, $("form")[0]);

上記のようなコードだったとします。コンポーネントがformタグを返すのに、ReactDom.renderの第二引数にformタグを入れてた。第二引数のDOM要素の内側をごっそり第一引数のコンポーネントで置き換える動作だったので、出来上がったHTMLはformタグが二重になってる状態です。こうなると、getInitialStateで設定した初期値は何事もなく描画されるけど、onChangeだけ効かないって不思議状態になる。HTMLの方をいじらないで済まそうとせずに、formを囲うdivを加えて修正しました。

大文字のHTMLタグはダメ

var App = React.createClass({
    render: function() {
        return (<P>ホゲホゲ</P>);
    }
});
ReactDom.render(<App/>, $("#app")[0]);

Pが大文字でクラッシュ。小文字に直せばOK。

タグにインラインのStyle指定

facebook.github.io

ドキュメントにちゃんと書いてありました。