はじめに
経緯
前回の記事「TypeScript, React, Sass, webpack, gulp導入記 - モダンなクライアントサイドWeb開発に挑戦した話」で、ビューのライブラリとしてReactを用いましたが、Vue.jsについても検証してみたいと思い、TypeScript上でVue.jsを利用してみました。
筆者の環境
- Windows 10 Pro 64ビット
- IntelliJ IDEA 15.0 (無くても構いません)
各種ツールのインストール
前回の記事と同じです。
プロジェクトの作成
プロジェクトディレクトリの作成
前回の記事から、以下のみ異なります。
プロジェクトディレクトリ名はhello-vuejs
となります。
package.jsonの作成
前回の記事と同じです。
ビルド用モジュールのインストール
前回の記事と同じです。
IntellJ IDEAのプロジェクトの作成
前回の記事から、以下のみ異なります。
- [File]-[Settings]で設定ダイアログを開きます。
- [Language & Frameworks > TypeScript]を開きます。
- [Command line options]に
--module commonjs --experimentalDecorators
を入力します。
- [Command line options]に
- [Language & Frameworks > TypeScript]を開きます。
最低限必要なソースファイルの作成
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="css/common.css" rel="stylesheet" type="text/css">
<script src="js/bundle.js" type="text/javascript"></script>
</head>
<body>
<div id="content">
<router-view></router-view>
</div>
</body>
</html>
// とりあえず空で良い
tslint.jsonの作成
前回の記事と同じです。
gulpfile.jsの作成とgulpの実行
前回の記事から、以下のみ異なります。
gulpfile.js
の内容は以下のようになります。
var gulp = require('gulp');
var ts = require('gulp-typescript');
var sass = require('gulp-sass');
var del = require('del');
var webpack = require('gulp-webpack');
gulp.task('clean', function() {
del(['dist']);
del(['build']);
});
gulp.task('copy', function() {
gulp.src(
[ 'src/**/*', '!src/js/*', '!src/css/*' ],
{ base: 'src' }
).pipe(gulp.dest('dist'));
});
gulp.task('ts', function() {
return gulp.src(['src/js/*.{ts,tsx}'])
.pipe(ts({
target: "ES5",
module: "commonjs",
experimentalDecorators: true
}))
.js
.pipe(gulp.dest('build/js'));
});
gulp.task('bundle', ['ts'], function() {
gulp.src('./build/js/*.js')
.pipe(webpack({
entry: ['./build/js/main.js'],
output: {
filename: 'bundle.js',
library: 'app'
},
devtool: 'source-map',
resolve: {
extensions: ['', '.js']
},
}))
.pipe(gulp.dest('dist/js'));
});
gulp.task('scss', function() {
gulp.src(['src/css/*.scss'])
.pipe(sass())
.pipe(gulp.dest('dist/css'));
});
gulp.task('watch', function() {
gulp.watch(['src/**/*', '!src/js/*', '!src/css/*'], ['copy']);
gulp.watch('src/js/*.{ts,tsx}', ['bundle']);
gulp.watch('src/css/*.scss', ['scss']);
});
gulp.task('default', ['copy', 'bundle', 'scss', 'watch']);
実装
アプリケーション概要
ダミーのRESTful APIであるJSONPlaceholderの/posts
に接続し、CRUD操作を行います。
実装するクラスの構成は以下の通りです。
ライブラリの取得
>npm install jquery vue vue-class-component vue-router --save
--save
オプションにより、配備するNodeモジュールがインストールされ、package.json
に記録されます。
型定義ファイルの取得
>typings init
>typings install jquery --ambient --save
>typings install vue --ambient --save
>typings install vue-router --ambient --save
--ambient
オプションによりDefinitelyTypedから、型定義ファイルが検索されます。
--save
オプションにより、取得した型定義ファイルが、typings.json
に記載されます。
なお、typings.json
に記録された型定義ファイルを改めて取得したいときは、以下で取得できます。
>typings install
サブモジュールの実装
/// <reference path="../../typings/browser.d.ts" />
import * as $ from 'jquery';
export interface ICollectionListener<T> {
onUpdateCollection: (sender: Collection<T>) => void;
}
export class Collection<T> {
private models: T[];
private url: string;
private idAttribute: string;
private listeners: ICollectionListener<T>[];
constructor(url: string) {
this.url = url;
this.idAttribute = 'id';
this.listeners = [];
}
public addListener(listener: ICollectionListener<T>): void {
this.listeners.push(listener);
}
public removeListener(listener: ICollectionListener<T>): void {
let index: number = this.listeners.indexOf(listener);
if (index >= 0) {
this.listeners.splice(index, 1);
}
}
public get(): T[] {
return this.models;
}
public add(item: T): JQueryPromise<any> {
let d: JQueryDeferred<any> = $.Deferred();
$.ajax({
url: this.url,
dataType: 'json',
type: 'post',
data: item,
success: (data: any): void => {
if (!(this.idAttribute in data)) {
console.log('response body must contain id attribute.');
}
this.models.push(data);
d.resolve();
for (let i: number = 0; i < this.listeners.length; ++i) {
this.listeners[i].onUpdateCollection(this);
}
},
error: (xhr: JQueryXHR, status: string, err: string): void => {
d.reject(xhr, status, err);
},
});
return d.promise();
}
public fetch(): JQueryPromise<any> {
let d: JQueryDeferred<any> = $.Deferred();
$.ajax({
url: this.url,
dataType: 'json',
cache: false,
success: function(data: any): void {
this.models = data;
d.resolve();
for (let i: number = 0; i < this.listeners.length; ++i) {
this.listeners[i].onUpdateCollection(this);
}
}.bind(this),
error: function(xhr: JQueryXHR, status: string, err: string): void {
d.reject(xhr, status, err);
}.bind(this),
});
return d.promise();
}
public set(item: T): JQueryPromise<any> {
let d: JQueryDeferred<any> = $.Deferred();
let id: string = item[this.idAttribute];
$.ajax({
url: this.url + '/' + id,
dataType: 'json',
type: 'put',
data: item,
success: (data: any): void => {
for (let i: number = 0; i < this.models.length; ++i) {
if (this.models[i][this.idAttribute].toString() === id) {
this.models[i] = item;
break;
}
}
d.resolve();
for (let i: number = 0; i < this.listeners.length; ++i) {
this.listeners[i].onUpdateCollection(this);
}
},
error: (xhr: JQueryXHR, status: string, err: string): void => {
d.reject(xhr, status, err);
},
});
return d.promise();
}
public remove(id: string): JQueryPromise<any> {
let d: JQueryDeferred<any> = $.Deferred();
$.ajax({
url: this.url + '/' + id,
dataType: 'json',
type: 'delete',
success: (data: any): void => {
for (let i: number = 0; i < this.models.length; ++i) {
if (this.models[i][this.idAttribute].toString() === id) {
this.models.splice(i, 1);
break;
}
}
d.resolve();
for (let i: number = 0; i < this.listeners.length; ++i) {
this.listeners[i].onUpdateCollection(this);
}
},
error: (xhr: JQueryXHR, status: string, err: string): void => {
d.reject(xhr, status, err);
},
});
return d.promise();
}
}
メインモジュールの実装
/// <reference path="../../typings/browser.d.ts" />
/// <reference path="../../node_modules/vue-class-component/index.d.ts" />
/// <reference path="typebone.ts" />
import * as Vue from 'vue';
import * as VueRouter from 'vue-router';
import VueComponent from 'vue-class-component';
import * as $ from 'jquery';
import * as Typebone from './typebone';
class AppModel {
public collection: Typebone.Collection<IPostItem> = new Typebone.Collection<IPostItem>('http://jsonplaceholder.typicode.com/posts');
}
let appModel: AppModel = new AppModel();
interface IPostItem {
userId: number;
id: number;
title: string;
body: string;
}
@VueComponent({
template: `
<div class="post">
<input type="text" v-model="item.title"/>
<input type="text" v-model="item.body"/>
<input class="update" type="button" value="Update" v-on:click="updatePost(item)"/>
<input class="delete" type="button" value="Delete" v-on:click="deletePost(item)"/>
</div>
`,
props: ['item'],
})
class Post extends Vue {
public updatePost(post: IPostItem): void {
appModel.collection.set(post).fail(
(xhr: JQueryXHR, status: string, err: string) => {
console.error(xhr.statusText, status, err);
}
);
}
public deletePost(post: IPostItem): void {
appModel.collection.remove(post.id.toString()).fail(
(xhr: JQueryXHR, status: string, err: string) => {
console.error(xhr.statusText, status, err);
}
);
}
}
@VueComponent({
template: `
<div class="postList" v-for="post in posts">
<post :item="post"></post>
</div>
`,
props: ['posts'],
})
class PostList extends Vue {
}
@VueComponent({
template: `
<form class="postForm" v-on:submit.prevent="onPostSubmit">
<input type="text" placeholder="title" v-model="title"/>
<input type="text" placeholder="body" v-model="body"/>
<input type="submit" value="Post"/>
</form>
`,
})
class PostForm extends Vue {
private title: string;
private body: string;
public data(): any {
return {
title: '',
body: '',
};
}
public onPostSubmit(): void {
if (this.title === '' || this.body === '') {
return;
}
this.$dispatch('post-submit', {
title: this.title,
body: this.body,
});
this.title = '';
this.body = '';
}
}
@VueComponent({
template: `
<div class="postBox">
<h1>Posts</h1>
<post-list :posts="posts"></post-list>
<post-form v-on:post-submit="addPost"></post-form>
</div>
`,
})
class PostBox extends Vue implements Typebone.ICollectionListener<IPostItem> {
private posts: IPostItem[];
public data(): any {
return {
posts: [],
};
}
public attached(): void {
appModel.collection.addListener(this);
appModel.collection.fetch().fail(
(xhr: JQueryXHR, status: string, err: string) => {
console.error(xhr.statusText, status, err);
}
);
}
public detached(): void {
appModel.collection.removeListener(this);
}
public addPost(item: IPostItem): void {
appModel.collection.add(item).fail(
(xhr: JQueryXHR, status: string, err: string) => {
console.error(xhr.statusText, status, err);
}
);
}
public onUpdateCollection(sender: Typebone.Collection<IPostItem>): void {
this.posts = appModel.collection.get();
}
}
@VueComponent({})
class App extends Vue {
}
Vue.component('post', Post);
Vue.component('post-list', PostList);
Vue.component('post-form', PostForm);
Vue.use(VueRouter);
let router: any = new VueRouter();
router.map({
'/': {
component: PostBox,
},
});
$(function() {
router.start(App, '#content');
});
SCSSの実装
$primary-color: #111188;
$background-color: #eeeeee;
body {
color: $primary-color;
background-color: $background-color;
}
改めてビルド
>gulp clean
>gulp
以上で、dist/index.html
を閲覧できるようになりました。なお、ダミーのRESTful APIを用いているため、期待通りに動作しない機能もあります。
まとめ
Reactの時と同様、Vue.jsをTypeScriptから利用するためには各種情報を調査せねばならず、ハードルが高く思われました。ただ、一度導入をしてしまえれば、後は快適にTypeScriptによる静的型付けを活かしたプログラミングができそうです。