Vue中jsx不徹底應用指南

前言:文章不介紹任務背景知識,沒有原理說明,偏向於實踐的總結和經驗分享。

文章全部的代碼是基於Vue CLI 3.x版本,不會涉及到一步步經過Webpack來配置JSX所須要的知識點。html

在使用Vue開發項目時絕大多數狀況下都是使用模板來寫HTML,可是有些時候頁面複雜又存在各類條件判斷來顯示/隱藏和拼湊頁面內容,或者頁面中不少部分存在部分DOM結構同樣的時候就略顯捉襟見肘,會寫大量重複的代碼,會出現單個.vue文件過長的狀況,這個時候咱們就須要更多的代碼控制,這時候可使用渲染函數前端

渲染函數想必平時幾乎沒有人去寫,由於寫起來很痛苦(本人也沒有寫過)。更多的是在Vue中使用JSX語法。寫法上和在React中差很少,可是功能上仍是沒有React中那麼完善。vue

在寫JSX的過程當中不得考慮一個樣式的問題,雖然能夠直接在.vue文件中不寫<tempate>部分,只寫<script><style>部分,而不用擔憂樣式做用域問題。可是更多的時候仍是推薦直接使用.js的方式來寫組件,這個時候就涉及到樣式做用域的問題了。node

在React的生態中,有不少CSS-IN-JS的解決方案,好比styled-jsxemotionstyled-components等,目前最活躍和用戶量最多的是styled-components,目前已經擁有良好的生態圈子。若是須要在樣式中做一些像Sass/Less中的顏色計算,可使用polished來實現,固然不止這麼簡單的功能。可是在Vue中可以使用的方案就太少了,由於Vue使用模板來寫HTML自己是開箱即用的樣式scoped,在使用JSX寫組件的時候就面臨着樣式問題,一種方案是在組件包裹<div>中取一個特殊的名字,而後樣式都嵌套寫在這個class下面,可是不免會遇到命名衝突的狀況,並且每次還得變着花樣取名稱。此外,就是引入CSS-IN-JS在Vue對應的實現,但目前來看Styled-components官方提供了一個Vue版本的叫vue-styled-components和emotion的vue-emotion,可是用的人實在太少。像styled-components進行了重大更新和變化,可是Vue版本的仍是最初的版本,並且有時候還出現樣式不生效的狀況。react

接下來進入正題,從簡單語法到經驗分享(大牛請繞行)git

基本用法

首先須要約定一下,使用JSX組件命名採用首字母大寫的駝峯命名方式,樣式能夠少的能夠直接基於vue-styled-components寫在同一個文件中,複雜的建議放在單獨的_Styles.js_文件中,固然也能夠不採用CSS-IN-JS的方式,使用Less/Sass來寫,而後在文件中import進來。github

下面是一個通用的骨架:express

import styled from 'vue-styled-components'

const Container = styled.div`
    heigth: 100%;
`

const Dashboard = {
  name: 'Dashboard',
  
  render() {
    return (
        <Container>內容</Container>
    )
  }
}

export default Dashboard

插值

在JSX中使用單個括號來綁定文本插值編程

<span>Message: {this.messsage}</span>
<!-- 相似於v-html -->
<div domPropsInnerHTML={this.dangerHtml}/>
<!-- v-model -->
<el-input v-model={this.vm.name} />

在jsx中不須要把v-model分紅事件綁定和賦值二部分分開來寫,由於有相應的babel插件來專門處理。segmentfault

樣式

在JSX中能夠直接使用class="xx"來指定樣式類,內聯樣式能夠直接寫成style="xxx"

<div class="btn btn-default" style="font-size: 12px;">Button</div>

<!-- 動態指定 -->
<div class={`btn btn-${this.isDefault ? 'default' : ''}`}></div>
<div class={{'btn-default': this.isDefault, 'btn-primary': this.isPrimary}}></div>
<div style={{color: 'red', fontSize: '14px'}}></div>

遍歷

在JSX中沒有v-forv-if等指令的存在,這些所有須要採用Js的方式來實現

{/* 相似於v-if */}
{this.withTitle && <Title />}

