聊聊 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 + 2
,user.firstName
,formatName(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
}
對於所有其它的修飾符,私有前綴都不是必須的,因爲你可以在事件處理函數中使用事件方法:
v-show 與 v-model
大多數指令並不能在 JSX 中使用,對於原生指令,只有v-show
是支持的。
而v-model
是Vue
提供的一個語法糖,它本質上是由 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-ui
的Dialog
時,彈框內容就使用了默認插槽,在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