Yeoman + AngularJS + RequireJS (+ Ionic)

これまでに書いたリサーチ系ポスト

環境構築メモ (1) Gulp
環境構築メモ (2) Grunt
環境構築メモ (3) RequireJS
環境構築メモ (4) Bower
フレームワーク習得メモ (1) AngularJS
フレームワーク習得メモ (2完) Ionic
環境構築メモ (5完) Yeoman

これらをまとめるクライマックスです。

これまでに、Gulp, Gruntで自己流ワークフローを確立し、今までキャッチアップしそびれていたツールとして、Require, Bower, AngularJS を勉強してきました。

そして、これらを一式、きちんと動くように束ねたものが、
soohei/yeoman-angular-coffee-requirejs-seed
です。

 

Yeomanで、
yo angular –minsafe –coffee
して出来るテンプレートをベースに、AngularJSをRequireJSから始動するように変更し。
distのビルドプロセスにも、RequireJSのOptimizerを追加しました。

Gruntの処理への主な変更箇所は、
wiredepを無効に
useminでJSをconcatするのを無効に
などです

また、RequireJSでOptimizeする時に、JS内の変数名を書き換えられてしまうと、Angularが動かなくなってしまうので、
mangle: false
にして、JS内の変数名を保護しています。

 

RequireJSの処理としては、

・angularは、exportの記述が必要
domready! を使う (!は意味がある)
・bower_compornents 内のコードを書き換えたくないので、一旦CDNは諦める。
※ optimizeでファイルを結合してしまうと、CDN読み込み前に、jQueryプラグインや、Angularの依存ファイルが読まれてしまい、エラーになる
※ jQuery依存のライブラリを一つ一つ、「define([‘jquery’], function($) { })」のように囲めば解決は出来る)
※ angular依存のライブラリを一つ一つ、「define([‘angular’], function(angular) {  })」のように囲めば解決は出来る)

となりました。

 

最後にIonicですが、

bower install ionic#1.0.0-beta.13 –save

して、


ionic: {
  exports: 'ionic'
}

ionicAngular: {
  deps: [
    'ionic'
    'angular'
    'angularAnimate'
    'angularSanitize'
    'uiRouter'
  ]
  exports: 'ionicAngular'
}


define [
  'angular'
  'ionic'
  'ionicAngular'
],

(angular) ->
  'use strict'
  return angular.module 'MyApp', ['ionic']

 

という感じで使えましたが、フレームワーク自体がバギーで、ドキュメント通りにやっても正しく動かないことがある。

具体的には、ion-nav-barが勝手にinvisibleになる。
http://stackoverflow.com/questions/27460173/ng-include-with-ion-nav-bar-not-displaying
http://forum.ionicframework.com/t/ionic-beta-1-0-0-navbar-not-show-on-screen/2346
http://forum.ionicframework.com/t/ionic-beta-1-0-0-navbar-not-show-on-screen/2346

ionicを読み込んだ時点で、ウィンドウのスクロールが無効になり、
スクロールの可否をIonicに委ねる羽目になる。

というあたりで思考停止して、部分的な利用は難しそうだと思いました。

