プログラミングとSEOと暇つぶし

駆け出しエンジニアdallPのブログです。元SEOコンサルタントです。プログラミング、SEO、アフィリエイト、お金などについて役に立つかもしれない情報をやりたいように書きます。

MeteorJSとReactを勉強してみる その7: コンポーネントのステートにUIの状態を保存してみよう【公式翻訳】

MeteorJSを勉強してみる第七弾です。

タイトルの意味がわかりにくいですね。 まあ実際に書いてみないと何を言っているか分からないと思います。

英語って難しい。


コンポーネントのステートに一時的なUIの状態を保存してみよう

このステップでは、クライアント側でのデータフィルタリング機能をアプリに追加します。 これによりユーザーは、未完了のタスクだけを表示させることができます。

Reactコンポーネントのステートを、クライアント側のみで利用される一時的な状態を保持するためにどのように使えるのかを見ていきましょう。

まず、Appコンポーネントチェックボックスを追加します。

imports/ui/App.js

before

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { withTracker } from 'meteor/react-meteor-data';

import { Tasks } from '../api/tasks.js';

import Task from './Task.js';

// App Component - represnts the whole app
class App extends Component {
  handleSubmit(event) {
    event.preventDefault();

    // find the text field via the React ref
    const text = ReactDOM.findDOMNode(this.refs.textInput).value.trim();

    Tasks.insert({
      text,
      createdAt: new Date()
    });

    // clear form
    ReactDOM.findDOMNode(this.refs.textInput).value = '';
  }

  renderTasks() {
    return this.props.tasks.map((task) => (
      <Task key={task._id} task={task} />
    ));
  }

  render() {
    return (
      <div className="container">
        <header>
          <h1>Todo List</h1>
          <form className="new-task" onSubmit={this.handleSubmit.bind(this)}>
            <input
              type="text"
              ref="textInput"
              placeholder="Type to add new Tasks."
            />
          </form>
        </header>

        <ul>
          {this.renderTasks()}
        </ul>
      </div>
    );
  }
}

export default withTracker(() => {
  return {
    tasks: Tasks.find({}, { sort: { createdAt: -1 } }).fetch()
  };
})(App);

after

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { withTracker } from 'meteor/react-meteor-data';

import { Tasks } from '../api/tasks.js';

import Task from './Task.js';

// App Component - represnts the whole app
class App extends Component {
  handleSubmit(event) {
    event.preventDefault();

    // find the text field via the React ref
    const text = ReactDOM.findDOMNode(this.refs.textInput).value.trim();

    Tasks.insert({
      text,
      createdAt: new Date()
    });

    // clear form
    ReactDOM.findDOMNode(this.refs.textInput).value = '';
  }

  renderTasks() {
    return this.props.tasks.map((task) => (
      <Task key={task._id} task={task} />
    ));
  }

  render() {
    return (
      <div className="container">
        <header>
          <h1>Todo List</h1>

          <label className="hide-completed">
            <input
              type="checkbox"
              readOnly
              checked={this.state.hideCompleted}
              onClick={this.toggleHideCompleted.bind(this)}
            />
            Hide Completed Tasks
          </label>

          <form className="new-task" onSubmit={this.handleSubmit.bind(this)}>
            <input
              type="text"
              ref="textInput"
              placeholder="Type to add new Tasks."
            />
          </form>
        </header>

        <ul>
          {this.renderTasks()}
        </ul>
      </div>
    );
  }
}

export default withTracker(() => {
  return {
    tasks: Tasks.find({}, { sort: { createdAt: -1 } }).fetch()
  };
})(App);

新しく追加した<input>タグの中で、this.state.hideCompletedを呼んでいるのがわかると思います。

Reactコンポーネントは「ステート」と呼ばれる特殊な属性を持っており、ここにカプセル化されたコンポーネントのデータを格納しておくことができます。

これを利用するためには、コンポーネントのコンストラクタの中でthis.state.hideCompletedの値を初期化しなければなりません。

imports/ui/App.js

before

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { withTracker } from 'meteor/react-meteor-data';

