◆ JSX 嫌い
  ◆ JavaScript に HTML を構文的に混ぜないでほしい
  ◆ ビルド必要で試しづらい
◆ lit-html などのようなテンプレートストリング機能で十分そう
  ◆ ⇨つくってみた

JavaScript の話です

JavaScript 以外をメインにするとか言っておきながら なんか Web なものを作る事になったのでクライアントサイド的にはなんだかんだ JavaScript になりそうです(Elm/TypeScript などはさすがに面倒ですし)
なので サーバサイドとして Node.js 以外を使うようにするのがありかも?

そう考えつつも今回はクライアントサイドの話です

最近の Framework

たまには Framework とかどうだろうといくつか眺めていて そういえば最近は新しいの増えてなくて人気もほぼ決まった感じかなと思いました
Native 対応とかもあってほぼ React 一強な感じがします
ただ 完全 React 対応も大変みたいで これまでのを元に部分的にやりたいなら Vue もいいとか
一応 Angular もあわせて三大といわれることがあるみたいですけど Angular はここ最近全然目にしません
日本語だけでなく英語のページでもです
大規模に向いてるらしいですし 大きな企業が作るサイトで使われてて 個人ではあまり使われないから紹介記事とは少なめなんでしょうか
最近マイクロソフトは React とか聞いたような聞かなかったような

React の JSX

とりあえず React って人気ですし 一度は使ってみようとか思いつつ使ってませんでした
とりあえず 公式のチュートリアルでも と思ってページを開くと

JSX

そうでした
こいつです
こいつのせいで 毎回使ってみようとしては敬遠してたんです

これがあるので Babel の変換が必須になって気軽に試しづらいです
ちょっと試すくらいなら CDN から JavaScript ライブラリをロードするだけで済ませたいのに Webpack とか使ってビルドなんて準備がいるのはあんまり気が乗りません

ですが ちゃんとドキュメントを読んでみると 別に必須ではなくて普通に JavaScript だけでも JSX の部分を書けるようです
まぁ Babel でも変換してるだけなのでできるとは思ってましたが 内部表現的な複雑なことになってると思ってたので意外と単純でした

これが

<div>
<App foo={bar} />
<div className="baz">abc</div>
</div>

こうなります

React.createElement(
"div",
null,
React.createElement(
App,
{foo: bar},
null
),
React.createElement(
"div",
{className: "baz"},
"abc"
)
)

React.createElement を使って 1 つ目の引数にコンポーネントを 2 つ目の引数に属性のオブジェクト 3 つ目以降の引数に子要素を渡します
Babel の REPL で簡単に試せました
https://babeljs.io/repl/

見比べてみると 自分で JavaScript で書くのはあまり見やすくない上に長いです
JSX が使われるわけですね

なんか JSX を見ていると埋め込み部分など lit-html や hyperHTML ぽく感じます
テンプレートストリングにして埋め込み機能でいいんじゃないかと思います
そうすれば JavaScript の構文なので Babel を通さずに HTML 風に書いても React を使えます

JSX alternative

そんなわけで JSX 代わりにテンプレートストリングで使えるものを作ってみました

ライブラリ部分はこれ

const wmap = new WeakMap()
const rp = "RP__10393496__"

function react(tpls, ...values) {
if (!wmap.has(tpls)) {
const t = document.createElement("template")
t.innerHTML = tpls.join(rp).trim()
const updates = []
let vi = 0
const parseChildren = parent => {
return [...parent.childNodes].flatMap(node => {
if (node.nodeType === 1) {
const node_info = { type: "component" }
let component_index = -1
if (node.nodeName === rp) {
component_index = vi++
const update = values => {
node_info.component = values[component_index]
}
updates.push(update)
} else {
node_info.component = node.nodeName.toLowerCase()
}
for (const attr of node.attributes) {
node_info.attributes = node_info.attributes || {}
const attr_name =
attr.name[0] === "!" ? attr.name.slice(1) : attr.name.replace(/-./g, x => x[1].toUpperCase())
if (attr.value === rp) {
const index = vi++
const update = values => {
node_info.attributes[attr_name] = values[index]
}
updates.push(update)
} else if (attr.value.includes(rp)) {
const parts = attr.value.split(rp)
const index = vi
vi += parts.length - 1
const update = values => {
let i = 0
node_info.attributes[attr_name] = parts.reduce((a, b) => a + values[index + i++] + b)
}
updates.push(update)
} else {
node_info.attributes[attr_name] = attr.value
}
}
node_info.children = parseChildren(node)
if (node.nodeName === rp) {
const component_end_index = vi++
updates.push(values => {
if (values[component_index] !== values[component_end_index]) {
throw new Error("Begin and end component are not matched.")
}
})
}
return [node_info]
} else if (node.nodeType === 3) {
if (node.textContent.includes(rp)) {
return node.textContent.split(new RegExp(`(${rp})`)).map(e => {
if (e === rp) {
const node_info = { type: "value" }
const index = vi++
const update = values => {
node_info.value = values[index]
}
updates.push(update)
return node_info
} else {
return { type: "text", text: e }
}
})
} else {
return [{ type: "text", text: node.textContent }]
}
}
})
}
const tree = parseChildren(t.content)
if (vi !== values.length) {
throw new Error("invalid embedding")
}
wmap.set(tpls, { tree, updates })
}

const { tree, updates } = wmap.get(tpls)
for (const update of updates) update(values)

return toReactElement(tree)
}