Ionicのエキスパートがいたら教えてください。(連絡先: @soohei, http://td-inc.jp/contact)

コーディング環境 (2014.08)

日々進化している最近のコーディング (フロントエンド開発) 環境について、
ここ1年くらいのノウハウを詰めて、
極端には新しいもの好きではない視点で整理します。

目次:
1. psdからのスライス
2. Compass
3. スプライトイメージの自動化
4. CoffeeScript
5. Gruntによる自動化
6. ローカルサーバー
7. その他

1. psdからのスライス

slicy

Slicyというアプリを使い始めてからは、ずっとこれです。
Photoshopのレイヤーやグループ名に画像のファイル名(+拡張子)を付けることで、
ワンクリックで画像を書き出せます。

ファイル名に@2xと付けることで、Retina用画像(@2x)と、
等倍画像を同時に書き出すこともできます。

その他に@boundsというレイヤーをつくって矩形領域を指定したり、
@slicesというフォルダをつくって、その中にファイル名と
領域指定用のレイヤーをつくることも出来ます。

公式サイトのヘルプを読んだり、
サンプルをダウンロードすると理解が早いです。

 

2. Compass

CSSにネスト構造をつくって記述できるフレームワーク。
インストールの方法などは公式ドキュメント参照。
このエントリーでは、”gem install compass –pre” でインストール出来る、
Compass 1.0.0.rc.1を使って、SCSSの文法で書いています。

必要なコマンドは
compass watch

 

3. スプライトイメージの自動化

Compassのmixinを使えば、関数や変数を使ってCSSを記述できます。
一番の恩恵として、スプライトイメージの生成の自動化について、
色々試行錯誤した結果、現時点で使っているmixinを紹介します。
参考: Using Compass Generated Sprite Sheets in Responsive Sass, by Jayson Jacobs.

・サーバーにアップするファイルはassetsフォルダにまとめる
・サーバーにアップしないファイルはsourceフォルダにまとめる
・Source Mapを有効にするとChromeでscssファイルのデバッグができる
参考: Working with CSS Preprocessors – Google Chrome
・ロゴ用の画像として、
/resource/images/sprite/logo.png
/resource/images/sprite_2x/logo_2x.png (2倍サイズ)
の2枚が用意されているとする。

config.rb

css_dir = "assets/css"
sass_dir = "source/sass"
images_dir = "source/images"
generated_images_dir = "assets/images"
javascripts_dir = "assets/js"
fonts_dir = "assets/fonts"
output_style = :nested
relative_assets = true
line_comments = true
preferred_syntax = :scss
sass_options = { :sourcemap => true }

HTML

<div class="logo">
<div class="sprite">Logo</div>
</div>

→ .sprite部分に、background-imageが適用される

scssファイル

// スプライト画像
.sprite {
display: block;
overflow: hidden;
background-repeat: no-repeat;
text-align: left;
text-indent: -10000px;
font-size: 1px;
line-height: 1px;
}

@mixin sprite-background($sprites, $sprites-img, $name, $scale, $skip-base-params:false) {
background-image: $sprites-img;
@include background-size(image-width(sprite-path($sprites)) / $scale auto);

//retina時に重複するパラメータをスキップするための分岐
@if not($skip-base-params){
height: image-height(sprite-file($sprites, $name)) / $scale;
width: image-width(sprite-file($sprites, $name)) / $scale;
$ypos: nth(sprite-position($sprites, $name), 2) / $scale;
background-position: 0 $ypos;
}
}

$sprites: sprite-map("sprite/*.png", $spacing: 2px);
$sprites-img: sprite-url($sprites);
$sprites_2x: sprite-map("sprite_2x/*.png", $spacing: 4px);
$sprites-img_2x: sprite-url($sprites_2x);

@mixin sprite-image ($name) {
.sprite {
@include sprite-background($sprites, $sprites-img, $name, 1);
}
.backgroundsize.retina_2x & {
.sprite {
@include sprite-background($sprites_2x, $sprites-img_2x, $name, 2, true);
}
}
}

→ /assets/images/ フォルダ内にスプライトイメージが書き出される。

 

また、Slicyからの作業の効率化を考えて、一つ、自作のRubyのスクリプトを使っています。
ruby mv_sprite.rb
で実行すると、実行したディレクトリ以下の@2xという名前がついた画像を、
_2xにリネームして、さらに_2x付きのディレクトリを作ってその中へ移動します。
@がついたファイル名が、Compassではコンパイルエラーを起こすことへの対処です。
(Compass 0.13.alpha.12時点)
上記mixinも、Slicy書き出し→このRubyの処理、が前提になっています。

例:
assets/images/sprite/logo.png
assets/images/sprite/logo@2x.png

assets/images/sprite/logo.png
assets/images/sprite_2x/logo_2x.png

mv_sprite.rb

# ディレクトリを再帰的に抽出する
require 'pp'
require 'FileUtils'

root = File.expand_path(File.dirname(__FILE__))
root = ARGV[0] if ARGV[0]

# フォルダの一覧を取得
dir_entries = Dir.glob(root + "/" + "**/*")
# pp dir_entries

for filePath in dir_entries do
# pp file
ext = File::extname(filePath) # 拡張子取得

# 画像のみに処理
if( ext == '.jpg' || ext == '.png' || ext == '.gif' )
dir = File::dirname(filePath) # ディレクトリ
base = File::basename(filePath,ext) # 拡張子除去
twox_index = base.index('@2x')

# @2xを含む
if( twox_index && twox_index >= 0 )
p "#{dir}/#{base}#{ext}"
parent_dir = File::dirname(dir) # e.g. path_to_htdocs/common/img
my_dir = File::basename(dir) # e.g. sprite
twox_my_dir = "#{my_dir}_2x" # e.g. sprite_2x
twox_dir = "#{parent_dir}/#{twox_my_dir}" # e.g. path_to_htdocs/common/img/sprite_2x
twox_base_ext = "#{base.sub('@2x', '_2x')}#{ext}" # e.g. hoge_2x.png

# ディレクトリがなければ作る
FileUtils.mkdir_p(twox_dir) unless FileTest.exist?(twox_dir)

# ファイルを移動する
File.rename filePath, "#{twox_dir}/#{twox_base_ext}"

p "#{twox_dir}/#{twox_base_ext}"
p '--'
end
end
end

 

4. CoffeeScript

JavaScripterの間ではすっかり浸透したCoffeeScript を使います。
コードの行数、文字数が減らせるので、その分コメントを充実させられます。

必要なコマンドは
coffee -o assets/js -cw assets/coffee

 

ここまでの内容をまとめたものは、
https://github.com/soohei/compass-coffee-template
にあります。

 

5. Gruntによる自動化

注) 最新の状況はこちら

