JavaScript

Как избежать нарушения принципов SOLID в Vue. JS приложение

Spread the love

В этой статье я хотел бы обсудить, как мы можем избежать нарушения принципов SOLID в нашем проекте Vue.JS.

Что такое SOLID? SOLID — это аббревиатура, созданная Майклом Фезерсом и продвигаемая американским инженером-программистом Робертом С. «Дядя Боб» Мартином в его книге «Принципы проектирования и шаблоны проектирования». Эти принципы являются очень важной частью парадигмы объектно-ориентированного программирования, предназначенной для того, чтобы сделать нашу программу более гибкой, читаемой и поддерживаемой для последующей разработки. Принципы SOLID включают следующие понятия:

  • Принцип единой ответственности (Single responsibility principle)
  • Принцип открытости / закрытости (Open-close principle)
  • Принцип подстановки Лисков (Liskov substitution principle)
  • Принцип отделения интерфейсов (Interface segregation principle)
  • Принцип инверсии зависимостей (Dependency inversion principle)

Давайте посмотрим на все эти принципы в реальном проекте Vue.JS и как мы можем избежать нарушений принципов. Мы собираемся создать простое приложение со списком Todo.

Прим. переводчика: более подробнее о SOLID можно почитать здесь:


Предварительные условие

Давайте создадим новое приложение Vue.JS, используя vue cli (https://cli.vuejs.org/).

vue create todo-app

В нашем приложении я собираюсь использовать vue 2.6.10 + typescript 3.4.3, поэтому, если вы еще не знакомы с typescript, вы можете найти документацию здесь(https://www.typescriptlang.org/docs/home.html). Так же нужно будет выбрать опцию установки с vue-router. Так же нужно будет проверить наличие установленных библиотек: node-sass, sass-loader

После установки нам нужно немного почистить и удалить все демонстрационные компоненты. Наша структура каталогов src выглядит следующим образом

src
----views
-------Home.vue
----App.vue
----main.ts
----router.ts
----shims-tsx.d.ts
----shims-vue.d.ts
----types.ts

Содержимое файла main.ts

import Vue from 'vue';
import App from './App.vue';
import router from "./router";

Vue.config.productionTip = false;

new Vue({
  router,
  render: (h) => h(App),
}).$mount('#app');

Содержимое файла router.ts

import Vue from "vue";
import Router from "vue-router";
import Home from "./views/Home.vue";

Vue.use(Router);

export default new Router({
  routes: [
    {
      path: "/",
      name: "home",
      component: Home
    }
  ]
});

Содержимое файла App.vue

<template>
  <div id="app">
    <router-view />
  </div>
</template>
<style lang="scss">
body {
  margin: 0;
  padding: 0;
}
</style>

Файл Home.vue

<template>
  <div>
    Content
  </div>
</template>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'
@Component
export default class Home extends Vue {}
</script>

Теперь мы готовы идти дальше…


Принцип единой ответственности (SRP)

Давайте предположим, что нам нужно изменить компонент views/Home.vue, чтобы получить список задач и показать их пользователю. Это может выглядеть так:

Home.vue

<template>
  <div>
    <header class="header">
      <nav class="header-nav" />
      <div class="container">
        <h1>My todo list</h1>
      </div>
    </header>
    <main>
      <div class="container">
        <div class="todo-list">
          <div
            v-for="{ id, title, completed } in todos"
            :key="id"
            class="todo-list__task"
          >
            <span :class="{ 'todo-list__task--completed': completed }">
              {{ title }}
            </span>
          </div>
        </div>
      </div>
    </main>
  </div>
</template>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'
import { ITodo } from '@/types'
@Component
export default class Home extends Vue {
  todos: ITodo[] = []
  mounted() {
    this.fetchTodos()
  }
  fetchTodos(): void {
    fetch('https://jsonplaceholder.typicode.com/todos/')
      .then(response => response.json())
      .then((todos: ITodo[]) => (this.todos = todos))
  }
}
</script>
<style lang="scss">
.header {
  width: 100%;
  &-nav {
    background: teal;
    width: 100%;
    height: 56px;
  }
}
.container {
  padding: 1.5rem;
}
.todo-list {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: stretch;
  &__task {
    width: 24%;
    padding: 1.5rem;
    margin: 0.5%;
    text-align: left;
    color: #4169e1;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
    transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
    &:hover {
      box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25),
        0 10px 10px rgba(0, 0, 0, 0.22);
    }
    &--completed {
      color: #2e8b57;
      text-decoration: line-through;
    }
  }
}
</style>

types.ts

export interface ITodo {
  id: number
  userId: number
  title: string
  completed: boolean
}

По сути, мы создали все приложение целиком в одном компоненте views/Home.vue. Здесь мы видим нарушение SRP, которое говорит нам: «У каждого компонента должна быть только одна причина для изменения». В данном случае у нам может быть множество причин что бы изменить этот компонент:

  1. Например метод fetchTodos() для извлечения задач. Это может быть любая причина: библиотека в axios или любую другую библиотеку, API, сам метод и т. д.
  2. Необходимость добавление больше элементов в этот компонент: боковая панель, меню, нижний колонтитул и т. д.
  3. Необходимость в изменения существующих элементов: заголовок или список задач.

Мы описали по крайней мере три причины, по которым нам возможно потребуется изменить views/Home.vue. Настоящая проблема начинается, когда приложение будет расти и меняться. Компонент будет становится больше и больше, и мы не сможем помнить весь наш код и в итоге потеряем контроль на ним. Мы можем избежать нарушения SRP, извлекая каждую причину в отдельный компонент, класс или функцию. Давайте сделаем рефакторинг.

Прежде всего, мы можем отделить метод fetchTodos, создав новый класс Api и поместив его в файл api.ts.

api.ts

export class Api {
  private baseUrl: string = 'https://jsonplaceholder.typicode.com/'
  constructor(private url: string) {}
  async fetch() {
    const response = await fetch(`${this.baseUrl}${this.url}`)
    return await response.json()
  }
}

Далее давайте извлечем заголовок в новый компонент components/Header.vue

Header.vue

<template functional>
  <header class="header">
    <nav class="header-nav" />
    <div class="container">
      <h1>{{ props.listName }}</h1>
    </div>
  </header>
</template>
<style lang="scss" scoped>
.header {
  width: 100%;
  &-nav {
    background: teal;
    width: 100%;
    height: 56px;
  }
}
</style>

Создадим новый компонент TodoList.vue

<template>
  <div class="container">
    <div class="todo-list">
      <div v-for="todo in todos" :key="todo.id" class="todo-list__task">
        <span :class="{ 'todo-list__task--completed': todo.completed }">
          {{ todo.title }}
        </span>
      </div>
    </div>
  </div>
</template>
<script lang="ts">
import { Component, Vue, Prop } from 'vue-property-decorator'
import { ITodo } from '@/types'
@Component
export default class TodoList extends Vue {
  @Prop({ required: true, default: () => [] }) todos!: ITodo[]
}
</script>
<style lang="scss" scoped>
.todo-list {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: stretch;
  &__task {
    width: 24%;
    padding: 1.5rem;
    margin: 0.5%;
    text-align: left;
    color: #4169e1;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
    transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
    &:hover {
      box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25),
        0 10px 10px rgba(0, 0, 0, 0.22);
    }
    &--completed {
      color: #2e8b57;
      text-decoration: line-through;
    }
  }
}
</style>

