リズムのじかん

javascript、typescriptなど中心に書きます。

node.jsアプリのデプロイをCapistrano3で自動化する

Capistranoまだよく分かってないけど、ひとまずワンパス通ったのでメモです。 あまり自信はありませんが、ご参考になれば幸いです。

capistranoはv3.2.1を使っています。

やりたいこと

node.jsアプリケーションのデプロイ作業を自動化したい。

サーバ環境

デプロイの流れ

  1. [ローカル]デプロイ資産をまとめる(js,cssファイルのminifyなど)
  2. [ローカル]まとめたデプロイ資産をデプロイ用リポジトリへpush
  3. [リモート]デプロイ用リポジトリからpull
  4. [リモート]npm install --production
  5. [リモート]アプリケーション再起動

1はgruntでタスクを組んでいます。

3〜5はcapistranoで自動化しています。

1,2,3〜5の各処理を呼び出すメインのshellscriptを作り、デプロイ時にはメインのshellscriptを実行するだけにしています。

Capistranoのセットアップ

上記のデプロイの自動化を実現するため、もろもろセットアップします。

さくらVPSにnodeとかインストールする

アプリケーションを実行するのに必要な各種ミドルウェアを、さくらVPSにインストールします。 また今回はforeverでnodeを実行する前提です。

  • nvmインストール
  • nodeインストール
  • foreverインストール
  • mongodbインストール

etc...

詳細は省略。 ググればたくさん良い記事があるので、そちらを参照してください。

ローカルからさくらVPSへ鍵認証で接続できるようにする

人に説明できるほど詳しくないので、詳細はググってください。 ここではsakura_rsaという名前で鍵を作っています。 以下のように.ssh/configを書いて、ssh sakuraでパスワードなしで接続できるようにしています。

[ローカル] ~/.ssh/config

Host sakura
 Hostname 192.168.222.222
 Port 9999
 IdentityFile ~/.ssh/sakura_rsa
 User qop
 Protocol 2

デプロイ用リポジトリへ鍵認証で接続できるようにする

デプロイ用リポジトリにローカルとリモートから鍵認証で接続できるようにします。 手順の詳細はググってください。

f:id:chords:20141108195239p:plain

自分はbitbucketを使っているので、その例を上げます。 鍵の名前は「id_rsa」です。

[ローカル] ~/.ssh/config

Host bitbucket
 HostName bitbucket.org
 IdentityFile ~/.ssh/id_rsa
 User git
 Port 22
 TCPKeepAlive yes
 IdentitiesOnly yes

[リモート] さくらVPSからのssh接続は以下が分かり易いです。

http://www.xn--vps-073b3a72a.com/6.html

Capistrano3のインストール

Capistrano3をインストールします。 以下は自分のGemFileです。(バージョン指定してないけど)

source "https://rubygems.org"

gem "capistrano"
gem "capistrano_colors"

Capistrano3でデプロイタスクをコーディング

config/deploy/production.rbに、本番環境の設定を記載。

server '192.168.222.222',
  user: 'qop',
  roles: %w[app web db],
  ssh_options: {
    keys: [File.expand_path('~/.ssh/sakura_rsa')],
    port: 9999,
    forward_agent: true,
    auth_methods: %w(publickey)
  }

config/deploy.rbに、デプロイタスクを記載。

lock '3.2.1'

set :application, 'app_name'
set :repo_url, 'git@bitbucket.org:account_name/repository_name.git'
set :deploy_to, '/remote/deploy/path'

# User
set :user, 'qop'

# Default value for default_env is {}
set :node_env, (fetch(:node_env) || fetch(:stage))
set :default_env, { node_env: fetch(:node_env) }
  
# Default value for keep_releases is 5
set :keep_releases, 5

set :pid_file_name, 'xxxxx.pid'
set :pid_file, "/home/#{fetch(:user)}/.forever/pids/#{fetch(:pid_file_name)}"

