1 of 41

JSX hacks

汎用木構造データ生成機としてのJSX�- TypeScriptでJSXをHTML以外に活用する -

Node学園 33時限目

フューチャー(株)渋川よしき

2 of 41

お前誰よ

渋川よしき� 本田技術研究所:〜2010年12月� DeNA:〜2017年8月� フューチャー(株):2017年9月〜� 三女の父��著書� つまみぐい勉強法(共著)、� Real World HTTP、Mithril、� Goならわかるシステムプログラミング��翻訳� エキスパートPythonプログラミング、� ソフトウェア開発スクラム、� ポモドーロテクニック入門、� アート・オブ・コミュニティetc�

好きな言語� JavaScript®️ / Go / Python��プログラミング以外� インラインスケート@光が丘公園��アカウント� github.com/shibukawa� twitter.com/shibu_jp�

2

3 of 41

4 of 41

有名企業のお客様のシステムを多数手がけています

  • ABCマート様の販売スタッフ向けスマフォアプリ「s NAVI」を全店リリース
  • アルペン様の次世代物流IT基盤を構築
  • 読売巨人軍様のファンサービス戦略を実現する会員管理システムを構築
  • 毎日新聞様の紙・デジタルを連動させる新基幹システム
  • 不二家洋菓子店様のタブレットを使った新POSシステム
  • 広島銀行様のITコストの適正化
  • トマト銀行様の渉外融資システムのAWS移行
  • 佐川急便様の配送伝票入力業務の自動化に向けたAIシステム開発
  • 千葉銀行様の新営業・融資システムの開発開始
  • あとは東京カレンダーとか、コードキャンプ、TrexEdgeが関連会社です

弊社プレスリリースサイトから抜粋

https://www.future.co.jp/press_room/

5 of 41

フューチャーをざっくり言うと

  • 二人のエンジニアが創ったITコンサル会社
  • コンサルから実装、運用まで全部やる
  • 中立(ニュートラル)のポジション
  • 「ないものはつくる」の精神

2016年4月1日に持株会社体制に移行

CONFIDENTIAL

Copyright ©2017 by Future Architect, Inc. Japan

5

6 of 41

フューチャーで有名なのは

  • 脆弱性検知ツールのVuls
  • Software Designでも最近何度か特集されています。

https://github.com/future-architect/vuls

6

7 of 41

Cheetah Grid

  • Google Spreadsheetと同じく、Canvas描画で高速な表描画の�JSライブラリ
  • 100万行でも100ミリ秒�(デモページ見てね)
  • Vue.js用のコンポーネントもあります。
  • MithrilとReactはないのでこれから作ってみようかと。

https://github.com/future-architect/cheetah-grid

7

8 of 41

その他のお知らせ

第2特集

Webに携わるエンジニアの必須科目

ベーシックなJavaScriptをちゃんと身につけよう

  • 第1章:JavaScriptの用途・仕様の変遷…… うひょ
  • 第2章:JavaScriptの特徴を理解してスムーズに習得しよう…… うひょ
  • 第3章:ES2015~2018を踏まえた,今どきのJavaScriptの書き方� …… 渋川 よしき

ちなみに、第一特集も弊社メンバーが関わっています!

8

9 of 41

JSX hacks

汎用木構造データ生成機としてのJSX: JSXをHTML以外に活用する

Node学園 33時限目

フューチャー(株)渋川よしき

10 of 41

JSX

  • Reactのキモ
    • コードの中にHTML(っぽいもの)が書ける
      • HTMLの中にJavaScriptが書けるし、JavaScriptの中にまたHTMLも書ける
      • マークアップとViewのロジックの分離は「関心の分離(Separation of concerns)」ではなくて「技術の分離(Separation of technologies)」だ by Reactの開発者
      • 制御構文などは新しいものを覚えなくても良い
    • 周辺サポートもしっかりしていて、コードハイライトや文法チェックもきちんとされる
      • E4Xで書いてエディタに文句を言われながらXULでUIを作ったようなストレスがない!
      • Babelのローダーを使う以外に、TypeScript単体でも変換をサポートしている
    • 見た目が周りのロジックと違うので、修正箇所を探しやすい
      • 関心は分離されている
      • 言語と近すぎるDSLもまた厳しい
      • 全部がXMLのAntとかに疲れた人にも優しい

11 of 41

