Chienomi

PureBuilder SimplyとReactを組み合わせる

開発::web

PureBuilder SimplyとReactはやや競合した思想を持っている。

PureBuilder Simplyはプレコンパイルした静的ファイルをサーブすることで、シンプルなアプローチで軽量に動作し、高いアクセシビリティを確保できるのがメリットだ。

一方、Reactはモダンな環境をターゲットとし、大規模なアプリケーションを構築する。ファイルは動的に随時読み込まれ、APIとのアクセスを行う。

しかし、両者は排他的な関係ではない。PureBuilder SimplyとReactを組み合わせることも可能だ。 もちろん、その場合両者それぞれの持ち味をフルに活かすことはできない。だが、それぞれの美点を活かしつつ共存することは可能だ。

PureBuilder Simplyの場合、静的ファイルとしてHTMLがサーブされることを期待している。 PureBuilder Simplyが得意とするドキュメント型のウェブサイトにおいて多くの場合それは最も適切な形だが、ページ全体が読み直されてしまうのは好ましくない場合もあるだろう。

逆に、PureBuilder Simplyのウェブページ内にReactアプリケーションを組み込みたい場合は、全く支障がない。これは、Reactの「既存のページにReactを組み込む」手順を参考にして欲しい。

なお、私はそれほどReactに詳しいわけではないので、Reactに関する解説はしない。 あくまで本記事で解説するのは、PureBuilder SimplyのページをReactからロードする、つまり「サイト全体の枠組みはReactで提供し、そのうち記事部分にPureBuilder Simplyを用いて強力なPandoc Markdown/ReSTructured textによる記述を可能にする」ことをテーマとする。

PureBuilder Simplyのプロジェクトを始める

まずは普通にインストールしよう。

$ git clone 'git://github.com/reasonset/purebuilder-simply.git'
$ cd purebuilder-simply
$ sudo rsync -v *.rb /usr/local/bin/
$ mkdir ~/my_website
$ rsync -rv docroot-sample/docbase/ ~/my_website/Source
$ mkdir ~/my_website/{Build,React}

ドキュメントソースルートに移動し、テンプレートを作り直す。

$ pandoc -D html5 >| template.html

デフォルトはこれだが

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="$lang$" xml:lang="$lang$"$if(dir)$ dir="$dir$"$endif$>
<head>
  <meta charset="utf-8" />
  <meta name="generator" content="pandoc" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes" />
$for(author-meta)$
  <meta name="author" content="$author-meta$" />
$endfor$
$if(date-meta)$
  <meta name="dcterms.date" content="$date-meta$" />
$endif$
$if(keywords)$
  <meta name="keywords" content="$for(keywords)$$keywords$$sep$, $endfor$" />
$endif$
$if(description-meta)$
  <meta name="description" content="$description-meta$" />
$endif$
  <title>$if(title-prefix)$$title-prefix$ – $endif$$pagetitle$</title>
  <style>
    $styles.html()$
  </style>
$for(css)$
  <link rel="stylesheet" href="$css$" />
$endfor$
$if(math)$
  $math$
$endif$
  <!--[if lt IE 9]>
    <script src="//cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv-printshiv.min.js"></script>
  <![endif]-->
$for(header-includes)$
  $header-includes$
$endfor$
</head>
<body>
$for(include-before)$
$include-before$
$endfor$
$if(title)$
<header id="title-block-header">
<h1 class="title">$title$</h1>
$if(subtitle)$
<p class="subtitle">$subtitle$</p>
$endif$
$for(author)$
<p class="author">$author$</p>
$endfor$
$if(date)$
<p class="date">$date$</p>
$endif$
</header>
$endif$
$if(toc)$
<nav id="$idprefix$TOC" role="doc-toc">
$if(toc-title)$
<h2 id="$idprefix$toc-title">$toc-title$</h2>
$endif$
$table-of-contents$
</nav>
$endif$
$body$
$for(include-after)$
$include-after$
$endfor$
</body>
</html>

