Webの技術でローカルアプリが作れるNW.js

js_ts

Webの技術でローカルアプリが作れるNW.js

https://nwjs.io/

NW.js (node-webkit js) は、js,html,cssでローカル環境で動くデスクトップアプリケーションが作れるビルドツール(というのが正確なのかはわからない)。

実際には、Chromiumnode.js を使ってコードを動かす環境を使っている。

jsはnode.jsやブラウザ環境(es6 etc)などの環境により、結構仕様が変わってめんどくさいのはいつものこと。nwjsも例に漏れず、この違いを意識して書かないといけない。

では、nwjsはどうなのかというと、nwjsではWeb用のコンテキストとノード用のコンテキストが並行して動く。Web用のコンテキストがWebで言うところのフロントエンドで、ノード用のコンテキストがバックエンドだと思うとイメージしやすい(実際は違うけど)。

Webでフロントエンドとバックエンドがhttp(s)を利用して通信してアプリケーションが完成するように、NW.jsではそれぞれのコンテキストがプロセス間通信をすることによってアプリケーションが完成する。プロセス間通信はそれ用の仕組みが用意されておりデータを受け渡しすること自体は難しくないので安心して欲しい。

コンテキストの話はこの後もう一度書く。

Getting Start

ともあれ、軽く使ってみる。

1.プロジェクトディレクトリを用意する

htmlファイルやcss、jsファイルは普通にウェブアプリを書くときと同じように書けば問題ない。何でも良いので簡単なプロジェクトを用意する。

[]project-root
├ []src
│ ├ []app
│ │ └ main.js
│ ├ []assets
│ │ └ icon.png
│ ├ []styles
│ │ └ common.css
│ ├ []views
│ │ └ main.html
│ └ package.json
└ package.json

紛らわしいのは、プロジェクトのルートにあるpackage.jsonsrcディレクトリ内にあるpackage.jsonだと思う。

名前が同じだけで、npm用とnwjs用で、全く別物である。

npmのpackage.jsonは、npm initで作れば良いし、nw用のpackage.jsonは、今はとりあえず空のファイルを作っておけば良い。あとで書く。

nwjs関連のパッケージをインストール

nwjsを使うには以下の2つのパッケージが必要になる。

nwは、その場でさっとアプリを立ち上げて動作を確認できる開発用のコマンドで、nw-builderが本番用にビルドするためのコマンドである。

$ npm install --save-dev nw nw-builder

(開発環境のみで必要なのでsave-devオプション付けてるけど、実際はビルド済みパッケージのみが配布物に成ると思うので、どっちでも良いよ)

インストールしたコマンドを実行できるように、スクリプトを登録しておこう。

root/package.json

{
  "name": "example",
  "version": "1.0.0",
  "description": "test project for my first nwjs app",
  "devDependencies": {
    "nw": "^0.44.3",
    "nw-builder": "^3.1.2"
  },
  "scripts":{
    "dev":"nw src/",
    "build":"nwbuild --platforms win32,win64,osx64,linux32,linux64 --buildDir dist/ src/"
  }
}

マニフェストファイルを書く

root/src/package.jsonは、npm用のroot/package.jsonとは違い、nwjs用のマニフェストファイル(ビルドツールに指示を与えるファイル)だ。次はコレを書いていく。

とはいっても、やはり色々設定があってここで書ききることも、全ての機能を触りきることも出来ないので、最小限だけ書く。詳しく知りたい場合は下記のURL参照。

http://docs.nwjs.io/en/latest/References/Manifest%20Format/

root/src/package.json

{
  "name":"example",
  "version": "1.0.0",
  "main":"views/main.html",
  "window":{
    "min_width":400,
    "min_height":400,
    "icon":"assets/icon.png"
  }
}

nameversionは必須で、npm用のpackage.jsonの値に揃えておけば問題ないだろう。他は、だいたい項目名から推測できるような設定で、mainはアプリが立ち上がって最初に開くファイル。windowはアプリのウィンドウの設定ファイルだ。

nwで立ち上げ

本番環境のビルドは時間がかかって大変だし、無駄に成果物を排出するので、その場でさっと確認するときにはnwコマンドを使う。このコマンドをさっと使えるように、スクリプトに既に登録していることを思い出してほしい。

登録するスクリプトを打てばよいだけなので、プロジェクトルートで下記のコマンドを実行。

$ npm run dev

新しいウィンドウが開き、アプリが立ち上がることが確認できる。

nwbuildでビルド

本番環境のビルド用のコマンドも、すでにスクリプトに登録済みなので、プロジェクトルートで下記コマンドを実行すればよいだけ。

