Webの技術でローカルアプリが作れるNW.js
Posted
Webの技術でローカルアプリが作れるNW.js
NW.js (node-webkit js) は、js
,html
,css
でローカル環境で動くデスクトップアプリケーションが作れるビルドツール(というのが正確なのかはわからない)。
実際には、Chromium
と node.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.json
とsrc
ディレクトリ内にあるpackage.json
だと思う。
root/package.json
: npm用のpackage.jsonroot/src/package.json
: nwjsが読み取る マニフェストファイル と呼ばれるファイル
名前が同じだけで、npm用とnwjs用で、全く別物である。
npmのpackage.json
は、npm init
で作れば良いし、nw用のpackage.json
は、今はとりあえず空のファイルを作っておけば良い。あとで書く。
nwjs関連のパッケージをインストール
nwjsを使うには以下の2つのパッケージが必要になる。
- nw
- nw-builder
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"
}
}
name
とversion
は必須で、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 Context
と Node Context
が存在している。
ブラウザコンテキスト、ノードコンテキストの違いにより、例えばブラウザコンテキストではWebAPI
を含むコンテキストが利用が出来る。しかし、ノードコンテキストにあるような__dirname
、process
、Buffer
などは使えない。逆に、ノードコンテキスト上でdocument
やwindow
などは使うことが出来ない。
実際には、これらを意識して書かないといけない。また、必要であればnwjsが用意したnw
オブジェクトを利用してブラウザコンテキストからノードコンテキスト(Nodeオブジェクト)にアクセスしないといけない。
余談だが、複数のコンテキストによってアプリが動くのは、nwjsや先に少し触れたelectronなどに限ったことではない。
例えば、WebAPI
にはWeb Worker API
とよばれる機能がある。これは、メインとは別のスレッドを走らせて、重たい処理などをそちらで非同期的に行ってもらい、メインのスレッドの処理が止まらないようにするための仕組みだ。Web Worker
を使うと、新しいスレッドを生成し、この中でjsを走らせる。この時、新しいスレッドはWeb Worker Context
上で走る。つまり、ブラウザのメインスレッドとは別のコンテキスト上で走ることになり、Worker内からDOMを触るようなことは制限される。
nwjsで2つのコンテキストが走るのも、これと同じ関係だと考えて良い。
どちらのコンテキストで実行されるのか
2つのコンテキストを意識しないといけないのは分かったが、どちらのコンテキストで実行されるのかという話になる。
ブラウザコンテキスト
次の方法によって読み込まれたものはブラウザコンテキストとして実行される。
<script>
により読み込まれたjsファイル- JQuery
$.getScript()
(JQueryのスクリプト読込用関数)- その他、
RequireJS
の仕様など、従来のWebの方法で読み込まれたスクリプト又は直接埋め込まれたスクリプト
htmlの<script>
タグで読み込まれたjsファイル、さらにそこから要求されたjsファイルはWebコンテキストで走るのだと思えば良い。
つまり普通にWebアプリを作る感覚で書けば、ファイルの保存などのローカルファイルの操作をしない部分ではブラウザコンテキスト上を走ると思って良い。
ノードコンテキスト
前述したが、普通にWebアプリを作る感覚で書けば、基本的にはブラウザコンテキスト上で走る。 NW.jsは、指定されたものをノードコンテキスト上で走らせる。では、 指定されたもの とは何かと言うと、次の方法によって読み込まれたものはノードコンテキストとして実行される
require()
によってロードされたNode.jsのモジュール- マニフェストファイル内で指定されたスクリプト(
node-main
)
ひとづずつ見ていく。
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が用意した特別な方法により 別コンテキストのオブジェクトを参照しなくてはならない。
ブラウザコンテキストからノードコンテキストのオブジェクトにアクセスする方法は、以下のような方法がある。以下の方法は、全てブラウザコンテキスト上で行う操作になる。
require()
によってNode.js外部モジュールを取り込む(説明済み)nw
のAPIを叩くnw.global
または単にglobal
で、ノードコンテキストのグローバル変数にアクセスできるnw.process
または単にprocess
で、ノードコンテキストのprocess
にアクセスできる- ブラウザコンテキスト上では使えないはずの
Buffer
が使える。これも、ノードコンテキストにアクセスしている。
これらの方法を利用して、ロジックが走っているノードコンテキストから必要な値を参照し、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"
}
}
上記のサンプルでは、
main.html
が読み込まれるmain.html
から、js-browser/main.js
が読み込まれる(js-browser/main.js
はブラウザコンテキスト上で走る)js-browser/main.js
からNode.jsのモジュールであるjs-node/main.js
が読み込まれる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
の中に含まれるようにしてしまう。こうすると、ソースがもろに裸になるのを防げるらしい。