JSXについての感想

  • JSXのコンポーネント作成の生産性は高い
    • 要素の集合としてコンポーネントを作る。その要素はコンポーネントとプリミティブの集合。最終的にはプリミティブになっていく
    • Qtで、paintEventをオーバーライドして独自コンポーネントを作ったりしていたのと比べると、親のコードを分離して子供のコンポーネントにする難易度が相当低い�
  • 昔ながらのHTML生成テンプレートよりも構造化されやすい
    • コンポーネントを小さくするのが容易なので、階層が深くても1つ1つはシンプルにする設計になりやすい
    • HTMLだと長文でも頑張ってしまいがち・・・
    • テンプレートの継承をして、一部を置き換えて・・・というのは常に全体を把握していないと実装しにくいので、おっさんには辛い

12 of 41

妄想

  • HTMLは木構造
    • タグの中に子供と属性があって・・・
    • 木構造を効率よく作るためのDSL?
    • というか、コンピュータがあつかうものはほとんどが木構造。ソースコードも、JSONとかのデータも、CSVとかの表も
  • JSXを別の生成用途に作ってみても面白いのでは?
    • 実際、いろいろある。UIとか系は多い
    • 自分なりの木構造データ生成ツールを自由に作れるようになると楽しそう
    • 差分更新などはないので、仮想DOMはいらない。あくまでもJSXのみ
    • 超巨大数千行のswagger.yaml
      • 構造化とかファイル分割を!

13 of 41

JSXを利用するメリット

オリジナル言語

JSX

内部DSL

字句解析

構文解析

意味解析

出力

エラー出力

コードハイライト

コード補完

14 of 41

JSXを使って言語を作るメリット

  • 字句解析、構文解析はそのままおまかせ
  • 必須属性とか親子関係などの状態まではJSXがみてくれる
  • 必須属性がないとか文法エラーはJSXがみてくれる
  • コードハイライトとか入力支援はエディタがやってくれるし、Linterも既存のが使える

ほとんど、出力部分だけ実装すればOK

15 of 41

JSXの動きの仕組み(1): タグの変換

render() {� return (� <App>� <>� <Header name="yosuke_furukawa"/>� </>� </App>� );�}

render() {� return (� React.createElement(App, null, � React.createElement(undefined, null,� React.createElement(� Header, {name: "yosuke_furukawa"}� )� )� )� );�}

16 of 41

JSXの動きの仕組み(1): タグの変換

render() {� return (� <App>� <>� <Header name="yosuke_furukawa"/>� </>� </App>� );�}

render() {� return (� React.createElement(App, null, � React.createElement(undefined, null,� React.createElement(� Header, {name: "yosuke_furukawa"}� )� )� )� );�}

タグがReact.createElement呼び出しに変換

タグがReact.createElement呼び出しに変換

17 of 41

JSXの動きの仕組み(1): タグの変換

render() {� return (� <App>� <>� <Header name="yosuke_furukawa"/>� </>� </App>� );�}

render() {� return (� React.createElement(App, null, � React.createElement(undefined, null,� React.createElement(� Header, {name: "yosuke_furukawa"}� )� )� )� );�}

タグがReact.createElement呼び出しに変換

タグ属性は第二引数に(省略時はnull)

18 of 41

JSXの動きの仕組み(1): タグの変換

render() {� return (� <App>� <>� <Header name="yosuke_furukawa"/>� </>� </App>� );�}

render() {� return (� React.createElement(App, null, � React.createElement(undefined, null,� React.createElement(� Header, {name: "yosuke_furukawa"}� )� )� )� );�}

タグがReact.createElement呼び出しに変換

子要素は第三引数以降に(可変長引数)

19 of 41

JSXの動きの仕組み(1): タグの変換

render() {� return (� <App>� <>� <Header name="yosuke_furukawa"/>� </>� </App>� );�}

render() {� return (� React.createElement(App, null, � React.createElement(undefined, null,� React.createElement(� Header, {name: "yosuke_furukawa"}� )� )� )� );�}

タグがReact.createElement呼び出しに変換

Fragment構文の時は先頭がundefinedに

20 of 41