$ npm run build

実行すると、各プラットホームのビルド用にsdkをそれぞれダウンロードするので、全てのプラットホームをせんたくしている場合には結構時間がかかる。

終わったら、distディレクトリが生成されている。linuxの場合、その中でプロジェクト名(拡張子なし)のファイルがあると思うので、それが実行ファイルになる。実行すると、アプリが開く。

所感

似たようなプロジェクトとしてelectronがある。electronもWeb用のコンテキストとノード用のコンテキストを走らせることでローカルアプリを作る仕組みになっているが、NW.jsの方がコンテキストの仕分けが緩いなどの違いがある。

マニフェストファイルを作るだけと簡単。構造がシンプルということは、流用も簡単ということだ。普通にwebアプリを作ったものをスタンドアロン化したい、スタンドアロンなんだけど機能をwebにも公開したいなどの場合は、これで十分かもしれない。

コンテキスト

やはりコンテキストから逃げることは出来ない。

nwjsでは、nodeのモジュールを利用することが出来る。しかし、実際にアプリがGUIとして動くときにはhtml5ベースのアプリとして起動する。つまり、nwjsにはBrowser ContextNode Context が存在している。

ブラウザコンテキスト、ノードコンテキストの違いにより、例えばブラウザコンテキストではWebAPIを含むコンテキストが利用が出来る。しかし、ノードコンテキストにあるような__dirnameprocessBufferなどは使えない。逆に、ノードコンテキスト上でdocumentwindowなどは使うことが出来ない。

実際には、これらを意識して書かないといけない。また、必要であればnwjsが用意したnwオブジェクトを利用してブラウザコンテキストからノードコンテキスト(Nodeオブジェクト)にアクセスしないといけない。

余談だが、複数のコンテキストによってアプリが動くのは、nwjsや先に少し触れたelectronなどに限ったことではない。 例えば、WebAPIにはWeb Worker APIとよばれる機能がある。これは、メインとは別のスレッドを走らせて、重たい処理などをそちらで非同期的に行ってもらい、メインのスレッドの処理が止まらないようにするための仕組みだ。Web Workerを使うと、新しいスレッドを生成し、この中でjsを走らせる。この時、新しいスレッドはWeb Worker Context上で走る。つまり、ブラウザのメインスレッドとは別のコンテキスト上で走ることになり、Worker内からDOMを触るようなことは制限される。 nwjsで2つのコンテキストが走るのも、これと同じ関係だと考えて良い。

どちらのコンテキストで実行されるのか

2つのコンテキストを意識しないといけないのは分かったが、どちらのコンテキストで実行されるのかという話になる。

ブラウザコンテキスト

次の方法によって読み込まれたものはブラウザコンテキストとして実行される。

htmlの<script>タグで読み込まれたjsファイル、さらにそこから要求されたjsファイルはWebコンテキストで走るのだと思えば良い。

つまり普通にWebアプリを作る感覚で書けば、ファイルの保存などのローカルファイルの操作をしない部分ではブラウザコンテキスト上を走ると思って良い。

ノードコンテキスト

前述したが、普通にWebアプリを作る感覚で書けば、基本的にはブラウザコンテキスト上で走る。 NW.jsは、指定されたものをノードコンテキスト上で走らせる。では、 指定されたもの とは何かと言うと、次の方法によって読み込まれたものはノードコンテキストとして実行される

ひとづずつ見ていく。

require() によってロードされたNode.jsの外部モジュール

NW.jsのブラウザコンテキスト上で走っているjsの中では、require()によりNode.jsの外部モジュール、つまり、一般的にはnode_modulesの中に含まれているようなNode.jsモジュールを使用することが出来る。このモジュールは、require()元のブラウザコンテキスト上で走るのではなく、ノードコンテキスト上で実行される。例えば、以下のような場合…

// これはブラウザコンテキスト上で走るjs

// requireによりnode_modules内のNode.jsモジュールを読み込める
// 実際には、このモジュールの中身はノードコンテキスト上で実行されるので、モジュールを読み込んでいると言うよりは
// 他コンテキスト上で走るモジュールを操作するための窓口を取得していると言ったほうが正しいのかもしれない。
const module = reuqire('some_module');

// moudle.someFunction()はノードコンテキストで走る
const reurnVal = moudle.someFunction();

ブラウザコンテキスト上でmoudle.someFunction()をしているが、この関数の中身はノードコンテキスト上で走っており、戻値がブラウザコンテキストに渡され、ブラウザコンテキスト上で扱うことができる。