import { Tasks } from '../api/tasks.js';

import Task from './Task.js';

// App Component - represnts the whole app
class App extends Component {
  handleSubmit(event) {
    event.preventDefault();

    // find the text field via the React ref
    const text = ReactDOM.findDOMNode(this.refs.textInput).value.trim();

    Tasks.insert({
      text,
      createdAt: new Date()
    });

    // clear form
    ReactDOM.findDOMNode(this.refs.textInput).value = '';
  }

  renderTasks() {
    return this.props.tasks.map((task) => (
      <Task key={task._id} task={task} />
    ));
  }

  render() {
    return (
      <div className="container">
        <header>
          <h1>Todo List</h1>

          <label className="hide-completed">
            <input
              type="checkbox"
              readOnly
              checked={this.state.hideCompleted}
              onClick={this.toggleHideCompleted.bind(this)}
            />
            Hide Completed Tasks
          </label>

          <form className="new-task" onSubmit={this.handleSubmit.bind(this)}>
            <input
              type="text"
              ref="textInput"
              placeholder="Type to add new Tasks."
            />
          </form>
        </header>

        <ul>
          {this.renderTasks()}
        </ul>
      </div>
    );
  }
}

export default withTracker(() => {
  return {
    tasks: Tasks.find({}, { sort: { createdAt: -1 } }).fetch()
  };
})(App);

after

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { withTracker } from 'meteor/react-meteor-data';

import { Tasks } from '../api/tasks.js';

import Task from './Task.js';

// App Component - represnts the whole app
class App extends Component {
  constructor(props) {
    super(props);

    this.state = {
      hideCompleted = false;
    };
  }

  handleSubmit(event) {
    event.preventDefault();

    // find the text field via the React ref
    const text = ReactDOM.findDOMNode(this.refs.textInput).value.trim();

    Tasks.insert({
      text,
      createdAt: new Date()
    });

    // clear form
    ReactDOM.findDOMNode(this.refs.textInput).value = '';
  }

  renderTasks() {
    return this.props.tasks.map((task) => (
      <Task key={task._id} task={task} />
    ));
  }

  render() {
    return (
      <div className="container">
        <header>
          <h1>Todo List</h1>

          <label className="hide-completed">
            <input
              type="checkbox"
              readOnly
              checked={this.state.hideCompleted}
              onClick={this.toggleHideCompleted.bind(this)}
            />
            Hide Completed Tasks
          </label>

          <form className="new-task" onSubmit={this.handleSubmit.bind(this)}>
            <input
              type="text"
              ref="textInput"
              placeholder="Type to add new Tasks."
            />
          </form>
        </header>

        <ul>
          {this.renderTasks()}
        </ul>
      </div>
    );
  }
}

export default withTracker(() => {
  return {
    tasks: Tasks.find({}, { sort: { createdAt: -1 } }).fetch()
  };
})(App);

ここで初期化したthis.stateは、イベントハンドラ内でthis.setStateを呼ぶことで更新することができます。 これにより、ステートは非同期的に更新され、同時にコンポーネントの再レンダリングを引き起こします。

では、実際にステートの内容を更新する役目を持つtoggleHideCompleted()を実装しましょう。

imports/ui/App.js

before

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { withTracker } from 'meteor/react-meteor-data';

import { Tasks } from '../api/tasks.js';

import Task from './Task.js';

// App Component - represnts the whole app
class App extends Component {
  constructor(props) {
    super(props);

    this.state = {
      hideCompleted = false;
    };
  }

  handleSubmit(event) {
    event.preventDefault();

    // find the text field via the React ref
    const text = ReactDOM.findDOMNode(this.refs.textInput).value.trim();

    Tasks.insert({
      text,
      createdAt: new Date()
    });

    // clear form
    ReactDOM.findDOMNode(this.refs.textInput).value = '';
  }

