シンプルなアプリケーション
Mithril を使用する際に必要となる主要な機能をどのように使うかを示す、シンプルなアプリケーションを開発してみましょう。
最終結果のインタラクティブな例は、こちら でご覧いただけます。
まず、アプリケーションのエントリポイントを作成しましょう。index.html
ファイルを作成します。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>My Application</title>
</head>
<body>
<script src="bin/app.js"></script>
</body>
</html>
<!doctype html>
は、HTML 5 ドキュメントであることを示します。最初の charset
meta タグはドキュメントのエンコーディングを、viewport
meta タグはモバイルブラウザでのページの拡大縮小方法を指定します。title
タグには、このアプリケーションのブラウザタブに表示されるテキストが含まれており、script
タグはアプリケーションを制御する JavaScript ファイルへのパスを示しています。
アプリケーション全体を 1 つの JavaScript ファイルで作成することも可能ですが、後でコードベースをナビゲートするのが難しくなる可能性があります。代わりに、コードを_モジュール_に分割し、それらのモジュールを_バンドル_して bin/app.js
を作成しましょう。
バンドラツールを設定する方法はたくさんありますが、そのほとんどは npm 経由で配布されています。実際、Mithril を含む最新の JavaScript ライブラリやツールのほとんどがそのように配布されています。npm をダウンロードするには、Node.js をインストールしてください。npm は Node.js と一緒に自動的にインストールされます。Node.js と npm をインストールしたら、コマンドラインを開き、次のコマンドを実行します。
npm init -y
npm が正しくインストールされている場合、package.json
ファイルが作成されます。このファイルには、スケルトンプロジェクトのメタ記述が含まれています。このファイルでプロジェクトと作成者の情報を編集してください。
Mithril.js をインストールするには、インストール ページの指示に従ってください。Mithril.js がインストールされたプロジェクトのスケルトンができたら、アプリケーションを作成する準備が完了です。
まず、状態を保存するモジュールを作成します。src/models/User.js
というファイルを作成しましょう。
// src/models/User.js
var User = {
list: [],
};
module.exports = User;
次に、サーバーからデータをロードするコードを追加します。サーバーと通信するには、Mithril.js の XHR ユーティリティである m.request
を使用できます。まず、Mithril.js をモジュールに含めましょう。
// src/models/User.js
var m = require('mithril');
var User = {
list: [],
};
module.exports = User;
次に、XHR 呼び出しをトリガーする関数を作成します。loadList
と名付けましょう。
// src/models/User.js
var m = require('mithril');
var User = {
list: [],
loadList: function () {
// TODO: make XHR call
},
};
module.exports = User;
次に、m.request
呼び出しを追加して XHR リクエストを行います。このチュートリアルでは、REM (DEAD LINK, FIXME: https //rem-rest-api.herokuapp.com/) API (迅速なプロトタイピング用に設計されたモック REST API) に対して XHR 呼び出しを行うことにします。この API は、GET https://mithril-rem.fly.dev/api/users
エンドポイントからユーザーのリストを返します。m.request
を使用して XHR リクエストを行い、そのエンドポイントからのレスポンスでデータを設定しましょう。
注: REM エンドポイントを機能させるには、サードパーティ Cookie を有効にする必要がある場合があります。
// src/models/User.js
var m = require('mithril');
var User = {
list: [],
loadList: function () {
return m
.request({
method: 'GET',
url: 'https://mithril-rem.fly.dev/api/users',
withCredentials: true,
})
.then(function (result) {
User.list = result.data;
});
},
};
module.exports = User;
method
オプションは、HTTP メソッド を指定します。サーバーに副作用を与えることなくデータを取得するには、GET
メソッドを使用する必要があります。url
は API エンドポイントのアドレスです。withCredentials: true
は、Cookie を使用することを示します (REM API の要件)。
m.request
呼び出しは、エンドポイントからのデータで解決される Promise を返します。デフォルトでは、Mithril.js は HTTP レスポンスのボディが JSON 形式であるとみなし、自動的に JavaScript オブジェクトまたは配列にパースします。.then
コールバックは、XHR リクエストの完了後に実行されます。この例では、コールバックで result.data
配列を User.list
に代入しています。
loadList
に return
ステートメントがあることにも注意してください。これは Promise を扱う際の一般的な良い習慣であり、XHR リクエストの完了後に実行するコールバックをさらに登録することを可能にします。
このシンプルなモデルは、User.list
(ユーザーオブジェクトの配列) と User.loadList
(サーバーデータで User.list
を設定するメソッド) の 2 つのメンバーを公開します。
次に、User モデルモジュールからデータを表示するためのビューモジュールを作成しましょう。
src/views/UserList.js
というファイルを作成します。まず、Mithril.js とモデルをインポートしましょう。両方とも必要になります。
// src/views/UserList.js
var m = require('mithril');
var User = require('../models/User');
次に、Mithril.js コンポーネントを作成しましょう。コンポーネントは、view
メソッドを持つオブジェクトです。
// src/views/UserList.js
var m = require('mithril');
var User = require('../models/User');
module.exports = {
view: function () {
// TODO add code here
},
};
デフォルトでは、Mithril.js ビューは hyperscript を使用して記述されます。Hyperscript は、複雑なタグに対して HTML よりも自然にインデントできる簡潔な構文を提供します。また、JavaScript で記述されているため、JavaScript のツール群を最大限に活用できます。例えば:
- Babel を使用して、ES6+ を IE 用に ES5 にトランスパイルしたり、JSX (インライン HTML のような構文拡張) を適切な hyperscript 呼び出しにトランスパイルしたりできます。
- ESLint を使用して、特別なプラグインなしで簡単に linting できます。
- Terser または UglifyJS (ES5 のみ) を使用して、コードを簡単に minify できます。
- Istanbul を使用して、コードカバレッジを取得できます。
- TypeScript を使用して、コードを簡単に分析できます。(コミュニティがサポートする型定義 が利用可能であるため、独自にロールする必要はありません。)
hyperscript から始めて、アイテムのリストを作成してみましょう。Hyperscript は Mithril.js で推奨される記述方法ですが、JSX もよく似ています。
// src/views/UserList.js
var m = require('mithril');
var User = require('../models/User');
module.exports = {
view: function () {
return m('.user-list');
},
};
".user-list"
文字列は CSS セレクターであり、予想どおり、.user-list
はクラスを表します。タグが指定されていない場合、div
がデフォルトです。したがって、このビューは <div class="user-list"></div>
と同じ意味になります。
次に、以前に作成したモデル (User.list
) からユーザーのリストを参照し、データを動的に表示してみましょう。
// src/views/UserList.js
var m = require('mithril');
var User = require('../models/User');
module.exports = {
view: function () {
return m(
'.user-list',
User.list.map(function (user) {
return m('.user-list-item', user.firstName + ' ' + user.lastName);
})
);
},
};
User.list
は JavaScript 配列であり、hyperscript ビューは単なる JavaScript であるため、.map
メソッドを使用して配列をループ処理できます。これにより、div
のリストを表す vnode の配列が作成され、それぞれにユーザーの名前が含まれます。
もちろん、User.loadList
関数を呼び出していないため、User.list
はまだ空の配列であり、このビューは空白のページを表示することになります。このコンポーネントのレンダリング時に User.loadList
を呼び出すには、コンポーネントの ライフサイクルメソッド を利用します。
// src/views/UserList.js
var m = require('mithril');
var User = require('../models/User');
module.exports = {
oninit: User.loadList,
view: function () {
return m(
'.user-list',
User.list.map(function (user) {
return m('.user-list-item', user.firstName + ' ' + user.lastName);
})
);
},
};
コンポーネントに oninit
メソッドを追加し、User.loadList
を参照させていることに注意してください。これは、コンポーネントが初期化されると、User.loadList が呼び出され、XHR リクエストがトリガーされることを意味します。サーバーが応答を返すと、User.list
が設定されます。
また、oninit: User.loadList()
(最後に括弧付き) を実行しなかったことにも注意してください。oninit: User.loadList()
は関数を 1 回だけすぐに呼び出すのに対し、oninit: User.loadList
はコンポーネントがレンダリングされるときにのみその関数を呼び出す点が異なります。これは重要な違いであり、JavaScript 初心者が陥りやすい点です。関数をすぐに呼び出すと、コンポーネントがレンダリングされる前に XHR リクエストが実行されてしまいます。また、コンポーネントが再作成された場合 (例えば、アプリケーション内を移動した場合) 、関数は期待どおりに再度呼び出されません。
以前に作成したエントリポイントファイルである src/index.js
からビューをレンダリングしましょう。
// src/index.js
var m = require('mithril');
var UserList = require('./views/UserList');
m.mount(document.body, UserList);
m.mount
呼び出しは、指定されたコンポーネント (UserList
) を DOM 要素 (document.body
) にレンダリングし、以前にあった DOM をすべて消去します。ブラウザで HTML ファイルを開くと、人の名前のリストが表示されるはずです。
現在、スタイルを定義していないため、リストはかなりプレーンに見えます。そこで、スタイルをいくつか追加しましょう。まず、styles.css
というファイルを作成し、index.html
ファイルに含めます。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>My Application</title>
<link href="styles.css" rel="stylesheet" />
</head>
<body>
<script src="bin/app.js"></script>
</body>
</html>
これで、UserList
コンポーネントをスタイルできます。
.user-list {
list-style: none;
margin: 0 0 10px;
padding: 0;
}
.user-list-item {
background: #fafafa;
border: 1px solid #ddd;
color: #333;
display: block;
margin: 0 0 1px;
padding: 8px 15px;
text-decoration: none;
}
.user-list-item:hover {
text-decoration: underline;
}
ブラウザウィンドウをリロードすると、スタイルが設定された要素が表示されるはずです。
アプリケーションにルーティングを追加しましょう。
ルーティングとは、画面と一意な URL を紐付け、「ページ」間の移動を可能にすることです。Mithril.js はシングルページアプリケーション向けに設計されているため、ここで言う「ページ」は、従来の HTML ファイルとは異なります。シングルページアプリケーションにおけるルーティングでは、同じ HTML ファイルを使用し続け、JavaScript によってアプリケーションの状態を変化させます。クライアントサイドルーティングには、ページ遷移時の画面のちらつきを抑えることができるという利点があります。また、Web サービス指向アーキテクチャ (例えば、事前にレンダリングされた HTML ではなく JSON 形式でデータを取得するアプリケーション) と組み合わせることで、サーバーから送信されるデータ量を削減できます。
m.mount
呼び出しを m.route
呼び出しに変更することで、ルーティングを追加できます。
// src/index.js
var m = require('mithril');
var UserList = require('./views/UserList');
m.route(document.body, '/list', {
'/list': UserList,
});
m.route
は、アプリケーションを document.body
にレンダリングすることを指定します。"/list"
はデフォルトルートであり、存在しないルートにアクセスした場合にリダイレクトされるルートです。{"/list": UserList}
は、既存のルートと、それぞれのルートに対応するコンポーネントを定義しています。
ブラウザでページをリロードすると、ルーティングが機能していることを示すために、URL に #!/list
が追加されるはずです。このルートは UserList をレンダリングするため、以前と同様に画面に人のリストが表示されるはずです。
この文字列は、m.route.prefix
で設定できます。サーバー側の設定が必要になる場合もあるため、このチュートリアルではハッシュバンを使用することにします。
ユーザーを編集するためのルートをアプリケーションに追加しましょう。まず、views/UserForm.js
というモジュールを作成します。
// src/views/UserForm.js
module.exports = {
view: function () {
// TODO implement view
},
};
次に、src/index.js
からこのモジュールを require
します。
// src/index.js
var m = require('mithril');
var UserList = require('./views/UserList');
var UserForm = require('./views/UserForm');
m.route(document.body, '/list', {
'/list': UserList,
});
最後に、それを参照するルートを作成できます。
// src/index.js
var m = require('mithril');
var UserList = require('./views/UserList');
var UserForm = require('./views/UserForm');
m.route(document.body, '/list', {
'/list': UserList,
'/edit/:id': UserForm,
});
新しいルートに :id
が含まれていることに注意してください。これはルートパラメータで、ワイルドカードのように考えることができます。例えば、/edit/1
は id
が "1"
である UserForm
に対応し、/edit/2
は id
が "2"
である UserForm
に対応します。
これらのルートパラメータに対応するように、UserForm
コンポーネントを実装しましょう。
// src/views/UserForm.js
var m = require('mithril');
module.exports = {
view: function () {
return m('form', [
m('label.label', 'First name'),
m('input.input[type=text][placeholder=First name]'),
m('label.label', 'Last name'),
m('input.input[placeholder=Last name]'),
m('button.button[type=submit]', 'Save'),
]);
},
};
styles.css
にさらにスタイルを追加しましょう。
/* styles.css */
body,
.input,
.button {
font: normal 16px Verdana;
margin: 0;
}
.user-list {
list-style: none;
margin: 0 0 10px;
padding: 0;
}
.user-list-item {
background: #fafafa;
border: 1px solid #ddd;
color: #333;
display: block;
margin: 0 0 1px;
padding: 8px 15px;
text-decoration: none;
}
.user-list-item:hover {
text-decoration: underline;
}
.label {
display: block;
margin: 0 0 5px;
}
.input {
border: 1px solid #ddd;
border-radius: 3px;
box-sizing: border-box;
display: block;
margin: 0 0 10px;
padding: 10px 15px;
width: 100%;
}
.button {
background: #eee;
border: 1px solid #ddd;
border-radius: 3px;
color: #333;
display: inline-block;
margin: 0 0 10px;
padding: 10px 15px;
text-decoration: none;
}
.button:hover {
background: #e8e8e8;
}
現在、このコンポーネントはユーザーイベントに応答するために何も行いません。src/models/User.js
の User
モデルにコードを追加しましょう。現在のコードは次のとおりです。
// src/models/User.js
var m = require('mithril');
var User = {
list: [],
loadList: function () {
return m
.request({
method: 'GET',
url: 'https://mithril-rem.fly.dev/api/users',
withCredentials: true,
})
.then(function (result) {
User.list = result.data;
});
},
};
module.exports = User;
単一のユーザーをロードできるようにするコードを追加しましょう。
// src/models/User.js
var m = require('mithril');
var User = {
list: [],
loadList: function () {
return m
.request({
method: 'GET',
url: 'https://mithril-rem.fly.dev/api/users',
withCredentials: true,
})
.then(function (result) {
User.list = result.data;
});
},
current: {},
load: function (id) {
return m
.request({
method: 'GET',
url: 'https://mithril-rem.fly.dev/api/users/' + id,
withCredentials: true,
})
.then(function (result) {
User.current = result;
});
},
};
module.exports = User;
User.current
プロパティと、そのプロパティを設定する User.load(id)
メソッドを追加したことに注意してください。この新しいメソッドを使用して UserForm
ビューを設定できるようになりました。
// src/views/UserForm.js
var m = require('mithril');
var User = require('../models/User');
module.exports = {
oninit: function (vnode) {
User.load(vnode.attrs.id);
},
view: function () {
return m('form', [
m('label.label', 'First name'),
m('input.input[type=text][placeholder=First name]', {
value: User.current.firstName,
}),
m('label.label', 'Last name'),
m('input.input[placeholder=Last name]', { value: User.current.lastName }),
m('button.button[type=submit]', 'Save'),
]);
},
};
UserList
コンポーネントと同様に、oninit
で User.load()
を呼び出しています。"/edit/:id": UserForm
ルートの :id
がルートパラメータであったことを思い出してください。このルートパラメータは UserForm
コンポーネントの vnode の属性となるため、/edit/1
にルーティングすると vnode.attrs.id
の値は "1"
になります。
次に、UserList
ビューを修正して、そこから UserForm
に遷移できるようにしましょう。
// src/views/UserList.js
var m = require('mithril');
var User = require('../models/User');
module.exports = {
oninit: User.loadList,
view: function () {
return m(
'.user-list',
User.list.map(function (user) {
return m(
m.route.Link,
{
class: 'user-list-item',
href: '/edit/' + user.id,
},
user.firstName + ' ' + user.lastName
);
})
);
},
};
ここでは、.user-list-item
vnode を、同じクラスと子を持つ m.route.Link
に置き換えました。必要なルートを参照する href
を追加しています。これは、リンクをクリックすると、ハッシュバン #!
以降の URL が変更される (つまり、現在の HTML ページをアンロードせずにルートが変更される) ことを意味します。内部的には、<a>
タグを使ってリンクを実装しており、すべて問題なく動作します。
ブラウザでページをリロードすると、人をクリックしてフォームに移動できるようになるはずです。また、ブラウザの [戻る] ボタンを使って、フォームから人のリストに戻ることも可能です。
フォーム自体は、"Save" を押しても自動的に保存されません。このフォームを実際に機能させてみましょう。
// src/views/UserForm.js
var m = require('mithril');
var User = require('../models/User');
module.exports = {
oninit: function (vnode) {
User.load(vnode.attrs.id);
},
view: function () {
return m(
'form',
{
onsubmit: function (e) {
e.preventDefault();
User.save();
},
},
[
m('label.label', 'First name'),
m('input.input[type=text][placeholder=First name]', {
oninput: function (e) {
User.current.firstName = e.target.value;
},
value: User.current.firstName,
}),
m('label.label', 'Last name'),
m('input.input[placeholder=Last name]', {
oninput: function (e) {
User.current.lastName = e.target.value;
},
value: User.current.lastName,
}),
m('button.button[type=submit]', 'Save'),
]
);
},
};
ユーザーが入力した内容を反映させるため、両方の入力フィールドに oninput
イベントを追加し、User.current.firstName
と User.current.lastName
プロパティを更新するようにしました。
さらに、"Save" ボタンがクリックされたときに User.save
メソッドが実行されるように設定しました。次に、このメソッドを実装します。
// src/models/User.js
var m = require('mithril');
var User = {
list: [],
loadList: function () {
return m
.request({
method: 'GET',
url: 'https://mithril-rem.fly.dev/api/users',
withCredentials: true,
})
.then(function (result) {
User.list = result.data;
});
},
current: {},
load: function (id) {
return m
.request({
method: 'GET',
url: 'https://mithril-rem.fly.dev/api/users/' + id,
withCredentials: true,
})
.then(function (result) {
User.current = result;
});
},
save: function () {
return m.request({
method: 'PUT',
url: 'https://mithril-rem.fly.dev/api/users/' + User.current.id,
body: User.current,
withCredentials: true,
});
},
};
module.exports = User;
一番下の save
メソッドでは、PUT
HTTP メソッドを使用して、サーバー上のデータを更新(存在しない場合は挿入)しています。
アプリケーションでユーザーの名前を編集し、変更を保存してみましょう。ユーザーリストに戻ると、変更が反映されているはずです。
現状では、ブラウザの戻るボタンでしかユーザーリストに戻れません。メニュー、あるいはより汎用的なグローバル UI 要素を配置できるレイアウトがあると便利です。
src/views/Layout.js
ファイルを作成します。
// src/views/Layout.js
var m = require('mithril');
module.exports = {
view: function (vnode) {
return m('main.layout', [
m('nav.menu', [m(m.route.Link, { href: '/list' }, 'Users')]),
m('section', vnode.children),
]);
},
};
このコンポーネントはシンプルで、ユーザーリストへのリンクを持つ <nav>
要素があります。/edit
リンクと同様に、m.route.Link
を使用してルーティング可能なリンクを作成しています。
また、vnode.children
を子要素として持つ <section>
要素もあります。vnode
は Layout コンポーネントのインスタンス(つまり、m(Layout)
の呼び出しで返される vnode)への参照であり、vnode.children
はその vnode のすべての子要素を指します。
次に、スタイルを更新します。
/* styles.css */
body,
.input,
.button {
font: normal 16px Verdana;
margin: 0;
}
.layout {
margin: 10px auto;
max-width: 1000px;
}
.menu {
margin: 0 0 30px;
}
.user-list {
list-style: none;
margin: 0 0 10px;
padding: 0;
}
.user-list-item {
background: #fafafa;
border: 1px solid #ddd;
color: #333;
display: block;
margin: 0 0 1px;
padding: 8px 15px;
text-decoration: none;
}
.user-list-item:hover {
text-decoration: underline;
}
.label {
display: block;
margin: 0 0 5px;
}
.input {
border: 1px solid #ddd;
border-radius: 3px;
box-sizing: border-box;
display: block;
margin: 0 0 10px;
padding: 10px 15px;
width: 100%;
}
.button {
background: #eee;
border: 1px solid #ddd;
border-radius: 3px;
color: #333;
display: inline-block;
margin: 0 0 10px;
padding: 10px 15px;
text-decoration: none;
}
.button:hover {
background: #e8e8e8;
}
src/index.js
のルーターを修正して、レイアウトを適用します。
// src/index.js
var m = require('mithril');
var UserList = require('./views/UserList');
var UserForm = require('./views/UserForm');
var Layout = require('./views/Layout');
m.route(document.body, '/list', {
'/list': {
render: function () {
return m(Layout, m(UserList));
},
},
'/edit/:id': {
render: function (vnode) {
return m(Layout, m(UserForm, vnode.attrs));
},
},
});
各ルートを RouteResolver(基本的には render
メソッドを持つオブジェクト)に置き換えました。render
メソッドは、m()
呼び出しをネストすることで、通常のコンポーネントの view と同じように記述できます。
注目すべき点として、m()
呼び出しでは、セレクタ文字列の代わりにコンポーネントを使用できます。ここでは、/list
ルートで m(Layout, m(UserList))
と記述しています。これは、Layout
のインスタンスを表すルート vnode があり、その唯一の子として UserList
vnode が存在することを意味します。
/edit/:id
ルートでは、ルートパラメータを UserForm
コンポーネントに渡す vnode
引数も利用できます。したがって、URL が /edit/1
の場合、vnode.attrs
は {id: 1}
となり、m(UserForm, vnode.attrs)
は m(UserForm, {id: 1})
と同等です。JSX で記述すると <UserForm id={vnode.attrs.id} />
となります。
ブラウザでページをリフレッシュすると、アプリケーションのすべてのページにグローバルナビゲーションが表示されるようになります。
これでチュートリアルは完了です。
このチュートリアルでは、サーバーからユーザーをリスト表示し、個別に編集できるシンプルなアプリケーションを作成する手順を説明しました。追加の課題として、ユーザーの作成と削除機能を実装してみてください。
Mithril.js コードのより多くの例については、examples ページを参照してください。ご質問がある場合は、Mithril.js チャットルーム までお気軽にお越しください。