9個vue3開發技巧,提升效率幫助你早點下班!

2022-09-14 14:01:05
vue3也釋出很長時候了,官方也將預設版本切換為vue3,而且也出現了完善的中文檔案,不知同志們是否已經使用了了呢?本渣體驗了一段時間,還是相當的絲滑,些許開發經驗奉上,望大家能早點下班

善用h(createVNode)和render 函數

我們知道在vue3中匯出了一個神奇的createVNode 函數 當前函數它能建立一個vdom,大家不要小看vdom, 我們好好利用它,就能做出意想不到的效果比如我們要實現一個彈窗元件

我們通常的思路是寫一個元件在專案中參照進來,通過v-model來控制他的顯示隱藏,但是這樣有個問題,我們複用的時候的成本需要複製貼上。我們沒有辦法來提高效率,比如封裝成npm 通過呼叫js來使用。【相關推薦:】

然而,有了 createVNode 和render 之後所有問題就迎刃而解了

// 我們先寫一個彈窗元件
        const message = {
            setup() {
                const num = ref(1)
                return {
                    num
                }
            },
            template: `<div>
                        <div>{{num}}</div>
                        <div>這是一個彈窗</div>
                      </div>`
        }
  // 初始化元件生成vdom
  const vm = createVNode(message)
  // 建立容器,也可以用已經存在的
  const container = document.createElement('div')
  //render通過patch 變成dom
  render(vm, container)
// 彈窗掛到任何你想去的地方  
document.body.appendChild(container.firstElementChild)

經過上面這一通騷操作,我們發現我們可以將他封裝為一個方法,放到任何想放的地方。

善用JSX/TSX

檔案上說了,在絕大多數情況下,Vue 推薦使用模板語法來搭建 HTML。然而在某些使用場景下,我們真的需要用到 JavaScript 完全的程式設計能力。這時渲染函數就派上用場了。

jsx和模板語法的優勢對比

jsx和模板語法都是vue 支援的的書寫範疇,然後他們確有不同的使用場景,和方式,需要我們根據當前元件的實際情況,來酌情使用

什麼是JSX

JSX 是一種 Javascript 的語法擴充套件,JSX = Javascript + XML,即在 Javascript 裡面寫 XML,因為 JSX 的這個特性,所以他即具備了 Javascript 的靈活性,同時又兼具 html 的語意化和直觀性

模板語法的優勢

  • 1、模板語法書寫起來不怎麼違和,我們就像在寫html一樣
  • 2、在vue3中由於模板的可遍歷性,它能在編譯階段做更多優化,比如靜態標記、塊block、快取事件處理程式等
  • 3、模板程式碼邏輯程式碼嚴格分開,可讀性高
  • 4、對JS功底不那麼好的人,記幾個命令就能快速開發,上手簡單
  • 5、vue官方外掛的完美支援,程式碼格式化,語法高亮等

JSX的優勢

  • 1、靈活、靈活、靈活(重要的事情說三遍)
  • 2、一個檔案能寫好多個元件
  • 3、只要JS功底好,就不用記憶那麼多命令,上來就是一通輸出
  • 4、JS和JSX混用,方法即宣告即用,對於懂行的人來說邏輯清晰

對比

由於vue對於JSX的支援,社群裡,也是爭論來爭論去,到底要分個高低,然後本渣認為,他倆本來沒有高低,您覺得哪個適合,就用哪個即可,缺點放在對的地方他就是優勢 要發揚咱們老前輩們傳下來的中庸之道,做集大成者,將兩者結合使用,就能發揮無敵功效,亂軍之中博老闆青睞。

接下來說一下本人的一點粗淺理解,我們知道元件型別,分為容器型元件和展示展示型元件 在一般情況下,容器型元件,他由於可能要對於當前展示型元件做一個標準化或者宰包裝,那麼此時容器型元件中用JSX就再好不過

舉個例子:現在有個需求,我們有兩個按鈕,現在要做一個通過後臺資料來選擇展示哪一個按鈕,我們通常的做法,是通過在一個模板中通過v-if去控制不同的元件

