Skip to content

Instantly share code, notes, and snippets.

@Imater
Last active October 12, 2021 22:16
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save Imater/333e55de970374a789b4b623875f59d0 to your computer and use it in GitHub Desktop.
Save Imater/333e55de970374a789b4b623875f59d0 to your computer and use it in GitHub Desktop.

Javascript на больших проектах

Общая информация, codestyle, статический анализ кода

Команда каждого проекта, а также заказчик (если он участвует в коде разработки), должны договариватся о code style проекта, желательно с самого старта. Для того, чтобы это сделать, как правило, настраивают статический анализатор кода. Для js, чаще всего используют eslint. Его конфигурация хранится в корне проекта в файле конфигурации (пример .eslintrc файла).

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

Для новичков использование eslint позволяет очень быстро обучиться правильному code style. Существует два наиболее признанных code style: google, yandex

Для проектов CSSSR, широкую практику получило использование Airbnb JavaScript Style Guide, к тому же его основной раздел посвящён более современной модификации языка ES6. Поверх этой конфигурации, можно наложить свои правила, например отключение ";" в конце строк.

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

ES6, babel, webpack

Javascript развивается, постепенно устраняя недостатки и добавляя различные удобства. Для изучающих js с нуля, необходимо знать ES6, но пропусктать этап изучения ES5 не рекомендуется. Необходимо уметь читать код, в том числе существующий код проектов (legacy code).

В новых проектах лучше использовать ES6, настроив компиляцию этого кода в ES5 для поддержки в браузерах. Node.js сервер уже поддерживает ES6. Код в ES6 более компактный, предсказуемый, читаемый

Фреймворки

Экспресс обзор фрейворков и их кода, вы можете сделать в TodoMVC. На данный момент мы используем React и Angular. Если вы изучаете с нуля, эти фрейворки очень разные, поэтому выбор за вами. Если выбор за React, то можете смотреть наши уроки в React Learning Club

Основная задача фрейворка и его вспомогательных библиотек:

  • уменьшить кол-во кода
  • увеличить его читаемость
  • обеспечить обновление данных, когда они меняются в модели
  • обеспечить (если умеет) серверный рендеринг и перенос данных с сервера на клиент
  • обеспечить роутинг и передачу компонентам параметров из url
  • обеспечить модульность кода, удобное разделение кода на разные виды компонентов, сервисов, фабрик и обеспечить связь между ними
  • привести различные практики кода к стандарту
  • обеспечить библиотеками и обеспечить их удобное переиспользование

Лучшие практики

Строгая типизация

Язык Javascript не имеет строгой типизации, поэтому в больших проектах, для того, чтобы избавиться от неочевидных преобразований данных, используют такие статические анализаторы как свежий Flow от Facebook и проверенный TypeScript от Microsoft, можно считать, что это отдельный язык, но после компиляции, все элементы строгой типизации убираются из кода. Данные системы, позволяют ускорять рефакторинг, так как на любом этапе вы и ваш редактор знаете структуру и типы всех данных, поэтому переименование, преобразование переменных, функций, основные виды рефакторинга становятся более безопасными.

Практика показывает, также, что использование строгой типизации и подобных систем, ускоряет разработку, так как вы реже компилируете и запускаете код, чтобы найти те ошибки, которые эти системы отображают по мере набора кода прямо в редакторе. Многие недооценивают те 5-30 секунд, которые требуются для запуска проекта и сколько мозготоплива сгорает за это время.

Пример написания функции в Flow:

/* @flow */
const multiply = (x: number): number => x * 2;

При использовании функции таким образом:

/* @flow */
multiply('this is string')

будет показана ошибка с указанием несовместимых типов.

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

При использовании Flow, типизация пишется один раз и не требует propTypes в классах компонентов React.

Модульная архитектура

Разбивайте большое приложение на максимально изолированные модули. По "правилу треугольника" между/над ними будет общий модуль, который обеспечивает связь между ними. Сами модули не должны общаться между собой напрямую. Весь общий код лучше размещать в общем модуле. При правильном разбиении, вы сможете делать приложение для браузера, сервера и мобильных устройств не используя "копи-паст" между ними.

