◆ 前回の事前変換が必要なのはイマイチだったので実行時に動的に関数を作る
◆ 事前変換は不要にする
◆ 事前変換よりはパフォーマンスは劣るけどそれは最初だけ
◆ 2 回目以降は作った関数がキャッシュされるのであまり気にしなくていい

ここまで

これの続きです
テンプレートリテラルを使ったテンプレートファイルでは

const html = require("./html.js")
module.exports = value => html`
<div>${value.body}</div>
`

のようにテンプレート本体以外のところが多いです
これをどうにかしたくて `` の内側だけを別ファイルに書けるものを作ったのですが 実行のために `` の外側と合成して 1 つの JavaScript とする必要があり 事前に変換処理が必要でした

変換処理が面倒で 単純にテンプレートリテラルで書くときほど扱いやすくないなら 機能面で優れているテンプレートエンジンの方が良さそうというところです

事前変換を不要にする

事前の変換がやっぱりネックだったのでそれをなくそうと思います
とは言っても JavaScript のコードとして処理しないといけないのに それが分割されています
実行時に JavaScript ファイルを作って書き込んで それを require を考えましたが それなら eval を使ったほうが速いです
パフォーマンス面ではやっぱり事前ビルドよりは劣ることになるので あまりやろうと思わなかったのですがテンプレートエンジンと比べると十分速いので 実行時に動的にテンプレート処理する関数を作ることにしました

Web サーバ用としても Node.js は PHP のようにリクエストごとにすべて 1 から処理しないといけないわけではなく 最初に一回やればそれ以降のリクエストの処理ではキャッシュを利用できます
コマンドラインツールとしてコマンドを何度も実行するような使い方にしなければ問題はなさそうです

作ってみたものはココにおいています
一応 2 パターン用意してます

html ファイル

今回の例では HTML ファイルの種類は 1 つだけです

<div>${h1(value.foo)}</div>
<div>text: ${value.bar}</div>
<div>html: ${{ html:value.bar }}</div>

中身はこれだけで 外側に JavaScript コードのない HTML ファイルです
タグ関数は前回と同じ html.js です
「{ html: "<br/>" }」 のように html プロパティだとエスケープなしです

page1

templates フォルダ内の page1.html と page1.js が 1 つ目のものです
page1.html の中身は上の HTML です
HTML に対して .js ファイルを 1 つ用意して そこで関数を作ります

const fs = require("fs")
const html = require("../helper/html.js")
const h1 = require("../helper/h1.js")

const tpl = fs.readFileSync(__dirname + "/page1.html").toString().trim()

module.exports = eval("value => html`" + tpl + "`")

必要なモジュールの require と HTML ファイルの読み取り後に eval で動的に関数を作ります
自動 require だった前回のより 書く量は多いですが .js ファイルの方はテンプレートの追加時や require の追加時にしか変更しなくていいところなので事前変換の手間に比べるとマシです
それに基本コピペで必要な module とファイル名を変えるくらいです
自分で全部書く分 自由度が高い利点もあります

page2

templates フォルダ内の page2.html と page2.json が 2 つ目のものです
page2.html の中身は上の HTML です

page1 では自由度が高いと言っても基本コピペで変えないですし 似たような処理の .js ファイルがテンプレートファイルの数だけできるのは気持ちの良いものではないです
修正したい場合に全部の .js ファイルを変えないといけないですし
そこで .json に設定だけ書いて処理自体は共通にしたものが page2 です

JSON ファイルの中身はこうなります

{
"requires": {
"html": "../helper/html.js",
"h1": "../helper/h1.js"
},
"argument": "value",
"type": "html"
}

requires に require するモジュールのリストを書きます
argument は引数として受け取る名前です
type はテンプレートファイルの種類で ファイル名の拡張子やタグ関数として使われます
この JSON を処理する部分が helper/template.js です

const fs = require("fs")
const cache = {}

const create = (modules, source) => {
// hide outer variables
var fs, cache, module, exports, require, __dirname, __filename
with (modules) { return eval(source) }
}

module.exports = name => {
if (!cache[name]) {
const base_path = __dirname + "/../templates"
const json_path = `${base_path}/${name}.json`
const def = require(json_path)
const tpl_path = `${base_path}/${name}.${def.type}`
const tpl = fs.readFileSync(tpl_path).toString().trim()
const source = `${def.argument} => ${def.type}\`${tpl}\``
const modules = {}
for (const [name, path] of Object.entries(def.requires)) {
modules[name] = require(`${base_path}/${path}`)
}
cache[name] = create(modules, source)
}
return cache[name]
}

共通部分になるので 外側の module とかにアクセスするのは好ましくないので 別の関数に分けてスコープ内に変数宣言だけしてアクセスできないようにしています

この方法だと それぞれがモジュールとならずキャッシュされないのでキャッシュの仕組みはここで用意しています

index.js

index.js は page1 と page2 に引数を渡して HTML を作る例です

const p1 = require("./templates/page1.js")

const tpl = require("./helper/template.js")
const p2 = tpl("page2")

console.log("== p1 ==")
console.log(
p1({
foo: "p1",
bar: "<b>bold</b>"
})
)
console.log("== p2 ==")
console.log(
p2({
foo: "p2",
bar: "<b>bold</b>"
})
)

page1 はそのまま page1.js を require して関数を呼び出します
page2 は template.js を require して JSON ファイルの名前を渡すとテンプレート関数を受け取れます

== p1 ==
<div><h1>p1</h1></div>
<div>text: &lt;b&gt;bold&lt;/b&gt;</div>
<div>html: <b>bold</b></div>
== p2 ==
<div><h1>p2</h1></div>
<div>text: &lt;b&gt;bold&lt;/b&gt;</div>
<div>html: <b>bold</b></div>

使える?

複雑な使い方はしてないので 何か問題が出てくる可能性はありますが 前回のよりはアリだと思ってます
できることは JavaScript のテンプレートリテラルの機能ほぼそのままで テンプレート部分を別ファイルに分けれるようにしてるだけなので これでできないことがあるならテンプレートリテラルは諦めてテンプレートエンジンを使ったほうがいいと思います

気になるところを言えば eval ですが ユーザが送信してきたデータなどではなく テンプレートを書いた人のコードなのでセキュリティ的な心配はないはずです
それにパフォーマンス面でも 関数を作るときは eval ではない方法よりも劣りますが 作ってしまえば後はリテラルで定義した関数と違いはないです
一応実行速度を比較してみましたが特に違いはありませんでした

${} の中身が特別扱いされない問題は残りますが とりあえずこれでテンプレート問題は解決できたと言って良さそうです