手记

【前端骚操作】易、轻、快!超牛逼纯JS前端框架Mithril

先看框架使用的成果:Koojee,看不了网页的请看截图:

这是工程项目,链接那个是 Vue 的,截图是 Mithril 的,效果是一样一样的。


先介绍一下 Mithril,一张图说明问题:

有经验的小伙伴应该很清楚关于 Vue 或者 React 以及 Angular 打包后包的大小,最小的 Vue 也要上 100K 以上。虽然有按需加载这种解决方案,但是,还是很大。。。

Mithril 的优势之一就是小,就是轻,加载快,响应快,如上方的对比测试。

关于 Mithril 的优势,主要是两个(快的这个需要测试,请相信官网):

  1. 非常轻巧

  2. 相当简单

轻巧非常容易理解了,就是压缩包只有 10K 不到,这个大小在当前的网络环境中,几乎就是秒加载,可以忽略不计的大小了。

所谓的简单,本文将使用上述的工程例子来简单说明,请小伙伴们跟紧了。


一般来说,一个 MVx 的 JS 框架到手,首先需要考虑的是数据驱动,向 Vue 的 data绑 定,React 的 setState 等,而 Mithril 相对这二者来说,更加简单,就是普通的变量,恩,就是这种:var a = 1。然后在 Mithril 的 m 函数里面使用,变化就会直接使跟数据有关的 DOM 渲染了。

以下与工程无关的代码将直接使用官网的例子。

var count = 0 // added a variable

var Hello = {
    view: function() {
        return m("main", [
            m("h1", {class: "title"}, "My first app"),
            // changed the next line
            m("button", {: function() {count++}}, count + " clicks"),
        ])
    }
}

m.mount(root, Hello)

上述代码可以看到,count 只是普通的变量,然后 count 被用在一个 Hello 的组件里面,然后点击 button 就会造成 count + 1,从而使 Hello 组件渲染,效果如下:

好了,数据驱动理解了,提一下 m 函数与 m.render 还有 m.mount 即可进入我们的项目了。


m函数

m 函数就是 JS 生成 DOM 的函数,与h函数非常相似,h函数介绍

m 函数有三个参数:标签名称(支持 emmet),标签属性对象(一个 object 对象),标签内容。

m 函数还有种参数:组件,组件的参数对象。

如:

m('div#main', {class: 'normal'}, '我是div')

// <div id='main' class='normal'>我是div</div>

m('div#main', {class: 'normal'}, [m('button', '我是button')])

// <div id='main' class='normal'>
// <button>我是button</button>
// </div>

m([组件], [组件参数]) // 组件参数建议使用object

除了第一个参数,其余两个都可以为空,就是不写,然后第三个参数可以是单个 m 函数返回值也可以是数组。


m.render 函数

插入渲染结果的 DOM,插入的对象。

如:

m.render(document.body, "My first app")

就是 body 里面插入一段字符。

这里就是为什么我对MithrilJS念念不忘的原因,它不用强制生成个类似:<div id='app'></div>根结构的东西就可以直接渲染了。看什么,说的就是你,Vue。

m.render 只渲染一次,后续如果有数据变动,也不会渲染,所以我们不用。


m.mount 函数

这个与 m.render 参数差不多。

m.mount(document.body, [生成的组件])

m.mount 生成的会多次渲染,这是首选,但是我们也不用,原因是我们的页面是单页面牵扯到页面内路由,而 Mithril 提供了 m.route 这个路由启动渲染,我们使用的是这个,具体会在项目代码里面说。


进入项目

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <title>WBRoom</title>
    <link rel="stylesheet/less" href="less/app.less">
  </head>
  <body>

    <script src="https://cdn.bootcss.com/less.js/3.0.4/less.js"></script>
    <script src="https://cdn.bootcss.com/mithril/1.1.6/mithril.js"></script>
    <!-- <script src="https://cdn.bootcss.com/less.js/3.0.4/less.min.js"></script> -->
    <!-- <script src="https://cdn.bootcss.com/mithril/1.1.6/mithril.min.js"></script> -->
    <script src="js/components/header.js"></script>
    <script src="js/components/footer.js"></script>
    <script src="js/pages/homepage.js"></script>
    <script src="js/pages/share.js"></script>
    <script src="js/app.js"></script>
  </body>
</html>

