聊聊 Vue 中的 JSX

作者:Rockky

原文:https://juejin.cn/post/7188898676993949752

JSX 簡介

JSX 是一種 Javascript 的語法擴展,即具備了Javascript的全部功能,同時又兼具html的語義化和直觀性。它可以讓我們在 JS 中寫模板語法:

const el = <div>Vue 2</div>;

上面這段代碼既不是 HTML 也不是字符串,被稱之爲 JSX,是 JavaScript 的擴展語法。JSX 可能會使人聯想到模板語法,但是它具備 Javascript 的完全編程能力。

什麼時候使用 JSX

當開始寫一個只能通過 level prop 動態生成標題 (heading) 的組件時,你可能很快想到這樣實現:

<script type="text/x-template" id="anchored-heading-template">
<h1 v-if="level === 1"> 
    <slot></slot> 
</h1> 
<h2 v-else-if="level === 2"> 
    <slot></slot> 
</h2> 
<h3 v-else-if="level === 3"> 
    <slot></slot> 
</h3> 
</script>

這裏用 template 模板並不是最好的選擇,在每一個級別的標題中重複書寫了部分代碼,不夠簡潔優雅。如果嘗試用 JSX 來寫,代碼就會變得簡單很多:

const App = {
  render() {
    const tag = `h${this.level}`
    return <tag>{this.$slots.default}</tag>
  }
}

或者如果你寫了很多 render 函數,可能會覺得下面這樣的代碼寫起來很痛苦:

createElement(  
    'anchored-heading'{  
        props: {  
            level: 1  
        }  
    }[  
    createElement('span''Hello'),  
        ' world!'  
    ]  
)

特別是對應的模板如此簡單的情況下:

<anchored-heading :level="1">  
    <span>Hello</span> world!  
</anchored-heading>

這時候就可以在 Vue 中使用 JSX 語法,它可以讓我們回到更接近於模板的語法上:

import AnchoredHeading from './AnchoredHeading.vue'  
  
new Vue({  
    el: '#demo',  
    render: function (h) {  
        return (  
            <AnchoredHeading level={1}>  
                <span>Hello</span> world!  
            </AnchoredHeading>  
        )  
    }  
})

在開發過程中,經常會用到消息提示組件 Message,可能的一種寫法是這樣的:

Message.alert({
  messge: '確定要刪除?',
  type: 'warning'
})

但是希望message可以自定義一些樣式,這時候你可能就需要讓Message.alert支持JSX了(當然也可以使用插槽 /html等方式解決)

Message.alert({
  messge: <div>確定要刪除<span style="color:red">xxx</span>的筆記?</div>,
  type: 'warning'
})

此外,一個 .vue 文件裏面只能寫一個組件,這個在一些場景下可能不太方便,很多時候寫一個頁面的時候其實可能會需要把一些小的節點片段拆分到小組件裏面進行復用,這些小組件其實寫個簡單的函數組件就能搞定了。平時可能會由於 SFC 的限制讓我們習慣於全部寫在一個文件裏,但不得不說可以嘗試一下這種方式。

// 一個文件寫多個組件
const Input = (props) => <input {...props} />
export const Textarea = (props) => <input {...props} />
export const Password = (props) => <input type="password" {...props} />

export default Input

比如這裏封裝了一個 Input 組件,我們希望同時導出 Password 組件和 Textarea 組件來方便用戶根據實際需求使用,而這兩個組件本身內部就是用的 Input 組件,只是定製了一些 props。在 JSX 裏面就很方便,寫個簡單的函數組件基本上就夠用了,通過 interface 來聲明 props 就好了。但是如果是用模板來寫,可能就要給拆成三個文件,或許還要再加一個 index.js 的入口文件來導出三個組件。

由於 JSX 的本質就是 JavaScript,所以它具有 JavaScript 的完全編程能力。再舉個例子,我們需要通過一段邏輯來對一組 DOM 節點做一次 reverse,如果在模板裏面寫,那估計要寫兩段代碼。

雖然這個例子可能不太常見,但是不得不承認,在一些場景下,JSX 還是要比模板寫起來更加順手。

從 Vue 2 開始,template 在運行之前,會被編譯成 JavaScript 的 render function

Vue 推薦在絕大多數情況下使用 template 來創建你的 HTML。然而在一些場景中,就需要使用 render 函數,它比 template 更加靈活。這些 render function 在運行時階段,就是傳說中的 Virtual DOM

JSX 在 Vue2 中的基本使用

