公式チュートリアルから始めるVue.js vol.1「Markdown エディタ」

公式チュートリアルから始めるVue.js vol.1「Markdown エディタ」

はじめに

今回は、Vue.jsの公式チュートリアル「Markdown エディタ」を通して、Vue.js独自の記法と関連技術を解説します。

デモと概要

Markdown エディタ – Vue.js

左カラムにmarkdown記法のテキストを入力すると、右カラムでプレビューできるアプリケーションです。

HTML

<div id="editor">
 <textarea :value="input" @input="update"></textarea>
 <div v-html="compiledMarkdown"></div>
</div>

<script src="https://unpkg.com/vue@2.1.10/dist/vue.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/marked/0.3.2/marked.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js"></script>
<script src="App.js"></script>

読み込むscript

以下の3つを使用します。

  • Vue.js(2系の最新を使っています)
  • marked.js(markdown記法をhtmlに変換するmarked関数を使うため)
  • underscore.js(functionの発火タイミングを調整する_debounceを使うため)

:value

v-bind:value="input"の略記です。

Vue.jsではv-で始まる属性を用います。これを 「ディレクティブ」 といいます。
v-bindは、数あるディレクティブの一つで、属性と式を束縛する ためのものです。

「束縛」することで、プロパティの変化に連動して、動的に属性またはプロパティの値が変化 します。
v-bind:value="input"でいうと、valueプロパティは常にinputプロパティと同値をとります。

inputプロパティはJavaScript中にて定義しますので、後ほど解説します。

@input=”update”

v-on:input="update"の略記です。

v-onもディレクティブの一つです。
これを指定した要素は、イベントリスナー が設定されます。

v-on:input="update"とはinputイベントが発生したときにupdateメソッドを呼び出します。

ちなみにinputイベントはHTML5から採択されたイベントで、<input><textarea>要素の値が変更された時」 に発生します。
input – Web 技術のリファレンス | MDN

updateメソッドはJavaScript中にて定義しますので、後ほど解説します。

v-html=”compiledMarkdown”

v-htmlは、指定した要素のinnerHTMLプロパティを更新する ためのディレクティブです。
この例でいうと、compiledMarkdownプロパティがHTML文字列としてinnerHTMLプロパティに新たに書き込まれます。

innerHTMLとは下記の部分です。

<div>→innerHTMLプロパティを書き換えるとここが書き換わるよ←</div>

compiledMarkdownプロパティはJavaScript中にて定義しますので、後ほど解説します。

JavaScript

new Vue({
  el: '#editor',
  data: {
    input: '#hello'
  },
  computed: {
    compiledMarkdown: function () {
      return marked(this.input, { sanitize: true })
    }
  },
  methods: {
    update: _.debounce(function (e) {
      this.input = e.target.value
    }, 300)
  }
})

書き方の説明

new Vue({ AA:'aa',BB:'bb',CC:{cc1:cc1,cc2:cc2},DD:dd})のような書き方をします。
これは、Vueオブジェクトという、Vue.js独自に設計されたオブジェクトのテンプレートを複製し、そこにプロパティやメソッドを記述しているものです。

このひとまとまりを Vueインスタンス と呼びます。

el

el: '#editor',

elは、どの要素にVueインスタンスを与えるかを記述します。
HTML要素もしくはCSSセレクタで指定することができます。

今回は#editorにVueインスタンスを与えています。

data

data: {
  input: '# hello'
},

dataはデータを管理するオブジェクトです。

今回はinputという名前のプロパティが入っています。
ここで指定したプロパティ値は初期値として扱われます。
今回はinputプロパティの初期値は# helloです(#はMarkdown記法で<h1>要素の意味)

これはHTMLで出て来た:value="input"に関連する部分です。

束縛関係にあるvalueプロパティは常にinputプロパティと同値をとります。
これは、何らかの操作にてinputプロパティの値が書き換わった時、リアクティブに(反応的に)書き換わっているということです。

後ほど、「何らかの操作」を行う部分を解説します。

注意:textarea要素とvalue属性、textareaオブジェクトとvalueプロパティ

HTMLの<textarea>要素にはvalue属性はありません。

ここでのvalueは、JavaScriptのHTMLTextAreaElementオブジェクトのvalueプロパティです。

<textarea>要素の初期値を決定するときは<textarea>初期テキスト</textarea>のように記述します。
さもinnerHTMLプロパティに初期テキストを書き込んでいるように見えますが、そうではありません。textareaオブジェクトにおいては、こinnerHTMLプロパティではなくvalueプロパティに書き込む必要があります。

少しややこしいですが、HTML要素の「属性」とオブジェクトの「プロパティ」は別もの だということを覚えておきましょう。

innerHTMLプロパティを書き換えても、textareaに表示される文字列が変化しない!」 というハマりを避けられます。

computed

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

computedは算出プロパティです。内部的に関数で動的に更新されるプロパティと捉えるのがよいでしょう。
ここではcompiledMarkdownというプロパティがあります。