Gruntはこれまでに書いた、CompassやCoffeeScriptのコンパイル処理、
ソースの結合やMinify、ファイルの監視、簡易サーバーの起動などを
全て一つのコマンドで実行してしまう脅威の仕組みです。
他にGulpというのがあるようなのですが、まだ試していないです..
セットアップ方法は省略します。普段使っている基本的な設定・プラグインの構成は以下。

package.json (使用しているパッケージの一覧)

{
"name": "sample",
"version": "0.0.0",
"author": "Sohei Kitada",
"devDependencies": {
"grunt": "~0.4.5",
// ベンダープレフィックス補完
"grunt-autoprefixer": "~1.0.0",
// メディアクエリーをまとめる
"grunt-combine-media-queries": "~1.0.19",
// CoffeeScript
"grunt-contrib-coffee": "~0.11.0",
// Compass
"grunt-contrib-compass": "~0.9.1",
// ファイルの結合
"grunt-contrib-concat": "~0.5.0",
// 簡易サーバー
"grunt-contrib-connect": "~0.8.0",
// Minify JS
"grunt-contrib-uglify": "~0.5.1",
// ファイルの更新検知
"grunt-contrib-watch": "~0.6.1",
// CSSのプロパティをソートする
"grunt-csscomb": "~3.0.0",
// Minify CSS
"grunt-csso": "~0.6.3",
// シェルコマンドの実行
"grunt-exec": "~0.4.6"
}
}

 

Gruntfile.coffee (タスク)

module.exports = (grunt) ->
# load package.json
grunt.initConfig
pkg: grunt.file.readJSON 'package.json'

compass:
dist:
options:
config: 'config.rb'
outputStyle: 'compressed'
environment: 'production'
dev:
options:
config: 'config.rb'

coffee:
compile:
options:
join: true
files:
'source/js/script.js': ['source/coffee/**/*.coffee']

exec:
mv_sprite:
cmd: 'ruby mv_sprite.rb'

autoprefixer:
options:
browsers: ['last 2 version', 'ie 8', 'ie 9']
default:
src: 'assets/css/style.css'
dest: 'assets/css/style.css'

csso:
default:
src: 'assets/css/style.css'
dest: 'assets/css/style.css'