然而有了JSX與函數式元件之後,我們發現邏輯更清晰了,程式碼更簡潔了,品質更高了,也更裝X了

我們來看

先整兩個元件

//btn1.vue
<template>
  <div>
      這是btn1{{ num }}
      <slot></slot>
  </div>
</template>
<script>
import { ref, defineComponent } from 'vue'
export default defineComponent({
  name: 'btn1',
  setup() {
      const num = ref(1)
      return { num }
  }
})
</script>
//btn2.vue
<template>
  <div>
      這是btn2{{ num }}
      <slot></slot>
  </div>
</template>
<script>
import { ref, defineComponent } from 'vue'
export default defineComponent({
  name: 'btn2',
  setup() {
      const num = ref(2)
      return { num }
  }
})
</script>

用JSX配合函數式元件來做一個容器元件

// 容器元件
import btn1 from './btn1.vue'
import btn2 from './btn2.vue'
export const renderFn = function (props, context) {
  return props.type == 1 ? <btn1>{context.slots.default()}</btn1> : <btn2>{context.slots.default()}</btn2>
}

引入業務元件

//業務元件
<template>
 <renderFn :type="1">1111111</renderFn>
</template>
<script>
import { renderFn } from './components'
console.log(renderFn)
export default {
 components: {
   renderFn
 },
 setup() {
 },
};
</script>

善用依賴注入(Provide / Inject)

在善用依賴注入之前是,我們先來了解一些概念,幫助我們更全面的瞭解依賴注入的前世今生

IOC 和DI 是什麼

控制反轉(Inversion of Control,縮寫為IoC),是物件導向程式設計中的一種設計原則,可以用來減低計算機程式碼之間的耦合度。其中最常見的方式叫做依賴注入(Dependency Injection,簡稱DI),還有一種方式叫「依賴查詢」(Dependency Lookup)。通過控制反轉,物件在被建立的時候,由一個調控系統內所有物件的外界實體,將其所依賴的物件的參照傳遞(注入)給它。

什麼是依賴注入

依賴注入 用大白話來說:就是將範例變數傳入到一個物件中去

vue中的依賴注入

在vue中,我們套用依賴注入的概念,其實就是在父元件中宣告依賴,將他們注入到子孫元件範例中去,可以說是能夠很大程度上代替全域性狀態管理的存在

1.png

前端(vue)入門到精通課程:進入學習

我們先來看看他的基本用法

父元件中宣告provide

//parent.vue
<template>
    <child @setColor="setColor"></child>
    <button @click="count++">新增</button>
</template>

<script >
import { defineComponent, provide, ref } from "vue";
import Child from "./child.vue";
export default defineComponent({
    components: {
        Child
    },
    setup() {
        const count = ref(0);
        const color = ref('#000')
        provide('count', count)
        provide('color', color)
        function setColor(val) {
            color.value = val
        }
        return {
            count,
            setColor
        }
    }
})
</script>

子元件中注入進來

//child.vue
//使用inject 注入
<template>
    <div>這是注入的內容{{ count }}</div>
    <child1 v-bind="$attrs"></child1>
</template>

<script>
import { defineComponent, inject } from "vue";
import child1 from './child1.vue'
export default defineComponent({
    components: {
        child1
    },
    setup(props, { attrs }) {
        const count = inject('count');
        console.log(count)
        console.log(attrs)
        return {
            count
        }
    }
})
</script>

正因為依賴注入的特性,我們很大程度上代替了全域性狀態管理,相信誰都不想動不動就引入那繁瑣的vuex

接下來我們來舉個例子,現在我麼有個頁面主題色,他貫穿所有元件,並且可以在某一些元件內更改主題色,那我們常規的解決方案中,就是裝個vuex然後通過他的api下發顏色值,這時候如果想改,首先要發起dispatch到Action ,然後在Action中觸發Mutation接著在Mutation中再去改state,如此一來,你是不是發現有點殺雞用牛刀了,我就改個顏色而已!

