MiddlemanでVisualforceを書く

はじめに
----
[Middleman](http://middlemanapp.com/jp/)とはRuby製の静的サイトジェネレータで、普段Webアプリを書いている時に使っているテンプレートエンジン(Slim, HAML, ERBなど、NodeならJadeとか)やCoffeeScriptなどのAltJs、Sassなどのプリプロセッサを使って静的サイトを作っちゃおうというツールです。競合ツールはたくさんあって、NodeならGruntでがんばったり、最近だとWebPackが人気だと思います。

最近はapex:タグを投げ捨てても許される風潮になってきたので、それならSalesforce1アプリじゃなくてもdoctype=html-5.0で書いちゃおう、それならHTMLやJavaScriptを直接書きたくないから静的サイトジェネレータ使おう、と思って始めました。

まあ[atskimuraさんのエントリ](http://atskimura.hatenablog.com/entry/2014/12/15/094631)の「Force.comへのデプロイ」のところとだいたい同じなんですが！

TLDR
---
### 何ができるようになるか
Slim, Sass, CoffeeScriptでVisualforceと性的リソース作れるよ！コマンド一発でデプロイできるようになるよ！（デプロイはAnt Migration Tool）

###必要なもの
RubyとNode.js（Bower使ってるので）があれば十分ですが、Salesforce Migration Toolもあったほうがいいと思います。

`gem install bundler`と`npm install -g bower`も必要です。

### サンプルと使い方
[こちらのリポジトリ](https://github.com/tarot/middleman-visualforce-sample)からcloneして、`bundle install`と`bower install`を実行すれば使えるようになります。Middlemanのサイトには`middleman init`しろと書いてますが、init済みのプロジェクトをpushしてるので必要ないです。

サンプルはすぐ`bundle exec middleman build`でビルドできるはずです。buildディレクトリにVisualforceと静的リソース、package.xmlが出力されるので、Migration Toolでデプロイできます。また、Middlemanにも開発サーバ機能があって、`bundle exec middleman`で起動します。起動したら http://localhost:4567/__middleman でサイトマップを確認できます。

build.xmlにはdeployUnpackagedとdryRun（CheckOnly）を定義しています。build.propertiesは入ってないのでサンプルからコピーしてください。

### こういうのが簡単に作れます
サンプルに入っているVisualforceをSitesで公開しています。
https://tarot-developer-edition.na14.force.com/myfirstsite/sample

RemoteActionはCRUC/FLS制御していないので使えますが、RemoteObjectの方は参照権限を外しているのでエラーになります。JavaScriptコンソールでご確認ください。

サンプルの解説
----
Visualforce開発で触るのは主に↓のディレクトリです。

|ローカルディレクトリ|開発サーバ|ビルド後|
|:-----------|:------------|:------------|
|/source/pages/\*.page.slim|/pages/\*.page と<br/>/pages/\*.page-meta.xml|/build/pages/\*.page と<br/>/build/pages/\*.page-meta.xml|
|/source/pages/layouts/*.slim|なし|なし|
|/source/staticresouces/css/\*<br/>/source/staticresouces/js/\*<br/>/source/staticresouces/img/\*|/staticresources/css/\*<br/>/staticresources/js/\*<br/>/staticresources/img/\*|/build/staticresources/assets.resouce<br/>にまとめてZip圧縮される|
|/bower_component<br/>の配下のフォント|/staticresources/fonts/\*|/build/staticresources/assets.resouce<br/>これも同じZipにまとめられる|
|/source/staticresouces/sample/\*|/staticresources/sample/\*|/build/staticresources/sample.resouce<br/>ディレクトリ配下をZipにまとめる|
|/source/package.xml|見ても意味が無い|/build/package.xml|

Visualforce, CSS, JavaScript, 画像, その他の静的リソースをそれぞれ対応するディレクトリに置いておけば、面倒なことはMiddlemanがやってくれます。

Slim, Sass, CoffeeScriptを使っていますが、HAMLやLESS, JadeやTypeScriptも使えます（設定は必要ですが）。

### Visualforce
Middlemanは[apex:compositionのような機能](http://middlemanapp.com/jp/basics/templates/)があって、[/source/pages/layouts/layout.slim](https://github.com/tarot/middleman-visualforce-sample/blob/master/source/pages/layouts/layout.slim)と、それぞれのHTMLになるファイルを自動的に合成してくれます。上の表で「なし」「なし」となっているのは、他のページのレイアウトを決めるためのファイルなのでビルドしても意味が無いからです。layout.slimにヘッダを書いておけば、/source/pages/*.slimはbodyの下だけに集中して書くことができます。ちなみにlayout.slimの`yield`と書かれている部分にそれぞれのページがはめ込まれます。

#### [/source/pages/Sample.page.slim](https://github.com/tarot/middleman-visualforce-sample/blob/master/source/pages/Sample.page.slim)

```
---
api_version: 32.0
description: false
label: Sample
controller: false
action: false
extensions: false
title: Sample Visualforce
---
```
最初のここは.page-meta.xmlとapex:pageの属性の設定です。あとtitleタグ。Middlemanの[Frontmatter](http://middlemanapp.com/jp/basics/frontmatter/)という機能を利用しています。layoutにちょっとだけページ固有の情報を入れたいときに便利です。（&lt;head&gt;がlayoutにあるのにtitleどうやって変えればいいの！とか）

```
- content_for :remote_objects
  apex:remoteObjects
    apex:remoteObjectModel name="Contact" fields="Id,Name,FirstName,LastName,Email,Phone"
```
この部分はRemoteObject用の設定です。使わないならまるごと消していいです。これもMiddlemanの[テンプレートヘルパ](http://middlemanapp.com/jp/basics/helpers/)の機能です。各ページはlayout.slimの`yield`にはめ込まれますが、`content_for`を使ったところだけ別のところにはめ込むことができます。これは2行目の`yield_content`のところに入ります。ますます`apex:composition`っぽい。

リンクやimg srcは`{!$Page.PageName}`や`{!URLFOR($Resource.resoucename, 'path/to/file')}`を使います。~~これと`{!$Site.Prefix & $Page.PageName}`~~（これ要らなかった…）これらは開発サーバでも使えるようにしています。他の`$Label`などはデプロイしなければそのままで表示されます。

### 静的リソース
ビルドするときは/source/staticresources配下の.resource-meta.xmlが基準になります。例えばsample.resource-meta.xmlがあれば、sampleディレクトリをZipで固めてsample.resourceにします。それ以外のディレクトリはassets.resourceにZipで固めます。

ただし、Middlemanの若干おせっかいな機能でハマらないように、CSSは/source/staticresources/cssに、JavaScriptは/source/staticresources/jsに全部置いて、assets.resourceに固めたほうがいいと思います。CSSから参照するフォントや画像もassets.resourceに入れましょう。

#### CSS
[/source/staticresources/css/](https://github.com/tarot/middleman-visualforce-sample/tree/master/source/staticresources/css)にSassやLESSを置いておくだけです。（多分）アセットパイプライン機能の影響で、他の場所にCSSを置くとうまく行きません。

Middlemanには[アセットパイプライン](http://middlemanapp.com/jp/basics/asset-pipeline/)という機能があって、BowerでインストールしたCSSやJavaScriptを一つのファイルにまとめることができるのですが、画像やフォントを使ったCSSだと結構地獄です。bootstrap-sf1の画像とフォントだけ使えるように設定しましたが、おとなしく/source/staticresources/fontsや/source/staticresources/iconsなどに手でコピーした方がいいかもしれません。

bootstrap-sf1を使わないなら[config.rb](https://github.com/tarot/middleman-visualforce-sample/blob/master/config.rb)のここのところをバッサリ消してください。

```
%w(fonts icons).each do |folder|
  dir = Pathname.new File.join bower_components, 'bootstrap-sf1', 'dist'
  Dir[File.join dir, folder, '**', '*'].reject { |f| File.directory? f }.each do |f|
    sprockets.import_asset(Pathname.new(f).relative_path_from(bower_components)) {
      Pathname.new(config[:css_dir]) + '..' + Pathname.new(f).relative_path_from(dir)
    }
  end
end
```

BowerでインストールしたCSSは、Sassの先頭で

```
//=require "bootstrap-sf1/dist/css/bootstrap"
```

のようにして指定すると勝手にくっつけられます。コメント行に`=require "..."`と書くだけです。（bootstrap-sf1はbower.jsonの記述が間違ってるのでほぼフルパスを書かないとダメ。jQueryは`#=require "jquery"`でいいんですが…）

#### JavaScript, RemoteAction, RemoteObject
JavaScriptも[/source/staticresources/js/](https://github.com/tarot/middleman-visualforce-sample/tree/master/source/staticresources/js)にCoffeeScriptなどを置いておくだけです。（多分）アセットパイプライン機能の影響で、他の場所にJavaScriptを置くとうまく行きません。

JavaScriptではRemoteActionやRemoteObjectをどうするか悩みます。私は開発中だけ`Visualforce.remoting.Manager.invokeAction`と`SObjectModel`のMockを書いたJavaScriptロードするようにしました。

JavaScriptは[/lib/apexremote.coffee](https://github.com/tarot/middleman-visualforce-sample/blob/master/lib/apexremote.coffee)で、これは[ベタ書きのJSONを返すだけのURL](https://github.com/tarot/middleman-visualforce-sample/blob/master/lib/apexremote.rb)を見に行くだけです。SObjectModelはどのオブジェクトでも使えるようにしたかったのですが、Chromeは[Proxy API](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Proxy)（Rubyのmethod_missingみたいなことができる）が無効化されたので、[結局1個1個定義することにしました](https://github.com/tarot/middleman-visualforce-sample/blob/master/lib/apexremote.coffee#L89)。

```
for sobject in ['Contact', 'Account']
  SObjectModel[sobject] = class extends SObjectModel
    constructor: (props) -> super sobject, props
```

実装について
----
[Middlemanのカスタム拡張](http://middlemanapp.com/jp/advanced/custom/)について少しだけ説明されているのでそれを参考にしました。[全部/libに入っています](https://github.com/tarot/middleman-visualforce-sample/tree/master/lib)。（ディレクトリ構成やRubyの書き方的な参考には全くならないと思います）

### [visualforce.rb](https://github.com/tarot/middleman-visualforce-sample/blob/master/lib/visualforce.rb)

このクラスには処理の本体はあまりありません。ほとんど他のクラスを使うように登録しているだけです。

* 標準設定の上書き（layouts_dir, js_dir, css_dirを書き換えている）
* 一応、この拡張だけで使う設定の登録（色々設定を変えられるようにはなってないと思います）
* ビルド前処理（`before_build`）、後処理（`after_build`）の登録
* 開発中にHTMLを書き換える機能の登録と、apexremote用のMockAPIエンドポイントの登録（`app.configure :development`）
* xmlがlayoutを使わないように設定、layout.htmlができないように設定、.page-meta.xmlを自動生成する設定

### [middleware.rb](https://github.com/tarot/middleman-visualforce-sample/blob/master/lib/middleware.rb)
開発サーバで使うときにHTMLを書き換える処理はこのクラスでやっています。[Rackミドルウェア](http://middlemanapp.com/jp/advanced/rack-middleware/)を利用しています。Nokogiriを使おうと思いましたが&amp;が消えたりするので、ひたすら正規表現で置き換えてるだけです。

* &lt;head&gt;の最初にapexremote.coffeeを差し込み
* apex:pageとapex:remoteObjectsの除去
* ~~`{!$Site.Prefix &amp; $Page.xxx}`~~、`{!$Page.xxx}`、`{!URLFOR($Resource.xxx, 'yyy')}`、を開発サーバに向ける
* doctype htmlの追加とContent-Typeなどの設定

こいつでSObjectModelのサブクラスを定義できるようにしたらよかった。

### [builder.rb](https://github.com/tarot/middleman-visualforce-sample/blob/master/lib/builder.rb)
静的リソースのZip圧縮と、package.xmlの構築をやっています。私はマカーなので`UNF::Normalizer`を使っていますが、プロファイルとかのメタデータを扱うまでは多分必要ないです。（プロファイルはファイル名が「なんとか**プ**ロファイル」とかになるので、Unicode正規化問題に直撃します）

終わりに
----
まとめとか苦手なので勘弁して下さい。

* リポジトリ https://github.com/tarot/middleman-visualforce-sample
* サンプルSites https://tarot-developer-edition.na14.force.com/myfirstsite/Sample
* テストがなくてすいません…

Visualforceを書くのにも、Middlemanなどの静的サイトジェネレータをお勧めします。Hybrid Localなモバイルアプリでも使えますし、Salesforce以外でも使えます。いつまでSalesforceで消耗してるの？

とは言ってもRemoteActionやRemoteObjectを使うのは辛いです。APIならローカルプロキシ立てるだけなのでそれほど大変ではないんですが… なにかいい方法があったら教えて下さい。

それから、Middlemanはいろいろと消耗することが多かったので、これから裏側の実装までやるならWebPackなど他のツールを使ったほうがいいんじゃないかなあと思います。うまくできたらぜひ公開してください。