JSXの動きの仕組み(1): タグの変換

  • すべて関数呼び出しに置き換えられる
    • TypeScriptだと、tsconfig.jsonのcompilerOptions.jsx”react”にすると、React.createElement()呼び出しに変換される
    • ”preserve” にすると、そのまま変換せずに出力される
    • compilerOptions.jsxFactoryで関数名を変えられる(Preactで使うには “h”
      • ただし、fragment構文が使えなくなる
  • 第一引数はタグで指定したもの
    • 小文字にすると文字列になる(組み込みのHTMLタグ)
    • 大文字にすると自作のクラスやら関数をそのまま渡せる
  • 第二引数は属性のObject
  • 第三引数以降は子供の要素が入っていく
    • タグ以外の文字列やホスト言語の要素もそのまま渡される

21 of 41

JSXの動きの仕組み(1): タグの変換

  • 子供の要素
    • タグを書くことができる
    • テキストを書くと、文字列になる
      • JSONっぽいものをダイレクトに書いても文字列になってしまうので注意
    • { }でくくると、その中にJavaScriptが書ける
      • 配列、オブジェクトなどを子として返すこともできる(通常のReactではやらない)
      • そのJavaScriptの中にさらにタグを書くこともできる

22 of 41

自作レンダラーを作る

  • React.createElementという名前の関数を自分で作ってみると、JSX構文を利用するコードを自分で作ってみることができる
  • 仮想DOMでなくて、いっぱつ作りきりのレンダラーを作るのは難しくない
  • 本格的に作るならReactFiberReconcilerとかのAPIを使った方が良い
    • 差分検知なども活用できる

23 of 41

JSXの動きの仕組み(2): タグの処理順序

  • 一番親要素のrender()を実行すると、React.createElement()呼び出しがネストされているreturn文が実行される
  • ネストされた関数呼び出しなので、普通のプログラミング言語の特性として内側から順番に処理されていく
  • タグの中にタグのネストがあると、評価順序はちょっと難しい

24 of 41

例: 2つのコンポーネント(Parent, Child)

render() {� return (� <div className="p1">� <Child>� <div className="p2">hello world</div>� </Child>� </div>� );�}

render() {� return (� <div className="c1">� {this.props.children}� </div>� );�}

25 of 41

等価な式

  • Fiberなら親のコンポーネントをp2→p1を評価した後に、Childのc1を評価?
  • 簡易的な自作をするならParentのrender() { p2→Childのrender() { c1} →p1 }になるように実装するのが簡単っぽい
    • render()の結果をトラバースし、タグが含まれて入ればインスタンス化してrender()呼び出し

render() {� return (� <div className="p1"><div className="c1"><div className="p2">hello world</div></div></div>� );�}

26 of 41

お題1: JSONを作ってみる

  • render()が普通のJSオブジェクトを返す
    • JSオブジェクトをトラバースして、中にコンポーネントがあったら、コンポーネントのrender()を呼び出し、その結果をトラバースし・・・という構成にする
    • タグと文字列だけの通常のJSXのrender()よりも少し煩雑
    • 親のrenderが処理し終わったらJSONができているはず!

27 of 41

const React = {� createElement<P extends {}, T extends Component<P, C>, C>(� component: new (props?: P) => T,� props?: P,� ...children: C[]� ): T | C[] {� let childrenValue: C | C[] | undefined;� if (children.length === 1) {� childrenValue = children[0];� } else if (children.length > 1) {� childrenValue = children;� }� if (component === undefined) {� // <> </> fragment syntaxreturn children;� } else {� const instance = new component(� Object.assign({}, props, { children: childrenValue })� );� return instance;� }� }�};��export default React;

28 of 41

type PropsWithChildren<P extends {}, C> = P & { children?: C | C[] };��interface NodeArray extends Array<Node> {}�type Text = string | number;�type Child = Text;�export type Node = Child | {} | NodeArray | boolean | null | undefined | Component;��export abstract class BaseComponent {� public componentWillMount(): void {}� public componentDidMount(): void {}� public abstract render(props: any): any;�}��export abstract class Component<P extends {} = {},C = {}> extends BaseComponent {� get props(): PropsWithChildren<P, C> {� return this._props;� }�� constructor(private _props: PropsWithChildren<P, C>) {� super();� }�� public abstract render(props: PropsWithChildren<P, C>): any;�}

29 of 41

function renderTraverse(instance: any): any {� if (isLiteral(instance)) {� return instance;� } else if (instance instanceof Component) {� instance.componentWillMount();� const vdom = instance.render(instance.props);� const result = renderTraverse(vdom);� instance.componentDidMount();� return result;� } else if (Array.isArray(instance)) {� return instance� .map(node => { return renderTraverse(node); })� .filter(node => { return node !== undefined && node !== null; });� } else if (typeof instance === "object") {� const result: { [key: string]: any } = {};� for (const [key, value] of Object.entries(instance)) {� const entry = renderTraverse(value);� if (entry !== undefined) {� result[key] = entry;� }� }� return result;� }�}

30 of 41

export function render(component: Component): any {� return renderTraverse(component);�}��function isLiteral(source: any): boolean {� return (

source === null ||

source instanceof Date ||

typeof source === "number" ||typeof source === "string" ||

typeof source === "boolean"

);�}

31 of 41

雑に作った第1弾:JSON

class Child extends Component<{name: string}> {� public render(): Node {� return { type: "child", name: this.props.name };� }�}��class Parent extends Component<{name: string}, Child> {� public render(): Node {� return { type: "parent", name: this.props.name, children: this.props.children};� }�}��class Root extends Component {� public render(): Node {� return (<Parent name="test1"><Child name="test2" /></Parent>);� }�}��render(<Root />)

32 of 41

雑に作った第1弾: JSON

  • とりあえずタグのネスト+JSONっぽいものは出るようになった
    • js-yaml使えばYAMLになるし、@iarna/toml使えばTOMLになる!便利!�
  • 作ってみると、ユースケースを満たすためにいろいろな機能が欲しくなる
    • docker-composeのservicesのキーは、所有者としてはその子の各サービスのコンポーネントで設定したいが、そのままだとservicesを定義する側が責任を持つことになってしまう�
  • 今の構造のままJSX化しても記述量が増えるだけ
    • 構造化データを使っているものは記述量としてはキー名以外の重複は少ない
    • 例えば、docker-composeなら、<service><volume><database></service>のように依存関係をネストで表現し、それをdocker-composeの構造で出力するなどの構造変換の提供とかをするなら価値観が出せそう

33 of 41

雑に作った第2弾:Excel

render(� <Excel filename="sample.xlsx">� <Sheet name="test">� <Cell>{[[1, 2, 3, 4], [5, 6, 7, 8]]}</Cell>� <Cell origin={3}>� {[� ["あのイーハトーヴォのすきとおった風、"],� ["夏でも底に冷たさをもつ青いそら、"],� ["うつくしい森で飾られたモリーオ市、"],� ["郊外のぎらぎらひかる草の波。"]� ]}� </Cell>� </Sheet>� </Excel>� );

34 of 41

35 of 41

雑に作った第2.1弾:Excel方眼紙

render(� <Excelfilename="sample2.xlsx"template={join(__dirname, "hougan.xlsx")}� >� <Sheet name="申請書">� <Cell hougan={12} origin="C3">� 東京都千代田区丸の内1丁目9−2� </Cell>� <Cell hougan={12} origin="C6">� リクルートテクノロジーズ� </Cell>� <Cell origin="C8">古川陽介</Cell>� </Sheet>� </Excel>� );

36 of 41

37 of 41

雑に作った第2弾:Excel

  • Excelもワークシート、行と列を持った3次元構造データと言える
  • 表をそのまま木構造にするのではなくて、必要なところだけoriginを指定して書けるようにしたら便利
  • 既存のファイルをテンプレートとして任意のセルに値を入れられるようにしたら、意外と便利そう?
    • ついでに、1文字1セルの方眼紙にも対応してみた。
    • 連結セルにも入れられる
    • テキストボックスの扱い方はまだわかってないので、神Excelのすべてはまだ退治できそうもない
    • うちの会社では見かけたことはないけど。

38 of 41

おまけ: エラーメッセージ用にタグ定義箇所を取得

import { get as getStackTrace } from "stack-trace";��const React = {� createElement(...) {� const stackTrace = getStackTrace();� const filePath = relative(process.cwd(), stackTrace[1].getFileName());� const funcName = stackTrace[1].getFunctionName();� const columnNumber = stackTrace[1].getColumnNumber();� const lineNumber = stackTrace[1].getLineNumber();� const position = `${funcName} in ${filePath} (${lineNumber}:${columnNumber})`;� }�}

39 of 41

まとめ

  • 構造化データを扱うDSLとしてのJSX
    • エディタサポートまで含めて得られるので、言語をゼロから作るのと比べて圧倒的に楽
      • ループとか条件分岐も利用できる
    • 親子関係とか属性といった木構造用構文も最初からあるので内部DSLよりも少し楽
    • 文法エラーもリアルタイムで見えるし、処理時にエラー箇所をユーザーに提示するのも簡単
  • 実際に作ってみて
    • 既存の構造化データと1:1に変換するコンポーネントを作ってもうまみは少ない
      • タイプ数はむしろ増える可能性
    • データ構造とは違う階層構造のコンポーネントを提供してあげるのも良さそう
  • コードはそのうち公開するかも?
    • 雑に一度作ってみて、無駄な部分もいろいろ見えたのでスクラップアンドビルドしたい

40 of 41

最後に

41 of 41

Real World HTTPミニ版

  • 書籍版(360ページ)から、HTTPの基礎的な内容の部分を取り出し(100ページほど)、50ページぐらい書き足したバージョン
  • 社内教育とかに使ってもらえるように著作権法よりも広い引用を許可しています
  • 近日中に公開予定