gruntで簡単JSビルド

gruntってのは、JS/CSSをまとめたり、JS lint, Qunit, minifyできたりってのができるビルドツールです。
また、minifyなどの組み込みタスクの他に、RakeやAntと同じようにカスタムタスクを記述して実行することも可能です。
さらに、watchというので、対象のファイルを監視して、それをトリガーとしてタスクを実行することも可能です。(inofityみたいなやつ)
他の類似ツールと比べて、簡単に使えたのでメモとして残しときます。

環境

OS: MacOSX 10.7
実行環境: Node.js(Homebrew)
パッケージ管理: npm

手順

node環境のインストール

面倒なので(ry

この辺みてインストールしてください。
naveを使ったnode.jsインストールと、最近のnpmの使い方 - ラシウラ
Nodeとnpmのインストール - 自分の感受性くらい

MacOSX/homebrewでnodeを、パッケージ管理にnpmを使っているという想定でいきます。

gruntのインストール

$ npm install -g grunt

プロジェクトディレクトリセットアップ

$ cd project_dir
$ npm init  #<= package.jsonを作る(※あれば不要)

`npm init`で対話式にpackage.jsonを作成します。(自前で作成してもいいけど)
gruntは、package.jsonの内容を読み込むことができるので、Client側であっても作成しておくと便利です。

package.jsonのサンプル

{
author: "Satoshi Ohki <roothybrid7@gmail.com>",
name: "grunt-sandbox",
description: "Grunt sandbox project.",
version: "0.0.0",
repository: {
type: "git",
url: "git://github.com/roothybrid7/grunt-sandbox.git"
},
engines: {
node: "~0.6.14"
},
noAnalyze: true,
dependencies: { },
devDependencies: {
grunt: "~0.3.2",
grunt-sample: "~0.1.0"
},
optionalDependencies: { }
}

gruntセットアップ

本日の主役、下記のコマンドを実行し、何個か質問に答えるとカレントディレクトリにgrunt.jsが作成されます。

$ grunt init:gruntfile
gruntの設定

設定は、この中に記述します。

grunt.initConfig({
[...]
});
ファイルをまとめる、minifyする

まとめたいファイルを、srcに指定して、destにまとめたファイル名を指定します。

'<json:package.json>'

は、jsonファイルをしている組み込みのディレクティブです。
destに、<%= pkg.name %>とありますが、package.jsonのnameを使うように指定しています。
metaのbannerで、package.jsonの値をいろいろ使用しています。ファイルのヘッダコメントとして使用しています。

'<config:concat.dist.dest>'

の箇所は、ディレクティブを使って、initConfig内のconcat.dist.destの値を取得しています。
このディレクティブを使えば、minifyでconcatに指定したのと同じファイルを簡単に指定できるようになります。

設定サンプル

grunt.initConfig({
    pkg: '<json:package.json>',
    meta: {
      banner: '/*! <%= pkg.title || pkg.name %> - v<%= pkg.version %> - ' +
        '<%= grunt.template.today("yyyy-mm-dd") %>\n' +
        '<%= pkg.homepage ? "* " + pkg.homepage + "\n" : "" %>' +
        '* Copyright (c) <%= grunt.template.today("yyyy") %> <%= pkg.author.name %>;' +
        ' Licensed <%= _.pluck(pkg.licenses, "type").join(", ") %> */'
    },
    concat: {
      dist: {
        src: [
          '<banner>',
          'js/snip/**/*.js'
        ],
        dest: 'dist/<%= pkg.name %>.js'
      }
    },
    min: {
      dist: {
        src: ['<banner>', '<config:concat.dist.dest>'],
        dest: 'dist/<%= pkg.name %>.min.js'
      }
    }
});

concatやminは、別にdistという名前のプロパティでなくても任意のプロパティ名を使うことが可能です。しかも複数のプロパティを宣言できます。
例えば、ページ毎にまとめるファイルが異なる場合は、下記のような設定が可能です。

設定サンプル

grunt.initConfig({
[...]
    concat: {
      about: {
        src: [
          '<banner>',
          '<file_strip_banner:js/snip/0-testscript.js>'
        ],
        dest: 'dist/<%= pkg.name %>-about.js'
      },
      config: {
        src: [
          '<banner>',
          'js/snip/1-testscript.js',
          'js/snip/2-testscript.js'
        ],
        dest: 'dist/<%= pkg.name %>-config.js'
      },
      dashboard: {
        src: [
          '<banner>',
          'js/snip/3-testscript.js',
          'js/snip/4-testscript.js',
          'js/snip/5-testscript.js',
          'js/snip/6-testscript.js',
          'js/snip/7-testscript.js',
          'js/snip/8-testscript.js',
          'js/snip/9-testscript.js'
        ],
        dest: 'dist/<%= pkg.name %>-dashboard.js'
      },
    },
    min: {
      about: {
        src: ['<banner>', '<config:concat.about.dest>'],
        dest: 'dist/<%= pkg.name %>-about.min.js'
      },
      config: {
        src: ['<banner>', '<config:concat.config.dest>'],
        dest: 'dist/<%= pkg.name %>-config.min.js'
      },
      dashboard: {
        src: ['<banner>', '<config:concat.dashboard.dest>'],
        dest: 'dist/<%= pkg.name %>-dashboard.min.js'
      }
    },
[...]
});
JS Lintの設定

JS lintを実行してくれるんですが、はっきりいってつかってません!!
なぜかといえば、jQueryの'$'とかunderscoreの'_'とかをErrorとして報告してきやがるからです。

下記のように設定すると、grunt.js自身と、js/snip/ディレクトリ以下のjavascriptファイルをすべてにJS lintを実行してくれます。

設定サンプル

grunt.initConfig({
    lint: {
      files: [
        'grunt.js',
        'js/snip/**/*.js'
      ]
    },
});
testの設定

qunit使っている人は使えばいいと思う。(これもつかってない)
下記のように設定すればいいらしいです。

設定サンプル

grunt.initConfig({
    qunit: {
      files: ['test/**/*.html']
    },
});

gruntタスク実行

defaultタスクの実行

grunt.jsのあるディレクトリで、`grunt`とだけ実行すると、defaultタスクが実行されます。(他のビルドツールと一緒)

$ grunt

defaultタスクは下記のように設定します。
実行したいタスクを、第2引数にスペース区切りで指定します。

  // Default task.
  grunt.registerTask('default', 'concat min');
その他の組み込みタスクの実行

concat, min, lintなどの組み込みタスクを実行する場合、`grunt [task]`とtaskに実行したいタスクを指定します。

サンプル

$ grunt concat

タスクの一部だけ実行する場合には、grunt.jsで指定したプロパティ名を引数に指定します。

サンプル

$ grunt concat:about #<= concatタスクのaboutのみ実行される

デモ

100個のスクリプトをまとめて、さらにminifyするサンプルを書いてみました。

grunt.js設定ファイル

/*global module:false*/
module.exports = function(grunt) {
    var utils = grunt.utils,
        task = grunt.task,
        helper = grunt.helper,
        file = grunt.file,
        config = grunt.config,
        log = grunt.log,
        os = require('os').platform();

    /**
     * Compare function.
     *
     * @param {*} a A comparable value1.
     * @param {*} b A comparable value2.
     * @return {number} The compare result.
     */
    var compareFilename = function(a, b) {
            var numA = parseInt(a, 10),
                numB = parseInt(b, 10);
            return numA - numB;
        };

    /**
     * Returns the rest of the elements in an array.
     *
     * @param {Array} arr An source array.
     * @param {number=} index The values of the array that index onward.
     * @return {Array} The rest array.
     */
    grunt.registerHelper('rest', function(arr, index) {
        if (utils.kindOf(index) !== 'number') {
            index = 1;
        }
        return arr.slice(index);
    });

    /**
     * Returns basename.
     *
     * @param {string} filepath A file path string.
     * @param {string=} suffix A remove suffix.
     * @return {string} basename string.
     */
    grunt.registerHelper('basename', function(filepath, suffix) {
        var base = '',
            separator = os.match(/^win/) ? '\\' : '/';

        if (filepath === '.' || filepath === separator) {
            base = filepath;
        } else {
            var entries = filepath && filepath.split(separator);
            while (entries.length) {
                if ((base = entries.pop())) break;
            }
        }

        if (utils.kindOf(suffix) === 'string' && suffix.length > 0) {
            base = base.replace(suffix, '');
        }

        return base;
    });

    /**
     * Returns sorted filelist.
     *
     * @param {string} patterns A file path wildcard.
     * @return {string} sorted filelist.
     */
    grunt.registerHelper('sort_files', function(patterns) {
        var files = file.expandFiles(patterns);

        return sortedFiles = utils._.sortBy(files, function(file) {
            var basename = helper('basename', file);
            return parseInt(basename, 10);
        });
    });

    // Project configuration.
    grunt.initConfig({
        pkg: '<json:package.json>',
        meta: {
            banner: '/*! <%= pkg.title || pkg.name %> - v<%= pkg.version %> - ' + '<%= grunt.template.today("yyyy-mm-dd") %>\n' + '<%= pkg.homepage ? "* " + pkg.homepage + "\n" : "" %>' + '* Copyright (c) <%= grunt.template.today("yyyy") %> <%= pkg.author.name %>;' + ' Licensed <%= _.pluck(pkg.licenses, "type").join(", ") %> */'
        },
        lint: {
            files: ['grunt.js', 'js/snip/**/*.js']
        },
        concat: {
            about: {
                src: ['<banner>', '<file_strip_banner:js/snip/0-testscript.js>'],
                dest: 'dist/<%= pkg.name %>-about.js'
            },
            config: {
                src: ['<banner>', 'js/snip/1-testscript.js', 'js/snip/2-testscript.js'],
                dest: 'dist/<%= pkg.name %>-config.js'
            },
            dashboard: {
                src: ['<banner>', 'js/snip/3-testscript.js', 'js/snip/4-testscript.js', 'js/snip/5-testscript.js', 'js/snip/6-testscript.js', 'js/snip/7-testscript.js', 'js/snip/8-testscript.js', 'js/snip/9-testscript.js'],
                dest: 'dist/<%= pkg.name %>-dashboard.js'
            },
            dist: {
                src: ['<banner>', helper('sort_files', 'js/snip/**/*.js')],
                dest: 'dist/<%= pkg.name %>.js'
            },
            distStrip: {
                src: ['<banner>', utils._.map(helper('sort_files', 'js/snip/**/*.js'), function(file) {
                    return '<file_strip_banner:' + file + '>';
                })],
                dest: 'dist/<%= pkg.name %>-strip.js'
            }
        },
        min: {
            about: {
                src: ['<banner>', '<config:concat.about.dest>'],
                dest: 'dist/<%= pkg.name %>-about.min.js'
            },
            config: {
                src: ['<banner>', '<config:concat.config.dest>'],
                dest: 'dist/<%= pkg.name %>-config.min.js'
            },
            dashboard: {
                src: ['<banner>', '<config:concat.dashboard.dest>'],
                dest: 'dist/<%= pkg.name %>-dashboard.min.js'
            },
            dist: {
                src: ['<banner>', '<config:concat.dist.dest>'],
                dest: 'dist/<%= pkg.name %>.min.js'
            }
        },
        watch: {
            files: '<config:lint.files>',
            tasks: 'lint'
        },
        jshint: {
            options: {
                curly: true,
                eqeqeq: true,
                immed: true,
                latedef: true,
                newcap: true,
                noarg: true,
                sub: true,
                undef: true,
                boss: true,
                eqnull: true,
                browser: true
            },
            globals: {}
        },
        uglify: {}
    });

    // Default task.
    grunt.registerTask('default', 'concat min');

    grunt.registerTask('sort_files_with_printout_test', 'print out sort files.', function() {
        log.writeln(this.name + ', ' + this.nameArgs);
        var sortedFiles = helper('sort_files', 'js/snip/**/*.js');
        log.writeln(helper('concat', sortedFiles));
    });
}; 

Cocoa/iOSライブラリのPodspecを修正して、CocoaPodsで依存関係の解決

みなさんCocoaPodsを活用してますか? CocoaPodsを使用するとライブラリの検索から依存関係の管理・インストールまで簡単に行うことができます。

CocoaPodsでライブラリを管理するためには、基本的にはライブラリの作者がpodspecというファイルを記述してCocoaPodsで使えるように必要があります。

今回使ってみようと思ったのは、QuickDialogというiOS HIG(ヒューマンインタフェースガイドライン)に準拠した設定画面のフォームが簡単に作れるものです。

このライブラリはCocoaPodsに対応してるんですが、依存するフレームワークの記述がないためインストール後にプロジェクトをビルドしようとすると以下のようにエラーになります。

f:id:zepbag:20120113020403j:image
f:id:zepbag:20120113020402j:image


ここで、手動でフレームワーク(この場合MapKit.framework)を追加してもいいんですが、CocoaPodsは自分専用に作成したリポジトリからpodspecを参照してライブラリをインストールすることが可能です。

なので新たに自分専用リポジトリを作成し、修正したpodspecを配置してビルドできるようにしてみます。

今回修正するpodspecの対象ライブラリ

  • QuickDialog

Podspecファイルの作成とバージョン番号の変更

必要なもの

# ライブラリをcloneして、CocoaPodsでインストールしたバージョンのタグにHEADを移動
git clone git://github.com/escoz/QuickDialog.git
cd QuickDialog
git reset --hard 0.1

# CocoaPds/Specからpodspecファイルを取得
wget https://raw.github.com/CocoaPods/Specs/master/QuickDialog/0.1/QuickDialog.podspec

# podspecに修正を加える
edit QuickDialog.podspec

# 以下を追加/
--- ../QuickDialog/QuickDialog.podspec.orig	2012-01-13 00:36:09.000000000 +0900
+++ QuickDialog/0.1/QuickDialog.podspec	2012-01-13 01:30:24.000000000 +0900
@@ -14,6 +14,7 @@
 
   s.source_files = 'quickdialog'
   s.clean_paths  = 'sample', '*.xc*', 'libQuickDialog', 'other'
+  s.framework = 'MapKit'
   s.requires_arc = true
 
   def s.post_install(target)

# 誤りがないかチェック
pod spec lint QuickDialog.podspec

自分用のCocoaPods用のpodspecを配置する

修正したpodspecファイルを自分のリポジトリにコミットして管理しますので
あらかじめ専用のリポジトリを作っておきます。
私の場合は、podspecsという名前で作成しました。

git clone git@github.com:roothybrid7/podspecs.git
cd podspecs
# ライブラリ名(podspecのs.name)/バージョン番号(s.version)のディレクトリ作成
mkdir -p QuickDialog/0.1
# 先ほど作成したpodspecファイルをコピー
cp <QuickDialog.podspec> QuickDialog/0.1/
# commit and push.
git add . && git ci -m 'Add QuickDialog podspec.' && git push origin master

CocoaPodsにpodspecのリポジトリを追加

CocoaPodsに作成したリポジトリを登録します。

# podspecのrepositoryを追加
pod repo add mypods git://github.com/roothybrid7/podspecs.git

Podfileの修正とアップデート

先ほど修正したpodspecファイルを参照するように、Podfileを修正します。

# podspecを配置したURLを記述する(raw)
-dependency 'QuickDialog', '~> 0.1'
+dependency 'QuickDialog', '~> 0.1', :podspec => 'https://raw.github.com/roothybrid7/podspecs/master/QuickDialog/0.1/QuickDialog.podspec'

修正したら `pod install` を実行して更新します。

これで再ビルドすると、MapKit.frameworkを手動で追加することなしに、正常にビルドすることができるようになりました。

f:id:zepbag:20120113020404j:image

終わりに

このようにライブラリのpodspecの記述がたりない場合、自分で自由に修正することが可能なので、自分専用リポジトリを作成して試してみてはいかがでしょうか

今回作成した自分専用podspecリポジトリ

build済みのGHUnitは実機上でテストできないので、自分でbuildしてください!!

GithubのDownloads · gabriel/gh-unit · GitHubにあるGHUnitIOS-0.4.33.zipを使って、実機上でテストを動かそうとしても下記のようにエラーになるので、

iOS: Linker could not find GHImageDiffView and YKUIImageViewControl -
GHUnit |
Google Groups


ここに書いてある通り、自分でbuildしてプロジェクトに放り込む
Issue #69: Framework missing symbols for ARMV7 · gabriel/gh-unit · GitHub

  1. git clone https://github.com/gabriel/gh-unit.git
  2. cd Project-iOS
  3. make
  4. Use the GHUnitIOS.framework under the build/Framework folder

流行のCocoaPods使ってみて、エラーになってはまった話(修正)

CocoaPodsを導入すると、iOS開発で使用するライブラリの導入管理が簡単になるとしって使ってみたんですが、いくつかはまったところがありました。

環境 

環境はこんな感じ

  • XCode 4.2.1
  • Mac OS X 10.7.2
  • MacRuby 0.10(RVM)
  • macgem 1.4.2
  • cocoapod 0.3.9

最初にはまったところ 

  1. 複数ターゲットでCocoaPodsで導入したライブラリを使用する場合、pod installをする前に必要なプロジェクトターゲットを作成する必要がある
  2. Header Search Pathが違う!! 一部のライブラリのヘッダファイルがHeadersディレクトリにシンボリックリンクされない!!
  3. テスト用static-libraryを作っても、デフォルト以外は自動でリンクはしてくれない
 
1. 複数ターゲットでCocoaPodsで導入したライブラリを使用する場合、pod installをする前に必要なプロジェクトターゲットを作成する必要がある
 

下のように、ターゲットが2つあるとして(アプリ用とテスト用)

後からターゲットを作成してしまうと、CocoaPodsでstatic-libraryがリンクされません。なので複数のターゲットを自動管理する場合は、pod installする前に必要なターゲットは作成しておくといいです。

f:id:zepbag:20120108164804j:plain

 2. Header Search Pathが違う!! 一部のライブラリのヘッダファイルがHeadersディレクトリにシンボリックリンクされない!!

*よく見たらHeadersディレクトリはありました!! なぜか全部のライブラリがコピーされない

:exclusive => trueでほかのtargetで指定したライブラリのシンボリックリンクが作られないのは、バグっぽいので修正してpull request送りました。(2012/01/09)

#104: fixed bug of directory cleaning. by roothybrid7 for CocoaPods/CocoaPods - Pull Request - GitHub

 > 上の暫定対策版をインストールできるようにしましたので、よろしければ、 CocoaPods 0.3.9 修正非公式版インストール — Gist からインストールしてみてください

 

3.のPodfileの記述に関係あるんですが、どうも targetブロックで:exclusive => trueを使ってしまうとtargetブロックの外で宣言しているライブラリのヘッダファイルがHeadersディレクトリにコピーされないみたいです。

 - targetブロックで:exclusive => trueを使った場合は、targetブロックの外のライブラリのヘッダファイルがコピーされない

platform :ios

dependency 'JSONKit', '~> 1.4'
dependency 'AFNetworking', '~> 0.7'
dependency 'BlocksKit', '~> 1.0'
dependency 'NSLogger'

# Testing framework for Test target.
target :test, :exclusive => true do
  dependency 'LRMocky'
end

f:id:zepbag:20120108190300j:plain

- targetブロックで:exclude => trueを使わなければ全部コピーされる

platform :ios

dependency 'JSONKit', '~> 1.4'
dependency 'AFNetworking', '~> 0.7'
dependency 'BlocksKit', '~> 1.0'
dependency 'NSLogger'

# Testing framework for Test target.
target :test do
  dependency 'LRMocky'
end

f:id:zepbag:20120108193304j:plain

 

ということで、今のところtest用に追加のライブラリだけをパッケージングするのは難しそうです。

3. テスト用static-libraryを作っても、デフォルト以外は自動でリンクはしてくれない

下記のように、Podfileを記述したとしてtargetブロックに囲まれた方のstatic-libraryは手動でリンクする必要があります。

最初、targetに指定する引数が、xcodeのプロジェクトターゲットと同じものだと思っていたのですが、全く関係ないらしいです。

よくある(:production, :develpment, :test)とおなじです。

CocoaPodsでは、下記の3つのシンボルが用意されています。(任意の文字列を使ってもいい) 

  • :default(targetブロックを使ってないところ)
  • :debug
  • :test
ちなみに、:exclusive => trueは targetブロックの外で宣言された dependencyのライブラリを含めないという意味です。
platform :ios

dependency 'JSONKit', '~> 1.4'
dependency 'AFNetworking', '~> 0.7'
dependency 'BlocksKit', '~> 1.0'
dependency 'NSLogger'

# Testing framework for Test target.
target :test, :exclusive => true do
  dependency 'OCMock'
  dependency 'LRMocky'
end

 

この構成だと下記の2つのstatic-libraryのターゲットを持つプロジェクトが生成されます。

  • libPods.a (デフォルト)
  • libPods-test.a (target :testで作ったやつ、ファイル名に指定した引数がsuffixとして付与される)
結局
 
targetブロックで:exclusive => trueをつけてしまうと、ブロック外で宣言しているヘッダファイルのシンボリックリンクが作られなくなるので、2.の:exclude => trueをつけない方で使用しようと思います。libPods.aとlibPods-test.aでライブラリが重複してしまいますが、アプリ用ターゲットには、デフォルトのlibPods.aを、テスト用ターゲットには、libPods-test.aをリンクして使用しようと思います。テスト用ターゲットにはライブラリが依存しているフレームワークの追加が必要になりますが。
 
 
まだ、手順やらドキュメント等が固まってない印象はありますが、いちいちダウンロードしてくるのにくらべればとても有益なツールだと思います。
 
 
参考
 

 

[Xcode][UI][iOS] XcodeでStoryboardのUser Defined Runtime Attributes使い方わかった

StoryBoardには、実行時、プロパティに値を設定できる機能があります。

例えばアニメーションやサイズの動的な変更でこれを使用すれば
最終調整で、プログラムでわざわざ値を修正して微調整するとかしなくてもよくなります。

環境

手順

UITableView Cellでパフォーマンス向上のためにCellを再利用する機能がありますが、
そこで使用するIDをUser Defined Runtime Attributeで宣言してみます。

  1. ViewControllerにプロパティを宣言
  2. Storyboardの画面を開いてプロパティを宣言したViewControllerを選択
  3. Utility areaの「Show the identity inspector」タブを選択
  4. [+]をクリックして、必要なAttributeを追加



実行結果


使用場面

Storyboard経由でViewControllerを呼び出すとき、`initWithNibName:bundle:`とか呼ばれないので、Viewの細かいサイズ変更などコードで行いたいが、プロパティのデフォルト値として定数値を設定したい場合。
`viewDidLoad`で毎回プロパティに値をセットするのはちょっと違うなというとき

例えば、独自のカスタムフォントを適用したが、Storyboard上では適用できないが、フォント名をプロパティのデフォルト値としてあらかじめ設定しときたいとか需要があると思います。

Sample Code

    // MasterViewController.h
    #import <UIKit/UIKit.h>

    @interface MasterViewController : UITableViewController

    @property (strong, nonatomic) NSString *cellIdentifier;
    @property (strong, nonatomic) NSArray *cellTitleList;

    @end

    // MasterViewController.m
    #import "MasterViewController.h"

    @implementation MasterViewController


    @synthesize cellIdentifier = _cellIdentifier;
    @synthesize cellTitleList = _cellTitleList;

    [...]
#pragma mark - View lifecycle

    - (void)viewDidLoad
    {
        [super viewDidLoad];
	// Do any additional setup after loading the view, typically from a nib.
        self.cellTitleList = [NSArray arrayWithObjects:@"title1", @"title2", @"title3", nil];
    }

    [...]
    #pragma mark - UITableViewDataSource

    - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
    {
        return 1;
    }

    - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
    {
        return [self.cellTitleList count];
    }

    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
    {
        UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:self.cellIdentifier];
        if (!cell) {
            cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:self.cellIdentifier];
        }
        cell.textLabel.text = [self.cellTitleList objectAtIndex:indexPath.row];
        return cell;
    }

