什麼是Inferno Inferno可以看做是React的另一個精簡、高性能實現。它的使用方式跟React基本相同,無論是JSX語法、組件的建立、組件的生命周期,還是與Redux或Mobx的配合、路由控制等,都可以基本按照React的方式來開發,只有微小的不同。不過Inferno是專門針對網頁開發的 ...
什麼是Inferno
Inferno可以看做是React的另一個精簡、高性能實現。它的使用方式跟React基本相同,無論是JSX語法、組件的建立、組件的生命周期,還是與Redux或Mobx的配合、路由控制等,都可以基本按照React的方式來開發,只有微小的不同。不過Inferno是專門針對網頁開發的,不能像React Native那樣開發移動端本地APP。
為什麼要用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可供參考的恐怕只有官方文檔(當然是英文,目前沒有中文翻譯),目前連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這樣小而美的框架。