概要

Meteorのチュートリアル(TODOアプリの作成)を進める中で得た考察・メモについて書きます。
これからチュートリアルに取り組む方は、各章のおわりに本記事を参照すると理解の助けになると思います。

フロントエンドのフレームワークはReact, Blaze, Angular が選べますが、
本記事ではReactのみ扱います。

筆者は投稿時点でWeb開発歴1年未満、Meteor, React, NodeJS, MongoDBは経験なしというスキルのため、
あくまでチュートリアル上で読み取れる情報から判断したことを書いていますので、
有識者の方は補足などあればコメントいただけると幸いです。

Meteor について

https://www.meteor.com

あまり日本では著名でないようなので説明します。
MeteorはすべてのコードをJavascriptで記述する、フルスタックのWebフレームワークです。
MEANスタックと近い構成で、NodeJS, MongoDB, また著名なフロントエンドのフレームワークが統合されています。

またCordovaも統合されているので、モバイル環境へのビルドがサポートされています。

開発スピードの速さを重要なフィーチャーとしていて、特に煩わしさを感じさせないワークフローが個人的には魅力的です。

  • コマンド一発でフルスタックなWeb開発環境をセットアップ (meteor create)
  • コマンド一発でデプロイ (meteor deploy, mup(meteor up))
  • コマンド一発でモバイル端末にビルド(meteor run ios/android, meteor run ios-device/android-device)
  • ホットコードプッシュ - コードの変更が即座に反映される
  • Optimistic UI
    • 通信を必要とするUIの操作を行った際、レスポンスを予測して即座にUIに反映する。サーバーのパフォーマンスを意識する必要がない楽観的(optimistic)なUI。
  • クレバーなパッケージ管理の思想
    • パッケージを一切追加していない、素の状態が最もセキュリティが高い。
    • プロジェクトテンプレートを生成($ meteor create)すると、セキュリティに穴を開けるパッケージが自動的に追加される。これによりプロトタイピングが容易な状態になっている
    • プロトタイピングが終わったら、パッケージリストを眺めてセキュリティを下げているパッケージを削除していけばよい。これにより、個人開発特有の「セキュリティに自信がないことによるローンチへの躊躇」がある程度改善される。

TO DO APP tutorial with React

1. Creating an app - Creating your first app

$ meteor create simple-todos でプロジェクトのテンプレートが生成されます。
テンプレートは .gitignore まで用意されており、これにより
プロジェクトの立ち上がりによくある「どれどれ、適当なGithubのリポジトリから .gitignore を拾ってくるか・・・」がないことを意味します。

$ meteor コマンドでビルトインのWebサーバー、MongoDBが立ち上がります。

コードの変更が即座にWebアプリに反映されるフィーチャーはホットコードプッシュと言います。

2. Components - Defining views with React components

Reactは, みたところHTMLの実体を返すような、 render() という関数を備えたコンポーネント(クラス)の集まりでできています。
またトップレベルのコンポーネントは App という名前で、この render() の中で、さらに子のコンポーネントを呼び出してHTMLを構成する仕組みになっています。
JSの中にHTMLが取り込まれたような文法となっていて、ひとつのコンポーネントを作ることは、一つのタグを作る行為に似ています。

またコンポーネントに与えられる情報はprops(多分propertiesの略)といって、必ず親のコンポーネントから与えられます。
つまりコンポーネントは完全なツリー構造になっているので、親がどんなpropsを与えるか、事前に設計されているのが望ましいと思われます。

ReactとBlazeの比較について

Reactのお作法を垣間見たところで、Reactという選択が正しかったのか不安になったので調べました。

React vs. Blaze
こちらの記事によると、

In closing

Go play with React! If for no other reason than to learn and experiment.

ということだったので、とても勇気付けられReactのまま進めることにしました。Angularは知らない。

3. Collections - Storing tasks in a collection

Meteorでは永続的なデータ保存の仕組みとしてCollectionsというものが用意されています。(チュートリアル上の文脈では、MongoDBと Meteor.Collections APIを指すと思われます)
MongoDBでは、SQLなどで言うテーブルは Collection,
レコードは Document が意味として一致します。

imports/api/tasks.js は、この時点ではファイル名からしか察せないのですが、
いわゆるDAO関数的なものを書くファイルです。

App.jsに追加した withTracker() について

