こんにちは。クレイの浅海です。最近の休日は3歳児とマイクラをしています。
さて、業務ではここ1年ぐらい、DocBaseのフロントエンドのフレームワークをBackbone.jsからReact.jsに変更する作業をしていました。
完全に作り直しです。2万行を超えるBackboneのコードとお別れをしました。バイバイバックボーン。
詳細 → DocBaseのフロントエンド改修をどのように進めたか
Reactで実装する上で苦労した点
DocBaseにはReactとマッチしない機能もいくつかあり、Reactで実装する上で苦労しました。その一つが、メモの表示です。「メモ」というのは、DocBaseの投稿の単位です。メモは例えばこのような投稿
メモの内容は、ユーザが投稿したmarkdownをサーバでHTMLに変換しデータベースに保存、それを表示時にサーバから受け取ったHTMLをReactが表示することになります。
ただ、この受け取ったHTMLを表示する場合に、Reactにおいては1つ問題があります。ReactはデータからHTMLを生成するのであって、生HTMLを扱うことには向いていないのです。
一応、ReactでdangerouslySetInnerHTML
を使用してHTMLをそのまま扱う方法もあります。
なのでメモをReactで表示したい場合は次のようにするだけで実装が完了・・
<div dangerouslySetInnerHTML={ { __html: memoHtml } } />
・・しません!
残念ながらDocBaseの仕様はそんなに単純ではなく、次のようにサーバから取得したHTMLを加工するいくつもの仕様が存在します。
- 外部へのaタグに対してnoreferrerを付与する
- twitterの埋め込みを有効化する
- mermaid.jsが記述されていたら有効化する
- MathJaxの構文が書かれていたら有効化する
- 差し込み用のdivタグを探して別のメモを入れ子で表示する
- imgタグで表示する画像のサイズを最適にする
などなど、他にも様々な加工が必要です。
これらを実現するためにdangerouslySetInnerHTML
でとりあえず表示して、それをReactのRef
からref.current.querySelector(query)
などしてDOMをごにょごにょと処理する・・、そんなのは辛いですね。折角、Reactで実装するというのに・・。
そんなとき、html-react-parserを使うと、良い感じにHTMLを加工できます。
https://www.npmjs.com/package/html-react-parser
html-react-parserの使い方
バージョン情報
本記事執筆時の検証で使用した各ライブラリのバージョンです。
- react 17.0.2
- html-react-parser 1.2.4
簡単な問題
まずは、html-react-parserの使い勝手を見るために、問題を非常に単純にしてみます。
やりたいことは次の1つだけとします。
- aタグに対してnoreferrerを付与する
この要求を実現するhtml-react-parserの使い方は次のようになります。
propsとして受け取ったhtmlをhtml-react-parserで変換処理を行い、その結果を表示するだけのReactコンポーネントを用意しました。
import parse, { domToReact } from 'html-react-parser'
const replace = (node) => {
if (node.name === 'a') {
return (
<a {...node.attribs} rel="noreferrer" >
{ domToReact(node.children) }
</a>
)
}
}
export const App = ({ html }) => {
return (
<div>
<section>
<h2>変換前のHTML</h2>
<p>{ html }</p>
</section>
<section>
<h2>変換後のDOM</h2>
<p>{ parse(html, { replace }) }</p>
</section>
</div>
);
}
どうでしょうか、このコンポーネントを実行すると、props.html
に渡したhtmlのaタグにreferrer属性がついて出力されていることがわかると思います。
こちらに動作サンプルを用意しました。
動作サンプル: CodePen
html-react-parserにはparse
という関数が用意されています。このparseにはreplaceオプションとして関数を渡すことができ、パースされた結果のノード単位でreplace関数が実行されるので、どのようなタグにどのような加工を施すかを細かく実装できます。
replace関数がJSX.Elementを返せばnodeをそれで置き換えますし、何も返さなければnodeは置き換えません。
reaplce関数内で使っているdomToReact
を使っている理由ですが、node変数の中身はJSX.Elementではないので、nodeをJSX.Elementに変換するためです。
仕様を増やす
上記のように、html-react-parserのparse
を使って生HTMLを自在に加工できることがわかりました。単純な問題では上記の方法で問題が無いのですが、状況によっては注意する点があります。それを仕様を増やして確認してみます。
ここに2つめの仕様を追加しました。
- aタグに対してnoreferrerを付与する
- 独自のtodayタグがあった場合は現在時刻を差し込む
<today />
というタグがあった場合、これを<time>現在時刻</time>
に差し替えるという仕様を追加しました。このtodayタグに関する判定をreplace関数に追加します。
具体的には、次の用に実装します。
const replace = (node) => {
if (node.name === 'a') {
return (
<a {...node.attribs} rel="noreferrer" >
{ domToReact(node.children) }
</a>
)
}
// 以下を追加
else if (node.name === 'today') {
return (<time>{ (new Date).toDateString() }</time>)
}
}
todayというタグだった場合は、<time>
タグのJSXを返しています。これで動作するでしょうか?試しに実行してみましょう。
コンポーネントを次のように呼び出す、つまりは入力したhtmlが<today />
の時、
<App html="<today />" />
html-react-parserで処理した後のhtmlは次の用になります。
<time>Mon Apr 19 2021</time>
正しく動作しているように見えますね。ですが、次のようなケースではどうでしょうか。
aタグの中にtodayタグがあります。
<App html="<a href=\"//google.co.jp\"><today /></a>" />
これの結果は、次です。
<a href="//google.jp" rel="noreferrer"><today></today></a>
なんということでしょう、todayタグがそのまま出てしまっています。これは直す必要があります。
どうやら、aタグの加工時に使っているdomToReact
に渡したnode.children
に対して変換処理が実行されていないようです。
幸いにも、domToReact
にはこれを解決するための方法が用意されています。domToReactにはparse関数と同じように使えるreplaceのオプションが存在するので、これを使えばparseと同じようにnodeに対してreplaceが実行されます。
次のように修正しました。
const replace = (node) => {
if (node.name === 'a') {
return (
<a {...node.attribs} rel="noreferrer" >
- { domToReact(node.children) }
+ { domToReact(node.children, { replace }) }
</a>
)
}
else if (node.name === 'today') {
return (<time>{ (new Date).toDateString() }</time>)
}
}
実行して確認してみます。結果はこのようになりました。
<a href="//google.jp" rel="noreferrer"><time>Mon Apr 19 2021</time></a>
素晴らしい、成功ですね。aタグにnoreferrerが付いたかつ、todayタグが日付に変換されています。
おわりに
html-react-parserを使うことにより、生HTMLの内容を自由に変換することが、複雑なセレクタを駆使したりすることとなく簡単に行えること実感できたでしょうか。
Reactだけでは難しいこのような処理を,かなり単純に実装することが出来るので、似たような問題を抱えている場合は検討してみることをおすすめします。
宣伝
そんな1年かけて改修した DocBase は社内・社外の垣根を超えて情報共有ができる情報共有サービスです。
細かい権限設定ができるので、オープンな情報もクローズドな情報も DocBase に集約できます。そして大きな企業さまでも全員で使えるよう、セキュリティには最も力を入れています。
Reactに改修後、高速化にも取り組んでいるので情報共有サービスの中でもサクサク使えるほうだと思います。
チームを作成して30日間は無料ですべての機能が使えるので、ぜひチームを作成して試してみてください。
このエントリーに対するコメント
日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)
- トラックバック
「いいね!」で応援よろしくお願いします!