【译】Vue Patterns

9,841 阅读9分钟
原文链接: github.com

英文原版:learn-vuejs
中文翻譯:yoyoys

此頁面集結了許多有用的 Vue 實作模式、技術、技巧、以及有幫助的參考連結。

元件宣告

單文件組件(Single File Component, SFC) - 最為常見

<template>
  <button class="btn-primary" @click.prevent="handleClick">
    {{text}}
  </button>
</template>

<script>
export default {
  data() {
    return {
      text: 'Click me',
    };
  },
  methods: {
    handleClick() {
      console.log('clicked');
    },
  },
}
</script>

<style scoped>
.btn-primary {
  background-color: blue;
}
</style>

字串樣板 (String Template) (或是 es6 樣板字面值 (Template Literal))

Vue.component('my-btn', {
  template: `
    <button class="btn-primary" @click.prevent="handleClick">
      {{text}}
    </button>
  `,
  data() {
    return {
      text: 'Click me',
    };
  },
  methods: {
    handleClick() {
      console.log('clicked');
    },
  },
});

渲染函式 (Render Function)

Vue.component('my-btn', {
  data() {
    return {
      text: 'Click me',
    };
  },
  methods: {
    handleClick() {
      console.log('clicked');
    },
  },
  render(h) {
    return h('button', {
      attrs: {
        class: 'btn-primary'
      },
      on: {
        click: this.handleClick,
      },
    });
  },
});

JSX

Vue.component('my-btn', {
  data() {
    return {
      text: 'Click me',
    };
  },
  methods: {
    handleClick() {
      console.log('clicked');
    },
  },
  render() {
    return (
      <button class="btn-primary" onClick={this.handleClick}>
        {{this.text}}
      </button>
    );
  },
});

vue-class-component (使用 es6 classes)

<template>
  <button class="btn-primary" @click.prevent="handleClick">
    {{text}}
  </button>
</template>

<script>
import Vue from 'vue';
import Component from 'vue-class-component';

@Component
export default MyBtn extends Vue {
  text = 'Click me';

  handleClick() {
    console.log('clicked');
  }
}
</script>

<style scoped>
.btn-primary {
  background-color: blue;
}
</style>

參考連結

元件條件渲染 (Component Conditional Rendering)

指令 (Directives) (v-if / v-else / v-else-if / v-show)

v-if

<h1 v-if="true">只在 v-if 值為 true 時渲染</h1>

v-ifv-else

<h1 v-if="true">只在 v-if 值為 true 時渲染</h1>
<h1 v-else>只在 v-if 值為 false 時渲染</h1>

v-else-if

<div v-if="type === 'A'">只在 `type` 等於 `A` 時渲染</div>
<div v-else-if="type === 'B'">只在 `type` 等於 `B` 時渲染</div>
<div v-else-if="type === 'C'">只在 `type` 等於 `C` 時渲染</div>
<div v-else>只在 `type` 不等於>fmf `A` 或 `B` 或 `C` 時渲染</div>

v-show

<h1 v-show="true">永遠都會渲染,但是只在 `v-show` 值為 true 時顯示</h1>

如果你需要同時在多個元素上面做條件式渲染,你可以在 <template> 元素上使用這些指令 (v-if / v-else / v-else-if /v-show)。 注意:<template> 元素不會實際渲染一個 DOM。

<template v-if="true">
  <h1>所有元素</h1>
  <p>都會被渲染成為 DOM</p>
  <p>除了 `template` 元素</p>
</template>

JSX

如果你在你的 Vue 應用程式中使用 JSX,你可以使用所有 javascript 語句,例如 if elseswitch case 、三元運算 (ternary) 與 邏輯運算式 (logical operator)

if else 語句

export default {
  data() {
    return {
      isTruthy: true,
    };
  },
  render(h) {
    if (this.isTruthy) {
      return <h1>值為真時渲染</h1>;
    } else {
      return <h1>值為假時渲染</h1>;
    }
  },
};

switch case 語句

import Info from './Info';
import Warning from './Warning';
import Error from './Error';
import Success from './Success';

export default {
  data() {
    return {
      type: 'error',
    };
  },
  render(h) {
    switch (this.type) {
      case 'info':
        return <Info text={text} />;
      case 'warning':
        return <Warning text={text} />;
      case 'error':
        return <Error text={text} />;
      default:
        return <Success text={text} />;
    },
  }
};

你也可以透過物件的對應來簡化 switch case

import Info from './Info';
import Warning from './Warning';
import Error from './Error';
import Success from './Success';

const COMPONENT_MAP = {
  info: Info,
  warning: Warning,
  error: Error,
  success: Success,
};

export default {
  data() {
    return {
      type: 'error',
    };
  },
  render(h) {
    const Comp = COMPONENT_MAP[this.type || 'success'];

    return <Comp />;
  },
};

