你也許不知道的Vuejs - 最佳實踐(1)

yugasun發表於2018-06-01

by yugasun from yugasun.com/post/you-ma… 本文可全文轉載,但需要保留原作者和出處。

有了前面文章的鋪墊,相信一路看過來的新手的你開發一箇中型的 Vuejs 應用已經不在話下,包括 Vuejs 生態核心工具(vue-router,vuex)的使用也不成問題。但是在實際專案開發過程中,我們要做的工作不僅僅是完成我們的業務程式碼,當一個需求完成後,我們還需要考慮更多後期優化工作,本篇主要講述程式碼層面的優化。

被忽視的 setter 之計算屬性

我們先回到上一篇的狀態管理案例,使用 vuex 方式共享我們的 msg 屬性,先建立 src/store/index.js

import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

const types = {
  UPDATE_MSG: 'UPDATE_MSG',
};

const mutations = {
  [types.UPDATE_MSG](state, payload) {
    state.msg = payload.msg;
  },
};

const actions = {
  [types.UPDATE_MSG]({ commit }, payload) {
    commit(types.UPDATE_MSG, payload);
  },
};

export default new Vuex.Store({
  state: {
    msg: 'Hello world',
  },
  mutations,
  actions,
});
複製程式碼

然後在元件 comp1 中使用它:

<template>
  <div class="comp1">
    <h1>Component 1</h1>
    <input type="text" v-model="msg">
  </div>
</template>
<script>
export default {
  name: 'comp1',
  data() {
    const msg = this.$store.state.msg;
    return {
      msg,
    };
  },
  watch: {
    msg(val) {
      this.$store.dispatch('UPDATE_MSG', { msg: val });
    },
  },
};
</script>
複製程式碼

同樣對 comp2 做相同修改。當然還得在 src/main.js 中引入:

import Vue from 'vue';
import App from './App';
import store from './store';

Vue.config.productionTip = false;

/* eslint-disable no-new */
new Vue({
  store,
  el: '#app',
  template: '<App/>',
  components: { App },
});
複製程式碼

如果還不知道 vuex 基本使用,建議先閱讀官方文件。

好了,我們已經實現 msg 的共享了,並且對其變化進行了 watch,在輸入框發生改變時,通過 $store.dispatch 來觸發相應 UPDATE_MSG actions 操作,實現狀態修改。但是你會發現修改 comp1 中的輸入框,通過 vue-devtools 也可檢視到 Vuex 中的的 state.msg 的確也跟著變了,但是 comp2 中輸入框並沒有發生改變,當然這因為我們初始化 msg 時,是直接變數賦值,並未監聽 $store.state.msg 的變化,所以兩個元件沒法實現同步。

有人又會說了,再新增個 watch 屬性,監聽 $store.state.msg 改變,重新賦值元件中的 msg 不就行了,確實可以實現,但是這樣程式碼是不是不太優雅,為了一個簡單的 msg 同步,我們需要給 data 新增屬性,外加兩個監聽器,是不是太不划算?

其實這裡是可以通過計算屬性很好地解決的,因為元件中的 msg 就是依賴 $store.state.msg 的,我們直接定義計算屬性 msg,然後返回不就可以了。

ok,修改 comp1 如下:

<template>
  <div class="comp1">
    <h1>Component 1</h1>
    <input type="text" v-model="msg">
  </div>
</template>
<script>
export default {
  name: 'comp1',
  computed: {
    msg() {
      return this.$store.state.msg;
    },
  },
};
</script>
複製程式碼

我們再次修改 comp1 中的輸入框,開啟控制檯,會報如下錯誤:

vue.esm.js?efeb:591 [Vue warn]: Computed property "msg" was assigned to but it has no setter.
...
複製程式碼

因為我們使用的是 v-model 來繫結 msg 到 input 上的,當輸入框改變,必然觸發 msgsetter(賦值)操作,但是計算屬性預設會幫我定義好 getter,並未定義 setter,這就是為什麼會出現上面錯誤提示的原因,那麼我們再自定義下 setter 吧:

<template>
  <div class="comp1">
    <h1>Component 1</h1>
    <input type="text" v-model="msg">
  </div>
</template>
<script>
export default {
  name: 'comp1',
  computed: {
    msg: {
      get() {
        return this.$store.state.msg;
      },
      set(val) {
        this.$store.dispatch('UPDATE_MSG', { msg: val });
      },
    },
  },
};
</script>
複製程式碼

可以看到,我們正好可以在 setter 中,也就是修改 msg 值得時候,將其新值傳遞到我們的 vuex 中,這樣豈不是一舉兩得了。同樣的對 comp2 做相同修改。執行專案,你會發祥,comp1 輸入框的值comp2 輸入框的值store 中的值 實現同步更新了。而且相對與上面的方案,程式碼量也精簡了很多~

可配置的 watch

先來看段程式碼:

// ...
watch: {
    username() {
      this.getUserInfo();
    },
},
methods: {
  getUserInfo() {
    const info = {
      username: 'yugasun',
      site: 'yugasun.com',
    };
    /* eslint-disable no-console */
    console.log(info);
  },
},
created() {
  this.getUserInfo();
},
// ...
複製程式碼

這裡很好理解,元件建立的時候,獲取使用者資訊,然後監聽使用者名稱,一旦發生變化就重新獲取使用者資訊,這個場景在實際開發中非常常見。那麼能不能再優化下呢?

答案是肯定的。其實,我們在 Vue 例項中定義 watcher 的時候,監聽屬性可以是個物件的,它含有三個屬性: deepimmediatehandler,我們通常直接以函式的形式定義時,Vue 內部會自動將該回撥函式賦值給 handler,而剩下的兩個屬性值會預設設定為 false。這裡的場景就可以用到 immediate 屬性,將其設定為 true 時,表示建立元件時 handler 回撥會立即執行,這樣我們就可以省去在 created 函式中再次呼叫了,實現如下:

