Reactで生HTMLを自由自在に加工する このエントリをはてなブックマークに登録

2021年05月18日

takurutakuru

こんにちは。クレイの浅海です。最近の休日は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日間は無料ですべての機能が使えるので、ぜひチームを作成して試してみてください。

⇒ 30日間無料で DocBase を試してみる

  1. メモからはじめる情報共有 DocBase 無料トライアルを開始
  2. DocBase 資料をダウンロード

「いいね!」で応援よろしくお願いします!

このエントリーに対するコメント

コメントはまだありません。

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)


トラックバック

we use!!Ruby on RailsAmazon Web Services

このページの先頭へ