Closure Compilerを使用したときのオブジェクトメンバに関する注意点

Closure Compilerでスクリプトを最適化する場合、変数名等は短縮されてしまいます。
そのため、例えばオブジェクトのメンバー参照時に問題がおこることがあります。
例えば、Object(連想配列)の初期化時と参照方式に、文字列添字のみ等を使った場合は問題がおこらないのに、
初期化時のメンバー名と参照方式を混在した場合は、正しくメンバーを参照できない問題が発生します。

確認

ということで、Closure compilerの出力結果を確認してみました。


元のスクリプト

var obj = {
  name1: 'foo',   // メンバー名(key)が最適化時に短縮される
  'name2': 'bar'  // こっちはそのまま
};

var result = 'obj.name1: ' + obj.name1;
result += '<br />';
result = 'obj["name2"]: ' + obj['name2'];
result += '<br />';
//ADVANCED_OPTIMIZATIONの時に正しくメンバー参照できない
result = 'obj.name2: ' + obj.name2;  
result += '<br />';
result += 'obj["name1"]: ' + obj['name1']; 

SIMPLE_OPTIMIZATINOS

var obj = {name1:"foo", name2:"bar"}, result = "obj.name1: " + obj.name1;
result += "<br />";
result = 'obj["name2"]: ' + obj.name2;
result += "<br />";
result = "obj.name2: " + obj.name2;
result += "<br />";
result += 'obj["name1"]: ' + obj.name1;

