テキスト内のリンクを検出して自動的にリンク用のコンポーネントをレンダリングするみたいなことをするときにどうするのが良いのかよくわかってなかったので調べて、ちょっと実装してみた内容をメモしておきます📝(もっといいいやり方があるかも知れないです💦)
⚠v-html
を使用していているのでXSSには十分に注意してください。
実装サンプルはComposition APIを使って記載しているので、そのあたりの内容は下記を参照してください。
ケース: テキスト内の特定の文字列をリンク用の子コンポーネントに変換する
今回は下記のようなpropsでテキストを渡してあげるとリンクn
の形式の文字列が含まれていたら、<a href="/">リンクn</a>
に変換してくれるようなコンポーネントを考えてみます。
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>home</title> </head> <body> <div id="vue-root"> <linked-text initialize-text="これがリンク1だよ。これもリンク2だよ"/> </div> </body> </html>
リンク用のコンポーネントは下記のような感じになっています。
<template> <a href="/">{{ message }}</a> </template> <script lang="ts"> import Vue from "vue"; import VueCompositionApi, { defineComponent } from "@vue/composition-api"; Vue.use(VueCompositionApi); type Props = { initializeMessage: string }; export default defineComponent({ name: "Link", props: { initializeMessage: { type: String, default: "", }, }, setup(props: Props) { const message = props.initializeMessage; return { message, }; }, }); </script> <style></style>
これがリンク1だよ。これもリンク2だよ
を渡したときの表示イメージはこんな感じです🎨
対応方法MEMO
色々調べて方針としては下記のような感じで考えてみました。
- テキストの表示部分は
v-html
を指定 - テキスト内の特定の文字列(
リンクn
)を変換しやすいようにタグの文字列(<span class="link">リンクn</span>
)に置換 - マウント後、生成したタグを持つ要素を取得して子コンポーネントに置換
実際に実装してみたのが以下です。
<template> <div class="link-list"> <p v-html="text" /> </div> </template> <script lang="ts"> import Vue from "vue"; import VueCompositionApi, { defineComponent, onMounted } from "@vue/composition-api"; import Link from "@js/components/Link.vue"; Vue.use(VueCompositionApi); const LINK_TAG_STR = ["<span class='link'>", "</span>"]; const LINK_TAG_SELECTOR = "span.link"; const LINK_STR_REGXP = /リンク./g; function buildChildComponent(props: object) { // Vue.extendで対象コンポーネントのコンストラクタを取得 // https://012-jp.vuejs.org/guide/components.html const LinkComponent = Vue.extend(Link); // 子要素のinstanceを生成して返す return new LinkComponent({ propsData: props }); } function sanitize(text: string) { // 本来ならXSSが発生しないように適切にsanitizeする必要があるが割愛 return text; } function replaceLinkTag(text: string) { // 特定の文字列をタグの文字列に変換 return text.replace(LINK_STR_REGXP, `${LINK_TAG_STR[0]}$&${LINK_TAG_STR[1]}`); } export default defineComponent({ name: "LinkList", components: { Link }, props: { initializeText: { type: String, required: true, }, }, setup(props: { initializeText: string }) { // サニタイズしつつ置換対象の特定の文字列をタグに変換 const text = replaceLinkTag(sanitize(props.initializeText)); onMounted(() => { document.querySelectorAll(LINK_TAG_SELECTOR).forEach((link) => { // Componentからインスタンスを生成 const instance = buildChildComponent({ initializeMessage: link.textContent }); instance.$mount(); // インスタンスをマウント // 対象のnodeをLinkコンポーネントで置換 if (link.parentNode) link.parentNode.replaceChild(instance.$el, link); }); }); return { text, }; }, }); </script> <style></style>
ポイントは、onMounted
の中でVue.extend(CompnentClass)
で対象のコンポーネント用のコンストラクタを取得してComponentのインスタンスを生成してあげて、それをinstance.$mount()
後にinstance.$el
で要素として取得し生成したタグと置換してあげているところかなと思います💦
function buildChildComponent(props: object) { // Vue.extendで対象コンポーネントのコンストラクタを取得 // https://012-jp.vuejs.org/guide/components.html const LinkComponent = Vue.extend(Link); // 子要素のinstanceを生成して返す return new LinkComponent({ propsData: props }); }
リンクなど動的にコンポーネントに置換したいようなケースは、そこそこありそうなのでXSSには注意しながらいい感じに実装していきたいですね、それでは👋