我們來看有了依賴注入 應該怎麼處理

首先我們知道vue是單項資料流,也就是子元件不能修改父元件的內容,於是我們就應該想到使用$attrs使用它將方法透傳給祖先元件,在元件元件中修改即可。

我們來看程式碼

//子孫元件child1.vue
<template>
    <div :style="`color:${color}`" @click="setColor">這是注入的內容的顏色</div>
</template>

<script>
import { defineComponent, inject } from "vue";

export default defineComponent({
    setup(props, { emit }) {
        const color = inject('color');
        function setColor() {
            console.log(0)
            emit('setColor', 'red')
        }
        return {
            color,
            setColor
        }
    }
})
</script>

將當前子孫元件嵌入到child.vue中去,就能利用簡潔的方式來修改顏色了

善用Composition API抽離通用邏輯

眾所周知,vue3最大的新特性,當屬Composition API 也叫組合api ,用好了他,就是你在行業的競爭力,你也有了不世出的技能

我們一步步來分析

什麼是Composition API

使用 (datacomputedmethodswatch) 元件選項來組織邏輯通常都很有效。然而,當我們的元件開始變得更大時,邏輯關注點的列表也會增長。尤其對於那些一開始沒有編寫這些元件的人來說,這會導致元件難以閱讀和理解。

於是在vue3中為了解決當前痛點,避免在大型專案中出現程式碼邏輯分散,散落在當前元件的各個角落,從而變得難以維護,Composition API橫空出世

所謂Composition API 就是在元件設定物件中宣告 setup函數,我們可以將所有的邏輯封裝在setup函數中,然後在配合vue3中提供的響應式API 勾點函數、計算屬性API等,我們就能達到和常規的選項式同樣的效果,但是卻擁有更清晰的程式碼以及邏輯層面的複用

基礎使用

<template>
    <div ref="composition">測試compositionApi</div>
</template>
<script>
import { inject, ref, onMounted, computed, watch } from "vue";
export default {
    // setup起手
    setup(props, { attrs, emit, slots, expose }) {

        // 獲取頁面元素
        const composition = ref(null)
        // 依賴注入
        const count = inject('foo', '1')
        // 響應式結合
        const num = ref(0)
        //勾點函數
        onMounted(() => {
            console.log('這是個勾點')
        })
        // 計算屬性
        computed(() => num.value + 1)
        // 監聽值的變化
        watch(count, (count, prevCount) => {
            console.log('這個值變了')
        })
        return {
            num,
            count
        }

    }
}
</script>

通過以上程式碼我們可以看出,一個setup函數我們幹出了在傳統選項式中的所有事情,然而這還不是最絕的,通過這些api的組合可以實現邏輯複用,這樣我們就能封裝很多通用邏輯,實現複用,早點下班

舉個例子:大家都用過複製剪貼簿的功能,在通常情況下,利用navigator.clipboard.writeText 方法就能將複製內容寫入剪下板。然而,細心的你會發現,其實賦值剪下板他是一個通用功能,比如:你做b端業務的,管理系統中到處充滿了複製id、複製文案等功能。

於是Composition API的邏輯複用能力就派上了用場

