Skip to content
Mithril.js 2
Main Navigation 指南API

繁體中文

English
简体中文
Español
Français
Русский
Português – Brasil
Deutsch
日本語
한국어
Italiano
Polski
Türkçe
čeština
magyar

繁體中文

English
简体中文
Español
Français
Русский
Português – Brasil
Deutsch
日本語
한국어
Italiano
Polski
Türkçe
čeština
magyar

外觀

Sidebar Navigation

入門

安裝

簡單應用程式

資源

JSX

在舊版瀏覽器上使用 ES6+

動畫

測試

範例集

第三方整合

路徑處理

關鍵概念

虛擬 DOM 節點

組件

生命周期方法

Keys

自動重繪系統

雜項

框架比較

從 v1.x 遷移

從 v0.2.x 遷移

API

本頁導覽

從 v0.2.x 遷移 ​

v1.x 和 v2.x 在很大程度上與 v0.2.x 的 API 相容,但仍存在一些重大變更。遷移到 v2.x 幾乎完全相同,因此以下說明主要適用於兩者。

如果您正在遷移,請考慮使用 mithril-codemods 工具來協助自動化最直接的遷移。

移除 m.prop ​

在 v2.x 中,m.prop() 已經轉換為一個更強大的 stream 微函式庫,但它不再是核心的一部分。您可以在文件中閱讀關於如何使用可選的 Streams 模組。

v0.2.x ​

javascript
var m = require('mithril');

var num = m.prop(1);

v2.x ​

javascript
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 ​

javascript
// 這些是等效的
m.component(Component);
m(Component);

v2.x ​

javascript
m(Component);

移除 m.withAttr ​

在 v0.2.x 中,事件監聽器可以使用 oninput: m.withAttr("value", func) 等語法。在 v2.x 中,只需直接從事件的 target 讀取即可。它能很好地與 m.prop 協同運作,但由於後者已被移除,轉而採用核心外的解決方案,且 v1.x 沒有出現類似廣泛且慣用的 stream 用法,因此 m.withAttr 失去了大部分用處。

v0.2.x ​

javascript
var value = m.prop('');

// 在您的視圖中
m('input[type=text]', {
  value: value(),
  oninput: m.withAttr('value', value),
});

v2.x ​

javascript
var value = '';

// 在您的視圖中
m('input[type=text]', {
  value: value,
  oninput: function (ev) {
    value = ev.target.value;
  },
});

移除 m.version ​

它通常用途不大,您可以隨時自行添加回去。您應該優先使用功能偵測來了解哪些功能可用,且 v2.x API 的設計更便於實現這一點。

config 函數 ​

在 v0.2.x 中,Mithril.js 提供了一個單一的生命週期方法 config。v2.x 則提供了對 vnode 生命週期更精細的控制。

v0.2.x ​

javascript
m('div', {
  config: function (element, isInitialized) {
    // 每次重繪時執行
    // isInitialized 是一個布林值,表示節點是否已添加到 DOM
  },
});

v2.x ​

有關這些新方法的更多文件,請參閱 lifecycle-methods.md。