まず、 react-meteor-data をaddすることにより、withTrackerという関数の定義を通じて、CollectionsからReactへのデータの受け渡しが可能になります。
使い方としては、withTrackerの引数にオブジェクトを与えると、それがAppのpropsとして参照できるようになります。

Tasks.find({}).fetch() の Tasks は、imports/api/tasks.js で定義したTasksです。

export const Tasks = new Mongo.Collection('tasks');

MongoDB, Collectionsについて

Tasks.find({}).fetch() というシンタックスからわかるように、
LAMP環境でよくあるDAO関数を通じてデータをfetchしてくるのではなく、
Collectionのオブジェクト自体がfind()などのCRUD関数を持っているので、それを通じてデータを操作します。

またどう見てもクライアントのコードにクエリが書かれているのですが、
MeteorではあたかもクライアントからDBへのアクセスしているかのような書き方が可能なようです。

4. Forms and events - Adding tasks with a form

handleSubmit.bind(this) について

<form className="new-task" onSubmit={this.handleSubmit.bind(this)} >
この bind(this) の意味について、ググってもすぐにわからなかったので説明します。

handleSubmitはどんなformからでも呼び出せるような関数として作りたいのですが、
handleSubmit内で, document.getElementById() などでIDを指定しDOMを取得する方法だとその目的は達成できません。
そこで bind() の引数としてthisを与えることで、
handleSubmit内で参照する this は、常に呼び出し元のformを指すようになります。