import { watch, getCurrentScope, onScopeDispose, unref, ref } from "vue"
export const isString = (val) => typeof val === 'string'
export const noop = () => { }
export function unrefElement(elRef) {
    const plain = unref(elRef)// 拿到本來的值
    return (plain).$el ?? plain //前面的值為null、undefined,則取後面的值,否則都取前面的值
}
export function tryOnScopeDispose(fn) {
    // 如果有活躍的effect
    if (getCurrentScope()) {
        //在當前活躍的 effect 作用域上註冊一個處理回撥。該回撥會在相關的 effect 作用域結束之後被呼叫
        //能代替onUmounted
        onScopeDispose(fn)
        return true
    }
    return false
}
//帶有控制元件的setTimeout包裝器。
export function useTimeoutFn(
    cb,// 回撥
    interval,// 時間
    options = {},
) {
    const {
        immediate = true,
    } = options

    const isPending = ref(false)

    let timer

    function clear() {
        if (timer) {
            clearTimeout(timer)
            timer = null
        }
    }

    function stop() {
        isPending.value = false
        clear()
    }

    function start(...args) {
        // 清除上一次定時器
        clear()
        // 是否在pending 狀態
        isPending.value = true
        // 重新啟動定時器
        timer = setTimeout(() => {
            // 當定時器執行的時候結束pending狀態
            isPending.value = false
            // 初始化定時器的id
            timer = null
            // 執行回撥
            cb(...args)
        }, unref(interval))
    }
    if (immediate) {
        isPending.value = true

        start()
    }

    tryOnScopeDispose(stop)

    return {
        isPending,
        start,
        stop,
    }
}
//輕鬆使用EventListener。安裝時使用addEventListener註冊,解除安裝時自動移除EventListener。
export function useEventListener(...args) {
    let target
    let event
    let listener
    let options
    // 如果第一個引數是否是字串
    if (isString(args[0])) {
        //結構內容
        [event, listener, options] = args
        target = window
    }
    else {
        [target, event, listener, options] = args
    }
    let cleanup = noop
    const stopWatch = watch(
        () => unrefElement(target),// 監聽dom
        (el) => {
            cleanup() // 執行預設函數
            if (!el)
                return
            // 繫結事件el如果沒有傳入就係結為window
            el.addEventListener(event, listener, options)
            // 重寫函數方便改變的時候解除安裝
            cleanup = () => {
                el.removeEventListener(event, listener, options)
                cleanup = noop
            }
        },
        //flush: 'post' 模板參照偵聽
        { immediate: true, flush: 'post' },
    )
    // 解除安裝
    const stop = () => {
        stopWatch()
        cleanup()
    }

    tryOnScopeDispose(stop)

    return stop
}

export function useClipboard(options = {}) {
    //獲取設定
    const {
        navigator = window.navigator,
        read = false,
        source,
        copiedDuring = 1500,
    } = options
    //事件型別
    const events = ['copy', 'cut']
    // 判斷當前瀏覽器知否支援clipboard
    const isSupported = Boolean(navigator && 'clipboard' in navigator)
    // 匯出的text
    const text = ref('')
    //匯出的copied
    const copied = ref(false)
    // 使用的的定時器勾點
    const timeout = useTimeoutFn(() => copied.value = false, copiedDuring)

    function updateText() {
        //解析系統剪貼簿的文字內容返回一個Promise
        navigator.clipboard.readText().then((value) => {
            text.value = value
        })
    }

    if (isSupported && read) {
        // 繫結事件
        for (const event of events)
            useEventListener(event, updateText)
    }
    // 複製剪下板方法
    //navigator.clipboard.writeText 方法是非同步的返回一個promise
    async function copy(value = unref(source)) {
        if (isSupported && value != null) {
            await navigator.clipboard.writeText(value)
            // 響應式的值,方便外部能動態獲取
            text.value = value
            copied.value = true
            timeout.start()// copied.value = false 
        }
    }

    return {
        isSupported,
        text,
        copied,
        copy,
    }
}

這時我們就複用了複製的邏輯,如下程式碼中直接引入在模板中使用即可

<template>
    <div v-if="isSupported">
        <p>
            <code>{{ text || '空' }}</code>
        </p>
        <input v-model="input" type="text" />
        <button @click="copy(input)">
            <span v-if="!copied">複製</span>
            <span v-else>複製中!</span>
        </button>
    </div>
    <p v-else>您的瀏覽器不支援剪貼簿API</p>
</template>
<script setup>
import { ref, getCurrentScope } from 'vue'
import { useClipboard } from './copy.js'
const input = ref('')
const { text, isSupported, copied, copy } = useClipboard()
console.log(text)// 複製內容
console.log(isSupported)// 是否支援複製剪下板api 
console.log(copied)//是否複製完成延遲
console.log(copy) // 複製方法
</script>