ADVANCED_OPTIMIZATIONS

var a = {a:"foo", name2:"bar"}, b = "obj.name1: " + a.a;
b += "<br />";
b = 'obj["name2"]: ' + a.name2;
b += "<br />";
b = "obj.name2: " + a.b;
b += "<br />";
b += 'obj["name1"]: ' + a.name1;

なんだと...

追試

ではこれは?

メンバー名が非文字列のみ

元のコード

// メンバー名(key)は短縮される
var obj = {
  name1: 'foo',
  name2: 'bar'
};

var result = 'obj.name1: ' + obj.name1;
result += '<br />';
result += 'obj.name2: ' + obj.name2;
result += '<br />';
// メンバー名が非文字列なのでADVANCED_OPTIMIZATIONSで短縮されてしまうので最適化後エラー
result += 'obj["name1"]: ' + obj['name1'];
result += '<br />';
result = 'obj["name2"]: ' + obj['name2'];

ADVANCED_OPTIMIZATIONS

var a = {a:"foo", b:"bar"}, b = "obj.name1: " + a.a;
b += "<br />";
b += "obj.name2: " + a.b;
b += "<br />";
b += 'obj["name1"]: ' + a.name1;
b += "<br />";
b = 'obj["name2"]: ' + a.name2;
メンバー名が文字列のみ

