令人眼前一亮的 Vue 實(shí)戰(zhàn)技巧
前言
本文主要介紹日常項(xiàng)目開發(fā)過(guò)程中的一些技巧,不僅可以幫助提升工作效率,還能提高應(yīng)用的性能。以下是我總結(jié)一些平時(shí)工作中的技巧。
minxin 讓組件復(fù)用靈活化
Vue提供了minxin這種在組件內(nèi)插入組件屬性的方法,個(gè)人建議這貨能少用就少用,但是有個(gè)場(chǎng)景則非常建議使用minxin:當(dāng)某段代碼重復(fù)出現(xiàn)在多個(gè)組件中,并且這個(gè)重復(fù)的代碼塊很大的時(shí)候,將其作為一個(gè)minxin常常能給后期的維護(hù)帶來(lái)很大的方便。
比如我們?cè)陧?xiàng)目中封裝一個(gè)列表功能,有下拉刷新,加載自動(dòng)請(qǐng)求數(shù)據(jù),上拉加載下一頁(yè)數(shù)據(jù)等等,代碼如下:
- export default {
- data() {
- return {
- page: 1,
- limit: 10,
- busy: false, // 請(qǐng)求攔截,防止多次加載
- finish: false, // 是否請(qǐng)求完成,用于頁(yè)面展示效果
- pageList: [], // 頁(yè)面數(shù)據(jù)
- reqParams: {}, // 頁(yè)面請(qǐng)求參數(shù),可被改變的
- defaultParams: {}, // 頁(yè)面請(qǐng)求參數(shù),下拉刷新不會(huì)被重置的改變
- routeName: '', // 特殊情況,頁(yè)面需要復(fù)用別人的list的時(shí)候
- autoReq: true, // onload是否自己去請(qǐng)求
- lodingText: '', // 請(qǐng)求中底部顯示的文案
- noDataText: '暫無(wú)數(shù)據(jù)', // 自定義無(wú)數(shù)據(jù)文案
- lastText: '- 我是有底線的 -',
- noData: false, // 頁(yè)面無(wú)數(shù)據(jù)
- reqName: ''
- }
- },
- created() {
- this.autoReq && this.initPage(false, true)
- },
- onPullDownRefresh() {
- this.pullDownRefreshFn()
- },
- onReachBottom() {
- this.reachBottomFn()
- },
- methods: {
- // 重置初始化數(shù)據(jù)
- initPage(saveParams = true, refresh = false) {
- // 初始化所有變量
- this.page = 1
- this.busy = false
- this.finish = false
- this.noData = false
- this.lodingText = '數(shù)據(jù)加載中'
- if (saveParams) {
- const { page, limit } = this.reqParams
- page ? this.page = page : ''
- limit ? this.limit = limit : ''
- } else {
- this.reqParams = {}
- }
- this.getCommonList(refresh)
- },
- // 下拉刷新函數(shù)
- pullDownRefreshFn() {
- this.initData()
- this.initPage(false, true)
- },
- // 上啦加載函數(shù)
- reachBottomFn() {
- this.getCommonList()
- },
- // 重置數(shù)據(jù),方便調(diào)用(一般在外面自定義清空一些數(shù)據(jù))
- initData() { // 重置data里面的變量,方便外面引用這個(gè)mixin的時(shí)候,下拉刷新重置變量
- },
- // 列表獲取數(shù)據(jù)接口
- async getCommonList(refresh) {
- if (!this.reqName) return
- if (this.busy) return
- this.busy = true
- this.finish = false
- const httpFn = this.$http || getApp().globalData.$http// 兼容nvue
- try {
- const query = {
- ...this.defaultParams,
- ...this.reqParams,
- page: this.page,
- limit: this.limit
- }
- const { data } = await httpFn(this.reqName, query)
- if (this.page === 1) this.pageList = []
- /**
- * [Node.JS中用concat和push連接兩個(gè)或多個(gè)數(shù)組的性能比較](http://ourjs.com/detail/5cb3fe1c44b4031138b4a1e2)
- * 那么兩者在node.js的性能如何? 我們做了一組測(cè)試數(shù)據(jù),兩種分別測(cè)試100萬(wàn)次。
- * push比concat方法快3倍左右。因?yàn)閜ush只是在原數(shù)組的基礎(chǔ)上進(jìn)行修改,所以會(huì)快一點(diǎn)。
- * push返回的是數(shù)組的長(zhǎng)度,所以沒(méi)重新定義變量再判斷了
- * [Array.prototype.push.apply(arr1, arr2)無(wú)法自動(dòng)觸發(fā)DOM更新](https://www.imooc.com/wenda/detail/494323)
- * 因?yàn)?nbsp;this.pageList.push !== Array.prototype.push,,this.pageList.push指向的是vue重寫過(guò)的方法
- */
- this.finish = true
- const resLen = data.list ? data.list.length : 0
- if (resLen === 0) {
- this.resSuccess(data, refresh)
- return
- }
- const listLen = this.pageList.push.apply(this.pageList, data.list)
- if (listLen < data.count && this.limit <= resLen) { // 說(shuō)明還有數(shù)據(jù)
- this.busy = false
- this.page = Math.ceil(listLen / this.limit) + 1
- }
- this.resSuccess(data, refresh)
- } catch (e) {
- // 防止接口報(bào)錯(cuò)鎖死
- this.busy = false
- this.finish = true
- }
- },
- resSuccess(data, refresh) {
- if (this.finish && this.busy) {
- if (this.pageList.length > 0) {
- this.$nextTick(() => {
- setTimeout(() => {
- thisthis.lodingText = this.lastText
- }, 100)
- })
- } else {
- thisthis.lodingText = this.noDataText
- this.noData = true
- }
- }
- refresh && uni.stopPullDownRefresh()
- this.finishInit(data)
- },
- // 請(qǐng)求完成做點(diǎn)什么(方便外面導(dǎo)入的文件自己引用)
- finishInit(data) { // 請(qǐng)求完成做點(diǎn)什么
- // console.log('列表請(qǐng)求完成');
- }
- }
- }
很多人看到這樣場(chǎng)景,應(yīng)該會(huì)好奇為什么不封裝成一個(gè)組件?由于很多列表樣式不盡相同,所以封裝成一個(gè)組件可擴(kuò)展性不高。但我們可以通過(guò)minxin來(lái)簡(jiǎn)化代碼:
- <template>
- <view class="c-recommend-goods">
- <!-- 列表樣式 -->
- <view class="" v-for="item in pageList" :key="item.id">{{item}}</view>
- <!-- 空狀態(tài)&& 加載中等小提示 -->
- <c-no-data v-if="lodingText" :show-img="noData" :text="lodingText"></c-no-data>
- </view>
- </template>
- <script>
- import listMixins from '@/common/mixins/list.js'
- export default {
- mixins: [listMixins],
- data() {
- return {
- autoReq: false, // 進(jìn)入頁(yè)面自動(dòng)請(qǐng)求數(shù)據(jù)
- reqParams: {}, // 請(qǐng)求參數(shù)
- reqName: 'userCompanyList' // 請(qǐng)求地址
- }
- }
- }
- </script>
- <style></style>
我們只要定義請(qǐng)求參數(shù)和請(qǐng)求的地址,還有列表的樣式,就能實(shí)現(xiàn)一個(gè)不錯(cuò)的列表功能。
拯救繁亂的template--render函數(shù)
有時(shí)候項(xiàng)目中template里存在一值多判斷,如果按照下方代碼書寫業(yè)務(wù)邏輯,代碼冗余且雜亂。
- <template>
- <div>
- <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>
- <h4 v-else-if="level === 4">
- <slot></slot>
- </h4>
- <h5 v-else-if="level === 5">
- <slot></slot>
- </h5>
- <h6 v-else-if="level === 6">
- <slot></slot>
- </h6>
- </div>
- </template>
- <script>
- export default {
- data() {
- return {}
- },
- props: {
- level: {
- type: Number,
- required: true,
- },
- },
- }
- </script>
現(xiàn)在使用 render 函數(shù)重寫上面的例子:
- <script>
- export default {
- props: {
- level: {
- require: true,
- type: Number,
- }
- },
- render(createElement) {
- return createElement('h' + this.level, this.$slots.default);
- }
- };
- </script>
一勞永逸的組件注冊(cè)
組件使用前,需要引入后再注冊(cè):
- import BaseButton from './baseButton'
- import BaseIcon from './baseIcon'
- import BaseInput from './baseInput'
- export default {
- components: {
- BaseButton,
- BaseIcon,
- BaseInput
- }
- }
現(xiàn)在 BaseButton、 BaseIcon和BaseInput都可以在模板中使用了:
- <BaseInput
- v-model="searchText"
- @keydown.enter="search"
- />
- <BaseButton @click="search">
- <BaseIcon name="search"/>
- </BaseButton>
但如果組件多了后,每次都要先導(dǎo)入每個(gè)你想使用的組件,然后再注冊(cè)組件,便會(huì)新增很多代碼量!我們應(yīng)該如何優(yōu)化呢?
這時(shí),我們需要借助一下webpack的require.context() 方法來(lái)創(chuàng)建自己的(模塊)上下文,從而實(shí)現(xiàn)自動(dòng)動(dòng)態(tài)require組件。這個(gè)方法需要3個(gè)參數(shù):要搜索的文件夾目錄,是否還應(yīng)該搜索它的子目錄,以及一個(gè)匹配文件的正則表達(dá)式。
我們先在components文件夾(這里面都是些高頻組件)添加一個(gè)叫g(shù)lobal.js的文件,在這個(gè)文件里使用require.context 動(dòng)態(tài)將需要的高頻組件統(tǒng)統(tǒng)打包進(jìn)來(lái)。然后在main.js文件中引入global.js的文件。
- // global.js文件
- import Vue from 'vue'
- function changeStr (str) {
- return str.charAt(0).toUpperCase() + str.slice(1)
- }
- const requirerequireComponent = require.context('./', false, /\.vue$/)
- // 查找同級(jí)目錄下以vue結(jié)尾的組件
- const install = () => {
- requireComponent.keys().forEach(fileName => {
- let config = requireComponent(fileName)
- console.log(config) // ./child1.vue 然后用正則拿到child1
- let componentName = changeStr(
- fileName.replace(/^\.\//, '').replace(/\.\w+$/, '')
- )
- Vue.component(componentName, config.default || config)
- })
- }
- export default {
- install // 對(duì)外暴露install方法
- }
- // main.js
- import index from './components/global.js'
- Vue.use(index)
最后我們就可以隨時(shí)隨地在頁(yè)面中使用這些高頻組件,無(wú)需再手動(dòng)一個(gè)個(gè)引入了。
隱藏的大招--hook
開發(fā)過(guò)程中我們有時(shí)候要?jiǎng)?chuàng)建一個(gè)定時(shí)器,在組件被銷毀之前,這個(gè)定時(shí)器也要銷毀。代碼如下:
- mounted() {
- // 創(chuàng)建一個(gè)定時(shí)器
- this.timer = setInterval(() => {
- // ......
- }, 500);
- },
- // 銷毀這個(gè)定時(shí)器。
- beforeDestroy() {
- if (this.timer) {
- clearInterval(this.timer);
- this.timer = null;
- }
- }
這種寫法有個(gè)很明顯的弊端:定時(shí)器timer的創(chuàng)建和清理并不是在一個(gè)地方,這樣很容易導(dǎo)致忘記去清理!
我們可以借助hook對(duì)代碼整合,這樣代碼也更容易維護(hù)了:
- mounted() {
- let timer = setInterval(() => {
- // ......
- }, 500);
- this.$once("hook:beforeDestroy", function() {
- if (timer) {
- clearInterval(timer);
- timer = null;
- }
- });
- }
在Vue組件中,可以用過(guò)once去監(jiān)聽所有的生命周期鉤子函數(shù),如監(jiān)聽組件的updated鉤子函數(shù)可以寫成 this.$on('hook:updated', () => {})。
hook除了上面的運(yùn)用外,還可以外部監(jiān)聽組件的生命周期函數(shù)。在某些情況下,我們需要在父組件中了解一個(gè)子組件何時(shí)被創(chuàng)建、掛載或更新。
比如,如果你要在第三方組件 CustomSelect 渲染時(shí)監(jiān)聽其 updated 鉤子,可以通過(guò)@hook:updated來(lái)實(shí)現(xiàn):
- <template>
- <!--通過(guò)@hook:updated監(jiān)聽組件的updated生命鉤子函數(shù)-->
- <!--組件的所有生命周期鉤子都可以通過(guò)@hook:鉤子函數(shù)名 來(lái)監(jiān)聽觸發(fā)-->
- <custom-select @hook:updated="doSomething" />
- </template>
- <script>
- import CustomSelect from "../components/custom-select";
- export default {
- components: {
- CustomSelect
- },
- methods: {
- doSomething() {
- console.log("custom-select組件的updated鉤子函數(shù)被觸發(fā)");
- }
- }
- };
- </script>
簡(jiǎn)單暴力的router key
我們?cè)陧?xiàng)目開發(fā)時(shí),可能會(huì)遇到這樣問(wèn)題:當(dāng)頁(yè)面切換到同一個(gè)路由但不同參數(shù)地址時(shí),比如/detail/1,跳轉(zhuǎn)到/detail/2,頁(yè)面跳轉(zhuǎn)后數(shù)據(jù)竟然沒(méi)更新?路由配置如下:
- {
- path: "/detail/:id",
- name:"detail",
- component: Detail
- }
這是因?yàn)関ue-router會(huì)識(shí)別出兩個(gè)路由使用的是同一個(gè)組件從而進(jìn)行復(fù)用,并不會(huì)重新創(chuàng)建組件,而且組件的生命周期鉤子自然也不會(huì)被觸發(fā),導(dǎo)致跳轉(zhuǎn)后數(shù)據(jù)沒(méi)有更新。那我們?nèi)绾谓鉀Q這個(gè)問(wèn)題呢?我們可以為router-view組件添加屬性key,例子如下:
- <router-view :key="$route.fullpath"></router-view>
這種辦法主要是利用虛擬DOM在渲染時(shí)候通過(guò)key來(lái)對(duì)比兩個(gè)節(jié)點(diǎn)是否相同,如果key不相同,就會(huì)判定router-view組件是一個(gè)新節(jié)點(diǎn),從而先銷毀組件,然后再重新創(chuàng)建新組件,這樣組件內(nèi)的生命周期會(huì)重新觸發(fā)。
高精度權(quán)限控制--自定義指令directive
我們通常給一個(gè)元素添加v-if / v-show,來(lái)判斷該用戶是否有權(quán)限,但如果判斷條件繁瑣且多個(gè)地方需要判斷,這種方式的代碼不僅不優(yōu)雅而且冗余。針對(duì)這種情況,我們可以封裝了一個(gè)指令權(quán)限,能簡(jiǎn)單快速的實(shí)現(xiàn)按鈕級(jí)別的權(quán)限判斷。我們先在新建個(gè)array.js文件,用于存放與權(quán)限相關(guān)的全局函數(shù)
- // array.js
- export function checkArray (key) {
- let arr = ['admin', 'editor']
- let index = arr.indexOf(key)
- if (index > -1) {
- return true // 有權(quán)限
- } else {
- return false // 無(wú)權(quán)限
- }
- }
然后在將array文件掛載到全局中
- // main.js
- import { checkArray } from "./common/array";
- Vue.config.productionTip = false;
- Vue.directive("permission", {
- inserted (el, binding) {
- let permission = binding.value; // 獲取到 v-permission的值
- if (permission) {
- let hasPermission = checkArray(permission);
- if (!hasPermission) { // 沒(méi)有權(quán)限 移除Dom元素
- el.parentNode && el.parentNode.removeChild(el);
- }
- }
- }
- });
最后我們?cè)陧?yè)面中就可以通過(guò)自定義指令 v-permission來(lái)判斷:
- <div class="btns">
- <button v-permission="'admin'">權(quán)限按鈕1</button> // 會(huì)顯示
- <button v-permission="'visitor'">權(quán)限按鈕2</button> //無(wú)顯示
- <button v-permission="'editor'">權(quán)限按鈕3</button> // 會(huì)顯示
- </div>
圖片
動(dòng)態(tài)指令參數(shù)
Vue 2.6的最酷功能之一是可以將指令參數(shù)動(dòng)態(tài)傳遞給組件。我們可以用方括號(hào)括起來(lái)的 JavaScript 表達(dá)式作為一個(gè)指令的參數(shù):
- <a v-bind:[attributeName]="url"> 這是個(gè)鏈接 </a>
這里的 attributeName 會(huì)被作為一個(gè) JavaScript 表達(dá)式進(jìn)行動(dòng)態(tài)求值,求得的值將會(huì)作為最終的參數(shù)來(lái)使用。同樣地,你可以使用動(dòng)態(tài)參數(shù)為一個(gè)動(dòng)態(tài)的事件名綁定處理函數(shù):
- <a v-on:[eventName]="doSomething"> 這是個(gè)鏈接 </a>
接下來(lái)我們看個(gè)例子:假設(shè)你有一個(gè)按鈕,在某些情況下想監(jiān)聽單擊事件,在某些情況下想監(jiān)聽雙擊事件。這時(shí)動(dòng)態(tài)指令參數(shù)派上用場(chǎng):
- <template>
- <div>
- <aButton @[someEvent]="handleSomeEvent()" />
- </div>
- </template>
- <script>
- export default {
- data () {
- return {
- someEvent: someCondition ? "click" : "dbclick"
- }
- },
- methods: {
- handleSomeEvent () {
- // handle some event
- }
- }
- }
- </script>
過(guò)濾器讓數(shù)據(jù)處理更便利
Vue.js 允許你自定義過(guò)濾器,它的用法其實(shí)是很簡(jiǎn)單,但是可能有些朋友沒(méi)有用過(guò),接下來(lái)我們介紹下:
1.理解過(guò)濾器
- 功能:對(duì)要顯示的數(shù)據(jù)進(jìn)行特定格式化后再顯示。
- 注意:過(guò)濾器并沒(méi)有改變?cè)镜臄?shù)據(jù),需要對(duì)展現(xiàn)的數(shù)據(jù)進(jìn)行包裝。
- 使用場(chǎng)景:雙花括號(hào)插值和 v-bind 表達(dá)式 (后者從 2.1.0+ 開始支持)。
2.定義過(guò)濾器
可以在一個(gè)組件的選項(xiàng)中定義本地的過(guò)濾器:
- filters: {
- capitalize: function (value) {
- if (!value) return ''
- valuevalue = value.toString()
- return value.charAt(0).toUpperCase() + value.slice(1)
- }
- }
也可以在創(chuàng)建 Vue 實(shí)例之前全局定義過(guò)濾器:
- Vue.filter('capitalize', function (value) {
- if (!value) return ''
- valuevalue = value.toString()
- return value.charAt(0).toUpperCase() + value.slice(1)
- })
3.使用過(guò)濾器
使用方法也簡(jiǎn)單,即在雙花括號(hào)中使用管道符(pipeline) |隔開
- <!-- 在雙花括號(hào)中 -->
- <div>{{ myData| filterName}}</div>
- <div>{{ myData| filterName(arg)}}</div>
- <!-- 在 v-bind 中 -->
- <div v-bind:id="rawId | formatId"></div>
過(guò)濾器可以串聯(lián):
- {{ message | filterA | filterB }}
在這個(gè)例子中,filterA 被定義為接收單個(gè)參數(shù)的過(guò)濾器函數(shù),表達(dá)式 message 的值將作為參數(shù)傳入到函數(shù)中。然后繼續(xù)調(diào)用同樣被定義為接收單個(gè)參數(shù)的過(guò)濾器函數(shù) filterB,將 filterA 的結(jié)果傳遞到 filterB 中。接下來(lái)我們看個(gè)如何使用過(guò)濾器格式化日期的例子:
- <div>
- <h2>顯示格式化的日期時(shí)間</h2>
- <p>{{ date }}</p>
- <p>{{ date | filterDate }}</p>
- <p>年月日: {{ date | filterDate("YYYY-MM-DD") }}</p>
- </div>
- ......
- filters: {
- filterDate(value, format = "YYYY-MM-DD HH:mm:ss") {
- console.log(this)//undefined 過(guò)濾器沒(méi)有this指向的
- return moment(value).format(format);
- }
- },
- data() {
- return {
- date: new Date()
- };
- }