Angular2で高速SPA開発(2.5) TypeScriptをふつうのスクリプトっぽく使ってデータ投入

はじめに

前回までで、MongoDBとAngular(とExpressとNode.js)でデータベースにアクセスする処理(のようなもの)を作成しました。続いてもっとDBを扱ってる風な感じにしていきたいのですが、そのためのダミーデータを作成して投入してみたいと思います。

せっかくフロントエンドもバックエンドもTypeScriptで書ける!というのが今回の目玉ですので、データ投入部分もTypeScriptで書けるようにしていきましょう。

環境

いつものように開発環境を掲載しておきます。

  • MacBook Pro (Retina, 13-inch, Early 2015)
    • macOS Sierra 10.12.5
    • プロセッサ: 3.1 GHz Intel Core i7
    • メモリ: 16 GB 1867 MHz DDR3
  • Node.js v7.5.0
  • npm 5.0.3
  • MongoDB shell version v3.4.4
  • MongoDB server version: 3.4.4
  • tsc Version 2.4.2

ちなみにエディタはVisual Studio Code 1.14.2を使っております。

 

TypeScript on Node.js

Node.js上でTypeScript(をJSにトランスパイルしたもの)を実行するには少し設定を弄る必要があります。

以下が詳しいです。

TypeScriptでNode.jsアプリを開発する

簡単に説明しますと

  1. TypeScript用のNode.jsの型定義ファイルをインストールする
  2. tsconfig.json を編集する
    1. compilerOptions のtypeRoots にインストールした型定義ファイルのパスを指定する
    2. compilerOptionsmodule にcommonjs を指定する

が必要です。

上記記事では、typings を使ってdt~node をインストールしていますが、npm を使って@types/node を使うのがモダンなようです。

……と思っていたのですが、@types/node だけではNode.jsのグローバル定義の変数が参照できませんでしたので、ここではどちらもインストールすることにしました。

$ typings install dt~node --global
node

$ npm install @types/node
+ @types/node@8.0.17
updated 1 package in 3.735s

tsconfig.json はこのようになります。

{
    "compileOnSave": true,
    "compilerOptions": {
        "module": "commonjs",
        "downlevelIteration": true,
        "outDir": "./",
        "noImplicitAny": true,
        "typeRoots": [
            "node_modules/@types",
            "typings/globals"
        ],
        "target": "es6",
        "watch": true
    },
    "files": [
        "main.ts"
    ]
}

 

shebang

tsファイルにshebangをつけることで、簡単にnode 上で動作させられるようになります。つまり、ファイルの先頭行に

#!/usr/bin/env node

と書くと、トランスパイル後のjsファイルにもshebangが残されますので、

$ chmod +x main.js

として実行権限を付与すれば

$ ./main.js

として実行できます。トランスパイルの一手間はありますが、rubyやpythonのようなLL風にTypeScriptを利用可能です。

古いバージョンではうまくいかないかもしれませんが、最新版を使う限り問題はないでしょう。

Typescript aborts on shebang. #2749

tsc をウォッチモードで走らせておけば(tsc -w )、ファイルを保存するごとに勝手にトランスパイルしてくれるのでより楽になるでしょう。

エディタによってはtsconfig.json のcompileOnSave にtrue を設定しておけば保存時にトランスパイルしてくれるようですが、VSCodeでは対応してないようです……これで数時間ハマりました。

tsconfig.json | TypeScript

いっぽうAtomであればプラグインで対応可能です。

ランダムにデータを生成してみる

さて、投入するデータですが、ちょっとゲームっぽいものを意識して適当(≒雑)にモンスターデータのようなものを作ってみたいと思います。

必要そうなのは、

  • 名前(3-6文字)
  • レアリティ(1-5)
  • 属性(1-3個)
  • HP
  • 攻撃力

あたりでしょうか。HPや攻撃力はレアリティが高い(5に近い)ほど高い数値になりやすいようにやはり適当に設定しています。名前もランダムに作られるのでクオリティにかなり差ができますが、妙に親しみやすいような気がするものが多く作られます(?)たまに発音できなさそうなものが出現するのはまぁごあいきょうということで。

とりあえず1体生成してみました(すべてのステータスはランダムで生成しているため、同じ表示になるとは限らないことに注意してくださいね)。

