用Inferno代替React开发高性能响应式WEB应用

什么是Inferno

Inferno能够看作是React的另外一个精简、高性能实现。它的使用方式跟React基本相同,不管是JSX语法、组件的创建、组件的生命周期,仍是与Redux或Mobx的配合、路由控制等,均可以基本按照React的方式来开发,只有微小的不一样。不过Inferno是专门针对网页开发的,不能像React Native那样开发移动端本地APP。javascript

为何要用Inferno?

既然Inferno和React基本差很少,又没有开发本地APP的能力,那为何要用Inferno呢?简单来讲就是由于性能。css

首先Inferno自己的体积很是小,只有React的五分之一;在页面性能上,它也有着很是明显的优点。Inferno也使用了虚拟DOM技术,但即与React的不一样,也没有使用那个比较流行的开源virtual-dom项目,而是本身完整开发了一套虚拟DOM,它的实现相对轻量、高效,性能更好。至于Inferno的性能究竟有多好,能够参考Inferno主页(www.infernojs.org)上的跑分对比。java

所以,在一些很是重视性能的设备上试用Inferno就显得颇有优点了,尤为是移动端。虽然如今手机的更新换代很快,配置愈来愈高,可是网速和网络流量的制约依然要求下载的文件越小越好,并且手机上内存和CPU永远是很是宝贵的资源,对页面性能的要求依然高于PC。react

此外,Inferno有一些小的改进让它用起来比React更爽,尤为是按照flux模式构建纯函数组件时。程序员

总之,想享受React这样高效的响应式开发体验,又想得到接近于原生代码的高性能,Inferno是一个很是好的选择。npm

要使用Inferno应考虑些什么

可是若是真要把开发从React甚至其它框架上迁移到Inferno上来,有些问题须要先考虑清楚。json

1. 是否依赖集成化UI组件库

如今不少WEB软件,尤为是行业软件的开发,很是依赖于某一集成化的UI组件库。我甚至了解到一些开发人员开始学习React是由于想用Ant Design。VUE如今有如此好的发展也与其生态系统内愈来愈丰富的组件库有关。而Inferno毕竟是一个小众框架,起码如今想找到一个针对Inferno创建的完整的组件库是不可能的。尽管有inferno-compact这样的工具能够把react组件适配到Inferno中去,可是因为不少组件都会用到ref,而ref在两个框架中的用法是不同的(下文会详述),致使在Inferno中引用React组件困难重重。所以若是你的项目须要大量统一的、现成的组件的话,直接就放弃Inferno,老老实实用React或VUE就好。redux

不过当你的项目须要高度定制化,或者自己比较简单的话,就能够考虑用Inferno了。我就遇到了需求定制化程度过高,连Ant Design都没法知足的状况。而基于Inferno这样的框架来封装组件其实是一件很是惬意的事情。对于很是复杂的组件,好比日期选择、移动端滑动等,能够直接将那些不依赖于特定框架的库封装为Inferno组件,用起来也十分方便。api

2. 对浏览器兼容性的要求

Inferno只支持现代浏览器。若是你有很是重要的目标用户还非用IE9如下的浏览器不可,那最好仍是去用jQuery、用EasyUI。这是上一个时代的开发。数组

3. 是否有WEB和原生APP共享代码的计划

选择采用React的一个缘由多是React有一个衍生品是React Native,这就意味着一些大型应用可让移动端WEB和APP共享一套代码从而节约开发成本。但Inferno只能用于WEB开发,也正所以,它相对于React才有了大量的精简和性能优化。

4. 是否有本身解决问题的耐心

尽管Inferno和React用起来感受很像,但毕竟仍是有不一样之处。当出现问题时,react随便一搜就有一堆结果,而Inferno可供参考的恐怕只有官方文档(固然是英文,目前没有中文翻译),目前连stackoverflow上关于inferno的提问和回答也寥寥无几。固然,还有源代码。

并且Inferno比React年轻,又没有像React那样的facebook豪华团队来维护,尽管它基本稳定,但仍是会出现一些小问题。不过得益于他是一个处于活跃期的开源软件,这个版本发现的明显bug通常在随后的几个版本内就会被修复。

