Как стать автором
Обновить

Гайд по миграции с Vue 2 на Vue 3. Часть 2

Время на прочтение15 мин
Количество просмотров10K
Автор оригинала: Andy Li

Эта статья — перевод оригинальной статьи Andy Li из Vue Mastery "Vue 3 Migration Changes: Replace, Rename, and Remove (Pt. 2)".

Также я веду телеграм канал “Frontend по-флотски”, где рассказываю про интересные вещи из мира разработки интерфейсов.

Вступление

Команда Vue недавно выпустила долгожданный билд миграции для Vue 3. Если вы думали об обновлении своего приложения с Vue 2 до Vue 3, это то, что вам нужно.

Процесс обновления приложения до последней версии фреймворка может оказаться непростой задачей. Эта серия статей создана, чтобы упростить этот процесс.

Серия миграции на Vue 3 состоит из двух частей:

  1. Билд для миграции на Vue 3

  2. Изменения для миграции на Vue (эта статья)

Если вы не знакомы с билдом для миграции, ознакомьтесь со статьей Билд для миграции на Vue 3, это предварительная подготовка для этой статьи. Если у вас нет приложения для переноса, вы все равно можете использовать эту статью, чтобы узнать, что изменилось в Vue 3. Но имейте в виду, что мы обсуждаем здесь только изменения, мы не будем углубляться в новые функции, такие как Composition API. (ознакомьтесь с курсом Vue Mastery's Composition API, если вам это интересно)

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

Процесс миграции

Это процесс использования билда миграции, который вы могли видеть в предыдущей статье:

В предыдущей статье мы исправили несколько простых устаревших функций, чтобы проиллюстрировать, как использовать билд миграции. Эта статья об устаревших функциях Vue 2.

Основываясь на описанном выше процессе миграции, функции, устаревшие в Vue 3, можно разделить на четыре категории: несовместимые, замененные, переименованные и удаленные.

Каждая категория будет содержать различные устаревшие фичи и соответствующие им стратегии миграции (то, что вам нужно сделать, чтобы ваш код снова работал на Vue 3).

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

Опять же, если вы не понимаете что здесь происходит, вы можете освежить свою память с помощью статьи Билд для миграции на Vue 3.

Без лишних слов, давайте начнем наше путешествие!

Несовместимые

Устаревшие фичи в этой категории фактически приведут к тому, что ваше приложение вообще не будет работать, даже если запущен билд миграции. Поэтому нам нужно исправить их, прежде всего.

Named и Scoped слоты (заменены)

Слоты и слоты с ограниченной областью видимости - это сложные темы, которые мы не будем рассматривать в этой статье. Но это руководство может помочь вам реорганизовать код для Vue 3, если вы уже используете <slot> в своих компонентах.

В Vue 2 вы можете создать именованный слот с ограниченной областью видимости следующим образом:

<ItemList>
  <template slot="heading" slot-scope="slotProps">
    <h1>My Heading for {{ slotProps.items.length }} items</h1>
  </template>
</ItemList>

(Это все еще работает в Vue 2.6, но считается устаревшим и больше не будет работать в Vue 3.)

В Vue 3 вам нужно будет изменить его на это:

<ItemList>
  <template v-slot:heading="slotProps">
    <h1>My Heading for {{ slotProps.items.length }} items</h1>
  </template>
</ItemList>

Изменения:

  • Использование v-slot вместо того, чтобы объединить вместе slot и slot-scope, чтобы сделать то же самое.

  • Если вам не нужен slotProps, вы можете просто указать атрибут v-slot: heading без значения.

INSTANCE_SCOPED_SLOTS

Кстати, если ваш код использует свойство $scopedSlots, его необходимо переименовать в $slots в Vue 3.

Functional атрибут (удален)

COMPILER_SFC_FUNCTIONAL

В Vue 2 вы можете создать функциональный компонент в однофайловом компоненте (SFC) следующим образом:

<template functional>
  <h1>{{ text }}</h1>
</template>

<script>
export default {
  props: ['text']
}
</script>

В Vue 3 вам придется удалить атрибут functional:

<template>
  <h1>{{ text }}</h1>
</template>

<script>
export default {
  props: ['text']
}
</script>

Таким образом, технически вы больше не можете создавать функциональные компоненты в формате SFC. Но поскольку преимущество функциональных компонентов в производительности во Vue 3 намного меньше, это в любом случае небольшая потеря. (Мы все еще можем создавать функциональные компоненты в Vue 3, но не в <template> в файле .vue. Подробнее об этом в разделе «Функциональные компоненты» ниже)

Mounted Container

GLOBAL_MOUNT_CONTAINER

Vue 3 не заменяет элемент, к которому подключено ваше приложение, поэтому вы можете увидеть два div с id="app" в обработанном HTML.

Чтобы избежать дублирования стилей, вам придется удалить id="app" из одного из двух тегов <div>

v-if и v-for

COMPILER_V_IF_V_FOR_PRECEDENCE

Если вы используете v-if и v-for вместе в одном элементе, вам придется реорганизовать свой код.

Поскольку настройка Vue CLI ESLint по умолчанию фактически не позволит вам использовать v-if и v-for вместе в одном элементе даже в приложении Vue 2, маловероятно, что у вас действительно есть такой код.

Но в том случае, если он есть, то вот что изменилось.

В Vue 2 v-for имеет приоритет над v-if, а в Vue 3 v-if имеет приоритет над v-for.

Итак, код на Vue 2, который отображает числа меньше 10 выглядит так:

<ul>
  <li v-for="num in nums" v-if="num < 10">{{ num }}</li>
</ul>

В Vue 3 вам нужно написать это так:

<ul>
  <li v-for="num in numsLower10">{{ num }}</li>
</ul>

numsLower10 должно быть вычисляемым свойством.

v-if branch keys

COMPILER_V_IF_SAME_KEY

Если у вас есть один и тот же ключ для нескольких ветвей одного и того же условного v-if:

<ul>
  <li v-for="num in nums">
    <span v-if="num < 10" :key="myKey">{{ num }}</span>
    <span v-else class="high" :key="myKey">{{ num }}</span>
  </li>
</ul>

В Vue 3 вам придется удалить их (или назначить им разные ключи):

<ul>
  <li v-for="num in nums">
    <span v-if="num < 10">{{ num }}</span>
    <span v-else class="high">{{ num }}</span>
  </li>
</ul>

Vue автоматически назначит им уникальные ключи.

v-for key

COMPILER_V_FOR_TEMPLATE_KEY_PLACEMENT

Если вы используете v-for на <template> с :key на внутренних элементах:

<template v-for="num in nums">
  <div :key="num.id">{{ num }}</div>
</template>

Тогда в Vue 3 вам нужно будет поместить :key в <template>

<template v-for="num in nums" :key="num.id">
  <div>{{ num }}</div>
</template>

Transition classes (переименованы)

Если вы используете элемент <Transition> для анимации, вам придется переименовать имена классов для v-enter и v-leave:

  • v-enter => v-enter-from

  • v-leave => v-leave-from

(Это изменение немного особенное, потому что билд миграции не предупредит вас об этом.)

Теперь мы можем перейти к другим, менее неизбежным отказам от поддержки. Вы можете работать с ними в любом порядке.

Заменённые

Устаревшие фичи в этой категории удаляются, но заменяются новыми функциями в качестве решений.

Инициализация приложения (заменено)

GLOBAL_MOUNT / GLOBAL_EXTEND / GLOBAL_PROTOTYPE / GLOBAL_OBSERVABLE / GLOBAL_PRIVATE_UTIL

Ваш файл Vue 2 main.js может выглядеть примерно так:

import Vue from "vue" // import an object
import App from './App.vue'
import router from './router'
import store from './store'

Vue.use(store)
Vue.use(router)

Vue.component('my-heading', {
  props: [ 'text' ],
  template: '<h1>{{ text }}</h1>'
})

// create an instance using the new keyword
const app = new Vue(App)

app.$mount("#app");

В Vue 3 вам нужно будет изменить его на это:

import { createApp } from 'vue' // import a function
import App from './App.vue'
import router from './router'
import store from './store'

// create an instance using the function
const app = createApp(App)

app.use(store)
app.use(router)

app.component('my-heading', {
  props: [ 'text' ],
  template: '<h1>{{ text }}</h1>'
})

// no dollar sign
app.mount('#app')

Изменения:

  • Больше нет импорта Vue из пакета vue. Мы должны использовать новую функцию createApp для создания экземпляра приложения.

  • У метода mount нет знака доллара.

  • Вместо использования таких функций, как Vue.use и Vue.component, которые повлияли бы на поведение Vue глобально, теперь мы должны использовать эквивалентные методы экземпляра, такие как app.use и app.component.

Вот список изменений от старого Global API к новому Instance API:

  • Vue.component ⇒app.component

  • Vue.use ⇒ app.use

  • Vue.config ⇒ app.config

  • Vue.directive ⇒ app.directive

  • Vue.mixin ⇒ app.mixin

  • Vue.prototype ⇒ Vue.config.globalProperties

  • Vue.extend ⇒ (nothing)

  • Vue.util ⇒ (nothing)

Vue.extend удален. Поскольку экземпляр приложения больше не создается с помощью ключевого слова new и конструктора Vue, нет необходимости создавать конструктор подкласса путем наследования базового конструктора Vue с помощью функции extend.

Хотя Vue.util все еще существует, но теперь он приватный, поэтому вы тоже не сможете его использовать.

Vue.config.ignoredElements заменяется на app.config.compilerOptions.isCustomElement, который должен быть функцией вместо массива. И Vue.config.productionTip удален.

Функциональный компонент (заменен)

COMPONENT_FUNCTIONAL

Без использования <template> функциональный компонент Vue 2 можно создать следующим образом:

export default {
  functional: true,
  props: ['text'],
  render(h, { props }) {
    return h(`h1`, {}, props.text)
  }
}

В Vue 3 вам нужно будет изменить его на это:

import { h } from 'vue'

const Heading = (props) => {
  return h('h1', {}, props.text)
}

Heading.props = ['text']

export default Heading

Изменения:

  • Функциональный компонент должен быть функцией, а не параметром.

  • Хотя это и не является особенной темой функционального компонента, функция h в Vue 3 должна быть импортирована из пакета vue, а не передаваться в качестве параметра функции рендеринга.

v-for References (заменено)

V_FOR_REF

Если вы используете ref на элементе v-for для взятия всех ссылок HTML-элементов, к которым вы сможете получить доступ позже через this.$Refs.myNodes:

<template>
  <ul>
    <li v-for="item in items" :key="item.id" ref="myNodes">
      ...
    </li>
  </ul>
</template>

// later

...
mounted () {
  console.log(this.$refs.myNodes) // list of HTML element nodes
}

В Vue 3 вам нужно будет использовать :ref (с двоеточием) для привязки к коллбэк функции:

<template>
  <ul>
    <li v-for="item in items" :key="item.id" :ref="setNode">
      ...
    </li>
  </ul>
</template>

// later

...
data() {
  return {
    myNodes: [] // create an array to hold the nodes
  }
},
beforeUpdate() {
  this.myNodes = [] // reset empty before each update
},
methods: {
  setNode(el) { // this will be called automatically
    this.myNodes.push(el) // add the node
  }
},
updated() {
  console.log(this.myNodes) // finally, a list of HTML node references 
},

Здесь мы используем коллбэк функцию для добавления каждого узла в массив во время рендеринга. В конце у вас будет this.myNodes в качестве замены для this.$Refs.myNodes.

Native event (заменён)

COMPILER_V_ON_NATIVE

Чтобы проиллюстрировать проблему, допустим, у нас есть такой компонент SpecialButton:

<template>
  <div>
    <button>Special Button</button>
  </div>
</template>

Когда вы используете этот компонент (в родительском компоненте), давайте также предположим, что вы хотите добавить собственное событие click к элементу <div>, вы бы сделали это так (в Vue 2):

<SpecialButton v-on:click.native="foo" />

В Vue 3 модификатор native удален, поэтому приведенный выше код не будет работать.

Итак, как нам добавить нативное событие click к элементу <div>, расположенному в нашем компоненте SpecialButton?

Нужно будет удалить модификатор native, и он снова заработает:

<SpecialButton v-on:click="foo" />

Это устаревание было легко исправлено, но из-за этого возникла новая проблема.

В Vue 3 все события, прикрепленные к компоненту, будут обрабатываться как нативные события и будут добавляться к корневому элементу этого компонента. Это поведение по умолчанию, и поэтому нам больше не нужен модификатор native.

Но что, если событие, которое мы добавляем, не предназначено для использования в качестве нативного?

Например, мы хотим, чтобы SpecialButton генерировал кастомное событие:

<template>
  <div>
    <button v-on:click="$emit('special-click')">Special Button</button>
  </div>
</template>

В родительском компоненте мы должны слушать событие следующим образом:

<SpecialButton v-on:click="foo" v-on:special-click="bar" />

Как и событие click, кастомное событие click по умолчанию будет прикреплено к корневому элементу (<div>) компонента SpecialButton, что не является тем, чего мы хотим добиться. Хотя кастомное событие click по-прежнему будет генерироваться, проблема в том, что это же событие также присоединяется к элементу как нативное событие.

Сейчас это не кажется большой проблемой, так как кастомное событие click никогда не будет запускаться на <div>. Но это может быть проблемой, если настраиваемое событие называется click или любым именем, которое также является нативным событием. В этом случае один щелчок мыши вызовет несколько событий click. (один на <button>, другой ошибочно на <div>)

Решением этого является новый способ оповещения в Vue 3.

С помощью параметра emits мы можем настроить так как нам необходимо, чтобы наше событие не было ошибочно принято за нативное событие и добавлено в корневой элемент.

Итак, если мы укажем все пользовательские события (в нашем случае только одно), которые будут генерироваться в SpecialButton:

export default {
  name: 'SpecialButton',
  emits: ['special-click'] // ADD
}

Vue будет знать, что это кастомное событие, и не будет прикреплять наше событие click к корневому элементу как нативное событие.

Поэтому, если вы используете модификатор .native, вам придется его удалить. А чтобы предотвратить добавление непреднамеренных событий к корневому элементу, вам придется использовать emits для документирования всех настраиваемых событий, которые может генерировать компонент.

Переименованные

Устаревания в этой категории самые тривиальные. Все, что вам нужно сделать, это изменить старые имена на новые.

v-model prop and event (переименованные)

COMPONENT_V_MODEL

Если вы используете v-model в компоненте, вам придется переименовать этот prop и событие:

  • value ⇒ modelValue

  • $emit("input") ⇒ $emit("update:modelValue")

(Не забудьте указать название события в параметре emits, как упоминалось ранее)

Lifecycle hooks (переименованные)

OPTIONS_BEFORE_DESTROY / OPTIONS_BEFORE_DESTROY

Если вы используете хуки жизненного цикла beforeDestroy и destroy, вам придется переименовать их:

  • beforeDestroy ⇒ beforeUnmount

  • destroyed ⇒ unmounted

(Никаких изменений в других хуках)

Lifecycle events (переименованные)

INSTANCE_EVENT_HOOKS

Если вы отслеживаете события жизненного цикла компонента в его родительском компоненте:

<template>
  <MyComponent @hook:mounted="foo">
</template>

В Vue 3 вам придется переименовать префикс атрибута с @hook: на @vnode-:

<template>
  <MyComponent @vnode-mounted="foo">
</template>

Как упоминалось в предыдущем разделе, beforeDestroy и destroy были переименованы. Поэтому, если вы используете @hook:beforeDestroy и @hook:destroy, вам придется вместо этого переименовать их в @vnode-beforeMount и @vode-unmounted.

Custom directive hooks (переименованные)

CUSTOM_DIR

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

  • bind ⇒ beforeMount

  • inserted ⇒ mounted

  • componentUpdated ⇒ updated

  • unbind ⇒ unmounted

Если вы используете update хук, он был удалён в Vue 3, поэтому вам нужно переместить код оттуда в updated хук.

Удалённые

Эта категория посвящена удаленным фичам. Для некоторых из них нам просто нужно будет прекратить использовать их в нашем коде, а для других нужно будет найти обходные пути.

Reactive property setters (удалённые)

GLOBAL_SET / GLOBAL_DELETE / INSTANCE_SET / INSTANCE_SET

Vue 3 был переписан с новой системой реактивности, основанной на технологиях ES6, поэтому нет необходимости делать отдельные свойства реактивными. В результате Vue 3 больше не предлагает следующие API, поэтому вам придется их удалить:

  • Vue.set

  • Vue.delete

  • vm.$set

  • vm.$delete

(vm ссылается на экземпляр Vue)

vm.$children (удалённые)

INSTANCE_CHILDREN

Если вы используете this.$children в своем компоненте для доступа к дочернему компоненту:

<template>
  <AnotherComponent>Hello World</AnotherComponent>
</template>

...
mounted() {
  console.log(this.$children[0])
},

В Vue 3 вам придется использовать атрибут ref вместе со свойством this.$refs в качестве обходного пути.

Вкратце, если вы установите ref с именем дочернего компонента:

<template>
  <AnotherComponent ref="hello">Hello World</AnotherComponent>
</template>

Вы сможете получить к нему доступ, используя свойство $refs в своем JavaScript коде:

mounted() {
  console.log(this.$refs.hello)
}

vm.$listeners (удалённые)

INSTANCE_LISTENERS

Если вы используете this.$listeners для доступа к обработчикам событий, переданных из родительского компонента:

// Parent component
<MyComponent v-on:click="foo" v-on:mouseenter="bar" />

// Child component
mounted() {
  console.log(this.$listeners)
}

В Vue 3 вам придется обращаться к ним отдельно через свойство $attrs:

mounted() {
  console.log(this.$attrs.onClick)
  console.log(this.$attrs.onMouseenter)
}

vm.$on, vm.$off, vm.$once (удалённые)

INSTANCE_EVENT_EMITTER

Это может повлиять на ваш проект, если вы использовали vm.$on, vm.$off или vm.$once как часть механизма для PubSub. Вам придется удалить эти методы экземпляра и использовать для этого другую библиотеку.

В качестве потенциального решения обратите внимание на сторонний инструмент под названием tiny-emitter.

Filters (удалённые)

FILTERS

Если вы используете фильтры (синтаксис вертикальной черты) в своем шаблоне:

<template>
  <p>{{ num | roundDown }}</p>
</template>

...
filters: {
  roundDown(value) {
    return Math.floor(value)
  }
},

В Vue 3 вам придется вместо этого использовать простое вычисляемое свойство:

</template>

...
computed: {
  numRoundedDown() {
    return Math.floor(this.num)
  }
},

Обоснование удаления фильтров состоит в том, что используемый синтаксис не соответствует реальному поведению JavaScript (вертикальная черта должна быть побитовым оператором в JavaScript). Большинство вещей, которые мы помещаем в двойные фигурные скобки, являются настоящим JavaScript, поэтому было бы ошибкой, если бы этого не было.

Еще одно преимущество вычисляемого свойства над фильтром состоит в том, что код шаблона будет чище.

is attribute (удалённые)

COMPILER_IS_ON_ELEMENT

В Vue 2 вы можете применить атрибут is к нативному элементу, чтобы отобразить его как компонент:

<button is="SpecialButton"></button>

В Vue 3 вам нужно будет заменить нативный элемент на <component>, чтобы получить такое же поведение:

<component is="SpecialButton"></component>

Если вы не используете формат Single File Component, вы можете просто добавить префикс vue:

<button is="vue:SpecialButton"></button>

Keyboard codes (удалённые)

V_ON_KEYCODE_MODIFIER

В Vue 2 вы можете прослушивать события на определенных клавишах на клавиатуре с соответствующими кодами клавиш:

<input type="text" v-on:keyup.112="validateText" />

(112 представляет собой клавишу Enter)

В Vue 3 вам придется вместо этого использовать название:

<input type="text" v-on:keyup.enter="validateText" />

Всякое разное

В этой категории есть все устаревания, которые не соответствуют трем основным категориям (заменить, переименовать и удалить).

v-bind порядок

COMPILER_V_BIND_OBJECT_ORDER

v-bind теперь чувствителен к порядку.

Если вы используете v-bind="object" и ожидаете, что один или несколько других атрибутов переопределяют свойства объекта, вам необходимо переместить атрибут v-bind перед всеми другими указанными атрибутами.

Допустим, у вас было это:

<Foo a="1" b="2" v-bind:"{ a: 100, b: 200 }">

Свойства объекта будут переопределены двумя другими атрибутами, поскольку у них одинаковые имена a и b.

В Vue 3 вам нужно будет поставить v-bind перед другими атрибутами, чтобы добиться того же «перекрывающего» эффекта:

<Foo v-bind:"{ a: 100, b: 200 }" a="1" b="2">

v-bind модификатор sync

COMPILER_V_BIND_SYNC

Если вы используете v-bind с модификатором sync:

<MyComponent v-bind:title.sync="myString" />

В Vue 3 вам придется вместо этого использовать v-model:

<MyComponent v-model:title="myString" />

v-model был улучшен в Vue 3. Теперь вы можете передать аргумент для v-model, как в это с title выше, и вы даже можете иметь несколько v-model.

Но ваш линтер может выдать ошибку об этом новом способе использования v-model:

Чтобы исправить это, вам нужно добавить правило в свой package.json, чтобы отключить его:

"parserOptions": {
      "parser": "babel-eslint"
  },
  "rules": {
    "vue/no-v-model-argument": "off" // ADD
  }

(делайте это только в том случае, если ваш линтер выдает ошибку)

Async Component

COMPONENT_ASYNC

Асинхронный компонент в Vue 2 выглядит так:

const MyComponent = {
  component: () => import('./MyComponent.vue'),
  ...
}

В Vue 3 вам нужно будет изменить его на это:

import { defineAsyncComponent } from 'vue'

const MyComponent = defineAsyncComponent({
  loader: () => import('./MyComponent.vue'),
  ...
})

Изменения:

  • Использование новой функции defineAsyncComponent.

  • Название поля component изменено на loader.

Если ваш асинхронный компонент является просто функцией, он также должен быть заключен в defineAsyncComponent:

// Before
const MyComponent = () => import('./MyComponent.vue')

// After
const MyComponent = defineAsyncComponent(() => import('./MyComponent.vue'))

watch array deep

WATCH_ARRAY

Если вы используете watch для наблюдения за массивом, вы должны передать параметр deep:

watch: {
  items: {
    handler(val, oldVal) {
      console.log(oldVal + ' --> ' + val)
    },
    deep: true // ADD
  }
},

false в атрибутах

ATTR_FALSE_VALUE / ATTR_ENUMERATED_COERSION

Если вы используете false для удаления атрибута, который не является boolean:

// template
<img src="..." :alt="false" />

// rendered
<img src="..." />

В Vue 3 вместо этого нужно использовать null:

// template
<img src="..." :alt="null" />

// rendered
<img src="..." />

Сделав его false в Vue 3, он просто отобразит false в HTML.

Если вы используете так называемые «перечисляемые атрибуты», такие как draggable и проверка орфографии, они также подчиняются указанным выше правилам в Vue 3: false для установки false, null для удаления.

class и style

INSTANCE_ATTRS_CLASS_STYLE

В Vue 3 class и style включены в $attrs, поэтому вы можете столкнуться с некоторыми «сбоями», если ваш код ожидает, что class и style не будут частью $attrs.

В частности, если ваш код использует inheritAttrs: false в компоненте, в Vue 2 class и style по-прежнему будут передаваться корневому элементу этого компонента, поскольку они не являются частью $attrs, но в Vue 3 class и style больше не будет передаваться в корень, поскольку они являются частью $attrs.

Vuex и Vue Router

Если вы используете Vuex и Vue router, вам необходимо обновить их до Vuex 4 и Vue Router 4.

"dependencies": {
  "vuex": "^4.0.0",
  "vue-router": "^4.0.0",
  ...
}

Подобно Vue 3, Vuex 4 и Vue Router 4 также изменили свои глобальные API. Теперь вам нужно использовать createStore и createRouter так же, как и createApp:

import { createStore } from 'vuex'
import { createRouter } from 'vue-router'

const store = createStore({
  state: {...},
  mutations: {...},
  actions: {...},
})

const router = createRouter({
  routes: [...]
})

Ещё

Есть еще несколько устаревших штук, которые не рассматриваются здесь, потому что они либо слишком тривиальны, чтобы повлиять на что-либо, либо просто очень необычны. Но с приложением, запущенным в билде миграции, если вы когда-нибудь столкнетесь с каким-либо предупреждением, которое не было упомянуто здесь, вы можете поискать флаг предупреждения в Google и прочитать страницу документации об этом.

Как упоминалось ранее, мы также создали шпаргалку по некоторым устаревшим функциям, описанным в этой статье.

Теги:
Хабы:
+4
Комментарии6

Публикации

Истории

Работа

Ближайшие события