function toReactElement(tree) {
const root = tree[0]
if (tree.length !== 1 || root.type !== "component") {
throw new Error("Invalid root")
}

const createComponent = node_info => {
const children = node_info.children.map(e => {
if (e.type === "component") return createComponent(e)
if (e.type === "text") return e.text
if (e.type === "value") return e.value
})

return React.createElement(node_info.component, node_info.attributes, ...children)
}

return createComponent(root)
}

これをこういう風に使います

<!doctype html>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.6/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.6/umd/react-dom.development.js"></script>
<div id="root1"></div>
<div id="root2"></div>

<script>
// root1 example
class Hello extends React.Component {
render() {
return react`<div>Hello ${this.props.toWhat}</div>`
}
}

// これと同じ: ReactDOM.render(<Hello toWhat="World" />, document.getElementById("root"))
ReactDOM.render(react`<${Hello} to-what="World"></${Hello}>`, document.getElementById("root1"))

// root2 example
class App extends React.Component {
render() {
return react`<span>${new Date(+this.props.foo).toLocaleString()} | ${this.props.bar}</span>`
}
}

const render = val => {
ReactDOM.render(
react`
<div>
<${App} foo=${val.foo} bar=${val.bar}></${App}>
<div style=${{ color: val.color }} !data-text="color is ${val.color}" class-name="cls">
aaa${[1, 2, 3].map((e, i) => react`<p key=${i}>${e} is ${e % 2 ? "odd": "even"}</p>`)}zzz
</div>
<style>
div.cls::before { content: attr(data-text); display: block; }
</style>
</div>
`,
document.getElementById("root2")
)
}

let i = 0
setInterval(() => {
const color = Math.random()
.toString(16)
.slice(2, 5)
render({ foo: Date.now(), bar: i++, color: "#" + color })
}, 1000)
</script>

root1 のほうには単純に Hello World が表示されます
root2 のほうは 毎秒ごとに更新されて 現在の時刻と再描画回数のカウントが表示されます
あとはランダムに更新される色付きで繰り返し処理で 1~3 までの数字と分岐処理で奇数か偶数かも表示します

DEMO

記法

見たままの単純なもので

react タグ関数を使ったテンプレートストリングを使います
JSX とは微妙に違うので jsx ではなく react って名前にしてます

コンポーネントは文字列ではなく埋め込みで <${App}> のように書きます
こうしないとコンポーネントの関数やクラスの参照を取得できませんので……

属性部分はバリュー側に ${} を使った埋め込みを行います
バリュー全体が ${} の場合はそのままの型で React.createElement に渡されます
バリューの文字列の一部が ${} の場合は文字列として結合されて React.createElement に渡されます

HTML としてパースするので JSX のような大文字を使えません
自動で小文字変換されてしまうので元が大文字であったことの判断がつかないです
なのでテンプレート中では data-xxx みたいに - 区切りにして書き React.createElement に渡すときには camelCase に戻します
本当の data-xxx みたいな - を使って区切りたいものまで camelCase 化されるのでそのまま維持したい場合は 属性名を ! から始めます
!data-xxx は data-xxx として渡されます
コンポーネントが React コンポーネントか DOM 標準かで分けてもよさそうでしたが JSX や React の知識がほぼない状態なので例外がないとも言い切れずこうしてます
React を始めるために JSX 代替ライブラリを作ってる状態ですからね