Уменьшение сложности complexity index

В js любая функция является объектом и может быть внутри объекта, поэтому вы можете уменьшать сложность каждой из них, избавляться от лишних "if", "&&", "||", пример:

const keyPress = (keyCode) => {
 if(keyCode === 'Enter') {
   console.log('Enter')
 } else if (keyCode === 'Space') {
   console.log('Space')
 }
}
// лучше, так как добавлять новые клавиши, если их много, легче и можно переиспользовать фунции:
const keyPress = (keyCode) => {
 const fn = {
   'Enter': () => console.log('Enter'),
   'Space': () => console.log('Space')
 }
 if(typeof fn[keyCode] === 'function') {
   fn[keyCode]()
 }
}

Антипаттерны

Не доверяйте

Не доверяйте окружению. Например, вы надеетесь, что у вас есть объект window и используете его функции, но вы не учитываете, что код может выполняться на стороне сервера, где такого объекта нет. Или же вы модифицируете dom в зависимости от версии браузера, которую определяете из window, так вы получите, что сервер отрендерит одно, а браузер другое, что, например, в React выдаст предупреждение бесполезности изоморфного рендера.

Функция/компонент должны делать одну функцию и делать её хорошо (основной принцип Linux)

Старайтесь всё разбивать на функции и компоненты, которые выполняют лишь одну задачу. Это связано с законом композиции сложных проектов, который обеспечивает переиспользование. Пример: приложение будет состоять из 100 маленьких функций, которые переиспользуются или из 500 функций, которые выполняют по нескольку задач, поэтому их переиспользование затруднено или внутри будет множество условий "if". Функции/компоненты которые вы пишите, не должны знать того, без чего они могут обойтись. На примере компонента, рассмотрите два варианта:

  1. Вы вставили padding внутрь компонента. В одной форме он нужен, в другой не нужен, в третьей нужен разный padding по сторонам, все эти варианты вы будете указывать явно или ставить условия "if". Если эти условия внутри компонента изменит другой разработчик, то у вас "разползётся" неизвестная ему форма. Во всех остальных компонентах, вы будете придерживаться подобного подхода и будете "копипастить" код связанный с padding, в том числе использовать файлы стилей там, где компонент мог бы обходиться без стилей. В памяти приложения (и разработчика) этот "копипаст" занимает место.
  2. У вас отдельный компонент для установки padding, вы его используете там где он нужен с разными другими компонентами. Он один на всё приложение. Осознать его использование легко, код чистый и компактный. По использованию памяти этот вариант тоже оптимальный.

Данный пример, можно дополнить использованием Алфавита, так как каждая буква выполняет одну функцию, текст занимает оптимальное место и легко читается. А если появилось бы дополнительное условие, что букву "б" можно использовать только в словах с 5 буквами, а иначе вместо неё нужно использовать "б(8)", то появилась бы излишняя сложность. Тоже самое относится к любой состовляющей большого проекта. Старайтесь использовать этот принип при создании отдельных функций, отдельных компонентов, отдельных файлов, отдельных модулей, отдельных программ и инструментов.

Также, данный подход позволит использовать общий код для приложений для браузера, сервера, мобильных устройств (например React native)

Хранение вычисляемых значений

Старайтесь не сохранять в объектах то, что можно вычислить на основе имеющихся данных, так как данные необходимо "нормализовывать" Это обеспечивает оптимальное использование памяти, код становится более читаемым, так как вам не нужно инвалидировать это "кэшированное" значение, которое можно вычислить. Не бойтесь переносить это вычисляемое значение в функции и выполнять их многократно. Для оптимизации, вы можете просто добавить в эту функцию мемоизацию (кеширование результата функции на основании полного (указанного) набора аргументов, без лишнего кода, например memoizee). Также, нормализация данных, функций и компонентов, тесно перекликается с предыдущим пунктом и обеспечивает более компактный код.

Наследование и композиция (правило "треугольника")