namespace :deploy do

  desc 'Install node modules non-globally'
  task :npm_install do
    on roles(:app) do
      execute "cd #{release_path} && npm install --production"
    end
  end
 
  desc 'Start application'
  task :start do
    on roles(:app) do
      within current_path do
        execute :forever, 'start', '--pidFile', fetch(:pid_file), "#{current_path}/app.js"
      end
    end
  end
 
  desc 'Stop application'
  task :stop do
    on roles(:app) do
      within current_path do
        execute :forever, 'stop', "#{current_path}/app.js"
      end
    end
  end
 
  desc 'Restart application'
  task :restart do
    on roles(:app), in: :sequence, wait: 5 do
      within current_path do
        if test("[ -e #{fetch(:pid_file)} ]") && execute("kill -0 `cat #{fetch(:pid_file)}` > /dev/null 2>&1")
          execute :forever, 'restart', '--pidFile', fetch(:pid_file), "#{current_path}/app.js"
        else
          execute :forever, 'start', '--pidFile', fetch(:pid_file), "#{current_path}/app.js"
        end
      end
    end
  end
 
  after :publishing, :restart
 
  before :restart, 'deploy:npm_install'
 
end

デプロイ

bundle exec cap production deployで無事デプロイできました。

webstormでjs-beautifyを使ってコードをフォーマットする

あらすじ

webstormのフォーマッタは自分的には貧弱な気がしてました。 だってメソッドチェーンを自動改行してくれない。

sublimeだとjs-beautifyがいい感じにしてくれるのに。。

webstormのpluginにはそれっぽいのはありません。 gruntだったらできるけど、オーバーヘッドかかるし。

それならfile watcherでやったらできるかも。=>できました!

file watcherの設定

※下に追記していますが、file watcherよりexternal toolでやったほうが良い気がします。

js-beautifyをインストールする

npmでインストールします。 js-beautifyはグローバルインストール推奨だそうです。

npm install js-beautify -g

file watcherの設定

こんな感じです。

f:id:chords:20141018013146p:plain

  • typescriptでコーディングしているので、File Typeがtypescriptになっていますが、必要に応じてjavascriptなどに変えてください。
  • immediate file syncronizationのチェックを外すと、ファイル保存時に本タスクが実行されます。
  • js-beautifyのオプションはjs-beautifyを参考にしてください。
  • jsbeautify.jsonはこんな感じです。jsbeautify.jsonはプロジェクトルート直下に置いてます。
{
    "indent_size": 2,
    "indent_char": " ",
    "indent_level": 0,
    "indent_with_tabs": false,
    "preserve_newlines": true,
    "max_preserve_newlines": 10,
    "jslint_happy": false,
    "space_after_anon_function": false,
    "brace_style": "collapse",
    "keep_array_indentation": false,
    "keep_function_indentation": false,
    "space_before_conditional": true,
    "break_chained_methods": true,
    "eval_code": false,
    "unescape_strings": false,
    "wrap_line_length": 0
}

これで快適!

(追記) external toolの設定

file watcherよりexternal toolでやったほうが良い気がします。 file watcherだと、js-beautifyがファイルを上書きしたタイミングでもう一度file watcherが走るので。。

js-beautifyをインストールする

上に同じです。

external toolの設定

external toolにjs-beautifyを追加します。

f:id:chords:20141018124640p:plain

これで右クリックメニュー等に追加されます。

f:id:chords:20141018124726p:plain

ショートカットキーの設定

keymapでショートカットキーを設定します。

f:id:chords:20141018124804p:plain

これで今度こそ快適!

Yosemiteでwebstormが起動しない

早速macmacbook air Late 2010)をYosemiteにアップデートしました!!
長い長いインストールが終わって、webstormを起動すると、以下のメッセージが出て起動できません。

f:id:chords:20141017133915p:plain