以上程式碼參考vue版本的Composition API庫所有完整版請參考

善於使用getCurrentInstance 獲取元件範例

getCurrentInstance 支援存取內部元件範例, 通常情況下他被放在 setup中獲取元件範例,但是getCurrentInstance 只暴露給高階使用場景,典型的比如在庫中。

強烈反對在應用的程式碼中使用 getCurrentInstance。請不要把它當作在組合式 API 中獲取 this 的替代方案來使用。

那他的作用是什麼呢?

還是邏輯提取,用來代替Mixin,這是在複雜元件中,為了整個程式碼的可維護性,抽取通用邏輯這是必須要去做的事情,我們可以看element-plus 中table的複用邏輯,在邏輯提取中由於涉及獲取props、proxy、emit 以及能通過當前元件獲取父子元件的關係等,此時getCurrentInstance 的作用無可代替

如下element-plus程式碼中利用getCurrentInstance 獲取父元件parent中的資料,分別儲存到不同的變數中,我們只需要呼叫當前useMapState即可拿到資料

// 儲存資料的邏輯封裝
function useMapState<T>() {
  const instance = getCurrentInstance()
  const table = instance.parent as Table<T>
  const store = table.store
  const leftFixedLeafCount = computed(() => {
    return store.states.fixedLeafColumnsLength.value
  })
  const rightFixedLeafCount = computed(() => {
    return store.states.rightFixedColumns.value.length
  })
  const columnsCount = computed(() => {
    return store.states.columns.value.length
  })
  const leftFixedCount = computed(() => {
    return store.states.fixedColumns.value.length
  })
  const rightFixedCount = computed(() => {
    return store.states.rightFixedColumns.value.length
  })

  return {
    leftFixedLeafCount,
    rightFixedLeafCount,
    columnsCount,
    leftFixedCount,
    rightFixedCount,
    columns: store.states.columns,
  }
}

善用$attrs

$attrs 現在包含了所有傳遞給元件的 attribute,包括 classstyle

$attrs在我們開發中到底有什麼用呢?

通過他,我們可以做元件的事件以及props透傳

首先有一個標準化的元件,一般是元件庫的元件等等

//child.vue
<template>
    <div>這是一個標準化元件</div>
    <input type="text" :value="num" @input="setInput" />
</template>

<script>
import { defineComponent } from "vue";

export default defineComponent({
    props: ['num'],
    emits: ['edit'],
    setup(props, { emit }) {
        function setInput(val) {
            emit('edit', val.target.value)
        }
        return {
            setInput
        }
    }
})
</script>

接下來有一個包裝元件,他對當前的標準化元件做修飾,從而使結果變成我們符合我們的預期的元件

//parent.vue
 <template>
    <div>這一層要做一個單獨的包裝</div>
    <child v-bind="$attrs" @edit="edit"></child>
</template>

<script>
import { defineComponent } from "vue";
import child from './child.vue'
export default defineComponent({
    components: {
        child
    },
    setup(props, { emit }) {
        function edit(val) {
            // 對返回的值做一個包裝
            emit('edit', `${val}time`)
        }
        return {
            edit
        }
    }
})
</script>

我們發現當前包裝元件中使用了$attrs,通過他透傳給標準化元件,這樣一來,我們就能對比如element UI中的元件做增強以及包裝處理,並且不用改動原元件的邏輯。

優雅註冊全域性元件技巧

vue3的元件通常情況下使用vue提供的component 方法來完成全域性元件的註冊

程式碼如下:

const app = Vue.createApp({})

app.component('component-a', {
  /* ... */
})
app.component('component-b', {
  /* ... */
})
app.component('component-c', {
  /* ... */
})

app.mount('#app')

使用時

<div id="app">
  <component-a></component-a>
  <component-b></component-b>
  <component-c></component-c>
</div>