これを見て分かるとおり、NW.jsはブラウザコンテキスト上で走るファイル内で直接ノードコンテキストで走る処理を指示するような形で書ける。

何も考えなければこの仕組みは便利なものに見えるかもしれないが、コンテキスト間では単なるデータしか送りあえないような厳密なコンテキストの仕分けよりも、むしろコンテキストを意識して書かないとそのうち訳がわからなくなりそうでもある。

マニフェストファイル内で指定されたスクリプト(node-main)

マニフェストファイル(package.json)でnode-mainとして指定されたjsファイルは、ウィンドウが立ち上がる前にノードコンテキスト上で実行される。

package.json(マニフェストファイル)

{
  "name":"my_app",
  "version": "1.0.0",
  "main":"views/main.html",
  "node-main":"node/main.js",
  "window":{
    "toolbar": true,
    "min_width":400,
    "min_height":400,
    "icon":"assets/icon.png"
  }
}

ブラウザコンテキストからノードコンテキストのオブジェクトやNW.js-APIにアクセスする

NW.jsで作られるのは、デスクトップアプリなので、基本的にはNode.js(つまりノードコンテキスト上で走らせるプログラム)でアプリを組み立てて、guiに絡む部分だけブラウザコンテキストが値を参照して表示する、というのが望ましい。

それぞれのコンテキストは原則として独立している。つまり、 NW.jsが用意した特別な方法により 別コンテキストのオブジェクトを参照しなくてはならない。

ブラウザコンテキストからノードコンテキストのオブジェクトにアクセスする方法は、以下のような方法がある。以下の方法は、全てブラウザコンテキスト上で行う操作になる。

これらの方法を利用して、ロジックが走っているノードコンテキストから必要な値を参照し、guiアプリを組み立てる。

コンテキストが混ざってしまわないために

コンテキストの違いは書いたが、例えば全てのjsファイルをsrc/jsのようなディレクトリに放り込んでしまうと、ブラウザコンテキスト上で走るjsファイルと、ノードコンテキスト上で走るjsファイルが混在してしまって厄介。

きちんとルールを作って分離、管理する必要がある。

Pattern1.ブラウザコンテキストからrequire()で読込実行する

個人的にはとりあえずこのパターンで良いと思う方法。

1つめのパターンとしては、ブラウザコンテキストからrequireでノードコンテキストのエントリーポイントを取り込み、実行する方法だ。

サンプルとしては、下記のようなプロジェクトになる。

[]project-root
├ []src
│ ├ []js-browser
│ │ └ main.js
│ ├ []js-node
│ │ └ main.js
│ ├ []assets
│ │ └ icon.png
│ ├ []styles
│ │ └ common.css
│ ├ []views
│ │ └ main.html
│ └ package.json
└ package.json

src/views/main.html

<html>
  <head>
    <meta charset="utf-8">
    <title>my_app</title>
    <script type="text/javascript" src="../js-browser/main.js"></script>
  </head>
  
  <body>
    <h1>my app</h1>
  </body>
</html>

src/js-browser/main.js

// htmlにとりこまれたあとに実行されるので、htmlからのパス
const nodeContextEntry = require('../js-node/main');

// 実行
nodeContextEntry();

src/js-node/main.js

// node.jsのモジュールにする
module.exports = () => {
  // ブラウザコンテキストからは__dirnameは使えないので、これが使えることにより確認する
  setInterval(() => {
    console.log(`dirname : ${__dirname}`);
  }, 5000);
};

package.json(マニフェストファイル)

{
  "name":"my_app",
  "version": "1.0.0",
  "main":"views/main.html",
  "window":{
    "toolbar": true,
    "min_width":400,
    "min_height":400,
    "icon":"assets/icon.png"
  }
}

上記のサンプルでは、

  1. main.htmlが読み込まれる
  2. main.htmlから、js-browser/main.jsが読み込まれる(js-browser/main.jsはブラウザコンテキスト上で走る)
  3. js-browser/main.jsからNode.jsのモジュールであるjs-node/main.jsが読み込まれる
  4. js-browser/main.js内でjs-node/main.jsが実行される(js-node/main.jsが実行される)

となる。

Pattern2. main-nodeで実行する

マニフェストファイルでmain-nodeによりノードコンテキスト上でjsを走らせる。guiが必要な値は、globalでブラウザコンテキスト上から参照できるようにするなどする。

Pattern3. 全部外部モジュールにする

理屈はPattern1と同じだが、node部分を全部外部モジュールに分離してnode_modulesの中に含まれるようにしてしまう。こうすると、ソースがもろに裸になるのを防げるらしい。