  render() {
    return (
      <div className="container">
        <header>
          <h1>Todo List</h1>

          <label className="hide-completed">
            <input
              type="checkbox"
              readOnly
              checked={this.state.hideCompleted}
              onClick={this.toggleHideCompleted.bind(this)}
            />
            Hide Completed Tasks
          </label>

          <form className="new-task" onSubmit={this.handleSubmit.bind(this)}>
            <input
              type="text"
              ref="textInput"
              placeholder="Type to add new Tasks."
            />
          </form>
        </header>

        <ul>
          {this.renderTasks()}
        </ul>
      </div>
    );
  }
}

export default withTracker(() => {
  return {
    tasks: Tasks.find({}, { sort: { createdAt: -1 } }).fetch()
  };
})(App);

after

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { withTracker } from 'meteor/react-meteor-data';

import { Tasks } from '../api/tasks.js';

import Task from './Task.js';

// App Component - represnts the whole app
class App extends Component {
  constructor(props) {
    super(props);

    this.state = {
      hideCompleted = false;
    };
  }

  handleSubmit(event) {
    event.preventDefault();

    // find the text field via the React ref
    const text = ReactDOM.findDOMNode(this.refs.textInput).value.trim();

    Tasks.insert({
      text,
      createdAt: new Date()
    });

    // clear form
    ReactDOM.findDOMNode(this.refs.textInput).value = '';
  }

  renderTasks() {
    return this.props.tasks.map((task) => (
      <Task key={task._id} task={task} />
    ));
  }

  toggleHideCompleted() {
    this.setState({
      hideCompleted: !this.state.hideCompleted
    });
  }

  render() {
    return (
      <div className="container">
        <header>
          <h1>Todo List</h1>

          <label className="hide-completed">
            <input
              type="checkbox"
              readOnly
              checked={this.state.hideCompleted}
              onClick={this.toggleHideCompleted.bind(this)}
            />
            Hide Completed Tasks
          </label>

          <form className="new-task" onSubmit={this.handleSubmit.bind(this)}>
            <input
              type="text"
              ref="textInput"
              placeholder="Type to add new Tasks."
            />
          </form>
        </header>

        <ul>
          {this.renderTasks()}
        </ul>
      </div>
    );
  }
}

export default withTracker(() => {
  return {
    tasks: Tasks.find({}, { sort: { createdAt: -1 } }).fetch()
  };
})(App);

ここまでできたら、hideCompletedがtrueの時にはそのタスクを表示しない、という処理をrenderTasks関数に持たせる必要があります。

imports/ui/App.js

before

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { withTracker } from 'meteor/react-meteor-data';

import { Tasks } from '../api/tasks.js';

import Task from './Task.js';

// App Component - represnts the whole app
class App extends Component {
  constructor(props) {
    super(props);

    this.state = {
      hideCompleted = false;
    };
  }

  handleSubmit(event) {
    event.preventDefault();

    // find the text field via the React ref
    const text = ReactDOM.findDOMNode(this.refs.textInput).value.trim();

    Tasks.insert({
      text,
      createdAt: new Date()
    });

    // clear form
    ReactDOM.findDOMNode(this.refs.textInput).value = '';
  }

  renderTasks() {
    return this.props.tasks.map((task) => (
      <Task key={task._id} task={task} />
    ));
  }

  toggleHideCompleted() {
    this.setState({
      hideCompleted: !this.state.hideCompleted
    });
  }

  render() {
    return (
      <div className="container">
        <header>
          <h1>Todo List</h1>

          <label className="hide-completed">
            <input
              type="checkbox"
              readOnly
              checked={this.state.hideCompleted}
              onClick={this.toggleHideCompleted.bind(this)}
            />
            Hide Completed Tasks
          </label>

          <form className="new-task" onSubmit={this.handleSubmit.bind(this)}>
            <input
              type="text"
              ref="textInput"
              placeholder="Type to add new Tasks."
            />
          </form>
        </header>

        <ul>
          {this.renderTasks()}
        </ul>
      </div>
    );
  }
}

export default withTracker(() => {
  return {
    tasks: Tasks.find({}, { sort: { createdAt: -1 } }).fetch()
  };
})(App);

after