В Js чаще используется композиция, чем наследование. Композиция, это когда вы пишете функцию в отдельном файле (месте кода) и используете её там, где нужно. Наследование, это когда у предка свои функции, а у его наследников свои функции, но наследник может пользоваться и функциями предка. Принцип "треугольника" стоит вспомнить, когда вы не может определить "куда писать код". Если этот код будет использоваться несколькими потомками, размещаете код у предка. Если функция уникальная, размещаете её у потомка, который её использует.

Иногда это применяется не только для наследования, но и при создании файлов кода проекта. У вас есть общие компоненты, вы их размещаете в отдельной папке, а то что уникально, размещаете вместе с модулем конкретного раздела или страницы сайта. Тоже самое с файлам функций, если функция используется один раз, вы её размещаете в папке с кодом, который её использует, если она используется многократно, переносите в общую папку (например utils или helpers).

Лапша-код

Любая ваша сущность должна быть оптимально компактна. Файл с кодом, стараться делать не более 2х экранов, лучше вместить в один. В папке должно быть не более десятка файлов, если их более, задумайтесь о разбиении. Наример, если у вас много reducer-ов в проекте и они в одной папке, можно подумать выделить те, что используются в отдельных разделах сайта в каталоге этих разделов и подключать эти хранилища только когда они нужны. Область памяти человека не безгранична, поэтому такой код/проект/каталог будут более читаемы и их будет легко поддерживать.

Комментарии

Если вы используете хорошие, понятные названия переменных/файлов/функций/компонентов/props/названия_свойств_модели, то комментарии зачастую лишние и являются "вычисляемыми значениями". Но они необходимы в неочевидных местах, например в сложных регулярных выражениях, хитрых условиях, сложных алгоритмов.

Также комментарии полезны в тех случаях, если у вас есть настройки автодокументирования и вы указывете типы данных. Но при использовании строгой типизации, такие комментарии становятся значительно компактнее.

Скорость работы

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

// быстрее всех и не зависит от размера списка:
const a = {
  ...manyElements,
  test: 'test'
}
const result = a[test] //операция мгновенная, так как использует hash таблицу
const a = [
  ...manyElements,
  { key: 'test', value: 'test' }
];

// быстро, но зависит от размера списка:
const result = a.find(item => item.key === 'test') //операция будет перебирать элементы пока не найдёт результат,

// медленнее всех, так как перебирает все элементы списка
let result;
a.forEach(item => {
  if(item.key === 'test') {
    result = item;
  }
}) //операция будет последовательно перебирать каждый элемент

Исходя из примеров выше, вы можете принять решение сразу хранить объекты под уникальными ключами, чтобы находить элементы мгновенно. Если вам нужно искать по нескольким ключам, вы можете создать несколько объектов, с разными ключами, но хранить там объекты, которые будут лишь ссылками, поэтому это не займёт место. Для создания такого объекта из массива, можно использовать функцию reduce:

// быстрее всех и не зависит от размера списка:
const items = [
  {key: 'first', value: 'First'},
  {key: 'second', value: 'Second'}  
]
const distByKey = items
  .reduce((acc, item) => ({
    ...acc,
    [item.key]: item // хранение будет по ссылке, что практически не занимает память
  }), {})
const distByValue = items
  .reduce((acc, item) => ({
    ...acc,
    [item.value]: item // хранение будет по ссылке, что практически не занимает память
  }), {})
  

===

Используйте строгое сравнение "===", вместо "==". Оно не приводит обе стороны условия к единому типу и не тратит на это времени и неявной магии, которую разработчик может сходу не увидеть.

const a = 65;
const b = '65';
if (a == 'b') {
  // bad
}
if (a === parseInt(b, 10)) {
  // good
} 

if

Старайтесь минимизировать кол-во "if", тернарных операторов, "switch" и других операторов приводящих к разветвлению программы. Читать код изобилующий множеством условий, крайне сложно. Также, старайтесь делать проверку в самом начале и делать return, вместо того, чтобы писать длинный код внутри "if". Ну и фигурные скобки переносятся всегда, даже если внутри один лишь return.

const good = arg => {
  if(!arg) {
    return;
  }
  // long code
  // ...
  // ...
  // ...
}

const bad = arg => {
  if(arg) {
   // long code
   // ...
   // ...
   // ...
  } 
}

