v-htmlで外部読み込みscriptタグを実行したい


モチベーション

はてなブログからWordpress REST API + Nuxt.js製の自家製ブログに移行をしている。

はてなブログで30個ぐらいの記事を書いてきた。

ある程度サイトができたので、記事を巡回して動きを確かめていたらTwitterウィジェットとGistの外部埋め込みが動いていないのがわかった。

どうやらv-htmlはinnerHTMLを使っているためscriptタグがあっても実行しないそうだ。

まあ、セキュリティ的にはそうだよね。

けど、今まで動いてきた資産は動かしたいし、Wordpressとして機能するものが動かないというのは納得がいかない。

なので、頑張って動かしてみることとした。


解決策

mounted, updatedでできあがったDOMをいじくり回すmethodsを呼び出す。

既存の<script>タグを動くように置換する。

jQuery使わずにDOMをいじくり回そう。

    methods: {

runScript () {
const scripts = this.$el.querySelectorAll('script')
scripts.forEach(script => {
const parentNode = script.parentNode
let alternativeNode
// todo: ホワイトリスト方式にする
if (script.src.indexOf('https://gist.github.com/') !== -1) {
alternativeNode = document.createElement('iframe')
alternativeNode.src = URL.createObjectURL(new Blob(['<!DOCTYPE html><title></title>' + script.outerHTML], {type: 'text/html'}))
alternativeNode.onload = () => {
alternativeNode.height = alternativeNode.contentDocument.body.scrollHeight + 50
}
} else {
alternativeNode = document.createElement('script')
alternativeNode.src = script.src
}
parentNode.replaceChild(alternativeNode, script)
})
}
},
mounted () {
this.runScript()
},
updated () {
this.runScript()
}


Twitterウィジェットなど多くの<script>タグの場合

<script>タグを再配置するだけでOK


Document.writeを使ってその場所に書き込む系の<script>タグの場合

<script>タグを再配置しただけだと、Document.writeは次のようなエラーがconsoleに出て実行できない。

Failed to execute 'write' on 'Document': It isn't possible to write into a document from an asynchronously-loaded external script unless it is explicitly opened

後読みコンテンツでDocument.writeできないということらしい。

そこでiframeを生成し、その中でスクリプトが実行するようにすれば良いと教えてもらった

iframeの高さは読み込み終わる前にはわからないため、読み込み終わった後に高さを差し替えている。


どれがDocument.writeのコンテンツなのか

これについては外見ではわからない。

Document.writeの外部読み込みスクリプトは非一般的とみなして、見つけ次第ホワイトリスト形式で振り分けてあげる形にしようと思う(まだ実装していない)


単体のコンポーネントにしている

見通しを良くするために、記事の部分という単一コンポーネントに切り出してしまっている。

<template>

<div class="content" id="article-content" v-html="content"></div>
</template>

<script>
export default {
name: 'ArticleContent',
props: {
content: String
},
methods: {
runScript () {
const scripts = this.$el.querySelectorAll('script')
scripts.forEach(script => {
const parentNode = script.parentNode
let alternativeNode
// todo: ホワイトリスト方式にする
if (script.src.indexOf('https://gist.github.com/') !== -1) {
alternativeNode = document.createElement('iframe')
alternativeNode.src = URL.createObjectURL(new Blob(['<!DOCTYPE html><title></title>' + script.outerHTML], {type: 'text/html'}))
alternativeNode.onload = () => {
alternativeNode.height = alternativeNode.contentDocument.body.scrollHeight + 50
}
} else {
alternativeNode = document.createElement('script')
alternativeNode.src = script.src
}
parentNode.replaceChild(alternativeNode, script)
})
}
},
mounted () {
this.runScript()
},
updated () {
this.runScript()
}
}
</script>

<style scoped>
.content >>> iframe {
width: 100%;
border: none;
overflow: hidden;
}
</style>

https://github.com/sakapun/nuepress/blob/master/components/ArticleContent.vue


個人ブログならばいいけれども、プロダクションに使えるコードなのか

自分のサイトで、API も自分が管理しているものなので、今回はこれでいいとなった

WordPressの記事の対応であれば、外部読み込みscriptは、もとのWordPressでは普通に実行されているものなので特に心配する必要はないと判断した。

しかし、他のシチュエーションで使っても良いコードかというのは非常に注意した方が良い。

ただしWordPressのREST APIを使う場合に、今までの記事がいっぱいある場合や、使えるコンテンツを制御したくないというときには、このテクニックは必須になるんじゃないだろうかなという風に思う。