Java6がないんですね。
対処方法は簡単。「More Info」をクリックします。
以下のページが表示されるので、ダウンロードしてインストールしてください。

f:id:chords:20141017134836p:plain

起動しました!!

ちなみに自分の場合、Yosemiteにアップデートすると昔インストールしてたjavaが消えてました。
javaをインストールするには、terminalでjavaと入力すると自動でoracleのサイトが開くので、そこからJDKをインストールすればOKです。

オブジェクトをシリアライズする

業務でやったことのメモ。javaです。

やりたいこと

  • オブジェクトをシリアライズしてクライアントへ送信する。
  • クライアントへ送信するのでアウトプットはString型。
import java.io.*;
import org.apache.commons.codec.binary.Base64;

public class Utils {
  /**
   * オブジェクトをシリアライズします。
   * 
   * @param target
   * @return
   * @throws IOException
   */
  public static String serialize(Object target) throws IOException {
    ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
    ObjectOutputStream outputStream = new ObjectOutputStream(byteStream);
    outputStream.writeObject(target); // オブジェクトをシリアライズ
    outputStream.close();
    byteStream.close();
    byte[] byteArray = byteStream.toByteArray();
    // byte[] byteArray = encript(byteStream.toByteArray()); <= 暗号化が必要な場合
    return Base64.encodeBase64String(byteArray);
  }

  /**
   * デシリアライズします。
   * 
   * @param target
   * @return
   * @throws IOException
   * @throws ClassNotFoundException
   */
  public static Object deserialize(String target) throws IOException,
      ClassNotFoundException {
    byte[] byteArray = Base64.decodeBase64(target);
    // byte[] byteArray = decript(Base64.decodeBase64(target)); <= 復号化が必要な場合
    ByteArrayInputStream byteStream = new ByteArrayInputStream(byteArray);
    ObjectInputStream inputStream = new ObjectInputStream(byteStream);
    return inputStream.readObject();
  }
}

暗号化が必要な場合はcipherなど使って別途実装してください。

typescript初心者が躓いたこと

typescriptを始めて1ヶ月くらい経ちました。
そんなtypescript初心者がはまったところをまとめます。

typescriptを始めた理由

趣味でwebサービスを作っていて、言語はjavascriptなのですが、昼間の本業が忙しいと1週間とか1ヶ月とか間が空いてしまいます。
そうすると前に何をコーディングしていたか忘れてしまう。。。
このオブジェクトはどんな関数持ってたっけ?とか、この関数の戻り値の型はなんだっけ?とか。
javascriptに型チェックはないですが、型は意識してコーディングしている)
そしてやる気を無くしてしまう。。

そこで、
型を付けてコードの自動補完とかしてくれたら、どうにかなりそう(・∀・)
と始めたのがtypescriptです。
他のラッパー言語も考えましたが、coffeescriptは型チェックないし、haxeは気になるけどjQueryとかの外部ライブラリはどうするんだろう?、dartは何か嫌だwという感じでtypescriptにしました。

躓いたところ

Q1. 即時関数どう書くの?

A. いちおうそのまま書けます。

とりあえずtypescript始めようってことで、既存のjavascriptソースの拡張子をtsに置き換えて、typescriptっぽく型を付けようとしたのですが、早速何をどう書いていいかわからない。
javascriptで書いたほうが早いんじゃないか?、、と思いながら格闘します。

最初に躓いたのが即時関数はtypescriptでどう書くの?

//javascript
var isReady = (function(user){
if(!user.logined()){
  return false;
}
_.each(user.scores, function(score){
  if(score < 0){
    return false;
  }
});
return true;
})(currentUser);

いちおうそのまま書けます。

//typescript
var isReady = ((user:User):boolean => {
if(!user.logined()){
  return false;
}
_.each<number, boolean>(user.scores,(score) => {
  if(score < 0){
    return false;
  }
});
return true;
})(currentUser);

