LoginSignup
25
20

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-05-13

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
mkdir electron-api
cd electron-api
mkdir src
npm init -y

Electronのインストール


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

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
./node_modules/.bin/electron .

Screenshot from 2017-05-14 01-07-32.png

TypeScriptのインストール


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
node_modules/.bin/tsc

gulpの設定を作成します。

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
gulp

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

Express.jsのインストール


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

command
npm install express debug --save

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

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

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

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
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
gulp scripts
npm start

ブラウザで http://localhost:3000 にアクセスし、以下のように表示されれば成功です。
Screenshot from 2017-05-14 19-26-32.png

ElectronとExpressを統合する


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

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
gulp scripts
./node_modules/.bin/electron dist/main.js

以下のようにElectronが起動・表示されれば成功です。
Screenshot from 2017-05-14 22-39-56.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アクセス周りを調べようっと。

25
20
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
25
20