然而經過大佬的奇技淫巧的開發,我們發現可能使用註冊vue外掛的方式,也能完成元件註冊,並且是優雅的!

vue外掛註冊

外掛的格式

//plugins/index.js
export default {
  install: (app, options) => {
      // 這是外掛的內容
  }
}

外掛的使用

import { createApp } from 'vue'
import Plugin from './plugins/index.js'
const app = createApp(Root)
app.use(Plugin)
app.mount('#app')

其實外掛的本質,就是在use的方法中呼叫外掛中的install方法,那麼這樣一來,我們就能在install方法中註冊元件。

index.js中丟擲一個元件外掛

// index.js
import component from './Cmponent.vue'
const component = {
    install:function(Vue){
        Vue.component('component-name',component)
    }  //'component-name'這就是後面可以使用的元件的名字,install是預設的一個方法 component-name 是自定義的,我們可以按照具體的需求自己定義名字
}
// 匯出該元件
export default component

元件註冊

// 引入元件
import install from './index.js'; 
// 全域性掛載utils
Vue.use(install);

上述案例中,就是一個簡單的優雅的元件註冊方式,大家可以發現包括element-plus、vant 等元件都是用如此方式註冊元件。

善用</script setup>

<script setup> 是在單檔案元件 (SFC) 中使 的編譯時語法糖。相比於普通的 <script> 語法,它具有更多優勢:

  • 更少的樣板內容,更簡潔的程式碼。
  • 能夠使用純 Typescript 宣告 props 和丟擲事件。
  • 更好的執行時效能 (其模板會被編譯成與其同一作用域的渲染函數,沒有任何的中間代理)。
  • 更好的 IDE 型別推斷效能 (減少語言伺服器從程式碼中抽離型別的工作)。

它能代替大多數的setup函數所表達的內容,具體使用方法,大家請看請移步檔案

但是由於setup函數它能返回渲染函數的特性,在當前語法糖中卻無法展示,於是遍尋資料,找到了一個折中的辦法

<script setup>
import { ref,h } from 'vue'

const msg = ref('Hello World!')
const dynode = () => h('div',msg.value);

</script>

<template>
    <dynode />
  <input v-model="msg">
</template>

如此一來,我們就能在語法糖中返回渲染函數了

v-model的最新用法

我們知道在vue2中想要模擬v-model,必須要子元件要接受一個value props 吐出來一個 叫input的emit

然而在vue3中他升級了

父元件中使用v-model

 <template>
    <child v-model:title="pageTitle"></child>
</template>

<script>
import { defineComponent, ref } from "vue";
import child from './child.vue'
export default defineComponent({
    components: {
        child
    },
    setup(props, { emit }) {
        const pageTitle = ref('這是v-model')
        return {
            pageTitle
        }
    }
})
</script>

子元件中使用 title的props 以及規定吐出update:title的emit

<template>
    <div>{{ title }}</div>
    <input type="text" @input="setInput" />
</template>

<script>
import { defineComponent } from "vue";

export default defineComponent({
    props: ['title'],
    emits: ['update:title'],
    setup(props, { emit }) {
        function setInput(val) {
            emit('update:title', val.target.value)
        }
        return {
            setInput
        }
    }
})
</script>

有了以上語法糖,我們在封裝元件的時候,就可以隨心所欲了,比如我自己封裝可以控制顯示隱藏的元件我們就能使用v-model:visible單獨控制元件的顯示隱藏。使用正常的v-model 控制元件內部的其他邏輯,從而擁有使用更簡潔的邏輯,表達相同的功能

最後

目前開發中總結的經驗就分享到這裡了,錯誤之處,請大佬指出!

然後對vue原始碼有興趣的大佬,可以看下這個文章 寫給小白(自己)的vue3原始碼導讀

也可以直接看本渣的原始碼解析github vue-next-analysis

其中包含了vue原始碼執行思維導圖,原始碼中的程式碼註釋,整個原始碼的結構,各個功能的單獨拆解等。錯誤之處請大佬指出!

(學習視訊分享:、)