React+expressでisomorphicなWebページを作ってみた

Virtual DOMは速い以外にもDOMを文字列としてレンダリングできるという利点があります。

これを利用するとモダンなブラウザなどではクライアント側でJSをバリバリ動かして、古いIEやクローラー等にはサーバー側でレンダリングしたものを返したりといったことができます。

そういうわけで練習がてらReact+expressで試しにブログ風のウェブページを作ってみました([ukyo-react-sample/blog at master · ukyo/ukyo-react-sample](https://github.com/ukyo/ukyo-react-sample/tree/master/blog))。普通にページングできてタグがある感じのものです。

![blog](https://qiita-image-store.s3.amazonaws.com/0/380/8ee846d8-b873-5959-747d-825779297bf3.png)

モダンブラウザの場合は初回アクセスだけwebpackでビルドしたJSを含むファイルを受け取ってその後はpushStateを使ってルーティングを行い、必要なデータを得るためにAPIを直接叩きます。

![modern browser](https://qiita-image-store.s3.amazonaws.com/0/380/fa611a61-adff-2139-54fd-a0873a0317c1.png)

ボットや古いIEではアクセスする度にサーバーでレンダリングしたものを返します。APIはサーバー側で叩きます。

![ie,bot](https://qiita-image-store.s3.amazonaws.com/0/380/ae86b243-6173-f5be-0114-10e444b44753.png)


## 構成

どうでもいいファイルまでjsxになってますが、時間の都合上の問題なので気にしないでください。
特に重要なのはpublicとroutesの中のファイルなのでそこだけ。

```
├── public # Reactのコンポーネントはここ
│   ├── src
│   │   ├── common
│   │   │   └── req.jsx
│   │   ├── components
│   │   │   ├── EntryList.jsx
│   │   │   ├── Link.jsx
│   │   │   ├── MarkdownViewer.jsx
│   │   │   ├── Pager.jsx
│   │   │   ├── TagList.jsx
│   │   │   └── __tests__
│   │   │       ├── EntryList-test.jsx
│   │   │       ├── Link-test.jsx
│   │   │       ├── MarkdownViewer-test.jsx
│   │   │       └── TagList-test.jsx
│   │   ├── layouts
│   │   │   └── base.jsx
│   │   ├── main.jsx
│   │   ├── pages
│   │   │   ├── about.jsx
│   │   │   ├── entry.jsx
│   │   │   ├── error.jsx
│   │   │   ├── index.jsx
│   │   │   └── tag.jsx
│   │   └── routes.jsx
│   └── styles
├── routes # サーバー側のルーティング
│   ├── api
│   │   ├── entry.jsx
│   │   ├── index.jsx
│   │   └── tag.jsx
│   ├── index.jsx
│   └── react.jsx
```

## 各ページの構造

表示するページに相当するものはpublic/src/pages以下に置いておきます。各ファイルは外からは`{title, resources, Page}`のオブジェクトとして見えます(説明は以下)。必要に応じてpublic/src/components以下にコンポーネントとして切り分けておきます。あとisomorphicにしたいのでこれらファイルではpurejsなコードだけ書くようにします。

### `title`

title要素に設定する文字列を返す関数。

### `resources`

各メソッドごとにAPIにリクエストするための情報を生成します。実際にAPIからデータを得る部分はクライアント、サーバーそれぞれで実装します。

### `Page`

完全に表示するだけのReactComponentです。`props`にAPIから得たデータが入っている前提で使います。

```js:/public/src/pages/tag.jsx
var TagPage = React.createClass({
  render() {
    return (
      <div>
        <div className="tag-page-title"><i className="fa fa-tags"></i> {this.props.ctx.params.tag}</div>
        <EntryList entries={this.props.entries}/>
      </div>
    );
  }
});

var resources = {
  entries(ctx) {
    return {
      method: 'GET',
      url: `/tags/${ctx.params.tag}`
    }
  }
};

module.exports = {
  title(props) {
    return `tag | ${props.ctx.params.tag}`;
  },
  resources: resources,
  Page: TagPage
};
```

## ルーティング

以下の様なルーティングの設定を作ってサーバー・クライアント側でこれを使いまわします。

```js:public/src/routes.jsx
module.exports = {
  '/': require('./pages/index'),
  '/entry/:yyyy/:mm/:dd/:slug': require('./pages/entry'),
  '/tags/:tag': require('./pages/tag'),
  '/about': require('./pages/about')
};
```

データの読み込みは多少共通化しました(もうちょいできそうですがwebpackの設定でハマってこんなかんじに・・・)。`ctx`(expressで言うとこの`req`)と`handler`(上で説明したやつ)と`loadFn`(パラメータを受け取ってプロミスでデータを返すだけの関数)を登録するとデータをPromiseで返してくれます。

```js:common.jsx
module.exports = {
  loadPageData(params) {
    var {ctx, handler, loadFn} = params;
    var promises, props = {ctx: ctx};

    promises = _.map(handler.resources, (resource, k) => {
      var o = resource(ctx);
      o.url = constants.API_PATH + o.url;
      if (o.method === 'GET' && !_.isEmpty(ctx.query)) {
        o.url += `?${qs.stringify(ctx.query)}`;
      }
      return loadFn(o).then(data => props[k] = data);
    });

    return Promise.all(promises).then(() => props);
  }
};
```
### サーバー側

サーバー側ではexpressを使うのでそれに付いているルーターを使います。

モダンブラウザだけはそれ用のファイルを返して、ボット・IEは普通にルーティングさせます。これでボット・IEから見たら普通のブログとして機能します。

```js:routes/index.jsx
router.get('/*', (req, res, next) => {
  var ua = req.get('User-Agent').toLowerCase();
  // ie ~9 and bot
  if (/msie\s[6-9]|bot|crawler|baiduspider|80legs|ia_archiver|voyager|curl|wget|yahoo! slurp|mediapartners-google/.test(ua)) {
    return next();
  }
  // モダンブラウザだけJSバリバリ使うページを返す。
  res.render('index', { title: constants.BLOG_TITLE });
});
```

```js:routes/react.jsx
// 一応、rpはrequest-promise
var load = (o) => {
  o.uri = o.url;
  return rp(o).then(JSON.parse);
};

// server side rendering
var setupRoute = (handler, k) => {
  router.get(k, (req, res, next) => {
    loadPageData({
      ctx: req,
      handler: handler,
      loadFn: load
    })
    .then(props => {
      var title = handler.title(props);
      var {Page} = handler;
      res.render('server_index', {
        title: constants.BLOG_TITLE + (title ? ` | ${title}` : ''),
        result: React.renderToString(<Base><Page {...props}/></Base>)
      });
    })
    .catch(next);
  });
};

_.forEach(require('../public/src/routes'), setupRoute);

// error page
router.get('/*', (req, res) => {
  res.render('server_index', {
    title: constants.BLOG_TITLE + ' | ' + errorPage.title(),
    result: React.renderToString(<Base><errorPage.Page /></Base>)
  });
});
```

### クライアント側

expressの作者が作った[page.js](https://github.com/visionmedia/page.js)というルーターを使います。こいつはexpress風のpushStateを使ったルーターでサーバー側と同じ感じで使えます。

```js:public/src/main.jsx
var Main = React.createClass({
  getInitialState() {
    return {
      Page: Empty,
      props: {}
    };
  },

  setTitle(title) {
    document.title = constants.BLOG_TITLE + (title ? ` | ${title}` : '');
  },

  setupRoute(handler, path) {
    page(path, (ctx, next) => {
      loadPageData({
        ctx: ctx,
        handler: handler,
        loadFn: req // expressのreqと紛らわしいですが、簡単なXMLHttpRequestのラッパーです
      })
      .then(props => {
        this.setTitle(handler.title(props));
        this.setState({
          Page: handler.Page,
          props: props
        });
      })
      .catch(e => {
        console.log(e, e.stack);
        next();
      });
    });
  },

  setupErrorPage() {
    page('*', ctx => {
      var {Page} = errorPage;
      this.setTitle(errorPage.title());
      this.setState({
        Page: Page,
        props: {}
      });
    });
  },

  componentWillMount() {
    _.forEach(require('./routes'), this.setupRoute);
    this.setupErrorPage();
    page();
  },

  render() {
    var {Page, props} = this.state;
    return (
      <Base><Page {...props}/></Base>
    );
  }
});
```

大体同じですね。

## その他、開発環境周り

Reactで開発するにあたってはwebpack的なものは絶対必要だし、実際使ってみたらとんでもなく便利なものですね。bowerとnpm両方使わなくてもいいし、設定ファイル的なものもrequireするだけなので楽です。まぁ、webpackの設定についてはもうちょっとどうにかなりそうですが(isomorphic度も上がるはず)。


## 参考

* [kriasoft/react-starter-kit](https://github.com/kriasoft/react-starter-kit/) 構成の参考にしました
* [ukyo-react-sample/blog at master · ukyo/ukyo-react-sample](https://github.com/ukyo/ukyo-react-sample/tree/master/blog)
* [visionmedia/page.js](https://github.com/visionmedia/page.js)