Skip to content
This repository has been archived by the owner on Aug 20, 2018. It is now read-only.

Latest commit

 

History

History
1175 lines (955 loc) · 48.5 KB

README_RU.md

File metadata and controls

1175 lines (955 loc) · 48.5 KB

vue-patterns

This is a fork of https://github.com/learn-vuejs/vue-patterns

Полезные паттерны, методы, советы и рекомендации, а также тщательно подобранный список ссылок по Vue.

Переводы

Оглавление

Объявление компонентов

Однофайловый компонент (сокращенно — SFC) — наиболее распространённый

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

<script>
export default {
  data() {
    return {
      text: 'Нажми на меня',
    };
  },
  methods: {
    handleClick() {
      console.log('Клик по кнопке');
    },
  },
}
</script>

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

Шаблонные строки (или литералы шаблонов в ES6)

Vue.component('my-btn', {
  template: `
    <button class="btn-primary" @click.prevent="handleClick">
      {{ text }}
    </button>
  `,
  data() {
    return {
      text: 'Нажми на меня',
    };
  },
  methods: {
    handleClick() {
      console.log('Клик по кнопке');
    },
  },
});
Vue.component('my-btn', {
  data() {
    return {
      text: 'Нажми на меня',
    };
  },
  methods: {
    handleClick() {
      console.log('Клик по кнопке');
    },
  },
  render(h) {
    return h('button', {
        attrs: {
          class: 'btn-primary'
        },
        on: {
          click: this.handleClick,
        },
    });
  },
});
Vue.component('my-btn', {
  data() {
    return {
      text: 'Нажми на меня',
    };
  },
  methods: {
    handleClick() {
      console.log('Клик по кнопке');
    },
  },
  render() {
    return (
      <button class="btn-primary" onClick={this.handleClick}>
        {{this.text}}
      </button>
    );
  },
});
<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 = 'Нажми на меня';

  handleClick() {
    console.log('Клик по кнопке');
  }
}
</script>

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

Ссылки:

Взаимодействие компонента

Входные параметры и события

В целом, компонент Vue следует однонаправленному потоку данных, то есть входные параметры передаются вниз (см. официальное руководство), а события — наверх. Входные параметры — это данные только для чтения, поэтому невозможно изменить входные параметры дочерних компонентов. При изменении входных параметров, дочерние компоненты будут автоматически повторно отрендерены (входные параметры являются реактивными источниками данных). Дочерние компоненты могут генерировать событие только к непосредственному родительскому компоненту, так что он может изменять data, сопоставляемые с props дочернего компонента.

<template>
  <button @click="$emit('click')">{{ text }}</button>
</template>

<script>
export default {
  name: 'v-btn',
  props: {
    text: String,
  },
};
</script>
<template>
  <v-btn :text="buttonText" @click="handleClick"></v-btn>
</template>

<script>
export default {
  data() {
    return {
      clickCount: 0,
      buttonText: 'Стандартное название кнопки',
    };
  },
  methods: {
    handleClick() {
      this.buttonText = `Кнопка нажата ${++this.clickCount}`;
      console.log('Клик по кнопке', this.buttonText);
    }
  }
};
</script>

Ссылки:

Обработка событий компонента

Ссылки:

Условный рендеринг компонента

Директивы (v-if / v-else / v-else-if / v-show)

v-if

<h1 v-if="true">Рендеринг только, если условие v-if равняется true</h1>

