読者です 読者をやめる 読者になる 読者になる

レガシーコード生産ガイド

私に教えられることなら

ParenscriptからReact.jsを触ってみる

CommonLisp Parenscript 学習記録

初めてReact.jsを触ってみた記録。LISPJavaScriptを書くのだ、と決意が固まってきたので、いきなりParenscriptで書き始める。

Reactを読み込む

require系のライブラリを使わずに、window.Reactを使う場合。

Parenscriptでは、シンボルは小文字に、チェインケース(ps-awesome-function)ならキャメルケース(psAwesomeFunction)に自動的に変換されるので、 window.Reactを扱うときには(chain window React)だとwindow.reactに変換されてしまう。

なので(defvar react (getprop window "React"))とgetpropを使わないと取得できない。

最初のコンポーネント

まずここのチュートリアルをParenscriptで書いてみる。

http://facebook.github.io/react/docs/tutorial.html#your-first-component

JavaScriptXMLを埋め込めるらしいJSXシンタックスだけど、Parenscriptと混ぜるの面倒そうだし、いずれマクロでどうにかした方がもっと楽しくかけるだろうから、素のJavaScriptの方を使う。

表示用のindex.htmlはこんなカンジで

<style>
  .hello { font-size: 72px; color: #036; }
</style>
<body>
<script src="http://fb.me/react-0.12.2.js"></script>
<div id="content"></div>
<script src="./app.js"></script>
</body>

元コードをとりあえず眺めてみる。

var CommentBox = React.createClass({displayName: 'CommentBox',
  render: function() {
    return (
      React.createElement('div', {className: "commentBox"},
        "Hello, world! I am a CommentBox."
      )
    );
  }
});

React.render(
  React.createElement(CommentBox, null),
  document.getElementById('content')
);

CommentBoxでReactの「クラス」を作り、React.renderでその「クラス」を生成して指定したDOMエレメント内に配置しているようだ。 (この「クラス」というのがコンポーネント、つまりVirtualDOMとやらの事なんだろうか?調べてみよう。)

displayNameは http://facebook.github.io/react/docs/component-specs.html#displayname によると

The displayName string is used in debugging messages. JSX sets this value automatically;

ということで変数名?をJSXが自動的に付加してるみたいだ。

さて、Parenscriptで書いてみる。ライフゲームを書いたときに、chainを->でショートカットしてたんだけど、@というマクロがあることに今更気づいた。

  (defvar react (getprop window "React"))

  (defvar hello 
    ((@ react create-class)
     (create
      display-name "hello"
      render (lambda ()
               ((@ react create-element) "div" (create class-name "hello") "hello!")))))

  ((@ react render)
   ((@ react create-element) hello nil)
   ((@ document get-element-by-id) "content"))

psの出力結果をapp.jsに保存して確認。hello!と表示されている。

コンポーネントの合成

http://facebook.github.io/react/docs/tutorial.html#composing-components

まずはJSXを見てみる。

var CommentList = React.createClass({
  render: function() {
    return (
      <div className="commentList">
        Hello, world! I am a CommentList.
      </div>
    );
  }
});

var CommentForm = React.createClass({
  render: function() {
    return (
      <div className="commentForm">
        Hello, world! I am a CommentForm.
      </div>
    );
  }
});

var CommentBox = React.createClass({
  render: function() {
    return (
      <div className="commentBox">
        <h1>Comments</h1>
        <CommentList />
        <CommentForm />
      </div>
    );
  }
});

このJSXはどう変換されているのだろうか? http://facebook.github.io/react/docs/jsx-in-depth.html#namespaced-components を見ると、これが

var Form = MyFormComponent;

var App = (
  <Form>
    <Form.Row>
      <Form.Label />
      <Form.Input />
    </Form.Row>
  </Form>
;)

次のようになるらしい。

var App = (
  React.createElement(Form, null, 
    React.createElement(Form.Row, null, 
      React.createElement(Form.Label, null), 
      React.createElement(Form.Input, null)
    )
  )
);

第二引数はattributesで、それ以降の引数は全て子要素かな。

それを踏まえてParenscriptで書いてみる。

  (defvar react (getprop window "React"))

  (defvar hello-sub
    ((@ react create-class)
     (create
      display-name "hello-sub"
      render (lambda ()
               ((@ react create-element) "div" (create class-name "hello-sub") "hello-sub!")))))

  (defvar hello 
    ((@ react create-class)
     (create
      display-name "hello"
      render (lambda ()
               ((@ react create-element) "div" (create class-name "hello") nil
                ((@ react create-element) hello-sub nil))))))

  ((@ react render)
   ((@ react create-element) hello nil)
   ((@ document get-element-by-id) "content"))

これでhello-sub!が表示された。

短く書きたい

とりあえずマクロ使って、create-classのところを

  (def-component hello 
    (hello-sub (class-name "sub") 
      (hello-grandchild)
      (hello-grandchild))
    (hello-sub (hoge-attr "fuga")
      (div () "hello! grandchild")))                 

みたいに書けるようにすると楽そうだ。

しかし一番の目玉?の変数のバインド?でどう書くようになるのか検討がつかないので、それがわかるまでは我慢しよう。

我慢できなかった

まずpsの前で補助関数を定義。

コンポーネント定義の際に*react-my-components*内にコンポーネント名を追加する。それを調べて、React.createElementにタグ名を渡すか、コンポーネント(変数名)を渡すか切り替える(expand-tagname)。子要素は文字列はそのまま渡し、それ以外はmapcarで再帰する(expand-components)。

(eval-when (:compile-toplevel :load-toplevel :execute)
  (defparameter *react-my-components* ())

  (defun regist-my-component (name)
    (push name *react-my-components*))

  (defun my-component? (name)
    (find name *react-my-components*))

  (defun expand-tagname (name)
    (if (my-component? name)
        name
        (string-downcase (symbol-name name))))

  (defun expand-components (components)
    (let* ((name  (first  components))
           (attrs (second components))
           (children (cddr components)))
      `((@ react create-element) ,(expand-tagname name) (create ,@attrs)
        ,@(if (null children) nil
              (if (stringp (car children))
                  children
                  (mapcar #'expand-components children)))))))

ps内

  (defvar react (getprop window "React"))

  (defmacro def-component (name &body components)
    (regist-my-component name)
    `(defvar ,name
       ((@ react create-class)
        (create
         display-name ,(string-downcase (symbol-name name))
         render (lambda()
                  ,@(mapcar #'expand-components components))))))

  (def-component hello-sub
      (div () "hello-sub"))

  (def-component hello
      (div (class-name "hello") 
           (hello-sub)))

  ((@ react render)
   ((@ react create-element) hello nil)
   ((@ document get-element-by-id) "content"))

ウオー!

マクロ前

  (defvar hello-sub
    ((@ react create-class)
     (create
      display-name "hello-sub"
      render (lambda ()
               ((@ react create-element) "div" (create class-name "hello-sub") "hello-sub!")))))

  (defvar hello 
    ((@ react create-class)
     (create
      display-name "hello"
      render (lambda ()
               ((@ react create-element) "div" (create class-name "hello") nil
                ((@ react create-element) hello-sub nil))))))

マクロ後

  (def-component hello-sub
      (div () "hello-sub"))

  (def-component hello
      (div (class-name "hello") 
           (hello-sub)))

LISP、超楽しい。

広告を非表示にする