三元運算子 (ternary operator)

export default {
  data() {
    return {
      isTruthy: true,
    };
  },
  render(h) {
    return (
      <div>
        {this.isTruthy ? (
          <h1>值為真時渲染</h1>
        ) : (
          <h1>值為假時渲染</h1>
        )}
      </div>
    );
  },
};

邏輯運算子 (logical operator)

export default {
  data() {
    return {
      isLoading: true,
    };
  },
  render(h) {
    return <div>{this.isLoading && <h1>Loading ...</h1>}</div>;
  },
};

參考連結

動態元件

使用 is 屬性在 <component> 元素上

<component :is="currentTabComponent"></component>

上面的範例,原有 <component> 中的元件,在切換元件的同時將會被消滅。 如果你需要切換後仍保留 <component> 中元件的實體,而不被消滅的話,可以包裹一個 <keep-alive> 標籤,如下:

<keep-alive>
  <component :is="currentTabComponent"></component>
</keep-alive>

元件組合

基本組合 (Basic Composition)

<template>
  <div class="component-b">
    <component-a></component-a>
  </div>
</template>

<script>
import ComponentA from './ComponentA';

export default {
  components: {
    ComponentA,
  },
};
</script>

繼承 (Extends)

當你需要繼承一個單文件組件 (SFC) 時可以使用。

<template>
  <button class="button-primary" @click.prevent="handleClick">
    {{buttonText}}
  </button>
</template>

<script>
import BaseButton from './BaseButton';

export default {
  extends: BaseButton,
  props: ['buttonText'],
};
</script>

參考連結

混入 (Mixins)

// closableMixin.js
export default {
  props: {
    isOpen: {
      default: true
    }
  },
  data: function() {
    return {
      shown: this.isOpen
    }
  },
  methods: {
    hide: function() {
      this.shown = false;
    },
    show: function() {
      this.shown = true;
    },
    toggle: function() {
      this.shown = !this.shown;
    }
  }
}
<template>
  <div v-if="shown" class="alert alert-success" :class="'alert-' + type" role="alert">
    {{text}}
    <i class="pull-right glyphicon glyphicon-remove" @click="hide"></i>
  </div>
</template>

<script>
import closableMixin from './mixins/closableMixin';

export deafult {
  mixins: [closableMixin],
  props: ['text']
};
</script>

參考連結

預設插槽 (Slots (Default))

<template>
  <button class="btn btn-primary">
    <slot></slot>
  </button>
</template>

<script>
export default {
  name: 'VBtn',
};
</script>
<template>
  <v-btn>
    <span class="fa fa-user"></span>
    Login
  </v-btn>
</template>

<script>
import VBtn from './VBtn';
  
export default {
  components: {
    VBtn,
  }
};
</script>

參考連結

具名插槽(Named Slots)

BaseLayout.vue

<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>

App.vue

<base-layout>
  <template slot="header">
    <h1>這裡是頁面標題</h1>
  </template>

  <p>一段文件主體內的文字</p>
  <p>另外一段文字</p>

  <template slot="footer">
    <p>一些聯絡資訊</p>
  </template>
</base-layout>

作用域插槽 (Scoped Slots)

<template>
  <ul>
    <li
      v-for="todo in todos"
      v-bind:key="todo.id"
    >
      <!-- 保留一個插槽供每一個 todo 使用,-->
      <!-- 並將 將 `todo` 物件作為插槽參數傳遞給它,供外部元件使用。-->
      <slot v-bind:todo="todo">
        {{ todo.text }}
      </slot>
    </li>
  </ul>
</template>

<script>
export default {
  name: 'TodoList',
  props: {
    todos: {
      type: Array,
      default: () => ([]),
    }
  },
};
</script>
<template>
  <todo-list v-bind:todos="todos">
      <template slot-scope="{ todo }">
        <span v-if="todo.isComplete">✓</span>
        {{ todo.text }}
      </template>
  </todo-list>
</template>

<script>
import TodoList from './TodoList';

export default {
  components: {
    TodoList,
  },
  data() {
    return {
      todos: [
        { todo: 'todo 1', isComplete: true },
        { todo: 'todo 2', isComplete: false },
        { todo: 'todo 3', isComplete: false },
        { todo: 'todo 4', isComplete: true },
      ];
    };
  },
};
</script>

參考資料

渲染屬性 (Render Props)

大多數狀況下,你可以優先使用作用域插槽 (Scoped Slots) 之於渲染屬性 (Render Props),但是,在某些狀況下渲染屬性還是很有用的。

於單文件組件:

<template>
  <div id="app">
    <Mouse :render="__render"/>
  </div>
</template>

<script>
import Mouse from "./Mouse.js";
export default {
  name: "app",
  components: {
    Mouse
  },
  methods: {
    __render({ x, y }) {
      return (
        <h1>
          The mouse position is ({x}, {y})
        </h1>
      );
    }
  }
};
</script>
<style>
* {
  margin: 0;
  height: 100%;
  width: 100%;
}
</style>

