予定外!続きがあります
前進してgoldblendはどこへ行ったか。こんな平日の昼間になぜか前進したのは、他にやらなきゃいけないことがあるからですね、はい。反省しております。
環境
- node.js 0.10.33
- express 4.0.0-rc4
- CoffeeScript 1.9.2
続・やはり画面遷移毎にごりごり書くのはめんどい
早朝書いた記事の続きです。
画面遷移をぱっぱかぱっぱか書きたいので、字句解析も構文解析もしない、怪しいプリプロセッサをかましてなんとかしたろ、と思っている悪い人の話。
続・技術的な説明はごめんよ
下の方に、出力されたクラスが書いてありますが、「=>」で束縛とか地味に大事です、とか技術ブログっぽいことを書いてみたりするものの、ガチ勢に襲われるのが怖いので多くは語るまい。こうして見てみると、CoffeeScriptによって、Javascriptの クラスが使い物になる というのが大きいなあ、と思うのです。さすがに、こういうプリプロセッサを、Javascriptを出力言語にして作ろうとは思いません。
さて早速なのですが、後述のgoldblend.coffeeを適当な場所に置き、
以上です。mytest.coffeeが出力されます。監視用の「-w」オプションとか仕込んだままになっておりますが、なんと! 未だに怖くて動作確認していないので、ちゃんと動かないかも知れません。動作確認の取れている使い方は、上記の使い方だけです。まあ、監視オプションなんかなくても、どうせみんなGruntとか使ってるよね!
続・インスタントに珈琲だからgoldblend
さあ、こうしてインスタントに珈琲ができあがります。折角なのでここからは「goldblend」の公式BGMである「例の曲」を聞きながら、だばだー、だーーば、だばだー、と読んで頂ければ結構です。
さて、プリプロセッサが処理する構文はこれだけです。
# 設定行
# 全項目の設定変更をします
<<<{routername:router, middleware:logincheck, nocache: no}
# middlewareを無効化し、キャッシュ対策を有効にします
<<<{middleware:, nocache: yes}
# 画面定義
<<<classname(req,res,arg1,arg2)
params['title'] = 'タイトルとか挿しこんじゃう?'
params['author'] = '私だ'
@render @@, params
こんな感じになりました。設定行が新たにお目見えし、画面定義が何だか関数っぽい見た目になりました。いっそ、カッコの省略も認識しろ、と言いたいところなのですが、あんまりいわゆる「俺様ユーティリティ」に手間暇かけたくないので、もう今度こそ放置です。きっと湯水のように時間を使えて、賢くてお茶目でロリっぽくて、生まれたときからLLと一緒に育ってきた大学院生(14)が、こういうニーズに答えるもっと素敵なソリューションをnpmにあげてくれるのを信じています。
で、それぞれの意味を下に示します。
設定行の要素
- <<<{}
- 画面出力関係の設定だよ。ファイルの中で、直前の設定が有効だよ。設定行がある度に、設定は上書きされるよ。実装の都合っぽい感じの仕様なのは錯覚じゃないよ
- routername
- 文字通り、ここへのアクセスを仕切るrouterオブジェクト。「routername:」と値を省略すると、デフォルトの「router」というオブジェクト名として扱う。nullとかfalseとかも受け付ければほっこり人情を感じるのですが、世の中は世知辛いです
- middleware
- classname.getをrouterにセットする際、middlewareとして登録する。URLでアクセスしたり、リダイレクトしたりした際に、セッション確認をするためのミドルウェアを設定することを想定している。つまり、ログイン後に通る画面には、logincheck。カンマで複数並べる仕様は廃止したので、複数のmiddlewareを使うときは、ラッパー関数を書いてそれを設定すれば良いよね! なお、middlewareを外す際は、「middleware:」と値を省略して書けば良い。nullとかfalseとかも受け付ければほっこり人情を感じるのですが、世の中は世知辛いです
- nocache
- nocacheをtrueにすると、リダイレクトする際「?ランダム文字列」をURLの最後に付加してくれる(付けなければ、何も付加しない)。まああれだ、キャッシュが効いてしまって画面が更新されない事件のためですな
画面定義の要素
- <<<
- ここいらで画面を出力するよ
- classname
- クラス名であり、「受信機」のあっちこっちから
classname.redirect req,res,arg1,arg2 とすれば、一旦リダイレクトして、このクラスにぶら下がったスコープに到達しますし、
classname.direct req,res,arg1,arg2 とすれば直接いきなりスコープ内の表示処理を実行します。また、
http://your.domain/classname/arg1/arg2 でブラウザから実行させることもできます(リダイレクトできるのだから当たり前ですが、まあ、デバッグ用としても欲しいもんですよね)
また、使用するテンプレート名もこのクラス名で決まります。例えば、views/classname.jadeみたいなものを用意することになります。
なお、URLに展開されるとき、classname中のダブルアンダースコアは「/」に置換します。<<<class__name(req,res,arg)にアクセスするURLは、http://your.domain/class/name/argとなります。routerを使い分けられる仕様にしたのに、深い階層へのルーティングができなかったら、express4使ってる意味ないもんね - req,res
- req,resは必須の引数。ただし、request,responseなど、変数名を変えることは可能
- arg1,arg2
- req,resの後には、任意の個数の引数(出力画面を特定するために必要充分であること)が付けられる。引数は必要なければ、別段なくても良い。ただし多い方はちょいと注意で、引数は好きな数だけ設定できるが、URL長は1000byte程度までしか許されないのですね。そのへんのエラー処理はしてません
- 後に続くスコープ
- 画面定義行の後にはスコープが続く必要がある。そのスコープは、classname.directの後半としてそのまま用いられる。paramsオブジェクトには既にここで言うarg1,arg2が格納されている。想定しているのは、ここでキャッシュやDBから追加の情報を引っ張ってきて、paramsに積み増すこと。この辺、なんかラッパーを噛ませた方が良いでしょう。
params['main'] = dbquery(username, 'main')
params['sidebar'] = dbquery(username, 'link')
みたいなのが並ぶイメージです。このスコープ内では、「@render」は「res.render」として展開され、「@@」はclassname文字列に展開される。分岐したり継続の中に放り込んだりするなど自由だけど、必ず@renderを通るように作りましょう。そのために、「@render」をシンタックスハイライトしておくと便利。(このclassname.directからさらにリダイレクトするのは、ブラウザによってはリダイレクトの連続をぶった切ってくれるので、あんまりよろしくないです。画面遷移は「ぐちゃぐちゃ有向グラフ」にならざるを得ないことが多いので、無限ループがちょう怖いです)
@render @@+'-suffix', params
というように、付加文字列を付けて、classname-suffix.jade(いや、ECTでもいいのですぞ)を参照させるようなことが可能で、複数種類のテンプレートを使い分けるように分岐する場合に用います。
いずれにせよ、何らかのclassnameに対応したテンプレートが必要となります。
と、こんな仕様。キモさは減った。ちょっとだけだけど減った。でもまだキモいね。でも、もう自分的にはこれで使っていこうと思った。構文解析なんてしないもーん。
というわけで、キモいのが嫌いな人は、鮮やかな解決策を作って、ついでにnpmで入れられるようにして欲しいな。ついでに誰か!ECTのLINTも作って欲しいな(血涙)
続・閑話休題
話がそれましたが、そんなわけで、下記に入力例「mytest.gold」出力例「mytest.coffee」プリプロセッサ「goldblend.coffee」を列挙しました。
まったくもー。
# ==========================================
# == ライブラリ
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()
# ==========================================
# == 送信部
<<<{routername: router, middleware:logincheck, tocache:false}
<<<loginform(req,res,defaultname,retry)
@render @@, params
<<<dashboard(req,res,username)
@render @@, params
<<<dummy(req,res)
params['this_is'] = 'dummy'
@render @@, params
true
# ==========================================
# == 受信部(単純に表示を求めるだけではない表示部)
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()
# ==========================================
# == 送信部
# router||true
class loginform
@install: (obj) => obj.get '/loginform/:defaultname/:retry',@get
@redirect: (req,res,defaultname,retry) =>
params = ''
params += '/'+encodeURIComponent(defaultname)
params += '/'+encodeURIComponent(retry)
res.redirect '/loginform'+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 'loginform', params
loginform.install router
class dashboard
@install: (obj) => obj.get '/dashboard/:username',@get
@redirect: (req,res,username) =>
params = ''
params += '/'+encodeURIComponent(username)
res.redirect '/dashboard'+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 'dashboard', params
dashboard.install router
class dummy
@install: (obj) => obj.get '/dummy',@get
@redirect: (req,res) =>
params = ''
res.redirect '/dummy'+params+'?'+Math.random().toString(36).slice(-8)+Math.random().toString(36).slice(-8)
@get: (req,res) =>
@direct req,res
@direct: (req,res) =>
params = {}
params['this_is'] = 'dummy'
res.render 'dummy', params
dummy.install router
true
# ==========================================
# == 受信部(単純に表示を求めるだけではない表示部)
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'
if not(filesrc?)
console.log 'mi a ta ra naaai!'
process.exit 1
writetext = (dst, text) ->
fs.appendFileSync dst,text,'utf8', (err) -> console.log err
generateTail = (dst, indentsp, classname, routername) ->
text = indentsp+classname+'.install '+routername+"\n\n"
writetext dst,text
generateMain = (dst,indentsp,classname,temp,routername,mw,nocache) ->
args = []
for str,index in temp
str = str.replace(/^\s+/, '').replace(/\s+$/, '')
switch index
when 0
argreq = str
when 1
argres = str
else
args.push str
urlprm = ''
cvparams = ''
buildparams4sp = ''
reqparams4sp = ''
yamlparams6sp = ''
for str,index in args
urlprm += '/:'+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 ''
randomstring = 'Math.random().toString(36).slice(-8)'
appendnocache = if nocache then "+'?'+#{randomstring}+#{randomstring}" else ''
path = classname.replace /__/,'/'
text = """
# {indentsp}class #{classname}
# {indentsp} @install: (obj) => obj.get '/#{path}#{urlprm}',#{mw}@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}
"""
writetext dst,text
return argres
doconv = (dst, src) ->
console.log 'processing... '+src
content = fs.readFileSync(src).toString()
content = content.replace(/\r\n/, "\n").replace(/\r/, "\n").split("\n")
if fs.existsSync(dst)
fs.writeFile dst, '', 'utf8', (err) -> console.log err
comments = false
generate = false
indentsp = ''
defaultmode = true
routername = 'router'
mw = ''
nocache = true
for line,index in content
if index>0
writetext dst,"\n"
if comments
writetext dst,line
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) #空行を影響させない
normaloutput = true
addindent = generate
else
if /^\s*<<</.test(line)
if generate
#generate末尾出力
generateTail dst,indentsp,classname,routername
result = line.match /^(\s*)<<<\s*{([^{}]*)}/
if result == null
#newgenerate
if defaultmode
defaultmode = false
writetext dst,'#'+routername+'|'+mw+'|'+nocache.toString()
#indentspの取得
#argreq,argres,argsの取得
#classnameの取得
result = line.match /^(\s*)<<<\s*(\S+)\s*\(([^()]+)\)/
if result == null
console.log 'syntax error ['+index+']'
process.exit 1
#generate要素読み取り
indentsp = result[1]
classname = result[2]
temp = result[3].split(',')
argres = generateMain dst,indentsp,classname,temp,routername,mw,nocache
generate = true
normaloutput = false
else
#routernameの取得
#mwの取得
#nocache
defaultmode = false
scopeindentsp = result[1]
tempkv = {}
keyvalues = result[2].split(',')
for keyvalue in keyvalues
keyvalue = keyvalue.replace(/^\s+/, '').replace(/\s+$/, '')
keyvalue = keyvalue.split(':')
key = keyvalue[0].replace(/^\s+/, '').replace(/\s+$/, '')
value = keyvalue[1].replace(/^\s+/, '').replace(/\s+$/, '')
tempkv[key] = value
if tempkv.routername?
routername = tempkv.routername
if routername.length == 0
routername = 'router'
if tempkv.middleware?
mw = tempkv.middleware
if mw.length > 0
mw = mw + ','
if tempkv.nocache?
nocache = (/(true|ok|yes)/i.test(tempkv.nocache))
writetext dst,'#'+routername+'|'+mw+'|'+nocache.toString()
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 /@@/, "'"+classname+"'"
writetext dst,' '+line
normaloutput = false
else
#generate末尾出力
generateTail dst,indentsp,classname,routername
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
console.log 'completed!'
doconv filedst,filesrc
if watch?
fs.watchFile filesrc, (curr, prev) ->
doconv filedst,filesrc
else
process.exit 0