new takyam();

Qiitaぽい話はQiitaに書いていくことにする気がする http://qiita.com/takyam

僕が考えた最強の「CoffeeScriptで書くBackbone.js」

最近業務でBackboneを使ってるので、その中で考えた最強の構成。

CakefileとソースサンプルはBitbucketに上げました。

Compile-coffee-scripts-in-(sub)-directories
https://bitbucket.org/takyam/compile-coffee-scripts-in-sub-directories

ソースファイルの構成

src
├── 001_init
│   ├── 001_setup.coffee
│   └── 002_helpers.coffee
├── 002_common
│   ├── 001_init
│   │   └── setup.coffee
│   ├── 002_config
│   ├── 003_events
│   ├── 004_models
│   │   ├── 001_post.coffee
│   │   └── 002_comment.coffee
│   ├── 005_collections
│   │   ├── 001_posts.coffee
│   │   └── 002_comments.coffee
│   ├── 006_views
│   │   ├── 001_post.coffee
│   │   └── 002_comment.coffee
│   ├── 007_routers
│   └── main.coffee
└── 003_top
    ├── 001_init
    │   └── setup.coffee
    ├── 002_config
    ├── 003_events
    ├── 004_models
    │   ├── 001_post.coffee
    │   └── 002_comment.coffee
    ├── 005_collections
    │   ├── 001_posts.coffee
    │   └── 002_comments.coffee
    ├── 006_views
    │   ├── 001_post.coffee
    │   └── 002_comment.coffee
    ├── 007_routers
    └── main.coffee

https://bitbucket.org/takyam/compile-coffee-scripts-in-sub-directories/src/master/src

まず、

  • 001_init
  • 002_common
  • 003_top

のように、初期設定、共通処理、各ページ別みたいにディレクトリきります。
そして、それぞれの中に、

  • 001_init # 初期処理
  • 002_config # 設定系
  • 003_events # Backbone.Event
  • 004_models # Backbone.Model
  • 005_collections #Backbone.Collection
  • 006_views # Backbone.View
  • 007_routes # Backbone.Router
  • main.coffee # メインの実行処理(main:void的な)

を作ります。

この構成の場合のアプリの作り方

この構成のポイントとしては、001_init/setup.coffee のなかで、 Appオブジェクトを定義して、基本的にすべてのものをAppオブジェクト内に格納しちゃうって事でしょうか。

#001_init/001_setup.coffee
App =
  helpers: {}
  pages: {}

https://bitbucket.org/takyam/compile-coffee-scripts-in-sub-directories/src/master/src/001_init/001_setup.coffee

こんな感じでAppオブジェクトを用意してあげて、
commonや各ページ用の処理は、それぞれの 001_init/setup.coffee のなかで、
それぞれ用のオブジェクトをAppに紐づけて定義してあげます。

例えば common(共通処理) の場合

# 002_common/001_init/setup.coffee
App.common =
  config: {}
  events: {}
  models: {}
  collections: {}
  views: {}
  routers: {}

https://bitbucket.org/takyam/compile-coffee-scripts-in-sub-directories/src/master/src/002_common/001_init/setup.coffee

このように App.common オブジェクトを作成し、
App.common に関するものは、この下に追加できるようにします。

CoffeeScript の class名には、 .(ドット)でチェーンしたオブジェクトを指定できるので、 以下のようにclassを定義します。

# 002_common/006_views/001_post.coffee
class App.common.views.post extends Backbone.View
  el: '.post'
  initialize: =>
    @$save_btn = @$el.find('.save_btn') #適当な処理

https://bitbucket.org/takyam/compile-coffee-scripts-in-sub-directories/src/master/src/002_common/006_views/001_post.coffee

ポイントは先頭の、

class App.common.views.post extends Backbone.View

です。

こうする事で、ネームスペースを汚さずにViewでもModelでも追加していく事ができます。

で、最後にrun()させると。

$ ->
  new App.common.views.post()
  new App.common.views.comment()
  log('common finished')

特定のページでのみ実行する処理

余談ですが、

# 001_init/002_helpers.coffee