{
    name: 'ガベワホケル',
    rarity: 5,
    hp: 680,
    atk: 310,
    elements: [ '水', '土' ]
}

う〜〜ん、この……ともかく、それらしいデータができましたよ!

MongoDBにデータ投入

こんかいのアプリケーションでは、データベースにMongoDB、そのドライバにmongooseを使っていますので、同じようにしてアクセスしようと思います。

まずはmongoose および@types/mongoose をプロジェクトに追加します。

npm install mongoose @types/mongoose

とすればよいです。

データベースに投入するデータのスキーマをそれっぽく作っておきます。上で挙げた、「必要そうな項目」に属性名と型を具体的に与えます。

import * as mongoose from 'mongoose';

const monstersModel: mongoose.Model<mongoose.Document> = mongoose.model(
    'monsters',
    new mongoose.Schema({
        name: {
            type: String 
        },
        rarity: {
            type: Number
        },
        elements: {
            type: [String]
        },
        hp: {
            type: Number
        },
        atk: {
            type: Number
        }
    })
);

export { monstersModel };

また、生成部分はこうなります。

function randomMonster(): mongoose.Document
{
    const rarity = random(5) + 1;

    return new monstersModel({
        'name': randomName(),
        'rarity': rarity,
        'elements': randomElements(), 
        'hp': random(100 * rarity) * 10,
        'atk': random(100 * rarity) * 10
    });
}

この状態で

console.log(randomMonster());

とすれば

{ name: 'ガビ・ナジー',
  rarity: 4,
  hp: 570,
  atk: 30,
  _id: 597eab320764db34abacd666,
  elements: [ '土', '光', '闇' ] }

さきほどと同様のデータが作成されました(これまたヒドい名前とステータス……)。ただし、ここで返される値はmongoose.Document 型であり、直接は設定していませんが、自動で一意になるような_id を振ってくれます。

あとはこれをDBに突っ込めばオッケーです。

まず、mongoose.connect() でローカルで走っているインスタンスのmonsters というDBへコネクションを張ります(6行目)。

あとは必要な分だけ生成してはsave() していくだけです(11-16行目)。

最後にdisconnect() しておかないとコネクションが張られっぱなしになりスクリプトが終了しなくなってしまうので注意(19行目)。

#!/usr/bin/env node

import * as mongoose from 'mongoose';
import { randomMonster } from './random';

mongoose.connect('mongodb://localhost/monsters');

const num = 500;

for (let i = 0; i < num; i += 1) {
    const monster = randomMonster();
    monster.save((err) => {
        if (err) {
            throw err;
        }
    });
}
    
mongoose.disconnect();

このスクリプトを実行する際には、mongo をlocalhost 上で動かしておいてください。

なんだかもう少しスマートに書けそうだなぁとも思いますがまずは。

 

データがホントウに投入できたか確認しましょう。Angularで……ではなく、こんかいは直接MongoDBを叩きます。楽しみはあとにとっておくタイプです。