И наш последний шаг — изменения в Home.vue

Home.vue

<template>
  <div>
    <Header listName="My new todo list" />
    <main>
      <TodoList :todos="todos" />
    </main>
  </div>
</template>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'
import { ITodo } from '@/types'
import { Api } from '@/api'
import Header from '@/components/Header.vue'
import TodoList from '@/components/TodoList.vue'
@Component({
  components: { Header, TodoList }
})
export default class Home extends Vue {
  todos: ITodo[] = []
  async mounted() {
    this.todos = await this.fetchTodos()
  }
  async fetchTodos(): Promise<ITodo[]> {
    const api = new Api('todos')
    return await api.fetch()
  }
}
</script>
<style lang="scss">
.container {
  padding: 1.5rem;
}
</style>

Наш код в views/Home.vue теперь выглядит чище и читабельнее.


Принцип открытости/закрытости (OCP)

Давайте посмотрим на наш новый компонент components/TodoList.vue немного ближе. Мы видим, что этот компонент берет список задач и создает связку карточек для представления наших задач. Однако что произойдет, если мы захотим поменять эти карты или даже показать наши задачи в виде таблицы, а не карт? Мы должны изменить (модифицировать) наш компонент. Сейчас это выглядит немного негибким. OCP говорит нам: «Компоненты должны быть открыты для расширения, но закрыты для модификации». Давайте исправим это нарушение.

