在 Mithril 视图中表示一个 HTML 元素:
m("div", {class: "foo"}, "hello")
// 表示 <div class="foo">hello</div>
你也可以通过 Babel 插件使用 HTML 语法。
/** jsx m */
<div class="foo">hello</div>
vnode = m(selector, attributes, children)
参数 | 类型 | 是否必须 | 描述 |
---|---|---|---|
selector |
String|Object |
是 | CSS 选择器或组件 |
attributes |
Object |
否 | HTML 属性或元素属性 |
children |
Array |
否 | 子 vnodes。也可以写成解构参数。 |
返回 | Vnode |
vnode |
Mithril 提供了一个 hyperscript 函数 m()
,它可以用 JavaScript 语法表达任何 HTML 结构。它接受一个 selector
字符串(必须),一个 attributes
对象(可选)和一个 children
数组(可选)。
m("div", {id: "box"}, "hello")
// 等效 HTML:
// <div id="box">hello</div>
m()
函数返回的不是 DOM 元素,而是虚拟 DOM 节点或这 vnode,它是一个用 JavaScript 对象表示的 DOM 元素。
// a vnode
var vnode = {tag: "div", attrs: {id: "box"}, children: [ /*...*/ ]}
可以使用 m.render() 函数把 vnode 转换为真实的 DOM 元素。
m.render(document.body, m("br")) // 在 <body> 中输出了 <br>
多次调用 m.render()
时,只会对 DOM 中发生变更的部分进行更改,并不会每次都重新创建 DOM 树,因为重新创建 DOM 是非常耗费资源的,且会导致失去输入框的焦点等问题。
m()
函数的参数非常灵活:
// 简单的标签
m("div") // <div></div>
// 属性和子元素是可选的
m("a", {id: "b"}) // <a id="b"></a>
m("span", "hello") // <span>hello</span>
// 包含子节点
m("ul", [ // <ul>
m("li", "hello"), // <li>hello</li>
m("li", "world"), // <li>world</li>
]) // </ul>
// 是否使用数组也是可选的
m("ul", // <ul>
m("li", "hello"), // <li>hello</li>
m("li", "world") // <li>world</li>
) // </ul>
m()
函数的第一个参数可以是 CSS 选择器。它支持任何有效的 CSS 语法,包括:#
(id),.
(class)和 []
(属性)等。
m("div#hello")
// <div id="hello"></div>
m("section.container")
// <section class="container"></section>
m("input[type=text][placeholder=Name]")
// <input type="text" placeholder="Name" />
m("a#exit.external[href='http://example.com']", "Leave")
// <a id="exit" class="external" href="http://example.com">Leave</a>
如果省略了标签名,Mithril 会默认使用 div
标签。
m(".box.box-bordered") // <div class="box box-bordered"></div>
通常,建议你把 CSS 选择器用于静态属性(值不会改变的属性),并传入一个属性对象用于动态属性值。
var currentURL = "/"
m("a.link[href=/]", {
class: currentURL === "/" ? "selected" : ""
}, "Home")
// 等效 HTML:
// <a href="/" class="link selected">Home</a>
如果在 m()
函数的第一个参数和第二个参数中都存在 CSS 类名,则它们会合并到一起。
Mithril 同时使用 JavaScript API 和 DOM API(setAttribute
)来解析属性。这意味着你可以使用这两种语言来引用属性。
例如,在 JavaScript API 中,readonly
属性称为 element.readOnly
(注意大小写)。在 Mithril 中,下面所有的用法都是支持的:
m("input", {readonly: true}) // 小写
m("input", {readOnly: true}) // 大写
m("input[readonly]")
m("input[readOnly]")
Mithril 支持使用字符串或对象作为 style
的值。下面所有的用法都是支持的:
m("div", {style: "background:red;"})
m("div", {style: {background: "red"}})
m("div[style=background:red]")
使用字符串作为 style
的值时,在重绘时会覆盖元素中的所有内敛样式。
Mithril 不会为数字值添加单位。
Mithril 支持为所有的事件绑定事件处理程序,包括不是以 on${event}
这种规范定义的属性,例如 touchstart
:
function doSomething(e) {
console.log(e)
}
m("div", {onclick: doSomething})
Mithril 支持可以通过属性访问的 DOM 功能,例如 <select>
的 selectedIndex
和 value
属性。
m("select", {selectedIndex: 0}, [
m("option", "Option A"),
m("option", "Option B"),
])
组件可以把逻辑封装在一个单元内,并把它当元素使用。它是开发大型可扩展应用的基础。
组件是包含 view
方法的 JavaScript 对象。把组件作为 m()
函数的第一个参数传入,即可使用该组件。可以通过定义属性和子元素把参数传递给组件,如下面的示例所示:
// 定义一个组件
var Greeter = {
view: function(vnode) {
return m("div", vnode.attrs, ["Hello ", vnode.children])
}
}
// 使用该组件
m(Greeter, {style: "color:red;"}, "world")
// 等效 HTML:
// <div style="color:red;">Hello world</div>
要了解更多有关组件的信息,请参阅组件页面。
vnode 和组件拥有生命周期方法(又叫钩子),它们会在 DOM 元素的生命周期的不同时期被调用。Mithril 支持的生命周期方法包括:oninit
、oncreate
、onupdate
、onbeforeremove
、onremove
、onbeforeupdate
。
生命周期方法的定义和 DOM 事件处理函数的定义相同,但是传入了 vnode 作为参数:
function initialize(vnode) {
console.log(vnode)
}
m("div", {oninit: initialize})
钩子 | 描述 |
---|---|
oninit(vnode) |
vnode 被渲染成 DOM 元素之前运行 |
oncreate(vnode) |
vnode 添加到 DOM 以后运行 |
onupdate(vnode) |
重绘完成后运行 |
onbeforeremove(vnode) |
DOM 元素被移除之前运行。如果返回 Promise,Mithril 会在 Promise 完成后再移除 DOM 元素。只有在移除元素本身时才会触发该方法,移除子元素时不会触发该方法。 |
onremove(vnode) |
DOM 元素被移除之前运行。如果定义了 onbeforeremove 钩子,则 onremove 方法会在调用 done 之后被调用。在移除元素本身、或移除元素的父元素时,都会触发该方法。 |
onbeforeupdate(vnode, old) |
onupdate 之前运行。如果它返回 false ,则会阻止该元素及其子元素进行 diff。 |
要了解更多有关生命周期方法的信息,请参阅生命周期犯法页面。
列表中的 vnode 可以拥有一个称为 key
的特殊属性,用于对 DOM 元素进行标记。在生成 vnode 的模型数据改变时,可以通过 key
来找到指定的 DOM 元素。
通常,key
应该是数组中的唯一标志字段,即 key
字段的值应该是不重复的。
var users = [
{id: 1, name: "John"},
{id: 2, name: "Mary"},
]
function userInputs(users) {
return users.map(function(u) {
return m("input", {key: u.id}, u.name)
})
}
m.render(document.body, userInputs(users))
当列表中的 vnode 拥有 key
时,如果 users
数组被重新排序、视图被重新渲染,input
元素将按照和以前一致的排序方式进行排列,以便保持正确的焦点和 DOM 状态。
了解更多有关 key 的信息,参见 key 页面。
Mithril 完全支持 SVG。Xlink 也是支持的,但与之前 v1.0 版本的 Mithril 不同的是,必须明确定义命名空间:
m("svg", [
m("image[xlink:href='image.gif']")
])
MathML 也是完全支持的。
因为嵌套的 vnode 是标准的 JavaScript 表达式,所以你可以使用 JavaScript 来操作它们。
var user = {name: "John"}
m(".name", user.name) // <div class="name">John</div>
使用 Array
方法如 map 来循环数据列表。
var users = [
{name: "John"},
{name: "Mary"},
]
m("ul", users.map(function(u) { // <ul>
return m("li", u.name) // <li>John</li>
// <li>Mary</li>
})) // </ul>
// ES6:
// m("ul", users.map(u =>
// m("li", u.name)
// ))
使用三元运算符根据条件来设置视图中的内容。
var isError = false
m("div", isError ? "An error occurred" : "Saved") // <div>Saved</div>
在 JavaScript 表达式中,不能使用如 if
和 for
这样的 JavaScript 语句。最好全部使用上面的结构来写模版,以保持模版结构可读性和声明性。
在 Mithril 中,格式规范的 HTML 就是有效的 JSX 语法。要把一个 HTML 文件集成到一个使用 JSX 的项目中,除了复制粘贴外,要做的其他工作很少。
使用 hyperscript 时,需要把 HTML 代码转换为 hyperscript 语法,代码才能运行。为了方便,你可以使用 HTML-to-Mithril-template 模版转换器。
虽然 Mithril 很灵活,但有些代码模式还是不推荐使用:
不同的 DOM 元素有不同的属性,且往往有不同的行为。可变的选择器会泄漏组件的实现细节。
// 避免这种用法
var BadInput = {
view: function(vnode) {
return m("div", [
m("label"),
m(vnode.attrs.type || "input")
])
}
}
建议不要使用变量作为选择器,而是分别针对每个单独的选择器编写代码,或者对代码的可变部分进行重构。
// 推荐这种用法
var BetterInput = {
view: function(vnode) {
return m("div", [
m("label", vnode.attrs.title),
m("input"),
])
}
}
var BetterSelect = {
view: function(vnode) {
return m("div", [
m("label", vnode.attrs.title),
m("select"),
])
}
}
// 推荐这种用法
var BetterLabeledComponent = {
view: function(vnode) {
return m("div", [
m("label", vnode.attrs.title),
vnode.children,
])
}
}
JavaScript 语句通常需要更改 HTML 树的嵌套结构,使代码变得冗长和难以理解。创建虚拟 DOM 树的代码可能导致严重的性能问题(例如重新创建整个模版)。
// 避免这种用法
var BadListComponent = {
view: function(vnode) {
var list = []
for (var i = 0; i < vnode.attrs.items.length; i++) {
list.push(m("li", vnode.attrs.items[i]))
}
return m("ul", list)
}
}
推荐使用 JavaScript 表达式,例如三元运算符和 Array 方法。
// 推荐这种用法
var BetterListComponent = {
view: function() {
return m("ul", vnode.attrs.items.map(function(item) {
return m("li", item)
}))
}
}
当重绘时遇到一个 vnode 和之前渲染的 vnode 严格相等时,会跳过这个 vnode,不会更新其内容。虽然这看起来是一个优化性能的方式,但应该避免这种用法,因为这阻止了那个节点的动态更新 - 这会导致副作用,例如重绘时无法触发生命周期方法。
组件的文档中包含更多细节和一个这种反面模式的例子。