tag:crieit.net,2005:https://crieit.net/tags/JSX/feed 「JSX」の記事 - Crieit Crieitでタグ「JSX」に投稿された最近の記事 2019-08-20T17:41:42+09:00 https://crieit.net/tags/JSX/feed tag:crieit.net,2005:PublicArticle/15338 2019-08-20T17:41:42+09:00 2019-08-20T17:41:42+09:00 https://crieit.net/posts/HTML-JSX HTMLのノードを直接生成するJSXファクトリを簡単に書いてみた <p>JSXって知っていますか? JSXとは、Reactなどに使われているXML(HTML)とjavascriptの混在記法です。<br /> そして、JSXを内部的に処理する関数をJSXファクトリといいます。<br /> これは、BabelやTypeScriptコンパイラの設定で自由に変更可能です。<br /> ところでWeb Componentsという、新しいHTMLエレメントを自由に作れるjavascriptのAPIが存在するのはご存じでしょうか。</p> <p>これを使えば、Reactのようなことがネイティブのjavascriptでできるのではないだろうか?そう考えたのでやってみることにしました。</p> <p>まずはJSXファクトリの仕様から説明します。<br /> JSXファクトリは以下のようなシグネチャを持つ関数であれば何でも構いません。</p> <pre><code class="javascript">/** * @param element 文字列または関数またはクラス。文字列の場合はタグの名前、クラス・関数の場合はタグが表すコンポーネント。 * @param props 要素の属性。オブジェクトで渡す。存在しない場合はnull。 * @param children 可変長引数で、現在の要素の子要素を渡す。 */ function JSXFactory(element, props, ...children) {} </code></pre> <p>そしてこれは重要なことなのですが、この関数が返す値や動作は何でもよいのです。<br /> JSXとは、単なる糖衣構文でしかありません。つまり、その動作は実装した人にゆだねられます。<br /> 以下に、JSXの変換例を示します。</p> <pre><code class="jsx">import React from 'react'; let node = <div className="hoge">This is child text.</div>; </code></pre> <p>これは以下のようなjavascriptに変換されます。</p> <pre><code class="javascript">import React from 'react'; let node = React.createElement('div', {'className': 'hoge'}, 'This is child text'); </code></pre> <p>そしてこの、React.createElementにあたる関数はtsconfig.json内で以下のように設定できます。</p> <pre><code class="json">{ "compilerOptions": { "jsx": "react", "jsxFactory": "myCreateElement" } } </code></pre> <p>この場合はjsxFactoryにmyCreateElementを設定しているため、React.createElement関数の代わりにmyCreateElementが使われます。<br /> さて、これらの仕様を使って、普通のHTMLノードを生成するJSXファクトリを作ってみたいと思います。<br /> まずは型の宣言から。</p> <pre><code class="typescript">declare namespace JSX { export interface JSXElement<P = {}> {} export interface IntrinsicElements { [elemName: string]: any; } export type GlobalAttributes = EventAttributes | ElementAttributes; export type ElementAttributes = /* HTMLノードのグローバル属性(略) */ export type EventAttributes = /* イベント属性(略) */ } </code></pre> <p>シンプルに行きましょう。<br /> 組み込みの要素の型をいちいち定義していたら日が暮れます。<br /> なのでanyで誤魔化しました。</p> <p>続いては本題のJSXファクトリの実装です。</p> <pre><code class="typescript">import './types'; type PropType<K> = { [P in JSX.GlobalAttributes | keyof K]: any }; export function createElement<K, E extends JSX.JSXElement<K>>( element: E | string, props: PropType<K>, ...children: any[] ): Element { let elem: Element; if (typeof element === 'string') { elem = document.createElement(element); } else { elem = document.createElement( (element as any).name.replace(/(?!^)([A-Z0-9])/g, '-$1').toLowerCase() ); } Object.keys(props || {}) .filter(it => props.hasOwnProperty(it)) .forEach(it => elem.setAttribute(convertPropName(it), props[it as keyof PropType<K>]) ); children.forEach(it => !(it instanceof Element) ? it.hasOwnProperty('render') && typeof it.render === 'function' ? elem.appendChild(it.render()) : elem.append(it.toString()) : elem.appendChild(it) ); return elem; } function convertPropName(name: string): string { switch (name) { case 'className': return 'class'; case 'htmlFor': return 'for'; default: return name; } } </code></pre> <p>やってること自体はきわめて単純。<br /> <code>document.createElement</code>関数でノードを作り、引数に従って属性や子要素を追加しているだけです。<br /> JSXではclassとforがそれぞれclassNameとhtmlForになるため、convertPropName関数はそれを変換します。</p> <p>にしてもJSXファクトリ、かなり面白いですね。<br /> これ使えばいろいろできそうな予感。<br /> React以外でも活用の目はありそうです。</p> frodo821