Мы можем использовать слоты vue, чтобы сделать наши components/TodoList.vue более гибкими.

TodoList.vue

<template functional>
  <div class="container">
    <div class="todo-list">
      <slot />
    </div>
  </div>
</template>
<style lang="scss" scoped>
.todo-list {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: stretch;
}
</style>

И переместим наши карты в отдельный компонент components/TodoCard.vue

TodoCard.vue

<template>
  <div class="todo-list__task">
    <span :class="{ 'todo-list__task--completed': todo.completed }">
      {{ todo.title }}
    </span>
  </div>
</template>
<script lang="ts">
import { Component, Vue, Prop } from 'vue-property-decorator'
import { ITodo } from '@/types'
@Component
export default class TodoCard extends Vue {
  @Prop({ required: true }) todo!: ITodo
}
</script>
<style lang="scss" scoped>
.todo-list {
  &__task {
    width: 24%;
    padding: 1.5rem;
    margin: 0.5%;
    text-align: left;
    color: #4169e1;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
    transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
    &:hover {
      box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25),
        0 10px 10px rgba(0, 0, 0, 0.22);
    }
    &--completed {
      color: #2e8b57;
      text-decoration: line-through;
    }
  }
}
</style>

И далее обновим views/Home.vue

<template>
  <div>
    <Header listName="My new todo list" />
    <main>
      <TodoList>
        <TodoCard v-for="todo in todos" :key="todo.id" :todo="todo" />
      </TodoList>
    </main>
  </div>
</template>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'
import { ITodo } from '@/types'
import { Api } from '@/api'
import Header from '@/components/Header.vue'
import TodoList from '@/components/TodoList.vue'
import TodoCard from '@/components/TodoCard.vue'

@Component({
  components: { Header, TodoList, TodoCard }
})
export default class Home extends Vue {
  todos: ITodo[] = []
  async mounted() {
    this.todos = await this.fetchTodos()
  }
  async fetchTodos(): Promise<ITodo[]> {
    const api = new Api('todos')
    return await api.fetch()
  }
}
</script>
<style lang="scss">
.container {
  padding: 1.5rem;
}
</style>

Теперь мы можем легко заменить визуализацию наших задач другим компонентом.


Принцип подстановки Лисков (LSP)

Давайте далее рассмотрим наш класс Api.

Во-первых, мы переименуем и реорганизуем класс Api в класс BaseApi и перемещаем его в отдельный каталог api/BaseApi.ts

BaseApi.ts

export class BaseApi {
  protected baseUrl: string = 'https://jsonplaceholder.typicode.com/'
  async fetch(url: string): Promise<any> {
    const response = await fetch(`${this.baseUrl}${url}`)
    return await response.json()
  }
}

Как вы можете видеть, класс BaseApi имеет метод fetch(), который принимает один аргумент «url».

По некоторым причинам мы решили добавить библиотеку axios в наше приложение.

npm install --save axios

И создадим новый класс AxiosApi, который будет подклассом BaseApi в api/AxiosApi.ts

AxiosApi.ts

import axios from 'axios'
import { BaseApi } from '@/api/BaseApi'

export class AxiosApi extends BaseApi {
  constructor() {
    super()
  }
  async fetch({ url }): Promise<any> {
    const { data } = await axios.get(`${this.baseUrl}${url}`)
    return data
  }
}

Если мы заменим наш BaseApi (родительский класс) новым AxiosApi (подкласс) в методе fetchTodos() в views/Home.vue

import { AxiosApi } from '@/api/AxiosApi'...
async
fetchTodos(): Promise<ITodo[]> {
const api = new AxiosApi()
return await api.fetch('todos')
}

