React騷操作——jsx遇到template-directive

孤狼醬發表於2018-08-29

 “React 和 Vue 哪個更好?” 論壇上經常看到這樣的問題,然後評論區就直接開戰了。也有朋友轉行做前端,問我該學React還是Vue。幾年前,可能確實有必要考慮下到底該選擇哪一個,畢竟前端圈子這麼亂,誰又知道Vue能走多遠?React會不會不維護了呢?可現在兩者生態都很不錯,Vue確實好用,React學習成本也沒有傳聞中那麼高,template很好用,jsx也更靈活。可以兩者都去玩玩,根據個人喜好和專案需要來選擇用哪個。而如果能夠結合兩者的優點,那豈不是很有趣?

 我剛轉前端的時候,用的是vue版本好像還是1.0,那時候的感覺就是資料繫結比jquery操作dom省事兒太多了。後來又接觸了React,之後的專案大部分用React寫的,現在偶爾也用vue,總體感覺就是vue單檔案元件結構比較清晰,模板指令也很好用,而jsx更加靈活,之前有在react狀態管理部分做一些嘗試,可以像普通function一樣去更新狀態,也一直想在jsx中加上類似vue裡面的模板指令,直到前幾天比較閒,總算實現了 “條件渲染”和 “列表渲染”,結果還算不錯,過程也挺有趣。

 先來看看結果吧,以前要根據不同狀態來控制模組是否顯示,我們大概要寫這樣的程式碼:

render(){
    const visible = true
    return(
        <div>
            {
                visible ? <div>content<div>
                        : ''
            }
        </div>
    )
}
複製程式碼

 現在可以這麼玩:

render(){
    const visible = true
    return(
        <div>
            <div r-if = {visible}>content</div>
        </div>
    )
}
複製程式碼

 另一種常見的場景就是根據一個陣列來渲染出一個列表,一般是這麼寫:

render(){
    const list = [1, 2, 3, 4, 5]
    return(
        <div>
            {
                list.map((item,index)=>(
                	<div key={index}>{item}</div>
                ))
            }
        </div>
    )
}
複製程式碼

 現在可以更簡潔:

render(){
    const list = [1, 2, 3, 4, 5]
    return(
        <div>
            <div r-for = {item in list}>{item}</div>
        </div>
    )
}
複製程式碼

 以上程式碼會自動設定key,值為當前元素的索引。如果你想要自定義key,也可以加上,改成

<div r-for = {(item,index) in list} key = {index+1}>{item}</div>
複製程式碼

 結果還算不錯吧,程式碼更簡短,語義也比較明確,體驗也不必vue裡面的模板指令差,個人感覺在“”裡面寫js有點奇怪。而在{}裡面寫就很自然,就是普通的js程式碼塊嘛。

 至於實現方法嘛,其實很簡單,總共才幾十行程式碼,就是寫了一個babel外掛。

 我們寫的jsx也是通過babel轉譯成普通js程式碼的,然後才能在瀏覽器中執行,而babel編譯主要分為三個階段:解析、轉換、生成目的碼。解析部分就是將原始碼解析成抽象語法樹ast,轉換過程是對ast做一些處理,而生成目的碼部分就是將ast再轉換成js程式碼。babel-plugin就是在轉換部分做一些工作。

 比如對於以下jsx:

<div r-if = { visible }>{content}</div>
複製程式碼

 轉換成的ast結構大概是:

{
   	type: 'CallExpression',
    callee: {},
    arguments: {
        properties: []
    }
}
複製程式碼

 目的碼為:

React.createElement(
    'div',
    {'r-if': visible},
    content
)
複製程式碼

 React.createElement()方法呼叫對應ast中的CallExpression, React和createElement在callee中可以找到,可以以此來找出createElement(), r-if 等屬性以及第三個引數content在arguments的properties陣列中能找到。有了這些資訊,我們就可以遍歷ast,找到那些callee為React.createElement的CallExpression, 然後判斷arguments中如果出現了r-if, 就對ast做以下修改:首先移除r-if屬性,避免死迴圈;然後在CallExpression對應的節點外面再套一層ifStatement, 如此一來,轉換後的ast生成的目的碼大致如下:

if(visible){
    React.createElement(
        'div',
        {'r-if': visible},
        content
    )
}
複製程式碼

 至此,我們的目標就已經達到了,至於r-for列表渲染,原理類似,先找出有r-for屬性的CallExpression, 然後構造一個map方法對應的CallExpression, 當前CallExpression作為引數傳給map方法即可。需要注意的是,在同一次遍歷中解析出 r-if 和 r-for 的話,需要把map放在外層,ifStatement放在裡面,如果覺得這樣做結構比較混亂,可以拆分成不同的外掛。

 最後再說一下babel-plugin的寫法,其實也就是一個方法:

module.exports = function ({ types: t }) {
  return {
    visitor: {
      CallExpression(path) {
          // 在這裡通過修改path來修改ast。
      },
      Identifier(path) {
            
      }
    }
  }
}
複製程式碼

 types型別為babel-types, 提供了一些類似loadash的操作方法,比如做一些判斷、構造節點。visitor裡面寫對應型別節點的遍歷方法, 比如遍歷識別符號型別的就寫在Identifier中,方法呼叫就寫在CallExpression中。

 本文中提到的r-if 和 r-for 已經寫成了一個外掛,可以在github倉庫中找到:github.com/evolify/bab… 同時也釋出到了npm倉庫,可以直接安裝:

yarn add --dev babel-plugin-react-directive

 然後在.babelrc中配置即可:

{
    "plugins": [
        "react-directive"
    ]
}
複製程式碼

 我想要的目的已經達到了,但這並未結束,才剛剛開始,還可以實現其他的一些指令,比如r-if 是模組渲染或者不渲染的,我們經常也會遇到這種需求:只是單純的控制元素可見或者不可見,但元素還是佔用空間的,也就是控制visibility, 這也可以寫成一個指令。而babel能做的遠遠不止如此,無聊的時候可以好好玩一玩。

相關文章