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 は、ミューテーションのチェックやサブツリーでのライフサイクルメソッドのトリガーを行わずに、ツリーのその部分をスキップします。コンポーネントのドキュメントには、この問題に関する詳細 が含まれています。