Electron+TypeScript+Express.jsでweb-apiを作る

Electronで動くweb-apiを作りたいので以下のサイトを参考に試してみます。

>https://electron.atom.io/docs/tutorial/quick-start/
>http://mherman.org/blog/2016/11/05/developing-a-restful-api-with-node-and-typescript/#.WRb91DekKV5

+ LinuxMint v18.1
+ Node.js v7.10.0 (nvm使用)
+ Electron v1.6.7
+ TypeScript v2.3.2
+ Express.js v4.15.2

### プロジェクトフォルダの作成
---
まずは今回のプロジェクトフォルダとpackage.jsonを作成します。

```command:command
mkdir electron-api
cd electron-api
mkdir src
npm init -y
```

### Electronのインストール
---
Electronのインストールと必須ファイルの編集を行います。
LinuxMint(Ubuntu)&nvm の環境だとElectronの npm -g インストールは今の所失敗するので、
プロジェクトへのローカルインストールを行います。

```command:command
npm install electron --save-dev
touch main.js
touch index.html
```

各ファイルを以下のように編集します。

```package.json
{
  "name"    : "electron-api",
  "version" : "1.0.0",
  "main"    : "main.js"
}
```

```main.js
const {app, BrowserWindow} = require('electron')
const path = require('path')
const url = require('url')

// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let win

function createWindow () {
  // Create the browser window.
  win = new BrowserWindow({width: 800, height: 600})

  // and load the index.html of the app.
  win.loadURL(url.format({
    pathname: path.join(__dirname, 'index.html'),
    protocol: 'file:',
    slashes: true
  }))

  // Open the DevTools.
  win.webContents.openDevTools()

  // Emitted when the window is closed.
  win.on('closed', () => {
    // Dereference the window object, usually you would store windows
    // in an array if your app supports multi windows, this is the time
    // when you should delete the corresponding element.
    win = null
  })
}

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', createWindow)

// Quit when all windows are closed.
app.on('window-all-closed', () => {
  // On macOS it is common for applications and their menu bar
  // to stay active until the user quits explicitly with Cmd + Q
  if (process.platform !== 'darwin') {
    app.quit()
  }
})

app.on('activate', () => {
  // On macOS it's common to re-create a window in the app when the
  // dock icon is clicked and there are no other windows open.
  if (win === null) {
    createWindow()
  }
})

// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and require them here.
```

```index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Hello World!</title>
  </head>
  <body>
    <h1>Hello World!</h1>
    We are using node <script>document.write(process.versions.node)</script>,
    Chrome <script>document.write(process.versions.chrome)</script>,
    and Electron <script>document.write(process.versions.electron)</script>.
  </body>
</html>
```
編集が完了したら以下のコマンドで実行してみます。
Hello World!が起動したら成功です。

```command:command
./node_modules/.bin/electron .
```