{/* 相似於v-if 加 v-else */}
{this.isSubTitle ? <SubTitle /> : <Title />}

{/* 相似於v-for */}
{this.options.map(option => {
  <div>{option.title}</div>
})}

事件綁定

事件綁定須要在事件名稱前端加上on前綴,原生事件添加nativeOn

<!-- 對應@click -->
<el-buton onClick={this.handleClick}>Click me</el-buton>
<!-- 對應@click.native -->
<el-button nativeOnClick={this.handleClick}>Native click</el-button>
<!-- 傳遞參數 -->
<el-button onClick={e => this.handleClick(this.id)}>Click and pass data</el-button>

注意:若是須要給事件處理函數傳參數,須要使用箭頭函數來實現。若是不使用箭頭函數那麼接收的將會是事件的對象event屬性。

高級部分

在Vue中基於jsx也能夠把組件拆分紅一個個小的函數式組件,可是有一個限制是必需有一個外層的包裹元素,不能直接寫相似:

const Demo = () => (
    <li>One</li>
  <li>Two</li>
)

必需寫成:

const Demo = () => (
    <div>
      <li>One</li>
    <li>Two</li>
  </div>
)

而在React中可使用空標籤<></><react.Fragment></react.Fragment>來實現包裹元素,這裏的空標籤其實只是react.Fragment的一個語法糖。同時在React 16中直接支持返回數組的形式:

const Demo = () => [
  <li>One</li>
  <li>Two</li>
]

那麼在Vue中就只能經過遍從來實現相似的功能,大致思路就是把數據先定義好數據而後直接一個map生成,固然若是說元素的標籤是不一樣類型的那就須要額外添加標識來判斷了。

{
  data() {
    return {
      options: ['one', 'two']
    }
  },
    
    render() {
    const LiItem = () => this.options.map(option => <li>{option}</li>)
                                          
    return (
      <div>
            <ul>
              <LiItem />
          </ul>
         </div>
    )
  }
}

事件修飾符

在基礎部分簡單介紹了事件的綁定用法,這裏主要是補充一下事件修飾符的寫法。

在模板語法中Vue提供了不少事件修飾符來快速處理事件的冒泡、捕獲、事件觸發頻率、按鍵識別等。能夠直接查看官方文檔的事件&按鍵修飾符部分,這裏把相關內容原樣搬運過來:

修飾符 前綴
.passive &
.capture !
.once ~
.capture.once.once.capture ~!

使用方式以下:

<el-button {...{
    '!click': this.doThisInCapturingMode,
  '!keyup': this.doThisOnce,
  '~!mouseover': this.doThisOnceInCapturingMode
}}>Click Me!</el-button>

下面給出的事件修飾符是須要在事件處理函數中寫出對應的等價操做

修飾符 處理函數中的等價操做
.stop event.stopPropagation()
.prevent event.preventDefault()
.self if (event.target !== event.currentTarget) return
按鍵: .enter, .13 if (event.keyCode !== 13) return (對於別的按鍵修飾符來講,可將 13 改成另外一個按鍵碼)
修飾鍵: .ctrl, .alt, .shift, .meta if (!event.ctrlKey) return (將 ctrlKey 分別修改成 altKeyshiftKey 或者 metaKey)

下面是在事件處理函數中使用修飾符的例子:

methods: {
  keyup(e) {
    // 對應`.self`
    if (e.target !== e.currentTarget) return
    
    // 對應 `.enter`, `.13`
    if (!e.shiftKey || e.keyCode !== 13) return
    
    // 對應 `.stop`
    e.stopPropagation()
    
    // 對應 `.prevent`
    e.preventDefault()
    
    // ...
  }
}

ref和refInFor

在Vue中ref被用來給元素或子組件註冊引用信息。引用信息將會註冊在父組件的 $refs 對象上。若是在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;若是用在子組件上,引用就指向組件。

注意

  • 由於 ref 自己是做爲渲染結果被建立的,在初始渲染的時候你不能訪問它們 - 它們還不存在
  • $refs不是響應式的,所以你不該該試圖用它在模板中作數據綁定。

v-for 用於元素或組件的時候,引用信息將是包含 DOM 節點或組件實例的數組。

