Cordova+ionic+TypeScript+AngularJSでアプリ作成
以下の記事(「はじめに」以降)は1年以上前のものなので、結構内容が古いです。別途、「ionic + TypeScriptで今風な開発フローを実現」というシリーズの投稿を書いたので、そちらの方が内容が新しいものとなっています。気が向いたら覗いてみてください。
- ionic + TypeScript で今風な開発フローを実現【導入編】
- ionic + TypeScript で今風な開発フローを実現【型定義ファイル活用編】
- ionic + TypeScript で今風な開発フローを実現【module, concat編】
- ionic + TypeScript で今風な開発フローを実現【sourcemap編】
- ionic + TypeScript で今風な開発フローを実現【TypeScript編】
はじめに
Cordova + Ionic + TypeScript + AngularJSで「The Ionic Book」のTODOアプリを作成します。お気づきの点等ありましたらコメントにてご指摘いただければと思います。文書の整形は比較的雑です。(後日もっと綺麗にします ^^;;)
環境準備編
ionicプロジェクトの準備
# todoアプリの空プロジェクト作成
ionic start todo blank
# todoディレクトリに移動
cd todo
# iosプラットフォーム追加
ionic platform add ios
# デバッグとかなにかと便利なブラウザプラットフォーム追加
ionic platform add browser
# 依存npmパッケージインストール
npm install
# 依存bowerパッケージインストール
bower install
gulpfileの前整備
# gulpタスク管理ディレクトリとstylus管理ディレクトリ作成
mkdir gulp
gulpディレクトリ内のgulpファイルの読み込みをgulpfile.jsに追加。以後のタスクは全部この中にカテゴリ別に管理する。
var fs = require('fs');
fs.readdirSync(__dirname + '/gulp').forEach(function (task) {
require('./gulp/' + task)
});
CSS関連タスクの整備
# stylusはstylディレクトリに保存
mkdir styl
# stylus用
npm install --save gulp-stylus
CSS用gulpタスク作成
var gulp = require('gulp');
var sass = require('gulp-sass');
var stylus = require('gulp-stylus');
var minifyCss = require('gulp-minify-css');
var rename = require('gulp-rename');
gulp.task('styl', function (done) {
gulp.src('styl/**/*.styl')
.pipe(stylus())
.pipe(gulp.dest('www/css'))
.pipe(minifyCss({
keepSpecialComments: 0
}))
.pipe(rename({ extname: '.min.css' }))
.pipe(gulp.dest('./www/css/'))
.on('end', done);
});
gulp.task('sass', function(done) {
gulp.src('./scss/ionic.app.scss')
.pipe(sass())
.pipe(gulp.dest('./www/css/'))
.pipe(minifyCss({
keepSpecialComments: 0
}))
.pipe(rename({ extname: '.min.css' }))
.pipe(gulp.dest('./www/css/'))
.on('end', done);
});
gulp.task('css', ['sass', 'styl']);
gulp.task('watch:css', function () {
gulp.watch('styl/**/*.styl', ['styl']);
gulp.watch('scss/**/*.scss', ['sass']);
});
JavaScript関連
TypeScriptファイル作成
# AngularJS系のスクリプトは全部このディレクトリで管理する
mkdir ng
まずはwww/js/app.jsをそのまんまng/module.tsとして移動&リネームします。
// Ionic Starter App
// angular.module is a global place for creating, registering and retrieving Angular modules
// 'starter' is the name of this angular module example (also set in a <body> attribute in index.html)
// the 2nd parameter is an array of 'requires'
angular.module('starter', ['ionic'])
.run(function($ionicPlatform) {
$ionicPlatform.ready(function() {
// Hide the accessory bar by default (remove this to show the accessory bar above the keyboard
// for form inputs)
if(window.cordova && window.cordova.plugins.Keyboard) {
cordova.plugins.Keyboard.hideKeyboardAccessoryBar(true);
}
if(window.StatusBar) {
StatusBar.styleDefault();
}
});
})
そしてTypeScriptのコンパイラに怒られます。(筆者はWebStormを使っているのでTypeScriptのコンパイルはFileWatcherプラグインで行っています。Gulpでやってもいいのですが、FileWatcherはコンパイルエラーとかすぐにWebStorm上に出してくれるからそっちを使っている次第です。)
/usr/local/bin/tsc --sourcemap /Users/hoge/Projects/cordova/todo/ng/module.ts --target ES5
/Users/hoge/Projects/cordova/todo/ng/module.ts(6,1): error TS2304: Cannot find name 'angular'.
/Users/hoge/Projects/cordova/todo/ng/module.ts(12,17): error TS2339: Property 'cordova' does not exist on type 'Window'.
/Users/hoge/Projects/cordova/todo/ng/module.ts(12,35): error TS2339: Property 'cordova' does not exist on type 'Window'.
/Users/hoge/Projects/cordova/todo/ng/module.ts(13,9): error TS2304: Cannot find name 'cordova'.
/Users/hoge/Projects/cordova/todo/ng/module.ts(15,17): error TS2339: Property 'StatusBar' does not exist on type 'Window'.
/Users/hoge/Projects/cordova/todo/ng/module.ts(16,9): error TS2304: Cannot find name 'StatusBar'.
Process finished with exit code 2
とりあえずコンパイラの怒りを沈めるために以下のコードを追加しておく。
// TypeScriptの恩恵さよなら!
declare var angular:any;
declare var cordova:any;
declare var StatusBar:any;
interface Window {
cordova: any;
StatusBar: any;
}
gulpタスク作成
コンパイルされたjsファイルをひとまとめにしてwww/js/app.jsとして格納したいのでgulpの準備をします。まずは必要なnpmパッケージのインストール。
# ソースマップ作成用
npm install --save gulp-sourcemaps
# jsの圧縮用
npm install --save gulp-uglify
# AngularJSでいちいち依存関係を文字列で表現したくないとき用
npm install --save gulp-ng-annotate
そしてgulpタスクを書きます。ここでポイントとなるのはng/module.jsを必ず一番最初に処理させているところです。これはangular.moduleの初期化を必ず先にやっておく必要がある為、非常に重要。
var gulp = require('gulp');
var concat = require('gulp-concat');
var sourcemaps = require('gulp-sourcemaps');
var uglify = require('gulp-uglify');
var ngAnnotate = require('gulp-ng-annotate');
gulp.task('js', function () {
gulp.src(['ng/module.js', 'ng/**/*.js'])
.pipe(sourcemaps.init())
.pipe(concat('app.js'))
.pipe(ngAnnotate())
.pipe(uglify({mangle: false}))
.pipe(sourcemaps.write())
.pipe(gulp.dest('www/js'))
});
gulp.task('watch:js', ['js'], function () {
gulp.watch('ng/**/*.js', ['js'])
});
gulpfile.jsのデフォルトタスクに追加しておきます。
gulp.task('default', ['css', 'js']);
gulp実践
# defaultは省略可
gulp default
以下のファイルが作られていれば多分OK。app.jsはちゃんと圧縮されていることも確認できるはず。
- www/css/app.css
- www/css/app.min.css
- www/css/ionic.app.css
- www/css/ionic.app.min.css
- js/app.js
TypeScriptをもうちょっと整備
Cordovaの定義ファイルの依存ファイルをインストールしようとするとtsdの0.5.7版ではエラーが出て以下の問題にぶち当たります。
Installing definitions on a deeper level
ので、tsdのバージョン上げちゃってもいいよ!って人は以下の手順で0.6.0-betaに上げれば綺麗に依存関係をインストールできます。
# tsdのバージョンアップ(β版)
npm install tsd@next -g
# cordovaとcordova-ionicを依存関係付きでインストール
tsd install cordova/* --save --overwrite
tsd install cordova-ionic/* --save --overwrite
# angularとjqueryも使うのでインストール
tsd install angular --save
tsd install jquery --save
module.tsを少しだけTypeScriptっぽくする
- 定義ファイルの参照追加
- declareしてた連中を削除
- WindowインタフェースからStatusBarを削除。
以下のようになっていればOK。
/// <reference path='../typings/tsd.d.ts' />
// Ionic Starter App
// angular.module is a global place for creating, registering and retrieving Angular modules
// 'starter' is the name of this angular module example (also set in a <body> attribute in index.html)
// the 2nd parameter is an array of 'requires'
interface Window {
cordova: Cordova;
}
angular.module('starter', ['ionic'])
.run(function($ionicPlatform) {
$ionicPlatform.ready(function() {
// Hide the accessory bar by default (remove this to show the accessory bar above the keyboard
// for form inputs)
if(window.cordova && window.cordova.plugins.Keyboard) {
cordova.plugins.Keyboard.hideKeyboardAccessoryBar(true);
}
if(window.StatusBar) {
window.StatusBar.styleDefault();
}
});
})
定義ファイルも指定してあげたのでコード補完もしっかり利くようになりましたね!だんだんテンションが上がってきます。
とりあえずいったん起動
ここらへんでいったん起動してみます。
# iosエミュレータで起動!
ionic emulate ios
当たり前ですがデザインを何もいじってないので何も表示されません!
実装編
環境が整ったところでいよいよToDoアプリの実装をします。
todoアプリの枠を作成
コーディング
Starting your appと合わせる為にモジュール名を修正。
angular.module('todo', ['ionic'])
さらにそれに合わせてデザイン側も修正&ヘッダとサイドメニューを導入!いよいよionicのdirective登場!
<body ng-app="todo">
<ion-side-menus>
<!-- Center content -->
<ion-side-menu-content>
<ion-header-bar class="bar-dark">
<h1 class="title">Todo</h1>
</ion-header-bar>
<ion-content>
</ion-content>
</ion-side-menu-content>
<!-- Left menu -->
<ion-side-menu side="left">
<ion-header-bar class="bar-dark">
<h1 class="title">Projects</h1>
</ion-header-bar>
</ion-side-menu>
</ion-side-menus>
</body>
起動してみる
gulp走らせてから再びiosエミュレータで起動してみましょう。
# gulp(本来ならwatchで随時自動で更新されるようにすべき)
gulp default
# エミュレータ起動
ionic emulate ios
起動イメージがこちら↓↓↓
左が起動時の画面で、右が右にスワイプした場合にサイドメニューが出てきている状態です。とりあえずいろいろとちゃんと動いてそうですね!
todoアプリのタスク作成機能実装
では、いよいよ本格的な実装です。(Building out your appの部分を進めます)
todo一覧の表示
まずこのビューでのコントローラはTodoCtrlという名前にしたいのでng-controller属性をbodyタグに追加します。(as vm記法は好みがあると思いますがTypeScript側で$scopeを依存関係に含めずに実装できるのでなんとなく筆者は今のところ「as vm」派です。ここの実装はあくまで一例として認識いただければと思います)
<body ng-app="todo" ng-controller="TodoCtrl as vm">
そしてtodoの一覧を表示する為にコンテンツ部分はng-repeatを使ってタスクを列挙します。
<!-- Center content -->
<ion-side-menu-content>
<ion-header-bar class="bar-dark">
<h1 class="title">Todo</h1>
</ion-header-bar>
<ion-content>
<!-- our list and list items -->
<ion-list>
<ion-item ng-repeat="task in vm.tasks">
{{task.title}}
</ion-item>
</ion-list>
</ion-content>
</ion-side-menu-content>
デザイン側はこれでOK。裏方の実装を行います。
TodoCtrl.tsファイルを作成しましょう。(別ファイルにしててもgulp様がしっかり回収・圧縮してくれるのでご安心を!)
/// <reference path='../typings/tsd.d.ts' />
module todo {
"use strict";
// 一応interface作っておく
interface ITask {
title: string;
}
// コントローラ用のクラス
// ここの日本語訳多分変です...
class TodoCtrl {
// とりあえず表示用のテストデータを用意
tasks: ITask[] = [
{ title: 'コイン集め' },
{ title: 'きのこ食べる' },
{ title: '旗がとれるように身長伸ばす' },
{ title: '姫を探す' }
];
constructor() {}
}
// todoモジュールにちゃんと定義
angular.module('todo').controller('TodoCtrl', TodoCtrl);
}
ではgulpして起動してみましょう。
# gulpタスク
gulp
# iosエミュレータ起動
ionic emulate ios
いい感じです!
タスクの動的な作成
テストデータを表示しても何もおもしろくないので、自分でタスクを作れるようにしましょう。
まずはデザインの方にmodal表示用のscriptコードを書きます。これはの直後においてください。
<!-- </ion-side-menu>の直後に以下挿入!↓↓↓ -->
<script id="new-task.html" type="text/ng-template">
<div class="modal">
<!-- Modal header bar -->
<ion-header-bar class="bar-secondary">
<h1 class="title">New Task</h1>
<button class="button button-clear button-positive" ng-click="vm.closeNewTask()">Cancel</button>
</ion-header-bar>
<!-- Modal content area -->
<ion-content>
<form ng-submit="vm.createTask(task)">
<div class="list">
<label class="item item-input">
<input type="text" placeholder="What do you need to do?" ng-model="task.title">
</label>
</div>
<div class="padding">
<button type="submit" class="button button-block button-positive">Create Task</button>
</div>
</form>
</ion-content>
</div>
</script>
これがどんな動きをするためのものか、詳しくは後日またまとめます(多分)
とりあえずタスクの新規作成時に上の要素がニョーンと表示されるように準備しているのです。
次にタスクの新規作成用ボタンを配置しましょう。
<!-- Center content -->
<ion-side-menu-content>
<ion-header-bar class="bar-dark">
<h1 class="title">Todo</h1>
<!-- ↓↓↓これを新しく挿入! -->
<!-- New Task button-->
<button class="button button-icon" ng-click="vm.newTask()">
<i class="icon ion-compose"></i>
</button>
<!-- ↑↑↑これを新しく挿入! -->
</ion-header-bar>
<ion-content>
<!-- our list and list items -->
<ion-list>
<ion-item ng-repeat="task in vm.tasks">
{{task.title}}
</ion-item>
</ion-list>
</ion-content>
</ion-side-menu-content>
デザイン側はこれで完成。ということで裏方の実装をします。
インラインでポイントをコメントで記入してます。
/// <reference path='../typings/tsd.d.ts' />
module todo {
"use strict";
// 一応interface作っておく
interface ITask {
title: string;
}
// コントローラ用のクラス
class TodoCtrl {
// テストデータはもう不要なので削除
tasks: ITask[] = [];
taskModal:any;
// modal用の$ionicModalの注入とそのionicModalが$scopeを
// 必要とするので$scopeを注入
constructor(private $scope, private $ionicModal) {
// Create and load the Modal
$ionicModal.fromTemplateUrl('new-task.html', {
scope: this.$scope,
animation: 'slide-in-up'
}).then((modal) => {
// ここでthisを使いたい場合は要注意。TodoCtrlを
// 示したいので必ず()=>{}で関数を書かないといけない!
this.taskModal = modal;
});
}
// フォームのsubmit時のタスク作成
createTask(task: ITask) {
this.tasks.push({
title: task.title
});
this.taskModal.hide();
task.title = "";
}
// タスク作成のmodalを表示
newTask() {
this.taskModal.show();
}
// タスク作成modalを閉じる
closeNewTask() {
this.taskModal.hide();
}
}
// todoモジュールにちゃんと定義
angular.module('todo').controller('TodoCtrl', TodoCtrl);
}
gulpして起動しましょう。
# gulpタスク
gulp
# iosエミュレータ起動
ionic emulate ios
だいぶそれっぽくなってきましたね!さてさてそれでは次はサイドメニューの機能を追加していきましょう。
プロジェクト作成機能の実装
プロジェクトが作成できるようにボタンをコンテンツに追加します。
<!-- Center content -->
<ion-side-menu-content>
<ion-header-bar class="bar-dark">
<!-- ここを追加↓↓↓ -->
<button class="button button-icon" ng-click="vm.toggleProjects()">
<i class="icon ion-navicon"></i>
</button>
<h1 class="title">{{vm.activeProject.title}}</h1>
<!--- ここを追加↑↑↑ -->
<!-- New Task button-->
<button class="button button-icon" ng-click="vm.newTask()">
<i class="icon ion-compose"></i>
</button>
</ion-header-bar>
<!-- ここも少し変更しています↓↓↓ -->
<ion-content scroll="false">
<ion-list>
<ion-item ng-repeat="task in vm.activeProject.tasks">
{{task.title}}
</ion-item>
</ion-list>
</ion-content>
<!-- ここも少し変更しています↑↑↑ -->
</ion-side-menu-content>
そして何もなかったサイドバーには以下の機能を追加します。
- プロジェクト一覧の表示・選択
- 新規プロジェクト作成ボタン
- 選択中のプロジェクトのセル色が変わるようにのng-classには必要に応じてactiveを付加
<!-- Left menu -->
<ion-side-menu side="left">
<ion-header-bar class="bar-dark">
<h1 class="title">Projects</h1>
<button class="button button-icon ion-plus" ng-click="vm.newProject()">
</button>
</ion-header-bar>
<ion-content scroll="false">
<ion-list>
<ion-item ng-repeat="project in vm.projects" ng-click="vm.selectProject(project, $index)"
ng-class="{active: vm.activeProject == project}">
{{project.title}}
</ion-item>
</ion-list>
</ion-content>
</ion-side-menu>
デザインはこれで完了!
裏方にはプロジェクトの追加・保存・選択機能を追加しないといけないので、まずはサービスを作りましょう。(参照設定とか非効率なところありますがチュートリアルなので別ファイルに外だしするのは割愛)
/// <reference path='TodoCtrl.ts' />
/// <reference path='../typings/tsd.d.ts' />
module todo {
"use strict";
// プロジェクト用のインタフェース
export interface IProject {
title: string;
tasks: ITask[];
}
// Projectsサービスクラス
export class ProjectsSvc {
// 管理している全プロジェクト一覧を取得
all():IProject[] {
var projectString = window.localStorage['projects'];
if(!!projectString) {
return angular.fromJson(projectString);
}
return [];
}
// 管理している全プロジェクトをlocalStorageに保存
save(projects: IProject[]) {
window.localStorage['projects'] = angular.toJson(projects);
}
// 新規プロジェクトの作成
newProject(projectTitle: string): IProject {
return {
title: projectTitle,
tasks: []
};
}
// 最後に選択していたプロジェクトの索引取得
getLastActiveIndex() {
return parseInt(window.localStorage['lastActiveProject']) || 0;
}
// 最後に選択していたプロジェクトの索引保存
setLastActiveIndex(index: number) {
window.localStorage['lastActiveProject'] = index;
}
}
// Projectsサービスとして登録
angular.module('todo').factory('Projects', () => {
return new ProjectsSvc();
});
}
ではこのProjectsサービスをTodoコントローラで使います。結構修正・追加するのでまるごと載せてインラインでポイントを書きます!
/// <reference path='../typings/tsd.d.ts' />
/// <reference path='ProjectsSvc.ts' />
module todo {
"use strict";
// 一応interface作っておく
export interface ITask {
title: string;
}
// コントローラ用のクラス
class TodoCtrl {
projects: IProject[];
activeProject: IProject;
taskModal: any;
// modal用の$ionicModalの注入とそのionicModalが$scopeを
// 必要とするので$scopeを注入
constructor(private $scope, private $timeout: ng.ITimeoutService, private $ionicModal, private Projects: ProjectsSvc, private $ionicSideMenuDelegate) {
// 既存のプロジェクト読み込み又は初期化
this.projects = Projects.all();
// 最後に選択していたプロジェクトか、なければ最初のプロジェクトを選択しておく
this.activeProject = this.projects[Projects.getLastActiveIndex()];
// Create and load the Modal
$ionicModal.fromTemplateUrl('new-task.html', {
scope: this.$scope
}).then((modal) => {
// ここでthisを使いたい場合は要注意。TodoCtrlを
// 示したいので必ず()=>{}で関数を書かないといけない!
this.taskModal = modal;
});
// プロジェクトが存在しない場合は
// 作ってもらう。初期化が全て完了してから
// 実行してほしいので$timeoutで遅延実行する。
this.$timeout(() => {
if (this.projects.length > 0) {
// プロジェクトが既にあるなら無視
return;
}
while (true) {
var projectTitle = prompt('最初のプロジェクト名を指定:');
if (!!projectTitle) {
this.createProject(projectTitle);
break;
}
}
}
)
}
// プロジェクトの新規作成(非公開)
private createProject(projectTitle: string) {
var newProject = this.Projects.newProject(projectTitle);
this.projects.push(newProject);
// プロジェクト新規作成後すぐにアクティブ状態にしておきたいので設定
this.activeProject = newProject;
this.Projects.save(this.projects);
}
// プロジェクト新規作成(公開)
newProject() {
var projectTitle = prompt('Project name');
if (!!projectTitle) {
this.createProject(projectTitle);
}
}
// プロジェクトの選択
selectProject(project: IProject, index: number) {
this.activeProject = project;
this.Projects.setLastActiveIndex(index);
this.$ionicSideMenuDelegate.toggleLeft(false);
}
// フォームのsubmit時のタスク作成
createTask(task: ITask) {
if (!this.activeProject || !task) {
// タスク作成できるような状態じゃなかったら無視
return;
}
this.activeProject.tasks.push({
title: task.title
});
this.taskModal.hide();
// タスク作成するたびに前プロジェクト保存しているので
// 非効率的だけどチュートリアルなので…
this.Projects.save(this.projects);
task.title = "";
}
// タスク作成のmodalを表示
newTask() {
this.taskModal.show();
}
// タスク作成modalを閉じる
closeNewTask() {
this.taskModal.hide();
}
// サイドメニューのトグル用
toggleProjects() {
this.$ionicSideMenuDelegate.toggleLeft();
}
}
// todoモジュールにちゃんと定義
angular.module('todo').controller('TodoCtrl', TodoCtrl);
}
完成!
随分と長くなってしまいましたが、ようやく完成しました!記事を準備編と実装編に分けたほうが見やすかったような気がします。。。(反省)それでは最後に起動して動作確認しましょう。
# gulpタスク実行
gulp
# iosエミュレータ起動
ionic emulate ios
多分ちゃんと動いてくれてますね!ほっとします。
最後に
Cordova+ionic+TypeScript+AngularJSで開発してみて思ったこと。
- とりあえず楽しい
- へっぽこプログラマでもモバイル・アプリを作れるような気がする
- ionic serveを使えばブラウザでも開発できるのでデバッグもしやすいです
- TypeScriptのコード補完がやっぱりテンション上がる
- けどWebStormのコード補完がたまに追いつけていないorz
- TypeScriptでエラーが出た時のstacktraceはjavascriptの行番号なので追いかけるのが多少面倒。(そのうち便利で簡単なNodeみたいなstacktrace書き換えライブラリが出てくるはず)
ここまで読んでいただき、ありがとうございましたm(__)m
今後はテスト方法とか勉強していきます。