node-webkitで作ったアプリをTypeScript化,CoffeeScript化してみての雑感

素のJavaScriptで書いて出来上がったnode-webkitアプリを今後の参考とするためにCoffeeScript化,TYpeScript化。
ライブラリ以外で書いたJSコードは300行程度と、かなり小規模なアプリ。*1
bowerで入れたのがBootstrap,Bootstrap v3 datetimepicker,jQuery,Moment.js,Vue.js。
npmで入れたのがjsftp(grunt周りを除く)。

JSからCoffeeScriptへの作業は3時間ぐらい。
JSからTypeScriptへの作業は型定義ファイルを書くのに不慣れだったため2日半ほど。

こちらと大体同じような感想。
中、大規模ならTypeScript,小中規模ならCofeeScriptを使うとよさ気。

TypeScript化は型定義ファイルを書くのにほとんど時間を書けた感じ。
書いた型定義ファイルは自前の外部モジュール2つとjsftp、node-webkit等のこのアプリを動かすのに必要最低限度の部分だけ。
正直、型定義ファイルを書くのは結構面倒だった。
規模が小さいので、書くコストに見合うご利益を感じられなかったからだと思う。
noImplicitAnyオプションを付けなければもっと早く終わったと思うが、それだとTypeScriptのメリットがいきないと感じた。

*1:なお、お客さんへの納品物なのでソースの公開はなし。

node-webkitでTypeScriptを使ってはまるケース

node-webkitJavaScript Contextでのrequireでのパス解決でハマる場合がある。

例えばこんなファイル構成で

  • index.html
  • js
    • index.js(index.ts)
    • fileUploader.js

各ファイルがこんな感じ。

<!-- index.html -->
<!DOCTYPE html>
<html>
  <head >
    <meta charset="utf-8" />
    .................
    <script src="js/index.js"></script>
  </body>
</html>
//js/index.js
..................
var fileUploader = require('./js/fileUploader');
.................
//↑node-webkit的に問題なし.

//index.ts
import fileUploader = require('./js/fileUploader');
//↑index.tsからだとrequire('./fileUploader')なのでエラー。

↑node-webkitJavaScript Context(いわゆるブラウザの世界)でrequireをした場合、JavaScriptファイルからのパスではなく、そのJavaScriptを読み込んでいるhtmlからのパスになる。

参考:

回避策1

その場しのぎ的。requireをindex.htmlのインラインに持ってくる。

<!-- index.html -->
<!DOCTYPE html>
<html>
  <head >
    <meta charset="utf-8" />
    .................
    <script>
      //..................
       var fileUploader = require('./js/fileUploader');
      //................. 
    </script>
    <script src="
  </body>
</html>

回避策2

構造を変える

  • index.html
  • index.js(index.ts)
  • js
    • fileUploader.js
<!-- index.html -->
<!DOCTYPE html>
<html>
  <head >
    <meta charset="utf-8" />
    .................
    <script src="index.js"></script>
  </body>
</html>

うーむ。

requireしてnewして使用するnpmのパッケージにTypeScript用の型定義ファイルを書く

こういうの。

///<reference path='./typing/node.d.ts' />
///<reference path='./typing/jsftp.d.ts' />

import JSFtp = require('jsftp');

..........................
   var Ftp = new JSFtp({
      host: connect.host,
      port: connect.port,
      user: connect.account,
      pass: connect.password
    });
..........................

jsftp用に書いた型定義ファイル(簡略)

//  ./typing/jsftp.d.ts

/// <reference path="./node.d.ts" />

interface JSFtpOption {
  host?: string;
  port?: number;
  user?: string;
  pass?: string;
  ....................
}

declare module 'jsftp' {

  class Ftp {
    constructor(cfg?: JSFtpOption);
    put(from: string, to: string, callback?: (err: NodeJS.ErrnoException) => void): void;
    .......
  }

export = Ftp;
}

参考

declare module 'jsftp' {

  class Ftp {
    constructor(cfg?: {host?:string;port?:number;user?:string;pass?: string});

    put(from: string, to: string, callback?: (err: NodeJS.ErrnoException) => void): void;
    ...........
  }

export = Ftp;
}

オブジェクト型リテラルにすれば重複しないけど。 うーむ。

追記

おー、なるほど。

declare module JSFTP {
  export interface JSFtpOption {
    host?: string;
    port?: number;
    user?: string;
    pass?: string;
    .......................
  }
}

declare module 'jsftp' {

  class Ftp {
    constructor(cfg?: JSFTP.JSFtpOption);

    put(from: string, to: string, callback?: (err: NodeJS.ErrnoException) => void): void;
    .............................................
  }

export = Ftp;
}

このJSFTPモジュールは非インスタンス化モジュールで、型定義のネームスペースにしか存在しないので、変数空間を汚さないと。
ネームスペースを噛ませているので重複しにくくなりました。
勉強になりました。
ありがとうございます。

更に追記

あー・・・。TypeScriptリファレンス Ver.1.0対応の6-4-4インスタンス化・非インスタンス化モジュールに書いてあった。
読破したはずなのに覚えてなかった・・・。

*1:module.exports=Ftp

node-webkitでNode contextからGUIのAPIを使う

メニュー出したりとかデスクトップアプリ特有のネイティブのアレをNode側から使いたい。

1.require時に引数として渡す

//index.js

var gui = require('nw.gui');
//↓コレ
var fileUploader = require('fileUploader')(gui);

例なので全部渡しちゃってるけど必要な項目だけに絞って渡した方がいいと思う。

2.window.require('nw.gui')する

//fileUploader.js

var fs = require('fs');

var gui = window.require('nw.gui');

Window.globalとかGlobal.windowとかになっている話はこちらを参照

node-webkitの場合、windowにもrequireメソッドがあって、実装はというとこんな

function (name) {  if (name == 'nw.gui')    return nwDispatcher.requireNwGui();  return global.require(name);}

見てそのまま。
nw.guiはwindowのrequireを呼ばないといけないと。

contextが混在しているのはささっと呼べて便利といえば便利だけど、サンドボックス機構があったほうがメンテナンス的には幸せなのかもしれない。
以上。

参考

grunt-slim

メモ
Gruntfile.coffee

  ......................................
    slim:
      pretty:
        options:
          pretty: true
        files: [
          expand: true
          cwd: 'src'
          src: ['{,*/}*.slim']
          dest: 'app'
          ext: '.html'
        ]
  ......................................
  grunt.loadNpmTasks('grunt-slim')

圧縮されるのはアレなのでpretty: trueにした。

参考:grunt-slim

gruntで任意の場所でnpm i --productionする

node-webkitで作成したアプリのパッケージングはappディレクトリを対象として実行してて、npmをcd app してからnpm i --productionと手動で処理していたので、Gruntに登録した。

Gruntfile.coffeeの該当箇所

  ......
    exec:
      npm_i_production:
        cwd: 'app'
        cmd: 'npm i --production'
  .....
  grunt.loadNpmTasks('grunt-exec')
  grunt.registerTask 'default', ['slim', 'concat', 'copy', 'exec','nodewebkit']

OK.

参考