元のコード

// メンバー名(key)は短縮されない
var obj = {
  'name1': 'foo',
  'name2': 'bar'
};

var result = 'obj["name1"]: ' + obj['name1'];
result += '<br />';
result = 'obj["name2"]: ' + obj['name2'];
result += '<br />';
// メンバー名が文字列なので、ADVANCED_OPTIMIZATIONSでメンバー名(key)は短縮されないので最適化後参照エラー
result += 'obj.name1: ' + obj.name1;
result += '<br />';
result += 'obj.name2: ' + obj.name2;

SIMPLE_OPTIMIZATIONS

var obj = {name1:"foo", name2:"bar"}, result = 'obj["name1"]: ' + obj.name1;
result += "<br />";
result = 'obj["name2"]: ' + obj.name2;
result += "<br />";
result += "obj.name1: " + obj.name1;
result += "<br />";
result += "obj.name2: " + obj.name2;

ADVANCED_OPTIMIZATIONS

var a = {name1:"foo", name2:"bar"}, b = 'obj["name1"]: ' + a.name1;
b += "<br />";
b = 'obj["name2"]: ' + a.name2;
b += "<br />";
// メンバー名が文字列なので、ADVANCED_OPTIMIZATIONSでメンバー名(key)は短縮されないので最適化後参照エラー
b += "obj.name1: " + a.a;
b += "<br />";
b += "obj.name2: " + a.b;

