JSXを扱うライブラリを作ってみよう
ReactなどのライブラリはJSXを使ってHTMLを構築できます。
JSXを利用するプログラムを書いてJSXへの理解を深めてみます。
JSXとは?
JSXとは、JavaScript内でHTMLやXMLのような記法が利用できるようになるJavaScriptの拡張構文のことです。
以下のような記法が実現できます。
const html = (
<div attribute="hoge">
<span>Foo</span>
<span>Bar</span>
</div>
);
JSXはJavaScriptの構文ではないため、そのままでは実行できません。そこで、BabelやTypeScriptなどのトランスパイラはJavaScriptに変換するときに、以下のように変換します。
const html =
React.createElement("div", { attribute: "hoge" },
React.createElement("span", {}, "Foo"),
React.createElement("span", {}, "Bar"),
);
つまり、JSX記法は、React.createElementを呼び出す糖衣構文ということです。
React.createElementを自作したらよいのでは?
そういうことです。先ほど例に示したトランスパイル後のコードが動くようにコードを書いてあげれば、ReactをインストールしなくてもJSXを使ったアプリケーションを作ることができます。
やってみましょう。
上記コードを実行した結果が、こちらです。
最後の行でconsole.logしていますが、きちんとオブジェクトが出力されています。これはめちゃくちゃシンプルな例ですが、仮想DOMと言います。
{ tag: "div", attributes: { class: "foo" }, children: [ "hogehoge" ] }
React.createElementが出力したオブジェクトをHTMLにしよう
上で自作したReact.createElementは、以下のような仮想DOMを出力します。
{ tag: "div", attributes: { class: "foo" }, children: [ "hogehoge" ] }
これをHTMLに変換できれば、JSXを使えるようになったと言っても過言ではありません。
やってみましょう。
とてもシンプルですが、これで以下のような出力を得ることができるようになりました。
render(
<div>
<h1>Hello World</h1>
<p>This is a paragraph</p>
</div>
);
// <div><h1>Hello World</h1><p>This is a paragraph</p></div>
さらに、これから以下の機能を作っていきます。
- attributeに対応する
- コンポーネントを定義できるようにする
attributeに対応しよう
基本的にはattributeは難しくありません。オブジェクトのキーと値を name="value"
という形式に変換するだけです。関数名を書いただけで、Copilotが実装してしまいました(こういうのは、自分で手を動かすことで学びがあるものだと思うので、できる限り手書きするのがよいとは思います。)。
実行すると、以下の出力が得られました。
render(
<div class="foo bar">
<h1 data-testid="hoge">Hello World</h1>
<p>This is a paragraph</p>
</div>
);
// <div class="foo bar"><h1 data-testid="hoge">Hello World</h1><p>This is a paragraph</p></div>
コンポーネントを定義できるようにしよう
これまでは、render()
関数に渡せるのはただのシンプルなJSXでした。
しかし、ちゃんとしたアプリケーションを作れるようにするには、コンポーネント分けは必須の機能です。コンポーネントを定義できるようにしましょう。
React.createElementの役割は、コンポーネントを定義できるようになったとしてもやることは変わりません。仮想DOMを出力するだけです。
divやh1の代わりにコンポーネントをrenderする場合、React.createElementの第一引数(tag)にコンポーネントの関数が渡されます。React.createElementの型定義は以下のように変わります。
+ type FunctionComponent<T = {}> = (props: T, children: MyElement[]) => MyElement;
const React = {
+ createElement(tag: string | FunctionComponent, attributes: Record<string, any>, ...children: MyElement[]): MyElement {
- createElement(tag: string, attributes: Record<string, any>, ...children: MyElement[]): MyElement {
...
したがって、関数コンポーネントの中身を仮想DOMに変換したければ、関数を実行すればよいのです。
+ if (typeof tag === "function") {
+ return tag(attribute, children)
+ }
return ...
実行すると、以下の出力が得られます。
const Component: FunctionComponent<{ foo: string, bar: string }> = ({ foo, bar }) => {
return (
<div>
<h2>{foo}</h2>
<p>{bar}</p>
</div>
)
}
const App: FunctionComponent = () => {
return (
<div class="foo bar">
<h1 data-testid="hoge">Hello World</h1>
<p>This is a paragraph</p>
<Component foo="Hello" bar="World" />
</div>
)
}
render(<App />);
// <div class="foo bar"><h1 data-testid="hoge">Hello World</h1><p>This is a paragraph</p><div><h2>Hello</h2><p>World</p></div></div>
ReactDOMのrender関数について
今回は例として、文字列を出力する関数を作りました。
ReactDOMのrender関数は、文字列ではなくDOMを構築しているという違いがあります。
Reactのようなライブラリが作りたければ、文字列ではなくDOMを構築するプログラムを書けばよいということです。
まとめ
JSXを使ったライブラリを作るのは簡単。
ただし、実用に耐える仮想DOMを作るのはめっちゃ大変。
さらに、実用に耐えるrender関数を作るのもめっちゃ大変。
でも、作ることができればフロントエンドの理解がより深まります。
もし、よければ作ってみてください。
最終的な成果物はこちらです。
以上です。よろしくお願いします。
Discussion