配置

在 Vue 2 中,JSX 的編譯需要依賴 @vue/babel-preset-jsx 和 @vue/babel-helper-vue-jsx-merge-props 這兩個包。前面這個包來負責編譯 JSX 的語法,後面的包用來引入運行時的 mergeProps 函數。

npm install @vue/babel-preset-jsx @vue/babel-helper-vue-jsx-merge-props

並在 babel.config.js 中添加配置:

module.exports = {
  presets: ['@vue/babel-preset-jsx'],
}

文本插值

模板代碼裏文本插值默認是用雙大括號:

<h1>{{ msg }}</h1>

在 JSX 中則需要使用單大括號:

const name = 'Vue'
const element = <h1>Hello, { name }</h1>

和模板語法中的文本插值一樣,大括號內支持任何有效的 JS 表達式,比如:2 + 2user.firstNameformatName(user)等。

條件與循環渲染

在模板代碼裏面我們通過v-for去遍歷元素,通過v-if去判斷是否渲染元素,在 JSX 中,對於v-for,可以使用for循環或者array.map來代替,對於v-if,可以使用if-else語句,三元表達式等來代替

使用if-else語句

const element = (name) ={
  if (name) {
    return <h1>Hello, { name }</h1>
  } else {
    return <h1>Hello, Stranger</h1>
  }
}

使用三元表達式

const element = icon ? <span class="icon"></span> : null;

使用數組的 map 方法

const list = ['java''c++''javascript''c#''php']
return (
  <ul>
  {list.map(item ={
   return <li>{item}</li>
  })}
  </ul>
)

屬性綁定

在模板代碼中,一般通過 v-bind:prop="value":prop="value"來給組件綁定屬性,在JSX裏面就不能繼續使用 v-bind 指令了,而是通過單大括號的形式進行綁定:

const href = 'https://xxx.com'
const element = <a href={href}>xxx</a>
const properties = {a: 1, b: 2}

此外,模板代碼中能通過<div v-bind="properties"></div>批量綁定標籤屬性。

在 JSX 中也有相應的替換方案:<div {...properties}></div>

class 綁定同樣也是使用單大括號的形式

const element = <div className={`accordion-item-title ${ disabled ? 'disabled' : '' }`}></div>
const element = <div class={
    [ 'accordion-item-title', disabled && 'disabled' ]
  }
>Item</div>
複製代碼

style 綁定需要使用雙大括號

const width = '100px'
const element = <button style={{ width, fontSize: '16px' }}></button>

事件綁定

在模板代碼中通過 v-on 指令監聽事件,在 JSX 中通過on + 事件名稱的大駝峯寫法來監聽,且綁定事件也是用大括號,比如 click 事件要寫成onClick,mouseenter 事件要寫成onMouseenter

const confirm = () ={
  // 確認提交
}
<button onClick={confirm}>確定</button>

有時候我們希望可以監聽一個組件根元素上面的原生事件,這時候會用到.native修飾符,但是在 JSX 中同樣也不能使用,不過也有替代方案,監聽原生事件的規則與普通事件是一樣的,只需要將前面的on替換爲nativeOn,如下

 render() {
    // 監聽下拉框根元素的click事件
    return <CustomSelect nativeOnClick={this.handleClick}></CustomSelect>
  }

除了上面的監聽事件的方式之外,我們還可以使用對象的方式去監聽事件

  render() {
    return (
      <ElInput
        value={this.content}
        on={{
          focus: this.handleFocus,
          input: this.handleInput
        }}
        nativeOn={{
          click: this.handleClick
        }}
      ></ElInput>
    )
  }

對於 .passive.capture 和 .once 這些事件修飾符,Vue 提供了相應的前綴可以用於 on

例如:

on: {  
    '!click': this.doThisInCapturingMode,  
    '~keyup': this.doThisOnce,  
    '~!mouseover': this.doThisOnceInCapturingMode  
}

對於所有其它的修飾符,私有前綴都不是必須的,因爲你可以在事件處理函數中使用事件方法:具體可查閱 Vue 規範文檔。

v-show 與 v-model

大多數指令並不能在 JSX 中使用,對於原生指令,只有v-show是支持的。

v-modelVue提供的一個語法糖,它本質上是由 value屬性(默認) + input事件 (默認) 組成的,所以,在JSX中,我們便可以迴歸本質,通過傳遞value屬性並監聽input事件來手動實現數據的雙向綁定:

