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のバージョンとの互換を気にする必要がなくなるので、独自実装の方が長い目で見ると楽かもしれません