結論

  • 文字列添字は短縮されない
  • 非文字列添字は短縮される

Closure-library単体で、WebAPI Libraryとして作成した場合は、
オブジェクトのメンバーにドット参照でアクセスしていなければADVANCED_OPTIMIZATIONSをかけて、
jQueryとかその他Libraryが混在してそうなアプリ側のスクリプトは、SIMPLE_OPTIMIZATIONSにすればいいのか?


混ぜるな危険ってことですね...

Slim3のGlobalTransactionをよくわからないけど使ってみた

親子関係のある複数のモデルをLocal Transactionを使って保存する場合、Keyによって関連を定義しないと処理を行えないらしいです。
トランザクション - Google App Engine — Google Developers


Slim3にはGlobalTransactionがあるのでEntityGroupを気にしなくても整合性を保って更新できるって事なので使ってみました

環境

  • Slim3 version 1.0.12
  • App Engine SDK 1.5.2

汎用的に処理を行うため、Handlerクラスを定義してます。

  • PersistWithTx: Global Transactionで永続化
  • DataHandler: 永続化のための処理
  • Person: Personモデル(親)
  • Address: Addressモデル(子)
  • PersonService: サービスクラス

ソース

Global Transactionを使って永続化を行う処理

モデル処理の実行サンプル

新規登録のテストケース

