Мой самый последний проект был на довольно типичном веб-проекте Java, где у нас был компонент, который должен быть написан на JavaScript. Ничего особенного и ничего большого. Похоже, что люди все еще не воспринимают JavaScript всерьез в подобных средах. Поэтому я хотел занять несколько минут и поговорить о том, как мы разработали JavaScript для этого проекта. Такой совет, который я дам здесь, хорошо подходит для веб-проектов с небольшим или средним количеством JavaScript. Если вы пишете большие части своего приложения на стороне клиента, вы, вероятно, захотите использовать полностью интегрированную среду стека, чтобы помочь вам, поэтому эти вещи менее актуальны.
Конечно, большинство, если не все, что я расскажу здесь, можно почерпнуть из других источников, и, возможно, лучше. И если вы опытный разработчик JavaScript, вам, вероятно, хорошо без этой статьи.
Мне пришлось сделать две вещи, чтобы стать эффективными в использовании JavaScript. Первым было научиться игнорировать синтаксис. Синтаксис неуклюжий и определенно мешает. Но с правильными привычками (такими как наличие ярлыка для литералов функции / лямбды и уверенность в том, что возвращаемое значение всегда должно быть в той же строке, что и оператор return), я смог увидеть синтаксис и в основном использовать JavaScript в Схемоподобный стиль. Второе — полностью игнорировать объектную систему. Я использую много литералов объектов, но на самом деле никаких конструкторов или ключевых слов this. Обе эти функции могут быть использованы хорошо, но они также очень неуклюжи, и трудно заставить всех в команде понять одинаково. Я люблю ОО на основе прототипа в качестве модели и с успехом использовал его в Ioke и Seph. Но с JavaScript я вообще уклоняюсь от этого.
Шаблон модуля
Основная идея шаблона модуля заключается в том, что вы инкапсулируете весь свой код в анонимные функции, которые затем немедленно оцениваются для создания фактического объекта верхнего уровня. Поскольку в JavaScript есть некоторые неприятные проблемы с глобальными переменными (например, они есть), безопаснее всего поместить весь свой код в один или несколько из этих модулей. Вы также можете заставить ваши модули принимать зависимости, которые вы хотите использовать. Простой модуль может выглядеть так:
var olaBiniSeriousBanking = (function() { var balance = 0; function deposit(num) { balance += num; } function checkOverdraft(amount) { if(balance - amount < 0) { throw "Can't withdraw more than exists in account"; } } function withdraw(amount) { checkOverdraw(amount); balance -= amount; } return {deposit: deposit, withdraw: withdraw}; })();
В этом случае переменная баланса полностью скрыта внутри лексического замыкания и доступна только для функций ввода и вывода. Эти функции также не находятся в глобальном пространстве имен, так что нет риска забить. Также возможно иметь много и много вспомогательных функций, которые никто не сможет увидеть. Это облегчает уменьшение ваших функций — и, кстати, самая большая проблема, с которой я сталкивался в качестве кода JavaScript, заключается в том, что функции имеют тенденцию быть очень большими. Не делай этого!
Полезным вариантом шаблона модуля является извлечение функции построения и присвоение ей имени. Даже если вы можете использовать его сразу, это позволяет создать более одного из них, использовать разные зависимости или сделать его доступным из тестов, чтобы вы могли внедрить соавторов:
var olaBiniGreeterModule = (function(greeting) { return {greet: function(nam { console.log(greeting + ", " + name); }}; }); var olaBiniGreeterEng = olaBiniGreeterModule("Hello"); var olaBiniGreeterSwe = olaBiniGreeterModule("Hejsan");
RequireJS
Шаблон модуля хорош сам по себе, но есть некоторые вещи, которые может сделать загрузчик, который делает вещи еще лучше. Есть несколько вариантов этих загрузчиков модулей, но мой любимый до сих пор — RequireJS. У меня есть несколько причин для этого, но главная из них, вероятно, заключается в том, что он очень легкий и на самом деле является чистой победой даже для очень небольших веб-приложений. Позволяя RequireJS управлять вашими модулями, есть много преимуществ. Основным из них является то, что он заботится о зависимостях между модулями и загружает их автоматически. Это означает, что вы можете определить одну единственную точку входа для вашего JavaScript, а RequireJS обязательно загрузит все остальное. Еще одним хорошим аспектом RequireJS является то, что он позволяет вам вообще избегать глобальных имен. Все обрабатывается обратными вызовами внутри RequireJS. Так как это выглядит? Что ж,простой модуль с зависимостью может выглядеть так:
// in file foo.js require(["bar", "quux"], function(bar, quux) { return {doSomething: function() { return bar.something() + quux.something(); }}; });
Если у вас есть что-то еще, использующее foo, то этот файл будет загружен, будут загружены bar.js и quux.js, и результаты их загрузки (возвращаемое значение из функции модуля) будут отправлены в качестве аргументов функции это создает модуль foo. Таким образом, RequireJS берет на себя всю эту загрузку. Но как вы начинаете? Ну, у вас должен быть один единственный скрипт-тег в вашем HTML, который будет указывать на require.js. Вы также добавите дополнительный атрибут к этому тегу сценария, который указывает на точку входа в JavaScript:
<script data-main="scripts/main" src="scripts/require.js"> </script>
Это сделает много вещей. Он загрузит require.js. Он установит каталог scripts в качестве основы для всех ссылок на модули в вашем JavaScript. И он загрузит scripts / main.js, как будто это модуль RequireJS. И если вы хотите использовать наш foo-модуль ранее, вы можете создать main.js, который выглядит следующим образом:
// in file main.js require(["foo"], function(foo) { require.ready(function() { console.log(foo.doSomething()); }); });
Это гарантирует, что foo.js и его зависимости bar.js и quux.js будут загружены до вызова функции. Однако один из аспектов JavaScript, который иногда ошибается, заключается в том, что вам нужно подождать, пока DOM не будет готов выполнить JavaScript. С RequireJS мы используем функцию ready внутри объекта require, чтобы убедиться, что мы можем что-то сделать, когда все будет готово. Ваш основной модуль всегда должен ждать, пока документ готов.
Вообще, RequireJS очень помог со структурой и зависимостями, и это позволяет очень просто разбить JavaScript на гораздо более мелкие части. Мне это очень нравится. Однако есть несколько недостатков. Главное в том, что он плохо взаимодействует с серверным JavaScript (или, по крайней мере, не взаимодействовал с ним месяц назад). Кроме того, он не обеспечивает чистый способ получить доступ к функциям модуля без их выполнения, что становится раздражающим при тестировании этих вещей. Об этом я немного подробнее расскажу в разделе о тестировании.
Нет JavaScript в HTML
Я не хочу никакого JavaScript в HTML, если я могу избежать этого. Единственный тег script должен быть тем, который запускает ваш модуль и загружает фреймворк — в моем случае RequireJS. У нас вообще нет встроенных обработчиков событий на страницах. Мы начали с того места, где на некоторых наших страницах было много обработчиков событий, и мы переориентировали его на гораздо меньшую базу кода, с которой было намного проще работать, извлекая все эти вещи в отдельные модули JavaScript. Это имеет побочный эффект: семантически идентифицировать все, с чем вы хотите работать, можно с помощью CSS-классов или атрибутов данных. Старайтесь избегать запутанных путей, чтобы найти элементы. Можно добавить некоторые дополнительные классы и атрибуты, чтобы сделать ваш JavaScript простым и понятным.
Инициатива работает на готовом
С точки зрения того, как мы структурируем модули в реальном приложении, мы фактически не делаем много работы при запуске. Вместо этого большая часть работы включает настройку обработчиков событий и так далее. Мы делаем так, чтобы модули верхнего уровня представляли метод init, который, как ожидается, будет вызываться основным модулем при запуске. Представьте себе систему, в которой у вас есть додзё в качестве основного фреймворка, и у вас есть этот код:
// foo.js require(["bar"], function(bar) { function sayHello(node) { console.log("hello " + node); } function attachEventHandlers(dom) { dom.query(".fluxCapacitors").onclick(sayHello); } function init(dom) { bar.init(dom); attachEventHandlers(dom); } return {init: init}; }); // main.js require(["foo"], function(foo) { require.ready(function() { foo.init(dojo); }); });
Это обеспечит настройку всех обработчиков событий и приведение приложения в правильное состояние для использования.
Много обратных вызовов
После того, как вы научились игнорировать многословность анонимных лямбд в JavaScript, они становятся очень удобными инструментами для создания API и вспомогательных функций. В общем, код, который мы пишем, использует много обратных вызовов и вспомогательных функций-обёрток. Я также использую функции, которые генерируют новые функции довольно свободно, делая такие вещи, как карри и подобные аспекты. Довольно типичный пример примерно такой:
function checkForChangesOn(node) { return function() { if(dojo.query(node).length() > 42) { console.log("Warning, flux reactor in flax"); } }; } dojo.query(".clixies").onclick(checkForChangesOn(".fluxes")); dojo.query(".moxies").onclick(checkForChangesOn(".flexes"));
Этот вид абстракции может привести к очень читабельному и чистому JavaScript, если все сделано хорошо. Это также может привести к коду, где очень маленький кусочек может быть. Фактически, один из способов сделать синтаксис немного более терпимым — это использовать создание анонимных функций в фабричных функциях, подобных этой.
Много анонимных объектов
Анонимные объекты отлично подходят для многих вещей. Они работают вместо именованных аргументов и могут быть очень полезны для возврата более одного значения. В нашей кодовой базе мы часто используем анонимные объекты, и это определенно помогает с удобочитаемостью кода.
тестирование
We use Jasmine for unit testing our JavaScript. This works quite well in general. Since this is a fairly typical Java web application we wanted to run it as part of our regular build process. This means we ended up using the JUnit Jasmine runner, which allow us to run these tests outside of browsers and format the results using all the available JUnit tools. Since we’ve tried to make the scripts as modular and small as possible, and also extracting most of the DOM behavior, we have avoided using HTML fixtures. This means our tests are leaning more towards traditional unit tests, rather than BDD style tests — which I’m not sure I’m comfortable with. But with the current size of the application, this is not really a problem.
Seeing as we wanted to test each module in isolation, we wanted to be able to instantiate the RequireJS module with our custom mock dependencies. This ended up not being very easy with RequireJS, so instead of trying to fit in to that model, we just don’t load RequireJS at all during testing, but instead have a top-level require function that just saves away the module function with a well defined name. This means we can instantiate the modules as many times as we want and inject different mocks for different purposes.
In general, Jasmine works well for us, but there are some features missing from the mocking/stubbing framework that makes certain things a bit complicated. One thing I miss a lot is the capability of having stubs returning different valueus depending on the arguments sent in. Some ugly code has been written to get around this.
Open questions
Our current JavaScript process works well for us, but there are still some open things we haven’t done yet. First among these is to integrate JSLint into our build process. I really think that should be there, so I have no excuse. We don’t have tests running inside of browsers. I’m actually OK with this, since we’re trying to do more unit level coverage with Jasmine. Hopefully our acceptance tests cover some of the browser based testing. We are not doing minification at all, and we probably won’t need it based on the current expected usage. For a different audience we would certainly minify everything — this is something RequireJS can do really well though. We don’t have any coverage tool running on our JavaScript either. This is something I’m also uncomfortable with, but I haven’t really found a good tool that allows us to run coverage as part of our CI process yet. I also care more about branch coverage than line coverage, and no tool seems to give you this at the moment.
Summary
JavaScript can be completely OK to work with, provided you treat it as a real language. It’s quite powerful, but we also have a lot of bad habits based on hacking together small things, or just doing what works. As we go forward with JavaScript, this needs to stop. But the good news is that if you’re a decent developer, you shouldn’t have any problem picking anything of this up.
From http://olabini.com/blog/2011/10/javascript-in-the-small/