это сломает наше приложение, потому что мы не следовали LSP: «При расширении класса помните, что вы должны иметь возможность передавать объекты подкласса вместо объектов родительского класса, не нарушая клиентский код».

Как вы могли заметить, мы передали объект в качестве аргумента в метод fetch() класса AxiosApi, но вместо него класс BaseApi принимает строку. В этом случае мы не можем безболезненно заменить подкласс родительским классом.

AxiosApi.ts

import axios from 'axios'
import { BaseApi } from '@/api/BaseApi'

export class AxiosApi extends BaseApi {
  constructor() {
    super()
  }
  async fetch(url: string): Promise<any> {
    const { data } = await axios.get(`${this.baseUrl}${url}`)
    return data
  }
}

Теперь мы можем использовать и BaseApi, и AxiosApi. И мы можем еще глубже погрузиться и улучшить наш код, создав класс Api в api/api.ts, который расширяет BaseClass и имеет приватного провайдера свойств.

api.ts

import { BaseApi } from '@/api/BaseApi'
import { AxiosApi } from '@/api/AxiosApi'

export class Api extends BaseApi {
  private provider: any = new AxiosApi()
  async fetch(url: string): Promise<any> {
    return await this.provider.fetch(url)
  }
}

Теперь методу fetchTodos() в views/Home.vue не нужно знать, какую библиотеку мы используем, и мы можем легко переключать провайдера в классе Api.

import { Api } from '@/api/api'
...async fetchTodos(): Promise<ITodo[]> {
  const api = new Api()
  return await api.fetch('todos')
}

Принцип отделения интерфейсов (ISP)

Сейчас мы визуализируем наши задачи в виде карточек. Давайте также добавим простой список задач в components/TodoRow.vue

TodoRow.vue

<template>
  <div class="todo-list__row">
    <span>{{ todo.id }}: </span>
    <span :class="{ 'todo-list__row--completed': todo.completed }">{{
      todo.title
    }}</span>
  </div>
</template>

<script lang="ts">
import { Component, Vue, Prop } from 'vue-property-decorator'
import { ITodo } from '@/types'

@Component
export default class Home extends Vue {
  @Prop({ required: true }) todo!: ITodo
}
</script>

<style lang="scss">
.todo-list {
  &__row {
    width: 100%;
    text-align: left;
    color: #4169e1;
    &--completed {
      text-decoration: line-through;
      color: #2e8b57;
    }
  }
}
</style>

и заменим визуализацию карточками на визуализацию списком

Home.vue

<template>
  <div>
    <Header listName="My new todo list" />
    <main>
      <TodoList>
        <TodoRow v-for="todo in todos" :key="todo.id" :todo="todo" />
      </TodoList>
    </main>
  </div>
</template>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'
import { ITodo } from '@/types'
import { Api } from '@/api/api'
import Header from '@/components/Header.vue'
import TodoList from '@/components/TodoList.vue'
import TodoCard from '@/components/TodoCard.vue'
import TodoRow from '@/components/TodoRow.vue'

@Component({
  components: { Header, TodoList, TodoCard, TodoRow }
})
export default class Home extends Vue {
  todos: ITodo[] = []
  async mounted() {
    this.todos = await this.fetchTodos()
  }
  async fetchTodos(): Promise<ITodo[]> {
    const api = new Api()
    return await api.fetch('todos')
  }
}
</script>
<style lang="scss">
.container {
  padding: 1.5rem;
}
</style>

Как вы можете заметить, мы отправляем весь объект todo как prop в TodoCard.vue и TodoRow.vue, но мы используем только часть этого объекта. Мы не используем свойство userId в обоих компонентах и id в TodoCard.vue. Мы нарушаем ISP: «Компоненты не должны зависеть от свойств и методов, которые они не используют».

Есть несколько способов исправить эту проблему:

  • Нарезать интерфейс Todo на несколько небольших интерфейсов
  • Передавать только используемые свойства компонентам

Давайте проведем рефакторинг нашего кода и используем функциональные (не сохраняющие состояние) компоненты.

Home.vue