cmq:
default:
src: 'assets/css/style.css'
dest: 'assets/css/style.css'

csscomb:
default:
src: 'assets/css/style.css'
dest: 'assets/css/style.css'

concat:
jsdefault:
src: [
'source/jslib/**/*.js'
'source/js/**/*.js'
]
dest: 'assets/js/script.js'

license: {
src: [
'source/jslib/_license.js'
'assets/js/script.js'
]
dest: 'assets/js/script.js'
}

uglify:
default:
src: 'assets/js/script.js'
dest: 'assets/js/script.js'

connect:
uses_defaults: {}

watch:
options: # enable livereload
livereload: true
compass: # watch scss files
files: 'source/sass/**/*.scss'
tasks: ['compass:dev']
coffee: # watch scss files
files: 'source/coffee/**/*.coffee'
tasks: ['coffee:compile']
js: # watch js files
files: ['source/js/**/*.js', 'source/jslib/**/*.js']
tasks: ['concat:jsdefault']
image: # watch image files
files: 'source/images/**'
tasks: ['exec:mv_sprite']
html: # watch html files
files: '**/*.html'

# load Grunt Plugins
grunt.loadNpmTasks('grunt-autoprefixer')
grunt.loadNpmTasks('grunt-combine-media-queries')
grunt.loadNpmTasks('grunt-contrib-coffee')
grunt.loadNpmTasks('grunt-contrib-compass')
grunt.loadNpmTasks('grunt-contrib-concat')
grunt.loadNpmTasks('grunt-contrib-connect')
grunt.loadNpmTasks('grunt-contrib-uglify')
grunt.loadNpmTasks('grunt-contrib-watch')
grunt.loadNpmTasks('grunt-csscomb')
grunt.loadNpmTasks('grunt-csso')
grunt.loadNpmTasks('grunt-exec')

# tasks

# defalt
grunt.registerTask('default', ['connect', 'watch']);

# development
grunt.registerTask('dev', ['exec:mv_sprite', 'compass:dev', 'coffee:compile', 'concat:jsdefault']);

# distribution
grunt.registerTask('dist', ['exec:mv_sprite', 'compass:dist', 'autoprefixer', 'cmq', 'csscomb', 'csso', 'coffee:compile', 'concat:jsdefault', 'uglify', 'concat:license']);

return

 

「grunt」の処理内容
・connect
簡易Webサーバーの立ち上げ (プロジェクトによっては使わない)

・ファイルの更新の監視
1) images内が更新されたら…「3. スプライトイメージの自動化」で紹介したRubyコマンドを実行
2) scssが更新されたら…Compassを再コンパイル
3) coffeeが更新されたら…CoffeeScriptを再コンパイル → jsファイルが書き出される
4) jsが更新されたら…JSファイルを一つのファイルに結合

「grunt development」の処理内容
・上記 1),2),3),4)を順番に実行

「grunt distribution」の処理内容
・1)を実行
・2)をdist用の設定 (outputStyle: ‘compressed’) で実行
・ベンダープレフィックスを追加
・メディアクエリーをまとめる
・CSSのプロパティをソート
・CSSをMinify
・3)を実行
・4)を実行
・JSをMinify
・上部にライブラリ関係のライセンスを追加

ここまでの内容をまとめたものは、
https://github.com/soohei/grunt-template
にあります。

 

6. ローカルサーバー

本気で開発する場合は、MAMPでローカルサーバーを立てています。
詳しくは、
MAMP 2 → MAMP 3
MAMPでバーチャルホストの設定 (2種類)
あたりに書かれています。

 

7. その他

最後に、ここまでのワークフローで生まれた、
本番環境にアップ不要のファイルはサーバーに転送しないように、
Transmitを設定しています。

transmit

 

coffeescriptでprivate

あらためて復習。
CoffeeScriptのクラスではprivateな変数・関数はつくれないっぽい。
(継承のオーバーライドを想定しない場合は、できないことはないっぽい →参考欄の1つ目)
最初に比較用のASのコードを載せています。

 

– AS脳で理解するための比較用 ActionScript
→ private var, private function がcoffeeだと使えない

 