または、typescriptなら名前付き関数として定義して呼び出したほうが自然な気がします←好みレベルの問題と思われる。

// typescript
private isReady(user:User):boolean  {
if(!user.logined()){
  return false;
}
_.each<number, boolean>(user.scores,(score) => {
  if(score < 0){
    return false;
  }
});
return true;
}

twitterでコメントいただきましたが、ラムダ式()=>{ }の部分はfunction(){ }とも書けます。
このときthisの意味合いが異なるので使い分けが必要です。以下が参考になります。
http://phyzkit.net/typescript/#chapter3_section9

Q2. 型はどこまで書けばいいの?

A. 右辺の型が明確であれば、左辺の型付けは不要。

最初は以下のように一生懸命型を書いてました。

// javascript
var d = Q.defer();
// typescript
var d:Q.Deferred<User> = Q.defer<User>();

javascriptより書く量多い、、つらい、、と思ってましたが、結論としては右辺の型が決まっていれば左辺の型は書かなくてOKです。

var d = Q.defer<User>();

これなら我慢できる。よく考えたらC#型推論と同じですね、C#の人が作った言語ですし。

ただしwebstormを使っていると、右辺を型変換した場合に入力補完されないことがあるようです。

// コンパイルは通るが、webstormで入力補完してくれない。visualstudioは知らない。
var Author = <IAuthor>mongoose.model('Author', AuthorSchema);
// 以下のように書くと、webstormで入力補完してくれます。
var Author:IAuthor = <IAuthor>mongoose.model('Author', AuthorSchema);

Q3. import, export, module, declare, referenceの使い分けがよくわからない。。

A. とりあえず自己流で使い分け。

typescriptのimport, export, module, declare, referenceなどは、公式のハンドブックを見ても、結局どれ使えばいいの?という感じでよくわかりません。(declareとかは明確ですが)
いろいろ試行錯誤して個人的には以下で納得しています。

declare

外部ライブラリ(jQueryなど)に型付けを付け加えるときに使う。TSDを使えばよいので、個人で書くことはない。

reference

グローバルで読み込むスクリプトに対して使うといい感じ。
クライアントサイドだど、htmlで読み込んだスクリプト対して使う。

<script src="/bower_components/requirejs/require.js"></script>

サーバーサイドだと、nodeや、グローバルに読み込ませたスクリプトに対して使う。

global._ = require('underscore')
module

使っていない。業務ロジックと関係のないFramework層の何かを作るときに使うはずだと勝手に思っている。

import, export

クラスのimportとexportに使う。
1ファイル1クラス構成にして、export = [クラス名] と書くと簡単。
もちろんクラス以外もexportするが、1ファイル1エクスポートが簡単。

// サンプル:Author.ts
import Score = require('./Score');
class Author {
  constructor(public scores: Score[]) {
     // initialize
  }
  public sum():number {
    // scores loop
    return 0;
  }
}
export = Author

※export XXX, export YYYと複数エクスポートするのは、使わなくて良いと思う。(moduleを書くときには使うと思われる)

Q4. インタフェースをimportするのは無駄じゃない?

A. コンパイル後のjsでは、不要なスクリプトの読み込みは発生しません。

typescriptで型付けのためにクラスやインタフェースをimportしますが、jsでは必ずしもそのスクリプトを読み込む必要はありません。例えば、

  • インタフェースは実装を持たないため、読み込む必要がない。
  • クラスは、コンストラクタを呼び出す場合は読み込んでnewしますが、インスタンスの関数を呼び出すだけれであれば読み込む必要はない。

無駄なスクリプトの読み込みはしたくないけど、importしないとコンパイル通らないし、、と最初ものすごく悩みましたが、コンパイラは不要なスクリプトの読み込み処理を生成しないようです。いい感じですね。
Q3のtsファイルをコンパイルすると以下のjsファイルになります。(commonjs形式でコンパイルした場合)