<template>
  <div>
    <Header listName="My new todo list" />
    <main>
      <TodoList>
        <!--<TodoCard-->
        <!--v-for="{ id, title, completed } in todos"-->
        <!--:key="id"-->
        <!--:title="title"-->
        <!--:completed="completed"-->
        <!--/>-->
        <TodoRow
          v-for="{ id, title, completed } in todos"
          :key="id"
          :id="id"
          :title="title"
          :completed="completed"
        />
      </TodoList>
    </main>
  </div>
</template>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'
import { ITodo } from '@/types'
import { Api } from '@/api/api'
import Header from '@/components/Header.vue'
import TodoList from '@/components/TodoList.vue'
import TodoCard from '@/components/TodoCard.vue'
import TodoRow from '@/components/TodoRow.vue'

@Component({
  components: { Header, TodoList, TodoCard, TodoRow }
})
export default class Home extends Vue {
  todos: ITodo[] = []
  async mounted() {
    this.todos = await this.fetch()
  }
  async fetch(): Promise<ITodo[]> {
    const api = new Api()
    return await api.fetch('todos')
  }
}
</script>
<style lang="scss">
.container {
  padding: 1.5rem;
}
</style>

TodoCard.vue

<template functional>
  <div class="todo-list__task">
    <span :class="{ 'todo-list__task--completed': props.completed }">
      {{ props.title }}
    </span>
  </div>
</template>
<style lang="scss" scoped>
.todo-list {
  &__task {
    width: 24%;
    padding: 1.5rem;
    margin: 0.5%;
    text-align: left;
    color: #4169e1;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
    transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);

    &:hover {
      box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25),
        0 10px 10px rgba(0, 0, 0, 0.22);
    }

    &--completed {
      color: #2e8b57;
      text-decoration: line-through;
    }
  }
}
</style>

TodoRow.vue

<template functional>
  <div class="todo-list__row">
    <span>{{ props.id }}: </span>
    <span :class="{ 'todo-list__row--completed': props.completed }">{{
      props.title
    }}</span>
  </div>
</template>
<style lang="scss">
.todo-list {
  &__row {
    width: 100%;
    text-align: left;
    color: #4169e1;

    &--completed {
      text-decoration: line-through;
      color: #2e8b57;
    }
  }
}
</style>

Теперь все выглядит лучше.


Принцип инверсии зависимостей (DIP)

DIP говорит: «Классы (компоненты) высокого уровня не должны зависеть от классов (компонентов) низкого уровня. Оба должны зависеть от абстракций. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций ».

Что такое классы высокого и низкого уровня.

  • Классы низкого уровня реализуют базовые операции, такие как работа с API.
  • Классы высокого уровня содержат сложную бизнес-логику, которая заставляет классы низкого уровня что-то делать.

Давайте вернемся к нашему классу Api и создадим новый интерфейс для нашего класса Api в types.ts.

export interface IApi {
fetch(url: string): Promise<any>
}

и обновим все наши классы API в каталоге api и views/Home.vue.

api.ts

import { BaseApi } from '@/api/BaseApi'
import { AxiosApi } from '@/api/AxiosApi'
import { IApi } from '@/types'