javascript
m('div', {
  // 在建立 DOM 節點之前呼叫
  oninit: function (vnode) {
    /*...*/
  },
  // 在建立 DOM 節點之後呼叫
  oncreate: function (vnode) {
    /*...*/
  },
  // 在更新節點之前呼叫,回傳 false 以取消
  onbeforeupdate: function (vnode, old) {
    /*...*/
  },
  // 在更新節點之後呼叫
  onupdate: function (vnode) {
    /*...*/
  },
  // 在移除節點之前呼叫,並回傳一個 Promise,此 Promise 會在
  // 準備好從 DOM 移除節點時解析
  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 ​

javascript
m('div', {
  onclick: function (e) {
    m.redraw.strategy('none');
  },
});

v2.x ​

javascript
m('div', {
  onclick: function (e) {
    e.redraw = false;
  },
});

同步重繪已變更 ​

在 v0.2.x 中,可以透過將真值傳遞給 m.redraw() 來強制 Mithril.js 立即重繪。在 v2.x 中,為了更清楚地表達,此功能已被拆分為兩種不同的方法。

v0.2.x ​

javascript
m.redraw(true); // 立即且同步地重繪

v2.x ​

javascript
m.redraw(); // 在下一個 requestAnimationFrame 刻度上排定重繪
m.redraw.sync(); // 立即呼叫重繪並等待其完成

移除 m.startComputation/m.endComputation ​

它們被視為反模式,且存在許多有問題的邊緣情況,因此已在 v2.x 中移除,且沒有替代方案。

元件 controller 函數 ​

在 v2.x 中,元件中不再有 controller 屬性,請改用 oninit。

v0.2.x ​

javascript
m.mount(document.body, {
  controller: function () {
    var ctrl = this;

    ctrl.fooga = 1;
  },

  view: function (ctrl) {
    return m('p', ctrl.fooga);
  },
});

v2.x ​

javascript
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 ​

javascript
var Component = {
  controller: function (options) {
    // options.fooga === 1
  },

  view: function (ctrl, options) {
    // options.fooga === 1
  },
};

m('div', m.component(Component, { fooga: 1 }));

v2.x ​

javascript
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 ​

javascript
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 ​

javascript
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 節點的子節點以字面意義表示,除了在僅存在單一陣列子節點時直接使用子節點外,沒有經過標準化。它回傳的結構更像是這樣,字串以字面意義表示。

javascript
m("div", "value", ["nested"])

// 變成:
{
	tag: "div",
	attrs: {},
	children: [
		"value",
		["nested"],
	]
}

在 v2.x 中,DOM vnode 的子節點已標準化為單一一致結構的物件。

javascript
m("div", "value", ["nested"])

// 大致變成:
{
	tag: "div",
	attrs: null,
	children: [
		{tag: "#", children: "value"},
		{tag: "[", children: [
			{tag: "#", children: "nested"},
		]},
	]
}

如果 DOM vnode 上僅存在單一文字子節點,則會將 text 設定為該值。

javascript
m("div", "value")

// 大致變成:
{
	tag: "div",
	attrs: null,
	text: "",
	children: undefined,
}

有關 v2.x vnode 結構以及如何標準化的更多詳細資訊,請參閱 vnode 文件。

為了簡潔起見,此處省略了大多數 v2.x vnode 屬性。

Key ​

在 v0.2.x 中,您可以自由混合帶有 key 和不帶 key 的 vnode。

在 v2.x 中,片段和元素的子節點清單必須全部帶 key 或全部不帶 key。出於此檢查的目的,Hole 也被認為是不帶 key 的 - 它不再忽略它們。

如果您需要解決這個問題,請使用包含單一 vnode 的片段的慣用法,例如 [m("div", {key: whatever})]。

view() 參數 ​

在 v0.2.x 中,視圖函數會傳遞對 controller 實例的參考,以及 (可選) 傳遞給元件的任何選項。在 v2.x 中,它們僅會傳遞 vnode,如同 controller 函數。

v0.2.x ​

javascript
m.mount(document.body, {
  controller: function () {},

  view: function (ctrl, options) {
    // ...
  },
});

v2.x ​

javascript
m.mount(document.body, {
  oninit: function (vnode) {
    // ...
  },

  view: function (vnode) {
    // 使用 vnode.state 而不是 ctrl
    // 使用 vnode.attrs 而不是 options
  },
});

將元件傳遞給 m() ​

在 v0.2.x 中,您可以將元件當作 m() 的第二個引數傳遞,而無需任何包裝。為了協助維持 v2.x 的一致性,必須始終使用 m() 呼叫進行包裝。

v0.2.x ​

javascript
m('div', Component);

v2.x ​

javascript
m('div', m(Component));

將 vnode 傳遞給 m.mount() 和 m.route() ​

在 v0.2.x 中,m.mount(element, component) 允許 vnode 作為第二個引數,而非 元件 (即使它沒有記載)。同樣地,m.route(element, defaultRoute, routes) 接受 vnode 作為 routes 物件中的值。

在 v2.x 中,這兩種情況都要求使用元件。

v0.2.x ​

javascript
m.mount(element, m('i', 'hello'));
m.mount(element, m(Component, attrs));

m.route(element, '/', {
  '/': m('b', 'bye'),
});

v2.x ​

javascript
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 中,路由模式可以透過將字串 "pathname"、"hash" 或 "search" 賦值予 m.route.mode 來設定。在 v1.x 中,它被 m.route.prefix = prefix 取代,其中 prefix 可以是任何前綴。如果前綴以 # 開頭,則以 "hash" 模式運作;? 則為 "search" 模式;任何其他字元(或空字串)則為 "pathname" 模式。它也支援上述的組合,例如 m.route.prefix = "/path/#!" 或 ?#。

預設值也已更改為使用 #! (hashbang) 前綴,而不僅僅是 #。因此,如果您使用的是預設行為,並且想要保留現有的 URL,請在初始化路由之前指定 m.route.prefix = "#"。

v0.2.x ​

javascript
m.route.mode = 'hash';
m.route.mode = 'pathname';
m.route.mode = 'search';

v2.x ​

javascript
// 直接對應
m.route.prefix = '#';
m.route.prefix = '';
m.route.prefix = '?';

m.route() 和錨點標籤 ​

處理可路由連結現在是使用特殊的內建元件,而不是使用屬性。如果您在 <button> 等元素上使用此功能時,可以使用 selector: "button" 屬性指定該標籤名稱。

v0.2.x ​

javascript
// 點擊此連結將載入 "/path" 路由,而不是導航
m('a', {
  href: '/path',
  config: m.route,
});

v2.x ​

javascript
// 點擊此連結將載入 "/path" 路由,而不是導航
m(m.route.Link, {
  href: '/path',
});

路徑模板 ​

在 v1.x 中,有三種獨立的路徑模板語法,雖然它們很相似,但實際上是 2 種獨立設計的語法,並有 3 種不同的實現。它是以相當臨時的方式定義的,並且參數通常沒有經過轉義。現在,所有內容都會在 :key 時進行編碼,在 :key... 時則為原始資料。如果內容意外地被編碼,請使用 :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,請改用 :key...。

在 v2.x 中,不會執行內嵌查詢字串中的插值,例如 /api/search?q=:query。請改為透過具有適當 key 名稱的 params 傳遞這些參數,而無需在查詢字串中指定它。

請注意,這也適用於 m.jsonp。從 m.request + dataType: "jsonp" 遷移到 m.jsonp 時,您也需要注意這一點。

m.route(route, params, shouldReplaceHistoryEntry) 路徑 ​

現在這些允許插值,且其工作方式與 m.request 相同。

m.route 路由模式 ​

形式為 :key... 的路徑 key 在 v1.x 中會傳回經過 URL 解碼的值,但在 v2.x 中會傳回原始 URL。

過去,像 :key.md 這樣的寫法會被錯誤地接受,導致產生的參數值被設定為 keymd: "..."。現在情況並非如此 - .md 現在是模式的一部分,而不是名稱。

讀取/寫入當前路由 ​

在 v0.2.x 中,所有與當前路由的互動都是透過 m.route() 這個方法進行的。在 v2.x 中,這已被分解為兩個函式。

v0.2.x ​

javascript
// 取得當前路由
m.route();

// 設定新路由
m.route('/other/route');

v2.x ​

javascript
// 取得當前路由
m.route.get();

// 設定新路由
m.route.set('/other/route');

存取路由參數 ​

在 v0.2.x 中,讀取路由參數是完全透過 m.route.param() 這個方法處理。此 API 在 v2.x 中仍然可用,此外,任何路由參數都會作為 vnode 上 attrs 物件中的屬性傳遞。

v0.2.x ​

javascript
m.route(document.body, '/booga', {
  '/:attr': {
    controller: function () {
      m.route.param('attr'); // "booga"
    },
    view: function () {
      m.route.param('attr'); // "booga"
    },
  },
});

v2.x ​

javascript
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 ​

javascript
var qs = m.route.buildQueryString({ a: 1 });

var obj = m.route.parseQueryString('a=1');

v2.x ​

javascript
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 被省略,並且這延續到 m.request。如果您之前依賴此行為,請變更您的程式碼以完全從物件中省略 key。如果不容易做到這一點,且您需要保留 v0.2.x 的行為,則可能值得使用一個簡單的工具函式,從物件中移除所有值為 undefined 的 key。

javascript
// 每當您需要從物件中省略 `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 ​

javascript
var Component = {
  controller: function () {
    this.onunload = function (e) {
      if (condition) e.preventDefault();
    };
  },
  view: function () {
    return m('a[href=/]', { config: m.route });
  },
};

v2.x ​

javascript
var Component = {
  view: function () {
    return m('a', {
      onclick: function () {
        if (!condition) m.route.set('/');
      },
    });
  },
};

在元件移除時執行程式碼 ​

元件在被移除時,不再呼叫 this.onunload。它們現在使用標準化的生命週期掛鉤 onremove。

v0.2.x ​

javascript
var Component = {
  controller: function () {
    this.onunload = function (e) {
      // ...
    };
  },
  view: function () {
    // ...
  },
};

v2.x ​

javascript
var Component = {
	onremove: function() {
		// ...
	}
	view: function() {
		// ...
	}
}

m.request ​

m.request 所回傳的 Promise 不再是 m.prop getter-setter。此外,不再支援 initialValue、unwrapSuccess 和 unwrapError 選項。

此外,請求不再具有 m.startComputation/m.endComputation 語義。相反,當請求 Promise 鏈完成時,總是會引發重繪(除非設定了 background: true)。

data 參數現在被拆分為 params (會插入到 URL 並附加到請求的查詢參數) 以及 body (要在底層 XHR 中傳送的請求主體)。

在 v0.2.x 中,您是使用 dataType: "jsonp" 來發起 JSONP 請求。在 v2.x 中,您現在可以使用 m.jsonp,它所提供的 API 與 m.request 大致相同,只是少了與 XHR 相關的部分。

v0.2.x ​

javascript
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 ​

javascript
var data = [];
m.request({
  method: 'GET',
  url: 'https://api.github.com/',
}).then(function (responseBody) {
  data = responseBody;
});

setTimeout(function () {
  console.log(data); // 注意:不是 getter-setter
}, 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); // 注意:不是 getter-setter
}, 1000);