$ mongo
MongoDB shell version v3.4.4
connecting to: mongodb://127.0.0.1:27017
MongoDB server version: 3.4.4
(...省略
> use monsters
switched to db monsters
> db.monsters.find()
{ "_id" : ObjectId("597eb14c4b4d5f3c068078c6"), "name" : "ムオイェユツ", "rarity" : 3, "hp" : 410, "atk" : 610, "elements" : [ "土", "闇", "光" ], "__v" : 0 }
{ "_id" : ObjectId("597eb14c4b4d5f3c068078c7"), "name" : "シミマ", "rarity" : 2, "hp" : 1040, "atk" : 1240, "elements" : [ "水" ], "__v" : 0 }
{ "_id" : ObjectId("597eb14c4b4d5f3c068078c8"), "name" : "プケロ", "rarity" : 5, "hp" : 3960, "atk" : 2850, "elements" : [ "光", "闇", "水" ], "__v" : 0 }
{ "_id" : ObjectId("597eb14c4b4d5f3c068078c9"), "name" : "レヂモサズス", "rarity" : 4, "hp" : 3440, "atk" : 1140, "elements" : [ "土" ], "__v" : 0 }
{ "_id" : ObjectId("597eb14c4b4d5f3c068078ca"), "name" : "ゼィシイ", "rarity" : 3, "hp" : 300, "atk" : 1350, "elements" : [ "光", "水" ], "__v" : 0 }
{ "_id" : ObjectId("597eb14c4b4d5f3c068078cb"), "name" : "スッセォ", "rarity" : 4, "hp" : 1060, "atk" : 1630, "elements" : [ "光" ], "__v" : 0 }
{ "_id" : ObjectId("597eb14c4b4d5f3c068078cc"), "name" : "ニケシメ", "rarity" : 3, "hp" : 530, "atk" : 760, "elements" : [ "光" ], "__v" : 0 }
{ "_id" : ObjectId("597eb14c4b4d5f3c068078cd"), "name" : "サミケザ", "rarity" : 3, "hp" : 2170, "atk" : 200, "elements" : [ "水" ], "__v" : 0 }
{ "_id" : ObjectId("597eb14c4b4d5f3c068078ce"), "name" : "サンヌヘ", "rarity" : 3, "hp" : 440, "atk" : 1360, "elements" : [ "闇" ], "__v" : 0 }
{ "_id" : ObjectId("597eb14c4b4d5f3c068078cf"), "name" : "クタシタ", "rarity" : 2, "hp" : 1430, "atk" : 1210, "elements" : [ "土", "水" ], "__v" : 0 }
{ "_id" : ObjectId("597eb14c4b4d5f3c068078d0"), "name" : "ヂレヒヨゥカ", "rarity" : 1, "hp" : 750, "atk" : 870, "elements" : [ "水" ], "__v" : 0 }
{ "_id" : ObjectId("597eb14c4b4d5f3c068078d1"), "name" : "ザクベルザ", "rarity" : 2, "hp" : 320, "atk" : 880, "elements" : [ "土" ], "__v" : 0 }
{ "_id" : ObjectId("597eb14c4b4d5f3c068078d2"), "name" : "ラヌギェヅ", "rarity" : 3, "hp" : 240, "atk" : 570, "elements" : [ "闇" ], "__v" : 0 }
{ "_id" : ObjectId("597eb14c4b4d5f3c068078d3"), "name" : "ザァヤハノベ", "rarity" : 1, "hp" : 820, "atk" : 30, "elements" : [ "光", "土" ], "__v" : 0 }
{ "_id" : ObjectId("597eb14c4b4d5f3c068078d4"), "name" : "ニダビェコ", "rarity" : 5, "hp" : 4910, "atk" : 4780, "elements" : [ "闇", "水" ], "__v" : 0 }
{ "_id" : ObjectId("597eb14c4b4d5f3c068078d5"), "name" : "ピッー", "rarity" : 2, "hp" : 890, "atk" : 280, "elements" : [ "闇", "土", "水" ], "__v" : 0 }
{ "_id" : ObjectId("597eb14c4b4d5f3c068078d6"), "name" : "ハダセ", "rarity" : 2, "hp" : 380, "atk" : 310, "elements" : [ "闇", "光" ], "__v" : 0 }
{ "_id" : ObjectId("597eb14c4b4d5f3c068078d7"), "name" : "マィッヌグ", "rarity" : 4, "hp" : 1830, "atk" : 2440, "elements" : [ "闇", "土" ], "__v" : 0 }
{ "_id" : ObjectId("597eb14c4b4d5f3c068078d8"), "name" : "ツソギザチギ", "rarity" : 2, "hp" : 1040, "atk" : 710, "elements" : [ "闇", "光", "土" ], "__v" : 0 }
{ "_id" : ObjectId("597eb14c4b4d5f3c068078d9"), "name" : "ヒユッ・ザメ", "rarity" : 4, "hp" : 2240, "atk" : 270, "elements" : [ "土", "光", "水" ], "__v" : 0 }
Type "it" for more
(...省略

よさそうですねそれにしてもクトゥルフ風の素敵な名前が並んでいることです

おわりに

こんかいはTypeScriptをローカルのスクリプトとして使い、MongoDBへデータを投入しました。データの投入が目的ですのであまり気にはなりませんが、使い捨てのコードをササッと書くにはやはり大げさかな?と感じますね。

さて、次回以降で、Angularからこれらのデータを参照したり変更したりする感じの部分を作成し、もうちょっとウェブアプリっぽくしていきたいと思います。

お楽しみに。では。