export class Api extends BaseApi implements IApi {
  private provider: any = new AxiosApi()
  async fetch(url: string): Promise<any> {
    return await this.provider.fetch(url)
  }

AxiosApi.ts

import axios from 'axios'
import { BaseApi } from '@/api/BaseApi'
import { IApi } from '@/types'

export class AxiosApi extends BaseApi implements IApi {
  constructor() {
    super()
  }
  async fetch(url: string): Promise<any> {
    const { data } = await axios.get(`${this.baseUrl}${url}`)
    return data
  }
}

BaseApi.ts

import { IApi } from '@/types'

export class BaseApi implements IApi {
  protected baseUrl: string = 'https://jsonplaceholder.typicode.com/'
  async fetch(url: string): Promise<any> {}
}

FetchApi.ts

import { BaseApi } from '@/api/baseApi'
import { IApi } from '@/types'

export class FetchApi extends BaseApi implements IApi {
  constructor() {
    super()
  }
  async fetch(url: string): Promise<any> {
    const response = await fetch(`${this.baseUrl}${url}`)
    return await response.json()
  }
}

Home.vue

<template>
  <div>
    <Header listName="My new todo list" />
    <main>
      <TodoList>
        <!--<TodoCard-->
        <!--v-for="{ id, title, completed } in todos"-->
        <!--:key="id"-->
        <!--:title="title"-->
        <!--:completed="completed"-->
        <!--/>-->
        <TodoRow
          v-for="{ id, title, completed } in todos"
          :key="id"
          :id="id"
          :title="title"
          :completed="completed"
        />
      </TodoList>
    </main>
  </div>
</template>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'
import { ITodo, IApi } from '@/types'
import { Api } from '@/api/api'
import Header from '@/components/Header.vue'
import TodoList from '@/components/TodoList.vue'
import TodoCard from '@/components/TodoCard.vue'
import TodoRow from '@/components/TodoRow.vue'

@Component({
  components: { Header, TodoList, TodoCard, TodoRow }
})
export default class Home extends Vue implements IApi {
  todos: ITodo[] = []
  async mounted() {
    this.todos = await this.fetch()
  }
  async fetch(): Promise<ITodo[]> {
    const api = new Api()
    return await api.fetch('todos')
  }
}
</script>
<style lang="scss">
.container {
  padding: 1.5rem;
}
</style>

Теперь наши низкоуровневые (api классы) и высокоуровневые (views/Home.vue) классы зависят от одного интерфейса. Направление исходной зависимости было инвертировано: низкоуровневые классы API теперь зависят от высокоуровневой абстракции IApi.


Заключение

В этой статье мы рассмотрели все принципы SOLID в небольшом проекте Vue.JS. Я надеюсь, что это поможет вам избежать некоторых архитектурных ошибок в ваших проектах и улучшить ваше понимание принципов SOLID.

Код можно найти здесь  https://github.com/NovoManu/SOLID-vue

Удачи!

Оригинальная статья Manu Ustenko: How to avoid SOLID principles violations in Vue. JS application

Была ли вам полезна эта статья?
[10 / 4.9]

Spread the love
Editorial Team

View Comments

  • Хорошая статья, но не совсем понятно, зачем Home и провайдеры в Api (Axios и Fetch) реализуют IApi. Home не должен меняться, если в интерфейс IApi добавится какой-то метод. Он вообще не обязан иметь метод fetch, это просто совпадение. Axios и Fetch также не должны следовать интерфейсу для Api, у них вполне мог бы быть свой интерфейс (а'ля IApiProvider).

    Если я где-то не прав, поправьте меня, если не сложно.

  • Жаль, что на комменты не отвечают, а так рад что наткнулся на этот блог. Отличный перевод и статья актуальная!

    • Отвечают, когда есть что ответить. А так некоторые вопросы относятся скорее к авторам статей (оригинальный статей), поэтому иногда лучше напрямую у них спрашивать.

  • Пункт про ISP - полный бред. Каким образом компонент зависит от неиспользуемого свойства? Если в будущем это свойство понадобится, тогда что?

    • в будущем и добавишь, если оно понадобиться.

      • Круто! Так это нарушает принцип О. Мне нужно будет лезть во все родительские компоненты и прокидывать это свойство. Если мы передаём весь объект, это свойство лежит по ссылке и никому не мешает

Recent Posts

Vue 3.4 Новая механика v-model компонента

Краткий перевод: https://vuejs.org/guide/components/v-model.html Основное использование​ v-model используется для реализации двусторонней привязки в компоненте. Начиная с Vue…

11 месяцев ago

Анонс Vue 3.4

Сегодня мы рады объявить о выпуске Vue 3.4 «🏀 Slam Dunk»! Этот выпуск включает в…

11 месяцев ago

Как принудительно пере-отобразить (re-render) компонент Vue

Vue.js — это универсальный и адаптируемый фреймворк. Благодаря своей отличительной архитектуре и системе реактивности Vue…

2 года ago

Проблемы с установкой сертификата на nginix

Недавно, у меня истек сертификат и пришлось заказывать новый и затем устанавливать на хостинг с…

2 года ago

Введение в JavaScript Temporal API

Каким бы ни было ваше мнение о JavaScript, но всем известно, что работа с датами…

2 года ago

Когда и как выбирать между медиа запросами и контейнерными запросами

Все, кто следит за последними событиями в мире адаптивного дизайна, согласятся, что введение контейнерных запросов…

2 года ago