这里面是纯原生,没有使用 webpack 之类的打包,所以引用了很多组件 js,当然你也可以写到一个里面,但是那实在不好看也不好拓展。

使用了 less,为了偷懒,你让我在工程里面使用原生 CSS,臣妾真的做不到了。


less

less 的内容就不做过多展示,这个有兴趣在下方去源码里面看吧,这里略过了。


js

大头来了,我这里会直接贴出代码,然后在路由那里说一下,为什么我不多说呢,因为 Mithril 真的太简单了(我会告诉你我懒得说么)。。。

header.js

const Header = (function() {
  return {
    view(v) {
      const titles = ['首页', '分享', '设计', '绘画', '摄影']
      const hrefs = ['#!/', '#!/share']
      const selectedIndex = v.attrs.selectedIndex

      return m("header", [
        m("h1", "KOOJEE"),
        m('section.titles',
          titles.map((title, index) => m('a', {class: 'title' + (selectedIndex === index ? ' selected' : ''), href: hrefs[index] || '#'}, titles[index])
          )
        ),
      ])
    }
  }
})()

这里使用了自运行函数,为了是避免变量名污染。

组件就是个对象,组件 DOM 结构实在 view 这个属性值下的一个函数返回,函数可携带 vnode 对象,用来传参等等,获取参数使用v.attrs

其他就是常规内容,渲染了一个header标签,里面包了一个 h1 标签,包了一个 section.titles 标签, section.titles 里面根据titles数组生成了 5 个 a.title 标签。

这就是 header 了。


footer.js

const Footer = (function() {
  return {
    view() {
      return m(
        'footer'
      )
    }
  }
})()

目前没东西,所以渲染了一个空的 footer 标签用来占位。


homepage.js

const Homepage = (function() {
  const h2 = '最终选择了生活……'
  const h3 = '宁愿向着远方哭泣,不愿望着当下诧异。'
  let contentImgs = ['Bitmap1.png', 'Bitmap2.png', 'Bitmap3.png', 'Bitmap4.png']
  let arts = [
    {
      img: 'Bitmap5.png',
      main: '经历',
      desc: '我在东方等你,不济那远去的夕阳。',
    },
    {
      img: 'Bitmap6.png',
      main: '兴趣',
      desc: '给个游戏,能躺半年。',
    },
    {
      img: 'Bitmap7.png',
      main: '性格',
      desc: '热辣似火,妖娆弄人?不不不,就是呆萌。',
    },
    {
      img: 'Bitmap8.png',
      main: '态度',
      desc: '你到底准备用什么态度和姑奶奶说话?',
    },
  ]

  const Content = {
    onbeforeremove(v) {
      v.dom.classList.add("exit")
      return new Promise(function(resolve) {
          setTimeout(resolve, 500)
      })
    },
    view() {
      return m('main#homepage.fancy',
        m('section.content-center', [
          m('h2', h2),
          m('h3', h3),
          contentImgs.map((contentImg, index) => m('img', {class: 'img' + index, src: assetsPath + contentImg, alt: 'content-img'}))
        ])
      )
    }
  }

  return {
    view(v) {
      return [
        m(Header, {selectedIndex: 0}),
        m(Content),
        m(Footer),
      ]
    }
  }
})()

因为将来要使用数据驱动,所以把数据提出去,方便后续服务器请求数据操作。

这里使用了 m 函数的组件模式,如上例子中的:m(Header, {selectedIndex: 0}),就是 homepage 组件里面包含了一个 Header 组件,并且给 Header 组件传参了,这个参数的使用在上面 Header 组件那里可以看到。

注意这里有个内部的 Content 组件,这个是用来做动画的,比如你页面内路由切换时,为了看起来更舒服,可以做个过渡动画,Homepage 组件自带 fancy 类的 CSS,然后 Homepage 的 Content 组件在声明周期 onbeforeremove 即将要消失的时候添加了一个 exit 类的 CSS,具体两个 CSS 如下:

.fancy {animation:fade-in 0.5s;}
@keyframes fade-in {
    from {opacity:0;}
    to {opacity:1;}
}

.exit {animation:fade-out 0.5s;}
@keyframes fade-out {
    from {opacity:1;}
    to {opacity:0;}
}

了解 CSS 的同学就知道是什么动画了,对,就是淡入与淡出,分别 0.5s。