export default {
  data() {
    return {
      name: ''
    }
  },
  methods: {
    // 監聽 onInput 事件進行賦值操作
    handleInput(e) {
      this.name = e.target.value
    }
  },
  render() {
    // 傳遞 value 屬性 並監聽 onInput事件
    return <input value={this.name} onInput={this.handleInput}></input>
  }
}

此外,在腳手架vue-cli4中,已經默認集成了對v-model的支持,可以直接使用<input v-model={this.value}>,如果項目比較老,也可以安裝插件babel-plugin-jsx-v-model來進行支持。

同樣的,在JSX中,對於.sync也需要用屬性 + 事件來實現,如下代碼所示:

export default {
  methods: {
    handleChangeVisible(value) {
      this.visible = value
    }
  },
  render() {
    return (
      <ElDialog
        title="測試.sync"
        visible={this.visible}
        on={{ 'update:visible': this.handleChangeVisible }}
      ></ElDialog>
    )
  }
}

插槽

(1)默認插槽:

使用element-uiDialog時,彈框內容就使用了默認插槽,在JSX中使用默認插槽的用法與普通插槽的用法基本是一致的,如下

 render() {
    return (
      <ElDialog title="彈框標題" visible={this.visible}>
        {/*這裏就是默認插槽*/}
        <div>這裏是彈框內容</div>
      </ElDialog>
    )
  }

自定義默認插槽:

Vue的實例this上面有一個屬性$slots, 這個上面就掛載了一個這個組件內部的所有插槽,使用this.$slots.default就可以將默認插槽加入到組件內部

export default {
  props: {
    visible: {
      type: Boolean,
      default: false
    }
  },
  render() {
    return (
      <div class="custom-dialog" vShow={this.visible}>
        {/**通過this.$slots.default定義默認插槽*/}
        {this.$slots.default}
      </div>
    )
  }
}

(2)具名插槽

有時候我們一個組件需要多個插槽,這時候就需要爲每一個插槽起一個名字,比如element-ui的彈框可以定義底部按鈕區的內容,就是用了名字爲footer的插槽

 render() {
    return (
      <ElDialog title="彈框標題" visible={this.visible}>
        <div>這裏是彈框內容</div>
        {/** 具名插槽 */}
        <template slot="footer">
          <ElButton>確定</ElButton>
          <ElButton>取消</ElButton>
        </template>
      </ElDialog>
    )
  }

自定義具名插槽: 在上節自定義默認插槽時提到了$slots,對於默認插槽使用this.$slots.default,而對於具名插槽,可以使用this.$slots.footer進行自定義

render() {
    return (
      <div class="custom-dialog" vShow={this.visible}>
        {this.$slots.default}
        {/**自定義具名插槽*/}
        <div class="custom-dialog__foolter">{this.$slots.footer}</div>
      </div>
    )
  }

(3)作用域插槽

有時讓插槽內容能夠訪問子組件中才有的數據是很有用的,這時候就需要用到作用域插槽, 在JSX中,因爲沒有v-slot指令,所以作用域插槽的使用方式就與模板代碼裏面的方式有所不同了。比如在element-ui中,我們使用el-table的時候可以自定義表格單元格的內容,這時候就需要用到作用域插槽

data() {
    return {
      data: [
        {
          name: 'xxx'
        }
      ]
    }
  },
  render() {
    return (
      {/**scopedSlots即作用域插槽,default爲默認插槽,如果是具名插槽,將default該爲對應插槽名稱即可*/}
      <ElTable data={this.data}>
        <ElTableColumn
          label="姓名"
          scopedSlots={{
            default: ({ row }) ={
              return <div style="color:red;">{row.name}</div>
            }
          }}
        ></ElTableColumn>
      </ElTable>
    )
  }

自定義作用域插槽:

使用作用域插槽不同,定義作用域插槽也與模板代碼裏面有所不同。加入我們自定義了一個列表項組件,用戶希望可以自定義列表項標題,這時候就需要將列表的數據通過作用域插槽傳出來。

render() {
    const { data } = this
    // 獲取標題作用域插槽
    const titleSlot = this.$scopedSlots.title
    return (
      <div class="item">
        {/** 如果有標題插槽,則使用標題插槽,否則使用默認標題 */}
        {titleSlot ? titleSlot(data) : <span>{data.title}</span>}
      </div>
    )
  }

使用自定義組件

只需要導入進來,不用再在 components 屬性聲明瞭,直接寫在 jsx 中:

