続き書いた
コメント欄や文中でのセルフ突っ込みや思いつきを形にして書いてみた新しいgoldblendと新しいページをアップしました。
環境
- node.js 0.10.33
- express 4.0.0-rc4
- CoffeeScript 1.9.2
やはり画面遷移毎にごりごり書くのはめんどい
CoffeeScriptを使うようにしてみて、圧倒的にタイプ量は減った。減ったのだが、やはり画面遷移をごりごり書くのはめんどい。expressとSocket.IOと同居させてごにょごにょし始めたら、そっちをごりごりやりたいので、遷移は楽ちんに書きたい。
で、これは個人的な話なのですが、画面遷移を書くとき、どうしてもリクエストを処理する「受信機」とレスポンスを返す「送信機」は別に書いた方が良くって、「/」が「受信機」、「*」が「送信機」みたいな書き方をしています。CGIをCで書いて、クラッシュしてめそめそしていた頃からですね。でもって、「これに近い形で画面遷移のコードを書いて、プリプロセッサを噛ませば良いじゃん!」というのが、今回の話です。なんだかんだ、色々嵌まって数時間かかってしまいましたことよ。
/adminloginform(retry)
*adminloginform(retry)
ユーザとパスワード入力ページ
(POST)⇒/adminlogin(user,pass)
/adminlogin(user,pass)
パスワード認証
NG⇒*adminloginform(true)
OK⇒(rd)*dashboard(user)
技術的な説明はごめんよ
CoffeeScriptにもまだそんなに詳しくないし、JavaScriptはマウスカーソルにまとわりつくウザい無意味な物体を制御するくらいしか経験がないので、ここでいきなり、プリプロセッサの正体に迫ります。
後述のgoldblend.coffeeを適当な場所に置き、
以上です。mytest.coffeeが出力されます。なんか、coffee -wcとかを真似て、「-w」オプションとか仕込んでおりますが、怖くて動作確認していないので、ちゃんと動かないかも知れません。動作確認の取れている使い方は、上記の使い方だけです。
インスタントに珈琲だからgoldblend
さあ、こうしてインスタントに珈琲ができあがります。入出力ほとんど同じだって? そりゃそうです。map出力なんてしないので、あんまりいじったらデバッグできなくなりますよう。
さて、新味のある場所はひとつだけ
というところだけですね。スペースは適当に見やすく入れても大丈夫です(たぶん)。
で、それぞれの意味は、これだ。
- <<<
- ここいらで画面を出力するよ
- classname
- クラス名であり、「受信機」のそこかしこから classname.redirect req,res,arg1,arg2 とすれば、リダイレクトで到達しますし、 classname.direct req,res,arg1,arg2 とすれば直接表示します。また、http://your.domain/classname/arg1/arg2 で到達することもできます(リダイレクトできるのだから当たり前)また、使用するテンプレート名もこのクラス名で決まります。例えば、views/classname.jadeみたいなものを用意することになります。
- req,res
- req,resは必須の引数。ただし、request,responseなど、変数名を変えることは可能
- arg1,arg2
- req,resの後には、任意の個数の引数(出力画面を特定するために必要充分であること)が付けられる。引数は必要なければ、別段なくても良い。ただし多い方はちょいと注意で、引数は好きな数だけ設定できるが、URL長は1000byte程度までしか許されないのですね。そのへんのエラー処理はしてません
- routername
- 文字通り、ここへのアクセスを仕切るrouterオブジェクト。「<>」と省略すると、デフォルトの「router」というオブジェクト名として扱う。今思ったけど、classname中のダブルアンダースコアを、URLに使うときには「/」として扱うようにしたら、も少し規模の大きいときには便利かも。後でやる
- middleware
- カンマ区切りで、classname.getをrouterにセットする際、middlewareとして登録する。URLでアクセスしたり、リダイレクトしたりした際に、セッション確認をするためのミドルウェアを設定することを想定している。つまり、ログイン後に通る画面には、logincheckをぽちぽちコピペするわけですな
- nocache
- 最後にキーワード「nocache」を付けると、リダイレクトする際「?ランダム文字列」をURLの最後に付加してくれる(付けなければ、何も付加しない)。まああれだ、キャッシュが効いてしまって画面が更新されない事件のためですな
と、こんな仕様だ。えらくキモいと思ったあなた、正解! いやね。ここまで可変長のパラメータやらフラグやらを付けると、yaml形式とかでぶら下げるしかないじゃん? でも、それってめんどいじゃん? そもそもめんどくさいのから逃げるためにプリプロセッサかまそうとしてるのに、本末転倒じゃん?
というわけで、キモいのが嫌いな人は、鮮やかな解決策を作って、ついでにnpmで入れられるようにして欲しいな。ついでにECTのLINTも作って欲しいな。ECTをdev版で動かしたとき、ブラウザ上に実行時エラーだしてくれるってのもいいな(さすがにdevであのスピードを維持したい御仁も少なかろう)
閑話休題
話がそれましたが、そんなわけで、下記に入力例「mytest.gold」出力例「mytest.coffee」プリプロセッサ「goldblend.coffee」を列挙しました。
まったくもー、誰の役にたつってんだ。バッドノウハウと言えばバッドノウハウ的なのだけど、「このニーズに合った、同じくらいコーディング量が少ない解法を教えてもらえるなら本望なので、ぜひコメントで正攻法を教えて頂ければ幸いです。「文字列からクラス生成」というような、JavaScript内で閉じてはいるけど、「逆にそっちがバッドノウハウじゃね?」的な解法はやあよ。
# ==========================================
# == ライブラリ
require 'coffee-script/register'
debug = console.log
http = require('http')
express = require('express')
path = require('path')
logger = require('morgan')
session = require('express-session')
redis = require('connect-redis')(session)
cookieParser = require('cookie-parser')
bodyParser = require('body-parser')
# multer = require('multer')
errorHandler = require('errorhandler')
# jade = require('jade')
ect = require('ect')
# ==========================================
# == express4を使うよ
app = express()
app.set('port', process.env.PORT || 3000)
# ==========================================
# == ビューエンジンのセットアップ
# app.set 'views', path.join(__dirname, 'views')
# app.set 'view engine', 'jade'
renderer = ect
watch: true
root: path.join(__dirname, 'views')
ext : '.ect'
app.engine 'ect', renderer.render
app.set 'view engine', 'ect'
# ==========================================
# == 各種の「その他」アクセスはpublicとかに流す
app.use logger('dev')
app.use bodyParser.json()
app.use bodyParser.urlencoded(extended: false)
# app.use multer()
app.use cookieParser()
app.use express.static(path.join(__dirname, 'public'))
# ==========================================
# == 主たる処理はこちら
router = express.Router()
# ==========================================
# == 送信部
# 「<<<classname」というトークンで、クラス名=テンプレート名=URLとなる
# 表示用クラスができあがる
# http://your.domain/classname/arg1/arg2 でも到達できるし、
# classname.redirect req,res,arg1,arg2 ではリダイレクトで到達できるし、
# classname.direct req,res,arg1,arg2 では、そのまま表示もできる
#
# 書式は <<<classname(req,res,arg1,arg2)<routername>(middleware) nocache
<<<loginform(req,res,defaultname,retry)<router>() nocache
@render @@, params
<<<dashboard(req,res,username)<router>() nocache
@render @@, params
<<<dummy(req,res)<>()
params['this_is'] = 'dummy'
@render @@, params
# ==========================================
# == 受信部(単純に表示を求めるだけではない表示部)
router.get '/', (req, res) ->
userpass.redirect req,res,''
router.post '/passinput', (req, res) ->
#パスワード受入処理
if result is ok
loginform.direct req,res,req.body.name,true
else
dashboard.redirect req,res,req.body.name
# ==========================================
# == 送受信部登録
app.use '/', router
# ==========================================
# == エラー処理諸々
# == 404エラーを発生させる
app.use (req, res, next) ->
err = new Error('Not Found')
err.status = 404
next err
# == 開発時エラーハンドラ(StackTrace表示)
if app.get('env') == 'development'
app.use (err, req, res, next) ->
res.status err.status or 500
res.render 'error',
message: err.message
error: err
# == 運用時エラーハンドラ
app.use (err, req, res, next) ->
res.status err.status or 500
res.render 'error',
message: err.message
error: {}
# == HTTPサーバの "error" へのイベントリスナ
onError = (error) ->
if error.syscall != 'listen'
throw error
bind = if typeof port == 'string' then 'Pipe ' + port else 'Port ' + port
# handle specific listen errors with friendly messages
switch error.code
when 'EACCES'
console.error bind + ' requires elevated privileges'
process.exit 1
when 'EADDRINUSE'
console.error bind + ' is already in use'
process.exit 1
else
throw error
return
# == HTTPサーバの "listening" へのイベントリスナ
onListening = ->
addr = server.address()
bind = if typeof addr == 'string' then 'pipe '+addr else 'port '+addr.port
debug 'Listening on ' + bind
return
# ==========================================
# == express4サーバ立ち上げ
normalizePort = (val) ->
port = parseInt(val, 10)
return if isNaN(port) then val else if port >= 0 then port else false
port = normalizePort(process.env.PORT or '3000')
app.set 'port', port
server = http.createServer(app)
server.listen port
server.on 'error', onError
server.on 'listening', onListening
# ==========================================
# == 外部からモジュールへのアクセス
module.exports = app
でもって、変換結果がこれ。
# ==========================================
# == ライブラリ
require 'coffee-script/register'
debug = console.log
http = require('http')
express = require('express')
path = require('path')
logger = require('morgan')
session = require('express-session')
redis = require('connect-redis')(session)
cookieParser = require('cookie-parser')
bodyParser = require('body-parser')
# multer = require('multer')
errorHandler = require('errorhandler')
# jade = require('jade')
ect = require('ect')
# ==========================================
# == express4を使うよ
app = express()
app.set('port', process.env.PORT || 3000)
# ==========================================
# == ビューエンジンのセットアップ
# app.set 'views', path.join(__dirname, 'views')
# app.set 'view engine', 'jade'
renderer = ect
watch: true
root: path.join(__dirname, 'views')
ext : '.ect'
app.engine 'ect', renderer.render
app.set 'view engine', 'ect'
# ==========================================
# == 各種の「その他」アクセスはpublicとかに流す
app.use logger('dev')
app.use bodyParser.json()
app.use bodyParser.urlencoded(extended: false)
# app.use multer()
app.use cookieParser()
app.use express.static(path.join(__dirname, 'public'))
# ==========================================
# == 主たる処理はこちら
router = express.Router()
# ==========================================
# == 送信部
# 「<<<classname」というトークンで、クラス名=テンプレート名=URLとなる
# 表示用クラスができあがる
# http://your.domain/classname/arg1/arg2 でも到達できるし、
# classname.redirect req,res,arg1,arg2 ではリダイレクトで到達できるし、
# classname.direct req,res,arg1,arg2 では、そのまま表示もできる
#
# 書式は <<<classname(req,res,arg1,arg2)<routername>(middleware) nocache
class loginform
@path: () => '/'+@.name
@install: (obj) => obj.get @path()+'/:defaultname/:retry',@get
@redirect: (req,res,defaultname,retry) =>
params = ''
params += '/'+encodeURIComponent(defaultname)
params += '/'+encodeURIComponent(retry)
res.redirect @path()+params+'?'+Math.random().toString(36).slice(-8)+Math.random().toString(36).slice(-8)
@get: (req,res) =>
defaultname = req.params.defaultname ? ''
retry = req.params.retry ? ''
@direct req,res,defaultname,retry
@direct: (req,res,defaultname,retry) =>
params =
defaultname: defaultname
retry: retry
res.render @.name, params
loginform.install router
class dashboard
@path: () => '/'+@.name
@install: (obj) => obj.get @path()+'/:username',@get
@redirect: (req,res,username) =>
params = ''
params += '/'+encodeURIComponent(username)
res.redirect @path()+params+'?'+Math.random().toString(36).slice(-8)+Math.random().toString(36).slice(-8)
@get: (req,res) =>
username = req.params.username ? ''
@direct req,res,username
@direct: (req,res,username) =>
params =
username: username
res.render @.name, params
dashboard.install router
class dummy
@path: () => '/'+@.name
@install: (obj) => obj.get @path()+'',@get
@redirect: (req,res) =>
params = ''
res.redirect @path()+params
@get: (req,res) =>
@direct req,res
@direct: (req,res) =>
params = {}
params['this_is'] = 'dummy'
res.render @.name, params
#==========================================
#== 受信部(単純に表示を求めるだけではない表示部)
dummy.install router
router.get '/', (req, res) ->
userpass.redirect req,res,''
router.post '/passinput', (req, res) ->
#パスワード受入処理
if result is ok
loginform.direct req,res,req.body.name,true
else
dashboard.redirect req,res,req.body.name
# ==========================================
# == 送受信部登録
app.use '/', router
# ==========================================
# == エラー処理諸々
# == 404エラーを発生させる
app.use (req, res, next) ->
err = new Error('Not Found')
err.status = 404
next err
# == 開発時エラーハンドラ(StackTrace表示)
if app.get('env') == 'development'
app.use (err, req, res, next) ->
res.status err.status or 500
res.render 'error',
message: err.message
error: err
# == 運用時エラーハンドラ
app.use (err, req, res, next) ->
res.status err.status or 500
res.render 'error',
message: err.message
error: {}
# == HTTPサーバの "error" へのイベントリスナ
onError = (error) ->
if error.syscall != 'listen'
throw error
bind = if typeof port == 'string' then 'Pipe ' + port else 'Port ' + port
# handle specific listen errors with friendly messages
switch error.code
when 'EACCES'
console.error bind + ' requires elevated privileges'
process.exit 1
when 'EADDRINUSE'
console.error bind + ' is already in use'
process.exit 1
else
throw error
return
# == HTTPサーバの "listening" へのイベントリスナ
onListening = ->
addr = server.address()
bind = if typeof addr == 'string' then 'pipe '+addr else 'port '+addr.port
debug 'Listening on ' + bind
return
# ==========================================
# == express4サーバ立ち上げ
normalizePort = (val) ->
port = parseInt(val, 10)
return if isNaN(port) then val else if port >= 0 then port else false
port = normalizePort(process.env.PORT or '3000')
app.set 'port', port
server = http.createServer(app)
server.listen port
server.on 'error', onError
server.on 'listening', onListening
# ==========================================
# == 外部からモジュールへのアクセス
module.exports = app
fs = require('fs')
for val,index in process.argv
if index>1
if /^-/.test(val)
if /w/.test(val)
watch = true
else
if /\.gold$/i.test(val) and fs.existsSync val
filesrc = val
filedst = val.replace /gold$/i, 'coffee'
console.log filesrc?
console.log filedst?
if not(filesrc?)
console.log 'mi a ta ra naaai!'
process.exit 1
doconv = (dst, src) ->
console.log 'processing... '+src
content = fs.readFileSync(src).toString().split("\n")
if fs.existsSync(dst)
fs.writeFile dst, '', 'utf8', (err) -> console.log err
firstline = true
comments = false
generate = false
indentsp = ''
for line,index in content
if firstline
firstline = false
else
fs.appendFileSync dst,"\n",'utf8', (err) -> console.log err
if comments
fs.appendFileSync dst,line,'utf8', (err) -> console.log err
if /^\s*###/.test(line)
comments = false
else
normaloutput = false
addindent = false
if /^\s*###/.test(line) #コメントのみ行を影響させない
comments = true
normaloutput = true
else if /^\s*#/.test(line) #コメントのみ行を影響させない
normaloutput = true
addindent = generate
else
if /^\s*<<</.test(line)
if generate
#generate末尾出力
fs.appendFileSync dst,indentsp+"\n",'utf8', (err) -> console.log err
fs.appendFileSync dst,indentsp+classname+'.install '+routername+"\n",'utf8', (err) -> console.log err
#newgenerate
#indentlv計測
#argreq,argres,argsの取得
#classnameの取得
#<router>
#(middleware)の取得
result = line.match /^(\s*)<<<\s*(\S+)\s*\(([^()]+)\)\s*<([^<>]*)>\s*\(([^()]*)\)\s*([a-z]*)/
if result == null
console.log 'syntax error ['+index+']'
process.exit()
#generate要素読み取り
indentsp = result[1]
classname = result[2]
tempargs = result[3].split(',')
args = []
for str,index in tempargs
str = str.replace(/^\s+/, '').replace(/\s+$/, '')
switch index
when 0
argreq = str
when 1
argres = str
else
args.push str
routername = result[4].replace(/^\s+/, '').replace(/\s+$/, '')
if routername.length == 0
routername = 'router'
middleware = result[5].replace(/^\s+/, '').replace(/\s+$/, '')
if middleware.length > 0
middleware = middleware + ','
nocache = (result[6] == 'nocache')
#generate冒頭定型出力
slashcolonparams = ''
cvparams = ''
buildparams4sp = ''
reqparams4sp = ''
yamlparams6sp = ''
for str,index in args
slashcolonparams += '/:'+str
cvparams += ','+str
if index>0
buildparams4sp += "\n"
reqparams4sp += "\n"
yamlparams6sp += "\n"
buildparams4sp += "#{indentsp} params += '/'+encodeURIComponent(#{str})"
reqparams4sp += "#{indentsp} #{str} = #{argreq}.params.#{str} ? ''"
yamlparams6sp += "#{indentsp} #{str}: #{str}"
emptyobject = if args.length == 0 then ' {}' else ''
appendnocache = if nocache then "+'"+'?'+"'+"+'Math.random().toString(36).slice(-8)'+"+"+'Math.random().toString(36).slice(-8)' else ''
text = """
#{indentsp}class #{classname}
#{indentsp} @path: () => '/'+@.name
#{indentsp} @install: (obj) => obj.get @path()+'#{slashcolonparams}',#{middleware}@get
#{indentsp} @redirect: (#{argreq},#{argres}#{cvparams}) =>
#{indentsp} params = ''
#{buildparams4sp}
#{indentsp} #{argres}.redirect @path()+params#{appendnocache}
#{indentsp} @get: (#{argreq},#{argres}) =>
#{reqparams4sp}
#{indentsp} @direct #{argreq},#{argres}#{cvparams}
#{indentsp} @direct: (#{argreq},#{argres}#{cvparams}) =>
#{indentsp} params =#{emptyobject}
#{yamlparams6sp}
"""
fs.appendFileSync dst,text,'utf8', (err) -> console.log err
generate = true
normaloutput = false
else if generate
spaces = line.match /^\s*/
if spaces == null
console.log 'a ri e nai error!'
process.exit()
if spaces[0].length > indentsp.length
#generateスコープ内出力
line = line.replace /@render\b/, argres+'.render'
line = line.replace /@@/, '@.name'
fs.appendFileSync dst,' '+line,'utf8', (err) -> console.log err
normaloutput = false
else
#generate末尾出力
fs.appendFileSync dst,indentsp+"\n",'utf8', (err) -> console.log err
fs.appendFileSync dst,indentsp+classname+'.install '+routername+"\n",'utf8', (err) -> console.log err
generate = false
normaloutput = true
else
normaloutput = true
if normaloutput
if addindent
fs.appendFileSync dst,' '+line,'utf8', (err) -> console.log err
else
fs.appendFileSync dst,line,'utf8', (err) -> console.log err
doconv filedst,filesrc
if watch?
fs.watchFile filesrc, (curr, prev) ->
doconv filedst,filesrc