import ReactDOM from 'react-dom';
import { withTracker } from 'meteor/react-meteor-data';

import { Tasks } from '../api/tasks.js';

import Task from './Task.js';

// App Component - represnts the whole app
class App extends Component {
  constructor(props) {
    super(props);

    this.state = {
      hideCompleted = false;
    };
  }

  handleSubmit(event) {
    event.preventDefault();

    // find the text field via the React ref
    const text = ReactDOM.findDOMNode(this.refs.textInput).value.trim();

    Tasks.insert({
      text,
      createdAt: new Date()
    });

    // clear form
    ReactDOM.findDOMNode(this.refs.textInput).value = '';
  }

  renderTasks() {
    let filteredTasks = this.props.tasks;

    if (this.state.hideCompleted) {
      filteredTasks = filteredTasks.filter((task) => !task.checked);
    }

    return filteredTasks.map((task) => (
      <Task key={task._id} task={task} />
    ));
  }

  toggleHideCompleted() {
    this.setState({
      hideCompleted: !this.state.hideCompleted
    });
  }

  render() {
    return (
      <div className="container">
        <header>
          <h1>Todo List</h1>

          <label className="hide-completed">
            <input
              type="checkbox"
              readOnly
              checked={this.state.hideCompleted}
              onClick={this.toggleHideCompleted.bind(this)}
            />
            Hide Completed Tasks
          </label>

          <form className="new-task" onSubmit={this.handleSubmit.bind(this)}>
            <input
              type="text"
              ref="textInput"
              placeholder="Type to add new Tasks."
            />
          </form>
        </header>

        <ul>
          {this.renderTasks()}
        </ul>
      </div>
    );
  }
}

export default withTracker(() => {
  return {
    tasks: Tasks.find({}, { sort: { createdAt: -1 } }).fetch()
  };
})(App);

ここまでできたら、保存してブラウザを確認してみましょう。 追加したチェックボックスにチェックを入れると、完了していないタスクのみが表示されるようになっているはずです。

未完了タスクの合計数を表示する

完了したタスクを弾くコードを書いてきましたが、同じようなやり方で「未完了タスクの数を表示する」こともできます。

これを実装するためには、データコンテナにタスクの数を入れ、renderメソッドに1行追加するだけでOKです。

既にクライアント側のコレクションにデータを持っているため、「タスク数」を追加するにあたってはサーバーにアクセスする必要は一切ありません。

imports/ui/App.js

before

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { withTracker } from 'meteor/react-meteor-data';

import { Tasks } from '../api/tasks.js';

import Task from './Task.js';

// App Component - represnts the whole app
class App extends Component {
  constructor(props) {
    super(props);

    this.state = {
      hideCompleted: false,
    };
  }

  handleSubmit(event) {
    event.preventDefault();

    // find the text field via the React ref
    const text = ReactDOM.findDOMNode(this.refs.textInput).value.trim();

    Tasks.insert({
      text,
      createdAt: new Date()
    });

    // clear form
    ReactDOM.findDOMNode(this.refs.textInput).value = '';
  }

  renderTasks() {
    let filteredTasks = this.props.tasks;

    if (this.state.hideCompleted) {
      filteredTasks = filteredTasks.filter((task) => !task.checked);
    }

    return filteredTasks.map((task) => (
      <Task key={task._id} task={task} />
    ));
  }

  toggleHideCompleted() {
    this.setState({
      hideCompleted: !this.state.hideCompleted
    });
  }

  render() {
    return (
      <div className="container">
        <header>
          <h1>Todo List</h1>

          <label className="hide-completed">
            <input
              type="checkbox"
              readOnly
              checked={this.state.hideCompleted}
              onClick={this.toggleHideCompleted.bind(this)}
            />
            Hide Completed Tasks
          </label>

          <form className="new-task" onSubmit={this.handleSubmit.bind(this)}>
            <input
              type="text"
              ref="textInput"
              placeholder="Type to add new Tasks."
            />
          </form>
        </header>

        <ul>
          {this.renderTasks()}
        </ul>
      </div>
    );
  }
}