Использование v-if и v-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` не равен ни `A`, ни `B`, ни `C`</div>

Использование v-show

<h1 v-show="true">Всегда рендерится, но виден только в том случае, если условия `v-show` равняются true</h1>

Если вы хотите по условию отобразить более одного элемента, вы можете использовать директивы (v-if / v-else / v-else-if /v-show) на элементе <template>. Обратите внимание, что элемент <template> фактические не будет отображаться в DOM. Это как невидимая обёртка.

<template v-if="true">
  <h1>Все элементы</h1>
  <p>будут отрендерены в DOM,</p>
  <p>за исключением элемента `template`</p>
</template>

JSX

Если вы используете JSX в своем Vue-приложении, то можете применять все техники, например использования выражения if else и switch case, а также тернарные и логические операторы.

Использование выражения if else

export default {
  data() {
    return {
      isTruthy: true,
    };
  },
  render(h) {
    if (this.isTruthy) {
      return <h1>Рендеринг, если значение равно true</h1>;
    } else {
      return <h1>Рендеринг, если значение равно false</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 />;
  },
};

Использование тернарного оператора

export default {
  data() {
    return {
      isTruthy: true,
    };
  },
  render(h) {
    return (
      <div>
        {this.isTruthy ? (
          <h1>Рендеринг, если значение равно true</h1>
        ) : (
          <h1>Рендеринг, если значение равно false</h1>
        )}
      </div>
    );
  },
};

Использование логического оператора

export default {
  data() {
    return {
      isLoading: true,
    };
  },
  render(h) {
    return <div>{this.isLoading && <h1>Загрузка ...</h1>}</div>;
  },
};

Ссылки

Динамический компонент

<component> с атрибутом is

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

В приведённом выше примере отрендеренный компонент будет уничтожаться, если другой компонент должен будет рендериться в <component>. Если необходимо, чтобы компоненты сохраняли свои экземпляры без их уничтожения в теге <component>, можно обернуть <component> в тег <keep-alive>:

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

Ссылки

Композиция

Библиотека

Простой пример композиции

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

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

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

Ссылки

Расширение компонента

Если вы хотите расширить один Vue-компонент, можно поступить следующим образом:

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

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

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

Ссылки:

Примеси

// 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 default {
  mixins: [closableMixin],
  props: ['text']
};
</script>

Ссылки:

Слоты (по умолчанию)

<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>
    Логин
  </v-btn>
</template>

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

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

Ссылки:

Именованные слоты

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

В большинстве случаев вы можете использовать слоты с ограниченной областью видимости вместо рендеринга входных параметров. Но в некоторых случаях это может быть полезно.

С однофайловым компонентом SFC

<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>
          Позиция мыши ({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;

Ссылки:

Передача входных параметров

Иногда вам может понадобиться передать входные параметры и обработчики дочернему компоненту, не объявляя всех входных параметров дочернего компонента. Вы можете привязать $attrs и $listeners в дочернем компоненте и установить inheritAttrs на false (в противном случае div и child-component получат атрибуты).

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

<script>
export default {
  name: 'PassingPropsSample'
  inheritAttrs: false,
  props: {
    title: {
      type: String,
      default: 'Привет, Vue!'
    }
  }
};
</script>

Из родительского компонента вы можете сделать следующее:

<template>
  <passing-props-sample
    title="Привет, передача входных параметров"
    childPropA="Эти реквизиты будут правильно сопоставлены c <child-component />"
    @click="handleChildComponentClick"
  >
  </passing-props-sample>
</template>

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

export default {
  components: {
    PassingPropsSample
  },
  methods: {
    handleChildComponentClick() {
      console.log('Клик по кнопке в дочернем компоненте');
    }
  }
};
</script>

Ссылки:

Компоненты высшего порядка (они же HOC)

Ссылки:

Внедрение зависимостей

Vue поддерживает механизм предоставления и внедрения объекта во всех потомки, независимо от глубины иерархии компонентов, при условии, что компоненты находятся в одной и той же цепочке родителей. Обратите внимание, что привязки provide и inject не являются реактивными, пока вы не передадите наблюдаемый объект.

<parent-component>
  <child-component>
    <grand-child-component></grand-child-component>
  </child-component>
</parent-component>

С приведенной выше иерархией компонентов в качестве примера для получения данных из parent-component вам нужно передавать данные (объект) в качестве props компоненту child-component и компоненту grand-child-component. Однако, если parent-component предоставляет (provide) данные (объект), grand-child-component может просто определить свойство inject для получения объекта, предоставляемого parent-component.

Ссылки:

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>
// 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>

Обработка ошибок

Хук errorCaptured

export default {
  name: 'ErrorBoundary',
  data() {
    return {
      error: false,
      errorMessage: '',
    };
  },
  errorCaptured (err, vm, info) {
    this.error = true;
    this.errorMessage = `${err.stack}\n\nобнаружена в методе ${info} компонента`;

    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() {
  this.fetchUserList();
},
watch: {
  searchText: 'fetchUserList',
}
// Делайте так
watch: {
  searchText: {
    handler: 'fetchUserList',
    immediate: true,
  }
}

Полезные ссылки

Руководство по стилю

Рефакторинг

Управление состоянием

Vuex

MobX

Компоненты без рендеринга

Примеры

Структура каталогов

Советы и хитрости

Маршрутизатор

Антипаттерны

Видео / Аудио

Repos

Платное

TypeScript

Flowtype

GraphQL


Разное

Книга Fullstack Vue

Fullstack Vue Book