読者です 読者をやめる 読者になる 読者になる

JavaScript大規模開発を語りに行ってきた「KOF2015」参加レポート

イベント JavaScript

どうも、大阪開発部のケノドンです。

11月6日~7日に大阪で開催された「関西オープンフォーラム(KOF)2015」にブース出展しました。今回も、ソフトウェア、ハードウエア、ロボット、OS、データベース等、様々なものが展示されていました。

f:id:cybozuinsideout:20151112181231j:plain:w250f:id:cybozuinsideout:20151112181237j:plain:w250

私達のブースは、大きく3つのテーマに分かれていました。

  1. 大阪オフィス移転
  2. 大規模フロントエンド開発について
  3. 悟空ボウズマンを助ける体験

大阪オフィス移転

11月2日から梅田阪急ビル オフィスタワーに移転しました。
近くに居らっしゃったら是非お越しくださいませ!

cybozu.co.jp

大規模フロントエンド開発について

大規模Webアプリケーション「kintone」のフロントエンド開発で利用しているツールや言語について語りました。

JavaScriptによる開発

JavaScriptの開発に当って、次のような課題が考えられます。

  • スクリプト言語のため、不具合やコードの異常が、コードを実行するまでわからない
  • 動的型付けなので、エンジニアがコードから型を読み取らなければならない
  • 書き方の自由度が高くて、既存のコードの調査や修正、影響範囲の調査などで、コードを読む時のコストが上がる
  • DRY が守られない
  • ブラウザが読み込んで実行する事で読み込むコード量が増えるとダウンロードに時間がかかってしまう

現在の kintone の JavaScript は30万行を超えており、保守にかかるコストが開発リソースを圧迫してもおかしくない程の規模になっています。

Google Closure Compiler

コードの確認

JSDoc で引数や関数の返り値の型の定義をする事によって、Closure Compiler はコードに不整合があることを検知してくれます。

/**
 * @param {boolean} isBoolean isBooleanの引数が boolean である事を Compiler に教えておく
 */
function logBoolean(isBoolean) {
  if (isBoolean) {
    console.log('It is true.');
  } else {
    console.log('It is false.');
  }
};

logBoolean(false);    // It is false.
logBoolean('false');  // 'false' は boolean ではないため、COMPILE ERROR!

同じように、変数に与えられる値、関数の使い方などを確認します。そのためはちゃんとJSDocを書かなければなりません。他にシンタックスエラー、関数に渡す引数の数、定義されていないプロパティなど、いろいろみてくれます。
このような Closure Compiler による事前解析によって、単純な不具合を防ぐ事が想像できると思います。

コードの圧縮

先ほどの例をもう一度使います。

/**
 * @param {boolean} isBoolean
 */
function logBoolean(isBoolean) {
  if (isBoolean) {
    console.log('It is true.');
  } else {
    console.log('It is false.');
  }
};

logBoolean(false);
logBoolean(true);

このコードは250byteぐらいあります。Closure Compiler が最適化をしてくれたら下記のようになります。

function a(b){b?console.log('It is true.'):console.log('It is false.')}a(!1);a(!0);

なんと、100byte以下になりました。
圧縮する事で、ブラウザ上の読み込みが速くなって、ユーザにより良いサービスを提供ができます。

Google Closure Library

Closure Tools の一つであり、包括的な JavaScript ライブラリです。

  • ライブラリでできることはライブラリに任せる
  • DOM管理が快適
  • Closure Compiler と相性が良い

ライブラリを使いこなすことで効率的かつ統一的なコードがかけます。

例えば、少し修正した体験アプリのクラス kintone.component.Button をご覧ください。

/**
 * ボタンのコンストラクタ
 *
 * @param {goog.dom.DomHelper=} opt_domHelper
 * @constructor
 * @extends {goog.ui.Component} Closure Library のクラスを継承している
 */