this.inputの値が変化すると、それに反応して関数内での処理が発生し、値を返します。

this.inputというのは、dataオブジェクトのinputプロパティのことです。
dataオブジェクト内のプロパティにアクセスしたいときはthis.プロパティ名のように書けばOKです。

compiledMarkdownで行われていること

marked.jsのmarked関数を使って、引き渡したmarkdown記法の文字列をhtmlに変換 しています。
chjj/marked: A markdown parser and compiler. Built for speed.

{sanitize:true}

marked()関数の第二引数に{sanitize:true}という記述があります。

これは、引き渡した文字列を変換するときに、特殊文字を一般的な文字列に変換する ための記述になります。
「サニタイジング」や「エスケープ」ともいいます。

例えば、渡される文字列が<strong>HTMLの特殊文字列<strong>だった場合を考えます。

サニタイジングを行うと、<strong>&lt;strong&gt;と置き換えられて返ってきます。
行わない場合は、そのまま返ってきますので、innerHTMLプロパティに書き込むと、そのまま<strong>HTMLの特殊文字列<strong>となり太字のテキストが表示されます。

MarkdownエディタでHTML要素を記述した時、文字列として処理するかHTML要素として処理するかはエディタによって異なる部分ですが、その部分の調整を担っている引数です。

methods

methods: {
  update: _.debounce(function (e) {
    this.input = e.target.value
  }, 300)
}

今回、methodsにはupdateメソッドが存在しています。
先述した「inputプロパティが何らかの操作によって書き換わる」部分にあたります。

引数e

イベントリスナー指定されている要素ので起こったイベントをオブジェクトとして引き渡しています。
今回はtextareaで発生するイベントがそれに当たり、e.target.valueではtextareavalueプロパティの値にアクセスしています。

updateメソッドで行われていること

このメソッドはdatainpute.target.valueすなわち「フォームの入力値」を入れるという処理を行っています。

debounce関数

_debounce()はVue.jsの機能ではありません。
正しくは、Vue.jsのバージョン1系には存在したが2系へのアップデートに伴い削除されました。

underscore.jsに同じものが存在するので、これを読み込むことで使えるようにしています。
debounce – Underscore.js

debounce「連続して関数が呼び出された時、最後の実行から指定した時間が経過するまでの呼び出しが無効になる」 という処理を行う。

今回は@inputとしているため、文字が入力されるたびに関数呼び出しがかかります。
しかし、debounceを用いると、「第二引数で指定した時間が経過する間、同イベントが発生しても処理は行わない」 ということができます。

また、立て続けに発生するイベントは一つにグループ化されます。
ですので、入力中には発火せず、入力後(キータイプをとめたら)実行される という処理が行われます。
延々と長いテキストを入力している間は、プレビュー画面に反映は行われません。

関連:throttle関数

underscore.jsの関連した関数にthrottle()という関数がある。
これは、立て続けに発生するイベントはグループ化されないので、都度、時間ごとに処理が行われます。
リアルタイムに近いのはこちらです。

今回はさしてどちらでも構いませんが、予測変換表示なんかはthrottleの方がいいですね。

このようにして 連続して発生するイベントを間引くことで、処理回数を減らし、負荷削減を行なっています。

まとめ:全体の流れ

1.文字入力がおこる

  • 文字が入力されるたびにinputイベントが発生。
  • @input="update"によりupdateメソッドが呼び出される。

2.debounceで間引かれつつ、updateメソッドが呼び出される

  • dataオブジェクトのinputプロパティが、入力された値に書き換わる。
  • inputプロパティはv-bind:valueと束縛関係にあるので、textareaオブジェクトのvalueプロパティもリアクティブに変化する。

3.inputの変更に反応して、compiledMarkdownが変化

  • inputプロパティが書き換わるたびにcompiledMarkdownプロパティの内部の関数が検知して自動で算出する。
  • inputプロパティの値はmarked()関数に渡され、サニタイズされたhtml文字列が返ってくる。

4.プレビュー画面に反映

  • 変化したcompiledMarkdownv-html="compiledMarkdown"により束縛されているので、返された値はinnerHTMLプロパティに随時書き込まれる。

まとめ

ディレクティブ

  • v-bindは「HTML要素の属性orオブジェクトのプロパティ」と「dataオブジェクトのプロパティ」を束縛する
  • 束縛関係にあるものは、プロパティの変化に反応して同じ値をとる
  • v-onは「イベントリスナーを設定する」
  • v-htmlは「innerHTMLプロパティ」と「dataオブジェクトのプロパティ」を束縛する

Vueインスタンス

  • elでVueインスタンスを与える要素を指定する
  • dataはオブジェクト。束縛するプロパティをここで管理する
  • computedオブジェクト内のプロパティはdataで管理されるプロパティの変化を検知して動的に変化する。
  • methodsオブジェクト内のメソッドはイベントハンドラを通して呼び出す。