85
85

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

VirtualDOMAdvent Calendar 2014

Day 16

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

Last updated at Posted at 2014-12-16

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

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

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

blog

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

modern browser

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

ie,bot

構成

どうでもいいファイルまで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から得たデータが入っている前提で使います。

/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
};

ルーティング

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

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で返してくれます。

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から見たら普通のブログとして機能します。

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 });
});
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というルーターを使います。こいつはexpress風のpushStateを使ったルーターでサーバー側と同じ感じで使えます。

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度も上がるはず)。

参考

85
85
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
85
85

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?