middleman を読む

middleman を読む手順

結構複雑で、難しいです。

  • rack / middleware
  • module, extend など ruby の詳細
  • hook の動き

などについての知識が必要になります。

根幹となる機能も “core-extensions” と拡張機能扱いで実装されているので、 追っていくのがなかなか大変です。

手順として、

  • まず console で触る
  • サンプルスクリプトを書き、簡単な拡張機能を書いてみる
  • middlemanのソース一式を手元に落とし、pry や printf デバッグなどでひとつずつ追っていく

という方法を取ります。

console で中身を見る

まずは middleman console で中身を見ながら追っていきます。

インストールと下準備

シンプルなシステム構成で init します。pry はソースを追うのに必要なのでいれておきます。

% middleman init mm-tets & cd mm-test
% vi Gemfile
gem 'pry-byebug'
gem 'middleman-pry'
gem 'rb-readline'
gem 'therubyracer'
% bundle

app (Middleman::Application)

console を起動し、self を見ます。

% bundle exec middleman console --verbose
[1] pry(#<Middleman::Application::MiddlemanApplication1>)> self
=> #<Middleman::Application:0x70265526584800>
[2] pry(#<Middleman::Application::MiddlemanApplication1>)> self.class
=> Middleman::Application::MiddlemanApplication1

Middleman::Application というのがアプリケーション用のクラス。箱。 これに後述の拡張機能を登録しフックを仕込み、順次駆動させていく形になります。 このクラスにextend バリバリつかって module を組み込んでいくため、 他のリクエスト、プロセスとの混乱しないよう、 リクエストごとに Middleman::Application::MiddlemanApplication1というように番号を振った派生クラスを作り、 それを利用していくことになります。

sitemap (Middleman::Sitemap::Store), resources

次に sitemap と sitemap.resources を見ます:

[3] pry(#<Middleman::Application::MiddlemanApplication1>)> sitemap
=> #<Middleman::Sitemap::Store:0x007fcfef180878
 @_cached_metadata={},
 @_lookup_by_destination_path=
  {"images/background.png"=>
    #<Middleman::Sitemap::Resource:0x007fcff0db7410
     @app=#<Middleman::Application:0x70265526584800>,
     @destination_path="images/background.png",
     @local_metadata={:options=>{}, :locals=>{}, :page=>{}, :blocks=>[]},
     @path="images/background.png",
     @source_file="/vagrant/source/mm-test/source/images/background.png",
     @store=#<Middleman::Sitemap::Store:0x007fcfef180878 ...>>,
   "images/middleman.png"=>
    #<Middleman::Sitemap::Resource:0x007fcff0db6bf0
     @app=#<Middleman::Application:0x70265526584800>,
     ...
[4] pry(#<Middleman::Application::MiddlemanApplication1>)> sitempap.resources
=> [#<Middleman::Sitemap::Resource:0x007fcff0db7410
  @app=#<Middleman::Application:0x70265526584800>,
  @destination_path="images/background.png",
  @local_metadata={:options=>{}, :locals=>{}, :page=>{}, :blocks=>[]},
  @path="images/background.png",
  @source_file="/vagrant/source/mm-test/source/images/background.png",
  @store=
   #<Middleman::Sitemap::Store:0x007fcfef180878
    @_cached_metadata={},
    @_lookup_by_destination_path=
     {"images/background.png"=>
       #<Middleman::Sitemap::Resource:0x007fcff0db7410 ...>,
      "images/middleman.png"=>
  • Middleman::Sitemap::Store:source/ 以下のデータを管理するコンテナクラス、
  • Middleman::Sitemap::Resource:それぞれのファイルなどのリソースのコンポーネントクラス。

この2つがキモとなります。

特定のリソースを取り出すには、sitemap.find_resource_by_path() を使います:

[5] pry(#<Middleman::Application::MiddlemanApplication1>)> page = sitemap.find_resource_by_path("index.html")
=> #<Middleman::Sitemap::Resource:0x007f2bd08ecda0
 @app=#<Middleman::Application:0x69913064777960>,
 @destination_path="index.html",
 @local_metadata={:options=>{}, :locals=>{}, :page=>{}, :blocks=>[]},
 @path="index.html",
 @source_file="/vagrant/source/mm-test/source/index.html.erb",
 @store=
  #<Middleman::Sitemap::Store:0x007f2bceda42c8
   @_cached_metadata={},
   @_lookup_by_destination_path=
   ...

これで page で特定のリソース(“/index.html”)のデータやパスなどの情報にアクセスできます。また render でテンプレートに従い加工されたテキストが返ります。

[6] pry(#<Middleman::Application::MiddlemanApplication1>)> page.data
=> {"title"=>"Welcome to Middleman"}
[7] pry(#<Middleman::Application::MiddlemanApplication1>)> page.path
=> "index.html"
[8] pry(#<Middleman::Application::MiddlemanApplication1>)> page.source_file
=> "/vagrant/source/mm-test/source/index.html.erb"
[17] pry(#<Middleman::Application::MiddlemanApplication1>)> page.render
=> "<!doctype html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    \n    <!-- Always force latest IE rendering engine or request Chrome Frame -->\n...

Middleman::Application クラス

Middleman::Application クラスを見てみます。

define hooks

37行目あたり にhook の定義がされています。

    # Before request hook
    define_hook :before

    # Ready (all loading and parsing of extensions complete) hook
    define_hook :ready

    # Runs before the build is started
    define_hook :before_build

    # Runs after the build is finished
    define_hook :after_build

ここでは4つのみですが、後ほどもっと出てきます。

core-extensions の register

142行目あたり から、いよいよ extensions の register が行われていきます。

まず、"include Middleman::CoreExtensions::Extensions" してますが、このモジュールには register()関数が定義されてて、これを使うことにより拡張機能(extension)を登録(register)することができます。詳しくは後述します。

このregister 関数を使って Middleman::CoreExtensions::Request, ShowException, FileWatcher… などの「コアな」extension をどんどん登録してきます。

拡張機能の仕組み

主な機能としては、

  • ヘルパー関数の提供
  • フックの仕込み

があります。

簡単な拡張機能を作りながら、動きを追っていきます。

ヘルパー関数

helpers/hello.rb

簡単なのは、helpers/ 以下のファイルに規則にそって定義することです:

% vi helpers/hello_helpers.rb
module HelloHelpers
  def hello
    "hello world"
  end
end

こうすると、Middleman::CoreExtensions::ExternalHelpers で require され Middleman::Extensionのhelpers を使って module し app 内に組み込まれます(app.class.send(:include, m))。

簡単な extension

次に extension でやってみます。

% vi extensions/hello/middleman-hello.rb
module Middleman
  module Hello
    module Helpers
      def hello
        "hello world"
      end
    end
    class Extension < Middleman::Extension
      helpers do 
        include Helpers
      end
    end
  end
end

Middleman::Extensions.register(:hello) do
  Middleman::Hello::Extension
end

% vi config.rb
require 'extensions/middleman-hello'
activate :hello

Middleman::Extension から派生させたクラスを作り、同様に helper 関数を組み込みます。

config.rb でそれを require し activate (後述)させれば、hello() 関数が使えるようになります。

手を加える

module Middleman
  module Hello
    class << self
      def registered(app)
        $stderr.puts("- middleman-hello registered")
        app.send :include, InstanceMethods
      end
    end
    module InstanceMethods
      def hello
        "hello world"
      end
    end
  end
end

Middleman::Extensions.register(:hello) do
  Middleman::Hello
end