kintone.component.Button = function(anyBoolean, opt_domHelper) {
  // 親クラスのコンストラクタを呼び出す。
  kintone.component.Button.base(this, 'constructor', opt_domHelper);
};
goog.inherits(kintone.component.Button, goog.ui.Component);

/**
 * 当クラスの DOM が生成するに呼ばれる関数
 * 
 * 親クラスの goog.ui.Component に定義されているので override アノテーションを設定
 * @override
 */
kintone.component.Button.prototype.createDom = function() {
  // 次の Closure Template に定義されている kintone.template.soy.buttonDiv のテンプレートから DOM を生成する
  var el = goog.soy.renderAsElement(kintone.template.soy.buttonDiv, null, null, this.getDomHelper());
  // 当インスタンスのエレメントを設定する。生成した DOM と JavaScript オブジェクトの連携ができた。
  this.setElementInternal(el);
};

/**
 * document に DOM が入れられた時に呼ばれる関数
 * 
 * ボタンにクリックされる事をListenする
 *
 * @override
 */
kintone.component.Button.prototype.enterDocument = function() {
  kintone.component.Button.base(this, 'enterDocument');
  // インスタンスの DOM の第一子エレメントを取得する。
  var callButton = goog.dom.getFirstElementChild(this.getContentElement());
  // クリックされた時に this.callBozuman を呼び出すように設定する。
  this.getHandler().listen(callButton, goog.events.EventType.CLICK, this.callBozman);
};

/**
 * 悟空ボウズマンの画像インスタンスを作成して表示する
 */
kintone.component.Button.prototype.callBozuman = function() {
  // 悟空ボウズマンインスタンスを作成する
  var goku = new kintone.component.Goku();
  // 悟空ボウズマンを document に入れる
  goku.render();

  // 当ボタン自体はdocumentから破壊する
  this.disposeInternal();
};

上記の様に、簡単に DOM の生成、管理ができます。

Google Closure Template

動的なUI生成システムです。

  • HTMLやUIパーツを再利用可能な塊として扱える
  • Java と JavaScript から利用できる
  • パーツ自体はHTMLを拡張したような形で、書きやすく読みやすい

再利用によって、同じような異なるコードを防ぐことができます。

上記に呼び出された kintone.template.soy.buttonDiv テンプレートの中身をみてみましょう。

{namespace kintone.template.soy}

/**
 * 悟空ボウズマンを呼び出すボタン
 */
{template .buttonDiv}
  <div class="content-wrapper">
    <a href="#" class="button">ボウズマンを助ける</a>
  </div>
{/template}

HTMLをパーツとして命名でき、パラメータも設定可能で、共通化しやすいのでとても便利です。

Google Closure Linter

JavaScript のファイルをチェックしてくれます。

  • セミコロンはある?
  • 規約と異なる書き方されてない?
  • JsDoc のアノテーションが正しく書けてる?

人による細かいミスを事前に防ぐことができます。

まとめ

  • 静的型付 + 圧縮 + 最適化したいから Closure Compiler
  • Closure Compiler と一緒に利用するため Closure Library
  • Closure Library と相性の良い Closure Template で DOM生成
  • コード規約を仕組みで縛るために Closure Linter

CSSの管理について

CSSでよく現れる問題

  • マジックナンバー:意味のわからないpx指定などが増える
  • いろんなブラウザで同じようなスタイルにするために、大量の記述が必要なことがある
  • 再利用ができなく、冗長したスタイルが増える

という事で、大規模にしてメンテナンスコストが爆発する!対策として、2つのツールを紹介します。

Sass

  • 変数や計算が利用できる ⇒ 値に意味をもたせられる
  • ネスト構造で直感的なスタイル指定 ⇒ 整理しやすい
  • コンポネント化が可能 ⇒ 再利用ができる

autoprefixer

  • 一つのスタイルからすべてのブラウザに必要なスタイルを生成してくれる

実装が楽になり、メンテナンスコストが減るのも、人が書くことによるミスを防げる事がメリットとなります。

サンプル

