アシアルブログ

アシアルの中の人が技術と想いのたけをつづるブログです

Chrome Dev Summitで披露された高速化のテクニックの数々(所感を含む)

f:id:masahirotanaka:20181119134014p:plain

こちらのブログではご無沙汰しています。田中です。今週はサンフランシスコにて開催されたChrome Dev Summitに参加させていただいていました。今年はweb.devの発表や新しいPageSpeed Insightsなど、例年にも増して盛りだくさんの発表が行われましたが、特に「スピードの改善テクニック」が数多く披露されたように思います。

Chrome Dev Summitのプレゼンテーションはすべて濃厚で、しかも多くの内容は「web.dev」に詳しく説明されています。ここでは、2日間にわたって行われたセッションを振り返りながら、高速化に関する内容をざっくりと追っていきたいと思います。

V8エンジンの改良

  • 以前と比べて、WebサイトのJavaScriptコード量は8倍に膨れ上がっている
  • V8の言語機能を今後も最適化していく:Promise、Async & await、フレームワーク対応等
  • ReactのHooks機能をV8で最適化できるように
  • WASM(Web Assembly)のコンパイラー「Liftoff」を用いることで、Unityアプリが10倍の速度向上
  • Chrome 70で、WebAseemblyがマルチスレッド対応

ビデオ用次世代コーデック「AV1」と画像フォーマット「WebP」

f:id:masahirotanaka:20181119132034p:plain:w400

Webサイトの容量の大半は、画像・フォント・JavaScriptが占めます。これらのファイルサイズをどう削減するかが、Webサイトの高速化にとって重要だということです。