例如我曾遇到过一个古怪的问题,Inferno渲染的一组CheckBox,当其序列发生变化后,点击一个CheckBox会把另外一个勾选上。经过跟踪源代码发现Inferno为了达到最优性能,当虚拟DOM发生变化时,对于同一位置上的先后相同标签名元素不从新渲染,而是给实际DOM上原来的节点从新赋予属性,可是onChange这个事件属性是通过特殊处理的,并不是原生,在从新赋属性的时候就没有改变这个事件处理函数,因此经过不把CheckBox的绑定值放在闭包中而放到元素上现用现读就能够规避这个bug,并且下一个版本就修正了这个问题。再看前一个版本的代码发现这个bug挺乌龙的,原本前面不存在这个问题,是开发者在精简事件处理代码的时候忽略了对onChange这样加工过的事件处理的方式。赶巧就这一个版本有这个bug,被我遇上了。

对于这个小众框架的稳定性的怀疑多是不少人不敢用它最重要的缘由,实际上国外已经有一些公司在生产中使用了Inferno,因此也不用过度担忧。

开始使用Inferno

若是你已经会使用React开发,那就基本上已经会使用Inferno了。至于Inferno项目的搭建也与React项目基本相同,只是要把一些依赖的包替换成Inferno相关的。若是手头上有个React的项目,那就请打开package.json文件看看有哪些包的名称带有React字样,基本上把全部“react”都替换成“inferno”就能够了,下面列列举了一些可替换成Inferno相关的依赖包:

  • react → inferno
  • react-component → inferno-component
  • react-redux → inferno-redux
  • react-mobx → inferno-mobx
  • react-router → inferno-router

以及一些可能用到的开发依赖包:

  • babel-plugin-react → babel-plugin-inferno
  • eslint-plugin-react → eslint-plugin-inferno
  • react-devtools → inferno-devtools

对于没有列举到的react相关的包名,能够到npmjs.com上验证一下是否存在相对应的Inferno相关的包。

若是要使用jsx语法,须要让babel将jsx标签译为inferno所接受的函数,这就要在babel配置文件.babelrc中的plugins节点中添加inferno,好比一个简单、完整的支持inferno的.babelrc文件是这个样子:

{
  "presets": ["env", "stage-0"],
  "plugins": ["inferno"]
}

若是要使用eslint,一样须要在eslint配置文件.eslintrc中添加相应插件:

{
  "parser": "babel-eslint",
  "plugins": [
    "inferno"
  ],
  ...
}

总之就是按照react的配置方式把“react”都替换成“inferno”就好了。

Inferno和React差别

Inferno的开发和React大同小异,把这些关键的“小异”弄清楚了,开发也就没有什么障碍了。

建立元素和组件

Inferno建立元素可使用JSX语法或者createElement函数,这与React相同,不过createElement函数并不在Inferno包中,而是须要另外引入一个inferno-create-element包。

此外,Inferno还提供了一个Hyperscript方式来建立元素。它的使用方式与createElement类似,不过类名和id能够用css语法和标签名写在一块儿,且div能够省略,这与pug(原名jade)很类似。另一个不一样就是子节点放在数组里。

下面列举了Inferno建立元素的三种方式:

import Inferno from 'inferno'
const demo = 
  <div id="example1" className="example-div">
    Hello,
    <a className="example-link" href="infernojs.org">
      Inferno
    </a>
  </div>
import createElement from 'inferno-create-element'
const demo = createElement('div', {
  id: 'example1', 
  className: 'example-div'
}, 
  'Hello, ',
  createElement('a', {
    className: 'example-link', 
    href: 'infernojs.org'
  }, 
    'Inferno'
  )
)
import h from 'inferno-hyperscript'
const demo = h('#example1.example-div', [
  'Hello, ',
  h('a.example-link', {
    href: 'infernojs.org'
  }, [
    'Inferno'
  ])
])

当向页面上渲染节点时,Inferno通React同样是使用render函数,不过Inferno的render函数是属于Inferno包的,而不像React那样是一个单独的react-dom包.

import Inferno from 'inferno'
Inferno.render(<div>Hello, Inferno</div>)

组件

Inferno声明组件有三种方式:函数组件、ES6(2015)的类继承Component类,以及使用createClass函数。这基本上都与React相同,不过Inferno的Component类来自于单独的包“inferno-component”,createClass函数也来自于单独的包“inferno-create-class”。

Inferno十分鼓励开发者使用函数组件,也就是只有一个至关于render函数的组件,它具备无状态、相似于纯函数的特色。下面还会看到Inferno提供了一系列很是方便于使用函数组件的特性。