// Author.js
//var Score = require('./Score'); <= この行は生成されない
var Author = (function () {
    function Author(scores) {
        this.scores = scores;
    }
    Author.prototype.sum = function () {
        return 0;
    };
    return Author;
})();
module.exports = Author;

Q5. クラスじゃなくて関数をexportしたいんだけど。。

A. クラスのstaticメソッドにするのが簡単。

関数をエクスポートしたい場合、最初はハンドブックにあるように、

// draw.ts
export function draw() {
  // 処理
}

と書いていました。この関数を使う側は以下のようになります。

import draw = require('./draw');
// draw(); <=これはNG。
draw.draw() //これはOK。

draw.tsは関数自体をexportするわけではなく、ひとつオブジェクトをはさみます。
それならファイル名をそれっぽい名前にして、読み込む変数もそれっぽい名前にして、、、でも何か違う気がする。。
いろいろ悩みましたが、個人的にはclassのstaticメソッドとして定義するのがよいと思います。
javaとか.netっぽい書き方になります。

// Score.ts
class Score {
  constructor(public value:number) {
     // initialize
  }
  public static draw(){
     // 処理
  }
}
export = Score;

使う側は以下のようになります。

import Score = require('./Score');
Score.draw();

他にもいろいろ学んだことがありますが、個別の記事で書いていきます。

ブラウザにPDFプラグインがインストールされているか判定するjavascript

以下のプログラムはきちんと検証できていません。自分用のメモです。

ブラウザにPDFプラグインがインストールされているか判定するスクリプトを、業務で止むに止まれず作った。
最終的には、業務調整して使わずに済んだので良かった。
UserAgentの判定よりも危うさがあるし。

var hasPdfPlugin = (function(){
  // for modern browser
  if(window.navigator){
    var plugins = window.navigator.plugins;
    for(var i = 0; i < plugins.length; i++){
      if(plugins[i].match(/PDF/i)){
        return true;
      }
    }
  }
  // for old versions of IE
  return !!(new ActiveXObject('AcroPDF.PDF') || new ActiveXObject('PDF.PdfCtrl'));
})();

mongoose + typescript

MVCのmodel

早速話は脱線します。
以下は個人的に違和感があります。あくまでも個人的な思いです。

  • railsの(scaffoldで作成される)model
  • mongooseのmodel
  • .netのEntityFrameworkをmodelと呼ぶこと

個人的に、論理モデルを"model"と呼ぶとしっくりきます。関数従属図の各要素が"model"のイメージです。
上に書いたものは、物理モデルを"model"と呼んでおり、個人的にしっくりきません。

関数従属図のサンプル
f:id:chords:20141008220703p:plain

ある業務をシステム化する場合、
データベースの正規化の違いにより物理モデルは変わりますが、論理モデルは変わりません。

物理モデルの部分は、DAOとかdbとかdataとかdocumentとか呼んで欲しいです。
.netならそのままEntityFrameworkとか呼んで欲しいです。

mongoose + typescript

概要

本題です。以下やりたいこと。

  • mongodbにAuthorコレクションが存在
  • mongodbのODMであるmongooseを用いてデータを操作する
  • データ操作用の独自のメソッドを追加する
  • Authorモデルを定義する

mongooseは物理モデルを"model"と読んでるので(個人的に)しっくりきません。
しかしながら、typescriptのmongooseの型定義では「Model」となっているので、
それを無理やりDAOとか呼ぶのも気が引けます。
仕方なく以下のサンプルでは、物理モデルをDocumentModelとし、論理モデルをmodelとしました。

フォルダ構造

modelのAuthorに業務上必要なものを実装します。
db配下にはデータベースの操作に必要なものを実装します。

├── db
│   ├── AuthorDocumentModel.ts
│   ├── IAuthorDocument.ts
│   ├── IAuthorDocumentModel.ts
│   └── db.ts
└── model
    └── Author.ts

