GENIEEエンジニアブログ

株式会社ジーニーのエンジニアがアドテクノロジー・マーケティングテクノロジーなどについて情報発信するブログです。

MAJINのフロントエンドを支える技術

はじめに

どうも、R&D本部のマーケティングオートメーション開発部所属の張です。

業務では弊社のマーケティングオートメーションプラットフォーム「MAJIN」のフロントエンド開発・保守、基盤の改善、DevOps推進などなどに携わっています。最近仕事で主に使う言語はGoです。

初回なので、今回はMAJINのフロントエンドで使う技術を簡単に紹介いたします。

MAJINのフロントエンド

MAJINは2016年7月にリリースしたばかりのマーケティングオートメーションプラットフォームで、社内では比較的若いプロジェクトです。そのため、MAJINでは他のプロジェクトよりトレンディな技術を多く採用する傾向があります。

ローンチの時にはフロントエンドとバックエンドを分けて別々で開発する体制をとりましたが、最近はチーム内の交流が増え、フルスタックエンジニアを目指すチームメンバーも急増しました。

今回は「フロントエンド」をテーマとしていますが、JavascriptやHTMLを使ったWebサイトの設計・実装といった「狭義のフロントエンド」ではなく、DBのデータを取得や更新する為のサーバ側の設計・実装(API等)も含めた「広義のフロントエンド」と定義させていただきます。

技術沿革

  • ローンチ時のフロントエンド:PHP 7 + Symfony 3.x + ES6 + Vue.js 1.x
  • 最近のフロントエンド: Python 3.5 + Flask + ES6 + Vue.js 2.x

PHP+Symfonyを選んだ&やめた理由

PHP+Symfonyを選んだ一番最初の理由は、社内で既に使われていて短期間にエンジニアを確保しやすいところでした。しかし、MAJINの開発加速に伴い、PHP+Symfonyの限界を徐々に感じました。

  • MAJINではRDBMSMySQLの他に、DynamoDBやAerospikeなどのNoSQL系のデータベースも使用しています。これらDBに対し、Symfonyのジェネレータ(Ruby on Rails の scaffolding に相当)やORMapperなどの機能のメリットをほとんど生かせませんでした。例えば最初にDoctrine Annotation ReaderでDynamoDB用のORMapperを作りましたが、クエリの性能とデータ一貫性のコントロールを考慮して結局AWS SDKをそのまま使うことにしました。
  • MAJINのフロントエンドの他のマイクロサービスのAPIとのやりとりが想定よりも多くなりました。これらのAPIで処理する機能にさらにSymfonyのようなフレームワークを挟むとオーバーヘッドが増えます。デバッグとテストもやりにくくなります。
  • MAJINの仕様上、コンテンツやシナリオなどのビジネス要件にはリッチな画面が要求されています。フロントエンドのSPA化(Single-Page Application)によってSymfonyの存在感がさらに薄れました。

Python 3.5+Flaskを選んだ理由

  • 一部のバックエンドとAPIPython 3.5で書かれているのでコードを共有ができ、PHP車輪の再発明をせずに工数を節約できます。
  • Flaskは手軽で軽量でウェブアプリケーションフレームワークとして使いやすいです。ライブラリも豊富で特に不自由さを感じません。
  • Flaskは自由度が高いので、必要に応じて機能の拡張も簡単にできます。MAJINにも開発効率向上のためにFlaskでいくつかのプラグインを開発しました。

ES6+Vue.jsを選んだ理由

f:id:tech-geniee:20170718181028j:plain

モダンなSPAの開発にはMVVM(Model–view–viewmodel)フレームワークが不可欠です。数多く存在するMVVMフレームワークの中でも、下記の理由でVue.jsを選びました。

  • 使い方が簡単、勉強コストが低い。
  • Monolithicなフレームワークではなく、初めから少しづつ適用していける(Progressive framework)。
  • 理論上と実際の性能がよかった。