$great-color: #0a3c59;            // 変数を定義
%common-border {                  // 再利用のためのスタイル定義
  border: 1px solid $great-color; // 変数を利用
}
.common-button {
  @extend %common-border;         // %common-border を呼び出す
}
.big-button {
  @extend %common-border;         // %common-border を呼び出す
  border-radius: 6px;
  &:hover {                       // ネスティングを利用
    color: $great-color;          // 変数を利用
  }
} 

悟空ボウズマンを助ける体験

Google Closure Compiler が具体的にどういう風に役立つかを感じてもらうために、体験アプリを作りました。
githubに公開しているので是非ご覧ください。

f:id:cybozuinsideout:20151110163412j:plain:w400

内容

コードに問題を含んだアプリを用意しました。Closure Compiler を使って、エラーの検知をし、対応していくような体験になります。

全部で5個、エラーを一個ずつ直してみましょう。

1. パースエラー

app/javascript/goku.js:133: ERROR - Parse error. ',' expected
  var el = goog.soy.renderAsElement(kintone.template.soy.gokuMessage, null, null, this.getDomHelper();
                                                                                                     ^

原因

goog.soy.renderAsElement の締め括弧が足りないというエラーになります。

解決

)を足す。

2. 定義されていないプロパティ

  this.getHandler().listen(callButton, goog.events.EventType.CLICK, this.callBozman);
                                                                         ^

原因

すぐ下に定義されているプロパティがあります。kintone.component.Button.prototype.callBozuman は確かにありますが、良くみたら綴りに差が出ています。u が不足しています。

解決

this.callBozmanu を足す。

3. 引数の数エラー

app/javascript/goku.js:112: ERROR - Function goog.ui.Component.prototype.getContentElement: called with 1 argument(s). Function requires at least 0 argument(s) and no more than 0 argument(s).
        gokuMessage.render(this.getContentElement(true));
                           ^

原因

エラーの内容は this.getContentElement に引数を一つ渡しているけれど、この関数は引数が必要ないということが書いてあります。

解決

引数として渡している true を消す。

4. 引数型エラー

app/javascript/setup.js:15: ERROR - actual parameter 1 of kintone.component.Button does not match formal parameter
found   : string
required: boolean
  var button = new kintone.component.Button(kintone.setup.anyBoolean());
                                            ^

原因

エラーの内容を見ると、 kintone.component.Buttonboolean の引数を求めていますが、string が渡されています。Compiler がなぜそう判断したかというと
* kintone.component.Button の Doc に @param {boolean} anyBoolean 意味のないboolean がある事で、Compiler は boolean を渡す必要性がわかる * kintone.setup.anyBoolean の中身は return true; と書いてあるにも関わらず、 Doc が @return {string} となっているので、Compiler は kintone.setup.anyBooleanstring を返すと判断する よって、kintone.setup.anyBoolean の名前とその内容から、Doc に問題があるとわかります。

解決

kintone.setup.anyBoolean の Doc に返り値の型を boolean に書き換える。

5. 返り値型エラー

app/javascript/setup.js:27: ERROR - inconsistent return type
found   : boolean
required: string
        return true;
               ^

原因

kintone.setup.anyBoolean は Doc に @return {string} が書いてあるのに return true; をしている事で、問題があると Compiler が判断しました。 実は一つ上のエラーを修正で、このエラーも直されました。

解決

kintone.setup.anyBoolean の Doc に返り値の型を boolean に書き換える。

まとめ

スクリプト言語である JavaScript でも Google Closure Tools を利用することで、事前にいろいろのような問題の検知ができる事がわかりました。kintone のように、規模が大きくなるととても大切な仕組みとなってきます。

最後に

二日間だけでしたがいろんな人と出会えてとてもよかったと思います。また、ブースに来てくださった皆様、アドバイスや応援のメッセージをくださった皆様にもありがとうございます!
来年のKOFに向かって皆様に楽しんで頂けるネタを考えて行きたいと思います!