Модульная организация javascript-кода

Часто бывает так, что на вашем сайте есть куча небольших кусков javascript-кода. Одни отвечают за инициализацию тех или иных библиотек, другие навешивают события на элементы интерфейса, но всех их объединяет одно: они никак не связаны друг с другом.

Попробуем улучшить организацию нашего кода.

Например, возьмём код инициализации библиотеки подсветки синтаксиса:

# тут какие-то ещё функции

syntaxHighlighterInit = ->
  $('pre code').each -> hljs.highlightBlock(this)

# выполняем нужные функции при событии ready
$ ->
  # ...
  syntaxHighlighterInit()

# выполняем нужные функции при событии page:load
$(document).on 'page:load', ->
  # ...
  syntaxHighlighterInit()

Господи, это ужасно, надеюсь никто так не пишет)

С точки зрения организации кода было бы разумно выделить каждый из таких отдельных кусков в свой небольшой модуль. Пусть у такого модуля будет функция инициализации, которая будет выполняться при событии ready (+ на page:load, если вы используете turbolinks). Такой модуль мог бы выглядеть, например, так:

# app/assets/javascripts/modules/syntax_highlighter.js.coffee

window.SyntaxHighlighter = {
  init: ->
    $('pre code').each ->
      hljs.highlightBlock(this)
}

$ -> SyntaxHighLighter.init()
$(document).on 'page:load', SyntaxHighLighter.init()

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

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

# app/assets/javascripts/blog.js.coffee

window.Blog = window.Blog || do ->
  modules = []
  initialized = false

  define = (name, module) ->
    Blog[name] = Blog[name] || do ->
      modules.push(name)
      new_module = module()
      new_module.init() if (Blog._initialized)
      return new_module
    return Blog[name]

  init = ->
    Blog[name].init() for name in modules
    Blog._initialized = true

  return {
    _init: init,
    define: define,
    _modules: modules,
    _initialized: initialized,
    _registered: false
  }

Blog._initialized = false;

$ ->
  if (!Blog._registered)
    $(document).ready(Blog._init)
    $(document).on('page:load', Blog._init)

    window.Blog._registered = true

Тот же модуль инициализации подсветки синтаксиса теперь выглядит так:

# app/assets/javascripts/modules/syntax_highlighter.js.coffee

Blog.define 'SyntaxHighlighter', ->
  return{
    init: ->
      $('pre code').each ->
        hljs.highlightBlock(this)
  }