Магические числа

Старайтесь не использовать в ваших функциях "захардкоженых предустановок", все цифры, селекторы должны быть вынесенны и передаваться аргументом или импортироваться из единого конфига. В том числе, если вы пишете функцию сложной сортировки, вы можете аргументом передавать не имя поля, а функцию по которой достаётся значение:

const sorting = (items, fn) => R.sort(fn, items)
// вместо :
const sorting = (items, name) => R.sort((a, b) => (a[name] > b[name] ? -1 : 1), items)

Используйте debug в браузере, а не console.log

В chrome вы можете зайти в developer tools, открыть "sources", там нажать cmd+P (ctrl+p) и набрать имя файла с вашим кодом (потому и удобно разбивать код на множество мелких файлов). Там вы можете поставить debug точку остановки, в том числе по правой клавише на ней установить условие остановки. Когда код дойдёт до этого места, вы сможете посмотреть не только конкретную переменную, но и все переменные окружения. Так вы не потратите время компиляции. Приучайте себя, сначала сложно привыкнуть, потом втянитесь. Отладка - это 70% времени разработки. Уменьшайте это время умелым дебагом, тестировнием и наличием storybook.

Совместимость с браузерами

При разработке необходимо учитывать совместимость js методов с версиями браузеров или сервера, где этот код будет запускаться. Как правило для проверки совместимости используют [caniuse.com][http://caniuse.com]. А для поддержки функций, которые вам необходимы, но не поддерживаются всеми версиями браузеров, лучше не засорять код условиями, а использовать различные shim решения, которые при использовании функции проверяют её существование и добавляют совместимый код функции, если её нет. Бывают комплексные решения, такие как modernizr

Особое внимание уделяйте совместимости при использовании "фреймворка" vanilla js

Тестирование

Используйте TDD при разработке любых функций, даже самых простых. Так скорость разработки только вырастает, так как вы не тратите время на многкратный запуск приложения для отладки, а также проверяете все виды входных данных одновременно. Плюс можете рефакторить приложение, меняя код функции не "прокликивая" функции, а запуская тесты. Причём тесты можно запускать в различных браузерах, что поможет отладить совместимость с браузерами. Для тестирования используют самые разные инструменты, что будет темой отдельных статей.

Интеграционное UI-тестирование, это когда тест сам прокликивает все формы, кнопки, авторизуется и так далее, оптимально поддерживать могут только специально выделенные тестировщики, разработчик, который увлекается подобным тестированием, попадает в западню поддержки этих тестов при постоянно изменяемом UI. Разработчик может писать Unit-тесты, а также UI тестирование отдельных компонентов. В современных react компонентах, это делать довольно несложно.

Сторибук (ui-kit)

При разработке и тестировании, в том числе pixelperfect, удобно использовать автоматически генерируемую storybook библиотеку ваших разрабатываемых компонентов. Использование storybook позволяет быстрее разрабатывать компоненты и гарантировать им изолированность от данных остальной страницы. Также, вы в любой момент можете проверить все состояния и виды компонентов. Это удобно и для тестировщиков и для ваших коллег разработчиков. Это своеобразное оглавление вашего проекта, где можно ознакомиться со всеми компонентами и избавить себя от "забывчивости в переиспользовании" компонентов.

Ну и учтите, что компонент открывается один, изолированно и поэтому компиляция и отрисовка ваших изменений (кода и стилей) происходит гораздо быстрее, чем если вы разрабатываете компонент прямо на странице с "соседями" и реальными запросами к базе данных. Данные компоненту предоставляются "фейковые", поэтому всё по умолчанию работает без асинхронных операций.

Именования функций, переменных, имён файлов и директорий

В js используется camelCase. В API запросах к серверу традиционно используется snake_case, хотя Google в последнее время и там переходит на использование camelCase. Если в названии вашей функции есть слово And, значит она выполняет больше одной фукнции. Тип данных обозначать в названии переменных не следует. Префиксы, bem стиль именования стоит исключить, если вы используете модульный подход.

При именовании, старайтесь анализировать так: если удалю это слово из названия, легко ли понять, что означает эта переменная без комментариев и излишних затрат "мыслетоплива".

Копипаст

Запретите себе использование "копипаста". Это когда одинаковый по сути код, повторяется по проекту многократно. Используйте в таких случаях композицию, которая заключается лишь в создании отдельного файла с отдельной функцией. Затраты времени (а они смешные при использовании snippets и автодополнении) возноградятся экономией на отладке. Основная проблема "копипаста", которую не видят новички, в том что скопированный многократно код с ошибкой, будет при найденом баге исправлен лишь в одном месте (разработчик не найдёт другие, если не он писал код) и приложение пройдёт новый виток "круга ада тестирования". Гораздо проще выделить функцию в отдельный файл, создать тест, простестировать пару примеров использования и импортировать функцию из этого каталога.

Генератор компонентов

Одним из подвидом "копипаста" является копирование из раза в раз компонентов и заготовок функций. Если это сложный процесс, нужно много прокликивать, подписывать, переименовывать, вспоминать все базовые библиотеки, которые нужно импортировать – то "подсознательно" разбивать большой компонент на подкомпоненты, вам будет "не охото". Как в известном мультике: "Лучше день потерять, чтобы научиться летать, а потом за час долететь, чем целый день идти".

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

Но более сложные сущности, лучше генерировать при помощи генераторов. Тут вам поможет yee-man, npm скрипт или то, что используем мы в React проектах: "Redux-cli"

DOM операции, частое обновление

Те кто начинал с jQuery или использует Vanilla js, зачастую не задумываются о миллисекундах, которые тратятся не только на операции изменения DOM-дерева, но и даже на операции чтения. В фреймворках React, Angular и некоторых других используется виртуальный DOM, операции с которыми дешевле (быстрее).

Но самый оптимальный подход, использовать однонаправленный поток данных, когда у вас есть объекты и массивы данных и на их основе отрисовывается DOM-дерево. Фреймворк (или ваш код) отвечает за перенос этих данных в реальный дом и делает это не по "кусочкам", а одним разом, вспоминая предыдущее и текущее состояние и вычислив, что нужно изменить.

В операциях с DOM есть свои антипаттерны, часть из них описана тут. Особенно осторожны будьте с анимацией (старайтесь использовать CSS, SVG анимацию, вместо js), скроллингом (старайтесь использовать vanilla-js), используйте debounce или throttle функции пользовательского ввода.

Гонки (мерцание контактов)

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

@korsun
Copy link

korsun commented Nov 14, 2016

Начал комментировать построчно, стер.

Получилась такая A Short History of Everything, для новичков написанная местами непонятно, а для тех, кто постарше, некорректно.

Примеры:

Разбивайте большое приложение на максимально изолированные модули. По "правилу треугольника" между/над ними будет общий модуль, который обеспечивает связь между ними. Сами модули не должны общаться между собой напрямую. Весь общий код лучше размещать в общем модуле. При правильном разбиении, вы сможете делать приложение для браузера, сервера и мобильных устройств не используя "копи-паст" между ними.
В Js чаще используется композиция, чем наследование. Композиция, это когда вы пишете функцию в отдельном файле (месте кода) и используете её там, где нужно. Наследование, это когда у предка свои функции, а у его наследников свои функции, но наследник может пользоваться и функциями предка. Принцип "треугольника" стоит вспомнить, когда вы не может определить "куда писать код". Если этот код будет использоваться несколькими потомками, размещаете код у предка. Если функция уникальная, размещаете её у потомка, который её использует.
Префиксы, bem стиль именования стоит исключить, если вы используете модульный подход.

Есть голословные утверждения:

В Js чаще используется композиция, чем наследование.

Существует два наиболее признанных code style: google, yandex

Статья называется "Основные неочевидности Javascript", что готовит читателя увидеть что-то вроде очередного опуса про приведение типов, а в результате охватывает линтинг, ES6, реакт, storybook (причем, реакт в абзаце с ним даже не упомянут), тестирование, именование запросов к апи, debounce & throttle... Всего помаленьку, про все очень поверхностно, сложно проследить за потоком сознания. Имхо, надо ограничить тему. Лучше меньше да лучше.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment