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

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

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

被忽視的 setter 之計算屬性

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

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 中使用它:java

<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 中引入:git

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 基本使用,建議先閱讀官方文檔。github

好了,咱們已經實現 msg 的共享了,而且對其變化進行了 watch,在輸入框發生改變時,經過 $store.dispatch 來觸發相應 UPDATE_MSG actions 操做,實現狀態修改。可是你會發現修改 comp1 中的輸入框,經過 vue-devtools 也可查看到 Vuex 中的的 state.msg 的確也跟着變了,可是 comp2 中輸入框並無發生改變,固然這由於咱們初始化 msg 時,是直接變量賦值,並未監聽 $store.state.msg 的變化,因此兩個組件無法實現同步。vue-router

有人又會說了,再添加個 watch 屬性,監聽 $store.state.msg 改變,從新賦值組件中的 msg 不就好了,確實能夠實現,可是這樣代碼是否是不太優雅,爲了一個簡單的 msg 同步,咱們須要給 data 添加屬性,外加兩個監聽器,是否是太不划算?vuex

其實這裏是能夠經過計算屬性很好地解決的,由於組件中的 msg 就是依賴 $store.state.msg 的,咱們直接定義計算屬性 msg,而後返回不就能夠了。api

ok,修改 comp1 以下:bash

<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

相關文章
相關標籤/搜索