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) テンプレートファイルを文字列としてロードする
// ファイルアクセスのモジュールをロードconstfs=require('fs');// テンプレートファイルを文字列としてロードする// テンプレートは関数の外で1回ロードすれば充分consttemplate=fs.readFileSync(`${__dirname}/templates/hoge.templ`).toString();
(2) (1)を改行コード /\r\n|\r|\n/ (正規表現)
で分割する
constrender=(props)=>{returntemplate.split(/\r|\n|\r\n/)}
(3) (2)を1要素(1行)ごとに取り出し、値の埋め込み部分を表す {{%
と %}}
で分割する
constrender=(props)=>{returntemplate.split(/\r|\n|\r\n/).map(line=>line.split(/{{%|%}}/););}
(4) (3)の奇数番目の文字列の両端のスペース を取り除く(トリミング)
constrender=(props)=>{returntemplate.split(/\r|\n|\r\n/).map(line=>{constwords=line.split(/{{%|%}}/);return0<words.length?words.map((w,i)=>0<i%2?w.replace(/^ */,'').replace(/ *$/,''):w).join(''):line});}
- 上記の処理をまとめ、必要な部分に値を埋め込む関数
// ファイルアクセスのモジュールをロードconstfs=require('fs');// テンプレートファイルを文字列としてロードする// テンプレートは関数の外で1回ロードすれば充分consttemplate=fs.readFileSync(`${__dirname}/templates/hoge.templ`).toString();constrender=(props)=>{returntemplate.split(/\r|\n|\r\n/).map(line=>{constwords=line.split(/{{%|%}}/);return0<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のバージョンとの互換を気にする必要がなくなるので、独自実装の方が長い目で見ると楽かもしれません