# URLと渡したパスが一致すればtrue、一致しなければfalseを返す
# @args string|RegExp path
# @return boolean
App.helpers.match_url = (path) ->
  if typeof(path).toLowerCase() is 'string'
    if path is '/'
      path = new RegExp('^/$')
    else
      path = new RegExp('^/' + path.replace(/(^\/|\/$)/g, '') + '/?$')
  location.pathname.match(path) isnt null

こんな感じのメソッドを用意しておいて、

# 003_top/main.coffee
if App.helpers.match_url('/')
  $ ->
    new App.pages.top.views.post()
    new App.pages.top.views.comment()
    log('top finished')

みたいな感じにすると、URLベース(ハッシュじゃなくてURL)で、
お手製ルーティングみたいな事ができるので良い感じです。

サブディレクトリのコンパイル

閑話休題。

そんなこんなで僕が考えた最強の「CoffeeScriptで書くBackbone.js」は出来るわけですが、
CoffeeScriptのコンパイラがコマンドラインレベルで、標準で用意しているのは、
特定ディレクトリ以下の *.coffee をコンパイルするだけです。

coffee -j production.js -cw src/*.coffee

こんな感じ。

ただ僕が考えた最強の(ryでは、サブディレクトリ以下もマージする必要があるため、
「coffeeコマンドで簡単にやるお(^ρ^)」はできません。

なので、簡単なCakefileを作りました。

fs = require('fs')
coffee = require('coffee-script')

source = ''

get_text = (path) ->
  text = ''
  stat = fs.statSync(path)
  if stat.isDirectory()
    files = fs.readdirSync(path).sort()
    path_base = path.replace(/\/$/, '') + '/'
    for file in files
      text += "\n\n" + get_text(path_base + file)
  else if stat.isFile()
    text += "\n\n" + fs.readFileSync(path, 'utf-8')

  return text

option '-s', '--src [DIR]', 'ソースとなるCoffeeScriptが格納されたディレクトリ'
option '-o', '--output [FILE]', '出力先のJSファイル名'

task 'compile', '対象のディレクトリをコンパイルします(default >> -s ./src -o production.js)', (options) ->
  src = options.src or './src'
  output = options.output or './production.js'

  src_full = __dirname + '/' + src
  output_full = __dirname + '/' + output

  fs.exists src_full, (exists) ->
    if !exists
      console.log 'Target isn\'t exists.'
      return false

    fs.stat src_full, (err, stats) ->
      if err isnt null
        console.log err
        return false

      if !stats.isDirectory()
        console.log 'Target isn\'t directory.'
        return false

      source = get_text(src_full)

      fs.open output_full, 'w', (err, fd)->
        if err isnt null
          console.log err
          return false

        fs.write fd, coffee.compile(source)

https://bitbucket.org/takyam/compile-coffee-scripts-in-sub-directories/src/master/Cakefile

syncしまくりなうえにno commentでお恥ずかしいのですが、
動きゃいいんだよ動けば。

READMEにも書いてますが、npm install したうえで、

cake compile

すると、src/ 以下のすべてのCoffeScriptをマージした上でコンパイルして、./production.js として吐き出すようになっています。
※出力先とソースディレクトリはオプションで変えられます

全処理が1つの javascript に

こうする事で、1JSファイルで、全ページ共通で1つのJSを読み込むだけでよくなります。
これはメリデメある事だとは思いますが、多くの場合効果的ではないでしょうか?

さらに、全CoffeeScriptから、共通処理を App.common.hoge() のように呼び出す事ができるのは、結構便利なのではないかと考えています。

これまでの場合、hoge_fuga() 関数を作った場合、必ず window.hoge_fuga = hoge_fuga しなければならず、各Scriptでそれをやると、名前空間の重複などの確認を、全ソースを確認しないと正確には行えませんでした。

今回のような構成にする事で、共通処理も1つのfunction()内に閉じられるので、名前空間の重複などもそこまで気にする事なく行えて幸せかなぁと。

まとめ

というわけで、サブディレクトリ切りまくったほうが、超巨大なJS開発においては幸せだと思ったので、思いつきでサンプルアプリとCakefileを作ってみました。

※サンプルアプリはそれっぽく書いてるだけで動くかどうか知りません(^ρ^)

CoffeeScript+Backbone.jsの良い運用方法とかだれか教えてください><

参考

大規模JSでのBackbone.js/CoffeeScript について考えてみた - mizchi log