デメリット

作っていて たぶん同じこと思って作ってる人は他にいてクオリティ高いツールがちゃんとありそうと思ったので 一部面倒そうな機能は入れてません

一応ドキュメントで書かれている コンポーネント名は大文字から始めないといけないとか components[name] のような式が使えないなどの制限は ${} が式なので気にせず使えます
{...val} は対応してません
プロパティをちゃんと書いたほうがわかりやすそうなのと 展開した場合に順序によって上書きされてほしいものとかの対応が面倒だったのです

それと テンプレートストリングの機能を使って最適化してるので 同じテンプレートを何度使ってもキャッシュが利用されるので 変更は最小限で済みます
HTML としてパースして変更箇所を探したりは最初の 1 回のみです
それ以降はほぼ動的に変更される箇所ごとにプロパティ代入を 1 回を行う関数を呼び出すくらいです
しかし それはあくまで React.createElement を行うためのツリー構造の構築までです

react`<${Hello} to-what="World"></${Hello}>`

というコードの場合は ツリーは⇩となります

[{
attributes: {toWhat: "World"}
children: []
component: class Hello
type: "component"
}]

このコードの場合は

react`
<div>
<${App} foo=${100} bar=${200}></${App}>
<div style=${{ color: "#ff0000" }} !data-text="color is ${"#ff0000"}" class-name="cls">
aaa${null}zzz
</div>
<style>
div.cls::before { content: attr(data-text); display: block; }
</style>
</div>
`

ツリーは⇩です

[{
children: [
{type: "text", text: "↵ "}
{
attributes: {foo: 100, bar: 200}
children: []
component: class App
type: "component"
}
{type: "text", text: "↵ "}
{
attributes: {className: "cls", style: {color: "#ff0000"}, data-text: "color is #ff0000"}
children: (3) [
{type: "text", text: "↵ aaa"}
{type: "value", value: [ReactElement {}, ReactElement {}, ReactElement {}]}
{type: "text", text: "zzz↵ "}
]
component: "div"
type: "component"
}
{type: "text", text: "↵ "},
{
children: [
{type: "text", text: "↵ div.cls::before { content: attr(data-text); display: block; }↵ "}
]
component: "style"
type: "component"
}
{type: "text", text: "↵ "}
]
component: "div"
type: "component"
}]

このデータから React.createElement を使って React の DOM データを作るわけですが ここ自体を最小限の更新にする方法は(たぶん)ないため毎回作成です
せっかく更新時の変更を最小限にしていても 結局 React 用の DOM を作ってそれを比較という処理になるのですから 効率的ではありません

これなら最初から lit-html や hyperHTML でいいように思います

結局使わない

こんなの作ったはいいけど 結局 Framework は使う予定ないんですけどね
たいていの Framework ってコンポーネントな機能がないからそれを補うのと jQuery でやってたような DOM の制御だと複雑になると手におえないのでそれを管理するのが目的です

出始めた頃は WebComponents が現実的に使える状況じゃなかったからで 今はもう WebComponents が出てしばらく経ちましたし IE 対応でもなければ WebComponents で十分だと思います
一部関連機能で動かないとかはありますが 基本的なところは Chrome/Firefox/Safari すべてで動きます
IE も polyfill 入れれば動くみたいなことどこかで見たので多分動くのでしょう(未確認)

また DOM 制御も hyperHTML や lit-html などの DOM を更新してくれるだけのライブラリを使うことができます
lit-element なんかは WebComponents の CustomElements として提供されてるので 特に向いてます

そういうわけで これまでなにかの Framework でいろいろ再利用できるパーツを作ってる人でもないなら新しく始めるのは WebComponents で良いと思います

追記

JSX ありとなしを比べてみたときは なしだと結構長いし見づらいしと思ったのですが React.createElement を 1 文字の変数で呼び出せるようにして JSX に近いフォーマットで書けば それほど長くありませんでした

<div>
<App foo={bar} />
<div className="baz">abc</div>
</div>

const c = (...a) => React.createElement(...a)

c("div", null, ...[
c(App, {foo: bar}),
c("div", {className: "baz"}, "abc"),
])

これだとそんなに大変でも見にくくもないのでテンプレートストリングで JSX 風に HTML を書いてパースなんてしなくても このままで良い気がしました