![Screenshot from 2017-05-14 01-07-32.png](https://qiita-image-store.s3.amazonaws.com/0/181357/e0c63558-a690-3c33-2d6d-5f9f49745ab6.png)


### TypeScriptのインストール
---

```command:command
npm install typescript --save-dev
npm install gulp gulp-typescript --save-dev
npm install -g gulp
touch tsconfig.json
touch src/test.ts
```

tsconfig.json, test.ts を編集します。

```tsconfig.json
{
  "compilerOptions": {
    "target": "es6",
    "module": "commonjs",
    "outDir": "dist"
  },
  "include": [
    "src/**/*.ts"
  ],
  "exclude": [
    "node_modules"
  ]
}
```
```test.ts
console.log('Hello, TypeScript!');
```

TypeScriptでのコンパイルをテストします。
test.ts がコンパイルされて dist/test.js が作成されれば成功です。

```command:command
node_modules/.bin/tsc
```

gulpの設定を作成します。

```command:command
touch gulpfile.js
```

```gulpfile.js
const gulp = require('gulp');
const ts = require('gulp-typescript');
const JSON_FILES = ['src/*.json', 'src/**/*.json'];

// pull in the project TypeScript config
const tsProject = ts.createProject('tsconfig.json');

gulp.task('scripts', () => {
  const tsResult = tsProject.src()
  .pipe(tsProject());
  return tsResult.js.pipe(gulp.dest('dist'));
});

gulp.task('watch', ['scripts'], () => {
  gulp.watch('src/**/*.ts', ['scripts']);
});

gulp.task('assets', function() {
  return gulp.src(JSON_FILES)
  .pipe(gulp.dest('dist'));
});

gulp.task('default', ['watch', 'assets']);
```

gulpでのコンパイルをテストします。
dist/test.js を削除した後、以下コマンドを実行します。
dist/test.js が再度作成されていれば成功です。

```command:command
gulp
```

これで.tsファイルの編集が監視され、.jsファイルが自動で出力されるようになります。

### Express.jsのインストール
---
Express.jsとデバッグ用ツールのdebugをインストールします。

```command:command
npm install express debug --save
```

Node, Express関連のTypeScript用型定義情報をインストールします。

```command:command
npm install @types/node @types/express @types/debug --save-dev
```

先ほど作成したtest.tsのファイル名をindex.tsに変更します。

```command:command
mv src/test.ts src/index.ts
```

index.tsを以下の内容で編集します。この時点ではAppモジュールはまだありません。

```index.ts
import * as http from 'http';
import * as debug from 'debug';

import App from './App';

debug('ts-express:server');

const port = normalizePort(process.env.PORT || 3000);
App.set('port', port);

const server = http.createServer(App);
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);

function normalizePort(val: number|string): number|string|boolean {
  let port: number = (typeof val === 'string') ? parseInt(val, 10) : val;
  if (isNaN(port)) return val;
  else if (port >= 0) return port;
  else return false;
}

function onError(error: NodeJS.ErrnoException): void {
  if (error.syscall !== 'listen') throw error;
  let bind = (typeof port === 'string') ? 'Pipe ' + port : 'Port ' + port;
  switch(error.code) {
    case 'EACCES':
      console.error(`${bind} requires elevated privileges`);
      process.exit(1);
      break;
    case 'EADDRINUSE':
      console.error(`${bind} is already in use`);
      process.exit(1);
      break;
    default:
      throw error;
  }
}

function onListening(): void {
  let addr = server.address();
  let bind = (typeof addr === 'string') ? `pipe ${addr}` : `port ${addr.port}`;
  debug(`Listening on ${bind}`);
}
```

package.jsonのscripts欄にstartコマンドを追加します。

```package.json
"scripts": {
  "start": "node dist/index.js"
},
```

index.tsで読み込んでいるAppモジュールを作成します。
POSTパラメータをJSONで取得するためのbody-parserと、
Expressのログ出力パッケージであるmorganもインストールしておきます。

```command:command
touch src/App.ts
npm install body-parser morgan --save
npm install @types/body-parser @types/morgan --save-dev
```

```App.ts
import * as path from 'path';
import * as express from 'express';
import * as logger from 'morgan';
import * as bodyParser from 'body-parser';

// Creates and configures an ExpressJS web server.
class App {

  // ref to Express instance
  public express: express.Application;

  //Run configuration methods on the Express instance.
  constructor() {
    this.express = express();
    this.middleware();
    this.routes();
  }

  // Configure Express middleware.
  private middleware(): void {
    this.express.use(logger('dev'));
    this.express.use(bodyParser.json());
    this.express.use(bodyParser.urlencoded({ extended: false }));
  }

  // Configure API endpoints.
  private routes(): void {
    /* This is just to get up and running, and to make sure what we've got is
     * working so far. This function will change when we start to add more
     * API endpoints */
    let router = express.Router();
    // placeholder route handler
    router.get('/', (req, res, next) => {
      res.json({
        message: 'Hello World!'
      });
    });
    this.express.use('/', router);
  }

}

export default new App().express;
```

ここまでで一通りWebアプリケーションサーバが作成できたので実行してみます。
gulpコマンドでコンパイルし、先ほど作成したstartコマンドで実行します。

```command:command
gulp scripts
npm start
```

ブラウザで http://localhost:3000 にアクセスし、以下のように表示されれば成功です。
![Screenshot from 2017-05-14 19-26-32.png](https://qiita-image-store.s3.amazonaws.com/0/181357/23fe6498-9af4-9fcd-3a89-8bf75ad3afed.png)

### ElectronとExpressを統合する
---

これまでElectronとExpressを個別に作成してきましたが、Electron起動時にExpressが起動するように統合します。
また、Electronのmain.jsをTypeScriptの文法に従って編集したmain.tsをsrc配下に作成します。
基本的にはElectronのmain.jsでExpressのindex.jsの内容が実行されるように編集しました。

```command:command
touch src/main.ts
```

```main.ts
import {app, BrowserWindow} from 'electron';
import * as http from 'http';
import * as debug from 'debug';

import App from './App';

export default class Main {
  static application;
  static BrowserWindow;
  static mainWindow: BrowserWindow;
  static port;
  static server;

  static main (app, browserWindow: typeof BrowserWindow) {
    Main.BrowserWindow = browserWindow;
    Main.application = app;
    Main.application.on('window-all-closed', Main.onWindowAllClosed);
    Main.application.on('ready', Main.onReady);
    Main.application.on('activate', Main.onActivate);
    Main.bootServer();
  }
    
  private static onReady() {
    Main.mainWindow = new Main.BrowserWindow({width: 800, height: 600});
    Main.mainWindow.loadURL('http://localhost:' + Main.port);
    //Main.mainWindow.loadURL('file://' + __dirname + '/index.html');
    Main.mainWindow.webContents.openDevTools();
    Main.mainWindow.on('closed', Main.onClose);
  }

  private static onWindowAllClosed() {
    if (process.platform !== 'darwin') {
       Main.application.quit();
    }   
  }

  private static onActivate() {
    if (Main.mainWindow === null) {
         Main.onReady();
    }
  }

  private static onClose() {
    // Dereference the window object.
    Main.mainWindow = null;
  }

  private static bootServer() {
    debug('ts-express:server');

    Main.port = Main.normalizePort(process.env.PORT || 3000);
    App.set('port', Main.port);

    Main.server = http.createServer(App);
    Main.server.listen(Main.port);
    Main.server.on('error', Main.onError);
    Main.server.on('listening', Main.onListening);
  }

  private static normalizePort(val: number|string): number|string|boolean {
    let port: number = (typeof val === 'string') ? parseInt(val, 10) : val;
    if (isNaN(port)) return val;
    else if (port >= 0) return port;
    else return false;
  }

  private static onError(error: NodeJS.ErrnoException): void {
    if (error.syscall !== 'listen') throw error;
    let bind = (typeof Main.port === 'string') ? 'Pipe ' + Main.port : 'Port ' + Main.port;
    switch(error.code) {
      case 'EACCES':
        console.error(`${bind} requires elevated privileges`);
        process.exit(1);
        break;
      case 'EADDRINUSE':
        console.error(`${bind} is already in use`);
        process.exit(1);
        break;
      default:
        throw error;
      }
  }

  private static onListening(): void {
    let addr = Main.server.address();
    let bind = (typeof addr === 'string') ? `pipe ${addr}` : `port ${addr.port}`;
    debug(`Listening on ${bind}`);
  }
}

Main.main(app, BrowserWindow);
```

ここまで作成したら、gulpコマンドでコンパイルしてElectronで実行してみます。

```command:command
gulp scripts
./node_modules/.bin/electron dist/main.js
```

以下のようにElectronが起動・表示されれば成功です。
![Screenshot from 2017-05-14 22-39-56.png](https://qiita-image-store.s3.amazonaws.com/0/181357/d485a251-38ce-be16-2fed-f2a4cf340880.png)

Electronとの統合が成功したらルート直下のmain.jsとindex.htmlは不要と思います。
また、package.jsonのmainタグの内容を削除し、startコマンドも編集しておきます。

```package.json
"scripts": {
  "start": "./node_modules/.bin/electron dist/main.js"
},
```

これがベストプラクティスかはわかりませんが、とりあえずここまで試せたので記載しておきます。
以下、最終的なpackage.jsonです。

```package.json
{
  "name": "electron-api",
  "version": "1.0.0",
  "description": "",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "./node_modules/.bin/electron dist/main.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@types/body-parser": "^1.16.3",
    "@types/debug": "0.0.29",
    "@types/express": "^4.0.35",
    "@types/morgan": "^1.7.32",
    "@types/node": "^7.0.18",
    "electron": "^1.6.7",
    "gulp": "^3.9.1",
    "gulp-typescript": "^3.1.6",
    "typescript": "^2.3.2"
  },
  "dependencies": {
    "body-parser": "^1.17.1",
    "debug": "^2.6.6",
    "express": "^4.15.2",
    "morgan": "^1.8.1"
  }
}
```

以上になります。次はdbアクセス周りを調べようっと。