Глава 10Модули
Начинаещ програмист пише своите програми, както една мравка изгражда хълм, като една цяла част наведнъж, без да мисли за по-голяма структура. Неговите програми ще бъдат, като хлабав пясък. Те могат да издържат известно време, но ако растат прекалено големи и ще се разпаднат.
Осъзнавайки този проблем, програмиста ще започне да прекарва много време мислейки за структурата. Неговите програми ще бъдат неподвижно структурирани, като скални скулптори. Те са солидни, но когато трябва да се променят, ще трябва да се приложи усилие.
Майсторът програмист, знае кога да прилага структура и кога да остави нещата в тяхната проста форма. Неговите програми, са като глина, стегнати и същевременно ковки.”
Всяка програма има форма. В малък мащаб, тази форма се определя чрез разделяне на функции и блоковете вътре в тези функции. Програмистите имат много свобода в начина, по който да структурират своите програми. Формата следва повече вкуса на програмиста, отколкото предназначената функционалност на програмата.
Когато гледате в по-голяма програма, която е цяла, отделни функции започват да се смесват с фона. Такива програми може да се направят по-разбираеми, ако имате по-добър начин за организация.
Модулите разделят програми в групи от код, които по някакъв критерий са свързани заедно. Тази глава разглежда някои от ползите, които това разделяне осигурява и показва техники за изграждане на модули в JavaScript.
Защо модулите помагат
Има редица причини, поради които, автори разделят своите книги в глави и раздели. Тези разделения правят по-лесно за едни читател да види как е изградена книгата и да намира конкретни части от нея, които го интересуват. Те също помагат на автора, като му предоставят ясен фокус за всеки раздел.
Ползите от организиране на програма в няколко файла или модули са сходни. Структурата помага на хората, които все още не са запознати с кода, да намират това, което търсят и прави по-лесно за програмист да пази неща, които са тясно свързани заедно.
Някои програми дори са организирани по модел на традиционен текст с ясно определена цел, с която читателя се насърчава да мине през програмата и с много проза (коментари) въвежда последоватено в описанието на кода. Това прави четенето на програмата много по-малко смущаващо, отколкото четене на неизвестен код, но има недостатък, който е повечето работа за да се създаде. Този стил се нарича грамотно програмиране. Проектите в тази книга могат да се считат за грамотни програми.
Като общо правило, структурирането на нещата има разходи на енергия. В ранните етапи на един проект, когато още не сте съвсем сигурни какво, къде или от какъв вид модули се нуждае програмата, аз подкрепям минималистичния безструктурен подход. Просто сложете всичко там, където ви е удобно, докато стабилизирате кода. По този начин, вие няма да губите време за преместване на части от програмата назад - напред и няма да се заключите случайно в конструкция, която всъщност не е част от вашата програма.
Namespacing
Повечето съвременни езици за програмиране имат ниво на обхват между глобалното (видимо от всеки) и локално (видимо от функцията в която е), JavaScript няма такъв обхват. По този начин, по подразбиране, всичко което трябва да се вижда извън обхвата на функцията се вижда на всякъде до най-високото ниво.
Именно проблемът със namespace
замърсяването с много несвързан код, налага да се споделя единен набор от глобални имена на променливи, за което споменахме в Глава 4, където обекта Math
беше даден, като пример за обект, който действа, като модул чрез групиране на свързана с математиката функционалност.
Въпреки, че JavaScript не предвижда изграждане на действителен модул все още, обектите могат да бъдат използвани за създаване на публично достъпни subnamespaces и функциите могат да се използват за създаване на изолирано, лично пространство във вътрешността на модула. По късно в тази глава, аз ще обсъдя начин за изграждане на разумно и удобно изолиране на namespace
за модули върху примитивните концепции, които JavaScript ни дава.
Повторна употреба
В “плосък” проект, който не е структуриран, като набор от модули, не е ясно кои части от кода са нужни, за да се използва определена функция. В моята програма за шпиониране на враговете (виж Глава 9), аз написах функция за четене на конфигурационни файлове. Ако искам да използвам тази функция в друг проект, трябва да отида и да копирам частите от старата програма, от които имам нужда свързани с функционалноста и да ги поставя в новата си програма. След това, ако намеря грешка в кода, аз ще го оправя само в зависимост от това в коя програма работя в момента и най-вероятно ще забравя да го оправя в другата програма.
След като, имате много такива споделени, дублирани парчета код, постепенно ще откриете, че губите много време и енергия за да ги местите и съхранявате.
Поставянето на самостоятелни парчета функционалност в отделни файлове и модули ги прави по-лесно проследими за обновяване и споделяне, защото всички различни части от кода, който искате да използвате, като модул са заредени от същия действителен файл.
Тази идея получава още мощност, когато отношенията между модулите - това кой модул от кой зависи - са изрично определени. След това можете да автоматизирате процеса за инсталиране и обновяване на външни модули (библиотеки).
Разширявайки тази идея още повече, си представете онлайн услуга, която следи и разпределя стотици хиляди библиотеки, което ви позволява да търсите функционалноста, от която имате нужда и след като я намерите, вашия проект автоматично да я изтегли.
Такава услуга съществува. Тя се нарича NPM (npmjs.org). NPM се състои от онлайн база данни на модули и инструмент за изтегляне и обновяване на модулите, от които зависи вашата програма. Тя се роди в резултат на Node.js среда за JavaScript, която ще обсъдим в Глава 20, но може да бъде полезна при програмирането за браузъра.
Отделяне
Друга важна роля на модулите е изолирането на части от код от друг, по същия начин, който интерфейсите на обектите правят в Глава 6. Един добре проектиран модул ще предостави интерфейс за външен код, за да може да се използва. Тъй като, модулът получава актуализация с корекции и нова функционалност, съществуващият интерфейс остава същия (ако е стабилен), така че и други модули да могат да използват новата подобрена версия без никакви промени в себе си.
Имайте в предвид, че един стабилен интерфейс не означава, че няма нови функции, методи или променливи, които да се добавят към него. Това просто означава, че съществуващата функционалност не е отстранена и нейното значение не се променя.
Един добър интерфейс за модул трябва да даде възможност на модула да расте без да се счупи стария интерфейс. Това означава, че излагайки някои от вътрешните концепции на модула, прави този интерфейс по-мощен и достатъчно гъвкав, за да се прилага в широк спектър от ситуации.
За интерфейси, които излагат единна, фиксирана концепция, като четец на конфигурационен файл, този дизайн идва естествено. За други, като текстов редактор, който има много различни аспекти, които се нуждаят от външен код за достъп(съдържание, стайлинг, действия на потребител и т.н.) се изисква внимателно планиране.
Използване на функциии, като namespaces
Функциите са единствените неща в JavaScript, които създават нов обхват. Така че, ако искаме нашите модули да имат свой собствен характер, ще трябва да ги основем на функции.
Помислете за този тривиален модул, за асоцииране на имена с номера за дните от седмицата, върнати от getDay
метода на Date
обекта:
var names = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; function dayName(number) { return names[number]; } console.log(dayName(1)); // → Monday
Функцията dayName
е част от интерфейса на модула, но променливата names
не е. Бихме предпочели да не я изсипваме в глобалния обхват.
var dayName = function() { var names = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; return function(number) { return names[number]; }; }(); console.log(dayName(3)); // → Wednesday
Сега names
е локална променлива в (анонимна) функция. Тази функция се създава и веднага се извиква и нейната върната стойност (действителната dayName
функция) се съхранява в променлива. Ние можем да имаме страници и страници код в тази функция, със сто локални променливи и всички те ще бъдат вътрешени за нашия модул, видими за самия модул, но невидими от външния код.
Можем да използваме подобен модел да изолираме код изцяло от външния свят. Следващия модул регистрира стойност на конзолата, но всъщност не предоставя никакви стойности за ползване към други модули.
(function() { function square(x) { return x * x; } var hundred = 100; console.log(square(hundred)); })(); // → 10000
Този код просто извежда квадрата на 100, но в реалния свят това може да бъде един модул, който добавя метод към някой прототип или създава нещо в уеб страница. Той е увит във функция, за да попречи на променливите, които използва вътрешно да замърсят глобалния обхват.
Защо увиваме namespace функциите в чифт скоби? Това е свързано с приумица в синтаксиса на JavaScript. Ако израза започва с ключовата дума function
, това е функционален израз. Обаче, ако изявление започва с function
, това е декларация на функция, която изисква име и не е израз, който може да се извика, като се напишат скоби след него. Може да мислите за допълнителните увиващи скоби, като трик да се принуди функцията да се тълкува, като израз.
Обекти, като интерфейс
Сега си представете, че искаме да добавим още една функция в нашия модул на седмицата, която прехвърля името на деня в номер. Не можем просто да върнем функция вече, трябва да увием двете функции в един обект.
var weekDay = function() { var names = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; return { name: function(number) { return names[number]; }, number: function(name) { return names.indexOf(name); } }; }(); console.log(weekDay.name(weekDay.number("Sunday"))); // → Sunday
За по-големи модули, събирането на всички изнасяни стойности в обект в края на функцията, става неудобно, тъй като много от изнесените функции има вероятност да бъдат големи и предпочитаме да ги напишем някъде другаде, свързани с вътрешен код. Удобна алтернатива е да се направи обект (условно наречен exports
) и да добавим свойства за всеки път, когато определяме нещо, което трябва да бъде изнесено. В следващия пример, модул функцията взема интерфейс на обект, като аргумент, позволявайки на кода извън функцията да го създаде и съхранява в променлива. (Извън функцията, this
се отнася до глобалния обхват на обекта).
(function(exports) { var names = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; exports.name = function(number) { return names[number]; }; exports.number = function(name) { return names.indexOf(name); }; })(this.weekDay = {}); console.log(weekDay.name(weekDay.number("Saturday"))); // → Saturday
Отделяне от глобалния обхват
Предишният модел обикновено се използва от JavaScript модули, предназначени за браузъра. Модулът ще регистрира една глобална променлива и ще увие нейния код във функция, така че да има свое собствено namespace. Но този модел все още създава проблеми, ако се случи множество модули да имат претенция към същото име или ако искате да заредите две версии на даден модул успоредно една до друга.
С малко връзки, можем да създадем система, която позволява на един модул да поиска директно интерфейса на обект от друг модул, без да минава през глобалния обхват. Нашата цел е require
функция, на която, когато се подаде името на модула, ще зареди файл (от диск или от мрежата, в зависимост от платформата върху ще се изпълнява) и ще върне съответната стойност на интерфейса.
Този подход решава проблемите, споменати по-рано и има допълнителна полза за вземане на зависимости в нашата програма, което прави трудно случайно да се използва някой друг модул без да се посочва, че имаме нужда от него.
За require
имаме нужда от две неща. Първо ни трябва функция readFile
, която връща съдържанието на даден файл, като string. (Тази функция не е налична в стандартния JavaScript, но различни среди за JavaScript, като браузър и Node.js, предоставят свои собствени начини за достъп до файловете. За сега нека просто се преструваме, че имаме тази функция.) Второ, трябва действително да можем да изпълним този string, като JavaScript код.
Оценяване на данни, като код
Има няколко начина да се вземат данни от string код и да се стартират, като част от настоящата програма.
Най-очевидният начин е със специалният оператор eval
, който ще изпълни поредицата от код в текущия обхват. Това обикновено е лоша идея, защото ще се разпадне от някое от нормалните свойства, които обикновено имат обхват, когато се изолират от външния свят.
function evalAndReturnX(code) { eval(code); return x; } console.log(evalAndReturnX("var x = 2")); // → 2
По добър начин за интерпретиране на данни, като код, е да се използва Function
конструктора. Той взема два аргумента: string, съдържащ разделен със запетаи списък от имена на аргументи и string, съдържащ тяло на функция.
var plusOne = new Function("n", "return n + 1;"); console.log(plusOne(4)); // → 5
Това е точно онова, от което имаме нужда за нашите модули. Ние можем просто да увием кода на модула в една функция и обхвата на тази функция да стане обхвата на нашия модул.
Require
Следващото е минимално изпълнение на require
:
function require(name) { var code = new Function("exports", readFile(name)); var exports = {}; code(exports); return exports; } console.log(require("weekDay").name(1)); // → Monday
Тъй като, new Function
конструктора увива кодa на модула във функция, ние не трябва да пишем функция за опаковане на namespace функция в самия файл на модула. И тъй като, правим exports
аргумент за модул функцията, самия модул не трябва да го декларира. Това премахва много елементарни неща от нашия примерен модул.
var names = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; exports.name = function(number) { return names[number]; }; exports.number = function(name) { return names.indexOf(name); };
Когато използваме този модел, един модул обикновено започва с няколко декларации на променливи, от които модулите зависят.
var weekDay = require("weekDay"); var today = require("today"); console.log(weekDay.name(today.dayNumber()));
Опростеното изпълнение на require
дадено преди това, има няколко проблема. От една страна, то ще зареди и стартира модул всеки път, когато се извиква require
, така че ако няколко модула имат същата зависимост или извикването към require
се постави вътре във функция, която се извиква многократно, се губи време и енергия.
Това може да бъде решено чрез съхраняване на модулите, които вече са заредени в обект и просто връщаме съществуващата стойност, когато е заредена многократно.
Вторият проблем е, че не е възможно за модул да изнася директно стойност, различна от exports
обекта, като функция. Например, от един модул може да искате да експортирате само конструктора на типа на обекта, който го дефинира. Точно сега, той не може да направи това, защото require
винаги използва exports
обекта, който създава експортната стойност.
Традиционното решение за това е да се осигури модул с друга променлива module
, която е обект, който има свойството exports
. Това свойство първоначално посочва празен обект, създаден от require
, но може да бъде заменен с друга стойност, за да експортира нещо друго.
function require(name) { if (name in require.cache) return require.cache[name]; var code = new Function("exports, module", readFile(name)); var exports = {}, module = {exports: exports}; code(exports, module); require.cache[name] = module.exports; return module.exports; } require.cache = Object.create(null);
Сега имаме модулна система, която използва една глобална променлива (require
), за да позволи на модулите да се намират и използват взаимно, без да минават през глобалния обхват.
Този стил на модулна система се нарича CommonJS modules, след псевдо стандарт, който пръв го е посочил. Тя е вградена система в Node.js. Реалните реализации правят много повече от примера, който показах. Най-важното е, че те имат много по-интелигентен начин за преминаване на модул към действителна част на код, който позволява и двете: пътища и файлове, по отношение на текущите файлове и имена на модули, които сочат директно към локално инсталираните модули.
Бавно зареждане на модули
Въпреки, че е възможно използването на CommonJS стил при писане на JavaScript за браузъри, той е малко по-ангажиращ. Причината за това е, че четенето на файл (модул) от Интернет е малко по-бавно, отколкото четене от твърдия диск. Докато скрипт се изпълнява в браузъра, нищо друго не може да се случи на интернет страницата, на която той работи, по причини които ще станат известни в Глава 14. Това означава, че при всяко извикване на require
да отиде и извади нещо от някакъв далечен уеб сървър, страницата ще замръзне за болезнено дълго време при зареждането на скриптовете.
Един начин да се справите с този проблем е да стартирате програма, като Browserify в кода си, преди да сте го връчили на уеб страницата. Тя ще гледа за извиквания към require
, решавайки всички зависимости и събирайки необходимия код в един голям файл. В самия сайт просто може да се зареди този файл, за да получи всички модули, от които се нуждае.
Друго решение е да увиете кода, който изгражда вашия модул във функция, така че програмата за зареждане на модул, първо да зареди неговите зависимости във фонов режим и след това да извика функцията за инициализиране на модул, след като зависимостите са вече заредени. Това е системата за асинхронни модули - Asynchronous Module Definition (AMD).
Нашата тривиална програма със зависимости, ще изглежда така в AMD:
define(["weekDay", "today"], function(weekDay, today) { console.log(weekDay.name(today.dayNumber())); });
Функцията define
е в центъра на този подход. Тя взема първия модул с масив от имена и след това функция, която приема само един аргумент за всяка зависимост. Тя ще зареди зависимостите(ако още не са заредени) във фонов режим, което позволява на страницата да продължи да работи, докато файловете се изтеглят. След като, всички зависимости са заредени, define
ще извика функцията, която и е дадена, с интерфейсите на тези зависимости, като аргументи.
Модулите, които се зареждат по този начин трябва да съдържат извикване към define
. Стойността, която се използва за техния интерфейс е тази, която се връща от функцията подадена към define
. Сега тука е модулът weekDay
отново:
define([], function() { var names = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; return { name: function(number) { return names[number]; }, number: function(name) { return names.indexOf(name); } }; });
За да мога да покажа минимално изпълнение на define
, ще се преструваме, че имаме backgroundReadFile
функция, която взема име на файл и функция и извиква функцията със съдържанието на файла, веднага след, като приключи зареждането. (Глава 17 ще ви обясня, как да пишете такива функции.)
За проследяване на модулите, докато са натоварени, изпълнението на define
ще използва обекти, които описват състоянието на модулите и ни казват дали те са на разположение и осигуряват техния интрефейс, когато са.
Функцията getModule
, когато и се даде име ще върне този обект и ще гарантира, че модулът планирано е зареден. Тя използва кеш обект, за да се избегне натоварване на същия модул два пъти.
var defineCache = Object.create(null); var currentMod = null; function getModule(name) { if (name in defineCache) return defineCache[name]; var module = {exports: null, loaded: false, onLoad: []}; defineCache[name] = module; backgroundReadFile(name, function(code) { currentMod = module; new Function("", code)(); }); return module; }
Предполагаме, че заредения файл съдържа също и (едно) извикване към define
. Променливата currentMod
се използва за да отрази това извикване към модул обекта, който в момента се зарежда, така че да може да се актуализира този обект, когато завърши зареждането. Ние ще се върнем на този механизъм в един момент.
Самата функция define
използва getModule
за извличане или създаване на модул обекти за зависимостите на текущия модул. Нейната задача е да планира moduleFunction
(функцията, която съдържа действителния код на модула) да се стартира, когато тези зависимости са заредени. За тази цел тя определя функцията whenDepsLoaded
, която да добавя към onLoad
масива всички зависимости, които все още не са заредени. Тази функция се връща незабавно, ако има все още незаредени зависимости, така че ще извърши действителната работа само веднъж, когато последната зависимост приключи със зареждането. Тя също така се извиква незабавно от самия define
в случай, че няма зависимости, които трябва да бъдат заредени.
function define(depNames, moduleFunction) { var myMod = currentMod; var deps = depNames.map(getModule); deps.forEach(function(mod) { if (!mod.loaded) mod.onLoad.push(whenDepsLoaded); }); function whenDepsLoaded() { if (!deps.every(function(m) { return m.loaded; })) return; var args = deps.map(function(m) { return m.exports; }); var exports = moduleFunction.apply(null, args); if (myMod) { myMod.exports = exports; myMod.loaded = true; myMod.onLoad.forEach(function(f) { f(); }); } } whenDepsLoaded(); }
Когато всички зависимости са на разположение, whenDepsLoaded
извиква функция, която държи модула, като му дава интерфейси на зависимостите, като аргументи.
Първото нещо, което define
прави е да съхрани стойността, която currentMod
имa, когато е извикан в променливата myMod
. Не забравяйте, че getModule
точно преди оценяването на кода за модула, съхранява съответния модул обект в currentMod
. Това позволява на whenDepsLoaded
да съхранява върнатa стойност от функцията на модула в свойството exports
на модула, определя свойството loaded
на модула на true и извиква всички функции, които чакат модула да се зареди.
Този код е много по-труден за следване от require
функцията. Неговото изпълнение не следва прост, начертан план. Вместо това, се създават множество операции, които да се случат в някакъв неопределен момент в бъдещето, което замъглява пътя на кода, когато се изпълнява.
Истинското изпълнение на AMD, е доста по-интелигентно при разрешаване на имена на модули към действителни URL адреси и като цяло по-стабилно, отколкото това показано по-рано. RequireJS (requirejs.org) осигурява изпълнението на този популярен стил за модулно зареждане.
Интерфейс дизайн
Проектиране на интерфейси за модули и типове обекти е един от най-фините аспекти на програмирането. Всяко не тривиално парче функционалност може да се моделира по различни начини. Намирането на начин, който работи добре изисква проницателност и далновидност.
Най-добрият начин за научаване на добрия дизайн за интерфейс е да се използват много интерфейси, някои добри, някои лоши. Опитът ще ви научи, какво работи и какво не. Никога не допускайте, че трудният интерфейс е “такъв, какъвто е”. Поправете го или го увийте в нов интерфейс, който работи по-добре.
Предвидимост
Ако програмистите могат да прогнозират начина на работа на интерфейса, те (или вие) няма да се отклоняват от целта толкова често при необходимостта да търсят, как да го използват. По този начин се опитват да следват конвенции. Когато има друг модул или част от стандартна JavaScript среда, която прави нещо подобно на това, което се опитвате да приложите, може би е добра идея да направите вашия интерфейс по подобие на съществуващ интерфейс. По този начин, ще се почувствате близки с хора, които знаят за съществуващия интерфейс.
Друга област, където предсказуемоста е важна за действителното поведение на вашия код. Може да се изкушите да направите ненужно сложен интерфейс с основанието, че е по-удобен за използване. Например, вие можете да приемете всички видове на различни типове и комбинации от аргументи, за да направите “правилното” нещо за всички тях. Или пък да предоставите десетки специализирани функции, които осигуряват удобство с малко по различен вкус, към функционалноста на модула. Това може да направи кода, на който се основава вашия интерфейс малко по-кратък, но същевременно ще го направи много по-труден за хората, които да изградят по-ясен смислен модел на поведение на модула.
Composability
Във вашите интерфейси се опитайте да използвате най-простите структури от данни, ако е възможно и направете вашите функции да правят едно, ясно нещо. Винаги, когато е практично използвайте чисти функции (виж Глава 3).
Например, не е необичайно за модули да предоставят свой собствен масив, като обект - колекция, със собствен интерфейс за преброяване и извличане на елементи. Такива обекти нямат map
и forEach
методи, както и всички съществуващи функции, които очаква истинкия масив и няма да можете да работите с тях. Това е пример за лоша composability (компонентност) - модулът не може да бъде лесно съставен с друг код.
Един пример ще бъде модул за проверка на правописа на текст, който може да ни потрябва, когато искаме да напишем текстов редактор. Проверката на правописа може да се направи да работи директно върху сложни структури от данни с използване на редактор и директно да извиква вътрешните функции на този редактор, за да има потребителя избор между предложения за правописа. Ако вървим по този път, модулът не може да се използва с други програми. От друга страна, ако ние определяме интерфейса за проверка на правописа, така че да можем да го подадем на прост string и той да върне позиция в този string, където е възможно да има правописна грешка, заедно с масив от предложения за корекции, тогава имаме интерфейс, който може да бъде в състава на други системи, защото strings и масиви са винаги на разположение в JavaScript.
Слоеве от интерфейси
При проектирането на интерфейс за комплексна част от функционалност - например, за изпращане на електронна поща - често ще се сблъсквате с една дилема. От една страна не искате да претоварите потребителя на вашия интерфейс с подробности. Той не трябва да учи вашия интерфейс 20 минути, преди да изпрати имейл. От друга старана, вие не искате да скриете всички детайли, когато хората трябва да правят по-сложни неща с вашия модул, той трябва да бъде в състояние за това.
Често решението е да се осигурят два интерфейса: един подробен ниско ниво за сложни ситуации и един високо ниво за рутинна употреба. Втория обикновено лесно може да бъде построен с помоща на инструментите предоставени от първия. В модула за поща, интерфейса за високо ниво би могъл да бъде само една функция, с която съобщава адреса на подателя и адреса на получателя и след това изпраща имейл. Интерфейса за ниско ниво ще позволи пълен контрол над пощата: заглавие, прикачени файлове, HTML поща и т.н.
Резюме
Модулите предоставят структура за по-големи програми, чрез отделяне на кода в различни файлове и namespaces. Давайки на тези модули добре дефинирани интерфейси ги прави по-лесни за използване и преизползване и дава възможност да се продължи използването им, когато модулът еволюира.
Въпреки, че езика на JavaScript е характерно безполезен, когато става въпрос за модули, гъвкавите функции и обекти, които той предоставя дават възможност да се определят по-скоро хубави модулни системи. Обхвата на функциите може да се използва, като вътрешни namespaces за модула, а обектите могат да се използват за съхранение на набора от експортни стойности.
Има два популярни добре определени подхода към такива модули. Единият се нарича CommonJS Modules и се върти около require
функцията, която извлича модул по име и връща интерфейса му. Другата се нарича AMD и използва define
функцията, която взема масив с имена на модули и функции, и след като зареди модулите изпълнява функция с техните интерфейси, като аргументи.
Упражнения
Имената на месеца
Напишете прост модул подобен на weekDay
, който може да преобразува числата на месеца (с нулева база, като в типа Date
) в имена и имената обратно в числа. Дайте му свое собствено namespace, тъй като ще се нуждае от вътрешен масив с имената на месеците и използвайте обикновен JavaScript, без зареждане на модулна система.
// Your code here. console.log(month.name(2)); // → March console.log(month.number("November")); // → 10
Това е почти точно следване на модула weekDay
. Функционалния израз извикан веднага, опакова променливата, която държи масива с имената, както и две функции, които трябва да се експортират. Функциите са поставени в един обект и се връщат. Върнатия обект на интерфейса се съхранява в променливата month
.
Връщане към електронния живот
Надявам се, че споменът за Chapter 7 е още пресен в съзнанието ви, да се върнем обратно към системата проектирана в тази глава и да излезем с начин за разделяне на кода в модули. За да освежим паметта си, това са функциите и типовете, определени в тази глава по реда на появяването им:
Vector Grid directions directionNames randomElement BouncingCritter elementFromChar World charFromElement Wall View WallFollower dirPlus LifelikeWorld Plant PlantEater SmartPlantEater Tiger
Не преувеличавайте и не създавайте твърде много модули. Една книга, която започва нова глава на всяка страница, вероятно ще ви се качи на нервите, дори и само заради пространството, което губи от новите заглавия. По същия начин, да отваряте 10 файла, за да прочетете един проект, не е полезно. Целта ви е от 3 до 5 модула.
Можете да определите, някои функции да станат вътрешни за техния модул и по този начин недостъпни за други модули.
Няма правилен отговор тука. Модулната организация до голяма степен е въпрос на вкус.
Ето какво измислих. Сложил съм скоби около вътрешните функции.
Module "grid" Vector Grid directions directionNames Module "world" (randomElement) (elementFromChar) (charFromElement) View World LifelikeWorld directions [reexported] Module "simple_ecosystem" (randomElement) [duplicated] (dirPlus) Wall BouncingCritter WallFollower Module "ecosystem" Wall [duplicated] Plant PlantEater SmartPlantEater Tiger
Аз имам реекспорт на directions
масива от grid
модула от world
, така че изградените модули на (екосистемата) не трябва да знаят или се притесняват за съществуването на
grid
модул.
Също така, дублирам две общи и малки помощни стойности (randomElement
and Wall
), тъй като те се използват като вътрешна информация в различни контексти и не принадлежат към интерфейсите за тези модули.
Кръгови зависимости
Сложна тема в управлението на зависимостите са кръговите зависимости, където модул А зависи от В и В също зависи от А. Много модулни системи просто забраняват това. CommonJS позволява ограничена форма: тя работи толкова дълго с модулите, докато не замени техния exports
обект по подразбиране с друга стойност и стартира достъп до всеки друг интерфейс само, след като завърши зареждането.
Сещате ли се за начин, по който може да се осъществи подръжка за тази функция? Погледнете назад към определението на require
и помислете каква функция трябва да направи, за да позволите това.
Номерът е да добавите exports
обект, създаден за модул към require
кеша, преди действителното стартиране на модула. Това означава, че модула все още не е имал шанс да презапише module.exports
, така че ние не знаем дали той може да иска да изнася някаква друга стoйност. След зареждането, кеша на обекта е презаписан от module.exports
, който може да бъде различна стойност.
Но ако в процеса на зареждане на модула е зареден втори модул, той ще пита за първия модул за неговия exports
обект по подразбиране, който най-вероятно е празен в този момент и ще бъде в кеша, а втория модул ще получи препратка към него. Ако той не се опита да направи нещо с този обект до завършване на зареждането на първия модул, нещата ще работят.