生命周期函数

咱们知道在React中,若是组件使用createClass函数建立或者继承Component类,就能够经过实现生命周期方法——如componentDieMount等——在组件生命周期的各关键点上作一些事情,而函数组件就没办法了。Inferno则能够经过给函数组件传入生命周期属性函数来实现生命周期管理。生命周期属性名大可能是在那些生命周期名称前面加on。Inferno支持的全部生命周期属性以下:

  • onComponentWillMount
  • onComponentDidMount
  • onComponentShouldUpdate
  • onComponentWillUpdate
  • onComponentDidUpdate
  • onComponentWillUnmount

这样咱们就能够在用inferno-redux的connect函数建立容器型组件时给函数组件注入生命周期属性函数了,这就使得在更多状况下可使用函数组件。

注意:生命周期属性只支持函数组件,对其余方式建立的组件无效。

NO_OP

shouldComponentUpdate这个生命周期对应的属性是onComponentShouldUpdate,咱们知道在这个生命周期中作一些是否须要渲染的判断能够提高性能。Inferno对此有一个更方便的办法,就是Inferno.NO_OP。这是一个无需从新渲染的标记,当在渲染的函数中(函数组件自己或是render函数)返回这个标记时,就至关因而在shouldComponentUpdate中返回了false。NO_OP和函数组件搭配使用很是简洁方便。遗憾的是函数组件的函数中没法获取组件上一次渲染的属性,最经常使用的根据属性来判断是否须要从新渲染的方法没法用在NO_OP上。

ref

在react中,能够给元素添加一个ref字符串属性,在组件渲染以后就能够经过this.refs.xxx来找到标记了ref属性的元素了。Inferno元素也支持ref属性,不过与react不一样的是它接受的不是一个字符串,而是回调函数。在组件渲染完成后会调用这个回调函数,传入的参数是元素的真实dom节点,你能够自由地把这个节点存储到任何地方以备之后使用,也能够当即使用。这样函数组件也可使用ref引用出来的元素了。

不过因为Inferno与React的这点差别,致使不少基于React的组件难以适配到Inferno上。目前我还没发现有好的解决办法,反正我也没有怎么尝试把react组件用到inferno上来,就像我前面说的,若是想用Ant Design这样的基于react的组件库,仍是老老实实用react吧。

另外有个值得注意的地方是ref属性只对原生dom元素有效,即'div'、'input'这类元素,而Inferno组件(非原生dom标签)在加载时会自动忽略掉ref属性。即使咱们想在本身写的组件中经过ref属性来手动传递元素,也会发现根本接收不到ref属性。真一点真是挺奇怪的。不过既然是本身写的组件,就能够根据须要随便命名属性了。好比能够定一个“elRef”属性,将其直接传给组件最外层标签(原生dom标签,非Inferno组件)的ref属性,这样就能够在使用这个组件的时候获得最外层实际dom元素。若是要让组件的ref跟React组件经过ref所拿到的内容一致,能够定一个“cpnRef”属性,在componentDidMount方法中调用并传入组件实例对象,即this。不过要注意作好项目中的命名规范。

onChange及事件函数绑定

Inferno的事件处理方式与React也基本相同,不过在change事件上的处理与React不一样,Inferno采用与Input原生的change事件相同的触发方式,不会让每一次键盘的输入都触发change事件。而要获得React中那样的onChange效果,可使用onInput。

inferno提供了一个很小的事件辅助函数:LinkEvent。咱们知道当用ES6的类来声明React组件时,做为类方法的事件处理函数需和“this”绑定才能访问到this,并且应当在构造函数中而不是在事件属性上进行绑定,以避免性能损失。Inferno经过LinkEvent给出了更简洁的方法,看下面示例就明白怎么用了:

import Inferno, { linkEvent } from 'inferno'
import Component from 'inferno-component'

class MyComponent extends Component {
  render () {
    return <div><input type="text" onChange={linkEvent(this, handleChange)} /><div>;
  }
}

function handleChange(instance, event) {
  instance.props.setValue(event.target.value)
}

linkEvent其实作了一个很是简单的事情,就是返回了一个这样的对象:{data: this, event: handleChange}。把这个对象直接写在onChange属性里面效果也是同样的。而Inferno的事件封装函数遇到了这样格式的对象时就会进行相应的特殊处理。

