mdのアンカでもVueルータを利用する
create: 2019-11-14
Blog Nuxt化 Nuxt.js Markdown

Markdownで書いた記事をNuxtで表示させるところまではできました。
問題になるのがリンクを表すアンカが、vue routerの仕組みに乗らないためにSPA動作にならないことです。

[記事へのリンク](/posts)
[外部リンク](https://www.google.com/)

<nuxt-link to="/posts">記事へのリンク</nuxt-link>
<a href="https://www.google.com" target="_blank">外部リンク</a>

になって欲しいところ。

実際には下記のようなアンカが生成されます。

<a href="/posts">記事へのリンク</a>
<a href="https://www.google.com/">外部リンク</a>

内部リンクはnuxt-linkじゃないし、外部リンクも同一ウィンドウに開く仕様。

んー、、、
ちょっと無理やりですがmountedサイクルでタグの動作を変更するミックスインを作成します。

export default {
  data: () => ({ atags: [] }),
  mounted() {
    const tags = this.$el.querySelectorAll('.markdown-body a[href]:not([href="#"])') // 1-i

    for (const tag of tags) {
      tag.setAttribute('data-x-href', tag.href) // 1-ii
      tag.href = '#' // 1-iii
      tag.addEventListener('click', this._open) // 1-iv
      this.atags.push(tag) // i-v
    }
  },
  methods: {
    _open(e) {
      e.preventDefault() // 2-i
      const href = e.target.getAttribute('data-x-href')
      if (href.startsWith(location.origin)) {
        this.$router.push(href.replace(location.origin, '')) // 2-ii
      } else {
        window.open(href, '_blank') // 2-iii
      }
    }
  },
  beforeDestroy() {
    // 3-i
    for (const tag of this.atags)
      tag.removeEventListener('click', this._open)
  }
}

これを Markdown を読み込むvueファイルのミックスインに登録すれば OK です!

ソースの解説

  1. mount 時の処理
    1. ページ内のアンカタグを探す。hrefを持つけどhref="#"ではないこと
    2. href属性をdata-x-hrefに移動する
    3. href属性は一律#に置き換える。hrefを消してしまうとアンカのスタイルが効かないため
    4. クリックイベントハンドラに_open関数を登録する
    5. 変換したタグは後処理用にキャッシュしておく
  2. クリック時の処理
    1. イベントはpreventDefaultして履歴に#が残るのを防ぐ
    2. 遷移先にlocation.originが含まれていれば内部リンクなので、vueルータにプッシュする
    3. 外部リンクなら別ウィンドウで開く
  3. 後処理
    1. キャッシュしていたタグからイベントをはがす

要するにアンカのデフォルト動作を剥奪して、クリックイベントでリンク先を判断してvueルーターを使うか新規ウィンドウで開くかを決定しています。
やりたいことはできたのでOKとする!