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-webkitのJavaScript 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-webkitのJavaScript 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; }
参考
- https://github.com/borisyankov/DefinitelyTyped/blob/master/tspromise/tspromise.d.ts
これだとJSFTPOptionが他の型定義ファイルと重複してしまう可能性がないわけではない。
ちなみにexport = Ftpとしてやる必要がある*1ので、JSFTPOptionをmodule 'jsftp'の中に入れてexport JSFTPOptionとは出来ない。中に入れてexport付けないとコンパイル時にprivateだと怒られるし。
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; }
オブジェクト型リテラルにすれば重複しないけど。 うーむ。
追記
@katahirado http://t.co/fQSZ6Qr7gU コレのmodule NodeJS みたいな感じで JSFTP専用モジュールに押し込んでしまうと重複しにくくて良いと思いますよ。 http://t.co/Lpl1CwFWfE
— わかめ@TypeScriptカッコガチ (@vvakame) 2014, 6月 16
おー、なるほど。
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モジュールは非インスタンス化モジュールで、型定義のネームスペースにしか存在しないので、変数空間を汚さないと。
ネームスペースを噛ませているので重複しにくくなりました。
勉強になりました。
ありがとうございます。
更に追記
@vvakame ありがとうございます。非インスタンス化モジュール…。なるほど、そんな方法が。
— Yuichi Katahira (@katahirado) 2014, 6月 16
あー・・・。TypeScriptリファレンス Ver.1.0対応の6-4-4インスタンス化・非インスタンス化モジュールに書いてあった。
読破したはずなのに覚えてなかった・・・。
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
bower_componentsをディレクトリ別の場所に
.bowerrcに記述
{ "directory": "app/bower_components" }
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.
参考