こんな感じ?

package jp.rh7.service;

import static org.junit.Assert.*;
import static org.hamcrest.CoreMatchers.*;
/* [ ... ]: その他パッケージ */

public class PersonServiceTest extends AppEngineTestCase {
/* [ ... ] */
    @Test
    public void createTerm() throws Exception {
        // Matchers
        Person chkPersonModel = new Person();
        chkPersonModel.setName("山田 太郎");
        Address addrModel = new Address();
        addrModel.setZipCode("123-4567");
        addrModel.setAddress("東京都なんとか区なんとかかんとか1-2");

        /* Excecute */
        service.createTerm(chkPersonModel, addrModel);
        List<Person> storedPersonList = QueryManager.getPersonList();
        /* result */
        assertThat(storedPersonList, is(notNullValue()));
        assertThat(storedPersonList.size(), is(1));
        Person storedPersonModel = storedPersonList.get(0);
        assertThat(storedPersonModel, is(notNullValue()));
        List<Address> storedAddrList =
            storedPersonModel.getAddressListRef().getModelList();
        assertThat(storedAddrList, is(notNullValue()));
        assertThat(storedAddrList.size(), is(1));
        Address storedAddrModel = storedAddrList.get(0);
        assertThat(storedAddrModel, is(notNullValue()));
        /* Check save values */
/* [ ... ] */
    }
}