Meteorのチュートリアル

GWということもあって、以前からやろうやろうと思っていたMeteorのチュートリアルを時間を取って消化しようと思う。

https://www.meteor.com/

Meteorとは

オープンソースのNode.jsフレームワークです。というよりも基本的にすべてが含まれていてNode.jsのフレームワークなの?という疑問が生じますが。
Meteor専用の拡張子.blazeが使用されており、それによってHTMLを使用しないデータのレンダリングをしています。

コンセプトとしては
– one language – 一つの言語で統一
– data on the wire – HTMLを使用しないデータのレンダリング
– embraces the ecosystem – ビルドツールを使ったエコシステム
– full stack reactivity – フルスタックの提供

UI構築なども最小限に抑えられるような形で進めています。まぁ何がやりたいのかというと、Meteorで全部できるよってことだと思う。

この溢れ出るフルスタック感

  • React
  • Vue
  • Angular
  • GraphQL
  • mongoDB
  • Corodova

これらのJavaScriptライブラリーが含まれたフルスタックフレームワークです。
プラットフォーム的にはデスクトップにもモバイルにも対応していて最強感が出てます。
しかしながら日本では一般的に使われているイメージがないです。というよりも、NoSQL採用というところで引っかかてるんじゃないでしょうか?海外のサイトはわりとNoSQLを採用している会社を見かけますが、日本ではRDBで構築する機会が多いですし、何よりも情報という面で日本語情報が少ない。その点で採用されていないのかもしれないですね。

基本的にNode.jsで使われているフレームワークはは柔軟性の高いExpress.jsが一人勝ちしていて、SailsやKoaなども採用されている印象があまりないです。
(日本はNode.js案件自体が多い印象がない。近年だとRailsのリプレイスにGoが使われてきている印象)
なお、日本の自動車メーカーのMazda、オランダのサイトはMeteorで組まれてます。

自分はJavaScript Developerという身分なのでMeteorもカバーしておこうと思います。
というわけで、チュートリアルに沿ってコードを書いてみる。

開発

チュートリアルはTODOアプリでReactを選択
Todo App with React

それではやりましょう。

$ curl https://install.meteor.com/ | sh
$ meteor create simple-todos --react
$ cd simple-tods
$ yarn start

※パッケージダウンロードに時間がかかります。
meteor createコマンドに--reactオプションをつけることでReactのボイラープレートを作ってくれます。スタートコマンドで立ち上げてhttp://localhost:3000/にアクセスすると画面が見えます。
ディレクトリを見るとわかりますが、本来Meteorは.htmlの拡張子が使われたものが用意されていますが、Reactに置き換わっています。これで準備は整いました。

残念ながらMeteorのチュートリアルはHooksで書かれていないので、フロントに関しては自分流で書き換えます。

$ nvim imports/ui/App.jsx

Read

App.jsxを書き換える

import React from 'react';

const getTasks = () => {
  return [
    { _id: 1, text: 'This is task 1' },
    { _id: 2, text: 'This is task 2' },
    { _id: 3, text: 'This is task 3' },
  ];
}

const Tasks = () => {
  return (
    <ul>
      {getTasks().map((task) => (
        <li key={task._id}>{task.text}</li>
      ))}
    </ul>
  )
}

export const App = () => {
  return (
  <div className="container">
    <header>
      <h1>Todo List</h1>
    </header>
    <Tasks />
  </div>
  );
}

CSSはまんまコピペ
client/main.css