m.request('https://api.github.com/', {
  method: 'POST',
  body: someJson,
});

此外,如果將 extract 選項傳遞給 m.request,則提供的函式的傳回值將直接用於解析請求的 Promise,並且 deserialize 回呼將被忽略。

m.request 標頭 ​

在 v0.2.x 中,Mithril.js 預設不會在請求上設定任何標頭。現在,它最多設定 2 個標頭:

  • 對於具有 != null 的 JSON body 的請求,Content-Type: application/json; charset=utf-8
  • 對於期望 JSON 回應的請求,Accept: application/json, text/*

這兩個標頭中的第一個,也就是 Content-Type,將會觸發 CORS 預檢請求,因為它並非 CORS 安全列表請求標頭,且這可能會因為伺服器上 CORS 的配置方式,而導致新的錯誤。如果您遇到這個問題,可能需要透過傳遞 headers: {"Content-Type": "text/plain"} 來覆寫這個標頭。(Accept 標頭不會觸發任何行為,因此您不需要覆寫它。)

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,並在不支援的環境中實作 polyfill。如果您過去會使用 m.deferred,現在應該改用 Promise。

v0.2.x ​

javascript
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 ​

javascript
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 ​

javascript
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 ​

javascript
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 ​

javascript
m(
  'svg',
  // `href` 屬性會自動命名空間
  m("image[href='image.gif']")
);

v2.x ​

javascript
m(
  'svg',
  // 使用者在 `href` 屬性上指定的命名空間
  m("image[xlink:href='image.gif']")
);

視圖中的巢狀陣列 ​

陣列現在代表片段,在 v2.x 的虛擬 DOM 中具有結構上的重要意義。雖然 v0.2.x 中的巢狀陣列會被展平為一個連續的虛擬節點列表,以便進行差異比較,但 v2.x 則保留了陣列結構 - 任何給定陣列的子節點,都不會被視為相鄰陣列的兄弟節點。

vnode 相等性檢查 ​

如果某個 vnode 嚴格等於佔據其在上次繪製時位置的 vnode,v2.x 就會跳過樹狀結構的該部分,而不檢查變更或觸發子樹中的任何生命週期方法。元件文件中包含關於此問題的更多詳細資訊。

Pager
上一頁從 v1.x 遷移
下一頁API

以 MIT 授權條款 發布。

版權所有 (c) 2024 Mithril Contributors

https://mithril.js.org/migration-v02x.html

以 MIT 授權條款 發布。

版權所有 (c) 2024 Mithril Contributors