我以为linkEvent的意义不只在于略微简化了事件处理函数的绑定,而是让函数组件能够容易地使用事件处理函数。以下面例子所示:

import Inferno, { linkEvent } from 'inferno'

export default function (props) {
  return <div><input type="text" onChange={linkEvent(props, handleChange)} /><div>;
}

function handleChange(props, event) {
  props.setValue(event.target.value)
}

list里的key

React要求渲染数组时数组中的每个元素必须有不相同的key属性,若没有key则会在控制台出现吓人的红色警告。

Inferno的元素也支持key属性,但做用与React不太同样。它的做用是指导元素渲染,对于同一级别的兄弟元素,在一次渲染中,跟上一此渲染具备相同key的元素认为是同一元素,这个元素就不会被替换,而会保持原有状态(好比Input元素的聚焦状态和非受控的输入值不变)。

注意,Inferno元素的key属性是对同一父元素中同一级别的兄弟元素有效,也就是不只限于数组元素。对于被渲染的数组,Inferno也不要求数组中的元素必须有key属性,并且对于具备重复key的元素不会进行排除,只会给出控制台警告。

在通常状况下Inferno不推荐使用key属性,除非须要保持元素的原生dom状态,或者须要在数组的中间添加、删除元素。

PropTypes

Inferno不支持像React那样用PropTypes来限定组件的属性。实际上我之前在用React开发的时候不多使用PropTypes,即使用,顶多也是体现出一个文档的功能,由于React不会对未匹配到PropTypes类型的属性抛出异常,而只是在开发时在控制台输出警告。的确PropTypes校验会在团队开发中让一些类型错误更容易被发现,不过对于用惯了动态类型语言的我来讲没有PropTypes并没感到有什么缺失,我甚至在开发一些组件的时候故意让某些属性能够是多个类型,以实现更灵活的api。

组件封装

前面提到过,对于一些比较复杂的组件,能够直接找一些成熟的、不依赖特定框架的第三方组件进行封装,用起来也很是方便。这个其实不是Inferno特有的能力,React一样很是擅长于此。不过一般因为React有完善的配套组件库,不少人在开发中不太有机会涉及到封装第三方组件。这里我举个例子以供参考。

就拿日期选择组件来讲。我不打算使用my97datepicker这样的“老式控件”,由于它须要生硬地引入一堆东西,不符合如今模块化开发的方式,我选择了在npm上找到的flatpickr。用npm将它安装后,就能够用下面的代码进行封装了:

import Inferno from 'inferno'
import Component from 'inferno-component'
import moment from 'moment'
import flatpickr from 'flatpickr'
import flatpickrZh from 'flatpickr/dist/l10n/zh.js'
import 'flatpickr/dist/flatpickr.css'
flatpickr.localize(flatpickrZh.zh)

export default class extends Component{
  render(){
    const {style, className, value} = this.props
    return (
      <input ref={el=>this.el = el} style={style} className={className} value={value}/>
    )
  }

  componentDidMount(){
    const {onChange, options} = this.props
    this.pickr = flatpickr(this.el, {
      onChange(dates){
        onChange(moment(dates[0]).format('YYYY-MM-DD'))
      },
      ...options
    })
  }

  componentWillUnmount(){
    this.pickr.destroy()
  }
}

封装组件的通常方式就是在Inferno组件渲染完成时,取出已渲染的dom元素,用它和经过props传入的属性来构建第三方组件。flatpickr须要用一个dom元素做为日历显示的触发元素,这里固定使用了一个input元素。flatpickr所需的属性除了onChagne外都经过options属性传入,在onChange里作了日期格式化,以在特定状况下组件使用更方便。因为有了onChange和value,这个组件能够像其余表单元素同样进行受控操做,并且为了和其它表单元素使用上一致,在实际项目中我会改变一下传给onChange的参数,模拟一个event对象,把值放到event.target.value中,便于批量处理。

最后不要忘记卸载组件时销毁第三方组件,不然可能会在页面上留下一堆垃圾。

写在最后

我相信经过阅读这些文字后,已经掌握了React开发的程序员就能够用Inferno进行开发了。我写这些既是对前段时间用Inferno的开发进行一些总结,也是但愿藉此来让更多人了解并尝试使用Inferno这样小而美的框架。

相关文章
相关标签/搜索