假如在jsx中想要引用遍歷元素或組件的時候,例如:

const LiArray = () => this.options.map(option => (
  <li ref="li" key={option}>{option}</li>
))

會發現從this.$refs.li中獲取的並非指望的數組值,這個時候就須要使用refInFor屬性,並置爲true來達到在模板中v-for中使用ref的效果:

const LiArray = () => this.options.map(option => (
  <li ref="li" refInFor={true} key={option}>{option}</li>
))

插槽(v-slot)

在jsx中可使用this.$slots來訪問靜態插槽的內容。

注意:在Vue 2.6.x版本後廢棄了 slotslot-scope,在模板中統一使用新的統一語法 v-slot指令。 v-slot只能用於Vue組件和 template標籤。
<div class="page-header__title">
    {this.$slots.title ? this.$slots.title : this.title}
</div>

等價於模板的

<div class="page-header__title">
  <slot name="title">{{ title }}</slot>
</div>

在Vue官方文檔中提到:父級模板裏的全部內容都是在父級做用域中編譯的;子模板裏的全部內容都是在子做用域中編譯的。所以像下面的示例是沒法正常工做的

<current-user>
    {{ user.firstName }}
</current-user>

<current-user>組件中能夠訪問到user屬性,可是提供的內容倒是在父組件渲染的。若是想要達到指望的效果,這個時候就須要使用做用域插槽了。下面是改寫後的代碼,更多知識點能夠直接查看官方文檔的做用域插槽

<!-- current-user組件定義部分 -->
<span>
    <slot v-bind:user="user">
      {{ user.lastName }}
  </slot>
</span>

<!-- current-user 使用 -->
<current-user>
    <template v-slot:default="slotProps">
      {{ slotProps.user.firstName }}
  </template>
</current-user>

上面的示例其實就是官方的示例,這裏須要說明的是,其實在Vue中所謂的做用域插槽功能相似於React中的Render Props的概念,只不過在React中咱們更多時候不只提供了屬性,還提供了操做方法。可是在Vue中更多的是提供數據供父做用域渲染展現,固然咱們也能夠把方法提供出去,例如:

<template>
    <div>
    <slot v-bind:injectedProps="slotProps">
      {{ user.lastName }}
      </slot>
  </div>
</template>

<script>
  export default {
    data() {
      return {
        user: {
          firstName: 'snow',
          lastName: 'wolf'
        }
      }
    },
    
    computed: {
      slotProps() {
        return {
          user: this.user,
          logFullName: this.logFullName
        }
      }
    },
    
    methods: {
      logFullName() {
        console.log(`${this.firstName} ${this.lastName}`)
      }
    }
  }
</script>

在父組件中使用:

<current-user>
    <template v-slot:default="{ injectedProps }">
      <div>{{ injectedProps.user.firstName }}</div>
        <el-button @click="injectedProps.logFullName">Log Full Name</el-button>
  </template>
</current-user>

在上面的代碼中咱們實際上使用解構的方式來取得injectedProps,基於解構的特性還能夠重命名屬性名,在propundefined的時候指定初始值。

<current-user v-slot="{ user = { firstName: 'Guest' } }">
  {{ user.firstName }}
</current-user>

若是組件只有一個默認的插槽還可使用縮寫語法,將v-slot:default="slotProps"寫成v-slot="slotProps",命名插槽寫成v-slot:user="slotProps",若是想要動態插槽名還能夠寫成v-slot:[dynamicSlotName],此外具名插槽一樣也有縮寫語法,例如 v-slot:header能夠被重寫爲#header

上面介紹了不少插槽相關的知識點足已說明其在開發過程當中的重要性。說了不少在模板中如何定義和使用做用域插槽,如今進入正題如何在jsx中一樣使用呢?

// current-user components
{
  data() {
    return {
      user: {
        firstName: 'snow',
        lastName: 'wolf'
      }
    }
  },
    
  computed: {
    slotProps() {
      return {
        user: this.user,
        logFullName: this.logFullName
      }
    }
  },
    
  methods: {
    logFullName() {
      console.log(`${this.firstName} ${this.lastName}`)
    }
  },
    
  render() {
    return (
        <div>
        {this.$scopedSlots.subTitle({
          injectedProps: this.slotProps
        })}
      </div>
    )
  }
}

