サイトトップ

Director Flash 書籍 業務内容 プロフィール

HTML5テクニカルノート

Vue.js + ES6: 簡単なMarkdownエディタをつくる


Vue.js公式サイトの「例」に、わずかなコードでつくられた「Markdown エディタ の例」があります。使っているライブラリやVueの構文についての説明はないので、いざつくってみようとするとつまづきそうです。そこで、解説を加えながら、少しずつ組み立ててみましょう。構文はECMAScript 2015(ECMAScript 6)を使いました(サンプル001)。

サンプル001■Vue.js + ES6: Markdown Editor Example

See the Pen Vue.js + ES6: Markdown Editor Example by Fumio Nonaka (@FumioNonaka) on CodePen.

01 データを要素にバインディングする

ページを左右ふたつに分けて、左にテキストを入力し、右にフォーマットして表示するレイアウトにします(図001)。<body>要素には、つぎのように<textarea>要素と<div>要素をそれぞれ入力および表示の領域として、親の<div>要素(id属性"editor")に加えます。また、スタイルは以下のとおりです(「Markdown エディタ の例」のCSSをそのまま用いました)。

<body>要素

<div id="editor">
	<textarea>hello</textarea>
	<div></div>
</div>

<style>要素

html, body, #editor {
	margin: 0;
	height: 100%;
	font-family: 'Helvetica Neue', Arial, sans-serif;
	color: #333;
}
textarea, #editor div {
	display: inline-block;
	width: 49%;
	height: 100%;
	vertical-align: top;
	box-sizing: border-box;
	padding: 0 20px;
}
textarea {
	border: none;
	border-right: 1px solid #ccc;
	resize: none;
	outline: none;
	background-color: #f6f6f6;
	font-size: 14px;
	font-family: 'Monaco', courier, monospace;
	padding: 20px;
}
code {
	color: #f66;
}

図001■レイアウトされたページ

図001

JavaScritpコードは、つぎのようにVueクラスのインスタンスを定めます。引数のオブジェクトのプロパティelVueオブジェクトが扱う要素で、<div>要素のid属性("editor")を渡しました。dataプロパティには、オブジェクトで任意のデータを納めます。プロパティや属性をデータとバインディングするのがv-bindディレクティブです。Vueインスタンスのデータ(input)は、v-bind ディレクティブで<textarea>要素のプロパティにバインディングしました[*1]。これでデータが要素のテキストとして加えられます(図002)。

<script>要素

const app = new Vue({
	el: '#editor',
	data: {
		input: '# hello'
	}
});

<body>要素

<div id="editor">
	<!--<textarea>hello</textarea>-->
	<textarea v-bind:value="input"></textarea>

</div>

図002■データが<textarea>要素のテキストとして加えられた

図002

[*1] <textarea>要素にはvalueという属性はありません。バインディングしているのは、JavaScriptが<textarea>要素を扱うHTMLTextAreaElementオブジェクトのプロパティvalueです(「公式チュートリアルから始めるVue.js vol.1『Markdown エディタ』」の「注意:textarea要素とvalue属性、textareaオブジェクトとvalueプロパティ」参照)。

02 テキストをMarkdownして表示する

Markdown」は、テキストのフォーマットを定める簡易な記法です。HTMLコードよりも簡単な書き方で、文字や段落の表記が整えられます。そして、markedはMarkdownのテキストをHTMLコードに変えるJavaScriptライブラリです。markedをjsDelivrからつぎのように読み込んでおきます。

<head>要素

<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>

要素の子となるHTMLのコード(innerHTML)を書き替えるにはv-htmlを用います。値には算出プロパティ(compiledMarkdown)を定めて与えることにします(「Vue.js + ES6入門 05: 項目を数えて表示する」02「条件に合ったデータを数えて返す」参照)。

<body>要素

<div id="editor">

	<!--<div>-->
	<div v-html="compiledMarkdown"></div>
</div>

算出プロパティは、Vue()コンストラクタに渡すオブジェクトのcomputedオプションに加えます。getterとして働きますのでメソッドで定め、値を返さなければなりません。HTMLコードの要素(<h1>)にデータ(input)をテキストとして含めました。これで、データがHTMLコードとして解釈され、バインディングした要素に加えられます(図003)。

<script>要素

const app = new Vue({

	computed: {
		compiledMarkdown() {
			return '<h1>' + this.input + '</h1>';
		}
	}
});

図003■データがHTMLコードとして右の領域に加えられた

図003

HTMLコードが正しく解釈されて要素の子に加えられることが確かめられましたので、marked()関数を使って書き替えます。引数に渡すのはMarkdownされたテキストです。HTMLコードが返されますので、算出プロパティ(compiledMarkdown)の戻り値とします。これでMarkdownしたテキストがHTMLコードのフォーマットで示されます(図004)。ここまでの<body>要素の記述を、以下のコード001にまとめました。

<script>要素

const app = new Vue({

	computed: {
		compiledMarkdown() {
			// return '<h1>' + this.input + '</h1>';
			return marked(this.input);
		}
	}
});

図004■MarkdownしたテキストがHTMLフォーマットで表示される

図004

コード001■MarkdownしたテキストをHTMLのフォーマットで別の要素に表示する

<body>要素

<div id="editor">
	<textarea v-bind:value="input"></textarea>
	<div v-html="compiledMarkdown"></div>
</div>
<script>
const app = new Vue({
	el: '#editor',
	data: {
		input: '# hello'
	},
	computed: {
		compiledMarkdown() {
			return marked(this.input);  
		}
	}
});
</script>

03 テキストの入力に応じてMarkdownしたテキストをフォーマットする

