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は引数として出力されたファイルが渡される
= File.read(ARGV[0])
docbody
# Frontmatterを読み込む
= JSON.load(File.read(ENV["pbsimply_frontmatter"]))
frontmatter
# ソースと同じ構造を持つためのサブディレクトリ
= ENV["pbsimply_subdir"]
subdir
# 出力ファイル名
= File.basename(ARGV[0], ".html") + ".json"
filename
# 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で構築しようとは思わないが)