event.preventDefault() について

  handleSubmit(event) {
    event.preventDefault();

この event.preventDefault(); は素のJSの関数です。
これを呼ぶとイベントをキャンセルできます。
このチュートリアルのケースでいうと、submitした時のページ遷移を抑止できます。
参考: https://www.w3schools.com/jsref/event_preventdefault.asp

Reactで定義したDOMを探す

Reactで定義したDOMを探すときは、 ReactDOM.findDOMNode() を使います。
テキストボックスのDOMに ref="textInput" というattributeを与えましたが、それにより
ReactDOM.findDOMNode(this.refs.textInput) という引き当てが可能になります。

MongoDB 参考記事

MongoDB のメソッドと SQLクエリ並べた解説がこちらにあります。
[Mongo] findメソッドのいろいろな使い方(MySQLと比較) - YoheiM .NET

5. Update and remove - Checking off and deleting tasks

checked={!!this.props.task.checked}

checked={!!this.props.task.checked} のシンタックスについて。
!!はそのまんま2重否定なのですが、
this.props.task.checked が null やら undefined だった場合でも強制的にboolにできるという効果があります。
左辺の checked はReactのプロパティなのですが、bool以外はあかんぞというお作法かもしれないです。
参考: http://www.f-sp.com/entry/2016/11/18/190732#x--xy

ここらへんから初心者お断り感が垣間見えます。

6. Running on mobile - Running your app on Android or iOS

ここは手順に従うだけです。
iPhoneアプリでのリリースを考えてる方は、ここでAppleのデベロッパープログラムに登録しましょう。(迷わず)

iOSのみ試しましたので、エミュレータのイメージを貼っておきます。
スクリーンショット 2018-04-16 22.59.17.png

iPhoneへのビルドもやってみました。

requires a development team. Select a development team in the project editor.Code signing is required for product type 'Application' in SDK 'iOS 10.0'

xcode - requires a development team. Select a development team in the project editor.Code signing is required for product type 'Application' in SDK 'iOS 10.0' - Stack Overflow

7. Temporary UI state - Storing temporary UI data in component state

this.state

Reactのコンポーネントは this.state というプロパティがあって、これに自由に値を設定できます。
ここではこの性質を使って、完了したタスクの表示・非表示切り替えを追加しています。

this.stateはコンストラクタで初期化するお作法のようです。

  constructor(props) {
    super(props);

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

例えばcheckboxに state.hideCompleted を指定しておくと
setState() でhideCompletedを変更した時に、非同期的にチェックの状態を変更してくれます。

そしてsetState()しない限りはチェックボックスのONOFFはできないようになっています。(試しにsetStateの中身を空にしてみましょう。)
チェックボックスのONOFFの制御はブラウザではなくReactが握っているということになります。

renderTasks 完了タスク非表示のロジックについて

renderTasksに完了タスク非表示のロジック入れたとき、おや?となると思います。
イベント登録をしてないのに、チェックを切り替えたら renderTasks() が呼ばれる動きをしています。
これは、stateが変更されるとrender()がもう1度実行されるという、Reactの挙動によるものです。

つまり、何かしらのstateが変わるたびに、全てのReactDOMは絶滅しもう一度作り直す(論理的には)という思想が伺えます。
このことから、大量のDOM差し替えが走らないような作りにするのが望ましいことが伺えます。

参考:Reactにおけるパフォーマンスチューニング(改善編)

8. Adding user accounts - Adding user accounts

Meteorのユーザー管理プラグイン accounts-ui, accounts-password を取り込むとユーザー管理が簡単に実装できるようです。
ところが今のところBlazeにしか対応してないので、ここではReactでBlazeのレンダリング結果をラップするようにして、それをComponent化しています。

Blaze.render() でログインボタンのテンプレートをレンダリングし、this.viewに入れています。
ここだけはよくわからなかったのですが、
this.view におもむろに突っ込むめばレンダリングされるようです。(ちなみにthis.viewはReactのComponentのプロパティではない模様)

また以下の便利なAPIが利用できます。
- Meteor.userId() でログインしてるユーザーのIDが取れる
- Meteor.user().username でユーザー名が取れる

9. Security with methods - Security with methods

ここまでの時点では、MongoDBに出すクエリはぜんぶクライアント側のコードに書かれていて、
セキュリティ的に全然よろしくないのでやめる章。

MeteorではMethods を定義するのがいい方法、とチュートリアルに記載がありますが、methods とはいわゆるDAO関数的なものをいうようです。
(一般用語なのかは不明)

meteor remove insecure

なんと今まではそれを許容するために、自動的に insecure というパッケージが追加されていたので、
$ meteor remove insecure で取り除きます。

セキュリティを強化するときにパッケージを追加するのではなく、抜け穴を作るパッケージを取り除くという思想はとてもクレバーです。

そもそもセキュリティに疎い人(私とか)がwebシステム作ろうとした時には、
まともなセキュリティを保つためにどんなパッケージが足りないのか?を色々調べる必要がありますが、
この思想だと、パッケージリストを眺めてinsecureっぽいものを削除していくだけで、
セキュリティ耐性が上がっていくので、心理的にもとても楽です。

またプロダクションコードが素の状態に近いということは、管理が簡単になることを意味します。
旧来のサーバー環境で、本番環境の稼働のためにセキュリティに関するパッケージ入れまくった結果、パッケージリストが複雑になるのは容易に想像できます。

ちなみに、サーバー動いてるときにパッケージの削除をしても、ホットコードプッシュが働くので、
パッケージを追加・削除するときにいちいちサーバーを落とす必要はありません。

imports/api/tasks.js

imports/api/tasks.js にCRUD操作をする Methods を定義していきます。

username: Meteor.users.findOne(this.userId).username,
これについて、Meteor.usersはMeteorのAPIです。
https://docs.meteor.com/api/accounts.html#Meteor-users

Mongo.collection のオブジェクトで、 findOne(this.userId) でユーザーのdocumentをとってきているようです。

Meteor.call('tasks.insert', text);
クライアントのコードで、今まで直にMongoDBを叩いていたところをこれに置き換えます。
call は、 Function.prototype.call() じゃなくて MeteorのAPIです。
https://docs.meteor.com/api/http.html#HTTP-call

すると imports/api/tasks.js で定義した Methods が呼ばれます。

一見すると記述の箇所を移動しただけのようにも見えますが、 imports/api/tasks.jsについては同じコードをクライアントとサーバー 両方でみられる(実行される)ようになります。

Optimistic UI

通信の必要なUI操作をしたとき、クライアントは AJAXのようなリクエストを送りますが、
サーバーからレスポンスが帰ってくるまで、レスポンスを予測してUIに反映させるという機能が備わっています。

A simulation of the method runs directly on the client to attempt to predict the outcome of the server call using the available information
What this means is that a newly created task actually appears on the screen before the result comes back from the server.

そしてレスポンスが届いたら、実際の結果をまたUIに反映します。

このフィーチャーは Optimistic UI (直訳で,楽観的UI)というそうです。
これにより、サーバーの応答時間を気にする必要がなくなります。

関数をオブジェクトのプロパティとして定義する文法

{
  'tasks.insert'(text) {
    check(text, String);
  ...

こういうシンタックスが出てきますが、関数をオブジェクトのプロパティとして定義するというだけのものです。
(あまり見慣れなかった)

ブラウザのコンソールにて、↓のようなコードで同じ文法を試すとよいと思います。

> const fs = { 'func2'(){console.log('bbbb')} }
> fs.func2()
bbbb

10. Publish and subscribe - Filtering data with publish and subscribe

今は、DBにあるすべてのデータをクライアントに渡している状態ですが、
例えば今からプライベートなタスクだけ表示するような実装を加えたとしても、
データとしては全員のタスクを受け取っていることになるので、セキュリティ的に全然よくないので是正していく章です。

これも、なんと今は autopublish というパッケージが自動的に追加されていることによって許容されている状態なので
$ meteor remove autopublish これで取り除きます。
すると, 送信が明示されてないデータは配信されなくなる
=今はタスクリストが空になっているので、配信するデータを Meteor.publish で明示していきます。

imports/api/tasks.js に Meteor.publish() の記述を追加する

おなじみの imports/api/tasks.js に Meteor.publish() の記述を追加していきます。

if (Meteor.isServer) {
  // This code only runs on the server
  Meteor.publish('tasks', function tasksPublication() {
    return Tasks.find();
  });
}

imports/api/tasks.js はサーバーとクライアント両方に読まれるので、
Meteor.isServer でコードを囲み、サーバー側だけに読まれるようにします。

そしてクライアントでも、サブスライブするデータを明示していきます。

export default withTracker(() => {
  Meteor.subscribe('tasks');

publish, subscribe の対象を紐づけているのが、チュートリアル上でいう publication name で、
ここでいうと ‘tasks’ がそれにあたります。
この方式はpublish/subscribe モデルというようです。

this.props.currentUser && this.props.currentUser._id

imports/ui/App.js
const currentUserId = this.props.currentUser && this.props.currentUser._id;

こういうシンタックスが出てきますが、
目的は this.props.currentUser が null/undefined だったときに currentUserId に null をいれる、という処理を簡素にしたい。
this.props.currentUser の null チェックを短く書きたい、というものです。

これを実現するためJSの3つの性質を利用します。
まず、変数が null/undefined だと、booleanとして評価したときにfalseになります。

> foo = null
> Boolean(foo)
false
> foo = 'yay'
> Boolean(foo)
true

そして残りの2つの性質

  • && は左辺を強制的にbooleanとして評価する
  • && は両辺がbooleanじゃないときは、trueなら右辺、falseなら左辺の値を返す

これらを利用し、次のような書き方が可能になります。

> foo = 'yay'
> baz = 'yap'
> foo && baz
"yap"
> foo = null
> foo && baz
null

信じがたいテクニックですが、現実として使われているようです。
参考: http://www.f-sp.com/entry/2016/11/18/190732#x--xy

11. Testing - Testing

mocha と chai というテストフレームワークを使います。
この例だと、api/tasks.js の tasks.remove() をテストしています。

mocha でテストの初期化、chaiでテスト(アサーション)を評価を行います。
mocha では, beforeEach() を記述してるとテスト前に呼ばれるので、それを駆使してタスクが1件だけある状態のDBを作ります。
chai では it() 関数がひとつのテストの単位となるようですが、この引数のコールバック関数にテストを記述していきます。

        const deleteTask = Meteor.server.method_handlers['tasks.remove'];

        // Set up a fake method invocation that looks like what the method expects
        const invocation = { userId };

        // Run the method with `this` set to the fake invocation
        deleteTask.apply(invocation, [taskId]);

        // Verify that the method does what we expected
        assert.equal(Tasks.find().count(), 0);

テストを実行するときは、
Meteor.server.method_handlers[‘tasks.remove’]; でメソッドのハンドラを取得して、
ハンドラ.apply() で関数呼び出すのがお作法のようです。

invocation はパラメータみたいな意味合いだと思いますが一般用語なのか不明です。
※どちらかというとコンテキストのような意味合いでは?とのご意見をいただきました。

12. Next step - What's next?

各々頑張りましょう!
ちなみにWebアプリで大変重要なデプロイについて説明がないことに気付いたと思いますが、
Guideに説明がありました。
https://guide.meteor.com/deployment.html

所感など

チュートリアルが初心者お断りなのと、日本語の情報があまりないところが寂しさを感じますが、
アイデアを素早く実現したいタイプのWebアプリで効果を発揮しそうなので、今後いい感じのアイデアがあれば利用していきたい所存です。

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.