export default withTracker(() => {
  return {
    tasks: Tasks.find({}, { sort: { createdAt: -1 } }).fetch(),
  };
})(App);

after

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { withTracker } from 'meteor/react-meteor-data';

import { Tasks } from '../api/tasks.js';

import Task from './Task.js';

// App Component - represnts the whole app
class App extends Component {
  constructor(props) {
    super(props);

    this.state = {
      hideCompleted: false,
    };
  }

  handleSubmit(event) {
    event.preventDefault();

    // find the text field via the React ref
    const text = ReactDOM.findDOMNode(this.refs.textInput).value.trim();

    Tasks.insert({
      text,
      createdAt: new Date()
    });

    // clear form
    ReactDOM.findDOMNode(this.refs.textInput).value = '';
  }

  renderTasks() {
    let filteredTasks = this.props.tasks;

    if (this.state.hideCompleted) {
      filteredTasks = filteredTasks.filter((task) => !task.checked);
    }

    return filteredTasks.map((task) => (
      <Task key={task._id} task={task} />
    ));
  }

  toggleHideCompleted() {
    this.setState({
      hideCompleted: !this.state.hideCompleted
    });
  }

  render() {
    return (
      <div className="container">
        <header>
          <h1>Todo List ({this.props.incompleteCount})</h1>

          <label className="hide-completed">
            <input
              type="checkbox"
              readOnly
              checked={this.state.hideCompleted}
              onClick={this.toggleHideCompleted.bind(this)}
            />
            Hide Completed Tasks
          </label>

          <form className="new-task" onSubmit={this.handleSubmit.bind(this)}>
            <input
              type="text"
              ref="textInput"
              placeholder="Type to add new Tasks."
            />
          </form>
        </header>

        <ul>
          {this.renderTasks()}
        </ul>
      </div>
    );
  }
}

export default withTracker(() => {
  return {
    tasks: Tasks.find({}, { sort: { createdAt: -1 } }).fetch(),
    incompleteCount: Tasks.find({ checked: { $ne: true } }).count()
  };
})(App);

ここまで来ると大分Reactの使い方に慣れてきた気がしますね。

  • renderメソッドには出力したいHTMLを書く
  • そのHTMLの中で動的に操作したい内容は変数化・メソッド化して外に出す
  • リスト要素のように共通の要素はrender内に直接書くのではなく別コンポーネントに分ける

僕も業務で使ってないので想像ですが、実際はrender()の中身も別ファイルに分けて、メソッドのみのファイルとUIのファイルを分けてしまうとより良さそうですね。

Meteorの公式ドキュメントに最適な構成に関するものもあったので、こちらも近々読んで翻訳してみます。

第八弾は明日にでも。

MeteorJSとReactを勉強してみる その6: iOSとAndroidでアプリを動かしてみよう【公式翻訳】

MeteorJSを勉強してみるシリーズ第六弾です。

作りたいサービスのイメージがある人はテンションが上がるのではないでしょうか。

ただ、現状(1.6系)のWindows版のMeteorではモバイル向けのビルドには対応していません。 残念ですが、Windowsの人は別のフレームワークを選ぶか、別途環境を用意しましょう。


iOSAndroidでアプリを動かす

ここまでではブラウザのみでアプリを立ち上げ、テストしてきました。 しかしMeteorはその他のプラットフォームでも動くように作られています。 つまり、作成中のTodoリストが多少のコマンドでiOSAndroidアプリになってしまうのです。

Meteorではスマホアプリを立ち上げるのに必要な設定が簡単にできるようになっていますが、全てのソースをダウンロードするのにはしばらく時間がかかります。 Android用のソースはおよそ300MB、そしてiOS用には2GBほどのXcodeをインストールする必要があります。 これらのインストールが面倒だという方は、一旦このステップを飛ばしても結構です。

iOSシミュレーターで動かす (Mac限定)

もしMacをお持ちであれば、アプリをiOSシミュレーターで動かすことができます。

ターミナルでアプリケーションのフォルダに行き、下記のコマンドを打ってください。

