Monaca無しでReact-OnsenUIをApache cordovaで使う

この記事は、[Onsen UI Advent Calendar 2016](http://qiita.com/advent-calendar/2016/onsenui) の6日目の記事として作成しました。

## はじめに
Monaca無しでReact-OnsenUIをApache cordovaで使う方法を説明します。今回はcordovaの機能やreactの説明は対象外とさせて頂きます。cordova自体のインストールなどは[Get Start](https://cordova.apache.org/#getstarted)を参考にしてください。

記事中ではcordovaでアプリケーションを作成せず、Ripple Emulatorで動作確認するためAndroid SDKやXcodeは必要ありません。

※Androidの実機やAVDエミュレータで動作確認する場合は、JavaのインストールやAndroid SDKのインストールが必要です。[cordovaのサイト](https://cordova.apache.org/docs/en/latest/guide/platforms/android/index.html)を参照して開発環境を設定してください。Macintoshが周りにないのでiOSはよくわかりません:smile:

## 必要なパッケージ
単に私が慣れているため、ビルドツールにはWebpackを使います。他のビルドツールに慣れているのならGulpやBrowserifyを使用しても構いません。

- Apache cordova
- onsenuiとreact-onsenui
- reactとbabel
- webpack
- ripple-emulator(手っ取り早いアプリの確認の為)

## cordovaプロジェクトを作成
> % cordova create react-onsenui-sample
> % cd react-onsenui-sample/www

これ以降はwwwディレクトリをメインに編集していきます。

## パッケージのインストール
wwwに以下のpackage.jsonを作成して、npm install を実行します。インストール後にnpm run rippleを実行すると、システムブラウザ上のエミュレータでcordovaのHello, Worldが表示されます。

```js:package.json
{
  "name": "react-onsenui-sample",
  "version": "0.0.1",
  "description": "",
  "main": "index.js",
  "scripts": {
    "webpack": "webpack",
    "ripple": "ripple emulate",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "license": "MIT",
  "devDependencies": {
    "babel-core": "~6",
    "babel-loader": "~6",
    "babel-plugin-transform-runtime": "~6",
    "babel-preset-es2015": "~6",
    "babel-preset-react": "~6",
    "babelify": "^7.2.0",
    "onsenui": "~2",
    "react": "~15.3.0",
    "react-dom": "~15.3.0",
    "react-onsenui": "~1",
    "ripple-emulator": "^0.9.36",
    "webpack": "~1"
  }
}
```

## Webpackの設定
/js/index.jsを入力して、Webpack後のファイルをdist.jsへ出力します。babelを使ってreactのjsxとES2015のトランスパイルを行います。reactコンポーネントの拡張子はjsxにしたいので、jsxファイルを認識する設定も入れておきます。

```js:webpack.config.js
module.exports = {
  entry: [__dirname + '/js/index.js'],
  output: {
    path: __dirname + '/',
    filename: 'dist.js'
  },
  module: {
    loaders: [
      {
        test: /\.js$|\.jsx$/,
        exclude: /node_modules/,
        loader: 'babel',
        query:{
          presets: ['react', 'es2015'],
          plugins: ['transform-runtime']
        }
      }
    ]
  },
  resolve: {
    extensions: ['', '.js', '.jsx']
  },
  devtool: 'source-map',
};
```

これで
> % npm run webpack

を実行するとindex.jsを起点としたビルドが開始されます。

## index.htmlの変更
onsenui関連のcssの追加と、トランスパイル済のdist.jsを読み込むように変更します。後はReactではお馴染みのテンプレートdivタグがあるだけです。

```html:index.html
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <meta http-equiv="Content-Security-Policy" content="default-src 'self' data: gap: https://ssl.gstatic.com 'unsafe-eval'; style-src 'self' 'unsafe-inline'; media-src *; img-src 'self' data: content:;">
        <meta name="format-detection" content="telephone=no">
        <meta name="msapplication-tap-highlight" content="no">
        <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width">
        <link rel="stylesheet" type="text/css" href="css/index.css">
        <link rel="stylesheet" href="node_modules/onsenui/css/onsenui.css">
        <link rel="stylesheet" href="node_modules/onsenui/css/onsen-css-components.css">
        <title>Hello World</title>
    </head>
    <body>
        <div id="app"></div>
        <script type="text/javascript" src="cordova.js"></script>
        <script type="text/javascript" src="dist.js"></script>
    </body>
</html>
```

## Reactコンポーネント
今回は単純にするため以下の仕様でコンポーネントを作成します。

-  Page1 -- カウント状態を持ちボタンクリックでインクリメント。現在のカウント状態をPage2のプロパティに渡す。
-  Page2 -- 渡されたカウントを表示する

 
```js:/js/index.js
import React, {Component} from 'react';
import ReactDOM from 'react-dom';
import {Navigator} from 'react-onsenui';
import Page1 from './Page1';
class App extends Component {
  constructor(props) {
    super(props);
    this.renderPage = this.renderPage.bind(this);
  }
  
  renderPage(route, navigator) {
    route.props = route.props || {};
    return (
      <route.comp key={route.key} navigator={navigator} {...route.props}/>
    );
  }
  
  render() {
    return (
      <Navigator
        initialRoute={{comp: Page1, key: 'Page1'}}
        renderPage={this.renderPage}
      />
    );
  }
}

ReactDOM.render(
  <App />
  , document.getElementById('app')
);
```
```js:/js/Page1.jsx
import React, {Component} from 'react';
import {
  Page,
  Toolbar,
  Button
} from 'react-onsenui';
import Page2 from './Page2';

export default class Page1 extends Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    }
  }

  renderToolbar() {
    return (
      <Toolbar>
        <div className='center'>Page1</div>
      </Toolbar>
    );
  }

  handlePage2(e) {
    this.props.navigator.pushPage({comp: Page2, key: 'Page2', props: {count: this.state.count}});
  }

  handleCountup(e) {
    const c = this.state.count + 1;
    this.setState({count: this.state.count + 1});
  }

  render() {
    return (
      <Page renderToolbar={this.renderToolbar.bind(this)}>
        <p>Page1</p>
          <div>{this.state.count}</div>
          <Button onClick={this.handleCountup.bind(this)}>
            Count up
          </Button>
          <Button onClick={this.handlePage2.bind(this)}>
            Page2
          </Button>
      </Page>
    );
  }
}
```

```js:/js/Page2.jsx
import React, {Component} from 'react';
import {
  Page,
  Toolbar,
  BackButton
} from 'react-onsenui';

export default class Page2 extends Component {
  constructor(props) {
    super(props);
  }

  renderToolbar() {
    return (
      <Toolbar>
        <div className='left'>
          <BackButton>Back</BackButton>
        </div>
        <div className='center'>Page2</div>
      </Toolbar>
    );
  }

  render() {
    return (
      <Page renderToolbar={this.renderToolbar.bind(this)}>
        <p>Page2</p>
        <div>{this.props.count}</div>
      </Page>
    );
  }
}
```

上記ファイルを作成後
> % npm run webpack
> % npm run ripple

と実行するとシステムブラウザ上のエミュレータで実行されます。

## あとがき
実は私OnsenUIってMonacaが無いと使えないと思い込んでいたんですね。多分、私以外にも結構いるのではないかと思います。公式サイトでもMonacaを使用せずに使えることが、一応[触れられて](https://ja.onsen.io/getting-started/)はいますが、これを見てアプリケーションをビルドできるかというと正直難しい気がします:sweat_smile:

ということで、OnsenUIは普通に使えるパッケージであることをお伝えできればと思いこの記事を作成してみました。

後、この記事ではさらっと流していますが、cordovaでandroidアプリを作成する環境を整えるのが結構煩わしいです。その辺をウィザードで簡単に設定できるのが、MonacaであったりVisual Studioであったりするのかな。私は使ったことが無いのでわかりませんが:smile:

## リポジトリ
記事中のソースをGitHubで公開しています。
https://github.com/teufelj/react-onsenui-sample