公司的一个项目的迭代,在前期研究中,考虑开发及生产环境的一些情况和要求,大佬们选择了 Mithril 这个框架,前端方面由我负责。在这之前我是没听说过 Mithril的,就花了点时间去学习,发现关于这个框架的内容还是比较少的。这篇文章也是简单整理了一下一些基础的内容,算是学习笔记。
1 Mithril 介绍
1.1 是什么
mithril是一个小巧的、mvc 模式的、用于构建单页面应用的现代化 JavaScript 框架。
最新版本(2.0.4)支持 IE11 以上的浏览器,v1版本支持 IE9 以上。
1.2 与其他框架的对比
首先,从框架的体积大小来说, Mithril 与其他框架相比都要小的多。这里贴一个 Mithril 官网的对比图:
对比现在流行的三大框架, Mithril 与 React 比较类似,两者都是由虚拟 DOM 通过 diff 算法渲染视图的,不过不一样的是 React 只是单纯的视图库,在实际项目中需要依赖其他第三方库,而 Mithril 虽然麻雀虽小,但是五脏俱全,它内置了例如路由功能和 XHR 工具,语法上,Mithril 也支持 JSX 的写法。
至于 Mithril 和 Angular 以及 Vue 之间,差不多可以类比 React 与这两者之间的区别。
1.3 特点
* 轻量级
- 压缩后体积小,无依赖
- api 少,上手简单
* 快速
- 提供了一个模板引擎与一个虚拟的 DOM diff 实现,实现高性能渲染
- 自动重绘
* mvc
- 层次化的 mvc 组件,耦合性低可维护性高
2 用法示例
2.1 安装
1. CDN
<!-- Development: whichever you prefer -->
<script src="https://unpkg.com/mithril/mithril.js"></script>
<script src="https://cdn.jsdelivr.net/npm/mithril/mithril.js"></script>
<!-- Production: whichever you prefer -->
<script src="https://unpkg.com/mithril/mithril.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/mithril/mithril.min.js"></script>
2. npm
npm install mithril --save
如果项目是使用 ts 开发的,需要安装类型声明文件。
```
npm install @types/mithril --save-dev
```
2.2 基本使用示例
const vnode = m('div.container', [
m("li", "hello"),
m("li", "mithril")
]);
m.render(document.body, vnode);
//
// 上面代码会生成如下的 HTML 结构
// <div class="container">
// <span>hello</span>
// <span>mithril</span>
// </div>
2.3 JSX
Mithril 也支持 JSX 语法:
const MyComponent = {
view(vnode) {
return (
<div>Hello Mithril</div>
)
}
}
m.mount(document.body, <MyComponent />);
要注意的是,使用 JSX 必须通过 Babel 对代码进行转换。
* 在 Webpack 中使用 Babel
如果项目使用了 Webpack 的话,直接在 Webpac k的配置中添加 Babel 配置。
首先创建 .babelrc 文件
{
"presets": ["@babel/preset-env"],
"plugins": [
["@babel/plugin-transform-react-jsx", {
"pragma": "m",
"pragmaFrag": "'['"
}]
]
}
然后安装 Babel 相关依赖
npm install @babel/core babel-loader @babel/preset-env @babel/plugin-transform-react-jsx --save-dev
最后在Webpack的配置文件中添加Babel相关的配置信息
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, './bin'),
filename: 'app.js',
},
module: {
rules: [{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader'
}
}]
}
}
* 独立安装 Babel
一样先要创建 .babelrc 文件,文件具体内容和上面 Webpack 中的一样。
然后安装依赖
npm install @babel/core @babel/cli @babel/preset-env @babel/plugin-transform-react-jsx --save-dev
JSX 与 Hyperscript
JSX 是一种 JavaScript 的语法扩展,能让我们可以在 JS 中写 html。 在React 中,JSX 只是 React.createElement()
函数的语法糖。
同样的,在 Mithril 中, JSX 最终也会被转换成 Hyperscript 语法,文章中的例子一般都是用的 Hyperscript (m("span", "hello mithril")
)。
上面 JSX 代码转换后:
对于 JSX 语法,熟悉 React 的就完全不是问题了,对于前端来说,使用 JSX 来写 DOM 会比较舒适,可以在 js 中使用 HTML 。但是 JSX 并不能直接运行,需要编译成 hyperscript 之后才能运行。而 hyperscript 就是标准的 javascript 语法,并不需要额外的编译过程。这两者之间各有各的好处,至于在实际开发使用哪种方式就看个人选择了。
3 核心概念
3.1 vnodes
虚拟 DOM 树是描述 DOM 树的javaScript数据结构,它由嵌套的虚拟 DOM 节点(也称为 vnode )组成。 Mithril 的虚拟 DOM 引擎使用 vnode 树来生成 DOM 树。
虚拟 DOM 节点(vnode)是一个 JavaScript 对象,具有以下属性:
属性 | 类型 | 说明 |
---|---|---|
tag | String | Object | DOM 元素的 nodeName ,也可以是 [ 用来表示一个片段,# 用来表示文本节点,< 用来表示 HTML 字符串。另外,也可以是组件。 |
key | String? | 用于把 DOM 元素映射到数据数组中对应项的值。 |
attrs | Object? | DOM 属性,事件,属性和生命周期方法的 hashmap。 |
children | (Array | String | Number | Boolean)? | 在大多数 vnode 类型中,children 属性是一个 vnode 数组。对于文本和 HTML 字符串节点,children 属性是字符串、数字和布尔值。 |
text | (String | Number | Boolean)? | 如果 vnode 只包含文本,则可以用该属性代替 children。使用这个属性是出于性能的考虑。组件 vnode 始终不会使用 text 属性,即使组件只包含文本。 |
dom | Element? | 指向与 vnode 对应的元素。在节点片段和 HTML 节点中,dom 指向所有节点中的第一个元素。 |
domSize | Number? | 只在节点片段和 HTML 节点中存在,在其他类型的 vnode 中为 undefined。它表示 vnode 表示的 DOM 元素的数量(从 DOM 元素引用的元素开始)。 |
state | Object | vnode 内的状态,在组件 vnode 中,state 是组件对象的浅克隆。 |
events | Object? | 一个保存事件处理操作的对象,在重绘时保持不变,可通过 DOM API 移除事件。如果没有已定义的事件,则 events 属性为 undefined。这个属性只在 Mithril 内部使用。 |
instance | Object? | 组件的实例对象,这个属性只在 Mithril 内部使用。 |
vnode 的 tag 属性决定了它的类型。有 5 种 vnode 类型:
类型 | 示例 | 说明 |
---|---|---|
元素 | {tag: "div"} |
表示 DOM 元素。 |
片段 | {tag: "[", children: []} |
表示 DOM 元素的列表,其父 DOM 元素可能还包含不在片段中的其他元素。当使用 m() 函数时,只能通过把数组传入 m() 函数的 children 参数中来创建 vnode 片段。m("[") 无法创建有效的 vnode。 |
文本 | {tag: "#", children: ""} |
表示 DOM 文本节点。 |
HTML 字符串 | {tag: "<", children: "<br>"} |
表示来自 HTML 字符串的 DOM 元素列表。 |
组件 | {tag: ExampleComponent} |
如果 tag 是包含 view 方法的 JavaScript 对象,vnode 则表示通过渲染该组件生成的 DOM。 |
3.2 组件
组件是对视图的一部分进行封装,以方便组织代码和重用。任何具有 view 方法的 JavaScript 对象都是 Mithril 组件。
const component = {
view() {
return m('div', 'mithril component')
}
}
- 生命周期
组件和虚拟 DOM 节点都有生命周期方法,包括oninit
、oncreate
、onupdate
、onbeforeremove
、onremove
和onbeforeupdate
。 - 状态
vnode 有状态属性,组件的状态有3种方式可以进行访问:初始化时、通过vnode.state
、通过组件方法中的this
关键字。 - 组件传值
把一个 attrs 对象传入到m()
函数的第二个参数,即可把参数传入到组件实例中,然后在组件的视图和生命周期方法中可以通过vnode.attrs
来访问数据:
const component = {
view(vnode) {
return m("div", "Hello, " + vnode.attrs.name);
}
};
m(component, { name: 'Component' });
3.3 生命周期
所有生命周期方法都使用 vnode
作为第一个参数,并把 this
关键字绑定到了 vnode.state
。
钩子 | 用途及触发时机 |
---|---|
oninit |
初始化钩子,会在添加到 DOM 之前调用,可用于通过 vnode.attrs 或 vnode.children 传入的参数来初始化组件状态。更新的时候不会再调用,只有回收了再去更新才回再次调用。 |
oncreate |
DOM 元素创建完成并添加到 document 后,会调用 oncreate 钩子,当更新元素时,不会调用该钩子。可用于读取布局的值或者初始化需要引用 DOM 元素的方法。 |
onbeforeupdate |
onbeforeupdate 钩子会在对 vnode 进行 diff 之前调用。如果定义了该函数,且返回 false,Mithril 会阻止对 vnode 进行 diff,同时也阻止对 vnode 的子元素进行 diff。这个钩子本身不会阻止生成虚拟 DOM 子树,除非子树被封装在一个组件中。该钩子可用于在 DOM 树过大时,减少更新的延迟。 |
onupdate |
onupdate 钩子会在 DOM 元素被更新后调用。只有当前的渲染循环中存在该元素时,才会调用该钩子。元素被创建或回收时,不会调用该钩子。vnode 中含有 onupdate 钩子的 DOM 元素不会被回收。 |
onbeforeremove |
onbeforeremove 钩子在 DOM 元素从 document 中分离之前被调用。如果返回 Promise,则会在 Promise 完成后再分离。可在这个钩子添加组件分离时的动画效果。 |
onremove |
onremove 钩子在 DOM 元素被移除之前调用。如果同时定义了 onbeforeremove 钩子,onremove 会在从 onbeforeremove 返回的 Promise 完成之后调用。无论是它本身还是它的父元素移除时都会触发这个钩子。 |
3.4 自动重绘
Mithril 的自动重绘系统会在数据层的数据改变后更新 DOM 。
需要注意的是调用 m.mount()
或 m.route()
才会开启自动重绘,通过 m.render()
渲染的 vnode 不会进行自动重绘。
下面3种方法可以触发自动重绘:
- DOM 事件之后触发
- 调用请求 (
m.request()
) 方法之后 - 改变路由时 (
m.route()
)
如果不想进行重绘 可以用 e.redraw = false
禁用自动重绘:
const componemt = {
text: 'initial state',
view() {
return m('div', [
m('h1', `${this.text}`),
m('button', {onclick: e => {
this.text = 'state change';
e.redraw = false; // 禁用重绘
}}, 'click')
])
}
}
m.mount(document.body, componemt);
一般在 setTimeout
、 setInterval
、 requestAnimationFrame
、 Promise
和第三方库的事件处理方法后并不会触发自动重绘,不过可以通过 m.redraw()
方法来手动重绘:
const componemt = {
text: 'initial state',
oninit() {
setTimeout(() => {
this.text = 'state change';
m.redraw(); // 手动进行重绘
}, 1000);
},
view() {
return m('h2', this.text);
}
}
m.mount(document.body, componemt);
3.5 keys
Key 是一种允许对 DOM 元素进行重新排序的机制,把列表中的指定数据项映射到各自对应的 DOM 元素。通常,key 属性应该数组中的唯一标识字段,即该字段的值不应产生重复。
有 key 意味着,如果数据数组被打乱,且视图被重新渲染,DOM 元素将按照和以前一致的排序方式进行排序,以便保持正确的焦点和 DOM 状态。
const userInputs1 = {
users: [
{ id: 1, name: 'John' },
{ id: 2, name: 'Mary' },
],
view() {
return m('div', this.users.map(user => m('input',
{
value: user.name,
onclick: () => {
setTimeout(() => {
this.users[0] = this.users.pop();
m.redraw();
}, 3000);
}
}
)));
}
}
const userInputs2 = {
users: [
{ id: 1, name: 'Andi' },
{ id: 2, name: 'Sam' },
],
view() {
return m('div', this.users.map(user => m('input',
{
key: user.id,
value: user.name,
onclick: () => {
setTimeout(() => {
this.users[0] = this.users.pop();
m.redraw();
}, 3000);
}
}
)));
}
}
m.mount(document.querySelector('.div-1'), userInputs1);
m.mount(document.querySelector('.div-2'), userInputs2);
从上面的示例可以发现,没有 key
的 input 元素在重绘后没有保持获取焦点的状态,而有 key
的 input 元素并不受影响。
把 Mithril 基本的内容都简单梳理了一遍,总的来说,这个框架上手简单功能也比较全面。还是挺好用的。目前也是才接触不久,后期有时间考虑再来深入的探究一下。
参考
mithril官网
mithril 中文介绍
(*官方最新的版本是2.0.4,中文文档的版本是1.1.7*)
Mithril.js 入门介绍
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。