Vue.jsの実装はごく簡単で、下記のようにModel,View,ViewModelをそれぞれ定義するだけで、入力値が即Modelに保存され、Model内の値もすぐViewに反映されます。一見簡単そうな仕組みですが、データフローを正確に定義することによって大規模な開発に莫大な威力を発揮できます。

See the Pen Example of two-way binding by Cheung Chifung (@cheungchifung) on CodePen.

<div id="main_view">
  <label for="name">Enter name:</label>
  <input type="text" v-model="name" id="name" name="name" />
  <p>Welcome, {{ name }}!</p>
</div>

世の中のMVVMフレームワークのReactiveの実装はいくつもあります。参考

  • Pub-Sub(backbone.js): pub, subの方式でデータを更新する。
  • Dirty Checking(augular.js): click等の特定のイベントが発火したらデータの更新検知を行う。
  • Virtual DOM(React): Virtual DOMを用いて、Modelの変化を高速にViewに反映する。
  • Hook(Vue.js): ECMAScript5のObject.defineProperty()を利用して値の変化を検知し、変化があったらSubscriberにメッセージを送りコールバックを呼ぶ。(2.0からVue.jsもVirtual DOMを導入した)

Vue.jsがObject.definePropertyを使うことで、this.name = "MAJIN"のように書くだけで、Observer(Vue.jsで変化を監視するコンポネート)が自動的に変化を検出してテンプレートに反映され、開発はかなり楽になります。その代わり、IE8以下などのECMAScript5未対応のブラウザは対応できません。 (MAJINのサポートブラウザは全てES5対応なので問題ありませんでした) Vue.jsのReactiveの仕組みを詳しく説明したいところですが、今回は割愛します。

また、Vue.jsの公式の比較記事によると、Vue.jsはReactに劣らないパフォーマンスを提供できます。MAJINではVue.js 1.0と2.0を使っていますが、性能面の問題は特に感じませんでした。(と言っても使い方次第でパフォーマンスが落ちることはもちろんあります。例えば検知不要な値を大量にObserverに突っ込んでブラウザが固まることもあります)

当初Vue.jsを選んだ時、公式のデモで使われていたES6をそのまま導入しましたが、ES6には型チェックがないので心細く、TypeScriptに乗り換えようと試みたこともあります。しかし、当時はTypeScriptとVue.jsの相性が悪く感じ、挫折してしまいました。 最近Vue.jsとTypeScriptを取り巻く環境も大きく変わりましたので、そろそろ再チャレンジしようと思います。

Vue.jsでハマったところ

Vue 1.x から 2.x

MAJINリリース時に2.xはまだstableになっていないため、古いコードは全部1.xを使いました。

2.xではいろんな特性が追加されましたが、特にアップグレードしなければならない理由が無く、そのまま放置しようと思いました。しかしVue.js 2.0リリース後半年も経たず、多くのプラグインが2.0にしか対応しない状態になってしまって、止むを得ずにVue.js 2.0に移行しました。

2.0へ移行するには、書き直しが必要な箇所が多く思ったより大変でした。MAJINは複数のエンドポイントでVue.jsを使っているので、vue1.xと2.xのページのwebpackファイルを分けて少しずつ移行することにしました。Vue.js 1.xに書いたUIコンポーネントもVue.js 2.xでそのまま動かないので、一部のUIコンポーネントも2.0版で書き直しました。

移行は辛かったですが、1.xと比べて使えるライブラリが増え、Virtual DOMなどの2.xのメリットを手に入れたので、移行する価値はあると思います。

エコシステムが未熟

Vue.js 1.0を導入する時にすでにVuex(ReactのReduxのVue.js版)やVue-routerがすでに出ましたが、多くのVue.jsのライブラリはまだ使いづらいものでした。最近はある程度改善されたようですが、いざという時にはやはり自力でカスタマイズするしかありません。

解決案は主に二つ:

  1. jQueryなどのライブラリのVue.js版を作る。
  2. 自分でVue.jsのプラグインを作る。