JSX

const Mouse = {
  name: "Mouse",
  props: {
    render: {
      type: Function,
      required: true
    }
  },
  data() {
    return {
      x: 0,
      y: 0
    };
  },
  methods: {
    handleMouseMove(event) {
      this.x = event.clientX;
      this.y = event.clientY;
    }
  },
  render(h) {
    return (
      <div style={{ height: "100%" }} onMousemove={this.handleMouseMove}>
        {this.$props.render(this)}
      </div>
    );
  }
};

export default Mouse;

參考連結

參數傳遞 (Passing Props)

有時候你想要傳遞所有參數 (props) 與事件 (listeners) 到子元件,但又不想要宣告所有子元件的參數。 你可以直接將 $attrs$listeners 綁定在子元件上。

<template>
  <div>
    <h1>{{title}}</h1>
    <child-component v-bind="$attrs" v-on="$listeners"></child-component>
  </div>
</template>

<script>
export default {
  name: 'PassingPropsSample'
  props: {
    title: {
      type: String,
      default: 'Hello, Vue!'
    }
  }
};
</script>

在父元件上,你可以這樣做:

<template>
  <passing-props-sample
    title="Hello, Passing Props"
    childPropA="This props will properly mapped to <child-component />"
    @click="handleChildComponentClick"
  >
  </passing-props-sample>
</template>

<script>
import PassingPropsSample from './PassingPropsSample';

export default {
  components: {
    PassingPropsSample
  },
  methods: {
    handleChildComponentClick() {
      console.log('child component clicked');
    }
  }
};
</script>

參考資料

高優先元件 (Higher Order Component, HOC)

參考連結

相依注入 (Dependency injection)

Vue 支援 提供注入 (Provide / inject) 機制來傳遞一個物件到所有子代元件中,不管結構有多深,只要都基於同一個父代即可。 注意: provideinject 並沒有響應能力 (reactive) ,除非你傳遞的物件本身就帶有響應能力。

<parent-component>  // 父元件
  <child-component>  // 子元件
    <grand-child-component></grand-child-component>  // 孫元件
  </child-component>
</ancestor-component>

上述的元件結構,若要從 父元件 取得資料,你必須要透過 參數(props) 傳遞資料到 子元件孫元件 之中。 但如果 父元件 提供 (provide) 資料(或物件), 孫元件 可以透過宣告直接 注入 (inject) 父元件 中所定義的資料(或物件)。

參考連結

提供注入 (Provide / Inject)

// ParentComponent.vue

export default {
  provide: {
    theme: {
      primaryColor: 'blue',
    },
  },
};
// GrandChildComponent.vue

<template>
  <button :style="{ backgroundColor: primary && theme.primaryColor }">
    <slot></slot>
  </button>
</template>

<script>
export default {
  inject: ['theme'],
  props: {
    primary: {
      type: Boolean,
      default: true,
    },
  },
};
</script>

注入裝飾器模式 (@Provide / @Inject Decorator)

// ParentComponent.vue

import { Component, Vue, Provide } from 'vue-property-decorator';

@Component
export class ParentComponent extends Vue {
  @Provide
  theme = {
    primaryColor: 'blue',
  };
}
// GrandChildComponent.vue

<template>
  <button :style="{ backgroundColor: primary && theme.primaryColor }">
    <slot></slot>
  </button>
</template>

<script>
import { Component, Vue, Inject, Prop } from 'vue-property-decorator';

export class GrandChildComponent extends Vue {
  @Inject() theme;

  @Prop({ default: true })
  primary: boolean;
};
</script>

錯誤處理 (Handling Errors)

errorCaptured 事件

export default {
  name: 'ErrorBoundary',
  data() {
    return {
      error: false,
      errorMessage: '',
    };
  },
  errorCaptured (err, vm, info) {
    this.error = true;
    this.errorMessage = `${err.stack}\n\nfound in ${info} of component`;
    
    return false;
  },
  render (h) {
    if (this.error) {
      return h('pre', { style: { color: 'red' }}, this.errorMessage);
    }

    return this.$slots.default[0]
  }
};
<error-boundary>
  <another-component/>
</error-boundary>

範例

參考連結

生產力小技巧

讓監聽器在 created 事件時就有效

// 不要這樣做
created() {
  this.fetchUserList();
},
watch: {
  searchText: 'fetchUserList',
}
// 這樣做
watch: {
  searchText: {
    handler: 'fetchUserList',
    immediate: true,
  }
}

有用的連結

組建間的溝通

重構技巧

Vuex

Mobx

不須渲染的元件 (Renderless Component)

範例

目錄結構

小技巧

專案範例

不良示範 (反模式)

影片與音訊課程

付費課程


其他資訊