テンプレートエンジンECT
Node.js+Expressでタスク管理アプリを作った時のメモ。
今回はテンプレートエンジンにECTを使う話。
bbpink/node-shpapad · GitHub
使い方(View)
ECTはレイアウト機構があるので、まず全てのページ用のレイアウトを作りました。
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8" /> <link href="/index.css" rel="stylesheet" /> <title>shpapad</title> <script src="/superagent.js"></script> </head> <body> <div id="wrap"> <% include "header" %> <% include "menu" %> <% content %> <% include "footer" %> </div> <script src="/shpapad.js"></script> </body> </html>
includeで他のテンプレート呼び出し、contentでテンプレート継承です。
レイアウトを継承しているリストページを見てみましょう。
<% extend "layout.ect" %> <section id="content" class="clear"> <ul class="list"> <% for v in @lists : %> <li id="<%- v.key %>" data-name="<%- v.name %>" data-count="<%- v.count %>"></li> <% end %> </ul> </section>
extendに指定したファイルを継承します。for文なんかも簡単でいいですね。
View側の基本的な使い方は特に難しくありませんでした。
使い方(Express)
まずExpressのテンプレートエンジンにECTを指定します。
var express = require('express'); var ECT = require("ect"); var app = express(); //テンプレートのファイルがあるディレクトリ、拡張子を指定してレンダラを生成 var ectRenderer = ECT({ watch: true, root: __dirname + '/views', ext : '.ect'}); app.set('view engine', 'ect'); //ECTを使うよ宣言をして... app.engine("ect", ectRenderer.render); //ブチ込み
表示自体はレスポンスオブジェクトのrenderメソッドを呼ぶだけです。
app.get("/list", function(req, res) { // ... いろいろあって ... var lists = []; res.render("list", { menu: [{href: "/list", value:"リスト"}], logoutable: true, lists: lists }); });
renderメソッドの第1引数には表示するテンプレート名を指定します。
第2引数はオブジェクトを指定、キーはそのままテンプレート側で参照できる変数名になります。
shpapadのリストページではメニュー、ログアウト用リンク、リスト本体を表示するので3つの値を渡しています。
Express側の実装も全然難しくないので導入の敷居は低いように思いました。
以上!!!
VanillaJSでSuperAgent
Node.js+Expressでタスク管理アプリを作った時のメモ。
今回はクライアントサイドJavaScript(Vanilla)でSuperAgentを使う話。
bbpink/node-shpapad · GitHub
VanillaJS+SuperAgentを選択した背景
- 画面の都合上、Ajaxリクエストができるとうれピー
- 単純なアプリだからライブラリを使うまでもない
- JavaScriptできます(jQuery奴)からの卒業
- でもAjaxリクエスト処理を生で書くのはメンドイ
使い方
htmlでSuperAgentと本体のJavaScriptファイルを読む、これはいつもどおり
jQueryでは「$(document).ready(function() {}」があって、ドキュメント読み込み後に実行開始できるんですが
Vanillaやるのは結構メンドイらしいのでbodyの最後に本体のJavaScriptを配置しています。
<head> <script src="/superagent.js"></script> <!-- SuperAgent本体 --> </head> <body> ... <script src="/shpapad.js"></script> <!-- 使うほうのJavaScriptファイル --> </body>
本体のJavaScriptではド頭に下記を定義し、requestという変数にSuperAgentをブチ込みます。
var request = window.superagent;
これだけ!
リクエスト & レスポンス
shpapadでは、クライアントサイドJSからの3種類のリクエストが存在しています。
- POST (リスト・タスクの追加)
- PUT(リスト・タスクの更新)
- DELETE(リスト・タスクの削除)
GETはやってないのですがまあ調べたら簡単にできるんじゃないでしょうか(適当)。
//リストの追加(POST) request.post("/list").type("form").send({value:"リスト名"}).end(function(err, res) { //res.okがtrueの場合は成功 if (res.ok) { //res.bodyに返却値が入ってくる、shpapadではapplication/jsonで値が入っています alert(res.body.id); } else { alert("なんか追加できない、オワタ"); return; } }); //リストの削除(DELETE) deleteButton.onclick = function() { if (confirm("本当に削除してもよろしいですか?")) { request.del(op + "?id=" + "リストのID").end(function(err, res) { //削除後の画面処理 }); } };
PUTはPOSTほとんど同じです。
ASIDE:POSTとPUTの使い分け
POSTリクエストべき等でなく、サーバサイドにリソースを「新規追加」
PUTリクエストはべき等で、サーバサイドのリソースを「更新」
1回のリクエスト毎にサーバサイドの状態が変わる = べき等でない
何度繰り返してもサーバサイドの状態が変わらない = べき等
shpapadではPOSTリクエストをする度にリストorタスクが増えていきますが、
PUTリクエストをしても変更が行われるだけで、リストorタスクの個数は増えていきません。
POSTでもPUTでもログが書かれる等の状態変化があるので両方べき等でないとかの意見もあるらしいんですが、
私はユーザ(リクエスト主)から見てべき等であればOKだと思っています。
(ここで勉強しました)
続く!(かもかも)
Node.jsでLevelDB
Node.js+Expressでタスク管理アプリを作った時のメモ。
今回はデータストアに選択したLevelDBの話。
bbpink/node-shpapad · GitHub
LevelDBを選択した背景
- 単純なタスク管理アプリだから高機能なデータストアが必要なかった
- Node.js Bindingsが存在する
- クソ速そう
使い方
まずはパッケージをインストールします。
npm install levelup npm install leveldown
shpapadではpackage.jsonに書かれています。
leveldownはLevelDBのレイヤーが低い部分で、サーバサイドで使うのであればあったほうが良いみたいです。
DBへはput、get、delでCRUDを行います。
var levelup = require("levelup"); var db = levelup("./data/db/shpapad_db"); //データを保存するディレクトリを指定してコンストラクト var hashedID = "hashed!"; //ハッシュ化されたユーザID //ユーザIDが存在する場合はログイン、存在しない場合はユーザを作成 db.get(hashedID, function(err, value) { if (err) { if (err.notFound) { //create new user db.put(hashedID, "", function(err) { if (err) { res.redirect(303, "/login"); } else { //auth OK req.session.user = userid; res.redirect(303, "/list"); } }); } else { //キー存在なし以外のなんかのエラー res.redirect(303, "/login"); } } else { //auth OK req.session.user = userid; res.redirect(303, "/list"); } });
データ構造
shpapadに必要なデータは「ユーザ」「リスト」「タスク」の3種類であり、
「ユーザ」が頂点に位置する木構造で表現する必要があります。
- ユーザ Aさん
- 明日やること
- 掃除する
- 明日やること
- ユーザ Bさん
- 好きな人
- ドラッグストアで買うものリスト
- トイレットペーパー
LevelDBのデータはキーでソートされているため、キー値を"!"で区切って木構造を表現しました。
「キー:バリュー」で表現した場合、上記のデータはこんな感じになります。
ハッシュ化されたユーザAさんのID:"" ハッシュ化されたユーザAさんのID!リスト作成時UnixDateTime:{name:リスト名(明日やること),リストに紐付くタスク一覧の件数(1)} ハッシュ化されたユーザAさんのID!リスト作成時UnixDateTime!タスク作成時UnixDateTime:タスク名(掃除する)} ハッシュ化されたユーザBさんのID:"" ハッシュ化されたユーザBさんのID!リスト作成時UnixDateTime:{name:リスト名(好きな人),リストに紐付くタスク一覧の件数(3)} ハッシュ化されたユーザBさんのID!リスト作成時UnixDateTime!タスク作成時UnixDateTime:タスク名(篠崎愛)} ハッシュ化されたユーザBさんのID!リスト作成時UnixDateTime!タスク作成時UnixDateTime:タスク名(星野源)} ハッシュ化されたユーザBさんのID!リスト作成時UnixDateTime!タスク作成時UnixDateTime:タスク名(ウーピー・ゴールドバーグ)} ハッシュ化されたユーザBさんのID!リスト作成時UnixDateTime:{name:リスト名(ドラッグストアで買うものリスト),リストに紐付くタスク一覧の件数(1)} ハッシュ化されたユーザBさんのID!リスト作成時UnixDateTime!タスク作成時UnixDateTime:タスク名(トイレットペーパー)}
また、db.get()ではキーを指定して1件ずつしかデータを取得できないため、db.createReadStream()を使用してキー値を範囲指定でがさっと取得します。
リストを取得する
app.get("/list", function(req, res) { if (req.session.user) { var hashedID = crypto.createHash("sha256").update(req.session.user).digest("hex"); //get lists var lists = []; db.createReadStream({gt: hashedID + "!", lte: hashedID + "!9999999999999" }) .on("data", function(data) { var obj = JSON.parse(data["value"]); lists.push({key:data["key"], name:obj["name"], count:obj["count"]}); }) .on("end", function(data) { res.render("list", { menu: [{href: "/list", value:"リスト"}], logoutable: true, lists: lists }); }); } else { res.redirect(303, "/login"); } });
1人のユーザに紐付くリストを取得する場合、キー値の検索条件は"ハッシュ化されたユーザID!" から"ハッシュ化されたユーザID!UnixDateTimeのMAX値" の範囲です。
タスクの場合も階層が1つ深くなるだけでリストの場合と同様の処理となります。
続く!(たぶん)
JavaScriptでGoogleアカウントOpenID Connect認証
Node.js+Expressでタスク管理アプリを作りました。
github: bbpink/node-shpapad · GitHub
作った背景
- もともとshpapadというタスク管理アプリを自作して使用していた
- 2015年7月にGAEのMaster/Slave Datastoreがサービス終了し、アクセスできなくなってしまった(shpapadはこれを使っていた)
- GAE特有のスピンアップ時間によるもっさり感がつらかった
- 規模はデカくないしリソースが空いてるVPSにRailsかExpressで速攻作りなおすか → 気分でNode.js+Express
実装の勘所(1) GoogleアカウントでOpenID Connect認証
まずGoogleでデベロッパ登録をしたら、OpenID Connectをするためだけのプロジェクトを作成します。プロジェクト情報を参照して「OAuthクライアントID」と「クライアントシークレット」の2つを入手すれば実装前の準備は完了。(この記事を見ながらやった気がします)
認証の流れは、
- リダイレクト先URLをパラメタに積んだGoogleドメインへのリンクをユーザがポチッ
- 飛んだ先のGoogleドメイン内で、shpapadから要求されている権限をユーザが許可
- 1で通知したリダイレクト先URL(shpapadドメイン)にリダイレクトされる
です。
リダイレクト先URL
shpapadではログイン用のAタグが以下のように書かれています。
非常に長い...........
ここではGoogle公式ドキュメントを参考にして、5つのGETパラメタを設定しました。
- client_id : 前述したOAuthクライアントID
- response_type : "code"を指定します。(requiredなのになぜかcodeしか指定できない)
- scope : ドキュメントを参照。shpapadではOpenID Connect認証をするだけなので"openid"だけを指定しています。
- redirect_uri : 前述したshpapad側に帰ってくるためのリダイレクト先URL。
- state : CSRF対策用の、shpapadサーバで発行したランダム文字列。
ユーザによる権限の承認
飛んだ先のGoogleドメイン内で、ユーザはアプリから要求されている権限が妥当であるか、そして承認するかどうかを判断します。
shpapadでは認証するだけなので"openid"のみを指定していますが、アプリケーションでメールアドレス等が必要な場合、リクエストパラメタのscopeに要求する権限を追加しておくと良いみたいです。
リダイレクトされたら
リダイレクト先URLにパラメタ付きでリクエストが飛んでくるので、サーバ側で受け取ります。
12行目まではリクエストパラメタに渡したstateの値の返却値チェックをしています。
shpapadではセッションに保存してあった値との照合をしています。
14行目からの流れは、
- リダイレクト先URLへのリクエストパラメタを取り出す
- 取り出したパラメタを使ってGoogleへトークンを要求
- 取得したトークンを使ってユーザ情報を取得
です。
shpapadでは、内部のユーザIDを生成するための元データである、Googleのuser_idを取得しています。
ユーザ情報を取得する際、レスポンスボディが少し長い時があって、19行目からの「response.on("data" .....」を使って連結しておかないとしくじる場合がありました。
続く!
P.S. 見たまま編集だとソースコードかけないのね.....
bbblogというブログシステムを作りました
実物(bbpink近況) http://sevensenses.jp/bbpink/
前々からあったんですがずっとオープンソース化したいなあと思っており、この度公開することができました。
作った動機
bbpink近況というブログは、初期の頃はMovableTypeで運用されていました。
MovableTypeはあれはあれで良かったんですが、記事が増えるにつれて全てをhtmlで書きだすオペレーション(名前忘れた)がクソ遅くなり、ちょっとしたスタイリングの変更でも全コンパイルが必要になって何時間待っても終わらなくなってしまいました。
こんなんやってられるかアホー!ということでWordPressを検討しはじめましたが、
(今は知りませんが)本当に最初の頃のWordPressはDBにMySQLを使う必要があったのです!
自宅サーバ(ノートPC:メモリ256MB)で運用されていたbbpink近況。ブログ一個のためにMySQL分のメモリを確保するのがバカらしいと判断した私はいっそ自分で軽いの作ればいいじゃんと思って開発を始めました.....
テクノロジ
- DBはSQLite
- テンプレートエンジンはTwig
- スタイリングはscssを使用、コンパイル結果オートキャッシング
- フルスクラッチオレオレフレームワーク(MVCアーキテクチャ、セキュリティロジック、フィルタリングなどなど)
俺ームワークを作って勉強になったこと
- いろんなフレームワークのソースを読んだ(ActiveRecordやCake、Javaサーブレットとかすごい参考になった)
- 安全なセッション管理とパスワード周り(ソルト、現代的でセキュアなハッシュ関数、ストレッチング)
- HTTPステータスコードを正しい意味で使用すること
- RSS/Atom標準準拠のXML作成
- リクエストパラメタのチェックの正しいやりかた
オレオレフレームワーク(笑)みたいな風潮があったりしますが絶対一回作ってみたほうが良い、マジで勉強になりました
bbblogの良いところ
正直言って自分以外の誰かが使うとは思っていませんが、念のため良いところを書くことにします。
- フォントやレイアウトにこだわっており見た目が良い!
- そこらへんにいる美女より美しいHTML(html5)、ぜひbbpink近況のソースコードを見てください
- 記事書くのも記事見るのもメチャクチャに軽い
- インストールに7操作しか必要なく異常に簡単
- 修正BSDライセンスに従えば何をしても自由
感想
社会人1年目の時に作ってたブツをリファクタリングしてgithubに置いたので懐かしいコードがあったりして感慨深いです。
今はもうほとんどPHPは使っていませんが新しい機能とかは追っていきたいですね。
JIRAを使い始めました
なぜJIRA?
入手
JIRAはサービスとして提供される形式と、自前のサーバにインストールする形式があります。
今回はVPS(ScientificLinux)にインストールしました。
お値段は1-10ユーザで$10。円高なので780円くらいでした。安い!
インストール
- DBMSが必要なのでMySQL(remiの5.5)をインストールし、適当な名前(jiraとか)のDBを作っておきます。
- クレジットカードでアトラシアンのサイトからライセンスを購入し、ライセンスキーをゲットします。
- あとはこのページに書いてあるとおりにやりました。
webサーバで見る
jiraを起動するとlocalhost:8080でアクセスできるようになります。
外部からはnginxのリバースプロキシを使用して80番から見えるようにしました。
nginxの設定はこんなカンジ。
メール送信の設定
SMTPサーバを使う方法と、アプリケーションサーバから直接送信する方法があるみたいです。
今回はアプリケーションサーバから送信するため、このページに沿って設定します。
- Configuring a JNDI locationのセクション通りに、server.xmlを編集します。(認証とか不要なので「Or if you do not require authentication ...」の方を入力します)
- 「Move the JavaMail Classes」のセクションに沿って、jarファイルの位置を修正します。インストールディレクトリのlibの中の方だけにmailとactivationのjarファイルがあればOKみたいです。
- jiraを再起動します。
sudo service jira stop sudo service jira start
つーか、メール来ないんですけど
上記設定をした後にプロジェクト作ってチケットを切ってもメールが来ません。
なんでやー!
プロジェクトの設定をしてないからやー!
- 管理モードでプロジェクトのページを開き、通知のタブを選択。
- 右側のActionsから規定の通知スキームを選択して終了。
それでもメールは来ない
どうやらデフォルトで、自分が追加変更した際は自分にメールが来ないようです。
- jira画面右上のログインユーザ名を選択。
- ユーザ設定の自分の変更を自分に通知に変更。「自分の変更を自分に通知」って哲学的ですね。
使用感
まだ使い始めたばっかりですがredmineより見た目がカッコイイのでうれしいです。
開発元のアトラシアンっていう名前も暮らし安心ってカンジがして良い。
Scala+Scalate+GAE/Jでcheck*padのクローンを作りました
ソースコード https://github.com/bbpink/shpapad
実物 http://shpapad.appspot.com/ (勝手に使ってもいいけどどうなっても知りません!)
環境選定までの道のり
check*padの代替を作ろうと思ったところまではいいのですがどうやって作るのかをまず考えました。
- 無料だからappengineでやろう
- Scalaで手軽にappengineアプリを作るならLiftっていうフレームワークがいいらしい
- ちょっといじってみたけど自動生成されるjavascriptファイルに日本語を入れられなくて詰んだ
- じゃあ俺ームワークを作ろう
- テンプレートエンジンはScalate
- ググるとescalateという単語がひっかかって大変だけど我慢
準備
Scalaとappengineの両方の素人だったのですが、webの情報だけを頼りになんとか作業環境を整えることができました。
やんなきゃいけないことは、
- appengineSDKを入手
- Scala本体をインストール
- sbtを入手
- @yasushia さんの作ったもの4つを入手
です。多いですね。
まずappengineSDKをダウンロードして展開後どっかに置いておきます。1.4.0を使いました。
Scala本体はlzPackってやつをダウンロードしてバシバシやってたらインストールできました。2.8.1。
windowsで開発したのですが、環境変数にSCALA_HOMEとか設定したかもしれません。
そしてsbtも同様です。sbt.batにパスを通しました。0.7.4です。
問題はyasushiaさんの4つです
scalateのテンプレートはjspみたいに初回アクセス時コンパイルされるらしいのですが、
appengineだとファイルシステムが使えないのでコンパイルできません。
これを回避するため、テンプレートをプリコンパイルしてくれちゃうのがこの4つなのです!!!
環境変数APPENGINE_SDK_HOMEにappengineSDKのパスを設定したら、
Yasushi-scalate-cli、Yasushi-sbt-scalate-plugin、Yasushi-sbt-appengine-pluginの3つをダウンロードし、
それぞれに対して
sbt publish-local
とやるとローカルのリポジトリに入ってくれるみたいです。
最後に今回のプロジェクトテンプレートとしてhello-scalate-appengineをダウンロードし、
sbt +update
とやるとようやく開発のための準備完了!!!
sbt clean package
でhello-scalate-appengineのコンパイルが通らなかったらなんかの手順を忘れているかもしくは間違ってるので教えてください。
実装(主にScalateの使い方)
HttpServletを継承したサーブレットクラスに
val te = new TemplateEngine te.resourceLoader = new ServletResourceLoader(getServletContext) val context = new ServletRenderContext(te, req, res, getServletContext) context.render("index.ssp", Map("name"->"bbpink"))
こんな感じで書いてレンダリングしてもらいます。
レンダリング結果を文字列として欲しい時は
val out = context.capture(context.render("index.ssp", Map("name"->"bbpink")))
でおk。
テンプレート側ではrenderで渡したMapを使うことができますが、
<%@ val name:String %>
みたいにして渡した変数を宣言しないと使えなくてメンドいです。(Mapのキーと変数名が一致していないとダメ!!)
テンプレート内で他テンプレートを呼び出す時もおんなじで、
<% render("header.ssp", Map("name" -> name)) %>
という感じでScalaのまんまっぽく書きます。
あとはScalateのドキュメントを読みまくれば大丈夫やれるできる!
http://scalate.fusesource.org/documentation/user-guide.html
あと難しかったのはデータストアです
appengine初心者は概念をつかむためにLowlevelAPIを使ったほうがいいらしかったので使ってみました。
無限にでかいExcelの表をvlookupしてることをイメージしたらなんとなくわかってきましたね。
初学者は無駄なプライドを捨て3000円をケチらずSlim3の本を読むのが絶対いいです。
オープンソース徹底活用 Slim3 on Google App Engine for Java
今後の課題
- Scalaっぽく書けるようになりたい!(Liftとかのソースを見てみたら面白いかも)
- 実はまだタスクの表示順を変えられません
- ユーザ定義順に表示するには1つのエンティティを更新したら他も全部更新しなきゃいけないしどうしたらいいの?
- なんかイカしたデータ構造とかで回避できたりしないかな?
感想
英語のドキュメントばっかりで難儀しましたがすごく楽しかったです!
Scalaの勉強会にもそのうち行ってみよう。