Typescriptを使うとAngularJSのControllerをクラスとして開発することができる。クラスとしてきれいに開発できるのでTypescriptは気に入っている。
module b2 {
class NavbarController {
constructor ($scope: ng.IScope) {
$scope.date = new Date();
}
}
angular.module('b2')
.controller('NavbarController',['$scope',NavbarController]);
}
でも、AngularJSへの登録コードは残ってしまう
AngularJSにクラスをコントローラーとして登録するコードを毎回書かなければならない。
angular.module('b2')
.controller('NavbarController',['$scope',NavbarController]);
せっかく、TypeScriptでクラスとして開発しているのに残念だ。
gulpのスクリプトでAngularJSへの登録コードを自動生成する
gulpでTypescriptをビルドするタイミングで、JavaScritpのASTを解析して、50行くらいのコードで実現できた。gulpはデータの流れが分かりやすいので、こういう処理を追加しやすい。
環境セットアップ
yo gulp-angular
を実行しTypescriptを選び、gulp & AngularJS & Typescriptのプロジェクトを作る。
npm install --save-dev through2 falafel
を実行し、JavaScriptnのASTを解析するライブラリーをインストールする。
gulp スクリプトの開発
コンパイル用のgulpのスクリプトファイルを編集していく。
'use strict';
var gulp = require('gulp');
var paths = gulp.paths;
var $ = require('gulp-load-plugins')();
gulp.task('scripts', function () {
return gulp.src(paths.src + '/{app,components}/**/*.ts')
.pipe($.typescript())
.on('error', function handleError(err) {
console.error(err.toString());
this.emit('end');
})
.pipe(gulp.dest(paths.tmp + '/serve/'))
.pipe($.size())
});
npmで取得したライブラリーを読み込む
through2とfalafelを読み込む。
'use strict';
var gulp = require('gulp');
// 追加したライブラリーを読み込む
var through = require('through2');
var falafel = require('falafel');
var paths = gulp.paths;
typescriptのコンパイル後に処理を挟み込む
angularifyメソッドをpipeで追加する。
gulp.task('scripts', function () {
return gulp.src(paths.src + '/{app,components}/**/*.ts')
.pipe($.typescript())
.on('error', function handleError(err) {
console.error(err.toString());
this.emit('end');
})
.pipe(angularify({ /* 追加 */
moduleName:'b2'
}))
.pipe(gulp.dest(paths.tmp + '/serve/'))
.pipe($.size())
});
JavaScriptにコンパイルされた後のファイルごとに処理をする
angularifyメソッドのなかでthroughを使うことでJavaScriptにコンパイルされた後のコードをファイルごとに操作することができる。
function angularify(opts) {
var stream = through.obj(function(file, enc, cb) {
if (file.isNull()) {
// do nothing
}
if (file.isBuffer()) {
// ファイルごとに中身を見て、変換することができる
var contents = file.contents.toString();
contents = transform(contents,opts.moduleName);
file.contents = new Buffer(contents.toString());
}
this.push(file);
return cb();
});
// returning the file stream
return stream;
};
JavaScriptを解析し、AngularJSへの登録処理を追加する
falafelを利用しASTに変換されたJavaScriptを解析しソースコードを変換する。
function transform(contents,moduleName){
// JavaScriptをASTに変換する
return falafel(contents, {tolerant: true}, function (node) {
registerControllerToModule(node,moduleName);
}).toString();
}
function registerControllerToModule(node,moduleName){
var decls, decl;
// Typescriptのクラスは一定のルールでJavaScriptに変換されるので、
// クラス宣言のパターンにマッチングして、コントローラーのクラス宣言に
// ひっかける
if (node.type === 'VariableDeclaration' &&
(decls = node.declarations) && decls.length === 1 &&
(decl = decls[0]) && decl.id.name.match(/.*Controller/)) {
if(decl.init.type === 'CallExpression'){
// コンストラクタのFunction
var constructor = decl.init.callee.body.body[0];
// コンストラクタの引数名
var constructorParams = constructor.params.map(function(param){
return '\''+param.name + '\'';
});
// コントローラー名
var controllerName = decl.id.name;
// コンストラクタの引数の後にFunctionを追加する
constructorParams.push(controllerName);
// AngularJSへの登録コードを組み立てる
var source = '\n ';
source += 'angular.module(\''+ moduleName +'\')';
source += '.controller(\''+controllerName+'\',['+constructorParams.join('\,')+']);';
// クラス宣言の後にAngularJSへの登録コードを追加する
node.update(node.source()+source);
}
return true;
}
return false;
}
gulp スクリプトの動作確認
AngularJSへの登録コードを削除したTypeScriptのクラスを用意する。
'use strict';
module b2 {
interface INavbarScope extends ng.IScope {
date: Date
}
class NavbarController {
constructor ($scope: INavbarScope) {
$scope.date = new Date();
}
}
}
gulp の実行後に生成されるファイルには、AngularJSへの登録コードが追加されている。
'use strict';
var b2;
(function (b2) {
var NavbarController = (function () {
function NavbarController($scope) {
$scope.date = new Date();
}
return NavbarController;
})();
angular.module('b2').controller('NavbarController',['$scope',NavbarController]);
})(b2 || (b2 = {}));
作った gulp スクリプト
'use strict';
var gulp = require('gulp');
var through = require('through2');
var falafel = require('falafel');
var paths = gulp.paths;
var $ = require('gulp-load-plugins')();
gulp.task('scripts', function () {
return gulp.src(paths.src + '/{app,components}/**/*.ts')
.pipe($.typescript())
.on('error', function handleError(err) {
console.error(err.toString());
this.emit('end');
})
.pipe(angularify({
moduleName:'b2'
}))
.pipe(gulp.dest(paths.tmp + '/serve/'))
.pipe($.size())
});
function angularify(opts) {
// creating a stream through which each file will pass
var stream = through.obj(function(file, enc, cb) {
if (file.isNull()) {
// do nothing
}
if (file.isBuffer()) {
var contents = file.contents.toString();
contents = transform(contents,opts.moduleName);
// console.log(contents);
file.contents = new Buffer(contents.toString());
}
this.push(file);
return cb();
});
// returning the file stream
return stream;
};
function transform(contents,moduleName){
return falafel(contents, {tolerant: true}, function (node) {
registerControllerToModule(node,moduleName);
}).toString();
}
function registerControllerToModule(node,moduleName){
var decls, decl;
if (node.type === 'VariableDeclaration' &&
(decls = node.declarations) && decls.length === 1 &&
(decl = decls[0]) && decl.id.name.match(/.*Controller/)) {
if(decl.init.type === 'CallExpression'){
var constructor = decl.init.callee.body.body[0];
var constructorParams = constructor.params.map(function(param){
return '\''+param.name + '\'';
});
var controllerName = decl.id.name;
constructorParams.push(controllerName);
var source = '\n ';
source += 'angular.module(\''+ moduleName +'\')';
source += '.controller(\''+controllerName+'\',['+constructorParams.join('\,')+']);';
node.update(node.source()+source);
}
return true;
}
return false;
}