meteor install-sdk ios

このコマンドで、プロジェクトソースからiOSアプリを立ち上げるのに必要な設定が行うことができます。


訳者注: 上記コマンドを打つと、下記が表示されます。

Please follow the installation instructions in the mobile guide:
http://guide.meteor.com/mobile.html#installing-prerequisites

結局はここを読んで自分でコマンドを打つ必要があるんですね笑

せっかくなので、ここで全て解説しておきます。

まずは下記コマンドを打って、表示される選択肢からGet Xcodeを選びましょう。

meteor add-platform ios

そうするとApp StoreXcodeの画面が表示されるので、そのままインストールします。

インストールには時間がかかりますので、しばらくコーヒーでも淹れながら待ちましょう。

完了したら下記をターミナルで打ちます。

sudo xcodebuild -license accept

これでXcodeのライセンス条項に同意したことになります。

基本的には上記で設定は完了です。


完了したら、下記のコマンドを打ちましょう。

meteor add-platform ios
meteor run ios

iOSシミュレーターが立ち上がり、アプリが中で動いているのが確認できるはずです。


訳者注: もし上記で立ち上がらなかった場合は、一旦Xcodeのアプリケーションそのものを開いてみてください。 Macのファインダーからアプリケーションフォルダの中を探しましょう。

開いたら、人によってはXcodeの機能をフルで利用するための追加インストールを行うか、というインフォメーションが表示されると思いますので、Installを選びましょう。

僕の場合はこれで立ち上がるようになりました。

初回立ち上げの際には、シミュレータの立ち上げにもそこそこ時間がかかります。 気長に待ちましょう。

Androidエミュレータで動かす

ターミナルでアプリケーションのフォルダに行き、下記を打ってください。

meteor install-sdk android


訳者注: このコマンドも、iOS同様にリンクを表示するだけです。

Please follow the installation instructions in the mobile guide:
http://guide.meteor.com/mobile.html#installing-prerequisites

リンク先は同じなので、別途開かなくてOKです。

個人的にAndroid向けのセットアップはかなり苦労しました。 おそらく環境により詰まった際の取るべき対応はことなるのですが、ここではサラッと僕が何をやったかだけ流します。

ドキュメントに従うなら、下記を行えば立ち上げまで行けるはず。

意味わかりませんか? 大丈夫、僕も分かりません。

上から進めていきましょう。

Java Development Kit(JDK)をインストール

Oracle Java Websiteに行き、Java Development Kitを選択してダウンロードします。

この時、自分の環境(MacなのかWinなのか)に適したJDKをダウンロードするよう注意しましょう。 まあそもそもMeteorがWindows向けのアプリビルドに対応してないので殆どの人はMac版をインストールすると思います。

ダウンロードしたzipを解凍し、表示されたアイコンをダブルクリックしたら後はインストーラの指示に従っていきましょう。

Android SDKとその他必要なツールをインストール

Androidの立ち上げには色々必要みたいです。

There is no need to use Android Studio if you prefer a stand-alone install. Just make sure you install the most recent versions of the Android SDK Tools and download the required additional packages yourself using the Android SDK Manager.

Meteor公式にはこんなこと書いてありますが、特に初心者が実行するのは至難の業だと思いますので、ここは素直にAndroid Studioをインストールしましょう。

Android Studioのインストール手順は基本的にインストーラに従うだけですが、不安な人はAndroid Studioの公式サポートの動画をチェックしましょう。 僕は見ても意味ないと思います。

さて、Meteor公式には「正しいバージョンのAndroid SDK Tools」をインストールするよう但し書きがあります。 具体的には

  • Meteor 1.4.3.1以降のバージョンの場合、25.2.xか26.0.0以降のAndroid SDK Tools(mac, linux)をインストールすること
  • バージョン25.3.0のSDK Toolsでは動かない
  • Meteor 1.4.2以前のバージョンの場合、23系のSDK Tools(mac, linux)をインストールすること

と言われても、Android Studioをインストールする時に「Android SDK Tools」のバージョン選択画面なんかありません。

