WindowsでWebアプリ開発
何やるかを考える。
前提
学んでもらうこと
開発環境構築
開発OS準備
以下のものを用意してもらう。
準備
- https://www.virtualbox.org/wiki/Downloads からVirtualBoxのexeファイルをダウンロードして、インストールする。
- https://www.ubuntulinux.jp/download/ja-remix-vhd から Ubuntu 12.4 のイメージディスクをダウンロードする。
- ダウンロードした圧縮ファイルを解凍し、中のvhdファイルを
C:\Users\$USER\.VirtualBox
のディレクトリに移動する。
イメージの作成
- VirtualBoxを起動
- 新規(N)から作成。名前を
ubuntu
(とりあえず)、タイプをLinux
、バージョンをUbuntu(32bit)
とそれぞれ設定して次へ - メモリ割り当ての確認があるがひとまずデフォルトのままでよい。
- ハードドライブの割り当てもデフォルトですすめる。
- ドライブの割り当てはすでにあるドライブを選択し、先ほどダウンロードしたvhdファイルを選択する。
- 設定完了。
開発OSのインストール
- ホーム画面から、
ubuntu
のイメージを選択して起動する。 - 初回だとCreating process for virtual machine "ubuntu"...というメッセージがでて初期化処理が走るためしばらく時間がかかるので注意。
- あとはノリで進める FIXME
- ひと通りインストールが終わると、ログインを要求されるのでログインする
- ログインできたら完了。
インストール後にやる設定
terminal
を開くsudo apt-get update && sudo apt-get upgrade
開発ツールのインストール
ssh接続を可能にする
vmのubuntuにminttyやputtyからアクセスできるようにする。
Ubuntu側での作業
sudo apt-get install openssh-server
Windows側での作業
1. VirtualBoxの設定を開く
2. ネットワーク
3. ポートフォワーディング
4. 新規ルールを追加
5. ルール名(ex: SSH)、ホストポート(ex: 50022)、ゲストポート(22)を追加
6. ssh user@localhost -p 50022
をputty等でsshできるか確認
7. 鍵通す。
zshのインストール
shellは慣れてるヤツで。
$ curl -L -O http://sourceforge.net/projects/zsh/files/zsh/5.0.5/zsh-5.0.5.tar.bz2 $ tar xjvf zsh-5.0.5.tar.bz2 $ cd zsh-5.0.5/ $ ./configure --enable-multibyte $ make $ sudo make install
login shellの変更。
$ which zsh /usr/local/bin/zsh $ cat /etc/shells # /etc/shells: valid login shells /bin/sh /bin/dash /bin/bash /bin/rbash # echo `which zsh` >> /etc/shells $ chsh パスワード: 新しい値を入力してください。標準設定値を使うならリターンを押してください ログインシェル [/bin/zsh]: /usr/local/bin/zsh $ exit
gitのインストール
https://code.google.com/p/git-core/downloads/list から最新版を取得。
curl -L -O 'https://git-core.googlecode.com/files/git-1.9.0.tar.gz' tar xzvf git-1.9.0.tar.gz cd git-1.9.0 ./configure make sudo make install
依存系のライブラリをインストール
sudo apt-get install zlib1g-dev sudo apt-get install tcl sudo apt-get install gettext
vimのインストール
curl -L -O ftp://ftp.vim.org/pub/vim/unix/vim-7.4.tar.bz2 tar xjvf vim-7.4.tar.bz2 cd vim74 ./configure --enable-multibyte --enable-luainterp --enable-perlinterp --enable-pythoninterp --enable-rubyinterp make sudo make install
to be continued
node-scheduleという予定を登録して実行するnpmライブラリの紹介
node.jsを使っていて出会った、使い勝手の良さそうなライブラリ(俺基準)を紹介していくシリーズを初めてみました。
node-schedule
https://www.npmjs.org/package/node-schedule
指定時間に登録したタスクを実行してくれるライブラリです。 Dateオブジェクト、日時指定、時刻指定、CRON形式での指定等に対応し、非常に使い勝手が良いです。
installation
npm install
usage
scheduleJob
メソッドを呼び出すことでタスクが登録されます。
第一引数に実行タイミング、第二引数にジョブ名(省略可)、第三引数に実行したいタスクを登録します。 返り値はJobオブジェクトです。
var schedule = require("node-schedule"); var job = schedule.scheduleJob(timing, name, fn);
JobオブジェクトはEventEmitterを利用しており、いくつかのフックポイントが提供されています。
- scheduled: タスクの登録時
- run: タスク実行
- cahceled: 予定のキャンセル時
job.on("scheduled", function () { console.log(this.name + "の予定が登録されました"); }); job.on("run", function () { console.log(this.name + "の予定が実行されました"); }); job.on("canceled", function () { console.log(this.name + "の予定がキャンセルされました"); });
Dateオブジェクトを使う場合
下記は2014/07/05 22:30:00
に処理を実行する場合の例です。
var job = schedule.scheduleJob(new Date(2014, 07, 05, 22, 30, 0), function () { console.log("登録されたジョブの実行"); });
日時を指定する場合
以下の項目をオブジェクトで定義します。
- year
- month
- day
- dayOfWeek
- hour
- minute
- second
下記は毎日18:30
に処理を実行する場合の例です。
var job = schedule.scheduleJob({ hour : 18 minute: 30 }, function () { console.log("実行"); });
CRON形式
下記は毎日18:30
に処理を実行する場合の例です。
-
による範囲指定は可能ですが、/
による指定はできないようです。(たぶん)
var job = schedule.scheduleJob('30 18 * * *', function () { console.log("実行"); });
まとめ
node-scheduleはご覧の通り、特定日時や特定時間での繰り返し処理に向いています。 記法が多岐に渡り、様々なタイマー処理に適応できます。
ちなみに僕はIRCBotでミーティングや予定のリマインドに利用しています。 ぜひ使ってみてください
AngularJSのfactoryとserviceを読み解く(後編)
前編: ngularJSのfactoryとserviceを読み解く(前編)
前回のAngulaJS!(なんちゃらライブ的な) factoryやserviceといったAngularJSの機能をドキュメントを読んだりぐーぐる先生に聞いたりしてもまったくさっぱりよくわからなかったのでAngularJSのコードを追ってみた。
factoryは初期化処理をInjectorで一度だけ実行してその結果をAngularJSのcacheFactoryを使ってキャッシュしているところまではわかった。
しかし、serviceはよくわからなかった。 よくわからないので振り返りから。
.service(name, fn)
- serviceが呼ばれる
- $injector.instantiateにfnが渡される
- $injector.instantiateで別のクラス(仮にAとする)を作成してprototypeを継承する
- $injector.instantiateでクラスAのインスタンスを生成して$injector.invokeにserviceに指定したfnと共に渡す
- $injector.invokeでDIに指定したDependenciesを解析、取得
- $injector.invokeで、引数をdependenciesに、コンテキストをクラスAのインスタンスにしていしてfnを実行する
- $injector.invokeの返り値がobjectまたはfunctionであればそれをfactoryに渡す
- $injector.invokeの返り値がobjectでもfunctionでもなければクラスAのインスタンスをfactoryに渡す
つまり、こういうことだ!
// serviceに登録するfunction(コンストラクタ) var User = function (options) { this.name = options.name || null; this.age = options.age || null; this.set = function (column, value) { this[column] = value; }; this.get = function (column) { return this[column]; }; }; // $injector.instantiate var Constructor= function () {}; Constructor.prototype = User.prototype; var instance = new Constructor(); // $injector.invoke User.apply(instance, [{name: "tarou", age: 10}]); var user = instance;
・・・実にわかりにくい。
気をつけなければならないのは、serviceの第二引数は初期化コールバック関数ではなく、クラスのコンストラクタであること。
factoryとおんなじ用に使うのは不適切で、angularとは別に定義したクラスを指定する(app.service("name", ["$rootScope", MyServiceClass])
のように)か、コンストラクタで完結するクラスを定義すべきだ。
var User = function (options) { this.name = options.name || null; this.age = options.age || null; this.set = function (column, value) { this[column] = value; }; this.get = function (column) { return this[column]; }; }; app.service('user', [User]);
app.service('user', [function () { this.name = options.name || null; this.age = options.age || null; this.set = function (column, value) { this[column] = value; }; this.get = function (column) { return this[column]; }; }]);
serviceはどういう時に使うべき?
factoryとの違いとして、serviceはインスタンスの生成を保証し、かつ同一の名前空間で他のインスタンスは作られない。
デザインパターンで言うところのSingletonであり、アプリケーション内で整合性を管理する用途にもちいられるのだと想定できる。
たとえば、複数のController間で一つのリソースを管理したい場合などがそれに該当すると思われる。
下手に$rootScopeに値を入れたりするよりは固く実装できるのではないだろうか。
まとめ
serviceはSingletonなので、それなりの扱いを用意しよう。 serviceメソッドの第二引数はコンストラクタなのでそれを念頭に置いて利用しよう
AngularJSのfactoryとserviceを読み解く(前編)
AngularJSのfactory
とservice
がどうにも覚えられないのでまとめてみた。
まとめ
- factoryはobjectをキャッシュしておく
- serviceはインスタンス化してキャッシュしておく
- providerの謎が深まった
factory
providerにfunctionらしきものを渡している。
$get
はInjectorに指定されたときに一度だけ呼ばれる。
function factory(name, factoryFn) { return provider(name, { $get: factoryFn }); }
https://github.com/angular/angular.js/blob/master/src/auto/injector.js#L666
ここまでだとまだ実行されない。
var app = angular.module('app', ['ngResource']).factory('User', function ($resource) { return $resource('/api/user/:id', {id: "@id"}, {}); });
ここで実行され、以降の呼び出しはreturn $resource('/api/user/:id', {id: "@id"}, {});
の評価結果がキャッシュされたヤツが呼び出される。
app.controller("UserCtrl", function (User) { $scope.user = User.$get(id: 1); });
service
serviceに指定された無名関数はconstructor
という名で$injector.instantiate
の引数に渡され、その処理をfactoryへと渡している。
function service(name, constructor) { return factory(name, ['$injector', function($injector) { return $injector.instantiate(constructor); }]); } function value(name, val) { return factory(name, valueFn(val)); } function constant(name, value) { assertNotHasOwnProperty(name, 'constant'); providerCache[name] = value; instanceCache[name] = value; }
https://github.com/angular/angular.js/blob/master/src/auto/injector.js#L668-L680
$injector.instantiate
は与えられた引数を、あたらにConstructor
クラスが作られ、serviceに登録した関数のprototypeを継承させいる。
なぜArrayの場合は末尾を取り出しているのかはよくわかってない。
['$rootScope', function () { ... }]
的なアレの為に、Arrayの判定してる。
Constructor
クラスをインスタンス化し、invoke(省略)に渡す。
その返り値がObjectまたはFunctionであればそのままをfactoryにわたすが、どちらでもない場合はConstructor
のインスタンスをfactoryに渡している。
function instantiate(Type, locals, serviceName) { var Constructor = function() {}, instance, returnedValue; // Check if Type is annotated and use just the given function at n-1 as parameter // e.g. someModule.factory('greeter', ['$window', function(renamed$window) {}]); Constructor.prototype = (isArray(Type) ? Type[Type.length - 1] : Type).prototype; instance = new Constructor(); returnedValue = invoke(Type, instance, locals, serviceName); return isObject(returnedValue) || isFunction(returnedValue) ? returnedValue : instance; }
https://github.com/angular/angular.js/blob/master/src/auto/injector.js#L804-L815
なお、invokeでは(重要なところだけを挙げれば)Constructor
クラスのインスタンスをコンテキストとしてserviceで指定した関数を実行しその結果を返しているだけである。
function invoke(fn, self, locals, serviceName){ if (typeof locals === 'string') { serviceName = locals; locals = null; } var args = [], $inject = annotate(fn, strictDi, serviceName), length, i, key; for(i = 0, length = $inject.length; i < length; i++) { key = $inject[i]; if (typeof key !== 'string') { throw $injectorMinErr('itkn', 'Incorrect injection token! Expected service name as string, got {0}', key); } args.push( locals && locals.hasOwnProperty(key) ? locals[key] : getService(key) ); } if (!fn.$inject) { // this means that we must be an array. fn = fn[length]; } // http://jsperf.com/angularjs-invoke-apply-vs-switch // #5388 return fn.apply(self, args); }
https://github.com/angular/angular.js/blob/master/src/auto/injector.js#L801
ここまで追ってみたけどよくわからないので、続きは明日。
Constructor
で空のクラスをprototypeだけ継承し、インスタンス化する。そののちにinvokによってDependenciesを引数としてserviceで指定したfunctionに渡すことで、DIとしての動きを作っているっぽい。
どう使うのかはまだ曖昧だけど、輪郭が見えてきた。
AngularJSでDOMの更新を監視する with iScroll4
iScrollをAngularJSと使いたい案件があったので、それを使うためにいろいろ調べた。
DOMを動かしてScroll
position:fixed
等、モバイルでは一部バグがあったり非サポートだったりして、その大体にiScrollやscrollerjs等といったライブラリを用いる。
これらはDOMをCSSとJSで動かしてあたかもスクロールしているかのように見せるライブラリだ。
いろいろライブラリがあるけれど、とりあえずiScroll4
を使った。
一度使ったことがあったし、ng-iscroll
も使っていたので。
ng-iScroll
Angular x iScrollを使うとなった時、google先生に問うと真っ先にでてくるのだけど、個人的な都合で利用を避けた。
DOMの変更を監視
ng-iScrollのやり方に沿えば、実際iScroll自体は動いたのだけれど、そのままだと、Ajaxで取得したHTML系のコンテンツが非同期的に投入されたときにiscroll.refresh()
してくれない。
そこでどうしたかっていうのが本題。
$watchを使う
以下のコードを見てくれ、どう思う?
scope.$watch(function () { scroller.refresh() });
・・・実はこれでも、XHRで取得したDOMを追加したときにrefreshしてくれる。 でもこの使い方間違っているので注意。
ドキュメントを見てもらえればわかるけれど、$watch
の第一引数は、監視する値を示していて、変更時に走らせる処理は第2引数に指定しなければならない。
一応、第1引数であるwatchExpression
は、functionを渡すことも可能で、値の変更時に呼ばれるのでこれでも動くが、明らかにBKであり、間違っている。
ではどうすればいいのか。
それを知るには、$watch
を含めたscope
のライフサイクルを理解しなければならない。
$watchと$digest
$watchの仕組みや動くタイミングは以下の記事が丁寧にかかれているので参考になった。
$watch
は$digest
で呼ばれており、XHRや値の変更時等にdigest
が呼ばれる。
以下の記事を併せて読むとより理解が深まるんじゃないかと。
AngularJS の $watch, $digest, $apply について書く
DOMの高さの更新を検知したい
さて、$watch
で登録したfunctionがいつどこで呼ばれるかがなんとなくわかったと思う。
今回僕が知りたかったのはiScrollの対象としたDOMの高さが変わったタイミングだ。 つまり、こうすれば取れるはず。
angular.module("myApp").directive("ngScrollable", function () { return { restrict: "A", link: function (scope, elements, attr) { var element = elements[0]; var content = element.children[0]; // スクロールさせたいコンテンツ。適宜セレクタは書き換えるべし。 var scroller = new iScroll(element, {}); scope.$watch(function () { return content.clientHeight }, function { scroller.refresh(); }); } } });
ここまでで2時間かかった・・・ Angular奥が深い
AngularJS x UI-RouterでpushStateを使う
何も考えずにAngularJSを使うと、遷移のURLはハッシュフラグメント(#
)を使うことになる。
しかし、ngRouterの$locationProvider
にはhtml5Mode
という機能があり、こいつを有効にすると、pushStateを使ってURLを構築することができる。
それをUI-Routerで使うよという話。
UI-Routerも$locationProvider
をそのまま利用可能
ほんとうにまんま一緒に有効化できる。
ngRouterの場合:
angular.module("myApp").config(["$locationProvider", function ($locationProvider) { $locationProvider.html5Mode(true); }]);
UI-Routerの場合:
angular.module("myApp").config(["$locationProvider", function ($locationProvider) { $locationProvider.htmlMode(true); }]);
ui-srefの扱い
UI-Routerを使っていて感動したのは、ui-sref
の扱い。
ui-sref
はstateを指定し、そのstateで定義したURLをhrefの属性に変換してくれるDirectiveですが、html5Modeを有効にすると、ハッシュ(#)を取り除いてくれる。
つまり、URLの差し替えとかせずにhtml5Modeに移行できる。 なので、UI-Routerを使っているなら、hrefよりもui-srefを使ったほうが可用性が高いです。
grunt-contrib-connectと併せて使いたい
変更を最小限にhtml5Modeを利用できるのは素晴らしいが、pushStateを使う以上は当然、サーバ側にも手を入れなければならない。
基本的には/
のアクセスは/index.html
にリライトされているだけなので、/hoge
とかのURLを(内部の遷移ではなく)叩いてしまうと、当然Not Foundになる。
わざわざテンプレートをRouterにまかせて管理しているのに、同じようなHTMLを大量に生産するのも無駄過ぎるし、一般的なプロラマなら違う方法があると考える。
connect-rewrite
grunt-pluginではないが、URLに対して特定コンテンツを返す様に設定できるnode-connectのプラグインがある。これをgrunt-contrib-connectのmiddlewareとして利用してやればいい。
/api
, /css
, /js
, /tmpl
, /fonts
, /img
のHTTP-PATHを除くリクエストをすべて/index.html
へリライトしたい場合は以下の様に書く。
connect: { server: { options: { host: "localhost", port: 8000, base: "www", middleware: function (connect, options) { if (!Array.isArray(options.base)) options.base =[options.base]; directory = options.directory || options.base[options.base.length - 1]; // node-moduleを読み込み modRewrite = require("connect-modrewrite"); middlewares = []; middlewares.push(connect.directory(directory)); // modRewriteの設定 middlewares.push(modRewrite([["!/api|/css|/js|/tmpl|/fonts|/img /index.html"]); } } } }
備忘録
pushState対応してて気づいた余談。
RouterのtemplateUrlは絶対パスで記述しよう
相対パスで指定してて、/hoge
なURLでアクセスした時にテンプレートが404になって困った。
templateUrlとしてはhttp://localhost:8000/tmpl/fuga.html
を期待してても、相対パスで書かれていると、/hoge
から/fuga
に遷移しようとしたときに読みに行くURLはhttp://localhost:8000/hoge/tmpl/fuga.html
になる。
WEB屋としてはあまりに当たり前なコトなのだけど、サンプルで書かれてた相対パスをそのまま使っていた。まあ、ハッシュフラグメントでやっている限りは困らないけど。
URLの指定にHashfragment(#
)ではなくHashbang(#!
)を使いたい
$locationProvider.hashPrefix('!');
ってすれば、#!
になる。
Angular UI-Routerのviewsの定義を確認してみた
state.views
の指定方法を理解してなかったので調べた。
基本
viewの名前はビュー名とステート名を@
で結合したviewName@stateName
で構成される。
無名のview
ui-view
は名前を与えず無名のviewとして定義ができる。
<div ui-view></div>
その場合、stateは""
でviewを指定する
$stateProvider .state("example", { views: { "": "example.html" } });
viewのstateを省略した場合
viewsの定義文字列は@
でビュー名とステート名を区切る。
ビュー名に@
が含まれていない場合、ビュー名の末尾に@
と親ステートの名前^1を結合してview名する。
$stateProvider .state("example", { views: { "main": { templateUrl: "example.html" } }) .state("example.child", { views: { "main": { templateUrl: "example.child.html" } } });
このようなstatesを定義した場合、viewsは以下に変換される
views: { "main@": { templateUrl: "example.html" } }
views: { "main@example": { templaetUrl: "example.child.html" } }
viewsを省略した場合
views
は省略することができる。
$stateProvider.state("example", {templateUrl: "example.html"});
上記の場合、viewsの指定は以下に変換される。
views: { "@": { templateUrl: "example.html" } }
rootステートについて
親要素を$stateProvider
に定義しないステートもroot
という親をもっている。
root = registerState({ name: '', url: '^', views: null, 'abstract': true });
これによって、例えば@
だけをviews
に指定した場合は最上位に定義されている無名のui-view
を指定したことになる。