/* CSS declarations go here */
body {
  font-family: sans-serif;
  background-color: #315481;
  background-image: linear-gradient(to bottom, #315481, #918e82 100%);
  background-attachment: fixed;

  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;

  padding: 0;
  margin: 0;

  font-size: 14px;
}

.container {
  max-width: 600px;
  margin: 0 auto;
  min-height: 100%;
  background: white;
}

header {
  background: #d2edf4;
  background-image: linear-gradient(to bottom, #d0edf5, #e1e5f0 100%);
  padding: 20px 15px 15px 15px;
  position: relative;
}

#login-buttons {
  display: block;
}

h1 {
  font-size: 1.5em;
  margin: 0;
  margin-bottom: 10px;
  display: inline-block;
  margin-right: 1em;
}

form {
  margin-top: 10px;
  margin-bottom: -10px;
  position: relative;
}

.new-task input {
  box-sizing: border-box;
  padding: 10px 0;
  background: transparent;
  border: none;
  width: 100%;
  padding-right: 80px;
  font-size: 1em;
}

.new-task input:focus{
  outline: 0;
}

ul {
  margin: 0;
  padding: 0;
  background: white;
}

.delete {
  float: right;
  font-weight: bold;
  background: none;
  font-size: 1em;
  border: none;
  position: relative;
}

li {
  position: relative;
  list-style: none;
  padding: 15px;
  border-bottom: #eee solid 1px;
}

li .text {
  margin-left: 10px;
}

li.checked {
  color: #888;
}

li.checked .text {
  text-decoration: line-through;
}

li.private {
  background: #eee;
  border-color: #ddd;
}

header .hide-completed {
  float: right;
}

.toggle-private {
  margin-left: 5px;
}

@media (max-width: 600px) {
  li {
    padding: 12px 15px;
  }

  .search {
    width: 150px;
    clear: both;
  }

  .new-task input {
    padding-bottom: 5px;
  }
}

構成は至ってシンプル。

こちらのリンクを見てもらえればわかる。
Example directory layout

clientはフロントエンドに関するものが入っており、Reactのお決まりの部分が書かれている
対してserverはExpress.jsと同じ要領でmain.jsが存在しており、この中にapiのモジュールをインポートしてつないでいく。
なお、importsディレクトリにはapiとuiがり、uiにはコンポーネントを格納する場所となっている、対してapiはコレクションなんかのサーバーサイドのモジュールを格納しておけるような構成になっている。

apiにtasks.jsを作成

import { Mongo } from 'meteor/mongo';

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

ダミーデータの表示からサーバーからデータを取得できるようにする

server/main.js

import '../imports/api/tasks.js';

ロードするようにmain.jsにも追記

次にAppに対してHOCを使用してpropsを通してDBからのデータが渡るように処理をする

import React from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import { Tasks } from '../api/tasks.js';

const App = (props) => {
  console.log(props); // 確認用
  return (
  <div className="container">
    <header>
      <h1>Todo List</h1>
    </header>
    <ul>
      {props.tasks.map((task) => (
        <li key={task._id}>{task.text}</li>
      ))}
    </ul>
  </div>
  );
}
export default withTracker(() => {
  return {
    tasks: Tasks.find({}).fetch(),
  };
})(App);

exportの指定をdefaultにしたのでApp.jsxの方も修正を忘れずに。

import App from '/imports/ui/App';

サーバーを立ち上げたままの状態でmongoDBにテストデータをインサートしましょう。

$ meteor mongo
$ db.tasks.insert({ text: "Hello world!", createdAt: new Date() });

ブラウザで確認、DBからのデータが表示されていればOK
なお、インサートしたデータはサーバーを落としてもmongoDBの方に格納されています。

Create

続いてインサート処理。Hooksを使って処理を行う。
ここまでくれば一緒でMongoDBのインサートをclickイベント時に行うだけ
textとcreatedAtのオブジェクトに値が入るようにする。mongoDBなのでスキーマが違っていても、オブジェクトの形式でインサートするとデータが入ってしまうので注意する。

import React, { useState } from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import { Tasks } from '../api/tasks.js';

const App = (props) => {
  console.log(props);
  const [value, setValue] = useState();
  const handleSubmit = () => {
    Tasks.insert({
      text: value,
      createdAt: new Date(), // current time
    });
    setValue('');
    event.preventDefault();
  }
  return (
  <div className="container">
    <header>
      <h1>Todo List</h1>
    </header>
    <form className="new-task">
      <input
        type="text"
        placeholder="Type to add new tasks"
        onChange={(e) => setValue(e.target.value)}
      />
      <button onClick={()=> handleSubmit()}>Send</button>
    </form>
    <ul>
      {props.tasks.map((task) => (
        <li key={task._id}>{task.text}</li>
      ))}
    </ul>
  </div>
  );
}
export default withTracker(() => {
  // ソート処理を追加
  return {
    tasks: Tasks.find({}, { sort: { createdAt: -1 } }).fetch(),
  };
})(App);

これで追加処理ができた。本来はバリデーションを追加したり、やることは多いのだけれど、今回は省く。

Delete & Update

更新処理とデリート処理を実装します。
まずデリート処理はtaskのidをmongoDBにリクエストするだけです。
次に更新処理はDoneチェックはを入れます。チェックボックスを配置してcheckedの値をmongoDBに更新リクエストさせます。そうすることで、呼び出し時にcheckedの値を持っているかもっていないかで終了済みかそうでないかが判定ができるようになります。

import React, { useState } from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import { Tasks } from '../api/tasks.js';

const App = (props) => {
  console.log(props)
  const [value, setValue] = useState();
  const handleSubmit = () => {
    Tasks.insert({
      text: value,
      createdAt: new Date(), // current time
    });
    setValue('');
    event.preventDefault();
  }

  const toggleChecked = (task) => {
    Tasks.update(task._id, {
      $set: { checked: !task.checked },
    });
  }

  const deleteThisTask = (task) => {
    Tasks.remove(task._id);
  }

  return (
  <div className="container">
    <header>
      <h1>Todo List</h1>
    </header>
    <form className="new-task">
      <input
        type="text"
        placeholder="Type to add new tasks"
        onChange={(e) => setValue(e.target.value)}
      />
      <button onClick={()=> handleSubmit()}>Send</button>
    </form>
    <ul>
      {props.tasks.map((task, i) => {
        const taskClassName = task.checked ? 'checked' : '';
        return (
          <li key={i} className={taskClassName}>
            <button className="delete" onClick={ () => deleteThisTask(task)}>
              ×
            </button>
            <input
              type="checkbox"
              readOnly
              checked={!!task.checked}
              onClick={() => toggleChecked(task)}
            />
            <span className="text">{task.text}</span>
          </li>
        )
      })}
    </ul>
  </div>
  );
}
export default withTracker(() => {
  // ソート処理を追加
  return {
    tasks: Tasks.find({}, { sort: { createdAt: -1 } }).fetch(),
  };
})(App);

まとめ

こちらのコードはGithubのリポジトリに上げておきました。

Meteor-Tutorial

使った所感としてはMeteorかなりよいですね。React + firebaseを使うくらいならMeteorでReact + MongoDBでサクッと実装するのがよいかもしれません。構成自体も難しくないですし、フルセットですぐに実装ができるため、すごくよいフレームワークだと思います。

関連記事