说到这里,刚好顺带过一下 Mithril 的声明周期,如下:

用法与 view 一样,放在对象的属性上,对应值是函数,都可以获取 vnode。

  1. oninit

    初始化时候,方便放一些准备用的数据,或者用来网络请求。此时可以拿到 vnode,但是不一定拿得到真实 DOM,所以这里不推荐进行相关的 DOM 操作,比如:vnode.dom

  2. oncreate

    创建成功,此时可以拿到真实 DOM 了。

  3. onupdate

    DOM 渲染刷新后。业务有刷新变动数据时候使用。

  4. onbeforeremove

    DOM 销毁前。常用,比如我们的离开动画。

  5. onremove

    DOM 销毁后。一样不建议进行真实 DOM 操作,用来销毁垃圾数据可以使用。

  6. onbeforeupdate

    DOM 渲染刷新前。业务有刷新变动数据时候使用。


share.js

const Share = (function() {
  let bannerSrc = 'https://wbroom-blog.oss-cn-hangzhou.aliyuncs.com/public/assets/share-bannre.png'

  const Content = {
    onbeforeremove(v) {
      v.dom.classList.add("exit")
      return new Promise(function(resolve) {
          setTimeout(resolve, 500)
      })
    },
    view() {
      return m('main#share.fancy', [
        m(`img.share-banner[src=${bannerSrc}]`),
        m('section.arts',
          [1,1,1,1,1,1].map((item, index) => m('figure.art', [
            m(`img.head[alt=${index}]`, {src: 'https://wbroom-blog.oss-cn-hangzhou.aliyuncs.com/public/assets/Bitmap1.png'}),
            m('figcaption.main', '别看,看也没博文。'),
            m('span.time', '耶稣生日的那天'),
            m('section.ctrl', [
              m('span', '点赞(1000)'),
              m('span', '评论(1000)'),
              m('span', '浏览(100000)')
            ])
          ]))
        ),
        m('section.pagination', '页码(待处理)'),
      ])
    }
  }

  return {
    view(v) {
      return[
        m(Header, {selectedIndex: 1}),
        m(Content),
        m(Footer),
      ]
    }
  }
})()

与 homepage 大同小异,只是堆页面,放置一些图片,页码也还没做~

注意,一样要设置进场动画与离场动画。


app.js

const rootPath = 'https://wbroom-blog.oss-cn-hangzhou.aliyuncs.com/'
const publicPath = rootPath + "public/"
const assetsPath = publicPath + "assets/"

m.route(document.body, '/', {
  "/": Homepage,
  '/share': Share,
})

这个就是常说的入口 js 了,由于存在 JS 对象的依赖关系,所以上面的组件不得不先加载,然后最后加载入口js。

第一:做一些全局的数据,比如本项目里面需要用到的图片资源 basic 路径等。

第二:路由的简单配置。

注意,Mithril 没有类似 Vue 的 router-view 这种组件,它还是建议把组件配到大组件里面,这个缺点就是如果业务的路由组件非常深入,就相对麻烦,但是优点还是简单!

这里配置根路径地址就是 Homepage 组件,当路由切换到 share 时候,就是 Share 组件显示了。

同样的,MithrilJS的路由标记像这样:#!/share,默认是使用#!,当然,你可以通过类似m.route.prefix("#")来修改路由标记,但是个人觉得意义不大。起码别人逛你网站的时候,懂行的一眼就看出来你用的是 Mithril 写的页面,恩,厉害!


到最后,index.html 解析所有的内容后,就渲染出文章开头的页面了,点击切换时候还有淡入淡出的效果(上面的链接看不到,因为 Vue 版本我还没加路……)。


OK,简单,快速,小巧的一个牛逼纯 JS 框架 Mithril,带你们走了一遍简单的。

Mithril 当然也是支持 ES6,以及 JSX 的,本人不是很喜欢 JSX 的写法,所以采用了原生的,而要用以上两个,都需要去配置下 webpack,这里不做过多介绍了。

如果想看源码,请到下面的 GitHub 源码里面,找到 mi 分支即可,默认 master 分支是 Vue 版本哦。


项目源码:Github


欢迎关注,如有需要 Web,App,小程序,请留言联系。

7人推荐
随时随地看视频
慕课网APP

热门评论

这层级让我想到了flutter?

查看全部评论