たとえば、アニメーションGIFは非常に効率が悪く、お薦めできません。そのアニメーションGIFを動画(mp4形式)に変換するだけで、たとえば6.8MBあったものは420KBまで圧縮できます。また、動画の圧縮フォーマットとして、AV1コーデックを用いると、x264と比較しても40〜50%の圧縮を実現されます。(ただしAV1は現状Chromeでのみサポート

同様に、画像フォーマットとしてWebPを用いると、30%ほどのファイルサイズ削減となります(WebPはEdge / Chrome / Firefoxで利用可能)。この場合、非対応ブラウザーと組み合わせるために、下記のように<picture>タグを利用します。旧ブラウザにおいては、<img>タグの内容が解釈される仕組みです。

<picture>
    <source type="image/webp" src="image.webp">
    <img src="image.jpg">
</picture>

Webフォントも読み込み時間が長くなる要因の一つです。その対策として、font-displayというCSSプロパティが登場しました。font-display: swapと指定した場合、カスタムフォントを読み込まれるまでは、システムのフォントが代わりに表示されるようになります。

@font-face {
  /* ... */
  font-display: swap;
}

font-displayプロパティは、現状Firefox/Chrome/Safariで対応されています

Houdini

f:id:masahirotanaka:20181119132402g:plain:w300動画より

CSSの機能をJavaScriptで拡張できるようにするための仕組みです。Worklet、CSS Paint API、Layout API、Typed OMといった複数の機能から構成されています。

たとえばCSS Paint APIを用いると、これまでCSSでは表現できなかった描画(たとえば角丸以外のボーダーを持つオブジェクト)を、JavaScriptで描画することができます。

Houdiniの実装状況は、こちらのWebサイトから確認できます。Chromeだけでなく、SafariFirefoxでも開発が進んでいるようです。

Native Lazy Loading

画像などのリソースを読み込む時間を遅延するかどうかを、lazyload属性を用いてブラウザーに指示することができるようになります。画像とIFRAMEに対して適用できます。

たとえば、

<img src="image.jpg" lazyload="on">

とすると、ブラウザーは実際にそのコンテンツを表示する必要があるまで、画像ロードは行われません。デフォルトは「auto」、よってブラウザーが遅延読み込みを行うかどうかを判断します。要するに、この機能が組み込まれたブラウザーの場合、何もしなくても勝手に遅延読み込みが有効になるようです。

f:id:masahirotanaka:20181119133217g:plain動画より

Preloading

プリロードを用いると、画像やスクリプトなどのリソースを前もってロードすることができます。これにより、例えば次以降のページで必要なアセットを前もって取得しておき、ページ表示速度を高速化することが簡単に実現します。

プリロードを設定するには、<link>タグを使用します。新しく追加された rel="preload"属性を使用して、下記の通り記述します。

<link rel="preload" href="image.png" as="image">
<link rel="preload" href="script.js" as="script">

この機能はChrome/Safari/Edge(一部サポート)で利用できます。Service Workerを用いなくても、コンテンツのプリフェッチが簡単に実現できますね。

Web Packaging APIとPortals

AMP(Acceledated Mobile Pages)の仕組みにより、たとえばGoogleの検索結果から次のページへの遷移速度を向上させることができるようになりました。しかし、この仕組みはGoogleドメイン上で遷移先のページを表示する必要があるため、URLが二重に表示されたり、セキュリティ上の懸念につながってしまいます。

Web Packagingという仕組みを用いると、Webサイトの正当性を担保することで、ブラウザーがコンテンツをプリフェッチし、キャッシュさせることが可能になります。実際にCloudflare社のデモでは、AMP Packagerという仕組みが「Cloudflare Worker(V8の単位でエッジサーバーで動作させる技術)」を用いてWebパッケージが作成されていました。

もう一つの仕組みとして、IFRAMEに似た「Portal」という仕組みが紹介されました。これは、<portal>というタグを用いて実現されるもので、IFRAMEと同様にページ内に別サイトを組み込める仕組みです。IFRAMEとは異なり、そのコンテンツに遷移する際に、モーフィングアニメーションが表示されます。これにより、ネイティブアプリのような画面トランジションをWebサイト間で表現できます。具体的なデモでは、「となりのヤングジャンプ」と「はてな」による漫画ビューアーが紹介されていました。

f:id:masahirotanaka:20181119135245g:plain:w200Portalを用いた例

高速化の改善事例

上に挙げたような施策を、実際にWebサイトに対して適用した事例が、いくつも紹介されました。ここでは、PinterestSpotifyStarbucksといったメジャーなWebサイトの改善事例、そしてSquooshというGoogleがデモンストレーション目的で開発したツールについて、その内容を紹介します。

Pinterest

モバイルWebを一から作り直した(その作業は3ヶ月しかかからなかったそうです)。その結果、起動速度が4倍高速化(以前21.2秒だったのが5.6秒に)したとのこと。さらにServiceWorkerを用いたプリフェッチを行い、これまで23秒かったところが3.9秒まで削減。

より具体的な数字として、JavaScriptのサイズは、650kbから200kbまで削減できたそうです。その削減の取り組みにおいては、Performance Budgetの可視化が重要。具体的には、継続的インテグレーションを用いてPerformance Budgetを自動的に測定する仕組みを導入し、開発プロセスに組み込まれています。

  • ログ: WebPackのバンドルサイズを記録
  • 警告: コア機能のバンドルが大きくなったら開発チームに警告
  • 防止: ESLintを用いて、インポートに制約をつける

こうした努力を経て、ユーザーの体験は向上し、新規ユーザーのサインアップはモバイルWeb経由がNo. 1になったそうです。

Spotify

1年前(2017年)のSpotifyにはモバイルWeb版がなく、音楽を聞くにはネイティブアプリのダウンロードが必要な状態でした。しかし、ブラジルなどの新興国では、モバイル端末のスペックが低く、端末の容量が限られているため、アプリダウンロードのハードルが高い。1曲聞きたいだけなのに、何MBもするアプリをダウンロードしたくない、という反応が多かったそうで、A/Bテストを実施しながら、ユーザーの反応を見ていきました。

最初の実験として、アプリのダウンロードを行う前に、1曲だけWeb経由で聴ける仕組みを導入しています。そうすると、初日再生率は25%向上されました。そこで、次の実験として、モバイルWebプレイヤーを組み込んだWebサイトとそうでないWebサイトの2種類に分離し、ユーザーの反応を確かめました。すると、モバイルWebプレイヤーを組み込んだ場合、初日再生率は54%向上したそうです。

SpotifyのモバイルWeb対応には、Media Session APIやPWA対応が行われておりますが、ブラウザーにコンテンツ保護機能が加わったことも重要だったということでした。

Starbucks

2017年にPWA版をリリースしています。WorkboxやIndexedDBを用いた高速化で、従来よりも2倍の速度を実現されました。また、Credentials Management APIを用いてログインの仕組みをスムーズになっています。

アプリはReactベースで作られています。また、デスクトップ用WebサイトもPWA対応されており、実際にWebオーダーの25%はデスクトップから行われてるようになりました。

Squoosh

Chrome ChromeのDeveloper Advocateチームが中心になって、PWAやWeb Assembly、WebWorkersなどをフルに活用して開発した画像圧縮サイトです。GitHubで公開されており、Chromeだけでなく、Edge/Safari/Firefoxでも動作します。

圧縮コーデックであるC/C++のライブラリはEmscriptenを用いてWebAssemblyに変換されています。そして、CPUコストの高い圧縮機能は、WebWorkersを用いてUIスレッドと分離し、画像圧縮中でもスムーズな描画ができるよう配慮されています。Webサイトは完全なPWA実装となっており、モバイルやタブレットでも動作します。

最後に

昨年までは「PWA」をいかに浸透させるかがテーマの中心だったように思いますが、今回のChrome Dev Summit 2018では、PWA(そしてPWAの真髄であるネイティブ並みのパフォーマンス体験)を具体的に実現するためのノウハウやテクノロジーが余すことなく紹介されたように思います。

ブラウザーに対する機能強化が、ペースダウンすることなく進んでいることが印象に残りました。一方で、たとえばHoudiniがEvergreen化された場合にCSS3・SVGWebGLCanvasとの棲み分けはどうしたらいいのか、より巨大化するブラウザーのランタイムは全体性能を低下させているのではないか、などの疑問も生まれそうです。

Chrome Dev Summit 2018、まとめると凄く楽しかったです!

Vue.jsはより速く、より小さく!〜Evan氏の来日講演から読み解くVue 3の進化〜

f:id:knight_9999:20181108183653p:plain:w600

こんにちは。Monaca/Cordova担当の内藤です。

今年(2018年)の11月3日に、秋葉原 UDXでVue Fes Japan 2018が開催されました。Vue.jsの開発者であるEvanさんをはじめ、Vue CLI UIクリエーターのGuillaumeさん、Microsoft シニアデベロッパーアドボケイトのSarahさん、Vue Test UtilsメインクリエーターのEddさんらをお招きしています。

開発者であるEvanさんの基調講演では、新しくリリースが予定されているVue 3.0の概要について、お話いただきました。Vue 3.0の特徴として、以下の点を解説していただきました。

  • より速く
  • より小さく
  • よりメンテナンスしやすく
  • よりネイティブ向けに作りやすく
  • よりあなたのコードの保守性を向上

f:id:knight_9999:20181108183756p:plain:w600

どれも魅力的な改善点ですが、このブログ記事では特に、コンポーネントレンダリングのロジックの変更されたいくつかの点についてクローズアップして、現在の最新安定板であるVue 2.5との比較しながら、見ていきたいと思います。講演でも説明されていましたが、「アプリ全体の速度が2倍に、そして、メモリ使用量が半減」というのは本当にすばらしいです。時間の関係で今回説明された部分以外にも、随所に工夫がされているのではないかと思います。

なお、仮想DOMの実装は、フルスクラッチから作り直しということなのですが、呼び出し方はほぼVue 2.x系のものと同様で、互換性は担保されているとのことですので、安心して開発に利用出来るようです。 Vue Fesでの講演スライドは、こちらで公開されています。

スライドの日本語翻訳は弊社代表の田中が行ないました。日本語スライドで何か気づいた点などありましたらコメントして下さい。

プロキシを用いた監視で、完全な言語レベルでの監視と、速度向上

f:id:knight_9999:20181108183800p:plain:w600

Vueをはじめ、データバインディングを行うフレームワークでは、データのプロパティの変化を監視しておく必要があります。これまでのVueでは、Object.defineProperty メソッドを利用して、getter、setterをオーバーライドすることで実現していました。 しかし、Vue 3.0では、このObject.definePropertyメソッドは利用せず、ネイティブプロキシを使うことで高速化されたということです。スライドでも

"プロキシを用いた監視で完全な言語レベル & 速度向上"

"Proxy-based observation mechanism with full language coverage + better perf"

と説明されていました。

 実際のコードがどうなっているのかは、Vue 3.0のコードが公開されるまで分かりませんが、おそらく、ES2015から導入されたProxyクラスを使って次のようになっているのではないかと思います。

  new Proxy(target, {
    get(target, name, receiver) {
      var value = Reflect.get(target, name, receiver);
      // 依存性の設定
      return value
    },
    set(target, name, value, receiver) {
      // 変更の通知
      Reflect.set(target, name, value, receiver);
    }
  });

一方、現状のVue 2.5では、内部でdefineReactiveという関数の中で、次のようにdefinePropertyメソッドを使って、オブジェクトのgetterやsetterをオーバーライドして、大まかに次のような処理を行なっています。

  Object.defineProperty(obj, key, {
    ...
    get: function reactiveGetter () {
      var value = getter ? getter.call(obj) : val;
      // 依存性の設定
      return value
    },
    set: function reactiveSetter (newVal) {
      var value = getter ? getter.call(obj) : val;
      if (setter) {
        setter.call(obj, newVal);
      }
      // 変更の通知
    }
  });

definePropertyが、プロパティごとにgetter、setterを再定義しなくてはいけなかったのに対して、Proxyではすべてのメソッドに対して一律に監視することが出来ることと、やはりネイティブで実行されるところで速度が向上しているのではないかと思います。

そこで、Proxyを使った方が早いかどうかを確認するために、次のようなサンプルコードを考えて見ました。 まず、definePropertyを使う場合のコードです。

function defineReactive(obj, key) {
  var val = obj[key];
  Object.defineProperty(obj, key, {
    get: function () {
      var value = val;
      return value;
    },
    set: function reactiveSetter (newValue) {
      val = newValue;
      obj.cb[key](newValue);
    }
  });
};

var myObject = { 
  name: "Asial", 
  age: 20, 
  score: 10,
  cb: {
    age: (v) => { console.log('calling set age :' + v) },
    score: (v) => { console.log('calling set score :' + v)},
  }
};

defineReactive(myObject, 'age');
defineReactive(myObject, 'score');

var startAt = new Date().getTime();
for (let i=0; i<100000; i++) {
  myObject.age = i;
  myObject.score = myObject.score + 5;
}
var endAt = new Date().getTime();
console.log('time = ' + (endAt - startAt));

myObjectのagescoreという属性に対し、getterとsetterを定義することで、監視を行なっています。setterが呼ばれた時は、対応するコールバックcbが呼ばれるようにして見ました。 私のローカル環境でこれを実行すると、2407 msという結果になりました。

次に、Proxyを使ったコードに書き換えて見ます。

function defineReactiveAll(obj) {
  return new Proxy(obj, {
    get(target, name, receiver) {
      var value = Reflect.get(target , name, receiver);
      return value;
    },
    set(target, name, value, receiver) {
      Reflect.set(target, name, value, receiver);
      obj.cb[name](value);
    }
  });
};

var myObject = { 
  name: "Asial", 
  age: 20, 
  score: 10,
  cb: {
    age: (v) => { console.log('calling set age :' + v) },
    score: (v) => { console.log('calling set score :' + v)},
  }
};

myObject = defineReactiveAll(myObject, myObject.cb);

var startAt = new Date().getTime();
for (let i=0; i<100000; i++) {
  myObject.age = i;
  myObject.score = myObject.score + 5;
}
var endAt = new Date().getTime();
console.log('time = ' + (endAt - startAt));

Proxyを使った場合の結果は、2573 msとなりました。

あれ、ほとんど同じではあるものの、definePropertyを使った場合よりもわずかですが遅い感じですね?

このサンプルでは、ややdefinePropertyの方が早いという結果になってしまいましたが、実際上は、definePropertyではプロパティごとに指定しなくてはならないため、大量のオブジェクトに対しては定義時にオーバーヘッドがかかってしまうことや、あるいは、既存のgetterやsetterがすでに実装されていた場合に上記サンプルコードより複雑になってしまうことなど、いろいろな要因が重なって、全体としてネイティブProxyを使うよりも重くなってしまうのではないかと思います。Vue 3.0のコードが公開されたら、どうなっているのか確認してみたいですね!

実行中のオーバーヘッドを削減するため、コンパイル時にヒントを追加

Vue 3.0では、コンパイル時に解析した情報をあらかじめ出力コードに組み込んでおくことにより、実行時の処理を向上させているとのことです。いくつかのケースについて、より詳しく見ていきましょう。

コンポーネント探索の高速化、単型の呼び出し、子要素の種類を検出

f:id:knight_9999:20181108183830p:plain:w600

実行時に不要な条件分岐を省略することで、JavaScriptエンジンが最適化しやすくすると説明されています。 エヴァンさんのスライドには、次のようなコードが解説されていました。

テンプレート

<Comp></Comp>
<div>
  <span></span>
</div>

コンパイラ出力 (Vue 3.0)

render() {
  const Comp = resolveComponent('Comp', this)
  return createFragment([
    createComponentVNode(Comp, null, null, 0 /* 子要素なし */), 
    createElementVNode('div', null, [
      createElementVNode('span', null, null, 0 /* 子要素なし */) 
    ], 2 /* 子vnodeは1つ */)
  ], 8 /* 複数のキーを持たない子 */) }

ここでちょっと主題から外れてしまいますが、このコードをよく見ると、テンプレートがコンポーネントではなくて、フラグメント(ルートNodeが1つになっていない)になっていますね。Vue 3.0では、フラグメントも利用出来るようです! Reactも、比較的最近なされたアップデートv16.2でフラグメントを使えるようになっているので、Vue 3.0でも同機能を実装されたようですね。もともとVueは1.x系ではフラグメントを持っていたので、再実装されたということかも知れません。

さて、現状のVue 2.5では、フラグメントは使えませんが、同じようなレンダリング処理をコンパイルすると、およそ次のような感じになります。 (比較のために、ES2015的な記述にしました。また、createTextVnodeなどは省略し、実際はStaticRendringとされている部分も全部一つの関数として表現しています)

コンパイラ出力 (Vue 2.5)

render() {
  return createElement('div', null, null, [
    createElement('Comp', null, null, null),
    createElement('div', null, null, null, [  
      createElement('span', null, null, null)
    ])
  ], 1)
}

Vue 3.0のコードでは、出力するDOMの種類によって createFragmentcreateComponentVNodecreateElementVnode が使い分けられているのに比べて、Vue 2.5のコードでは、すべて同じ createElement が利用されています。Vue 3.0のように、機能に応じて専用のメソッドが呼ばれることが、エヴァンさんのスライドでMonomorphic calls (単型の呼び出し)と呼ばれる手法ですね。このことは、Vue 3.0ではコンパイル時点でDOMの構造を静的に分析した結果が反映されていることを意味していて、これにより、JavaScriptエンジンが最適化しやすい状況を実現しているということなのだと思います。

また、第4パラメータに設定されている数値の意味はよくわかりませんが、Vue 2.5では内部配列の展開処理を区別するための2つしか値がなかったのに比べて、Vue 3.0ではおおくのフラグが用意されているように思えます。これも、実行時の処理を効率的に行うため、コンパイル時に判明したヒントを埋め込んでいるということだと思います。

スロット生成の最適化

 
f:id:knight_9999:20181108183825p:plain:w600

インスタンスが依存関係を正しくトラッキング出来るようになり、不要な親・子のレンダリングを回避出来るようになったとのことです。 これはどういうことでしょうか? サンプルコードは

テンプレート

<Comp>
  <div>{{ hello }}</div>
</Comp>

コンパイラ出力(Vue 3.0)

render() {
  return h(Comp, null, {
    default: () => [h('div', this.hello)] }, 16 /* コンパイラが生成したスロット */)
}

です。

これを見ると、render()関数が返しているオブジェクトの中に、default関数が定義されていて、この関数が呼び出された時に初めて、子要素である [h('div', this.hello)] を返すようになっています。 つまり、親要素のrender処理と、その中の一部であるスロットの処理が分離されていて、これにより、スロットが変更された時に子要素のみが再描画されるようになり、また、親要素が再描画された時でもスロットの内容が変更されていない時は子要素は再描画されなったということのようです。

これもVue 2.5で同じテンプレートをコンパイルしたときを見てみましょう。(ここでは、上記のコードに合わせるため、createElementではなくhとしています。また、vはcreateTextVnodeです。Vue 3.0に合わせてthisと記述していますが、実際はモデルオブジェクトです)

コンパイラ出力(Vue 2.5)

render() {
    return h('Comp', null, [
            h('div', [
                v(toString(this.hello))
            ])
        ])
}

Vue 2.5では、Comp全体を再描画する構造になっていて、スロットの部分だけを切り離すことが出来ません。そのため、スロットが変更された時は、要素全体を再レンダリングし、また、親要素が再描画になったときは子要素も再描画されることになります。

静的ツリーの巻き上げ

ツリー全体へのパッチ適用をスキップされているということです。スライドに記述されていたサンプルコードは次のようになります。

テンプレート

<div>
  <span class="foo">
    Static
  </span>
  <span>
{{ dynamic }}
  </span>
</div>

コンパイラ出力(Vue 3.0)

const __static1 = h('span', {
  class: 'foo'
}, 'static')

render() {
  return h('div', [
    __static1,
    h('span', this.dynamic)
  ])
}

コンパイラ出力を見た感じ、確かに、もとのテンプレートでスタティックになっている部分

  <span class="foo">
    Static
  </span>

を切り離して、その部分だけ巻き上げられて(関数の外側で)定数として定義されています。

Vue 2.5で同様のテンプレートをコンパイルしてみると、おおよそ次のような感じになります。(ここでは、上記のコードに合わせるため、createElementではなくhとしています。また、vはcreateTextVnodeです。Vue 3.0に合わせてthisと記述していますが、実際はモデルオブジェクトです)

コンパイラ出力(Vue 2.5)

render() {
  return h('div', [
    h('span', { staticClass: "foo" }, [
      v("Static")
    ]),
    h('span', [
      h( toString(this.dynamic) )
    ])
  ])
}

こちらでは、全体がrenderメソッドの中に記述されていますね。このため、Vue 2.5では実行時に静的部分も毎回評価されることになります。

インラインハンドラのホイスティング

もう一つ、巻き上げの例として、インラインハンドラの巻き上げについて見て見ましょう。

サンプルコードは

テンプレート

<Comp @event="count++"/>

コンパイラ出力(Vue 3.0)

import { getBoundMethod } from 'vue'
function __fn1 () {
  this.count++
}
render() {
 return h(Comp, {
   onEvent: getBoundMethod(__fn1, this)
 })
}

@eventというのが、抽象的なイベントを表しているのか(つまり、具体的には@click.nativeなどが入る)、それとも、Compコンポーネント内から発火するeventイベントのことなのかよく分からなかったのですが、ここでは、Compコンポーネント内からeventイベントが発火していると想定します。つまり、Compコンポーネントの作りが次のようになっていると仮定します。

<template>
  <div>
  <button v-on:click="fireEvent()">Click Here</button>
  </div>
</template>

<script>
export default {
  name: 'Comp',
  methods: {
    fireEvent () {
      this.$emit('event', null)
    }
  }
}
</script>

さて、Vue 3.0のコンパイラ出力では、getBoundMethod という処理の内容が良く分かりませんが、おおよそ this を 関数__ fn1 にバインドしていると予想できます。 いずれにしても、 __fn1 という関数部分は巻き上げられて(renderメソッドの外側で定義されて)いて、再レンダリングのときも、最初に1回定義されたものを再利用することが出来ます。こうして、実行時の処理を効率化しているようです。

同様のコードをVue 2.5でも確認してみましょう。

コンパイラ出力(Vue 2.5)

render() {
  return h('Comp', {
        on:{ "event": function ($event) { this.count++ } }
    })
}

このようになっていて、こちらでは、レンダリングの度に新しいイベント処理関数が定義されてしまいます。

まとめ

いかがでしたでしょうか? 基調講演で解説していただいたコードを元に、現状のVue 2.5と比較して、どのような変更がなされているかについて確認してみました。 まだVue 3.0のコードは一般公開されていないために、あとは想像するかないのですが、基調講演で垣間見ただけでもさまざまな工夫がされていて、とても期待出来る内容に仕上がっていると思います。

さらに、ネイティブでのレンダリングを行えるリアクティビティAPIや、タイプスクリプトサポートの改善、フックスAPI、タイムスライシングなど、すばらしい機能が盛りだくさんのVue 3.0とのことですので、リリースされるのが楽しみです。

f:id:knight_9999:20181108183834p:plain:w600

SVGを画像化する

はじめに

昨今のクライアントサイドでは、動的な画像のレンダリング、アニメーション、拡張・縮小を求められることが多々あります。そのような際にSVGは利用しやすい形式です。一方で、画像として内容を保存したくなることもあります。そのような場合に使える、SVG画像をPNG画像に変換する方法を簡単に述べます。

SVGとは

SVGファイル(スケーラブル・ベクター・グラフィックス、Scalable Vector Graphics)は画像形式の1つです。XMLをベースにした二次元ベクターデータで画像を描きます。ベクターデータとは「画像を、点の座標とそれを結ぶ線(ベクター、ベクトル)などの数値データをもとにして演算によって再現する方式」です。その大きな特徴は「拡大・縮小しても画質が損なわれない」ことです。

Illastratorなどでも作れますが、JavaScriptで簡単に作れます。実際にSVGを業務などでがっつりと利用するのであれば、d3jsがお勧めです。かなり扱いやすくできています。

実際の変換処理

やり方は非常に簡単で、以下の3ステップで可能です。

  1. SVG画像を作成する
  2. XMLSerializerを使ってSVG画像のデータを取り出す
  3. Canvasを使ってPNG形式に変換する

XMLSerializerは最新のブラウザであればほぼ使用可能です(詳細はこちら)。

コード

実際のコードは次のようになります。svg2jpeg関数にSVG要素(DOM)を渡すことで、PNG画像に変換します。変換処理では、SVG要素と同じサイズのCanvasを使い、Imageオブジェクトを利用してSVGデータをCanvasに貼り付けます。その後、CanvasのtoDataURL()メソッドを使用してPNG画像データを取り出します。

function svg2jpeg(svgElement, sucessCallback, errorCallback) {
  var canvas = document.createElement('canvas');
  canvas.width = svgElement.width.baseVal.value;
  canvas.height = svgElement.height.baseVal.value;
  var ctx = canvas.getContext('2d');
  var image = new Image;
  
  image.onload = () => {
    // SVGデータをPNG形式に変換する
    ctx.drawImage(image, 0, 0, image.width, image.height);
    sucessCallback(canvas.toDataURL());
  };

  image.onerror = (e) => {
    errorCallback(e);
  };

  // SVGデータを取り出す
  var svgData = new XMLSerializer().serializeToString(this.damageMap.nativeElement);
  image.src = 'data:image/svg+xml;charset=utf-8;base64,' + btoa(svgData);
}

// 使い方
svg2jpeg(document.getElmentById('SVG要素のID'), function(data) {
    // data: JPEGのbase64形式データ(文字列)
}, function(error) {
    // error: 何らかのエラーオブジェクト  
})

HTMLサンプル

実際に試すには、以下のコードをHTMLファイルに保存してブラウザで開いて見てください。「変換する」ボタンを押すことで、SVGPNG画像に変換し、表示します。ChromeFirefoxSafariでは動作確認済みです。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Sample</title>
  <style>
    .image {
      margin: 0 auto;
      display: flex;
      align-items: center;
      width: 900px;
    }
    .image__svg {
      width: 400px;
    }
    .image__sep {
      flex-grow: 1;
      text-align: center;
    }
    .image__converted {
      width: 400px;
      height: 400px;
      border: 1px solid rgba(0, 0, 0, 0.5);
    }
  </style>
</head>
<body>
<div class="image">
  <div class="image__svg">
    <svg
        id="svg"
        xmlns="http://www.w3.org/2000/svg"
        width="400"
        height="400"
        viewBox="0 0 400 400"
        preserveAspectRatio="xMidYMid meet"
        style="background-color: #eee;">
      <circle cx="50" cy="50" r="50" fill="rgba(255,0,0,0.5)" />
      <circle cx="200" cy="200" r="50" fill="rgba(0,255,0,0.5)" />
      <circle cx="350" cy="350" r="50" fill="rgba(0,0,255,0.5)" />
      <circle cx="350" cy="50" r="50" fill="rgba(255,255,0,0.5)" />
      <circle cx="50" cy="350" r="50" fill="rgba(0,255,255,0.5)" />
    </svg>
  </div>
  <div class="image__sep">
    <div>==&gt;&gt;</div>
    <div><button id="convert-button">変換する</button></div>
  </div>
  <div class="image__converted">
    <img src="" id="converted-image">
  </div>
</div>
<script>
  (function() {
    document.addEventListener('DOMContentLoaded', function() {
      document.getElementById('convert-button').addEventListener('click', function() {
        svg2imageData(document.getElementById('svg'), function(data) {
          console.log(data);
          document.getElementById('converted-image').src = data;
        }, function(error) {
          console.log(error);
          alert('failed to convert');
        });
      });
    });

    function svg2imageData(svgElement, successCallback, errorCallback) {
      var canvas = document.createElement('canvas');
      canvas.width = svgElement.width.baseVal.value;
      canvas.height = svgElement.height.baseVal.value;
      var ctx = canvas.getContext('2d');
      var image = new Image();

      image.onload = () => {
        ctx.drawImage(image, 0, 0, image.width, image.height);
        successCallback(canvas.toDataURL());
      };

      image.onerror = (e) => {
        errorCallback(e);
      };

      var svgData = new XMLSerializer().serializeToString(svgElement);
      image.src = 'data:image/svg+xml;charset=utf-8;base64,' + btoa(svgData);
    }
  }());
</script>
</body>
</html>

実際にブラウザで表示すると次のように表示されます。

f:id:asialkazushi:20180914141741p:plain
初期画面

変換ボタンをクリックすると、右側の領域にPNG画像が表示されます。開発ツールなどで確認してください。

f:id:asialkazushi:20180914141746p:plain
変換後

おわりに

画像や何らかのグラフ表示などの課題に直面した際には、SVGCanvasを使えればほぼ解決可能です。画像化の方法を組み合わせることで、サーバへ保存できるようにもなり、なお便利になります。

Vue.js + vuexによるToDoアプリケーションの実装

前々回の記事でLaravel 5.4 + Vue.jsの開発環境を構築し、前回の記事でLaravel 5.4によるWeb APIを作成しました。

今回は、作成したWeb APIを使用したToDoリストアプリケーションを、Vue.jsを使って作成します。

PHP側の積み残し



はじめに、昨日のコントローラーを修正します。
もともと、ItemControllerのindex()メソッドは「Item::all()」メソッドで全てのアイテムを返していました。しかし、画面上に表示するのは未完了のアイテムだけです。そこで、index()メソッドを以下のように変更します。



<?php

    public function index()
    {
        return response(Item::query()->where('checked', false)->get());
    }


これで、未完了(checkd=false)のアイテムだけを取得できるようになりました。tests/Feature/ItemTest.php にも機能テストを追加しておきましょう。



<?php

    public function testIndexReturnsOnlyUncheckedItems()
    {
        $item = Item::query()->find(1);
        $item->checked = 1;
        $item->save();

        $response = $this->get('/api/items');

        $response->assertStatus(200);
        $this->assertCount(0, $response->json());
    }


次に、ルーティングを変更し、新しいビューテンプレートを作成します。
routes/web.phpは以下のように、'/'へのアクセスでindexというテンプレートを表示するようにします。



<?php

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/

Route::get('/', function () {
    return view('index');
});


次に、resources/views/index.blade.phpに以下の内容でファイルを追加します。



<!doctype html>
<html>
<head>
    <meta charset="UTF-8">
    <title>ToDo App with Laravel 5.4 + Vue.js</title>
    <link rel="stylesheet" href="css/app.css"/>
    <script type="text/javascript">
        window.Laravel = window.Laravel || {};
        window.Laravel.csrfToken = "{{csrf_token()}}";
    </script>
</head>
<body>
<div id="app"></div>
<script src="js/app.js"></script>
</body>
</html>


以上でバックエンド側の準備は完了です。

完成図



Vue.jsアプリケーションの完成図を示します。



シンプルなToDoリストで、アイテムの一覧・追加・完了・削除の機能を持っています。最終的なファイル構造は以下のようになります。



resources/assets/js
├── api
│   └── items.js
├── app.js
├── bootstrap.js
├── components
│   ├── App.vue
│   ├── Item.vue
│   ├── ItemList.vue
│   └── NewItem.vue
└── store
    ├── action-types.js
    ├── index.js
    └── mutation-types.js

3 directories, 10 files


コンポーネントの構成



このアプリケーションのコンポーネントの構成は以下のようになっています。



新しいアイテムを追加するための「NewItem」と、アイテムのリストである「ItemList」が並列になっています。
ItemListの中にはItemが入れ子になっています。
これら全体を包含するのが「App」コンポーネントです。

ここで問題になるのが、NewItemからItemListへのデータの受け渡しです。
新しいアイテムを追加したら、一覧の末尾に追加したいのですが、Vue.jsが提供する機能(propsによるデータの受け渡し)では、コンポーネントに親子関係がある場合にしかできません。
Appコンポーネントが全ての親になっているので、Appコンポーネントにデータを管理する役割をもたせれば何とかなりそうですが…。

と、このようにコンポーネント間通信が必要になった場合には、状態管理のためのライブラリであるvuexの導入を検討するとよいでしょう。

vuexは、以下のコマンドでインストールできます。



$ npm install --save-dev vuex


このアプリケーションでは、状態管理にはvuexを使用します。

アプリケーションのセットアップ



はじめに、不要なコンポーネントを削除します。
resources/assets/js/components から、Example.vue(と、Hello.vue)を削除しましょう。

次に、Vue.jsでvuexを使えるようにする設定を行います。
resources/assets/js/bootstrap.js で、「window.Vue」の定義の直後に以下のコードを追加します。



window.Vue.use(require('vuex'));


これでVue.jsがvuexを使えるようになります。次に、アプリケーションの初期化処理を記述したresources/assets/js/app.jsを見てみます。



/**
 * First we will load all of this project's JavaScript dependencies which
 * includes Vue and other libraries. It is a great starting point when
 * building robust, powerful web applications using Vue and Laravel.
 */

require('./bootstrap');

/**
 * Next, we will create a fresh Vue application instance and attach it to
 * the page. Then, you may begin adding components to this application
 * or customize the JavaScript scaffolding to fit your unique needs.
 */

const App = require('./components/App.vue');
const store = require('./store/').default;

const app = new Vue({
    el: '#app',
    store,
    render: h => h(App)
});


ここでは、以下の仕事を行っています。

1. Appコンポーネントの読み込み
2. Storeオブジェクトの読み込み
3. Vueアプリケーションの初期化
4. Appコンポーネントの描画

AppコンポーネントとStoreについては後述します。
Vueアプリケーションの初期化時に、Appコンポーネントの描画を実行させているのがポイントです。

Storeの実装



Fluxの仕組みを解説をすると長くなってしまうので、解説は省略します。
vuexのドキュメントがわかりやすいので、こちらを参照してください。

vuexでは、Vuex.Storeオブジェクトに、アプリケーションの横断的な状態(state)を閉じ込めます。
また、stateの変更(mutation)はミューテーターと呼ばれる関数を介して行う必要があります。
このように、情報の一元化 + アクセス方法の制限によって、アプリケーションがもつデータの管理を行いやすくなるのがvuexの利点です。

依存関係の少ないところから見ていくとわかりやすいので、まずはWeb APIとの通信を行う機能の実装から紹介します。

resources/assets/js/api/items.js



const axios = require('axios');
const API_URI = '/api/items';

export const ItemsAPI = {
    getAllUnchecked(callback) {
        axios.get(API_URI)
            .then((response) => {
                callback(response.data);
            })
            .catch((error) => {
                console.log(error);
            });
    },
    create(content, callback) {
        axios.post(API_URI, {content: content})
            .then((response) => {
                callback(response.data);
            })
            .catch((error) => {
                console.error(error);
            });
    },
    check(id, callback) {
        axios.patch(API_URI + '/' + id, {checked: true})
            .then((response) => {
                callback(response.data);
            })
            .catch((error) => {
                console.error(error);
            });
    },
    delete(id, callback) {
        axios.delete(API_URI + '/' + id)
            .then((response) => {
                callback(response.data);
            })
            .catch((error) => {
                console.error(error);
            });
    }
};


実行している処理がほとんど同じなので、冗長な感じですが、やってることは簡単で、Web APIへ問合せて、結果をコールバック関数に渡しているだけです。

次に、Storeオブジェクトの実装を示します。



const Vuex = require('vuex');
const {MUTATION} = require('./mutation-types');
const {ACTION} = require('./action-types');
const {ItemsAPI} = require('../api/items');

const state = {
    items: []
};

const getters = {
    uncheckedItems: (state) => state.items.filter(item => !item.checked)
};

const actions = {
    [ACTION.GET_ITEMS] ({commit}) {
        ItemsAPI.getAllUnchecked(items => {
            commit(MUTATION.SET_ITEMS, items);
        });
    },
    [ACTION.CREATE_ITEM] ({commit}, content) {
        ItemsAPI.create(content, (item) => {
            commit(MUTATION.ADD_ITEM, item);
        });
    },
    [ACTION.CHECK_ITEM] ({commit}, id) {
        ItemsAPI.check(id, () => {
            // チェックされたアイテムをリストから削除
            commit(MUTATION.REMOVE_ITEM_BY_ID, id);
        });
    },
    [ACTION.DELETE_ITEM] ({commit}, id) {
        ItemsAPI.delete(id, () => {
            commit(MUTATION.REMOVE_ITEM_BY_ID, id);
        });
    }
};

const mutations = {
    [MUTATION.SET_ITEMS] (state, items) {
        state.items = items;
    },
    [MUTATION.ADD_ITEM] (state, item) {
        state.items.push(item);
    },
    [MUTATION.REMOVE_ITEM_BY_ID] (state, id) {
        state.items = state.items.filter(item => item.id !== id);
    }
};

export default new Vuex.Store({
    state,
    getters,
    actions,
    mutations
});


stateの中に、このアプリケーションが管理するデータを格納します。
gettersはデータの取得、mutationsはデータの変更を行う関数です。

ミューテーション(mutations)は、必ず同期処理にする必要があります。ミューテションで、HTTPリクエスト等の非同期処理を実行してはいけません。

非同期処理は、アクション(actions)の役割です。アクションは、(1) 非同期処理を実行する (2) ミューテーションを実行する という仕事を行います。外部からデータを取得して、それをstateに格納する際は、アクションを使用します。

アクション/ミューテーションは直接呼び出すことはできません。
外部から呼び出す際は、アクションなら「store.dispatch('アクションの名前')」、ミューテーションなら「store.commit('ミューテーションの名前')」という方式で呼び出す必要があります。
アクション/ミューテーションの名前に「ACTION.GET_ITEMS」といった定数を使用しているのは、文字列による呼び出しが必要だからです。
たとえば、GET_ITEMSというアクションでは、以下のようにしてミューテーションを呼び出しています。



[ACTION.GET_ITEMS] ({commit}) {
    ItemsAPI.getAllUnchecked(items => {
        commit(MUTATION.SET_ITEMS, items);
    });
},


データをサーバに送信する際に実行される流れは以下のようになります。

1. コンポーネントでイベントが発火
2. コンポーネントがStoreのアクションを呼び出す(dispatch)
3. アクションがAPIへ問合せを行う
4. アクションがリクエストの結果に応じてミューテーションを呼び出す(commit)
5. ミューテーションがstateを書き換える
6. 画面上に変更が反映される

コンポーネントの実装



次に、Storeを利用するコンポーネントの側を見ていきます。
まずは、全てのコンポーネントのルートであるAppコンポーネント(resources/assets/js/components/App.vue)です。



<template>
    <div class="container">
        <new-item></new-item>
        <item-list></item-list>
    </div>
</template>

<script>
    const NewItem = require('./NewItem.vue');
    const ItemList = require('./ItemList.vue');

    export default {
        components: {NewItem, ItemList}
    }
</script>


ここでは、NewItemとItemListを読み込んで、これらをコンテナとなるdiv要素に加えています。

NewItemコンポーネント



NewItemコンポーネント(resources/assets/js/components/NewItem.vue)は以下のようになります。



<template>
    <div class="row new-item">
        <label>
            新しいタスク
            <input type="text" name="content" v-model="content" @keydown.enter="addItem"
                   @compositionstart="composing=true" @compositionend="composing=false">
        </label>
        <input type="submit" value="追加" class="btn btn-sm btn-primary"
               @click="addItem">
    </div>
</template>

<script>
    const store = require('../store/').default;
    const {CREATE_ITEM} = require('../store/action-types');

    export default {
        data() {
            return {
                content: '',
                composing: false // IMEによる入力中か否かのフラグ
            }
        },
        methods: {
            addItem(event) {
                if (!this.content) return;
                store.dispatch(CREATE_ITEM, this.content);
                this.content = '';
            }
        }
    }
</script>

<style scoped>
    .new-item {
        margin: 10px 0;
        width: 100%;
        display: flex;
    }

    label {
        justify-content: flex-start;
        flex-grow: 1;
    }

    input[name=content] {
        width: 80%;
    }

    button {
        justify-content: flex-end;
    }
</style>


単純な入力ボックスです。入力ボックス上でEnterキーを押すと送信されるようにしています。注意点は、IMEの状態を管理する必要があるということです。変換モード時のEnterキーで送信されると非常に不便です。ここではcomposition(start/end)というイベントを利用して、IMEの変換モードでは送信を行わないようにしています。

また、前述したように、新しいアイテムを追加したら、ItemListに新しい要素が追加されるようにする必要があります。
ここでは以下の流れで処理を実行しています。

1. NewItemがCREATE_ITEMアクションをdispatch
2. CREATE_ITEMアクションがADD_ITEMミューテーションをcommitしてstate.itemsを書き換え
3. state.itemsが書き換えられたのでItemListにも変更が伝播する

CREATE_ITEMアクションの実装は以下のようになっています。



[ACTION.CREATE_ITEM] ({commit}, content) {
    ItemsAPI.create(content, (item) => {
        commit(MUTATION.ADD_ITEM, item);
    });
},


CREATE_ITEMアクションが呼び出しているADD_ITEMミューテーションの実装は以下のようになっています。
ここでstate.itemsに新しい要素が追加されると、ItemListは新しいItemを描画します。



[MUTATION.ADD_ITEM] (state, item) {
    state.items.push(item);
},


ItemListコンポーネント



次はItemListです。



<template>
    <div class="row">
        <ul class="list-group">
            <item v-for="item in items" v-bind:item="item"></item>
        </ul>
    </div>
</template>

<script>
    const Item = require('./Item.vue');
    const store = require('../store/').default;
    const {GET_ITEMS} = require('../store/action-types');

    export default {
        components: {Item},
        computed: {
            items: () => store.getters.uncheckedItems
        },
        created() {
            store.dispatch(GET_ITEMS);
        }
    }
</script>


ItemListのポイントは、リストが作成されたタイミング(created())で、GET_ITEMSアクションをdispatchしている点です。
GET_ITEMSアクションは、APIに問合せを行って、アイテムの一覧を取得し、そのデータをstateに格納します。

ItemListは、itemsというcomputedプロパティでstore.getters.uncheckedItemsという関数を使用しています。
この関数は、以下のようにstate.itemsにフィルタリングを行って返します。



const getters = {
    uncheckedItems: (state) => state.items.filter(item => !item.checked)
};


また、ItemListのテンプレートでは、以下のようにitemsプロパティを使用してItemを描画しています。



<item v-for="item in items" v-bind:item="item"></item>


APIからデータが返ってきて、state.itemsが変更されると、以下の流れで情報がでんぱして画面が更新されます。

1. uncheckedItems()が返す値が変わる
2. ItemListのitemsプロパティが返す値が変わる
3. ビューに変更が反映される

Itemコンポーネント



最後がItemコンポーネントです。リストの要素をコンポーネントにするかは意見の分かれるところでしょうが、Itemコンポーネントは、(1) 完了済みのチェックをつける (2) アイテムを削除する という独自の機能を持つため、別コンポーネントとして切り出しています。



<template>
    <li class="list-group-item">
        <input type="checkbox" name="checked" @click="checkItem" v-model="item.checked">
        <span class="content">{{item.content}}</span>
        <button class="btn btn-sm remove-button" @click="deleteItem">
            <i class="glyphicon glyphicon-remove"></i>
        </button>
    </li>
</template>

<script>
    const store = require('../store/').default;
    const {CHECK_ITEM, DELETE_ITEM} = require('../store/action-types');

    export default {
        props: ['item'],
        methods: {
            checkItem() {
                store.dispatch(CHECK_ITEM, this.item.id);
            },
            deleteItem() {
                if (!confirm("削除しますか?")) return;
                store.dispatch(DELETE_ITEM, this.item.id);
            }
        }
    }
</script>

<style scoped>
    li {
        display: flex;
    }

    input[name=checked] {
        cursor: pointer;
        margin-right: 10px;
    }

    .content {
        flex-grow: 1;
    }

    .remove-button {
        align-items: flex-end;
        width: 34px;
        height: 30px;
    }
</style>


ここでもやっていることは単純で、CHECK_ITEMアクションないしDELETE_ITEMアクションをdispatchしているだけです。
このように、Storeに機能を寄せて作ると、肥大化しがちなコンポーネントの実装をシンプルに保つことができます。

まとめ



vuexを使うのは初めてでしたが、責務が分かれてきれいに書ける反面、コード量はどうしても多くなりますね。
今回のアプリケーションくらいなら、Appコンポーネントでデータを一元管理するような実装でも十分かもしれません。

本当はOAuth 2.0による認証機能の実装もやる予定だったのですが、予想以上に分量が膨らんでしまったので、認証機能の実装は次回にします。
コードの全体はGitHubでも公開しているので、参考にしてください。

参考



Vue.js
vuex

Laravel 5.4で Vue.js開発環境を手軽に作る

こんにちは。宇都宮です。
最近はアシアル社内でもLaravelを使うことが増えてきました。また、フロントエンドも、ReactやVue.jsを使ったプロジェクトをちらほら見かけるようになってきました。
今回は、Laravelのインストール方法と、フロントエンド開発環境のセットアップ、簡単なVueコンポーネントの作り方を解説します。

Laravelとフロントエンド



Laravelでは、5.3から、Vue.jsが標準のJavaScriptフレームワークになりました。
さらに、5.4では、フロントエンドのビルドツールが、gulpベースのElixirから、WebpackベースのMixに変わりました。
Laravelをインストールすれば、Web APIはLaravelで作って、SPA(Single Page Application)をVue.jsで組む、といったことが簡単にできるようになっています。
なお、LaravelとVue.jsが密結合しているわけではないので、Vue.jsではなくReactやAngularを使うこともできます。

インストール



本記事では、Laravel 5.4.15を使用します。
また、Laravel 5.4の動作には、PHP 5.6.4以上と、いくつかのPHP拡張モジュールが必要です。

インストールにはいくつかの方法がありますが、以下ではcomposerを使用します。



composer create-project --prefer-dist laravel/laravel sample


上記コマンドを使用すると、現在のディレクトリに「sample」という名前のディレクトリが追加され、その中にLaravelアプリケーションが作成されます。
作成直後のディレクトリ構造は以下のようになります。



$ tree -L 1
.
├── app
├── artisan
├── bootstrap
├── composer.json
├── composer.lock
├── config
├── database
├── package.json
├── phpunit.xml
├── public
├── readme.md
├── resources
├── routes
├── server.php
├── storage
├── tests
├── vendor
└── webpack.mix.js

10 directories, 8 files


まずは、Laravelアプリケーションが正常に動くか確認するため、開発用Webサーバを起動してみましょう。



$ php artisan serve
Laravel development server started: <http://127.0.0.1:8000>


次に、ブラウザで「 http://127.0.0.1:8000 」にアクセスしてみましょう。
以下のような画面が表示されれば正常に動作しています。



フロントエンドのセットアップ



Laravel 5.4をインストールすると、以下の内容のpackage.jsonが付いてきます。



{
  "private": true,
  "scripts": {
  // 省略
  },
  "devDependencies": {
    "axios": "^0.15.3",
    "bootstrap-sass": "^3.3.7",
    "jquery": "^3.1.1",
    "laravel-mix": "^0.8.1",
    "lodash": "^4.17.4",
    "vue": "^2.1.10"
  }
}


Laravelをインストールしたディレクトリで「npm install」を実行すれば、必要なライブラリが入ります。
npmのために、事前にNode.jsをインストールしておきましょう。

インストールが完了すると、以下の環境が作成されます。



さきほど省略したpackage.jsonには、npmで実行できるスクリプトが定義されています。たとえば、上記コンパイル処理を一括で実行する際は「npm run dev」を使用します。

注意点として、2017年3月15日現在、「npm run dev」はcross-env.jsのパスの問題で失敗する可能性があります。
GitHubIssueが挙がっているので、将来的には直っているはずです。
取り急ぎの修正方法としては、cross-env.jsのパスを修正して、以下のようにしましょう。



  "scripts": {
    "dev": "node node_modules/cross-env/dist/bin/cross-env.js NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
    "watch": "node node_modules/cross-env/dist/bin/cross-env.js NODE_ENV=development node_modules/webpack/bin/webpack.js --watch --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
    "watch-poll": "node node_modules/cross-env/dist/bin/cross-env.js NODE_ENV=development node_modules/webpack/bin/webpack.js --watch --watch-poll --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
    "hot": "node node_modules/cross-env/dist/bin/cross-env.js NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js",
    "production": "node node_modules/cross-env/dist/bin/cross-env.js NODE_ENV=production node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js"
  },


scriptを修正して「npm run dev」を実行するとコンパイルが成功するはずです。

これで、public/js/app.jsにコンパイル済みのJavaScript、public/css/app.cssコンパイル済みのCSSが出力されるようになります。
また、「npm run watch」を走らせておくと、JavaScriptCSSを追加・変更した際に自動的にコンパイルが行われるようになります。

まずはVue.jsが使えるようになっているか確認しましょう。
resources/views/welcome.blade.php を以下の内容で置き換えます。



<!doctype html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Hello Vue</title>
    <link rel="stylesheet" href="css/app.css"/>
    <script type="text/javascript">
        window.Laravel = window.Laravel || {};
        window.Laravel.csrfToken = "{{csrf_token()}}";
    </script>
</head>
<body>
<div id="app">
    <example></example>
</div>
<script src="js/app.js"></script>
</body>
</html>


以下のように表示されればOKです。



ここでは、Laravelに付属するExampleという名前のVueコンポーネントを使用しています。
また、resources/assets/js/bootstrap.jsで参照している「window.Laravel.csrfToken」という変数の定義も行っています。この変数の使いみちについては後で説明します。

JavaScriptアプリケーションのブートストラップ



resources/assets/js/bootstrap.js に、JavaScriptアプリケーションを動作させるのに必要な初期化処理を記述します。




window._ = require('lodash');

/**
 * We'll load jQuery and the Bootstrap jQuery plugin which provides support
 * for JavaScript based Bootstrap features such as modals and tabs. This
 * code may be modified to fit the specific needs of your application.
 */

window.$ = window.jQuery = require('jquery');

require('bootstrap-sass');

/**
 * Vue is a modern JavaScript library for building interactive web interfaces
 * using reactive data binding and reusable components. Vue's API is clean
 * and simple, leaving you to focus on building your next great project.
 */

window.Vue = require('vue');

/**
 * We'll load the axios HTTP library which allows us to easily issue requests
 * to our Laravel back-end. This library automatically handles sending the
 * CSRF token as a header based on the value of the "XSRF" token cookie.
 */

window.axios = require('axios');

window.axios.defaults.headers.common = {
    'X-CSRF-TOKEN': window.Laravel.csrfToken,
    'X-Requested-With': 'XMLHttpRequest'
};

/**
 * Echo exposes an expressive API for subscribing to channels and listening
 * for events that are broadcast by Laravel. Echo and event broadcasting
 * allows your team to easily build robust real-time web applications.
 */

// import Echo from "laravel-echo"

// window.Echo = new Echo({
//     broadcaster: 'pusher',
//     key: 'your-pusher-key'
// });



ほとんどライブラリを読み込んでいるだけですが、1つ興味深い処理が行われています。



window.axios.defaults.headers.common = {
    'X-CSRF-TOKEN': window.Laravel.csrfToken,
    'X-Requested-With': 'XMLHttpRequest'
};


axiosは、HTTPクライアントのライブラリです。
ここでは、axoisの設定を変更し、リクエストヘッダにCSRFトークンと、XMLHttpRequestの目印を付けるようにしています。
このようにすることで、axiosを使って送られたリクエストに自動的にCSRFトークンが付与されるようになります。

Vue.jsアプリケーションの起動



アプリケーションの起動処理等は resources/assets/js/app.js に記述します。
ここは以下のようになっています。




/**
 * First we will load all of this project's JavaScript dependencies which
 * includes Vue and other libraries. It is a great starting point when
 * building robust, powerful web applications using Vue and Laravel.
 */

require('./bootstrap');

/**
 * Next, we will create a fresh Vue application instance and attach it to
 * the page. Then, you may begin adding components to this application
 * or customize the JavaScript scaffolding to fit your unique needs.
 */

Vue.component('example', require('./components/Example.vue'));

const app = new Vue({
    el: '#app'
});


(1) bootstrap.jsを読み込んで初期化処理を実行
(2) Example.vueというVueコンポーネントを読み込み
(3) Vueアプリケーションを起動

という流れです。

Vue.jsアプリケーションを起動する際には、どの要素をVue.jsアプリケーションのルートとするかを指定します。ここでは、idが"app"である要素がVue.jsアプリケーションのルートになるように指定しています。

Vue.jsでは、「コンポーネント」という単位で、再利用可能なパーツを定義することができます。
resources/assets/js/components/Example.vueは以下の内容になっています。



<template>
    <div class="container">
        <div class="row">
            <div class="col-md-8 col-md-offset-2">
                <div class="panel panel-default">
                    <div class="panel-heading">Example Component</div>

                    <div class="panel-body">
                        I'm an example component!
                    </div>
                </div>
            </div>
        </div>
    </div>
</template>

<script>
    export default {
        mounted() {
            console.log('Component mounted.')
        }
    }
</script>


templateタグの中には、コンポーネントのHTMLを記述します。同様に、scriptタグの中には、コンポーネントの動作を定義します。ここでは使用されていませんが、styleタグを使用して、コンポーネントに適用するCSSを定義することもできます。

独自コンポーネントの定義



次に、独自のVueコンポーネントを定義してみましょう。
resources/assets/js/components/Hello.vueというファイルを、以下の内容で追加します。



<template>
    <h2>Hello <span class="name">{{name}}</h2>!</div>
</template>

<script>
    export default {
        props: ['name']
    }
</script>

<style scoped>
    .name {
        font-weight: bold;
    }
</style>


ここでは、Vue.jsのコンポーネントに、外部から値を渡せるように実装しています。

はじめに、コンポーネントがもつプロパティを、propsプロパティの中に配列で定義します。
次に、コンポーネントを利用する箇所で「<hello name="Laravel"></hello>」のように、コンポーネントに渡したい値を記述します。
propsに定義したプロパティはコンポーネント内から参照できるので、{{name}} のように記述すると文字列としてプロパティの値を出力することができます。

※{{}}という記法はLaravelのテンプレートエンジン(Blade)でも値を出力する際に使用します。Vueコンポーネント内で{{}}を使用する際は気にする必要はありませんが、Bladeテンプレートの中でVue.jsの{{}}を使いたい場合「@{{}}」という風に先頭に@をつけると、Bladeはこの部分を無視します。

style要素にscoped属性をつけている点にも注目です。scoped属性を使うと、style要素内で定義されたスタイルが適用される範囲を限定することができます。ここでは、Vueコンポーネントのテンプレート内でのみ適用可能な「name」クラスを定義しています。テンプレートの外部で「name」クラスを使った場合には、このスタイルは適用されません。

次に、app.jsにHelloコンポーネントを登録します。



Vue.component('hello', require('./components/Hello.vue'));


最後に、welcome.blade.phpのbodyの中を以下のように書き換えます。



<div id="app">
    <hello></hello>
    <hello name="Laravel"></hello>
    <hello name="Vue.js"></hello>
</div>


以下のように表示されればOKです。



参考



Onsen UIが生まれたきっかけ



本記事はOnsen UI Advent Calendar 2016のエントリーです。Onsen UIが生まれたきっかけについて、簡単に紹介したいと思います。

当時PhoneGapといわれていたCordovaに足りなかったもの



2009年頃からWebViewを用いた、今でいうHTML5ハイブリッドアプリ開発をしていた訳ですが、当時のデバイスはパフォーマンスも低く、ブラウザーの機能が貧弱だったことも相まって、満足のいくアプリをHTML5「だけ」で作ることがほぼ不可能と言ってもいい状況でした。

思い返すとiOS 4Android 1.6の時代です。まだposition:fixedも使えず、CSSトランジションも怪しい動きをしていました。

一方で、コンテンツをHTML5で記述することのメリットは多く、アプリのマルチプラットフォーム対応や、サーバーからの動的配信で内容をアップデートすることができるなど、この分野の可能性を感じていました。

それがPhoneGap/Cordova/Monacaにつながっていくわけですが、とはいえUIWebViewだけでは、iOSAndroidのネイティブUIにかなう表現ができなかったのは事実です。

そこでネイティブUIフレームワーク



そこで2010年からMonacaを開発するに当たり、どのようにハイブリッドアプリでUI部分でブレークスルーを実現するかが特に大きな課題だと考えていました。その結果、私たちのチームが開発したのは「ネイティブUIとUIWebViewを組み合わせる」というアプローチです。

具体的には、コンテンツの中身はUIWebViewで記述しつつも、ナビゲーターやタブといったネイティブなトランジションが圧倒的に有利な部分はネイティブで描画する、という仕組みでした。

PhoneGapの拡張エンジンとして、オープンソースでリリースしました(メンテナンスはしていませんが、いまでも公開は続けています)。



このフレームワークでは、複数ページをスムーズに遷移するため、マルチWebView構成になっていました。ようするに、次のページに遷移した場合には別のWebViewが作られ、ページスタックが作られます。

ネイティブUIフレームワークの課題からOnsen UIへ



ただしこの仕組みには一つ大きな問題がありました。それは、WebViewが複数に分かれるため、JavaScriptでスコープを管理するのが大変難しくなったということでした。新しいページが全く別のJavaScriptインスタンスとして表示されるため、同じアプリであるにもかかわらず、ページ間でリソースを共有することが難しかったのです。

また、JSON形式でUI定義を記述できる仕組みとしていましたが、その結果デザインのカスタマイズに弱く、多様な表現をしたいアプリにとって制約になってしまうことも問題でした。

一方、iOS 7やAndroid 4が登場しフラットデザインが主流になるなか、WebViewでフルにUIを実装することの現実味が帯びてきました。そこで、社内で議論を行った末、これまで作っていたネイティブUIフレームワークを捨て、Onsen UIに移行するという決断を行いました。

これからのOnsen UI



最初はAngular 1のディレクティブ機能を使って実装したOnsen UIですが、当初は日本語の文献も全くなく、Angularは難しすぎるのではないか?という危惧もありました。しかし一方で、Angular以上にうまく部品をコンポーネント化できる仕組みがなく、Angularを選定したという経緯があります。

昨今ではCustom Elementsも安定したことから、Onsen UI 2.0よりAngularへの依存をなくし、ピュアなWeb Componentsフレームワークとなりました。そのメリットを生かし、Angular 2やReactへの対応とともに、Vue 2といったメジャーなフレームワークのサポートを進めていきたいと考えています。

ぜひ進化するOnsen UIにご期待ください。そして、日本発の世界的フレームワークの挑戦に向けて、GitHubのスターで応援ください!

Monacaのプレビューを使う際のTips

最近Monacaをハンズオンで使っています。開発環境を用意せず、ブラウザさえあればすぐに動くものが作れるというのは大きなポイントになります。



しかし普段の開発環境とは異なる分、詰まってしまうこともあるようです。そこで今回はMonaca IDEを使った開発において注意して欲しいポイントを紹介します。



Webブラウザとスマートフォンアプリでのイベントの違い



WebブラウザでjQueryなどを使った開発を行っている時に、 $(function() {}) をよく使うかと思います。これはDOMの構築などが終わり、JavaScriptを安全に実行できるようになった状態で呼び出されます。しかしMonaca(Cordova)アプリの場合、さらにプラグインの読み込みなども完了しなければ安全に開始できません。それは deviceready というイベントになります。ただしこのイベントはWebブラウザでは実装されていないので、Monaca IDEのプレビューでは使えません。



そこで次のようにコードを書くと便利です。





function onDeviceReady() {
    alert("読み込まれました");
}
var event = typeof cordova === 'undefined' ? 
                              'DOMContentLoaded' : 'deviceready';
document.addEventListener(event, onDeviceReady, false);


これはWebブラウザの場合はDOMContentLoaded、スマートフォンアプリの場合はdevicereadyをイベントリスナーに設定する指定です。iPhone/iPad/Androidの区別だけではスマートフォンのWebブラウザ判別には使えませんので、Cordovaアプリに特有のグローバル変数であるcordovaがあるかどうかを判定に使っています。これでプレビューの場合も確認が容易になります。



このテクニックはclickイベントとtapstartイベントを分ける際にも使えます。スマートフォンにおけるclickイベントは若干の遅延があるので、tapstartを積極的に使っていくべきなのですが、Monaca IDEのプレビューでは反応しなくなってしまいます。そこで処理分けすることでどちらでも動作する、かつ最適な動作が期待できるようになります。



リモートファイルを読み込む場合の注意



同様にMonaca IDEのプレビューではリモートファイルを読み込むのに制限(CORS)があります。そのため、スマートフォンアプリとして外部のXMLJSONを読み込む場合、同じファイルをMonacaプロジェクト内にも用意して、プレビューの時にはそのファイルを読み込むようにしましょう。処理分けは上記と同じ仕組みが使えます。



自社のアプリで、サーバと通信するといった場合にはCORSの設定を行っておけば問題ないでしょう。



JavaScriptエラーの確認の方法



Monaca IDEにはコンソールが表示されているので、プレビューで起こっているエラーも確認できるかと思ってしまう方がたくさんいます。しかしこれはMonacaデバッガー用のエラー表示になるので、プレビューのエラーは出ません。





そこでエラーの確認方法なのですが、プレビューのアプリの画面相当部分を右クリックします。そして出てきたコンテクストメニューで要素の検証であったり、検証といったDevToolsが開くメニューを選択します。これ以外の方法ですと、プレビュー全体であったり、Monaca IDEを対象としたDevToolsが開いてしまいますので注意してください。





DevToolsが開けば、コンソールを見てエラーメッセージが確認できます。なお、時々保存忘れというケースもありましたので、ちゃんとファイル保存されているかも確認してください。





プレビューはHTMLファイルを開いている時以外には表示されません



プレビューはあくまでもHTMLファイルを編集している場合に限って右側に表示されます。JavaScriptCSSなどを編集中に確認する場合は別ウィンドウとして表示されるので注意してください。なお、別ウィンドウの場合でもファイル編集後のリロードは自動的に行われます。








こうした点に注意すると、初学者の方であってもMonaca IDEを使ったアプリ開発がスムーズに開始できるかと思います。特にプレビューをうまく使えばデザインや動作確認が素早く行えるようになります。ぜひお試しください!

動画を使ったアプリ利用解析を実現するReproはMonacaで簡単に利用できます

Monacaでは多くの外部サービスと連携できるようになっています。今回はアプリマーケティングで欠かせない利用者を理解するのに役立つReproを紹介します。





Reproについて



Reproはアプリの操作を記録し、Web上で動画として操作を確認できるサービスです。ユーザの利用状態を可視化することで、どこでユーザが迷っているのかであったり、アクティブ率に関わるアプリ上の問題を発見することができます。



数多くのプラットフォームに対応しており、Monacaにも対応しています。しかも無料ユーザから利用できるようになっています。





Reproでトークンを取得する



まずReproにてユーザ登録を行います。そうするとトークンが取得できますので、コピーしておきます。Repro側で必要な作業はそれくらいで、とても簡単にはじめられます。



Monacaでの使い方



Monacaで適当なアプリを作成した後、設定メニューの*を選択します。





出てきた一覧の中でReproを選択します。





詳細な説明が出ますのでセットアップをクリックします。





確認が出ますのでOKをクリックして実行します。





これでReproのSDKがインストールされました。





設定を行う



次に測定するための情報を記述します。単純に全操作を記録する場合は次のように書きます。YOUR_APP_TOKENはReproにて取得したトークンに書き換えてください。





document.addEventListener("deviceready",onDeviceReady,false);
function onDeviceReady() {
    Repro.setup("YOUR_APP_TOKEN");
    Repro.startRecording();
}


さらに何かのイベント(クリックなど)を記録したい場合は任意の場所で次のように記述します。





Repro.track("App Launch");


また、ユーザを指定することもできます。





Repro.setUserID("foo@example.com");


これだけで使えますのでとても手軽です。



アプリをビルドする



Reproを使うためにはMonacaデバッガーではなくビルドしたアプリで使う必要があります。今回はiOSアプリとしてビルドしています。





ビルドが完了したら実機にインストールしてください。



Reproを試す



Reproを試す場合は、アプリを起動して適当に操作するだけでOKです。最後にホームボタンを押して終了です。アプリがバックグラウンドになると記録が停止します。動画がアップロードするまで管理画面には反映されませんが、ほとんど待ち時間なく反映されるはずです。





アップロードされた動画はWeb管理画面上でいつでも繰り返し再生ができます。イベントが記録がされていれば、動画の途中に印が入っています。








MonacaアプリにReproを組み込むのはとても簡単にできます。コード量も少ないので、すぐに使いこなせるでしょう。後はアプリの動線を解析し、より良いアプリ開発に取り組んでください。



Repro

Onsen UI 2.0の紹介と始め方

ハイブリッドによるスマートフォンアプリ開発を手軽なものにしてくれるUIフレームワークとして開発しているのがOnsen UIです。現在も開発が継続されており、間もなく2.0が正式リリースとなります(執筆時点でβ)。



特に大きな転換ポイントと言えるのが、AngularJSとの切り離しになります。そこで今回はOnsen UI 2.0の使い方を紹介します。



インストール方法は4つ



Onsen UIのインストール方法は主に4つ用意しています。いずれか使いやすいものを選んでください。



Node.js/npmを使う方法



Node.js/npmを使ってインストールする場合、次のようにコマンドを打ちます。





npm install onsenui@2.0.0-beta.7 --save // β版の現在の場合。
npm install onsenui --save // 2.0の正式版リリース後。現在は1.3系がインストールされます。


また、このままですと node_modules 以下に配置されて使いづらいので、browserifyをインストールします。





npm install -g browserify


そしてコードを書きます。例えばファイルを index.js とします。






require('onsenui');

// 自分のWebアプリケーションのコード


後は最後に browserify を実行します。





browserify index.js -o app.js


これで onsenuiが入った状態でJavaScriptコードが生成されます。JavaScript側ではapp.jsを読み込むようにすれば問題ありません。



Bowerを使う方法



BowerもNode.js/npm同様に進めることができます。Monaca IDEはBowerを使ったプロジェクトのインポートに対応していますので、さらに手軽と言えそうです。ただしBower自体はnpmを使ってインストールします。





npm instlal bower -g


そしてアプリを開発するプロジェクトのルートで bower initを実行します。





bower init


次にOnsen UIをインストールします。現在はバージョンを指定する必要があります。





bower install onsenui#2.0.0-beta.7 --save


ファイルは bower_components/onsenui/ 以下にインストールされます。



ファイルをダウンロードする



3つ目はJavaScript/スタイルシートファイルを直接ダウンロードする方法です。Releases · OnsenUI/OnsenUI-distから最新版がダウンロードできます。解凍したフォルダの中にあるjs/cssフォルダを好きな場所に配置してください。



Monacaのテンプレートを使う



最後にMonacaのテンプレートを使う方法です。Monacaのプロジェクトテンプレートの中に、Onsen 2.0 クイックスタートというテンプレートを用意していますので、これを選択するという方法になります。これは予めOnsen UI 2.0が組み込まれていますので手軽にはじめることができます。





こちらがOnsen UI 2.0クイックスタートプロジェクトです。



Reactと組み合わせたデモも内包されています。



実際に使ってみる



では実際にOnsen UI 2.0を使ってみたいと思います。例えばHTMLは次のようになります。ファイル名は index.html とします。





<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="apple-mobile-web-app-capable" content="yes" />
    <meta name="mobile-web-app-capable" content="yes" />
    <title>Onsen UI 2.0 Quickstart</title>
    <script src="node_modules/onsenui/js/onsenui.min.js"></script>

    <link rel="stylesheet" href="node_modules/onsenui/css/onsenui.css" type="text/css" media="all" />
    <link rel="stylesheet" href="node_modules/onsenui/css/onsen-css-components.css" type="text/css" media="all" />
</head>

<body>
  <ons-page>
  </ons-page>
  <ons-tabbar>
  <ons-tab page="page1.html" label="Page 1" icon="square" active="true"></ons-tab>
    <ons-tab page="page2.html" label="Page 2" icon="square"></ons-tab>
    <ons-tab page="page3.html" label="Page 3" icon="square"></ons-tab>
    <ons-tab page="page4.html" label="Page 4" icon="square"></ons-tab>
    <ons-tab page="page5.html" label="Page 5" icon="square"></ons-tab>
  </ons-tabbar>
</body>


見て分かる通り、onsenui.min.jsしかJavaScript側では読み込んでいません。スタイルシートはベースになるonsenui.cssと、各種コンポーネント用のonsen-css-components.cssを読み込んでいます。



bodyタグの中身はを使ったOnsen UIの定義になります。さらにpage1.htmlを次のような内容で作成します。





<h3 class = "title-h3">iOS Switch</h3>
<p>
  <ons-switch></ons-switch>
</p>
<h3 class = "title-h3">Material Switch</h3>
<p>
  <ons-switch modifier="material" checked></ons-switch>
</p>


こちらはヘッダーもなく、いきなりbodyタグ内の内容を記述します。さらにpage2.htmlを次のように定義します。





<ons-list>
  <ons-list-header>Page 2</ons-list-header>
  <ons-list-item>Item</ons-list-item>
  <ons-list-item>Item</ons-list-item>
</ons-list>


こちらはを使っています。いずれのファイルもJavaScriptの定義であったり、AngularJSの記述は一切ありません。



このファイルを開くと、次のように表示されます。





まさにスマートフォンアプリ風のUIになっているのが分かるかと思います。さらにons-tabbarの一番左のページがデフォルト表示になります。今回はpage2.htmlまでしか作っていませんが、タブ1とタブ2がタップで表示切り替えできる点も確認できるかと思います。






このように外部ライブラリへの依存性が減ったことで、自由度が高まったり、すでに多数あるJavaScriptフレームワークと組み合わせた開発ができるようになっています。Onsen UI 2.0は間もなく正式リリースになります。ぜひお試しください!



Onsen: HTML5 Hybrid App Framework & UI Components

Lebabを使ってECMAScript6を体験しよう

新しいJavaScriptであるECMAScript6(以下ES6)はすでにnode.jsをはじめとする幾つかの環境で使えるようになっています。クラスやimport、Promise、テンプレート文字列など便利な機能がたくさんありますが、なかなか使う機会に恵まれないのではないでしょうか。



そこでよく使われているのはBabelで、ES6で書いたコードを従来のJavaScriptであるECMAScript5(以下ES5)に変換してくれるソフトウェアです。Babelを使うことによって、コーディングは先進的なES6で行いつつ、実際の利用はES5で幅広いブラウザに対応させると言った利用ができます。



今回はその逆で、すでにあるES5のコードをES6に変換してくれるLebabというソフトウェアを紹介します。LebabはBabelを逆から読んだ名前になっています。



Lebabの使い方



Lebabはnpmを使ってインストールができます。





$ npm install -g lebab


コマンドオプションは次のようになっています。





$ lebab --help

  Usage: lebab [options] <file>

  Turn your ES5 code into readable ES6

  Available transforms:

    + class .......... prototype assignments to class declaration
    + template ....... string concatenation to template string
    + arrow .......... callback to arrow function
    + let ............ var to let/const
    + default-param .. use of || to default parameters
    + obj-method ..... function values in objects to methods
    + obj-shorthand .. {foo: foo} to {foo}
    + no-strict ...... remove "use strict" directives
    + commonjs ....... CommonJS module loading to import/export

  Options:

    -h, --help            output usage information
    -V, --version         output the version number
    -o, --out-file <out>  compile into a single file
    --enable <a,b,c>      enable only specified transforms
    --disable <a,b,c>     disable specified transforms


後はES5のJavaScriptファイルと、出力先のJavaScriptファイルを指定すればOKです。





$ lebab es5.js -o es6.js


利用するライブラリ、しないライブラリを指定することもできます。





$ lebab es5.js -o es6.js --enable let,arrow,commonjs


他にもWeb上でライブデモを使って試すこともできます。





サンプル



例えば変数の定義と、その後での上書きによってconst/letを使い分けてくれます。





// Let/const
var name = 'Bob', time = 'yesterday';
time = 'today';
  ↓
const name = 'Bob';
let time = 'yesterday';
time = 'today';


nameは定義したまま使っていますのでconst、timeはその後で上書きしているのでletを使っています。



テンプレート文字列も使えます。





// Template string
console.log('Hello ' + name + ', how are you ' + time + '?');
  ↓
// Template string
console.log(`Hello ${name}, how are you ${time}?`);


オブジェクトメソッドはfunctionが省略されます。





var bob = {
  // Object shorthand
  name: name,
  // Object method
  sayMyName: function () {
    console.log(this.name);
  }
};
  ↓
const bob = {
  // Object shorthand
  name,
  // Object method
  sayMyName() {
    console.log(this.name);
  }
};


さらにクラスもきちんと認識されます。





var SkinnedMesh = function SkinnedMesh() {
};

SkinnedMesh.prototype.update = function (camera) {
  camera = camera || createCamera();
  this.camera = camera;
};

Object.defineProperty(SkinnedMesh.prototype, 'name', {
  set: function (geometry) {
    this.geometry = geometry;
  },
  get: function () {
    return this.geometry;
  }
});

  ↓
class SkinnedMesh {
  update(camera=createCamera()) {
    this.camera = camera;
  }
  set name(geometry) {
    this.geometry = geometry;
  }
  get name() {
    return this.geometry;
  }
}


後はNode.jsの場合requireを使ってきましたが、importに書き換わります。





var lebab = require('lebab');
module.exports = SkinnedMesh;
  ↓
import lebab from 'lebab';
export default SkinnedMesh;


最後にアローファンクションです。これもES6の特徴的な書き方だと思います。





var render = function () {
  requestAnimationFrame(render);
};
  ↓
const render = () => {
  requestAnimationFrame(render);
};


Lebabを使うメリット



普段の開発の中でLebabを使うことはあまり多くないかと思います。しかし今後ES6が広まっていく中で、ES6の書き方を覚える必要が出てくるでしょう。そんな時にLebabを使って軽く試せるとモダンな書き方を確認できるようになります。



さらに既存のES5で書かれた多くのリソースもLebabを使うことでES6に変換できるようになります。資産が多すぎるために乗り換えを躊躇してしまっている場合は特に便利ではないでしょうか。



ぜひLebabを試してみてください!



mohebifar/lebab: Turn your ES5 code into readable ES6. It does the opposite of what Babel does.