全体がひとつの要素となるような本文だけの構成に変更する。

<div>
$if(toc)$
  <nav>
    $table-of-contents$
  </nav>
$endif$
  <article>
$body$
  </article>
</div>

これで、この内容を取り込めばReactで表示すべき内容になる。

React向けに調整する

Reactに対してはドキュメントだけを返せば良いのではなく、Frontmatterの値も欲しいだろう。 Frontmatterを返すことができれば、Blessを使ってさらなるドキュメント情報を含めることもできる。

これを実現するにはPost Pluginsの機能を使う。

Post Pluginsが起動されるとき、すでにドキュメントのFrontmatter情報は解放されてしまっているが、Post Pluginsはindex databaseへの書き込みを終えてから呼ばれるため、index databaseを読むことで目的を達成できる。 index databaseの位置は環境変数$pbsimply_indexesで知ることができる。

また、そもそもドキュメント処理時には、そのドキュメント用のFrontmatterがJSONファイルで提供される。 これは環境変数$pbsimply_frontmatterで知ることができるが、実際にはこのファイルは常に.pbsimply-frontmatter.jsonである。

さて、ドキュメントは標準設定を流用したので../Build以下に吐かれるわけだが、これを合成しよう。

まずは.post_generateディレクトリをソースドキュメントルートに作成し、そこに99-json-compile.rbを作るとしよう。

require 'json'
require 'fileutils'

# 固定の出力先
OUTDIR = "../React"

# Post Pluginsは引数として出力されたファイルが渡される
docbody = File.read(ARGV[0])

# Frontmatterを読み込む
frontmatter = JSON.load(File.read(ENV["pbsimply_frontmatter"]))

# ソースと同じ構造を持つためのサブディレクトリ
subdir = ENV["pbsimply_subdir"]

# 出力ファイル名
filename = File.basename(ARGV[0], ".html") + ".json"

# Reactがロードするオブジェクト
object = {
  "document" => docbody,
  "meta" => frontmatter,
  "path" => ENV["pbsimply_subdir"],
  "file" => filename
}

# 出力ディレクトリがなければ作成
unless File.exist?(File.join(OUTDIR, subdir))
  FileUtils.mkdir_p(File.join(OUTDIR, subdir))
end

# 出力
File.open(File.join(OUTDIR, subdir, filename), "w") {|f| JSON.dump(object, f) }

# スルー出力
# (オリジナルのHTMLファイルを出力しないとdocbodyが取れない)
STDOUT.puts docbody

PureBuilder Simplyは多少プログラミングができる人が強力かつ柔軟なツールとして使えるという点を考えているため、ここでもプログラムを書くことになるが、そもそもReactで開発しようとしている人なら問題はないだろう。 なお、Post Pluginsは別にRubyに縛られているわけではない。

Reactから使う

もうあとはJSONファイルを取ってくるだけなので何も難しくないだろう。 ただ、出力されたディレクトリをHTMLファイルとは異なるものにしたならば、pbsimply-testserverが使えないことには注意が必要だ。

まぁ、あまりむずかしい話ではない。

const getJson = async (path) => {
  const server = (/^http:\/\/localhost/).test(location.href) ? "http://localhost:8080" : ""
  return await fetch(server + path)
}

検証、エラーハンドリングなどにより多くのコード量が割かれるだろう。 もちろん、frontmatterを取り扱うためにstateの引き回しも必要だ。

class DocumentRect extends React.Component {
  constructor() {
    this.state = {path: "/index.json"}
  }

  render() {
    const docobject = getJson(this.state.path)
    // エラー処理やバリデーションは省略
    return (
      <div dangerouslySetInnerHTML={{
        __html: docobject.document}}
      />
    )
  }
}

ヘルプサイトなどでドキュメントとアプリケーションの両方が必要になるケースではそれなりに有用な手法である。

(私自身は非アプリケーションをReactで構築しようとは思わないが)