import MyComponent from './my-component'

export default {
  render() {
    return <MyComponent>hello</MyComponent>
  },
}

在 method 裏返回 JSX

我們可以定義method,然後在method裏面返回JSX, 然後在render函數里面調用這個方法,不僅如此,JSX還可以直接賦值給變量,比如:

 methods: {
    renderFooter() {
      return (
        <div>
          <ElButton>確定</ElButton>
          <ElButton>取消</ElButton>
        </div>
      )
    }
  },
  render() {
    const buttons = this.renderFooter()
    return (
      <ElDialog visible={this.visible}>
        <div>內容</div>
        <template slot="footer">{buttons}</template>
      </ElDialog>
    )
  }

用 JSX 實現簡易聊天記錄

假設該消息聊天記錄的消息類型只有三種:文本,圖片,引用。一條消息裏面可以包括任意類型的內容,引用類型消息內部可以不斷嵌套引用其他任意類型消息。效果圖大致如下:

消息數據結構如下:

message: [
      // 每個數組的第一個參數爲消息類型:0:文本 1:圖片 2:引用。第二個參數爲具體內容
      [
        0,
        '文本'
      ],
      [
        1,
        '圖片鏈接xxx'
      ],
      [
        2,
        [
          [
            0,
            '引用文本文本文本'
          ],
          [
            1,
            '引用圖片鏈接xxx'
          ]
        ]
      ]
    ]

主要有兩個思路:

1、思路一:在 render 裏返回一段用 array.map 渲染的消息模板,對於三種消息類型,使用 if-else 進行判斷分別渲染,對於引用類型的消息,可以封裝一個方法進行渲染,方法裏面如果還有引用類型消息就繼續遞歸渲染。

微信搜索公衆號:架構師指南,回覆:架構師 領取資料 。

methods: {
    // 展示引用消息
    showQuote (msg) {
      return (
        <div class="content-quote">
          <span class="quote-title">引用:</span>
          {msg.map(item ={
            if (item[0] === 0) {
              return <p class="content-text">{item[1]}</p>
            } else if (item[0] === 1) {
              return (
                <el-image
                  class="content-img"
                  src={item[1]}
                  preview-src-list={[item[1]]}>
                </el-image>
              )
            } else {
              return this.showQuote(item[1])
            }
          })}
        </div>
      )
    }
  },
  render (h) {
    return (
      <ul class="chat-record-list">
        {this.recordList.map(item ={
          return (
            <li
              class="chat-record-item"
              key={item.timeStamp}
            >
              <div class="title">
                <span class="person-info">
                  { `${item.sendUserNick}${item.sendUserNet}) → ${item.receiverNick}${item.receiverNet}` }
                </span>
                <span class="sendtime">
                  { this.formatTime('YYYY-mm-dd HH:MM:SS', item.timeStamp) }
                </span>
              </div>
              <div class="content">
                {item.message.map(msg ={
                  if (msg[0] === 0) {
                    return <p class="content-text">{msg[1]}</p>
                  } else if (msg[0] === 1) {
                    return (
                      <el-image
                        class="content-img"
                        src={msg[1]}
                        preview-src-list={[msg[1]]}>
                      </el-image>
                    )
                  } else {
                    // 遞歸渲染引用類型消息
                    return this.showQuote(msg[1])
                  }
                })}
              </div>
            </li>
          )
        })}
      </ul>
    )
  }

2、思路二:第一種思路中封裝的 showQuote 裏面的代碼與 render 中渲染消息內容的代碼基本相似,因此其實現方式不夠優雅。其實可以將整個消息的渲染封裝成一個組件,在該組件內引入自己,然後再渲染自己。由於具體細節代碼與上述類似,這裏只給出思路代碼,具體細節請忽略

// 當前組件就是RecordMessage組件,自己引入自己
import RecordMessage from './RecordMessage.vue'

export default {
    props: {
        message: {
            type: Array,
            default: () =[]
        }
    },
    render () {
        const parseMessage = msg ={
            const type = msg[0]
            if (type === 0) {
                // 文本
            } else if (type === 2) {
                // 圖片
            } else {
                // 引用類型
                return (
                    <div>
                        <div>引用:</div>
                        {
                            msg[1].map(subMsg =(
                                // 自己遞歸渲染自己
                                <recored-message>
                                </recored-message>
                            ))
                        }
                    </div>
                )
            }
        }
        return parseMessage(this.message)
    }
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/jPO_gp296wpUkUEaPo35nA