от Марка Кристиана и Джонни Роджерса
Для клиентов настольных компьютеров развертывается новая версия Slack, созданная с нуля, чтобы быть быстрее, эффективнее и проще в работе.
Общепринятая мудрость гласит, что никогда не следует переписывать код с нуля, и это хороший совет. Время, потраченное на переписывание того, что уже работает, - это время, которое не будет потрачено на то, чтобы сделать жизнь наших клиентов проще, приятнее и продуктивнее. А работающий код знает вещи: с трудом добытые знания, полученные за миллиарды часов кумулятивного использования и десятки тысяч исправлений ошибок.
Тем не менее, у программных кодовых баз есть продолжительность жизни. Настольная версия Slack - наш старейший клиент, выросший из периода быстрого развития и экспериментов на ранних этапах существования нашей компании. В течение этого периода мы оптимизировали продукт для соответствия рынку и в постоянном спринте, чтобы идти в ногу с нашими клиентами по мере роста их использования и ожиданий от продукта.
Сегодня, после более чем полувека стремительного роста, Slack используют миллионы людей в более крупных компаниях, работающих с большим объемом данных, чем мы могли себе представить, когда только начинали. Как и ожидалось, в основе настольного клиента начали проявляться некоторые внутренние трещины. Вдобавок технологический ландшафт сместился от инструментов, которые мы выбрали в конце 2012 года (jQuery, Signals и прямое манипулирование DOM), к парадигме составных интерфейсов и чистых абстракций приложений. Несмотря на все наши усилия, чтобы все работало быстро, стало ясно, что потребуются некоторые фундаментальные изменения, чтобы развить настольное приложение и подготовить его к следующей волне разработки продукта.
Архитектура существующего настольного приложения имела ряд недостатков:
- Обновление DOM вручную. Исходный пользовательский интерфейс Slack был построен с использованием HTML-шаблонов, которые необходимо было вручную перестраивать при изменении базовых данных, что затрудняло синхронизацию модели данных и пользовательского интерфейса. Мы хотели принять React, популярный фреймворк JavaScript, который сделал подобные вещи более автоматическими и менее подверженными потенциальным ошибкам.
- Активная загрузка данных. Модель данных была «полной», что означало, что каждый пользовательский сеанс начинался с загрузки всего, имеющего отношение к пользователю. Хотя теоретически это было здорово, на практике это было непомерно дорого для больших рабочих пространств и означало, что нам приходилось проделывать большую работу, чтобы поддерживать модели данных в актуальном состоянии в течение сеанса пользователя.
- Несколько процессов для нескольких рабочих областей. При входе в несколько рабочих областей каждая из этих рабочих областей фактически запускала автономную копию веб-клиента внутри отдельного процесса Electron, а это означало, что Slack использовал больше памяти, чем ожидали пользователи.
Первые две проблемы были такими вещами, которые мы могли постепенно улучшать со временем, но запуск нескольких рабочих пространств в рамках одного процесса Electron означал изменение фундаментального предположения первоначального дизайна - что всегда существует только одно рабочее пространство. работает за раз. Хотя мы сделали некоторые постепенные улучшения для людей с большим количеством незанятых рабочих мест, по-настоящему решить проблему множественных процессов означало переписать настольный клиент Slack с нуля.
Мало по малу
Корабль Тесея - это мысленный эксперимент, в ходе которого выясняется, является ли объект, каждая из частей которого со временем заменялась одна за другой, тем же самым, когда все сказано и сделано. Если все куски дерева на корабле были заменены, будет ли это один и тот же корабль? Если каждый фрагмент JavaScript в приложении был заменен, это одно и то же приложение? Мы очень на это надеялись, потому что это казалось лучшим курсом действий.
Наш план состоял в следующем:
- сохранить существующую кодовую базу;
- создать «современный» раздел кодовой базы, который будет рассчитан на будущее и будет работать так, как мы хотели;
- по крупицам модернизировать реализацию Slack, постепенно заменяя существующий код современным кодом;
- определить правила, которые будут обеспечивать строгий интерфейс между существующим и современным кодом, чтобы было легко понять их взаимосвязь;
- и постоянно поставлять все вышеперечисленное с существующим приложением, заменяя старые модули современными реализациями, подходящими для нашей новой архитектуры.
Последним шагом - и наиболее важным для наших целей - было создание современной версии Slack, которая сначала будет неполной, но постепенно продвигается к полноте функций по мере модернизации модулей и интерфейсов.
Мы использовали эту современную версию приложения для внутренних целей большую часть прошлого года, и теперь она развертывается для клиентов.
Современное время
Первым делом было создание современной кодовой базы. Хотя это был всего лишь новый подкаталог в нашей кодовой базе, в нем было три важных правила, обеспечиваемых соглашениями и инструментами, каждое из которых предназначалось для устранения одного из недостатков нашего существующего приложения:
- Все компоненты пользовательского интерфейса должны были быть созданы с помощью React.
- Весь доступ к данным должен был предполагать лениво загруженную и неполную модель данных.
- Весь код должен был «учитывать несколько рабочих пространств».
Первые два правила требовали много времени на выполнение, но были относительно простыми. Однако переход к архитектуре с несколькими рабочими пространствами оказался непростым делом. Мы не могли ожидать, что каждый вызов функции будет передавать идентификатор рабочей области, и мы не могли просто установить глобальную переменную, указывающую, какая рабочая область в настоящее время видна, поскольку многие вещи продолжают происходить за кулисами независимо от того, какое рабочее пространство пользователь в данный момент просматривает. в.
Ключом к нашему подходу стал Redux, который мы уже использовали для управления нашей моделью данных. Немного подумав и воспользовавшись библиотекой redux-thunk, мы смогли смоделировать практически все как действия или запросы в хранилище Redux, что позволило Redux обеспечить удобный уровень абстракции вокруг концепции отдельных рабочих пространств. У каждой рабочей области будет свое собственное хранилище Redux со всем, что находится в ней - данные рабочей области, информация о состоянии подключения клиента, WebSocket, который мы используем для обновлений в реальном времени - вы называете это. Эта абстракция создавала концептуальный контейнер вокруг каждой рабочей области без необходимости размещать этот контейнер в собственном процессе Electron, что и делал унаследованный клиент.
Осознав это, у нас появилась новая архитектура:
Наследие функциональной совместимости
На этом этапе у нас был план и архитектура, которые, как мы думали, будут работать, и мы были готовы работать над существующей кодовой базой, модернизируя все, пока у нас не останется совершенно новый Slack. Оставалось решить только одну последнюю проблему.
Мы не могли просто начать замену старого кода новым кодом волей-неволей; без какой-либо структуры, разделяющей старый и новый код, они в конечном итоге безнадежно запутались бы вместе, и у нас никогда не было бы нашей современной кодовой базы. Чтобы решить эту проблему, мы ввели несколько правил и функций в концепцию под названием legacy-interop:
- старый код не может напрямую импортировать новый код: доступен только новый код, который был «экспортирован» для использования старым кодом.
- новый код не может напрямую импортировать старый код: доступен только старый код, который был «адаптирован» для использования в современном коде.
Экспортировать новый код в старый было просто. В нашей исходной кодовой базе не использовались модули или импорт JavaScript. Вместо этого он хранил все в глобальной переменной верхнего уровня под названием TS. Процесс экспорта нового кода просто означал вызов вспомогательной функции, которая сделала новый код доступным в специальной части TS.interop этого глобального пространства имен. Например, TS.interop.i18n.t () будет вызывать нашу современную функцию локализации строк с поддержкой нескольких рабочих областей. Поскольку пространство имен TS.interop использовалось только из нашей устаревшей кодовой базы, которая загружала только одну рабочую область за раз, мы могли сделать простой поиск, чтобы определить идентификатор рабочей области за кулисами, не требуя, чтобы устаревший код беспокоился об этом.
Адаптация старого кода к новому была менее тривиальной. И новый код, и старый код будут загружены, когда мы будем запускать классическую версию Slack, но современная версия будет включать только новый код. Нам нужно было найти способ, позволяющий условно задействовать старый код, не вызывая ошибок в новом коде, и мы хотели, чтобы этот процесс был максимально прозрачным для разработчиков.
Наше решение называлось adapFunctionWithFallback, которое брало путь к функции в нашем устаревшем объекте TS для запуска, а также функцию, которую можно было бы использовать вместо этого, если бы мы работали только в современной кодовой базе. По умолчанию эта функция была отключена, а это означало, что, если бы базовый устаревший код не присутствовал, современный код, который пытался его вызвать, не имел бы никакого эффекта и не генерировал ошибок.
Имея оба этих механизма, мы смогли серьезно приступить к модернизации. Унаследованный код может обращаться к новому коду по мере его модернизации, а новый код может обращаться к старому коду до его модернизации. Как и следовало ожидать, с течением времени использовалось все меньше и меньше старого кода, адаптированного для использования из современной кодовой базы, с тенденцией к нулю по мере того, как мы готовились к выпуску.
Собираем все вместе
Эта новая версия Slack появлялась давно, и в нее вошли вклады десятков людей, которые последние два года работали над ее беспрепятственным развертыванием для клиентов. Ключом к его успеху является стратегия инкрементального выпуска, которую мы приняли на ранних этапах проекта: по мере того, как код был модернизирован, а функции были перестроены, мы предоставили их нашим клиентам. Первой «современной» частью приложения Slack была наша программа выбора смайлов, которую мы выпустили более двух лет назад, за которой последовали боковая панель канала, панель сообщений и десятки других функций.
Если бы мы подождали, пока весь Slack будет полностью переписан перед его выпуском, наши пользователи испытали бы худший повседневный опыт работы с эмодзи, сообщениями, списками каналов, поиском и множеством других функций, прежде чем мы смогли бы выпустить «большой взрыв». замена. Постепенный выпуск позволил нам как можно скорее принести реальную пользу нашим клиентам, помог нам сосредоточиться на постоянном улучшении и снизил риск выпуска нового клиента за счет минимизации количества полностью нового кода, используемого нашими клиентами в первый раз. время.
Принято считать, что перезаписи лучше избегать, но иногда преимущества слишком велики, чтобы их игнорировать. Одним из наших основных показателей было использование памяти, и новая версия Slack обеспечивает:
Эти результаты подтвердили всю работу, которую мы вложили в эту новую версию Slack, и мы с нетерпением ждем продолжения итераций и улучшения ее со временем.
Если руководствоваться стратегическим планированием, продуманными выпусками и поддерживаться талантливыми участниками, поэтапные переписывания - отличный способ исправить ошибки прошлого, построить себе новый корабль и сделать рабочую жизнь ваших пользователей проще и приятнее. и более производительный.
Будьте на связи
Мы очень хотим поделиться тем, что мы узнали в ходе этого процесса. В ближайшие недели мы будем писать больше на https://slack.engineering о:
- Сервисные работники, наше стремление к ускорению загрузки и офлайн-поддержке
- Slack Kit, наша библиотека компонентов пользовательского интерфейса
- Доступность, встроенная, а не закрепленная
- Gantry, наша платформа для запуска приложений.
- и развертывание новой клиентской архитектуры в любом масштабе