middleman を読む

middleman を読む手順

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

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

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

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

手順として、

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

という方法を取ります。

console で中身を見る

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

インストールと下準備

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

1
2
3
4
5
6
7
% 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 を見ます。

1
2
3
4
5
% 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[] (Middleman::Sitemap::Resource)

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
[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() を使います:

1
2
3
4
5
6
7
8
9
10
11
12
[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 でテンプレートに従い加工されたテキストが返ります。

1
2
3
4
5
6
7
8
[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 の定義がされています。

1
2
3
4
5
6
7
8
9
10
11
# 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/ 以下のファイルに規則にそって定義することです:

1
2
3
4
5
6
% 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 でやってみます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
% 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() 関数が使えるようになります。

手を加える

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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