JavaScriptで Infrastructure as Code
のツールを開発しています
以前こちらの記事で、サーバからShellScriptで取得してきた値を、HTML表示する機能を紹介しましたが、その際にHTMLに値を埋め込む簡単なテンプレートエンジンを自前で実装したので、その紹介です
テンプレートエンジンとは?
Wikipediaのページが、ちゃんとあるような専門用語なんですね
固い説明はそちらに任せるとして、ものすごく乱暴に説明すると、 hoge='fuga'
のとき I am a {{ hoge }}.
という文字列の {{ hoge }}
部分に I am a fuga.
というように 'fuga'
を埋め込んでくれるようなもののことです
私はAnsibleを本職でよく利用するのでPythonの世界で有名なテンプレートエンジンである Jinja2
の経験があります
実際のテンプレートエンジンでは {{ 2 + 3 }}
を 5
というように四則演算ができたり、テンプレートの中に関数を埋め込んだりできるのですが、この記事で紹介するのは、単に変数の値を埋め込む機能だけの簡単なものになります
npmで適当なテンプレートエンジン提供されてないの?
Node.jsでもいくつかテンプレートエンジンはあるようですが、今回それらを使いませんでした。理由は2つあります
- 調べた限りでもテンプレートエンジンありすぎて、どれが良いのかよく分からない
- 可能な限り依存パッケージは減らしたい (というかクライアントサイドでVue.js使った以外はnpmの依存パッケージは0にしました)
どうやって実装したのか
おおまかな処理の流れは、以下のような関数にまとめられます
- テンプレートファイルを文字列としてロードする
- 1を改行コード
/\r\n|\r|\n/ (正規表現)
で分割する - 2を1要素(1行)ごとに取り出し、値の埋め込み部分を表す
{{%
と%}}
で分割する - 3の奇数番目の文字列(
{{%
と%}}
で囲まれた文字列)の両端のスペース - 4の結果が引数として渡されたオブジェクトのキーとして存在する場合のみ、値を置換する
- 全体を 改行コード
\n
でつなげる
※テンプレート内に改行を含む場合は対応しませんでした
※また、別のテンプレートエンジンで使われそうな {{
}}
と見分けるために、あえて {{%
%}}
にしています
コードとしては以下のようになります
(1) テンプレートファイルを文字列としてロードする
// ファイルアクセスのモジュールをロード
const fs=require('fs');
// テンプレートファイルを文字列としてロードする
// テンプレートは関数の外で1回ロードすれば充分
const template=fs.readFileSync(
`${__dirname}/templates/hoge.templ`
).toString();
(2) (1)を改行コード /\r\n|\r|\n/ (正規表現)
で分割する
const render=(props)=>{
return template
.split(/\r|\n|\r\n/)
}
(3) (2)を1要素(1行)ごとに取り出し、値の埋め込み部分を表す {{%
と %}}
で分割する
const render=(props)=>{
return template
.split(/\r|\n|\r\n/)
.map(
line => line.split(/{{%|%}}/);
);
}
(4) (3)の奇数番目の文字列の両端のスペース
を取り除く(トリミング)
const render=(props)=>{
return template
.split(/\r|\n|\r\n/)
.map(
line => {
const words=line.split(/{{%|%}}/);
return 1 < words.length
? words.map(
(w, i) => 0 < i%2
? w.replace(/^ */, '')
.replace(/ *$/, '')
: w
).join('')
: line
}
);
}
- 上記の処理をまとめ、必要な部分に値を埋め込む関数
// ファイルアクセスのモジュールをロード
const fs=require('fs');
// テンプレートファイルを文字列としてロードする
// テンプレートは関数の外で1回ロードすれば充分
const template=fs.readFileSync(
`${__dirname}/templates/hoge.templ`
).toString();
const render=(props)=>{
return template
.split(/\r|\n|\r\n/)
.map(
line => {
const words=line.split(/{{%|%}}/);
return 1 < words.length
? words.map(
(w, i) => 0 < i%2
? props[
w.replace(/^ */, '')
.replace(/ *$/, '')
]
: w
).join('')
: line
}
).join('\n');
}
テンプレートを用意して実行するとこんな感じ
I am a {{% hoge %}}.
console.log(
render({ hoge: 'fuga', })
);
$ node TemplateEngine.js
I am a fuga.
おわりに
ただ値を埋め込むだけなら、とても簡単に実装できることが分かります
これなら余計なパッケージのバージョン管理やnodeのバージョンとの互換を気にする必要がなくなるので、独自実装の方が長い目で見ると楽かもしれません