1についてはVue.jsの公式サイトにデモがあります。多くのUIコンポーネントはこちらの方法で対応しました。Vue.jsには様々なUIのライブラリが存在していますが、MAJINに合うものはなかなか見つからず、従来のUIライブラリをこの方法で流用し、Vue.jsに対応しました。

2は難しそうに見えますが、実は簡単でした。Vue.jsのプラグインを作ることで、既存のシステムから移行する時にそのギャップを埋められる重要な手法です。ここでは一番簡単なMAJINの「i18nプラグイン」を使って説明いたします(説明の便宜上、言語を日本語に固定します)。

plugins/i18n.js

import campaign from 'i18n/campaign.ja.yml'
import _ from 'lodash'

const i18nMaster = {
    campaign,
}

let i18nCache = {}  // In-memory Cache

export const translate = (expression) => {
    if (i18nCache[expression] !== undefined) {
        return i18nCache[expression]
    }

    const paths = expression.split('.')
    let f = (_paths, master) => {
        try {
            let p = _paths.shift()
            let v = master[p]
            return typeof master === 'string' ? master : f(_paths, v)
        } catch (e) {
            if (__DEV__) console.warn(`[i18n] cannot find expression: ${expression}`)
            return null
        }
    }
    let v = f(paths, i18nMaster)
    if (v !== null) {
        i18nCache[expression] = v
    }
    return v
}

function I18n (Vue) {
    // Step 1: プラグインがインストール済みかのチェック
    if (I18n.installed) return
    I18n.installed = true

    // Step 2: Mixinでコードを入れる
    Vue.mixin({
        beforeCreate() {
            Object.defineProperty(this, "translate", {
                value: translate,
            })

            Vue.filter('translate', translate)
        },
    })
}

export default I18n

このプラグインの目的は、Symfony形式のyamli18nの定義ファイルをそのままVue.jsで使い回すことです。 冒頭のi18n/campaigin.js.ymlは今回使うi18nファイルの名前であり、その中の i18nwebpack.config.jsで設定したパスです。

webpack.config.js(抜粋)

resolve: {
  "alias": {
    "i18n": __dirname + "/path/to/translations",
  }
}

npm install yaml-loaderしてwebpack.config.jsに下記のコードを追加すると、i18nの定義ファイルはJavaScript Objectとして読み込まれます。

module: {
  loaders: [
    ...
    {
      test: /\.(yaml|yml)$/,
      loader: "json!yaml"
    },
    ...
  ],
}

そしてエンドポイントファイルに

Vue.use(I18n)

を追加すれば、ViewModelで使えます。

var myApp = new Vue({
  el: '#ma-app',
  data(): {
    return {
      message: this.translate('campaign.path.to.translate')
    }
  },
})

またはViewでfilterとして使えます。

<span>{{ 'campaign.path.to.translate' | translate }}</span>

MAJINでは、i18nのほか認証やValidatorなどのプラグインも自作しました。これらのプラグインを利用しフロントエンドのシステム移行を加速できました。

ビルド時間が長い

Vue.jsで書かれたコードの増加に伴い、ビルド時間が急増しました。また、Webpackのビルド時のCPUとメモリの消費率もかなり高まり、ひどい時はAWS上のJenkinsがOutOfMemoryで死ぬこともありました。

この問題の解決案は二つあります。

  1. webpack.optimize.CommonsChunkPluginを利用して重複ビルドを省く。(マルチエンドポイントのMAJINでは大きな効果がありました)
  2. HappyPack(https://github.com/amireh/happypack)でビルド処理を非同期化する。

この二つの手段の組み合わせで、本来10分以上かかったビルドが、今は2分弱で完了します。

終わりに

MAJINのフロントエンドで使った技術を選んだ理由とそれぞれの問題点を簡単に紹介しました。より細かい話は、これからのブログで随時掲載いたします。 どうぞご期待ください。