v0.2.x からの移行
v1.x と v2.x は、API 互換性が v0.2.x とほぼ同じですが、いくつかの破壊的変更があります。v2.x への移行はほぼ同様であるため、以下の注意点は主に両方に適用されます。
移行する際は、mithril-codemods ツールを使用して、最も簡単な移行を自動化することを推奨します。
m.prop
の削除
v2.x では、m.prop()
はより強力なストリームマイクロライブラリに置き換えられ、コアには含まれなくなりました。オプションの Streams モジュールの使用方法については、ドキュメントを参照してください。
v0.2.x
var m = require('mithril');
var num = m.prop(1);
v2.x
var m = require('mithril');
var prop = require('mithril/stream');
var num = prop(1);
var doubled = num.map(function (n) {
return n * 2;
});
m.component
の削除
v0.2.x では、コンポーネントは m(Component)
または m.component(Component)
のいずれかを使用して作成できました。v2.x では m(Component)
のみがサポートされます。
v0.2.x
// これらは同等です
m.component(Component);
m(Component);
v2.x
m(Component);
m.withAttr
の削除
v0.2.x では、イベントリスナーは oninput: m.withAttr("value", func)
のように使用できました。v2.x では、イベントのターゲットから直接値を取得します。m.withAttr
は m.prop
と連携していましたが、m.prop
がコア外のソリューションに移行したため、v1.x では同様の広範なストリームの使用が見られなくなりました。そのため、m.withAttr
の有用性が低下しました。
v0.2.x
var value = m.prop('');
// ビュー内
m('input[type=text]', {
value: value(),
oninput: m.withAttr('value', value),
});
v2.x
var value = '';
// ビュー内
m('input[type=text]', {
value: value,
oninput: function (ev) {
value = ev.target.value;
},
});
m.version
の削除
m.version
は一般的に使用頻度が低いため削除されましたが、必要であれば自分で追加できます。利用可能な機能を把握するには、機能検出を使用することを推奨します。v2.x API は、そのような機能検出をより容易に行えるように設計されています。
config
関数
v0.2.x では、Mithril.js は単一のライフサイクルメソッド config
を提供していました。v2.x では、vnode のライフサイクルをより細かく制御できます。
v0.2.x
m('div', {
config: function (element, isInitialized) {
// 各再描画で実行されます
// isInitialized は、ノードが DOM に追加されたかどうかを示すブール値です
},
});
v2.x
これらの新しいメソッドの詳細については、lifecycle-methods.md を参照してください。
m('div', {
// DOM ノードが作成される前に呼び出されます
oninit: function (vnode) {
/*...*/
},
// DOM ノードが作成された後に呼び出されます
oncreate: function (vnode) {
/*...*/
},
// ノードが更新される前に呼び出されます。false を返すと更新をキャンセルします
onbeforeupdate: function (vnode, old) {
/*...*/
},
// ノードが更新された後に呼び出されます
onupdate: function (vnode) {
/*...*/
},
// ノードが削除される前に呼び出されます。DOM からノードを削除する準備ができたら解決される Promise を返します
onbeforeremove: function (vnode) {
/*...*/
},
// ノードが削除される前に、onbeforeremove が done() を呼び出した後に呼び出されます
onremove: function (vnode) {
/*...*/
},
});
利用可能な場合、vnode の DOM 要素は vnode.dom
でアクセスできます。
再描画の動作の変更
Mithril.js のレンダリングエンジンは、半自動化されたグローバルな再描画に基づいて動作しますが、一部の API と動作が異なります。
再描画ロックの廃止
v0.2.x では、Mithril.js は一時的にブロックされた描画ロジックを防ぐ「再描画ロック」を許可していました。デフォルトでは、m.request
は実行時に描画ループをロックし、保留中のすべてのリクエストが解決されるとロックを解除します。同じ動作は、m.startComputation()
と m.endComputation()
を使用して手動で呼び出すことができました。後者の API と関連する動作は、v2.x で代替なしに削除されました。再描画ロックは、バグのある UI を引き起こす可能性がありました。アプリケーションの一部の処理が、ビューの他の部分の更新を妨げるべきではありません。
イベントハンドラーからの再描画のキャンセル
m.mount()
と m.route()
は、DOM イベントハンドラーの実行後も自動的に再描画します。イベントハンドラー内からこれらの再描画をキャンセルするには、渡されたイベントオブジェクトの redraw
プロパティを false
に設定します。
v0.2.x
m('div', {
onclick: function (e) {
m.redraw.strategy('none');
},
});
v2.x
m('div', {
onclick: function (e) {
e.redraw = false;
},
});
同期再描画の変更
v0.2.x では、真値 (truthy) を m.redraw()
に渡すことで、Mithril.js に即座に再描画させることができました。v2.x では、この機能は明確にするために 2 つの異なるメソッドに分割されました。
v0.2.x
m.redraw(true); // 即座に同期的に再描画します
v2.x
m.redraw(); // 次の requestAnimationFrame ティックで再描画をスケジュールします
m.redraw.sync(); // 即座に再描画を呼び出し、完了するまで待機します
m.startComputation
/m.endComputation
の削除
これらはアンチパターンとみなされ、多くの問題のあるエッジケースがあるため、v2.x で代替なしに削除されました。
コンポーネント controller
関数
v2.x では、コンポーネントに controller
プロパティがなくなりました。代わりに oninit
を使用してください。
v0.2.x
m.mount(document.body, {
controller: function () {
var ctrl = this;
ctrl.fooga = 1;
},
view: function (ctrl) {
return m('p', ctrl.fooga);
},
});
v2.x
m.mount(document.body, {
oninit: function (vnode) {
vnode.state.fooga = 1;
},
view: function (vnode) {
return m('p', vnode.state.fooga);
},
});
// または
m.mount(document.body, {
// これはデフォルトで vnode.state にバインドされます
oninit: function (vnode) {
this.fooga = 1;
},
view: function (vnode) {
return m('p', this.fooga);
},
});
コンポーネントの引数
v2.x のコンポーネントへの引数はオブジェクトである必要があります。String
/Number
/Boolean
のようなプリミティブな値は、テキストの子として扱われます。引数は、vnode.attrs
オブジェクトから読み取ることで、コンポーネント内でアクセスできます。
v0.2.x
var Component = {
controller: function (options) {
// options.fooga === 1
},
view: function (ctrl, options) {
// options.fooga === 1
},
};
m('div', m.component(Component, { fooga: 1 }));
v2.x
var Component = {
oninit: function (vnode) {
// vnode.attrs.fooga === 1
},
view: function (vnode) {
// vnode.attrs.fooga === 1
},
};
m('div', m(Component, { fooga: 1 }));
コンポーネント vnode の子
v0.2.x では、コンポーネント vnode の子は正規化されず、追加の引数として渡されるだけで、フラット化もされませんでした。(内部的には、部分適用されたコンポーネントを返し、コンポーネントが部分的に適用されていることに基づいて差分が計算されていました。)v2.x では、コンポーネント vnode の子は、解決済みの子配列として vnode.children
を介して渡されますが、v0.2.x と同様に、個々の子自体は正規化されず、子の配列もフラット化されません。
v0.2.x
var Component = {
controller: function (value, renderProp) {
// value === "value"
// typeof renderProp === "function"
},
view: function (ctrl, value, renderProp) {
// value === "value"
// typeof renderProp === "function"
},
};
m(
'div',
m.component(Component, 'value', function (key) {
return 'child';
})
);
v2.x
var Component = {
oninit: function (vnode) {
// vnode.children[0] === "value"
// typeof vnode.children[1] === "function"
},
view: function (vnode) {
// vnode.children[0] === "value"
// typeof vnode.children[1] === "function"
},
};
m(
'div',
m(Component, 'value', function (key) {
return 'child';
})
);
DOM vnode の子
v0.2.x では、DOM ノードの子は、単一の配列の子のみが存在する場合に子を直接使用することを除いて、正規化なしでそのまま表現されていました。文字列がそのまま表現された、これに似た構造を返しました。
m("div", "value", ["nested"])
// 次のようになります:
{
tag: "div",
attrs: {},
children: [
"value",
["nested"],
]
}
v2.x では、DOM vnode の子は、一貫した単一構造のオブジェクトに正規化されます。
m("div", "value", ["nested"])
// ほぼ次のようになります:
{
tag: "div",
attrs: null,
children: [
{tag: "#", children: "value"},
{tag: "[", children: [
{tag: "#", children: "nested"},
]},
]
}
DOM vnode に単一のテキストの子のみが存在する場合、代わりに text
プロパティにその値を設定します。
m("div", "value")
// ほぼ次のようになります:
{
tag: "div",
attrs: null,
text: "",
children: undefined,
}
v2.x vnode の構造と正規化の方法の詳細については、vnode のドキュメント を参照してください。
ここでは、簡潔にするために、v2.x vnode のプロパティのほとんどが省略されています。
Keys
v0.2.x では、キー付き vnode とキーなし vnode を自由に混在させることができました。
v2.x では、フラグメントと要素の両方の子リストは、すべてキー付きであるか、すべてキーなしである必要があります。穴 (undefined) も、このチェックの目的ではキーなしと見なされます。無視されなくなりました。
回避する必要がある場合は、[m("div", {key: whatever})]
のように、単一の vnode を含むフラグメントのイディオムを使用します。
view()
のパラメータ
v0.2.x では、view 関数には controller
インスタンスへの参照と、オプションとしてコンポーネントに渡されたオプションが渡されていました。v2.x では、controller
の代わりに、vnode
のみが渡されるようになりました。
v0.2.x
m.mount(document.body, {
controller: function () {},
view: function (ctrl, options) {
// ...
},
});
v2.x
m.mount(document.body, {
oninit: function (vnode) {
// ...
},
view: function (vnode) {
// ctrl の代わりに vnode.state を使用します
// options の代わりに vnode.attrs を使用します
},
});
コンポーネントを m()
に渡す
v0.2.x では、コンポーネントを m()
の 2 番目の引数として、ラップなしで渡すことができました。v2.x での一貫性を保つために、常に m()
の呼び出しでラップする必要があります。
v0.2.x
m('div', Component);
v2.x
m('div', m(Component));
vnode を m.mount()
と m.route()
に渡す
v0.2.x では、m.mount(element, component)
は、ドキュメントには記載されていませんでしたが、2 番目の引数として コンポーネント の代わりに vnode を使用することができました。同様に、m.route(element, defaultRoute, routes)
は、routes
オブジェクトの値として vnode を受け入れていました。
v2.x では、どちらの場合もコンポーネントが必要です。
v0.2.x
m.mount(element, m('i', 'hello'));
m.mount(element, m(Component, attrs));
m.route(element, '/', {
'/': m('b', 'bye'),
});
v2.x
m.mount(element, {
view: function () {
return m('i', 'hello');
},
});
m.mount(element, {
view: function () {
return m(Component, attrs);
},
});
m.route(element, '/', {
'/': {
view: function () {
return m('b', 'bye');
},
},
});
m.route.mode
v0.2.x では、ルーティングモードは m.route.mode
に "pathname"
、"ハッシュ"
、または "検索"
の文字列を割り当てることで設定できました。v1.x
では、これは m.route.prefix = prefix
に置き換えられました。ここで、prefix
は任意のプレフィックスです。prefix
が #
で始まる場合は "hash" モード、?
の場合は "search" モード、それ以外の文字(または空文字列)の場合は "pathname" モードとして動作します。m.route.prefix = "/path/#!"
や ?#
のように、上記の組み合わせもサポートされています。
デフォルトでは #
ではなく #!
(hashbang) プレフィックスが使用されるように変更されました。したがって、デフォルトの動作を使用しており、既存の URL を保持したい場合は、ルートを初期化する前に m.route.prefix = "#"
を指定してください。
v0.2.x
m.route.mode = 'hash';
m.route.mode = 'pathname';
m.route.mode = 'search';
v2.x
// 直接的な対応
m.route.prefix = '#';
m.route.prefix = '';
m.route.prefix = '?';
m.route()
とアンカータグ
ルーティング可能なリンクの処理は、属性の代わりに専用の組み込みコンポーネントを使用するようになりました。<button>
などでこれを使用していた場合は、selector: "button"
属性を使用してそのタグ名を指定できます。
v0.2.x
// このリンクをクリックすると、ナビゲーションする代わりに "/path" ルートがロードされます
m('a', {
href: '/path',
config: m.route,
});
v2.x
// このリンクをクリックすると、ナビゲーションする代わりに "/path" ルートがロードされます
m(m.route.Link, {
href: '/path',
});
パステンプレート
v1.x では、3 つの異なるパステンプレート構文がありましたが、それらは類似しているものの、別々に設計された 2 つの構文と 3 つの異なる実装がありました。これはアドホックな定義であり、パラメータは一般的にエスケープされませんでした。v2.x では、:key
はエンコードされ、:key...
は raw になります。予期せずにエンコードされる場合は、:path...
を使用してください。
具体的な各メソッドへの影響は以下の通りです。
m.request
URL
v2.x のパスコンポーネントは、補間時に自動的にエスケープされ、params
から値を読み取ります。v0.2.x では、m.request({url: "/user/:name/photos/:id", data: {name: "a/b", id: "c/d"}})
は、URL が /user/a%2Fb/photos/c/d
に設定されたリクエストを送信していました。v2.x では、対応する m.request({url: "/user/:name/photos/:id", params: {name: "a/b", id: "c/d"}})
は、リクエストを /user/a%2Fb/photos/c%2Fd
に送信します。キーをエスケープせずに補間したい場合は、代わりに :key...
を使用してください。
/api/search?q=:query
のようなインラインクエリ文字列の補間は、v2.x では実行されません。代わりに、適切なキー名を持つ params
を介してそれらを渡し、クエリ文字列で指定しないでください。
これは m.jsonp
にも適用されることに注意してください。m.request
+ dataType: "jsonp"
から m.jsonp
に移行する場合は、この点に注意する必要があります。
m.route(route, params, shouldReplaceHistoryEntry)
パス
これらは現在補間を許可しており、m.request
と同じように動作します。
m.route
ルートパターン
:key...
形式のパスキーは、v1.x では URL デコードされたものを返していましたが、v2.x では raw URL を返します。
以前は、:key.md
のようなものが誤って受け入れられ、結果のパラメータの値が keymd: "..."
に設定されていました。これは v2.x では修正され、.md
はパターンの一部であり、名前ではありません。
現在のルートの読み取り/書き込み
v0.2.x では、現在のルートとのすべてのやり取りは m.route()
を通じて行われていました。v2.x では、これは 2 つの関数に分割されました。
v0.2.x
// 現在のルートを取得する
m.route();
// 新しいルートを設定する
m.route('/other/route');
v2.x
// 現在のルートを取得する
m.route.get();
// 新しいルートを設定する
m.route.set('/other/route');
ルートパラメータへのアクセス
v0.2.x では、ルートパラメータの読み取りはすべて m.route.param()
を介して処理されていました。この API は v2.x でも引き続き利用可能ですが、ルートパラメータは vnode の attrs
オブジェクトのプロパティとしても渡されます。
v0.2.x
m.route(document.body, '/booga', {
'/:attr': {
controller: function () {
m.route.param('attr'); // "booga"
},
view: function () {
m.route.param('attr'); // "booga"
},
},
});
v2.x
m.route(document.body, '/booga', {
'/:attr': {
oninit: function (vnode) {
vnode.attrs.attr; // "booga"
m.route.param('attr'); // "booga"
},
view: function (vnode) {
vnode.attrs.attr; // "booga"
m.route.param('attr'); // "booga"
},
},
});
クエリ文字列の構築/解析
v0.2.x では、m.route
から派生したメソッド m.route.buildQueryString()
と m.route.parseQueryString()
を使用していました。v2.x では、これらは分割され、ルート m
に移動されました。
v0.2.x
var qs = m.route.buildQueryString({ a: 1 });
var obj = m.route.parseQueryString('a=1');
v2.x
var qs = m.buildQueryString({ a: 1 });
var obj = m.parseQueryString('a=1');
また、v2.x では、{key: undefined}
は m.buildQueryString
およびそれを使用するメソッド(m.request
など)によって key=undefined
としてシリアライズされます。v0.2.x では、key は省略されていました。以前にこの動作に依存していた場合は、オブジェクトから key を完全に省略するようにコードを変更してください。それが難しい場合は、値が undefined
であるオブジェクトからすべての key を削除するユーティリティ関数を使用することを検討してください。
// オブジェクトから `undefined` パラメータを省略する必要がある場合はいつでも呼び出します。
function omitUndefineds(object) {
var result = {};
for (var key in object) {
if ({}.hasOwnProperty.call(object, key)) {
var value = object[key];
if (Array.isArray(value)) {
result[key] = value.map(omitUndefineds);
} else if (value != null && typeof value === 'object') {
result[key] = omitUndefineds(value);
} else if (value !== undefined) {
result[key] = value;
}
}
}
return result;
}
アンマウントの防止
onunload
の e.preventDefault()
を介してアンマウントを防止することはできなくなりました。代わりに、特定の条件が満たされた場合に、明示的に m.route.set
を呼び出す必要があります。
v0.2.x
var Component = {
controller: function () {
this.onunload = function (e) {
if (condition) e.preventDefault();
};
},
view: function () {
return m('a[href=/]', { config: m.route });
},
};
v2.x
var Component = {
view: function () {
return m('a', {
onclick: function () {
if (!condition) m.route.set('/');
},
});
},
};
コンポーネントの削除時にコードを実行する
コンポーネントは、削除時に this.onunload
を呼び出さなくなりました。代わりに、標準化されたライフサイクルフック onremove
を使用するようになりました。
v0.2.x
var Component = {
controller: function () {
this.onunload = function (e) {
// ...
};
},
view: function () {
// ...
},
};
v2.x
var Component = {
onremove: function() {
// ...
}
view: function() {
// ...
}
}
m.request
m.request によって返される Promise は、もはや m.prop
ゲッター/セッターではありません。さらに、initialValue
、unwrapSuccess
、および unwrapError
オプションはサポートされなくなりました。
また、リクエストには m.startComputation
/m.endComputation
のセマンティクスはなくなりました。代わりに、リクエスト Promise チェーンが完了すると、常に再描画がトリガーされます(background: true
が設定されていない場合)。
data
パラメータは、URL に補間され、リクエストに追加されるクエリパラメータである params
と、基盤となる XHR で送信される body である body
に分割されました。
v0.2.x では、JSONP リクエストを開始するために dataType: "jsonp"
を使用していました。v2.x では、XHR 関連の機能がない m.request
とほぼ同じ API を持つ m.jsonp
を使用するようになりました。
v0.2.x
var data = m.request({
method: 'GET',
url: 'https://api.github.com/',
initialValue: [],
});
setTimeout(function () {
console.log(data());
}, 1000);
m.request({
method: 'POST',
url: 'https://api.github.com/',
data: someJson,
});
v2.x
var data = [];
m.request({
method: 'GET',
url: 'https://api.github.com/',
}).then(function (responseBody) {
data = responseBody;
});
setTimeout(function () {
console.log(data); // 注:ゲッター/セッターではありません
}, 1000);
m.request({
method: 'POST',
url: 'https://api.github.com/',
body: someJson,
});
// または
var data = [];
m.request('https://api.github.com/').then(function (responseBody) {
data = responseBody;
});
setTimeout(function () {
console.log(data); // 注:ゲッター/セッターではありません
}, 1000);
m.request('https://api.github.com/', {
method: 'POST',
body: someJson,
});
さらに、extract
オプションが m.request
に渡される場合、提供された関数の戻り値はリクエスト Promise を解決するために直接使用され、deserialize
コールバックは無視されます。
m.request
ヘッダー
v0.2.x では、Mithril.js はデフォルトでリクエストにヘッダーを設定しませんでした。現在、最大 2 つのヘッダーを自動的に設定します。
- JSON body が
!= null
であるリクエストの場合、Content-Type: application/json; charset=utf-8
- JSON レスポンスを期待するリクエストの場合、
Accept: application/json, text/*
最初の Content-Type
ヘッダーは、指定されたコンテンツタイプが CORS セーフリストリクエストヘッダーではありません ため、CORS プリフライトリクエストをトリガーし、サーバーの CORS 設定によっては、新しいエラーが発生する可能性があります。これで問題が発生した場合は、headers: {"Content-Type": "text/plain"}
を渡して、問題のあるヘッダーをオーバーライドする必要があるかもしれません。(Accept
ヘッダーは CORS プリフライトリクエストをトリガーしないため、オーバーライドする必要はありません。)
Fetch 仕様で CORS プリフライトチェックを回避できる唯一のコンテンツタイプは、application/x-www-form-urlencoded
、multipart/form-data
、および text/plain
です。それ以外のコンテンツタイプは許可されておらず、JSON も意図的に許可されていません。
m.deferred
の削除
v0.2.x では、独自のカスタム非同期コントラクトオブジェクト(m.deferred
として公開)を使用していましたが、これは m.request
の基礎として使用されていました。v2.x では、代わりに Promise を使用し、サポートされていない環境ではポリフィルを実装します。m.deferred
を使用していた場合は、代わりに Promise を使用する必要があります。
v0.2.x
var greetAsync = function () {
var deferred = m.deferred();
setTimeout(function () {
deferred.resolve('hello');
}, 1000);
return deferred.promise;
};
greetAsync()
.then(function (value) {
return value + ' world';
})
.then(function (value) {
console.log(value);
}); // 1秒後に「hello world」と表示されます
v2.x
var greetAsync = function () {
return new Promise(function (resolve) {
setTimeout(function () {
resolve('hello');
}, 1000);
});
};
greetAsync()
.then(function (value) {
return value + ' world';
})
.then(function (value) {
console.log(value);
}); // 1秒後に「hello world」と表示されます
m.sync
の削除
v2.x は標準に準拠した Promise を使用するため、m.sync
は不要になりました。代わりに Promise.all
を使用してください。
v0.2.x
m.sync([
m.request({ method: 'GET', url: 'https://api.github.com/users/lhorie' }),
m.request({
method: 'GET',
url: 'https://api.github.com/users/dead-claudia',
}),
]).then(function (users) {
console.log('Contributors:', users[0].name, 'and', users[1].name);
});
v2.x
Promise.all([
m.request({ method: 'GET', url: 'https://api.github.com/users/lhorie' }),
m.request({
method: 'GET',
url: 'https://api.github.com/users/dead-claudia',
}),
]).then(function (users) {
console.log('Contributors:', users[0].name, 'and', users[1].name);
});
xlink
名前空間が必要
v0.2.x では、xlink
名前空間は唯一サポートされている属性名前空間であり、特別なケースとして処理されていました。現在、名前空間の解析が完全にサポートされており、名前空間付きの属性は名前空間を明示的に宣言する必要があります。
v0.2.x
m(
'svg',
// `href` 属性は自動的に名前空間化されます
m("image[href='image.gif']")
);
v2.x
m(
'svg',
// `href` 属性のユーザー指定の名前空間
m("image[xlink:href='image.gif']")
);
ビューのネストされた配列
配列は、v2.x 仮想 DOM で構造的に重要な フラグメント を表すようになりました。v0.2.x のネストされた配列は、差分処理のために仮想ノードの 1 つの連続したリストにフラット化されていましたが、v2.x は配列構造を保持します。各配列の子要素は、隣接する配列の子要素とは兄弟関係とは見なされません。
vnode
等価性チェック
vnode が最後の描画でその場所を占める vnode と厳密に等しい場合、v2.x は、ミューテーションのチェックやサブツリーでのライフサイクルメソッドのトリガーを行わずに、ツリーのその部分をスキップします。コンポーネントのドキュメントには、この問題に関する詳細 が含まれています。