Vue のテンプレートの書き方を改良したい
- カテゴリ:
- JavaScript
- コメント数:
- Comments: 0
◆ 制御構文を見やすくしたい
◆ 独自記法で書いて Vue の HTML テンプレート形式に変換する
◆ HTML 形式で独自のタグを作って変換で十分そう
◆ 属性の値を "" で書くのを避けたい
◆ HTML としてパースするのだと無理そう
◆ JSX を使えるみたいなので htm でテンプレートを書くことにする
◆ 制御構文はイマイチだけど React や lit-html とも同じだしまぁ良しとする
◆ 独自記法で書いて Vue の HTML テンプレート形式に変換する
◆ HTML 形式で独自のタグを作って変換で十分そう
◆ 属性の値を "" で書くのを避けたい
◆ HTML としてパースするのだと無理そう
◆ JSX を使えるみたいなので htm でテンプレートを書くことにする
◆ 制御構文はイマイチだけど React や lit-html とも同じだしまぁ良しとする
独自記法
前回の最後に書いた部分の続きで 最終的に HTML 形式のテンプレートにすればあとは Vue がやってくれるんだから独自の書きやすい記法を作って それを Vue テンプレートの HTML に変換だけすれば良さそうというところです特殊な記法作ったところで 他人が書いたものがこれを使ってくれるわけはないので 対象は自分が書くもののみです
そうなるとリスナ設定の属性の value 部分の統一性のなさとかは自分が methods の関数名しか書かないようにすれば統一性があって読みやすくできますし とりあえず if や for を見やすくすればとりあえず十分に思いました
HTML を作ると言っても markdown みたいなものじゃなくて 結局 属性を細かに指定して子要素を書く以上 あまり HTML から離れたものにならないですし pug みたいなインデントベースでちょっと省略する程度にしかならなそうです
if や for で考えると属性に埋もれて目立たず見やすさに欠けるのが問題点なので HTML 形式のままでもできそうな気がします
例えば
<IF condition="x === y">
<div> x is y</div>
</IF>
と書けるだけでも十分です
タグ名で分岐だとわかりますし 大文字なのが特殊感を出していて他とは違って制御構文系だと判断できます
その方針で HTML タグで表すようにしてみました
上の IF タグだと else との対応がしづらいとか色々あって IF タグではなくなりました
CONDITION
条件分岐系は CONDITION タグを使います単純な if なら属性に if を使って条件を書きます
<CONDITION if="X">
<div></div>
</CONDITION>
↓
<template v-if="X">
<div></div>
</template>
else や else-if を使うには CONDITION の中に CASE を使ってそっちに if 属性で条件を指定します
<CONDITION>
<CASE if="X">
<div>1</div>
</CASE>
<CASE if="Y">
<div>2</div>
</CASE>
<CASE>
<div>3</div>
</CASE>
</CONDITION>
↓
<template v-if="X">
<div>1</div>
</template>
<template v-else-if="Y">
<div>2</div>
</template>
<template v-else>
<div>3</div>
</template>
通常の Vue テンプレート記法よりもネストは増えますが 条件分岐が目立つ分見やすくなったと思います
なくても良さそうですが 一応 switch 風にも書けるようにしてます
CONDITION では対象にする value を指定して CASE では is で value と一致確認する値を指定します
<CONDITION value="X">
<CASE is="Y">
<div>1</div>
</CASE>
<CASE is="Z">
<div>2</div>
</CASE>
<CASE>
<div>3</div>
</CASE>
</CONDITION>
↓
<template v-if="(X) === (Y)">
<div>1</div>
</template>
<template v-else-if="(X) === (Z)">
<div>2</div>
</template>
<template v-else>
<div>3</div>
</template>
見ての通り単純な変換で X は一時変数に保存されるわけではないのでコストが掛かったり結果が変わる関数呼び出しには使えません
FOR
ループは FOR タグですVue では v-for 属性に JavaScript の for の中身みたいな構文を書かないといけないのが気持ち悪いところでした
それをなくしたかったので of と as の属性を用意して v-for の中身を作るようにしました
<FOR of="items" as="item index">
<div></div>
</FOR>
↓
<template v-for="(item, index) of items">
<div></div>
</template>
変換
変換処理ですが 結局 HTML 形式にしたので自分で複雑なパーサは書かずに HTML として DOM を構築し 単純な変換をするだけで済みます事前処理なので Node.js で処理することになって DOMParser などに頼れないので jsdom を使うことになります
今回新しく作ったタグは大文字ですが HTML では大文字小文字の区別はなくすべて小文字として扱われます
なので jsdom でパースする前に CONDITION や FOR を認識できるように適当な他で使われなさそうな小文字のタグ名に変換が必要です
あとは querySelector で変換後のタグ名を探して置換するだけです
htm を Vue で使う
これで if や for が見やすくなって Vue を使うことが増えるかもと思ってましたしかし 多少見慣れれば v-if や v-for を template にだけ使うというルールで書いてればあまり困るものでもなかったです
それよりも属性の JavaScript 処理を "" で書かないといけないほうがストレスが大きいです
これをどうにかするには HTML としてパースする場合どうしようもないので 独自記法でパースするしかないです
ただ作るの面倒だし JSX で書けたらもうそれでいいのに…………書けた気がする
https://jp.vuejs.org/v2/guide/render-function.html
そういえばドキュメントで見た気がする と思って探すとありました
Vue でも React のように createElement 関数を呼び出すことで仮想 DOM の定義を作っています
引数の違いや render する場所のコンテキスト情報を含むので render 関数の引数として createElement 関数を受け取って処理しないといけないとか違いはありますが JSX で対応できるようです
そうなると htm でもいいのでは? と思います
試しに作ってみたところ htm でも使えました
Vue の場合は React の props の部分が複雑になって色々種類があります
今回はとりあえず試しに使う分に必要な一部機能だけで実装してます
「@」 から始まる属性をイベントリスナの定義として class 属性を class 定義として 残りは普通の属性と設定するようにしました
<!doctype html>
<script type="module">
import Vue from "https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.esm.browser.js"
import htm from "https://cdn.jsdelivr.net/npm/htm@3.0.4/dist/htm.module.js"
const html = h => htm.bind(
(type, attributes, ...children) => {
const on = {}
const attrs = {}
let cls = null
for (const [key, value] of Object.entries(attributes ?? {})) {
if (key === "class") {
cls = value
} else if (key.startsWith("@")) {
on[key.slice(1)] = value
} else {
attrs[key] = value
}
}
return h(type, { attrs, on, class: cls }, children)
}
)
new Vue({
el: "#app",
data: {
text: "abc",
},
methods: {
reverse() {
this.text = this.text.split("").reverse().join("")
},
},
render(h) {
return html(h)`
<div>
<p class=${{red: this.text === "abc", blue: this.text === "cba"}}>
${this.text}
</p>
<button @click=${this.reverse}>BUTTON</button>
</div>
`
},
})
</script>
<style>
.red { color: red; }
.blue { color: blue; }
</style>
<div id="app"></div>
変換処理は上の方の html 関数の定義のところで 使うのは render メソッドの中です
htm.bind に指定している関数はこうなってます
(type, attributes, ...children) => {
const on = {}
const attrs = {}
let cls = null
for (const [key, value] of Object.entries(attributes ?? {})) {
if (key === "class") {
cls = value
} else if (key.startsWith("@")) {
on[key.slice(1)] = value
} else {
attrs[key] = value
}
}
return h(type, { attrs, on, class: cls }, children)
}
見ての通り 「@ から始まってたら on に追加する」 というのを自分で処理します
ということは @ よりも # のほうが好きだなとかそういう好みがあれば好きにカスタマイズできます
.vue ファイルは HTML Imports に近くて好きでしたが htm の書き方のほうが使いやすそうなので .js 形式にしようと思います
.js 形式でも render 関数を上にもってくるとだいたいの見た目は一緒ですし
普通の .vue の場合
<template>
<div></div>
</template>
<script>
export default {
data: () => ({}),
methods: {},
}
</script>
htm を使った .js の場合
const render = h => html(h)`
<div></div>
`
export default {
data: () => ({}),
methods: {},
render,
}
デメリット
htm 使えば完璧というわけでもなく デメリットもありますv-if などの制御構文の属性は createElement に渡しても意味がないので分岐や繰り返しは自分で処理しないといけません
React で JSX や htm 使うのと同じ感じです
React のと一緒ならまぁいいかなくらいではありますが 上の方に書いたようなタグで処理できるほうが好きだったので せっかく作ったのが使えないのは残念です
もう一つは React より Vue のほうが良いと思うところでもあった v-model が使えなくなります
こういう directive 系は render を使うなら全部でやらないといけなくなるんですよね
と言っても v-model の機能なら htm.bind に渡す関数内に書いてしまえばテンプレートの各 input に書く必要はありません
if (type === "input" && attributes["v-model"]) {
const prop = attributes["v-model"]
on.input = eve => vm[prop] = eve.target.value
domProps.value = vm[prop]
}
こういうのを追加します
input タグで v-model 属性があるときに DOM への値の設定と input イベント時に data 側への反映をします
上の方に書いた例だと htm.bind 内で render 関数内の this へアクセスできないので 「html(h, this)」 のように追加で渡してそれを vm という引数で受け取っています
このコードはあくまで input タグの value のみを対象にしているので checkbox や select や textarea など実際に使えるものにするにはもっと複雑になります