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

时间:2021-10-05 19:39:26

什么是Inferno

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

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

为什么要用Inferno?

既然Inferno和React基本差不多,又没有开发本地APP的能力,那为什么要用Inferno呢?简单来说就是因为性能。

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

因此,在一些非常重视性能的设备上试用Inferno就显得很有优势了,尤其是移动端。虽然现在手机的更新换代很快,配置越来越高,但是网速和网络流量的制约依然要求下载的文件越小越好,而且手机上内存和CPU永远是非常宝贵的资源,对页面性能的要求依然高于PC。

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

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

要使用Inferno应考虑些什么

但是如果真要把开发从React甚至其它框架上迁移到Inferno上来,有些问题需要先考虑清楚。

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

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

不过当你的项目需要高度定制化,或者本身比较简单的话,就可以考虑用Inferno了。我就遇到了需求定制化程度太高,连Ant Design都无法满足的情况。而基于Inferno这样的框架来封装组件实际上是一件非常惬意的事情。对于非常复杂的组件,比如日期选择、移动端滑动等,可以直接将那些不依赖于特定框架的库封装为Inferno组件,用起来也十分方便。

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

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

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

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

4. 是否有自己解决问题的耐心

尽管Inferno和React用起来感觉很像,但毕竟还是有不同之处。当出现问题时,react随便一搜就有一堆结果,而Inferno可供参考的恐怕只有官方文档(当然是英文,目前没有中文翻译),目前连*上关于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这样小而美的框架。