Author.ts(論理モデル)

/// <reference path="../typings/tsd.d.ts" />
 
import db = require('../db/db');
import IAuthorDocument = require('../db/IAuthorDocument');
 
class Author {
  private _author:IAuthorDocument;
 
  constructor(author:IAuthorDocument) {
    this._author = author;
  }
 
  get name():string {
    return this._author.name;
  }
 
  get isValid():boolean {
    return !!this._author;
  }
 
  public static createNewAuthor = (name:string, email:string, callback:(err:any, author:Author)=>void) => {
    db.Author.createNewAuthor(name, email, (err:any, author:IAuthorDocument)=> {
      callback(err, new Author(author));
    });
  };
 
  public static getById = (authorId:string, callback:(err:any, author?:IAuthorDocument)=>void) => {
    db.Author.findById(authorId, (err:any, author:IAuthorDocument) => {
      if(err) {
        return callback(err);
      }
      if(!author) {
        return callback(new Error('not found.'), author);
      }
      callback(null, author);
    });
  };
}
 
export = Author;

AuthorDocumentModel.ts(物理モデル)

/// <reference path="../typings/tsd.d.ts" />
 
import mongoose = require('mongoose');
import IAuthorDocument = require('IAuthorDocument');
import IAuthorDocumentModel = require('IAuthorDocumentModel');
 
var AuthorSchema:mongoose.Schema = new mongoose.Schema({
  email: String,
  name: {type: String, required: true, index: {unique: true}},
  created: { type: Date, default: Date.now },
  updated: { type: Date, default: Date.now }
});
 
AuthorSchema.static('createNewAuthor',
  (name:string, email:string, callback:(err:any, result:IAuthorDocument)=>void) => {
    AuthorDocumentModel.findByName(name, (err:any, author:IAuthorDocument) => {
      if(err) {
        return callback(err, null);
      }
      if(author) {
        return callback(new Error('already exists.'), author);
      }
      author = <IAuthorDocument>new AuthorDocumentModel({name: name, email: email});
      author.save(callback);
    });
  });
 
AuthorSchema.static('findByName', (name:string, callback:(err:any, result:IAuthorDocument)=>void)=> {
  AuthorDocumentModel.findOne({name: name}, callback);
});
 
var AuthorDocumentModel:IAuthorDocumentModel = <IAuthorDocumentModel>mongoose.model('Author', AuthorSchema);
 
export = AuthorDocumentModel;

IAuthorDocument.ts(インタフェース)

/// <reference path="../typings/tsd.d.ts" />
 
import mongoose = require('mongoose');
 
interface IAuthorDocument extends mongoose.Document {
  email: string;
  name: string;
  created: Date;
  updated: Date;
}
 
export = IAuthorDocument;

IAuthorDocumentModel.ts(インタフェース)

/// <reference path="../typings/tsd.d.ts" />
 
import mongoose = require('mongoose');
import IAuthorDocument = require('IAuthorDocument');
 
interface IAuthorDocumentModel extends mongoose.Model<IAuthorDocument> {
  findByName:(name:string, callback:(err:any, author:IAuthorDocument)=>void)=>void;
  createNewAuthor:(name:string, email:string, callback:(err:any, result:IAuthorDocument)=>void)=>void;
}
 
export = IAuthorDocumentModel;

db.ts

/// <reference path="../typings/tsd.d.ts" />
 
import mongoose = require('mongoose');
import AuthorDocumentModel = require('./AuthorDocumentModel');
import IAuthorDocumentModel = require('./IAuthorDocumentModel');
 
class db {
 
  public static connect(db:string) {
    mongoose.connect('mongodb://' + db);
  }
 
  public static debug(debug:any) {
    mongoose.set('debug', debug);
  }
 
  static get Author():IAuthorDocumentModel {
    return <IAuthorDocumentModel>AuthorDocumentModel;
  }
}
 
export = db;