watch: {
  username: {
    immediate: true,
    handler: 'getUserInfo',
  },
},
methods: {
  getUserInfo() {
    const info = {
      username: 'yugasun',
      site: 'yugasun.com',
    };
    /* eslint-disable no-console */
    console.log(info);
  },
},
複製程式碼

Url改變但元件未變時,created 無法觸發的問題

首先預設專案路由是通過 vue-router 實現的,其次我們的路由是類似下面這樣的:

// ...
const routes = [
  {
    path: '/',
    component: Index,
  },
  {
    path: '/:id',
    component: Index,
  },
];
複製程式碼

公用的元件 src/views/index.vue 程式碼如下:

<template>
  <div class="index">
    <router-link :to="{path: '/1'}">挑戰到第二頁</router-link><br/>
    <router-link v-if="$route.path === '/1'" :to="{path: '/'}">返回</router-link>
    <h3>{{ username }} </h3>
  </div>
</template>
<script>
export default {
  name: 'Index',
  data() {
    return {
      username: 'Loading...',
    };
  },
  methods: {
    getName() {
      const id = this.$route.params.id;
      // 模擬請求
      setTimeout(() => {
        if (id) {
          this.username = 'Yuga Sun';
        } else {
          this.username = 'yugasun';
        }
      }, 300);
    },
  },
  created() {
    this.getName();
  },
};
</script>
複製程式碼

兩個不同路徑使用的是同一個元件 Index,然後 Index 元件中的 getName 函式會在 created 的時候執行,你會發現,讓我們切換路由到 /1 時,我們的頁面並未改變,created 也並未重新觸發。

這是因為 vue-router 會識別出這兩個路由使用的是同一個元件,然後會進行復用,所以並不會重新建立元件,那麼 created 周期函式自然也不會觸發。

通常解決辦法就是新增 watcher 監聽 $route 的變化,然後重新執行 getName 函式。程式碼如下:

watch: {
  $route: {
    immediate: true,
    handler: 'getName',
  },
},
methods: {
  getName() {
    const id = this.$route.params.id;
    // 模擬請求
    setTimeout(() => {
      if (id) {
        this.username = 'Yuga Sun';
      } else {
        this.username = 'yugasun';
      }
    }, 300);
  },
},
複製程式碼

ok,問題是解決了,但是有沒有其他不用改動 index.vue 的偷懶方式呢?

就是給 router-view 新增一個 key 屬性,這樣即使是相同元件,但是如果 url 變化了,Vuejs就會重新建立這個元件。我們直接修改 src/App.vue 中的 router-view 如下:

<router-view :key="$route.fullPath"></router-view>
複製程式碼

被遺忘的 $attrs

大多數情況下,從父元件向子元件傳遞資料的時候,我們都是通過 props 實現的,比如下面這個例子:

<!-- 父元件中 -->
<Comp3
  :value="value"
  label="使用者名稱"
  id="username"
  placeholder="請輸入使用者名稱"
  @input="handleInput"
  >

<!-- 子元件中 -->
<template>
  <label>
    {{ label }}
    <input
      :id="id"
      :value="value"
      :placeholder="placeholder"
      @input="$emit('input', $event.target.value)"
    />
  </label>
</template>
<script>
export default {
  props: {
    id: {
      type: String,
      default: 'username',
    },
    value: {
      type: String,
      default: '',
    },
    placeholder: {
      type: String,
      default: '',
    },
    label: {
      type: String,
      default: '',
    },
  },
}
</script>
複製程式碼

這樣一階元件,實現起來很簡單,也沒什麼問題,我們只需要在子元件的 props 中寫一遍 id, value, placeholder... 這樣的屬性定義就可以了。但是如果子元件又包含了子元件,而且同樣需要傳遞 id, value, placeholder... 呢?甚至三階、四階...呢?那麼就需要我們在 props 中重複定義很多遍了,這怎麼能忍呢?

於是 vm.$attrs 可以閃亮登場了,先來看官方解釋:

包含了父作用域中不作為 prop 被識別 (且獲取) 的特性繫結 (class 和 style 除外)。當一個元件沒有宣告任何 prop 時,這裡會包含所有父作用域的繫結 (class 和 style 除外),並且可以通過 v-bind="$attrs" 傳入內部元件—— 在建立高階別的元件時非常有用

作者還特別強調了 在建立高階別的元件時非常有用,他就是為了解決剛才我提到的問題的。它也沒什麼難度,那麼趕緊用起來吧,程式碼修改如下:

<!-- 父元件中 -->
<Comp3
  :value="value"
  label="使用者名稱"
  id="username"
  placeholder="請輸入使用者名稱"
  @input="handleInput"
  >

<!-- 子元件中 -->
<template>
  <label>
    {{ $attrs.label }}
    <input
      v-bind="$attrs"
      @input="$emit('input', $event.target.value)"
    />
  </label>
</template>
<script>
export default {
}
</script>
複製程式碼

這樣看起來是不是清爽多了,而且就運算元元件中再次引用類似的子元件,我們也不怕了。因為有了 $attrs,哪裡不會點哪裡......

總結

當然 Vuejs 的實踐技巧遠不止如此,這裡只是總結了個人在實際開發中遇到的,而且正好是很多朋友容易忽視的地方。如果你有更好的實踐方法,歡迎評論或者發郵件給我,一起交流學習。

原始碼在此

專題目錄

You-May-Not-Know-Vuejs

相關文章