- Оригинальный адрес:Build a state management system with vanilla JavaScript
- Оригинальный автор:ANDY BELL
- Перевод с:Программа перевода самородков
- Постоянная ссылка на эту статью:GitHub.com/rare earth/gold-no…
- Переводчик:Shery
- Корректор:IridescentMia coconilu
Управление состоянием не является чем-то новым в программном обеспечении, но все еще является относительно новым в приложениях, созданных с помощью JavaScript. Традиционно мы сохраняли состояние непосредственно в DOM или даже присваивали его глобальному объекту в окне. Но теперь у нас есть много вариантов, эти библиотеки и фреймворки могут помочь нам управлять состоянием. Такие библиотеки, как Redux, MobX и Vuex, упрощают управление межкомпонентным состоянием. Это значительно улучшает масштабируемость приложения и очень полезно для реактивных сред, ориентированных на состояние, таких как React или Vue.
Как работают эти библиотеки? Что, если мы напишем собственное управление состоянием? Оказывается, это очень просто и дает возможность изучить некоторые очень распространенные шаблоны проектирования, изучая некоторые современные API, которые одновременно полезны и полезны.
Прежде чем мы начнем, убедитесь, что у вас есть средний уровень знаний JavaScript. Вы должны знать о типах данных и, в идеале, о некоторых более современных функциях синтаксиса ES6+. если нет,это может тебе помочь. Стоит отметить, что я не говорю, что вы должны использовать это вместо Redux или MobX. Мы разрабатываем вместе небольшой проект, чтобы повысить свои навыки, эй, если вас волнует размер файла JavaScript, так что он может справиться с действительно небольшим приложением.
начиная
Прежде чем мы углубимся в код, давайте взглянем на то, что мы разрабатываем. Это «контрольный список выполненных дел», в котором резюмируется то, что вы сделали сегодня. Он волшебным образом обновит различные элементы пользовательского интерфейса, не полагаясь на фреймворк. Но это не совсем магия. За кулисами у нас уже есть небольшая система состояний, которая ожидает инструкций и поддерживает единый источник данных предсказуемым образом.
Круто, правда? Сначала займемся настройкой. Я собрал несколько шаблонов, чтобы этот урок был простым и увлекательным. Первое, что вам нужно сделать, этоКлонируйте его с GitHub,илиЗагрузите и распакуйте его ZIP-файл.
После того, как вы загрузили шаблон, вам нужно запустить его на локальном веб-сервере. мне нравится использоватьhttp-serverpackage для выполнения этих задач, но вы также можете использовать все, что захотите. Когда вы запустите его локально, вы увидите что-то вроде этого:
Начальное состояние нашего шаблона.
Установить структуру проекта
Откройте корневой каталог с помощью вашего любимого текстового редактора. На этот раз для меня корневой каталог был:
~/Documents/Projects/vanilla-js-state-management-boilerplate/
Вы должны увидеть такую структуру:
/src
├── .eslintrc
├── .gitignore
├── LICENSE
└── README.md
Опубликовать/подписаться
Далее откройтеsrc
папку, а затем введитеjs
папка. Создатьlib
новая папка. Внутри создайте файл с именемpubsub.js
новый файл.
твойjs
Структура каталогов должна выглядеть так:
/js
├── lib
└── pubsub.js
Потому что мы собираемся создать небольшойШаблон Pub/Sub (шаблон pub/sub), поэтому, пожалуйста, откройтеpubsub.js
. Мы создаем функциональность, позволяющую другим частям приложения подписываться на именованные события. Затем другая часть приложения может публиковать эти события, обычно с некоторой связанной полезной нагрузкой.
Pub/Sub иногда сложно освоить, так что насчет примера? Предположим, вы работаете в ресторане, и ваши клиенты заказывают закуску и основное блюдо. Если вы когда-либо работали на кухне, вы знаете, что, когда официанты убирают закуски, они сообщают шеф-повару, с какого стола убрана закуска. Вот подсказка к блюду за этим столом. На большой кухне может быть несколько поваров, готовящих разные блюда. Они всеподпискаНапоминание официанта о том, что клиент доел закуску, чтобы они сами знали, что делатьприготовить основное блюдо. Итак, у вас есть несколько шеф-поваров, подписанных на одно и то же приглашение (именованное событие) и выполняющих разные действия (обратные вызовы) при появлении запроса.
Надеюсь, это поможет понять. давай продолжим!
Режим PubSub просматривает все подписки и запускает их обратные вызовы при передаче соответствующей полезной нагрузки. Это отличный способ создать очень элегантный реактивный поток для вашего приложения, мы можем сделать это всего несколькими строками кода.
Добавьте следующее вpubsub.js
:
export default class PubSub {
constructor() {
this.events = {};
}
}
У нас есть совершенно новый класс, и мы будемthis.events
По умолчанию это пустой объект.this.events
Объект будет содержать наши именованные события.
После закрывающей скобки функции-конструктора добавьте следующее:
subscribe(event, callback) {
let self = this;
if(!self.events.hasOwnProperty(event)) {
self.events[event] = [];
}
return self.events[event].push(callback);
}
Это наш метод подписки. вы передаете уникальную строкуevent
作为事件名,以及该事件的回调函数。 если нашevents
Набор событий тоже не совпал, поэтому используем его для создания пустого массива, чтобы в дальнейшем не приходилось проверять его тип. Затем мы добавим обратный вызов в коллекцию. Если он уже существует, он напрямую добавит обратный вызов в коллекцию. Возвращаем длину множества событий, это событие для тех, кто хочет знать, сколько оно будет существовать удобнее.
Теперь, когда у нас есть метод подписки, угадайте, что мы будем делать дальше? Тебе известно:publish
метод. Добавьте следующее после вашего метода подписки:
publish(event, data = {}) {
let self = this;
if(!self.events.hasOwnProperty(event)) {
return [];
}
return self.events[event].map(callback => callback(data));
}
Метод сначала проверяет, существует ли входящее событие в нашей коллекции событий. Если нет, мы возвращаем пустой массив. Нет саспенса. Если есть событие, мы перебираем каждый сохраненный обратный вызов и передаем ему данные. Если обратного вызова нет (чего быть не должно), все в порядке, потому что мы находимся вsubscribe
Событие создается с пустым массивом в методе.
Это шаблон PubSub. Переходим к следующей части!
Магазин объекта (ядро)
Теперь у нас есть модуль Pub/Sub, а класс Store, основной модуль нашего апплета, имеет свою единственную зависимость. Теперь приступаем к совершенствованию.
Давайте сначала обрисуем в общих чертах, что это делает.
Магазин является нашим основным объектом. всякий раз, когда вы видите@import store from'../lib/store.js
, вы импортируете объект, который мы собираемся написать. он будет содержатьstate
объект, который, в свою очередь, содержит состояние нашего приложения,commit
метод, который вызовет наш>mutations,последнийdispatch
функция вызовет нашactions. в этом приложении иStore
Между ядрами объектов будет прокси-система, которая будет использовать нашуPubSub
Модули отслеживают и транслируют изменения состояния.
первый вjs
Создайте каталог с именемstore
новый каталог. Там создайте файл с именемstore.js
новый файл. теперь твойjs
Каталог должен выглядеть так:
/js
└── lib
└── pubsub.js
└──store
└── store.js
Открытьstore.js
и импортируйте наш модуль Pub/Sub. Для этого добавьте в начало файла следующее:
import PubSub from '../lib/pubsub.js';
Это будет хорошо знакомо тем, кто регулярно использует ES6. Тем не менее, запуск такого кода без инструмента упаковки может быть трудно распознан браузером. Для этого метода многоПоддержка браузера!
Далее приступим к созданию наших объектов. После импорта файла добавьте следующее непосредственно вstore.js
:
export default class Store {
constructor(params) {
let self = this;
}
}
С первого взгляда все очевидно, так что давайте добавим следующий пункт. мы будемstate
,actions
а такжеmutations
Добавьте объекты по умолчанию. Мы также добавилиstatus
свойство, которое мы будем использовать для определения того, что объект делает в любой момент времени. Это вlet self = this;
Назад:
self.actions = {};
self.mutations = {};
self.state = {};
self.status = 'resting';
После этого мы создадим новыйPubSub
например, он будет использоваться какstore
изevents
Стоимость имущества:
self.events = new PubSub();
Далее будем искать входящиеparams
объект, чтобы увидеть, были ли какие-либо переданы вactions
илиmutation
. при создании экземпляраStore
объект, мы можем передать объект данных. которая включает в себяactions
а такжеmutation
Коллекция , которые контролируют поток данных в нашем магазине. Добавьте следующий код после последней добавленной строки кода:
if(params.hasOwnProperty('actions')) {
self.actions = params.actions;
}
if(params.hasOwnProperty('mutations')) {
self.mutations = params.mutations;
}
Это все наши настройки по умолчанию и почти все возможные настройки параметров. Давайте посмотрим на нашуStore
Как объект отслеживает все изменения. мы будем использоватьПроксидля завершения этой операции. Работа, выполняемая прокси, в основном заключается в проксировании объекта состояния. Если мы добавимget
Intercept, мы можем отслеживать каждый раз, когда запрашиваем данные у объекта. а такжеset
Подобно методам перехвата, мы можем следить за изменениями, внесенными в объекты. Это основная часть, которая нас сегодня интересует. Добавьте следующее после последней строки кода, которую вы добавили, и мы обсудим, что она делает:
self.state = new Proxy((params.state || {}), {
set: function(state, key, value) {
state[key] = value;
console.log(`stateChange: ${key}: ${value}`);
self.events.publish('stateChange', self.state);
if(self.status !== 'mutation') {
console.warn(`You should use a mutation to set ${key}`);
}
self.status = 'resting';
return true;
}
});
Эта часть кода говорит, что мы захватываем объект состоянияset
работать. Это означает, что при запуске мутации что-то вродеstate.name ='Foo'
, этот перехватчик перехватывает его до того, как он будет установлен, и дает нам возможность обработать изменение или даже полностью его отклонить. Но в нашем контексте мы установим изменение, а затем зарегистрируем его в консоли. Затем мы используемPubSub
модуль публикуетstateChange
мероприятие. Будут вызваны любые обратные вызовы, подписанные на это событие. Наконец, мы проверяемStore
положение дел. если это не в настоящее времяmutation
, это может означать, что состояние было обновлено вручную. Мы добавили небольшое предупреждение в консоль, чтобы дать разработчикам некоторые подсказки.
Здесь много всего происходит, но я надеюсь, что вы, ребята, начнете понимать, как все это сочетается друг с другом, и, что важно, как мы можем централизованно поддерживать состояние, благодаря прокси и Pub/Sub.
Отправка и фиксация
Теперь мы добавилиStore
Основная часть, давайте добавим два метода. Один из них позвонит намactions
изdispatch
Другой позвонит намmutation
изcommit
. Давайте начнем сdispatch
начать сstore.js
серединаconstructor
Затем добавьте этот метод:
dispatch(actionKey, payload) {
let self = this;
if(typeof self.actions[actionKey] !== 'function') {
console.error(`Action "${actionKey} doesn't exist.`);
return false;
}
console.groupCollapsed(`ACTION: ${actionKey}`);
self.status = 'action';
self.actions[actionKey](self, payload);
console.groupEnd();
return true;
}
Процесс здесь таков: найдите действие, если оно существует, установите состояние и вызовите действие, а также создайте группы журналов, чтобы все наши журналы были красивыми и чистыми. Все, что регистрируется (например, журналы мутаций или прокси-серверов), останется в группе, которую мы определили. Если действие не задано, оно зарегистрирует ошибку и вернет false. Это очень просто, иcommit
Метод более простой.
существуетdispatch
После метода добавьте:
commit(mutationKey, payload) {
let self = this;
if(typeof self.mutations[mutationKey] !== 'function') {
console.log(`Mutation "${mutationKey}" doesn't exist`);
return false;
}
self.status = 'mutation';
let newState = self.mutations[mutationKey](self.state, payload);
self.state = Object.assign(self.state, newState);
return true;
}
Подход очень похож, но мы все равно должны понимать процесс сами. Если мутацию можно найти, мы запускаем ее и получаем новое состояние из ее возвращаемого значения. Затем мы объединяем новое состояние с существующим состоянием, чтобы создать нашу последнюю версию состояния.
После добавления этих методов нашStore
Объект почти готов. Теперь вы можете разделить приложение на модули, если хотите, так как мы добавили большую часть необходимой нам функциональности. Вы также можете добавить несколько тестов, чтобы убедиться, что все работает как положено. Я не закончу эту статью так. Давайте реализуем задуманное и продолжим улучшать наше маленькое приложение!
Создание базовых компонентов
Для связи с нашим магазином у нас есть три основных области, которые обновляются независимо в зависимости от того, что в них хранится. Мы будем перечислять отправленные элементы, визуальный подсчет этих элементов и еще один, который визуально скрыт, чтобы предоставить более точную информацию для программ чтения с экрана. Все они делают разные вещи, но все они извлекают выгоду из чего-то общего для управления своим локальным состоянием. Мы собираемся создать базовый класс компонента!
Во-первых, давайте создадим файл. существуетlib
каталог, продолжайте и создайте файл с именемcomponent.js
документ. Мой путь к файлу:
~/Documents/Projects/vanilla-js-state-management-boilerplate/src/js/lib/component.js
После создания файла откройте его и добавьте следующее:
import Store from '../store/store.js';
export default class Component {
constructor(props = {}) {
let self = this;
this.render = this.render || function() {};
if(props.store instanceof Store) {
props.store.events.subscribe('stateChange', () => self.render());
}
if(props.hasOwnProperty('element')) {
this.element = props.element;
}
}
}
Давайте поговорим об этом коде. Во-первых, мы должны импортироватьStore
Добрый. Это не потому, что нам нужен его экземпляр, это больше для проверкиconstructor
атрибут в. Говоря о которой, вconstructor
Посмотрим, есть ли у нас метод рендеринга. если этоComponent
class является родительским классом другого класса, тогда он может бытьrender
Установите свой собственный метод. Если заданного метода нет, мы создаем пустой метод, чтобы избежать ошибок.
После этого, как было сказано выше, мыStore
класс проверить. Мы делаем это, чтобы обеспечитьstore
собственность - этоStore
Экземпляр класса, поэтому мы можем уверенно использовать его методы и свойства. То есть так, подписываемся на общую ситуациюstateChange
событие, так что наш объект может сделатьОтзывчивый. Вызывается каждый раз при изменении состоянияrender
функция.
Это все, что нам нужно написать для этого класса. он будет использоваться как другой класс компонентаextend
родительский класс. Давай сделаем это вместе!
Создадим наш компонент
Как я уже говорил, мы собираемся завершить три компонента, каждый из которых проходит черезextend
ключевое слово, унаследованное от базового классаComponent
. Начнем с самого большого компонента: списка проектов!
В твоемjs
каталог, создайте файл с именемcomponents
, затем создайте новую папку с именемlist.js
новый файл. Мой путь к файлу:
~/Documents/Projects/vanilla-js-state-management-boilerplate/src/js/components/list.js
Откройте этот файл и вставьте весь этот код:
import Component from '../lib/component.js';
import store from '../store/index.js';
export default class List extends Component {
constructor() {
super({
store,
element: document.querySelector('.js-items')
});
}
render() {
let self = this;
if(store.state.items.length === 0) {
self.element.innerHTML = `<p class="no-items">You've done nothing yet 😢</p>`;
return;
}
self.element.innerHTML = `
<ul class="app__items">
${store.state.items.map(item => {
return `
<li>${item}<button aria-label="Delete this item">×</button></li>
`
}).join('')}
</ul>
`;
self.element.querySelectorAll('button').forEach((button, index) => {
button.addEventListener('click', () => {
store.dispatch('clearItem', { index });
});
});
}
};
Я надеюсь, что из предыдущего урока значение этого кода стало для вас самоочевидным, но мы все равно поговорим об этом. Мы сначалаStore
экземпляр, переданный нашему унаследованномуComponent
отец. это то, что мы только что написалиComponent
Добрый.
После этого мы объявляем метод рендеринга, который запускает Pub/Sub каждый разstateChange
Этот метод рендеринга вызывается каждый раз, когда происходит событие. в этотrender
Подход, мы будем генерировать список элементов или уведомление, когда нет проекта. Вы также заметите, что каждая кнопка сопровождается событием, и они запускают действие, а затем действие обработки нашего магазина. Этого действия не существует, но мы скоро его добавим.
Затем создайте еще два файла. Это два новых компонента, но они небольшие — поэтому просто вставляем в них некоторый код и переходим к остальным.
Во-первых, в вашемcomponent
каталог созданcount.js
, и вставьте в него следующее:
import Component from '../lib/component.js';
import store from '../store/index.js';
export default class Count extends Component {
constructor() {
super({
store,
element: document.querySelector('.js-count')
});
}
render() {
let suffix = store.state.items.length !== 1 ? 's' : '';
let emoji = store.state.items.length > 0 ? '🙌' : '😢';
this.element.innerHTML = `
<small>You've done</small>
${store.state.items.length}
<small>thing${suffix} today ${emoji}</small>
`;
}
}
Похоже на компонент списка, верно? Здесь нет ничего, что мы еще не рассмотрели, поэтому давайте добавим еще один файл. В то же самоеcomponents
добавить в каталогstatus.js
файл и вставьте в него следующее:
import Component from '../lib/component.js';
import store from '../store/index.js';
export default class Status extends Component {
constructor() {
super({
store,
element: document.querySelector('.js-status')
});
}
render() {
let self = this;
let suffix = store.state.items.length !== 1 ? 's' : '';
self.element.innerHTML = `${store.state.items.length} item${suffix}`;
}
}
Как и прежде, здесь нет ничего, что мы еще не рассмотрели, но, как видите, есть базовый классComponent
Как удобно, правда? ЭтоОбъектно-ориентированное программированиеОдно из многих преимуществ и основа большей части этого руководства.
Наконец, давайте проверимjs
Правильно ли указан каталог. Вот структура того, где мы сейчас находимся:
/src
├── js
│ ├── components
│ │ ├── count.js
│ │ ├── list.js
│ │ └── status.js
│ ├──lib
│ │ ├──component.js
│ │ └──pubsub.js
└───── store
└──store.js
└──main.js
давайте подключим его
Теперь у нас есть компоненты интерфейса и основныеStore
, все, что нам нужно сделать, это подключить все это.
Мы сделали так, чтобы системы и компоненты магазинов отображали данные и взаимодействовали с ними. Теперь давайте соединим две отдельные части приложения и заставим весь проект работать вместе. Нам нужно добавить начальное состояние, некотороеactions
и немногоmutations
. существуетstore
каталог, добавьте файл с именемstate.js
новый файл. Мой путь к файлу:
~/Documents/Projects/vanilla-js-state-management-boilerplate/src/js/store/state.js
Откройте файл и добавьте следующее:
export default {
items: [
'I made this',
'Another thing'
]
};
Смысл этого кода очевиден. Мы добавляем набор элементов по умолчанию, чтобы при первой загрузке наш апплет был полностью интерактивным. Давайте добавим немногоactions
. В твоемstore
каталог, создайте файл с именемactions.js
, и добавьте к нему следующее:
export default {
addItem(context, payload) {
context.commit('addItem', payload);
},
clearItem(context, payload) {
context.commit('clearItem', payload);
}
};
В этом приложении очень мало действий. По сути, каждое действие передает полезную нагрузку (связанные данные) в мутацию, которая, в свою очередь, отправляет данные в хранилище. Как мы видели раньше,context
даStore
экземпляр класса,payload
передается при запуске действия. Говоря о мутациях, давайте добавим некоторые. В том же каталоге добавьте файл с именемmutation.js
новый файл. Откройте его и добавьте следующее:
export default {
addItem(state, payload) {
state.items.push(payload);
return state;
},
clearItem(state, payload) {
state.items.splice(payload.index, 1);
return state;
}
};
Как и действия, эти мутации немногочисленны. На мой взгляд, ваши мутации должны быть простыми, потому что у них есть одна задача: изменить состояние хранилища. Так что примеры такие же простые, как и были изначально. Любая соответствующая логика должна происходить в вашемactions
середина. Как видите, в этой системе мы возвращаем новую версию состояния, чтобыStore
из<code>commit
Методы могут творить чудеса и обновлять все. При этом основные модули системы магазина на месте. Давайте свяжем все это вместе через индексный файл.
В том же каталоге создайте файл с именемindex.js
новый файл. Откройте его и добавьте следующее:
import actions from './actions.js';
import mutations from './mutations.js';
import state from './state.js';
import Store from './store.js';
export default new Store({
actions,
mutations,
state
});
Этот файл импортирует все наши модули магазина и связывает их вместе в виде краткогоStore
пример. Миссия выполнена!
Последняя часть головоломки
Последнее, что нам нужно сделать, это добавить начало этого урока.waaaayстраницаindex.html
включен вmain.js
документ. Как только мы разберемся с этим, мы сможем запустить браузер и наслаждаться нашей тяжелой работой! существуетjs
В корне каталога создайте файл с именемmain.js
новый файл. Вот мой путь к файлу:
~/Documents/Projects/vanilla-js-state-management-boilerplate/src/js/main.js
Откройте его и добавьте следующее:
import store from './store/index.js';
import Count from './components/count.js';
import List from './components/list.js';
import Status from './components/status.js';
const formElement = document.querySelector('.js-form');
const inputElement = document.querySelector('#new-item-field');
До сих пор все, что мы делали, это извлекали нужные нам зависимости. у нас естьStore
, наш внешний компонент и несколько элементов DOM. Затем мы добавляем следующий код, чтобы сделать форму интерактивной:
formElement.addEventListener('submit', evt => {
evt.preventDefault();
let value = inputElement.value.trim();
if(value.length) {
store.dispatch('addItem', value);
inputElement.value = '';
inputElement.focus();
}
});
Здесь мы добавляем прослушиватель событий в форму и предотвращаем ее отправку. Затем мы получаем значение текстового поля и обрезаем пробелы на обоих его концах. Мы делаем это, потому что хотим проверить, будет ли дальше что-то передано в хранилище. Наконец, если есть контент, мы будем использовать этот контент в качестве полезной нагрузки (связанных данных) для запуска нашегоaddItem
действие, и давайте блестящие новыеstore
справиться с этим для нас.
впусти насmain.js
Добавьте еще немного кода. В разделе «Прослушиватели событий» добавьте следующее:
const countInstance = new Count();
const listInstance = new List();
const statusInstance = new Status();
countInstance.render();
listInstance.render();
statusInstance.render();
Все, что мы здесь делаем, — это создаем новые экземпляры наших компонентов и вызываем каждый из них.render
метод, чтобы мы получили начальное состояние на странице.
С окончательным дополнением мы закончили!
Откройте браузер, обновите его и насладитесь славой нового приложения для управления состоянием. Давай, добавь несколько записей, например **"Завершите это замечательное руководство"**. Это аккуратно, не так ли?
Следующий шаг
Вы можете многое сделать с небольшой системой, которую мы собрали вместе. Вот несколько идей для дальнейшего самостоятельного изучения:
- Вы можете реализовать некоторое локальное хранилище, чтобы сохранить состояние даже при перезагрузке.
- Вы можете отделить интерфейсные модули и просто иметь небольшую систему состояний для своего проекта.
- Вы можете пойти дальше и разработать интерфейсный модуль этого приложения и сделать так, чтобы он выглядел великолепно. (Я очень хочу увидеть вашу работу, так что, пожалуйста, поделитесь!)
- Вы можете использовать некоторые удаленные данные и даже использовать API
- Вы можете систематизировать то, что вы узнали о
Proxy
и шаблоны Pub/Sub, а также изучить те навыки, которые можно использовать для разных работ.
Суммировать
Спасибо, что присоединились ко мне в изучении того, как работает государственная система. Эти большие основные библиотеки управления состоянием намного сложнее и умнее, чем то, что делаем мы, но все равно полезно понять, как работают эти системы, и демистифицировать их. В любом случае также полезно понимать мощь JavaScript без использования фреймворков.
Если вам нужна законченная версия этой маленькой системы, проверьте этоРепозиторий GitHub. вы также можетездесьПосмотрите демо.
Если вы будете развивать это дальше, я бы хотел это увидеть, поэтому, если вы это сделаете, пожалуйста, напишитеТвиттерСвяжитесь со мной или напишите в комментариях ниже!
Если вы обнаружите ошибки в переводе или в других областях, требующих доработки, добро пожаловать наПрограмма перевода самородковВы также можете получить соответствующие бонусные баллы за доработку перевода и PR. начало статьиПостоянная ссылка на эту статьюЭто ссылка MarkDown этой статьи на GitHub.
Программа перевода самородковэто сообщество, которое переводит высококачественные технические статьи из Интернета сНаггетсДелитесь статьями на английском языке на . Охват контентаAndroid,iOS,внешний интерфейс,задняя часть,блокчейн,товар,дизайн,искусственный интеллектЕсли вы хотите видеть более качественные переводы, пожалуйста, продолжайте обращать вниманиеПрограмма перевода самородков,официальный Вейбо,Знай колонку.