[Перевод] Создание системы управления состоянием с помощью собственного JavaScript

внешний интерфейс GitHub JavaScript Программа перевода самородков

Управление состоянием не является чем-то новым в программном обеспечении, но все еще является относительно новым в приложениях, созданных с помощью 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Как объект отслеживает все изменения. мы будем использоватьПроксидля завершения этой операции. Работа, выполняемая прокси, в основном заключается в проксировании объекта состояния. Если мы добавимgetIntercept, мы можем отслеживать каждый раз, когда запрашиваем данные у объекта. а также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Посмотрим, есть ли у нас метод рендеринга. если этоComponentclass является родительским классом другого класса, тогда он может быть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 &#x1f622;</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 ? '&#x1f64c;' : '&#x1f622;';

    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,внешний интерфейс,задняя часть,блокчейн,товар,дизайн,искусственный интеллектЕсли вы хотите видеть более качественные переводы, пожалуйста, продолжайте обращать вниманиеПрограмма перевода самородков,официальный Вейбо,Знай колонку.