ただ、途中の画面でインストールするパッケージの内容を見ることはできます。 そこでAndroid SDK Toolsのバージョンを確認して、上記に合致しない場合はエラーが出る可能性があります。

バージョンが違ったら、上記macとかlinuxとかのリンクから、適するバージョンのSDK Toolsをダウンロードしてください。

その上で、~/Library/Android/sdk/にあるtools/の中身を、ダウンロードしたSDK Toolsの中身と置き換えます。

置き換える際には下記のコマンドを打ちましょう。

cd ~/Library/Android/sdk/tools/
rm *
cp ~/Downloads/tools/* ~/Library/Android/sdk/tools/

パスにANDROID_HOMEとその他ツールのディレクトリを設定

~/.bash_profileを開き、下記を追加してください。 (zshの場合は~/.zshrc)

# Android
export ANDROID_HOME="$HOME/Library/Android/sdk"
export PATH=$PATH:$ANDROID_HOME/tools:$ANDROID_HOME/platform-tools

書き込み終わったら、下記のコマンドで読み込む.bash_profileを更新します。

source ~/.bash_profile

Android Virtual Device(AVD)をエミュレータ用に作成

この過程はMeteor公式だとオプションのように書いてるのですが、どうやら必須な気がしています。

実際にAVDを作成するには、まずAndroid Studioを立ち上げ、Start a new Android Studio Projectから新規プロジェクトを立ち上げましょう。

設定はデフォルトのままで良いです。 ただ、Activityの設定画面ではAdd No Activityを選択しておいたほうがプロジェクトの作成待ち時間が少ない気がします。

立ち上がったら、画面上のアイコンでスマホっぽい形をしたものを探して、マウスオンしてください。 「AVD Manager」と表示されたらビンゴです。 クリックして、Create Virtual Deviceを選択しましょう。

あとはエミュレータで立ち上げたい機種を選択し、どのAndroid OSを利用するか選択します。

僕は機種にPixel 2、OSはNugat(API level: 25)を選択しました。 OSのバージョンについては、多分インストールしてあるMeteorのバージョンによって適切なものが変わります。

gradleのパーミッションを書き換える (必要に応じて)

もはやここまで来ると、僕をはじめとして普段Androidを触らない人はよく分かんないのではと思うのですが、 何やらAndroid StudioのバージョンによってはgradleというJavaのビルドシステムのファイルを読み書きする際のパーミッションが書き換わってしまうようです。

ここで引っかかってる人は全世界にたくさんいるようなので、幸いWebにはソリューションがそこらに落ちてます。

もしmeteor run androidError: spawn EACCESのようなエラーが出た場合、これを疑ってください。

僕の場合は、下記コマンドで解決しました。

gradle-x.yの部分は、インストールされているgradleにより異なります。ご自分で確認してくださいね。

chmod +x /Applications/Android\ Studio.app/Contents/gradle/gradle-4.1/bin/gradle

これでgradleのパーミッションを書き換えることができます。

必要と思われる設定は以上です。

僕は色々調べながらトライアンドエラーしてたらいつの間にか朝の7時になってました。 まあ気長にやるか、一旦飛ばしてしまっても良い気がします笑


これでAndroidアプリを立ち上げる準備ができます。 完了したら、下記のコマンドを打ちましょう。

meteor add-platform android

ターミナルでライセンス条項に同意したら、下記の通りにコマンドを打ってください。

meteor run android

しばらく初期化処理が走った後、Androidエミュレータが立ち上がり、ネイティブAndroid環境でアプリが動くでしょう。 エミュレータの挙動はそこそこ遅いので、実際にアプリとして使った感じを試したければ、実機でアプリを動かす必要があります。

Androidの実機で動かす

まずは上記Android向けステップを全て完了させましょう。 そうしたら、お持ちの実機でUSBデバッグが可能な状態にあることを確認し、デバイスをUSBでパソコンに繋ぎましょう。

また、デバイス上で動かす前にAndroidエミュレーターを止めておくことをお忘れなきように。

その上で、下記のコマンドを打ちましょう。

meteor run android-device

これでアプリが立ち上がり、デバイスにインストールされます。

iPhoneiPadで動かす (Mac限定、Apple developer accoutが必要)

Apple developer accountをお持ちなら、iOSの実機でもアプリを動かせます。 下記のコマンドを打ってください。

meteor run ios-device

これであなたのiOSアプリのプロジェクトがXcodeで立ち上がります。 Xcodeでは、Xcodeがサポートするデバイスならどれでもアプリを立ち上げることができます。

これで、Meteorでスマホアプリを立ち上げることがいかに簡単か分かりましたね。

次回以降ではさらにアプリに機能を追加していきましょう。


無事にシミュレーターを立ち上げられたでしょうか? 苦労した人もいると思いますが、それにしても割りと簡単にアプリの立ち上げまで行ける方だと思います。

よくわからない中でも、頑張って調べた経験はきっとどこかで活きるので、時間のある方は是非スマホアプリでの立ち上げにも挑戦してみてください。

第七弾は今日明日で投稿します。

MeteorJSとReactを勉強してみる その5: タスクの完了と削除を実装してみよう【公式翻訳】

MeteorJSを勉強してみる第5弾です。 タスクを追加できるなら、もちろん削除もできるようにしたいですよね。


タスクの完了と削除を実装する

ここまでは、ただコレクションにドキュメント(タスクのデータ)を追加してきたのみでした。

今回は、これまでに追加したドキュメントを更新する方法と、削除する方法を学びましょう。

タスクのコンポーネントに、チェックボックスと削除ボタン、二つの要素を追加します。 いずれもイベントハンドラと一緒に追加してあげましょう。

imports/ui/Task.js

before

import React, { Component } from 'react';

// Task component - represents a single todo item
export default class Task extends Component {
  render() {
    return (
      <li>{ this.props.task.text }</li>
    );
  }
}

after

import React, { Component } from 'react';

import { Tasks } from '../api/tasks.js';

// Task component - represents a single todo item
export default class Task extends Component {
  toggleChecked() {
    // set the checked property to the opposite of its current value
    Tasks.update(this.props.task._id, {
      $set: { checked: !this.props.task.checked },
    })
  };

  deleteThisTask() {
    Tasks.remove(this.props.task._id);
  };

  render() {
    // give tasks a different className when they are checked off,
    // so that we can style them nicely in CSS.
    const taskClassName = this.props.task.checked ? 'checked' : '';

    return (
      <li className={ taskClassName }>
        <button className="delete" onClick={ this.deleteThisTask.bind(this) }>
          &times;
        </button>

        <input
          type="checkbox"
          readOnly
          checked={ !!this.props.task.checked }
          onClick={ this.toggleChecked.bind(this) }
        />

        <span className="text">{ this.props.task.text }</span>
      </li>
    );
  }
}

ドキュメントのアップデート

上記のコード内では、タスクにチェックをつけて完了させる際にTasks.updateを呼んでいます。

コレクションの持つupdate関数は、二つの引数を取ります。

  • 一つ目はコレクションの中でも「どのドキュメントを更新するか」を特定するセレクタ
  • 二つ目はマッチしたオブジェクトに何をすべきかを示す更新内容のパラメータ

です。

この場合、セレクタはタスクの_idです。 また、更新パラメータでは、「そのタスクは完了している」ことを表すためのcheckedを切り替えるために$setを使用しています。

ドキュメントの削除

また、上記コード内ではタスクを削除するためにTasks.removeを呼んでいます。 remove関数は引数をひとつだけ取り、その引数で「どのドキュメントを削除するか」を特定します。


短いですが第五回はこれだけです。

今更ですが、ここまで出てきた「コレクション」とか「フィールド」とか「ドキュメント」などは全てMongoDBの用語です。

僕は普段の業務ではMySQLを使っているので正直あまり慣れないですが、基本的な考え方は一緒です。

まだ調べてなかった人は、是非一度自分でググってみてください。

今週中にあと二記事くらいは上げたい所存です。