而後在父組件中以jsx使用:

<current-user {...{
  scopedSlots: {
    subTitle: ({ injectedProps }) => (
        <div>
          <h3>injectedProps.user</h3>
        <el-button onClick={injectedProps.logFullName}>Log Full Name</el-button>
      </div>
    )
  }
}}></current-user>

指令

這裏須要注意的是在jsx中全部Vue內置的指令除了v-show之外都不支持,須要使用一些等價方式來實現,好比v-if使用三目運算表達式、v-for使用array.map()等。

對於自定義的指令可使用v-name={value}的語法來寫,須要注意的是指令的參數、修飾符此種方式並不支持。以官方文檔指令部分給出的示例v-focus使用爲例,介紹二種解決辦法:

1 直接使用對象傳遞全部指令屬性

<input type="text" v-focus={{ value: true }} />

2 使用原始的vnode指令數據格式

{
  directives:{
    focus: {
      inserted: function(el) {
        el.focus()
      }
    }
  },
    
  render() {
    const directives = [
      { name: 'focus', value: true }
    ]
      
    return (
      <div>
          <input type="text" {...{ directives }} />
      </div>
    )
  }
}

過濾器

過濾器其實在開發過程當中用得卻是很少,由於更多時候能夠經過計算屬性來對數據作一些轉換和篩選。這裏只是簡單說起一下並無什麼能夠深究的知識點。

在模板中的用法以下:

<!-- 在雙花括號中 -->
{{ message | capitalize }}

<!-- 在 `v-bind` 中 -->
<div v-bind:id="rawId | formatId"></div>

在jsx中使用方法爲:

<div>{this.$options.filters('formatDate')('2019-07-01')}</div>
注意:因爲Vue全局的過濾器只用於模板中,若是須要用於組件的方法中,能夠把過濾器方法單獨抽離出一個公共Js文件,而後引入組件中,而後用於方法中。

一些簡單經驗分享

並非說咱們在開發Vue項目的時候必定要使用jsx的方式來寫,可是多掌握一種方式來靈活變通,提升工做效率,擴展思路未嘗不值得一試。並且,在有些場景下釋放js的徹底編程能力會讓你更加可以駕輕就熟。其實在使用模板方式的時候咱們並無徹底採用組件的思惟方式來作,或者說是作得不完全,不純粹,拆分的粒度不夠。更多 的時候並無考慮到組件怎麼切分和抽象,多人協做的時候如何處理依賴並明確本身的功能點。

關於DOM屬性、HTML屬性和組件屬性

在React中全部數據均掛載在props下,Vue則否則,僅屬性就有三種:組件屬性props,普通html屬性attrs和DOM屬性domProps。在Angular的文檔中關於插值綁定部分是重點說明了DOM屬性HTML屬性的區別,在大多數狀況下二者都有對應的同名屬性,也就是1:1映射關係,可是也有例外的狀況,好比HTML中colspan,DOM中的textContent。HTML屬性的值指定了初始值,而且不能改變,而DOM屬性的值表示當前值,是能夠改變的。

而後在Vue的模板語法中是不區分DOM屬性和HTML屬性的,例如:

<template>
    <div>
    <div>輸入的值:{{ title }}</div>
    <input type="text" value="我是DOM屬性值" v-model="title" @input="logTitle" />
  </div>
</template>

<script>
export default {
  data() {
    return {
      title: ''
    }
  },
  
  methods: {
    logTitle(e) {
      // 輸出DOM屬性
      console.log(e.target.value)
      // 輸出HTML屬性
      console.log(e.target.getAttribue('value'))
    }
  }
}
</script>

運行示例能夠看到input的初始值被設置爲了「我是DOM屬性值",當咱們在輸入框中添加或者刪除文字時,HTML屬性始終沒有變化,而綁定的DOM值一值在變更。而後再看一下在jsx中的實現:

<div>輸入值:{ this.title }</div>
<input type="text" value="我是DOM屬性" v-model={this.title} onInput={this.logTitle} />