v-onディレクティブは、コロン(:)のあとに引数として添えたイベントにリスナーを定めます。input<textarea>要素の値が変更された場合に起こるイベントです。つまり、リスナーはテキストがひと文字書き替わるたびに呼び出されます。

<body>要素

<div id="editor">
	<textarea v-bind:value="input" v-on:input="update"></textarea>

</div>

イベントリスナー(update())は、Vueオブジェクトのmethodsオプションにメソッドとして加えます。引数から得たEvent.targetプロパティはイベントが起こった<textarea>要素を参照しますので、入力したテキストを取り出すのはvalueプロパティです(前述注[*1]参照)。そのテキストでVueオブジェクトのデータ(input)を書き替えます。

<script>要素

const app = new Vue({

	methods: {
		update(eventObject) {
			this.input = eventObject.target.value;
		}
	}
});

これで左(<textarea>要素)にMarkdownで入力したテキストが、入力に応じて右(<div>要素)にHTMLのフォーマットで表示されます(図004)。Markdownのエディタとして最低限の動きはできたといえるでしょう。<body>要素の記述は、以下のコード002のとおりです。

図004■Markdownしたテキストが入力に応じてフォーマットされる

図004

コード002■Markdownしたテキストのキー入力に応じてフォーマットする

<body>要素

<div id="editor">
	<textarea v-bind:value="input" v-on:input="update"></textarea>
	<div v-html="compiledMarkdown"></div>
</div>
<script>
const app = new Vue({
	el: '#editor',
	data: {
		input: '# hello'
	},
	computed: {
		compiledMarkdown() {
			return marked(this.input);  
		}
	},
	methods: {
		update(eventObject) {
			this.input = eventObject.target.value;
		}
	}
});
</script>

04 処理を一定時間分まとめて行う

テキストを続けざまに入力したり削除しているとき、ひと文字ごとにフォーマットし直すのは、負荷が無駄に増えます。表示は少し遅れても、キー入力をある程度まとめて処理した方が効率的です。ユーティリティライブラリLodashを使えばそのように組み立てられます。このライブラリをやはりjsDelivrからつぎのように読み込んでください。

<head>要素

<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.15/lodash.min.js"></script>

用いるメソッドは_.debounce()です。第1引数の関数は、第2引数の時間(ミリ秒)遅れて実行されます。そして、その間メソッドが繰り返し呼び出された場合でも、関数の処理は1回にまとめられるのです。


_.debounce(関数, 時間)

前掲コード002でテキスト入力時(inputイベント)のリスナーメソッド(update())に、_.debounce()をつぎのように組み込みます。

<script>要素

const app = new Vue({

	methods: {
		update: _.debounce(function(eventObject) {
			this.input = eventObject.target.value;
		}, 1000)
	}
});

Vue.jsではもっともよく使われるふたつのディレクティブv-bindv-onについては省略した書き方ができます。つぎのように、v-bindはコロン:のみで済み、v-onはイベントの前に@を添えるだけです。書き上がった<body>要素の記述は、以下のコード003にまとめました。

<body>要素

<div id="editor">
	<!--<textarea v-bind:value="input" v-on:input="update"></textarea>-->
	<textarea :value="input" @input="update"></textarea>

</div>

コード003■テキスト入力を少しずつまとめてMarkdownする

<body>要素

<div id="editor">
	<textarea :value="input" @input="update"></textarea>
	<div v-html="compiledMarkdown"></div>
</div>
<script>
const app = new Vue({
	el: '#editor',
	data: {
		input: '# hello'
	},
	computed: {
		compiledMarkdown() {
			return marked(this.input);  
		}
	},
	methods: {
		update: _.debounce(function(eventObject) {
			this.input = eventObject.target.value;
		}, 1000)
	}
});
</script>

05 サニタイジングする

公式サイト「Markdown エディタ の例」では、marked()メソッドの呼び出しで、第2引数のオブジェクトsanitizeプロパティをtrueにして与えています。これはサニタイジングするためです。

<script>要素

compiledMarkdown: function () {
  return marked(this.input, { sanitize: true })
}

sanitizeオプションがないと、テキストにタグを含めてしまうことができます。たとえば、属性onclickにJavaScriptコードを書けば実行されてしまうのです(図005)。悪意のある攻撃を受ける危険が生じます。

図005■要素にonclick属性にJavaScriptコードを加えてクリックすると実行される

図005

sanitizeオプションをtrueに定めれば、タグを一般的な文字列に変える(エスケープする)ため、危険が防げるのです。けれど、marked 0.7.0からsanitizeオプションは、推奨されなくなりました。

図006■sanitizeオプションを有効にするとタグがエスケープされる

図005

サニタイジングというにはエスケープだけでは十分とはいえません。そのため、専用のライブラリをつかうことが勧められています。そのひとつがDOMPurifyです。使い方は簡単で、ライブラリを読み込んでDOMPurify.sanitize()メソッドにテキストを渡せば、サニタイジングして返されます。改めて、コード全体を掲げることはしません。冒頭のサンプル001には含めてありますので、そちらをご覧ください。

<head>要素

<script src="https://cdn.jsdelivr.net/npm/dompurify@2.0.7/dist/purify.min.js"></script>

<script>要素

const app = new Vue({

	computed: {
		compiledMarkdown() {
			const sanitized = DOMPurify.sanitize(this.input);
			// return marked(this.input);
			return marked(sanitized);
		}
	},

});


作成者: 野中文雄
更新日: 2019年11月21日 構文をECMAScript 2015に改め、本文は最新情報に合わせて加筆・補正した。
作成日: 2016年03月24日


Copyright © 2001-2019 Fumio Nonaka.  All rights reserved.