経緯
仕事でJavaScriptが必要になりしばらくは生のJavaScript+jQueryで開発していたのですが、要望などにより管理しきれないくらいオブジェクトや状態が増えてしまいました。
これはいよいよクラスやファイル分けしないと開発、保守、運用がめんどくさいなと。
そこでJavaScriptをC#っぽく書けそうなTypeScriptを導入してみようと思い、入門してみました。
注意事項
結構ほかの入門記事ではnode+CLI上で動作するものを作るものが多いですが、自分の目的はブラウザ上で動作するクライアントアプリの作成になるので、動作環境はブラウザを前提としています。
自身もまだJavaScriptやTypeScriptについて深く理解していないので、言語仕様や周辺ライブラリについては他の方の投稿を参考にしてください。
「こまけぇこたぁいんだよ!とりあえず動かしてみたい!」という方のお役には立てるかもしれません。
自分の備忘録のつもりで書いていますので悪しからず。
前提知識としてLinqとReactiveExtensionが必要です。
今回紹介する環境、および確認した環境
- Windows10
- Node.js v8.11.1
- npm 5.6.0
参考記事
先人たちの投稿に敬意と感謝を!
- VSCodeでTypeScript/Node.jsの開発環境を作る(UT・カバレッジ・ログ出力・リリース手順含む)
- --saveと--save-devの違いは何ですか?
- ちゃんと使い分けてる? dependenciesいろいろ。
- Using namespace spread over multiple files in TypeScript
- テストに使える簡易 API サーバがサクッと立てられる「json-server」
- いいからとにかくRxJSを動かしてみたいだけなんじゃ
- [フロントエンド] ES2015のimport/exportをブラウザで使うためのコンパイル(Babel × Browserify)
やったこと(この記事でやること)
- 環境の準備 & Hello,World!
- クラスを使ってみる
- 複数ファイルでモデルクラスを管理する
- 複数ファイルで管理しているクラスを一つの名前空間で管理する
- 外部ライブラリを呼び出して利用できるようにする
- Ajaxでサーバとやりとりするクラスを作成する
- json-serverでjsonを返すサーバを用意する
- RxJSでAjaxでの通信結果をSubscribeして、取得したデータからユーザモデルをインスタンス化し、jQueryでDOMに埋め込む
環境の準備 & Hello,World!
以下の環境を準備します。
設定方法などは好みによって分かれるものになりますので、設定方法などは他の方の記事を参考にしてください。
まずは以下をインストールしておく
-
Visual Studio Code (VSCode)
TypeScriptの開発するなら、このVSCodeで行うのがおすすめです。
エディタにターミナルがついててラクですし、ワークスペースファイルなんかも作れて便利です。
今回初めてしっかり使ってみましたが、今後Atomから乗り換えようと思ってます。 -
Node.js
これがないと始まりません。
同時にインストールされるパッケージマネージャーのnpmを使って、TypeScriptコンパイラや各種ライブラリのインストール、ビルドスクリプトの実行などを行います。
TypeScriptの開発環境をnpmでインストールする
- TypeScriptのインストール
ここからはVSCodeで作業をしていきます。
VSCodeを開き、【表示 > 統合ターミナル】(Ctrl + @でも可)でターミナルビューを開きます。
WindowsのデフォルトではPowerShellになっていますが、設定をすることで任意のCLIを利用することもできます。
- TypeScriptプロジェクトの作成と初期化
@kurogeleeさんの「VSCodeでTypeScript/Node.jsの開発環境を作る(UT・カバレッジ・ログ出力・リリース手順含む)」を参考にしている部分が大きいです。
詳しい説明などは元記事を確認してください。
1.下記コマンドでプロジェクトフォルダとプロジェクトの設定ファイルとなるpackage.jsonを作成します。
PS C:\Users\muramasa> mkdir c:\ts_sample
PS C:\Users\muramasa> cd c:\ts_sample
PS C:\ts_sample> npm init -y
Wrote to C:\ts_test\package.json:
{
"name": "ts_sample",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
2.ここで、作成したプロジェクトフォルダをVSCodeで開きましょう。
【ファイル > フォルダーを開く】(Ctrl + Shift + Oでも可)から、「C:\ts_sample」を選択し開きます。
すると、作成したフォルダをルートフォルダとしてプロジェクトを開くことができます。
3.package.jsonを編集
package.jsonから不要な記述を削除し、記述不足によるWarningを抑制するためprivate属性を設定します。
{
"name": "ts-sample",
"version": "1.0.0",
"private": true
}
4.続いてTypeScriptのトランスパイラをインストールします。
以下のコマンドでtypescriptライブラリをインストールします。
オプションの意味についてはこんな感じの理解になるのかな、と。
- --save-devオプションは、このプロジェクトの開発に必要な依存ライブラリの場合に指定する。
- --save-exactは、このプロジェクトの依存ライブラリのバージョンを固定して利用する場合に指定する。
PS C:\ts_sample> npm install --save-dev --save-exact typescript
+ typescript@2.9.2
added 1 package in 1.828s
5.TypeScriptのトランスパイル設定をします。
PS C:\ts_sample> ./node_modules/.bin/tsc --init
message TS6071: Successfully created a tsconfig.json file.
以降は@kurogeleeさんの「VSCodeでTypeScript/Node.jsの開発環境を作る(UT・カバレッジ・ログ出力・リリース手順含む)」から少しずつ外れていきます。
上記の記事ではnodeで動作するCLIツールを作ることが前提ですが、この記事ではブラウザ上で動作するクライアントを作成することが目的なので。
作成されたtsconfig.jsonを開き、以下のように設定をします。
本稿ではES2015(ES6)で進めていきます。
{
"compilerOptions": {
/* Basic Options */
"target": "es6",
"module": "commonjs",
"outDir": "./build",
"sourceMap": true,
/* Strict Type-Checking Options */
"strict": true,
},
"include": [
"src/**/*"
]
}
6.ビルドスクリプトを設定します。
下記コマンドで、ビルドに必要なライブラリをインストールします。
babelifyのインストール時に警告が出ますが、この手順で確認した限りでは動作していたので未調査です。
PS C:\ts_sample> npm install --save-dev --save-exact rimraf cpx babelify
npm WARN babelify@8.0.0 requires a peer of babel-core@6 || 7 || ^7.0.0-alpha || ^7.0.0-beta || ^7.0.0-rc but none is installed. You must install peer dependencies yourself.
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.4 (node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.4: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
+ rimraf@2.6.2
+ cpx@1.5.0
+ babelify@8.0.0
added 83 packages in 9.112s
package.jsonを開き、ビルドスクリプトを記述します。
scriptsという項目を追加し、そこにreleaseとbrowserifyという名前のスクリプトを追加します。
"scripts": {
"release": "rimraf build bin && tsc && cpx build/src/** bin && cd build && npm pack -cwd ..",
"browserify": "browserify --tranform babelify --outfile ./build/compiled_main.js ./build/main.js"
}
{
"name": "ts-sample",
"version": "1.0.0",
"private": true,
"scripts": {
"release": "rimraf build bin && tsc && cpx build/src/** bin && cd build && npm pack -cwd ..",
"browserify": "browserify --tranform babelify --outfile ./build/compiled_main.js ./build/main.js"
},
"devDependencies": {
"babelify": "8.0.0",
"cpx": "1.5.0",
"rimraf": "2.6.2",
"typescript": "2.9.2"
}
}
7."Hello, World!"とテストビルド
いよいよテストビルドをします。
ブラウザのコンソールにHello, world!と表示させるコードをビルドしてみます。
TypeScriptのファイルは.tsという拡張子で作成します。
srcというフォルダを作成し、main.tsというファイルを作成し、以下の内容を記述します。
console.log('Hello, World!');
そして、VSCodeのターミナルで以下のコマンドを実行します。
PS C:\ts_sample> npm run release
> ts-sample@1.0.0 release C:\ts_sample
> rimraf build bin && tsc && cpx build/src/** bin && cd build && npm pack -cwd ..
ts-sample-1.0.0.tgz
PS C:\ts_sample> npm run browserify
> ts-sample@1.0.0 browserify C:\ts_sample
> browserify --tranform babelify --outfile ./build/compiled_main.js ./build/main.js
【npm run release】の内容だけだと、ファイル分けした場合や外部ライブラリを使用した場合、ブラウザで動作しません。
import、exportという構文はまだブラウザでは対応していないためです。
そこでブラウザでも対応させるため、browserifyというコマンドでブラウザ用にトランスパイルしています。
(※参考:[フロントエンド] ES2015のimport/exportをブラウザで使うためのコンパイル(Babel × Browserify))
完了すると、buildというフォルダが作成され、compiled_main.jsというファイルがあります。
(function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
"use strict";
console.log('Hello, World!');
},{}]},{},[1]);
今回の内容だけであれば、下記コマンドでも確認ができますが、ブラウザで読み込んで実行してみましょう。
PS C:\ts_sample> node build/compiled_main.js
Hello, World!
index.htmlというファイルを作り、以下の内容を記述します。
<!DOCTYPE>
<html lang="ja">
<head>
<meta charset="utf8" />
<script src="build/compiled_main.js"></script>
</head>
<body></body>
</html>
index.htmlをブラウザで開き、コンソールを確認します。
(Chromeなら右クリックして「検証」、Firefoxなら右クリックして「要素を調査」でコンソール見れるはず)
コンソールに「Hello, World!」と表示されていれば環境構築はいったん完了です。
複数ファイルでモデルクラスを管理する
ここからはモデルクラスを作成して、jsonのシリアライズ・デシリアライズができるようにしていきます。
まずはsrc/modelというフォルダを作成し、model_base.tsファイルを作成、以下の内容を記述します。
export interface IModel{
}
export class ModelBase implements IModel{
}
続いてmodel_user.tsファイルを作成、以下の内容を記述します。
C#と似たような感じで書くことができます。
(C#の設計者がTypeScriptの開発にも関わっているみたいです。)
import { ModelBase } from "./model_base";
export class ModelUser extends ModelBase {
static readonly DEFAULT_ID: number = -1;
static readonly DEFAULT_NAME: string = 'hoge';
private _id: number;
private _name: string;
public get id() { return this._id; }
public get name() { return this._name; }
public set id(value: number) {
if (this._id !== ModelUser.DEFAULT_ID) {
return;
}
this._id = value;
}
public set name(value: string) {
if (this._name !== ModelUser.DEFAULT_NAME) {
return;
}
this._name = value;
}
public constructor(
id: number = ModelUser.DEFAULT_ID,
name: string = ModelUser.DEFAULT_NAME
) {
super();
this._id = id;
this._name = name;
}
}
複数ファイルで管理しているクラスを一つの名前空間で管理する
本当はModelという名前空間内に定義していきたいところですが、複数ファイルで同じ名前空間を扱うことがどうやらできないようです。
export namespace Model{
export interface IModel {
}
export class ModelBase implements IModel {
}
}
import { Model } from "./model_base";
export namespace Model{
export class ModelUser extends Model.ModelBase {
static readonly DEFAULT_ID: number = -1;
static readonly DEFAULT_NAME: string = 'hoge';
private _id: number;
private _name: string;
public get id() { return this._id; }
public get name() { return this._name; }
public set id(value: number) {
if (this._id !== ModelUser.DEFAULT_ID) {
return;
}
this._id = value;
}
public set name(value: string) {
if (this._name !== ModelUser.DEFAULT_NAME) {
return;
}
this._name = value;
}
public constructor(
id: number = ModelUser.DEFAULT_ID,
name: string = ModelUser.DEFAULT_NAME
) {
super();
this._id = id;
this._name = name;
}
}
}
↓こうなる。
つまりnamespace構文で複数ファイルで同一の名前空間を一緒に扱うのは無理っぽい。
...ではどうするかというと、以下の手順で同一の名前空間に複数ファイルに分けたクラスを格納できます。
「src/model/index.ts」というファイルを作成し、次の内容を記述して下さい。
export * from './model_base';
export * from './model_user';
こうすることで「src/model/index.ts」を読み込むとこのファイルがexportする名前空間にまとめることができます。
以下のように読み込むことで、名前空間に名前を付けることができます。
(実質的に読み込む際にしか名前空間に名づけることはできない)
(※参考:Using namespace spread over multiple files in TypeScript)
import * as Model from './model/index';
const model_base = new Model.ModelBase();
const model_user = new Model.ModelUser();
ではmain.tsで読み込んでインスタンス化してみましょう。
import * as Model from './model/index';
const user = new Model.ModelUser();
console.log(user);
ここでいったんビルドして確認してみます。
npm run release
npm run browserify
index.htmlをブラウザで開きコンソールを確認してみると以下のようになっているかと思います。
Ajaxでサーバとやりとりするクラスを作成する
ではここからは外部ライブラリを読み込んで利用していきます。
jQueryのAjaxでjsonを読み込んで読み込んだデータからモデルをインスタンス化していきます。
まずはライブラリをインストールします。
正確にはライブラリ本体ではなく、TypeScriptの型定義ファイルをインストールします。
イメージとしてはCやC++のヘッダファイルのようなものです。
TypeScriptは静的型付け言語なので、型付けがされていない既存のJavaScriptライブラリをそのまま利用することはできません。
なので実装の実体のないTypeScript用の型定義ファイルをインストールすることで、ライブラリを利用したTypeScriptのコードをコンパイルすることができるのです。
また、ライブラリの型定義ファイルがあっても、実装されたコードがなければコンパイル後に実際に動作させることができません。
別途読み込む必要があります。
PS C:\ts_sample> npm install --save-dev --save-exact @types/jquery
npm WARN babelify@8.0.0 requires a peer of babel-core@6 || 7 || ^7.0.0-alpha || ^7.0.0-beta || ^7.0.0-rc but none is installed. You must install peer dependencies yourself.
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.4 (node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.4: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
+ @types/jquery@3.3.4
added 1 package in 4.239s
これでjQueryをTypeScriptから呼び出せるようになりました。
httpフォルダを作り、その中にajax_client.tsというファイルを作成、以下の内容を記述します。
export enum METHOD_TYPE{
POST = 'POST',
GET = 'GET'
}
export class AjaxClient{
static readonly DEFAULT_URL: string = '';
static readonly DEFAULT_METHOD: METHOD_TYPE = METHOD_TYPE.POST;
private _url: string;
private _method_type: METHOD_TYPE;
public get url(){return this._url;}
public get method_type(){return this._method_type;}
public constructor(
url: string = AjaxClient.DEFAULT_URL,
method_type: METHOD_TYPE = AjaxClient.DEFAULT_METHOD
){
this._url = url;
this._method_type = method_type;
}
public Send(){
if(!this.isValidSettings()){
return;
}
const $ajax_stream = $.ajax({
url: this._url,
type: this._method_type
});
$ajax_stream.done(resp => console.log(resp));
$ajax_stream.fail(failed_stream => console.log(failed_stream));
}
private isValidSettings(){
return (this._url !== AjaxClient.DEFAULT_URL);
}
}
AjaxClientを使ってみましょう。
main.tsを書き換えます。
import * as HTTP from './http/ajax_client'
const url = 'http://localhost:3000/get';
const ajax_obj = new HTTP.AjaxClient(url, HTTP.METHOD_TYPE.GET);
ajax_obj.Send();
ここで一旦ビルドします。
npm run release
npm run browserify
jQueryのライブラリを追加したので、compiled_main.jsを呼び出しているindex.htmlを書き換えてjQueryを読み込むようにします。
<!DOCTYPE>
<html lang="ja">
<head>
<meta charset="utf8" />
<script src="https://code.jquery.com/jquery-3.3.1.js"></script>
<script src="build/compiled_main.js"></script>
</head>
<body></body>
</html>
これでAjaxのクライアントが利用できるようになりました。
ブラウザで確認してみると、アクセス先の「http://localhost:3000/get」 がないためerrorになっているはずです。
次はアクセス先のサーバを用意します。
json-serverでjsonを返すサーバを用意する
json-serverというコマンド一つで任意のjsonファイルの内容を返してくれるサーバを立てる便利なツールがありました。
これを使ってアクセス先のサーバを立てます。
下記コマンドでインストールしましょう。
PS C:\ts_sample> npm install -g json-server
C:\Users\muramasa\AppData\Roaming\npm\json-server -> C:\Users\muramasa\AppData\Roaming\npm\node_modules\json-server\bin\index.js
+ json-server@0.14.0
added 229 packages in 12.502s
次に、json-serverが返してくれるjsonの内容を作成しましょう。
testというフォルダを作成し、その中にresponse.jsonというファイルを作成します。
以下の内容を記述します。
{
"get": [
{"id": 0, "name": "hoge"},
{"id": 1, "name": "fuga"},
{"id": 2, "name": "foo"},
{"id": 3, "name": "bar"}
]
}
これで準備が完了したので、以下のコマンドでサーバを立ち上げます。
PS C:\ts_sample> json-server .\test\response.json
\{^_^}/ hi!
Loading .\test\response.json
Done
Resources
http://localhost:3000/get
Home
http://localhost:3000
Type s + enter at any time to create a snapshot of the database
http://localhost:3000/getからjsonが取得できるようになったので、index.htmlを開いてコンソールを確認してみましょう。
下記のようにjsonが取得できているはずです。
RxJSでAjaxでの通信結果をSubscribeして、取得したデータからユーザモデルをインスタンス化し、jQueryでDOMに埋め込む
なんか色々と詰め込みすぎた感がありますが、やってることはそんなに複雑じゃないです。
まずはこの後利用していくRxJSとLinq.jsのライブラリをインストールします。
PS C:\ts_sample> npm install --save-dev --save-exact rxjs rxjs-compat linq
npm WARN babelify@8.0.0 requires a peer of babel-core@6 || 7 || ^7.0.0-alpha || ^7.0.0-beta || ^7.0.0-rc but none is installed. You must install peer dependencies yourself.
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.4 (node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.4: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})
+ rxjs@6.2.1
+ linq@3.1.0
+ rxjs-compat@6.2.1
added 4 packages in 13.691s
ajax_client.tsを書き換えて、Subscribeできるようにします。
import * as Rx from 'rxjs/Rx';
export enum METHOD_TYPE{
POST = 'POST',
GET = 'GET'
}
export class AjaxClient{
static readonly DEFAULT_URL: string = '';
static readonly DEFAULT_METHOD: METHOD_TYPE = METHOD_TYPE.POST;
private _url: string;
private _method_type: METHOD_TYPE;
protected _done_subject: Rx.Subject<string>;
protected _fail_subject: Rx.Subject<JQuery.jqXHR<any>>;
public get doneAsObservable() { return this._done_subject.asObservable(); }
public get failAsObservable() { return this._fail_subject.asObservable(); }
public get url(){return this._url;}
public get method_type(){return this._method_type;}
public constructor(
url: string = AjaxClient.DEFAULT_URL,
method_type: METHOD_TYPE = AjaxClient.DEFAULT_METHOD
){
this._url = url;
this._method_type = method_type;
this._done_subject = new Rx.Subject<string>();
this._fail_subject = new Rx.Subject<JQuery.jqXHR<any>>();
}
public Send(){
if(!this.isValidSettings()){
return;
}
const $ajax_stream = $.ajax({
url: this._url,
type: this._method_type
});
$ajax_stream.done(resp => this._done_subject.next(resp));
$ajax_stream.fail(failed_stream => this._fail_subject.next(failed_stream));
}
private isValidSettings(){
return (this._url !== AjaxClient.DEFAULT_URL);
}
}
main.tsを書き換えます。
import * as HTTP from './http/ajax_client'
import * as Model from './model/index'
import * as Enumerable from 'linq';
const url = 'http://localhost:3000/get';
const ajax_obj = new HTTP.AjaxClient(url, HTTP.METHOD_TYPE.GET);
ajax_obj.Send();
const users = new Array<Model.ModelUser>();
ajax_obj.doneAsObservable
.filter(resp => resp.length > 0)
.subscribe(resp => {
Enumerable.from(resp)
.forEach(record => {
const _record = record as any;
users.push(new Model.ModelUser(_record.id, _record.name));
});
Enumerable.from(users)
.forEach(user_data => {
$('body').append(`Id: ${user_data.id} / Name: ${user_data.name} <br />`);
});
console.log(users);
});
ビルドしてブラウザで確認します。
npm run release
npm run browserify
json-server .\test\response.json
最後に
改めて記事にまとめてみると、詰め込みすぎた感がありますね…。
UnityでC#やってたので似たような書き方できるとラクだなーと思いLinqやRxJSを導入しました。
入門して二日程度なので、間違っていることのご指摘やアドバイスなどありましたら是非ともご教示いただけますと幸いです。