テンプレートリテラルを使うけど HTML ファイルとして書けるようにする
- カテゴリ:
- JavaScript
- つくった
- コメント数:
- Comments: 0
◆ テンプレートリテラルでテンプレート埋め込み処理をするとファイルは JavaScript ファイルになる
◆ HTML を作る場合は HTML の前後に JavaScript 処理が入って邪魔
◆ エディタのサポートも効かずに不便
◆ HTML だけを書いたファイルを使ってテンプレートリテラルの埋め込み処理をする
◆ 事前ビルドは必要
◆ HTML を作る場合は HTML の前後に JavaScript 処理が入って邪魔
◆ エディタのサポートも効かずに不便
◆ HTML だけを書いたファイルを使ってテンプレートリテラルの埋め込み処理をする
◆ 事前ビルドは必要
以前も書いた JavaScript のテンプレート問題です
テンプレートを モジュールファイルに分けるとこういう感じです
HTML だけでは済まず 上下に JavaScript としてのコードが少し含まれてしまいます
また 全体として JavaScript ファイルとなるのでエディタサポートが使えません
HTML ファイルとして書きたいので 上の例だと
だけを書いて
と
のような部分は自動で補います
また require やタグ関数の html も書かなくて済むようにしました
JavaScript の中の Identifier に対して自動で require を追加します
タグ関数はファイルの拡張子を使うことにしました
変換ツール
例
のようにコマンドライン引数で指定できます
指定がない場合はコマンドを実行したフォルダ内の src, dst, helper フォルダになります
src にはテンプレートファイルを入れます
今回は HTML ファイルなので .html 拡張子ですが SQL ファイルを作るなら .sql 拡張子です
拡張子はテンプレートリテラルのタグ関数の名前になります
dst は出力先のフォルダです
すべて .js ファイルで出力されます
helper にはヘルパーとして使う .js ファイルを入れます
自動 require されるものです
HTML 中で foo という関数を使うなら foo.js を helper に用意します
自動 require では foo.js の module.exports の値がテンプレートファイル中の foo として使えます
タグ関数も helper から自動で require するものなので .html ファイルを使うには html.js が必要です
htmlEscape 関数は略してますが html.js はこういう感じです
タグ関数として使うので エクスポートする関数の引数はタグ関数用のものです
と書くと自動でエスケープされ
と書くとエスケープされずそのまま埋め込まれます
配列が渡されると 中身それぞれに対して 上のエスケープするしないを処理してすべてを埋め込みます
この処理は変換とは関係ない 例として使ってる html.js の中身なので自由に変更できます
この記事中の HTML テンプレートはこのタグ関数を使う前提で書いています
html`` の内側に .html ファイルの中身がそのまま入ります
引数は $ として受け取ります
のような引数なら
[page.html]
のようなテンプレートにすると "Test Page" を h1 の中に埋め込めます
出力先の dst に設定したフォルダです
この機能のおかげでテンプレート内で他のテンプレートを使えます
[header.html]
[page.html]
page.html 中の header は header.html を変換した .js ファイルがエクスポートするものです
引数として "TITLE" を渡しているので header.html 内の $ は "TITLE" となっています
src, helper フォルダの中に直接ファイルを配置する必要があります
src フォルダは 3 つのファイルがあります
[page1.html]
[list.html]
[item.html]
h1 は helper で h1.js はこうなっています
[h1.js]
リポジトリでは package.json の dependencies に変換ツールを追加しているので
で変換ツールがインストールされます
コマンドラインツールとして動くようにしてるので
と実行すると変換処理が実行されます
でフォルダ指定できます
作りが雑なので --src=/path/to/src のような = を使う形式は非対応です
スペースが必要です
実行すると dst フォルダに page1.js と list.js と item.js ができています
リポジトリでは変換後のものを dst フォルダに含めているので変換しなくても結果を直接見れます
また index.js は変換後の .js ファイルを使う例です
を実行した結果の HTML はこうなります
output.html として含めています
ここまで書いたものに加えて 自動 require と エディタの ${} の扱いの問題があります
${} の中の処理が複雑になって変数を使ったり コールバック関数を定義したりすると 必要ないものまで require しようとしてエラーが出るはずです
スコープ上に存在しない変数の使用のみに制限すればいいのですが 簡単にできなさそうだったので対応してません
eslint で警告出たりするので できなくはないはずですけど
これによって色や補完などのサポート機能が使えます
しかし HTML などのテンプレートファイルで ${} を特別視しないため ${} の中で "</div>" などを使うと HTML タグとしてみなされます
タグの対応が合わずエラー扱いされたり 自動フォーマットでおかしくなったりと言った問題が出るはずです
テンプレート中の処理は減らしできるだけシンプルに書くべきだと思いますし 複雑になるなら helper 側でやるほうが良いと思います
require したいものと引数の $ 以外に Identifier を書けない制約があればテンプレートが複雑になることはほぼないと思います
一番の理由はテンプレートの変換の手間です
変更のたびに変換も必要って面倒です
Webpack などをあまり使いたくないのと一緒です
特にこれは JavaScript 標準のテンプレートリテラルで書けるから余計な手間がいらなくて高速なのが重要なところです
その魅力がなくなるならあえてこれを使うメリットはほぼないのですよね
ejs などの埋め込み系のテンプレートエンジンでも コンパイルしたときに これと同じようなテンプレートリテラル機能を使った JavaScript として出力することもできますし
それなら記法に自由度のあるテンプレートエンジンの方がメリットが大きいです
ejs では
と書けばエスケープの有無を変更できます
テンプレートリテラルだと ${} でしか書けないので そこに何を入れるかで判断するしかないです
他にも出力せずに JavaScript 処理だけをしたり 分岐や繰り返しなどもテンプレートエンジンのほうが便利です
JavaScript そのものだけで書けるメリットがあるから それらは多少不便だけど許容範囲だったのですが 一手間書けるならテンプレートエンジンでいい気がしてきます
ここまでのまとめ
簡単にまとめると JavaScript には テンプレートリテラル機能があるので単純埋め込みのテンプレートエンジンはいらなくなったはずなのに JavaScript ファイルとして書かないといけないせいで テンプレートファイルとして使えなくて扱いづらい のが問題ですテンプレートを モジュールファイルに分けるとこういう感じです
const html = require("./html-helper.js")
module.exports = values => html`
<p>このテンプレートリテラルの中に HTML を書く</p>
`
HTML だけでは済まず 上下に JavaScript としてのコードが少し含まれてしまいます
また 全体として JavaScript ファイルとなるのでエディタサポートが使えません
HTML ファイルを変換するツールを作った
この問題のせいでやっぱり不便すぎたので どうにかしたくて変換するツールを作りましたHTML ファイルとして書きたいので 上の例だと
<p>このテンプレートリテラルの中に HTML を書く</p>
だけを書いて
const html = require("./html-helper.js")
module.exports = values => html`
と
`
のような部分は自動で補います
また require やタグ関数の html も書かなくて済むようにしました
JavaScript の中の Identifier に対して自動で require を追加します
タグ関数はファイルの拡張子を使うことにしました
変換ツール
例
使いかた
上の URL にもある例を使いますフォルダ
この変換ツールは src, dst, helper の 3 つのフォルダが必要ですnode index.js --dst /path/to/templates
のようにコマンドライン引数で指定できます
指定がない場合はコマンドを実行したフォルダ内の src, dst, helper フォルダになります
src にはテンプレートファイルを入れます
今回は HTML ファイルなので .html 拡張子ですが SQL ファイルを作るなら .sql 拡張子です
拡張子はテンプレートリテラルのタグ関数の名前になります
dst は出力先のフォルダです
すべて .js ファイルで出力されます
helper にはヘルパーとして使う .js ファイルを入れます
自動 require されるものです
HTML 中で foo という関数を使うなら foo.js を helper に用意します
自動 require では foo.js の module.exports の値がテンプレートファイル中の foo として使えます
タグ関数も helper から自動で require するものなので .html ファイルを使うには html.js が必要です
html タグ関数
const htmlize = (value) => {
if (Array.isArray(value)) return value.map(htmlize).join("")
if (value.html) return value.html
if (value.text) return htmlEscape(value.text)
return htmlEscape(String(value))
}
module.exports = (tpls, ...values) => {
return tpls.reduce((a, b, i) => a + htmlize(values[i - 1]) + b)
}
htmlEscape 関数は略してますが html.js はこういう感じです
タグ関数として使うので エクスポートする関数の引数はタグ関数用のものです
<div>${"<br>"}</div>
と書くと自動でエスケープされ
<div>${{ html: "<br>"}}</div>
と書くとエスケープされずそのまま埋め込まれます
配列が渡されると 中身それぞれに対して 上のエスケープするしないを処理してすべてを埋め込みます
この処理は変換とは関係ない 例として使ってる html.js の中身なので自由に変更できます
この記事中の HTML テンプレートはこのタグ関数を使う前提で書いています
出力されるファイル
変換後に出力されるテンプレートのエクスポートされる部分はこういう感じになりますmodule.exports = $ => ({ html: html`<div>ここはそのまま</div>` })
html`` の内側に .html ファイルの中身がそのまま入ります
引数は $ として受け取ります
console.log(require("./templates/page.js")({ title: "Test Page" }))
のような引数なら
[page.html]
<h1>${$.title}</h1>
のようなテンプレートにすると "Test Page" を h1 の中に埋め込めます
同じフォルダの require
helper フォルダにマッチする .js ファイルがない場合は 同じフォルダからの require になります出力先の dst に設定したフォルダです
この機能のおかげでテンプレート内で他のテンプレートを使えます
[header.html]
<header>${$}</header>
[page.html]
<div>${header("TITLE")}</div>
page.html 中の header は header.html を変換した .js ファイルがエクスポートするものです
引数として "TITLE" を渡しているので header.html 内の $ は "TITLE" となっています
各フォルダの中はファイルのみ
自動 require でモジュールを探す都合上 src, helper フォルダの中のフォルダは無視されますsrc, helper フォルダの中に直接ファイルを配置する必要があります
例
例のリポジトリのテンプレートを見ていきますsrc フォルダは 3 つのファイルがあります
[page1.html]
<div>
<div>1 + 1 = ${1 + 1}</div>
<div>TEXT: ${$.html}</div>
<div>HTML: ${{ html: $.html }}</div>
<div>${h1("H1 TEXT")}</div>
<div>${list($.items)}</div>
</div>
[list.html]
<ul>${$.map(item)}</ul>
[item.html]
<li>${$.name}</li>
h1 は helper で h1.js はこうなっています
[h1.js]
const html = require("./html.js")
module.exports = (text) => {
return { html: html`<h1>${text}</h1>` }
}
リポジトリでは package.json の dependencies に変換ツールを追加しているので
yarn install
で変換ツールがインストールされます
コマンドラインツールとして動くようにしてるので
yarn tplb
と実行すると変換処理が実行されます
yarn tplb --src /path/to/src --dst /path/to/dst --helper /path/to/helper
でフォルダ指定できます
作りが雑なので --src=/path/to/src のような = を使う形式は非対応です
スペースが必要です
実行すると dst フォルダに page1.js と list.js と item.js ができています
リポジトリでは変換後のものを dst フォルダに含めているので変換しなくても結果を直接見れます
また index.js は変換後の .js ファイルを使う例です
const page1 = require("./dst/page1.js")
const { html } = page1({
html: `bold : <b>BOLD</b>`,
items: [
{ name: "ITEM1" },
{ name: "ITEM2" },
{ name: "ITEM3" },
]
})
console.log(html)
を実行した結果の HTML はこうなります
output.html として含めています
<div>
<div>1 + 1 = 2</div>
<div>TEXT: bold : <b>BOLD</b></div>
<div>HTML: bold : <b>BOLD</b></div>
<div><h1>H1 TEXT</h1></div>
<div><ul><li>ITEM1</li><li>ITEM2</li><li>ITEM3</li></ul></div>
</div>
問題点
これだけ見ると一見良さそうな気がしなくもないですが 思いつきでサッと作ったものだけあって 制限も多いですここまで書いたものに加えて 自動 require と エディタの ${} の扱いの問題があります
自動 require
acorn でソースコードをパースして type が Identifier の Node の name を require 対象としています${} の中の処理が複雑になって変数を使ったり コールバック関数を定義したりすると 必要ないものまで require しようとしてエラーが出るはずです
スコープ上に存在しない変数の使用のみに制限すればいいのですが 簡単にできなさそうだったので対応してません
eslint で警告出たりするので できなくはないはずですけど
エディタと ${}
HTML ファイルとしてしまったので エディタ上では HTML ファイルとして認識されますこれによって色や補完などのサポート機能が使えます
しかし HTML などのテンプレートファイルで ${} を特別視しないため ${} の中で "</div>" などを使うと HTML タグとしてみなされます
タグの対応が合わずエラー扱いされたり 自動フォーマットでおかしくなったりと言った問題が出るはずです
見方を変えると良いのかも?
不便な点ではありますが これらはテンプレート中の ${} で複雑なことをしようとするから発生する問題ですテンプレート中の処理は減らしできるだけシンプルに書くべきだと思いますし 複雑になるなら helper 側でやるほうが良いと思います
require したいものと引数の $ 以外に Identifier を書けない制約があればテンプレートが複雑になることはほぼないと思います
いる?
で 結局作ってみたけどこれいるの?ということですが……結局使わなそうな気はしてます一番の理由はテンプレートの変換の手間です
変更のたびに変換も必要って面倒です
Webpack などをあまり使いたくないのと一緒です
特にこれは JavaScript 標準のテンプレートリテラルで書けるから余計な手間がいらなくて高速なのが重要なところです
その魅力がなくなるならあえてこれを使うメリットはほぼないのですよね
ejs などの埋め込み系のテンプレートエンジンでも コンパイルしたときに これと同じようなテンプレートリテラル機能を使った JavaScript として出力することもできますし
それなら記法に自由度のあるテンプレートエンジンの方がメリットが大きいです
ejs では
<%= value %>
<%- value %>
と書けばエスケープの有無を変更できます
テンプレートリテラルだと ${} でしか書けないので そこに何を入れるかで判断するしかないです
${ value }
${ { html: value } }
他にも出力せずに JavaScript 処理だけをしたり 分岐や繰り返しなどもテンプレートエンジンのほうが便利です
JavaScript そのものだけで書けるメリットがあるから それらは多少不便だけど許容範囲だったのですが 一手間書けるならテンプレートエンジンでいい気がしてきます