2

公司的一个项目的迭代,在前期研究中,考虑开发及生产环境的一些情况和要求,大佬们选择了 Mithril 这个框架,前端方面由我负责。在这之前我是没听说过 Mithril的,就花了点时间去学习,发现关于这个框架的内容还是比较少的。这篇文章也是简单整理了一下一些基础的内容,算是学习笔记。

 1 Mithril 介绍

 1.1 是什么

mithril是一个小巧的、mvc 模式的、用于构建单页面应用的现代化 JavaScript 框架。
最新版本(2.0.4)支持 IE11 以上的浏览器,v1版本支持 IE9 以上。

 1.2 与其他框架的对比

首先,从框架的体积大小来说, Mithril 与其他框架相比都要小的多。这里贴一个 Mithril 官网的对比图:
437ed6fdd52b6bfe6fa93679cb17cc0b.png
对比现在流行的三大框架, 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 代码转换后:
f429e5983424b462e2a3f045422891b7.png
对于 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 节点都有生命周期方法,包括 oninitoncreateonupdateonbeforeremoveonremove 和 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种方法可以触发自动重绘:

  1. DOM 事件之后触发
  2. 调用请求 (m.request()) 方法之后
  3.  改变路由时 (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);

3bd04b11922f31780eb1f27fb7a7335f.gif
从上面的示例可以发现,没有 key 的 input 元素在重绘后没有保持获取焦点的状态,而有 key 的 input 元素并不受影响。


把 Mithril 基本的内容都简单梳理了一遍,总的来说,这个框架上手简单功能也比较全面。还是挺好用的。目前也是才接触不久,后期有时间考虑再来深入的探究一下。

参考
mithril官网  
mithril 中文介绍  
(*官方最新的版本是2.0.4,中文文档的版本是1.1.7*)  
Mithril.js 入门介绍

这个我不会啊
16 声望0 粉丝