package{
class SampleClass{
public static var staticVar:String = "staticVar";
public static var staticVar2:String = "staticVar2";

private static var _staticVar3:String = "_staticVar3";

public var publicVar2:String= "publicVar2";

/* CONSTRUCTOR */
function SampleClass(){
// インスタンスのプロパティ
this.publicVar = "publicVar";
}

// PUBLIC STATIC FUNCTION
public static staticPublicFunction(){
trace("\n**** from staticPublicFunction");
trace(this);
trace(_staticVar3);
trace(SampleClass.staticVar);
trace(publicVar);
trace(publicVar2);
}

/* PRIVATE FUNCTION */
private function _privateFunction(){
trace("\n**** from _privateFunction");
trace(this);
trace(_staticVar3);
trace(SampleClass.staticVar);
trace(publicVar);
trace(publicVar2);
}

// PUBLIC FUNCTION
public function publicFunction(){
trace("\n**** from publicFunction");
trace(this);
trace(_staticVar3);
trace(SampleClass.staticVar);
trace(publicVar);
trace(publicVar2);
}

public function callPrivateFunction(){
_privateFunction();
}

public function updateStaticVar3(){
_staticVar3 = "_staticVar3:changed";
}
}
}

 

– CoffeeScript

 

class SampleClass

### PUBLIC STATIC VAR ###
@staticVar: '@staticVar'
@staticVar2 = '@staticVar2'

###
# PRIVATE STATIC VAR
# クロージャー内の変数扱い? インスタンスの変数ではない
###
_staticVar3 = '_staticVar3'

### PUBLIC INSTANCE VAR ###
publicVar2: 'publicVar2'

###
# ここまでのまとめ:
# ・頭に@がつく場合
# 無条件で静的なプロパティになる
# ・@がない場合
# =を使うと静的なプロパティに、
# :を使うとインスタンス(prototype)のプロパティになる
###

### CONSTRUCTOR ###
constructor: ->
# インスタンスのプロパティ
@publicVar = '@publicVar'

### PUBLIC STATIC FUNCTION ###
@staticPublicFunction = ->
console.log('\n**** from staticPublicFunction')
console.log(@)
console.log _staticVar3 #OK
console.log SampleClass.staticVar #OK
console.log @publicVar #NG
console.log @publicVar2 #NG
@

###
# PRIVATE FUNCTION の出来損ない
# インスタンスのメンバー変数にアクセスできない!
# クロージャー内の関数扱い?
###
_privateFunction = ->
console.log('\n**** from _privateFunction')
console.log(@)
console.log _staticVar3 #OK
console.log SampleClass.staticVar #OK
console.log @publicVar #NG
console.log @publicVar2 #NG
@

### PUBLIC FUNCTION ###
publicFunction: ->
console.log('\n**** from publicFunction')
console.log(@)
console.log _staticVar3 #OK
console.log SampleClass.staticVar #OK
console.log @publicVar #OK
console.log @publicVar2 #OK
@

callPrivateFunction: ->
_privateFunction()
@

updateStaticVar3: ->
_staticVar3 = '_staticVar3:changed'
@

### 1つ目のインスタンス sample をつくって色々テスト ###

sample = new SampleClass()
console.log('\n**** from global')
console.log SampleClass.staticVar #OK
console.log sample.publicVar #OK
console.log sample.publicVar2 #OK
console.log sample._staticVar3 #NG

sample.publicFunction()
sample.callPrivateFunction()
SampleClass.staticPublicFunction()

### _staticVar3を更新する ###
sample.updateStaticVar3()

### 2つ目のインスタンス sample2 をつくって色々テスト ###
console.log '\n#### sample2 ####'

sample2 = new SampleClass()
console.log('\n**** from global')
console.log SampleClass.staticVar
console.log sample2.publicVar
console.log sample2.publicVar2
console.log sample2._staticVar3

sample2.publicFunction()
sample2.callPrivateFunction()
SampleClass.staticPublicFunction()

