僕がいたところ
納涼!ほんとにあった怖いコード(by CodeIQ×はてな)
前にいたプロジェクトの話。
本当のヤバさはコード以外にも沢山ありエンジニアにとっては阿鼻叫喚。
でも営業マンと販売戦略が良かったので、「文言を追加したい?10万ですね(´σ `) 」という世界だった。
作っていたのはWebアプリケーションもどきでした。
画面固有のjsファイル1万4千行。
メソッドは全てグローバル。
1000個くらい定義されてた。
まず、goto文をやりたがってるswitch文がいた。
switch(foo) { case "a": // ~ break; case "b": case "c": case "d": // ここに100行くらい case "e": case "f": // ここに200行くらい break; case "g": // ... まだまだ続くよ… }
という感じで2000行近くのswitch分がbreakあったりなかったりで書かれていた。
それが3,4種類あった。しかも微妙にガード式が違う
そして、バグをしょっちゅう引き起こす。訳の分からないコード達
function calc1_blur(){ var a = document.getElementById("hoge").value + document.getElementById("piyo").value document.getElementById("foo").value = a; calc2_blur(); } function calc2_blur(){ var a = document.getElementById("hoge").value + document.getElementById("piyo").value document.getElementById("bar").value = a; calc3_blur(); } function calc3_blur(){ if(document.getElementById("bar").value != ""){ var a = document.getElementById("fuga").value; document.getElementById("foo").value = document.getElementById("bar").value + a; calc4_blur(); } } function calc4_blur() { // なんか色々 calc2(); }
みたいな阿弥陀くじ。しかもblurでDOMにくっついてる
サーバーサイドのコードは
"select * from foo where id = " + id
のような古典的なSQLインジェクションウェルカムコード。
むしろリクエストでANDとかOR貰ってSQLインジェクション利用した実装がされてたりした。
<?=hoge?>
のようなXSSウェルカムコード。
むしろ開発者が反射型のXSSを利用した機能を実装したりしていた。
そして、グローバル変数が1万個以上定義されていて
if(flg_a && flg_b) { if(flg_c && flg_d) { if(flg_e && flg_f) { // これが16段くらいまでネストしてる } if(flg_g && flg_e && flg_i) { } } }
もちろんこんなレベルのコーディングなので別の箇所にも同じ条件式がコピペされてて
if(flg_a && flg_b) { if(flg_c && flg_d) { if(flg_e && flg_f) { // これが16段くらいまでネストしてる } if(flg_g && flg_h && flg_i && flg_j) { } } }
よくよく見ると、微妙に違う。
あと
hoge(); // おまじない。これがあると動くらしい
こんなのは普通過ぎました。
で、リリース時には複数のサーバーにアップロードするのだけど、
「バージョン管理からリリースしたいコードだけを手動でマージして、動かなければその場で修正しそれはバージョン管理にはコミットしない」
という謎な作業を行い、それをひたすら繰り返した挙句
「開発してた環境と本番のコードが違う!でもリリースしなきゃいけないからする」と言って沢山の障害を生み出していました。
一時テーブルじゃなくて実テーブルをcreateしてdropするコードがあるなんて事件もありました。
思い出してて辛くなったのでこの辺にします…
ちなみに、そのプロジェクトの一人前の条件は
「if文とfor文を覚え任意の処理を書けるようになること。」
でした。
Twitterにバルス!するやっつけコードを書いた
コードのネタは数年前にどこかで見たscala日本語プログラミングの話から(忘れてしまった…)
来たる8月2日、Twitter vs バルスの日
当日バルス数十分前に思いついたのでやってみた。 バルスを唱えるスクリプトをScalaで書く。
まず初めに
崩壊の呪文を唱える
こんな感じで実行できたら嬉しいのでそんな感じにします。
ただScalaは普通に日本語メソッド定義できちゃうので、普通に文字列でメソッドを定義してしまうのもつまらないし、形態素に分けて
"崩壊" の 呪文 を 唱 える
という感じで定義します。
「の」と「を」は多分格助詞、「える」は助動詞だと思うので、implicit conversion
class jp[A](self:A) { def の[B](f:A => B) = f(self) def を[B](f:A => B) = f(self) def える() = () } implicit def a[A](v:A) = new jp(v)
という感じにつなげれるようにします。
「崩壊の呪文」はバルスですね!ついでにアブラカタブラも実装しておきます。
def 呪文(s:String) = s match { case "崩壊" => Some("バルス!") case "死" => Some("アブラカタブラ!") case _ => None }
次に唱えるところですが、とりあえず動くところを確かめたいので
def 唱(str:Option[String]) = str.foreach{ s => println(s) }
としておきます。
で、メインである呪文を唱えるコードを追加
"崩壊" の 呪文 を 唱 える
最終的にできあがったのは
class jp[A](self:A) { def の[B](f:A => B) = f(self) def を[B](f:A => B) = f(self) def える() = () } def 呪文(s:String) = s match { case "崩壊" => Some("バルス!") case "死" => Some("アブラカタブラ!") case _ => None } def 唱(str:Option[String]) = str.foreach{ s => println(s) } implicit def a[A](v:A) = new jp(v) "崩壊" の 呪文 を 唱 える // バルス
とりあえず、ツイッターに投稿はできていませんが、思ったように呪文が唱えられそうです。
次にTwitterに呪文を実際に唱えるところ。
twitter4jのライブラリを使うと直ぐにできるので使います。
mavenの説明がありますが、みんな大好きsbtだとこんな感じ
name := "Bals" version := "1.0" scalaVersion := "2.10.2" libraryDependencies += "org.twitter4j" % "twitter4j-core" % "3.0.3"
各種APIを使用するのに必要なキーをTwitter Deveoperページで取得してきて「唱」を適当にツイートできるように実装します。
面倒なんで綺麗に書かずにやっつけコード
import twitter4j.conf.ConfigurationBuilder import twitter4j.TwitterFactory def 唱(str:Option[String]) = str.foreach{ s => val CONSUMER_KEY = "Consumer key" val CONSUMER_SECRET = "Consumer secret" val ACCESS_TOKEN = "Access token" val ACCESS_TOKEN_SECRET = "Access token secret" builder.setOAuthConsumerKey(CONSUMER_KEY) builder.setOAuthConsumerSecret(CONSUMER_SECRET) builder.setOAuthAccessToken(ACCESS_TOKEN) builder.setOAuthAccessTokenSecret(ACCESS_TOKEN_SECRET) new TwitterFactory(builder.build).getInstance.updateStatus(s) // これでツイートできる }
これで、唱えるとツイッター上に唱えれるようになったので、最後に全部くっつけて下記のようなコードになりました。
import twitter4j.conf.ConfigurationBuilder import twitter4j.TwitterFactory class jp[A](self:A) { def の[B](f:A => B) = f(self) def を[B](f:A => B) = f(self) def える() = () } def 呪文(s:String) = s match { case "崩壊" => Some("バルス!") case "死" => Some("アブラカタブラ!") case _ => None } def 唱(str:Option[String]) = str.foreach{ s => val CONSUMER_KEY = "Consumer key" val CONSUMER_SECRET = "Consumer secret" val ACCESS_TOKEN = "Access token" val ACCESS_TOKEN_SECRET = "Access token secret" builder.setOAuthConsumerKey(CONSUMER_KEY) builder.setOAuthConsumerSecret(CONSUMER_SECRET) builder.setOAuthAccessToken(ACCESS_TOKEN) builder.setOAuthAccessTokenSecret(ACCESS_TOKEN_SECRET) new TwitterFactory(builder.build).getInstance.updateStatus(s) } implicit def a[A](v:A) = new jp(v) "崩壊" の 呪文 を 唱 える
使い捨てのコードなので適当だけど、こんな感じでツイートできました。
抜き打ちテストが分からなかった
抜き打ちテストが分からなかった
(※ブログの存在忘れててgistに書いてしまった)
じゃあ this の抜き打ちテストやるぞーをやってみた。
結果として、コードが読めなくて2問空欄解答をするしかなかったので調べてみました。
あ、やってない人は読む前に先にやってみてください。
ちなみにthisの話はあんまりしません。
そして今日調べたばかりなので間違っている可能性や、そもそも自分の知識不足のせいでバカ発見されただけかも知れません。
ただのindirect eval
問題のコードはこれ(※どうやら改訂されてるようです)
('hoge', eval)('this') === window
正直、このコード見たとき何が起こってるのか理解できなかった。
実行結果じゃなくて、この構文が。
結論から言うとカンマ演算子なのだけど、jsでこの記法を見たことがなかったのでちょっと感動しました。
カンマ演算子とは、MDNに在るとおりforなんかでやる。
for (var i = 0, j = 0; i < 10; i++, j++) { console.log(i); console.log(j); }
の,です。 こう見ると、何それ当たり前の構文じゃん。って感じですが…
var a = (1, "foo"); // a === "foo"
ということができます。
つまりカンマ演算子の挙動としては、
ということです。 Cと同じなんですが、javaやC#ではなくなっていたので、まさか(失礼)javascriptにはいたとは…
つまり、
('hoge', eval)('this') === window
は
(1, eval)('this') === window (1 == 2, "foo", eval)('this') === window ("Foo".toLocaleLowerCase(), eval)('this') === window
と実質的に同じ挙動を示します。(評価されるので、++iとかしたら違いますが)
結論としてはただのindirect evalされただけのコードだったってことみたいです。
なので、eval絡みだと大体こんな感じ
var a = "global"; (function() { var a = "function"; console.log(a) // function eval("console.log(a)"); // function (eval)("console.log(a)"); // function console.log("ここからglobal"); var e = eval;e("console.log(a)"); // global var e2;(e2 = eval)("console.log(a)"); // global ((function(){return eval})())("console.log(a)"); // global ('hoge',eval)("console.log(a)"); // global })()
eval以外でこういった記法が役に立つケースあるのかな…?
{1, "foo"}の謎現象
ちなみにカンマ演算子に気づくまでに疑問に思ってやったところ、FirebugとChromeデバッガーコンソール、Opera Dragonfly上では、自分では理解できない現象が起きました。(IEは無いので調べてません。)
知っている人がいれば、どの辺の言語仕様を見ればいいのか教えてください。
{1, "foo"} // "foo" var a = {1, "foo"} // もちろんシンタックスエラー ({1, "foo"}) // シンタックスエラー
{1, "foo"} // "foo"
はなぜそう動く…?
圏論とかモナドについて②
群からMonoidまで適当に書きましたが、Scalazでたくさん出てくるモナドさんと戦うにはもう一つ別の方向から、いくつかの概念を知る必要があります。
マサカリが飛び交う戦場、圏論です。
圏について
- 対象と射の2種類の集合であること
- 射の合成演算・が存在
- ・の結合則が成り立つこと
- 単位元が存在すること(※恒等射)
対象と射の2種類の集合であること
これを説明するため、まず対象と射という物を簡単に紹介します。
対象とは
文字通りですが、対象とは対象です。
対象が型であったり、モノイドであったりします。色々な物が対象になります。
今はあまり難しく考えないでください。
対象という言葉に深く捕われず、読んでいくうちに感覚的にどういったことを対象といっているのか見てもらえればよいです。
射について
写像、函数、変換、作用素と同じような同義語みたいな物を思っておけばOKです。
厳密には射と写像が意味しているものは別ですが、とりあえず今は同義語みたいな物として思っておいてください。
射については本来の英語表記に戻します。
射=arrowです。
arrowつまり「→」。->をアロー演算子といいますよね。
->は→ですよね。
arrowですね!
f:x → yは
xはyに対応するよーってことで、射とは対応付けのことです。
対応関係を表わすものです。単射とか全射とか「射」を含む言葉を一つくらいは知ってますよね?
なんとなくイメージつきましたか…?
以上を踏まえて例を適当に挙げます。
ex)対象が型で射がメソッド
対象の集合はInt型とString型とします。
射はtoStringとか、toIntとかいっぱいあるので省略します。
f:Int → String
このときのfはtoString
とかですね!(※”とか”というのは他にもあるから)
val i:Int = 0 val s:String = i.toString
ほら、Int型のiがString型のsになりました。
さらにもう一例
f:String → Int
この時のfは、toInt
とかlength
とかhashCode
とかですね!
val s:String = "1" val i0:Int = s.toInt val i1:Int = s.length val i2:Int = s.hashCode
ほら、String型のsがInt型のiになりました。
「toInt以外は返ってくる値ダメじゃね?」とか思ったそこの貴方。(※思ってない人ごめんなさい)
対象が「型」なので、String型がInt型になればいいんです!
つまり
def f:String => Int def f:Int => String
こういった定義が成されていればそれでOKということです。
つまり対象が型であり、射がメソッドというのは
- 型の集合 String,Int ∈ 型
- メソッドの集合 toString,toInt,length,hashCode ∈ メソッド
なので満たしますね。
ちなみに、元の対象と先の対象はそれぞれ、ドメイン・コドメインと呼ばれます。
この場合だと
射の合成演算・が存在
射の合成演算子 ・ が 射f:T→R, g:R→Sがある時 f・g:T→Sが一意に成り立ちます。
書き方をかえると
R = f(T) S = g(R) S = g(f(T)) = (f・g)(T)
こちらも先ほどの例を引き続いて例を挙げてみると
f:Double → Int g:Int → String
という二つの射について、
f・g:Double → String
になるやつが定義できればいいんです。
def f:Double => Int = _.toInt def g:Int => String = _.toString def f_g:Double => String = g compose f
ここは、特に難しいことはないと思います。
単位元が存在すること(※恒等射)
単位元については元の際に説明しています。
圏論ではこの単位元律については恒等射という言葉で出てきます。が、意味しているものはほぼ一緒なので一応説明しておきます。
恒等射とは 各対象Aに対してid:A→Aとなる恒等射が存在し、任意のf:A→Bに対してf・id=f, id・g=gとなる。
射を合成して変えないものですね。
圏を定義してみる
ここまでを踏まえて簡単に定義してみると。
object Category { def id[A]: A => A = a => a def compose[A, B, C](f: A => B, g: B => C): A => C = g compose f trait CategoryLaw { def associative[A, B, C, D](ab: (A =>: B), bc: (B =>: C), cd: (C =>: D)) (implicit E: Equal[A =>: D]): Boolean = { val ad1 = compose(cd, compose(bc, ab)) val ad2 = compose(compose(cd, bc), ab) E.equal(ad1, ad2) } def leftIdentity[A, B](ab: (A =>: B))(implicit E: Equal[A =>: B]): Boolean = { val ab1 = compose(ab, id[A]) E.equal(ab, ab1) } def rightIdentity[A, B](ab: (A =>: B))(implicit E: Equal[A =>: B]): Boolean = { val ab1 = compose(id[B], ab) E.equal(ab, ab1) } } def categoryLaw = new CategoryLaw {} }
すごくシンプルに定義できると思います。
上記のコードを書く上で参考にしたscalaz 7.0では現在このような定義がされていました。
trait Category[=>:[_, _]] extends Compose[=>:] { self => //// // TODO GeneralizedCategory, GeneralizedFunctor, et al, from Scalaz6 ? /** The left and right identity over `compose`. */ def id[A]: A =>: A /** `monoid`, but universally quantified. */ def empty: PlusEmpty[({type λ[α]=(α =>: α)})#λ] = new PlusEmpty[({type λ[α]=(α =>: α)})#λ] with ComposePlus { def empty[A] = id } /** The endomorphism monoid, where `zero`=`id` and * `append`=`compose`. */ def monoid[A]: Monoid[A =>: A] = new Monoid[A =>: A] with ComposeSemigroup[A] { def zero = id } trait CategoryLaw extends ComposeLaw { /** `_ <<< id` is vacuous. */ def leftIdentity[A, B](ab: (A =>: B))(implicit E: Equal[A =>: B]): Boolean = { val ab1 = compose(ab, id[A]) E.equal(ab, ab1) } /** `id <<< _` is vacuous. */ def rightIdentity[A, B](ab: (A =>: B))(implicit E: Equal[A =>: B]): Boolean = { val ab1 = compose(id[B], ab) E.equal(ab, ab1) } } def categoryLaw = new CategoryLaw {} //// val categorySyntax = new scalaz.syntax.CategorySyntax[=>:] { def F = Category.this } }
今回は全部引用なのでscalazらしい(?)全角記号のλとかあってちょっと難しく見えますが、MonoidとSemigroupという単語を知っていれば読めないレベルの難しいコードではないと思います。
圏論とかモナドについて①
圏論とかモナドとかについて話さなければいけないので先にちょっと下書き的に書きます。
マサカリの燃料投下になればいいなぁ…(推敲できるし)
目的
圏論とかモナドとかという言葉に敏感に反応してマサカリを投げたりしない(できない)人を対象に、Scalazを使うため(?)の基礎知識を身につけることを目的にします。
モナドとか、色々な解説を読む上での必要となる言葉が何を意味しているのかを簡単にイメージできるレベルになることを目的とします
概要
説明の厳密さは除いてあります。
厳密さを除いたら数学じゃなくなりますが、厳密さを知る前の概要程度にとどめてください。
ただ、理解していなくても概要を知っておくとモから始まるアレを利用した解説や、trait達の意味を理解する"きっかけ"になるとは思います。
そして、これらのことを理解していなくてもある程度までの実用には耐えると考えています。
先に言いますが、これらの概念に対する理解をしていなくてもモナドを扱うことは可能です。モナドを意識しなくても、モナドは呼吸をするがごとく自然に、コードの至るところに出現しているからです。
よいコードを書こうとすればするほど、そのコード上の形式はなんであれ自然とモナドはそこにいます。
群論について
群論についての知識を持つことで、よりスムーズに理解できると思うので、先に説明します。
(ちなみに理系の大学でも学科によっては群論扱わなかったりするらしいですね。自分は大学行けなかったので、噂で聞きましたが)
群について
群そのものは知らなくても理解できますが、群というものの定義を知ることでモノイドや半群についての理解の助けになると思うので簡単に説明します。
元という言葉を使いますが、元=要素です。
集合の元といわれたら、集合の要素と認識して下さい。
まず群は下記の4つを満たすものを指す
- 集合の元の間に一意的な演算が成り立ち、その演算に関して元は閉じている
- その演算に対して結合則が成り立つ
- 演算には単位元が存在する
- 演算には逆元が存在する
定義ではすぐ分からないと思うので、厳密さもそんな書く気じゃないし特定の例で説明します。
ex)実数の集合Rと加法演算子+に関して群を成す
集合の要素の間に一意的な演算が成り立ち、その演算に関して集合は閉じている
a,b ∈ R なら a + b ∈ R
1 + 2 = 3
3も実数ですね。証明とかしないけど実数+実数=実数ですね。
その演算に対して結合則が成り立つ
(a , b) + c = a + (b + c)
(1 + 2) + 3 = 1 + (2 + 3) = 6 これは小学生のときにやりました。
演算には単位元が存在する
a + 0 = 0 + a = a
0が単位元になります。
ついでに乗法だと単位元は1です。x * 1 = 1 * x = x
単位元とは、演算の結果何も起こらない(変化しない)元のことです。
4つめ
演算には逆元が存在する
a + (-a) = 0
逆元が存在します。(逆数が一般化したもの、ある任意の元に対して単位元へ導ける元)
もし興味があれば、アーベル群について調べると上記の例をより高度な次元で理解できるようになると思います。
半群について
次に半群と呼ばれるものを説明します。
半群は群を更に弱めた概念で、群の内下記の二つを満たすものを指す
- 集合の要素の間に一意的な演算が成り立ち、その演算に関して集合は閉じている
- その演算に対して結合則が成り立つ
群と違い単位元や逆元が求められていない物です。
モノイドについて
モナドを理解する上で重要な単語の一つであるモノイドについて
モノイドは半群をより群に近づけた概念で、群の内下記の三つを満たすものを指す
- 集合の要素の間に一意的な演算が成り立ち、その演算に関して集合は閉じている
- その演算に対して結合則が成り立つ
- 演算には単位元が存在する
群から逆元を除いたもので、半群に単為元の制約をつけたものですね。
モノイド等、これらは代数的な構造と呼ばれます。
その他代数的構造はいっぱいありますが、まずはこれだけとりあえず知っていればいいです。
英語では
群=group
半群=Semigroups
モノイド=Monoidとなります。
以上を踏まえてScalazのSemigroup.scalaを見てみると
trait Semigroup[F] { self => def append(f1: F, f2: => F): F trait SemigroupLaw { def associative(f1: F, f2: F, f3: F)(implicit F: Equal[F]): Boolean = F.equal(append(f1, append(f2, f3)), append(append(f1, f2), f3)) } def semigroupLaw = new SemigroupLaw {}
という定義を確認できると思います。
appendを定義しろ(FとFの二つの引数からappend演算した結果Fになるものを定義しろ)
SemigroupLawで、appendで結合則が成り立つか確かめるよ。
と言っています。
モノイドは半群をより群に近めた概念。半群を継承したと考えられるので
ScalazのMonoid.scalaには下記のように定義してあります
trait Monoid[F] extends Semigroup[F] { self => def zero: F }
Semigroupに加えて、zero(零元)を定義しろと言っていますね。
ちょっと零元=単位元と考えてもられば、コードの意味が分かると思います。
MonoidやSemigroupsというtraitを継承したtraitが沢山あるので、少しはそれらのtraitの名前から概念・意味が理解できるようになったと思います。
一度群の物語はここで終了です。
ここで一度話しが変わります。(変わるというか、別方向から眺めたい圏というものを説明します)
Scalazでたくさん出てくるモナドさんと戦うにはもう一つ別の方向から、いくつかの概念を知る必要があります。
jQueryでcheckboxのcheckedを取得・設定する
jQueryにはなぜかcheckedを取るスマートな方法が用意されていない。
色々なサイトを見ているとattr("checked")とかで取る方法などが紹介されてるけど、1.9からは正しく動かない(※jQuery Core 1.9 Upgrade Guide)
ということで、Gistに上げておいた。
jQuery本体を修正してプルリクしちゃいたいけど、今まで絶対この問題は話題に上がってるはずだから今ない理由を先に調べないとなー
実装できない何か理由があるのかな?
Grunt導入してみた
普段YUI Compressorを使っていたんだけど、Gruntが良いっぽい噂を聞いたのでGruntを試してみる。
あのjQueryもどうやらコレみたい?GitHub見る限りは。
実行環境は
- Windows 7(64-bit)
アンチリンゴですし、Linux使ってると一般ユーザーから変な拡張子のファイルが送りつけられるといういじめを受けるので普段の環境はWindowsです。.netとか使うし。
すんなりと実行できたけど、一応やったことをメモ代わりに。初めてのインストール///なので余分な出順やもっと簡単に出来るかもしれないのでそういうのを知っていたら教えてくださいw
まずはnode.jsのインストール
GruntはNode.js上で動くので ここに行き、INSTALLのボタンをぽちっと押す。
入手したのはCurrent Version: v0.10.12
とりあえずNode.jsデフォルトに従うのが無難だと思うので、全てデフォルトでインストール。
インストールが終わったら
npm -v
と実行して
1.2.32
とインストールされているのを確認。
Grunt CLIをインストール
次に、Grunt CLIをインストールする
npm install -g grunt-cli
インストールが終わったら
grunt -version
で確認。
ビルドのための設定を行う
作るファイルは、これを読みながら、書いていく。
まずは、ルートフォルダにpackage.jsonを作成し下記の内容を記入する
{ "name": "princessjs.extend", "version": "0.0.1", "devDependencies": { "grunt": "~0.4.1", "grunt-contrib-jshint": "~0.6.0", "grunt-contrib-uglify": "~0.2.2" } }
npm install grunt --save-dev
を実行してインストール。
今回はファイル結合が一つの目的なので書き忘れてたconcatも入れる。
npm install grunt-contrib-concat --save-dev
package.jsonが更新されて
{ "name": "princessjs.extend", "version": "0.0.1", "devDependencies": { "grunt": "~0.4.1", "grunt-contrib-jshint": "~0.6.0", "grunt-contrib-uglify": "~0.2.2", "grunt-contrib-concat": "~0.3.0" } }
となった。
で、本命のGruntfile.jsを作成し下記の内容で保存
module.exports = function(grunt) { 'use strict'; grunt.initConfig({ pkg: grunt.file.readJSON('package.json'), concat: { core: { src: [ 'src/*.js', 'src/util/*.js'], dest: 'build/<%= pkg.name %>.js' }, extend: { src: [ 'src/extend/*.js'], dest: 'build/<%= pkg.nameExtend %>.js' } }, uglify: { options: { banner: '/*! <%= pkg.name %> <%= grunt.template.today("yyyy-mm-dd") %> */\n' }, core: { src: 'build/<%= pkg.name %>.js', dest: 'build/<%= pkg.name %>.min.js', }, extend: { src: 'build/<%= pkg.nameExtend %>.js', dest: 'build/<%= pkg.nameExtend %>.min.js', } }, jshint: { files: ['src/**/*.js'], options: { jshintrc: '.jshintrc' } } }); // Load grunt tasks from NPM packages grunt.loadNpmTasks("grunt-contrib-jshint"); grunt.loadNpmTasks('grunt-contrib-concat'); grunt.loadNpmTasks('grunt-contrib-uglify'); // Default grunt grunt.registerTask('build', [ 'jshint', 'concat', 'uglify' ]); };
今回はプロトタイプ拡張を別に分ける形で、結合・圧縮してみた。
で、このビルドを実行するには
grunt build
を実行するだけ。
実行するとbuildディレクトリが出来て、princess.js、princess.extend.jsとそれぞれのminが出来上がった。 minの方のコードを見てみると、変数名がaやらbやらjQueryのminと同じになってる!
一応圧縮で壊れていないか心配になったのでJsTestDriverを実行してみてもALLグリーン。
やったね!たえちゃん
今回実行したプロジェクトはここにあります