一樣運行後會發如今jsx寫法中並無直接將HTML屬性初始化爲DOM屬性值,即輸入框中當前值爲空字符串,這符合預期的行爲。

此外在模板語法中是沒法區分HTML屬性和DOM屬性命名同樣的場景,可是在jsx中能夠很好的區分:

<Demo title="我是組件屬性" domPropsTitle="我是DOM屬性" />

結果會就是在HMTL中顯示title="我是DOM屬性,而"我是組件屬性」傳遞給了組件。

在React中CSS的樣式寫義在jsx中的語法是以className="xx"的形式,而在Vue的jsx中能夠直接寫成class="xx"。實際上因爲class是Js的保留字,所以在DOM中其屬性名爲className而在HTML屬性中爲class,咱們能夠在Vue中這樣寫,通過Babel轉譯後獲得正確的樣式類名:

<div domPropsClassName="mt__xs"></div>
注意:若是同時寫了 class="xx" domPropsClassName="yy"那麼後者的優先級較高,和位置無關。因此儘可能仍是採用 class的寫法。

有使用過Bootstrap經驗的可能會注意到它裏面包含了不少ARIA屬性,這些屬性並不屬於DOM,在jsx中能夠經過attrsXX或者直接aria-xx的方式來添加:

<label aria-label="title"></label>
<label attrsAria-label="title"></label>

可是上面的換成domPropsAria-label就沒有任何效果。

注意:在jsx中全部DOM屬性( Property)語法爲 domPropsXx, HTML特性( Attribute)語法爲 attrsXx。更多的時候建議仍是少使用,或者說合理使用。

在jsx中還可使用混用的寫法,例如在組件中寫了<Demo title="demo" />,還能夠定義一個屬性對象,而後使用{...props}的方式寫在一塊兒,這個時候就會出現屬性合併的問題,一樣的事件多個地方聲明事件處理函數,都會觸發。

最後須要說起一點的是,在Vue中當給一個組件傳了不少props,可是有的並非組件聲明的,也有多是一些通用的HTML或者DOM屬性,可是在最終編譯後的HTML中會直接顯示這些props,若是不但願這些屬性顯示在最終的HTML中,能夠在組件中設inheritAttrs: false。雖然不顯示了,可是咱們依然能夠經過vm.$attrs獲取全部(除classstyle)綁定的屬性,包括不在props中定義的。

關於事件

前面已經把事件相關的知識點都介紹了,這裏主要是說起一下關於jsx事件綁定語法onXx和組件屬性(主要是函數prop)以on開頭的狀況如何處理。

雖然在寫組件的時候能夠避開命名以on開頭,可是在使用第三庫的時候,若是遇到了該如何處理呢?好比Element組件Upload不少鉤子都是以on開頭。 下面提供兩種解決辦法:

1.使用展開

<el-upload {...{
  props: {
    onPreview: this.handlePreview
  }
}} />
  1. 使用propsXx
<el-upload propsOnPreview={this.handlePreview} />

推薦使用第二種方式,寫起來要簡單些。

複雜邏輯條件判斷

在模板語法中可使用v-ifv-else-ifv-else來作條件判斷。在jsx中能夠經過?:三元運算符(Ternary operator)運算符來作if-else判斷:

const Demo = () => isTrue ? <p>True!</p> : null

而後能夠利用&&運算符的特性簡寫爲:

const Demo = () => isTrue && <p>True!</p>

對於複雜的條件判斷,例如:

const Demo = () => (
    <div>
      {flag && flag2 && !flag3
        ? flag4
         ? <p>Blash</p>
      : flag5
         ? <p>Meh</p>
      : <p>hErp</p>
        : <p>Derp</p>
    }
  </div>
)

能夠採用兩種方式來下降判斷識別的複雜度

  • 最好的辦法:將判斷邏輯轉移到子組件
  • 可選的hacky方法:使用IIFE(當即執行表達式)
  • 使用第三方庫解決:jsx-control-statements

下面是使用IIFE經過內部使用if-else返回值來優化上述問題:

const Demo = () => (
    <div>
    {
      (() => {
        if (flag && flag2 &&!flag3) {
          if (flag4) {
            return <p>Blah</p>
          } else if (flag5) {
            return <p>Meh</p>
          } else {
            return <p>Herp</p>
          }
        } else {
          return <p>Derp</p>
        }
      })()
    }
  </div>
)

還可使用do表達式,可是須要插件@babel/plugin-proposal-do-expressions的轉譯來支持,

const Demo = () => (
    <div>
    {
      do {
        if (flag1 && flag2 && !flag3) {
          if (flag4) {
            <p>Blah</p>
          } else if (flag5) {
            <p>Meh</p>
          } else {
            <p>Herp</p>
          }
        } else {
          <p>Derp</p>
        }
      }
    }
  </div>
)

再就是一種比較簡單的可選辦法,以下:

const Demo = () => {
  const basicCondition = flag && flag1 && !flag3;
  if (!basicCondition) return <p>Derp</p>
  if (flag4) return <p>Blah</p>
  if (flag5) return <p>Meh</p>
  return <p>Herp</p>
}

最後一種使用jsx插件的就不詳述和舉例了,有興趣的能夠直接查看文檔。

組件的傳值

在單個jsx文件中能夠寫不少函數式組件來切分更小的粒度,例如以前的文章Vue後臺管理系統開發平常總結__組件PageHeader,組件的形態有兩種,一種是普通標題,另外一種是帶有選項卡的標題,那麼在寫的時候就能夠這樣寫:

render() {
  // partial html
  const TabHeader = (
      <div class="page-header page-header--tab"></div>
  )
  
  // function partial
  const Header = () => (
      <div class="page-header"></div>
  )
  
  <div class="page-header">
      {this.withTab ? TabHeader : <Header/>}
  </div>
}

注意在拆分的時候,若是不須要作任何判斷能夠純粹是HTML片斷賦值給變量,若是須要條件判斷就使用函數式組件的方式來寫。須要注意的是因爲render函數會屢次被調用,寫的時候注意一下對性能的影響,目前能力有限這方面就不做展開了。

既然使用函數式組件,那麼一樣能夠在函數中傳遞參數了,參數是一個對象中,包含了如下屬性

children        # VNode數組,相似於React的children
data          # 綁定的屬性
    attrs       # Attribute
    domProps    # DOM property
    on                # 事件
injections  # 注入的對象
listeners:  # 綁定的事件類型
    click         # 點擊事件
    ...
parent            # 父組件
props                # 屬性
scopedSlots # 對象,做用域插槽,使用中發現做用域插槽也掛在這個下面
slots                # 函數,插槽

雖然能夠在函數式組件中傳參數、事件、slot可是我的以爲不建議這樣作,反而搞複雜了。

render() {
  const Demo = props => {
    return (
        <div>
          <h3>Jsx中的內部組件 { props.data.title }</h3>
        { props.children }
        <br />
        { props.scopedSlots.bar() }
      </div>
    )
  }
  
  return (
      <div>
        <Demo title="test" attrsA="a" domPropsB="b" onClick={this.demo}>
          <h3>我是Children</h3>
        <template slot="bar">
            <p>我是Slot內容</p>
        </template>
      </Demo>
    </div>
  )
}

上面的示例最終生成的HTML中會將<template>的內容轉換爲#document-fragment

總結

接觸Vue時間比較早,可是真正的Vue項目開發經驗一年不到,平時比較懶,不怎麼去深刻學習和研究,因此文章在敘述上沒有什麼條理性,有些知識點可能並沒表達清楚,不少東西仍是得多實踐去檢驗。若是有問題歡迎留言共同探討。

其實早一點實踐jsx的寫法,對於後面的Vue 3.0出現後能夠更快的融入其中,就像React對函數式組件中新增了鉤子(Hooks)函數,之後Vue也是主推函數式組件,之後模板語法方式的佔比會稍有降低。

文章並無包含全部Vue中jsx寫法的所有知識點,→_→因此叫不徹底指南^_^"

最後,感謝各位的支持!!!…(⊙_⊙;)… ○圭~○

相關文章
相關標籤/搜索