###
# sample2をつくっての実験のまとめ:
# _staticVar3 が変わってしまっていることからも、
# インスタンス変数ではなく、スタティックな変数であることがわかる。
###

 

– コンパイル結果 JavaScript

 

// Generated by CoffeeScript 1.4.0
(function() {
var SampleClass, sample, sample2;

SampleClass = (function() {
/* PUBLIC STATIC VAR
*/

var _privateFunction, _staticVar3;

SampleClass.staticVar = '@staticVar';

SampleClass.staticVar2 = '@staticVar2';

/*
# PRIVATE STATIC VAR
# クロージャー内の変数扱い? インスタンスの変数ではない
*/

_staticVar3 = '_staticVar3';

/* PUBLIC INSTANCE VAR
*/

SampleClass.prototype.publicVar2 = 'publicVar2';

/*
# ここまでのまとめ:
# ・頭に@がつく場合
# 無条件で静的なプロパティになる
# ・@がない場合
# =を使うと静的なプロパティに、
# :を使うとインスタンス(prototype)のプロパティになる
*/

/* CONSTRUCTOR
*/

function SampleClass() {
this.publicVar = '@publicVar';
}

/* PUBLIC STATIC FUNCTION
*/

SampleClass.staticPublicFunction = function() {
console.log('\n**** from staticPublicFunction');
console.log(this);
console.log(_staticVar3);
console.log(SampleClass.staticVar);
console.log(this.publicVar);
console.log(this.publicVar2);
return this;
};

/*
# PRIVATE FUNCTION の出来損ない
# インスタンスのメンバー変数にアクセスできない!
# クロージャー内の関数扱い?
*/

_privateFunction = function() {
console.log('\n**** from _privateFunction');
console.log(this);
console.log(_staticVar3);
console.log(SampleClass.staticVar);
console.log(this.publicVar);
console.log(this.publicVar2);
return this;
};

/* PUBLIC FUNCTION
*/

SampleClass.prototype.publicFunction = function() {
console.log('\n**** from publicFunction');
console.log(this);
console.log(_staticVar3);
console.log(SampleClass.staticVar);
console.log(this.publicVar);
console.log(this.publicVar2);
return this;
};

SampleClass.prototype.callPrivateFunction = function() {
_privateFunction();
return this;
};

SampleClass.prototype.updateStaticVar3 = function() {
_staticVar3 = '_staticVar3:changed';
return this;
};

return SampleClass;

})();

/* 1つ目のインスタンス sample をつくって色々テスト
*/

sample = new SampleClass();

console.log('\n**** from global');

console.log(SampleClass.staticVar);

console.log(sample.publicVar);

console.log(sample.publicVar2);

console.log(sample._staticVar3);

sample.publicFunction();

sample.callPrivateFunction();

SampleClass.staticPublicFunction();

/* _staticVar3を更新する
*/

sample.updateStaticVar3();

/* 2つ目のインスタンス sample2 をつくって色々テスト
*/

console.log('\n#### sample2 ####');

sample2 = new SampleClass();

console.log('\n**** from global');

console.log(SampleClass.staticVar);

console.log(sample2.publicVar);

console.log(sample2.publicVar2);

console.log(sample2._staticVar3);

sample2.publicFunction();

sample2.callPrivateFunction();

SampleClass.staticPublicFunction();

/*
# sample2をつくっての実験のまとめ:
# _staticVar3 が変わってしまっていることからも、
# インスタンス変数ではなく、スタティックな変数であることがわかる。
*/

}).call(this);


参考:
CoffeeScriptでstatic/private/publicなメンバ/メソッドをもったクラスのつくりかた « DevJamMemo
[改訂]CoffeeScriptでstatic/private/publicなメンバ/メソッドをもったクラスのつくりかた « DevJamMemo
JavaScriptでstatic/private/publicなメンバ/メソッドをもったクラスのつくりかた | ALUMICAN.NET
JavaScript のスコープチェーンとクロージャを理解する – tacamy memo
AcrionScript3やってた自分からみたCoffeeScript | 宇都宮ウエブ制作所

アルミ缶先生さすがっす