NW.js
ChromiumとNode.jsベースのデスクトップアプリフレームワーク。DOM APIとNode.js APIの直接統合により、Webアプリをデスクトップアプリとしてパッケージ化。Electronとの違いはメイン・レンダラープロセスの分離がない点。
GitHub概要
nwjs/nw.js
Call all Node.js modules directly from DOM/WebWorker and enable a new way of writing applications with all Web technologies.
トピックス
スター履歴
フレームワーク
NW.js
概要
NW.jsは、WebテクノロジーとNode.jsを組み合わせてクロスプラットフォームのデスクトップアプリケーションを構築するためのフレームワークです。Node.js + Chromiumの統合により、HTMLやCSS、JavaScriptといったWeb技術を使ってネイティブのデスクトップアプリケーションを開発できます。
詳細
NW.jsは2025年現在、Chromiumの最新版をベースとした安定したデスクトップアプリケーション開発フレームワークです。元々「node-webkit」として知られていましたが、現在はNW.jsとして開発されています。
NW.jsの主要な特徴:
- Node.js統合: DOMから直接Node.jsモジュールにアクセス可能
- ネイティブAPI: OSの機能にダイレクトアクセス(ファイルシステム、トレイアイコンなど)
- 軽量パッケージ: Electronと比較してバイナリサイズが小さい
- デバッグ機能: Chrome DevToolsによる強力なデバッグ環境
- マルチプラットフォーム: Windows、macOS、Linux対応
- グローバルショートカット: システム全体で動作するホットキー機能
Electronとの主な違いは、メインプロセスとレンダラープロセスの分離がなく、より簡潔なアーキテクチャを採用していることです。これにより開発者は複雑なプロセス間通信を意識することなく、シンプルにデスクトップアプリケーションを開発できます。
メリット・デメリット
メリット
- 学習コストの低さ: Web開発のスキルをそのまま活用可能
- 開発効率: HTMLとJavaScriptによる迅速な開発
- Node.js直接アクセス: レンダラーからNode.jsモジュールを直接使用
- 軽量: Electronと比較してシンプルなアーキテクチャ
- 豊富なAPIセット: ネイティブ機能への包括的なアクセス
- コードの一元管理: フロントエンドとバックエンドの境界がない
- デバッグ環境: Chrome DevToolsによる優れた開発体験
- 自動更新機能: アプリケーションの更新配信サポート
デメリット
- セキュリティリスク: DOMからNode.jsへの直接アクセスによる潜在的脆弱性
- 大きなバンドルサイズ: Chromiumエンジンを含むため配布ファイルが大きい
- メモリ使用量: ネイティブアプリと比較して多くのメモリを消費
- Electronとの差別化: 市場でのシェアが相対的に小さい
- プロセス分離なし: 複雑なアプリでのプロセス管理が困難
- セキュリティモデル: Webセキュリティの考慮が必要
主要リンク
書き方の例
基本的なNW.jsアプリケーション
// package.json - アプリケーションマニフェスト
{
"name": "hello-nwjs",
"version": "1.0.0",
"main": "index.html",
"window": {
"title": "Hello NW.js",
"width": 800,
"height": 600,
"min_width": 400,
"min_height": 300,
"icon": "app.png"
}
}
<!-- index.html - メインウィンドウ -->
<!DOCTYPE html>
<html>
<head>
<title>Hello NW.js World!</title>
<meta charset="utf-8">
<style>
body {
font-family: 'Arial', sans-serif;
text-align: center;
padding: 50px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
margin: 0;
}
.container {
background: rgba(255, 255, 255, 0.1);
padding: 30px;
border-radius: 10px;
backdrop-filter: blur(10px);
}
button {
background: #4CAF50;
color: white;
border: none;
padding: 15px 32px;
margin: 10px;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
}
button:hover {
background: #45a049;
}
.info {
margin: 20px 0;
padding: 15px;
background: rgba(255, 255, 255, 0.2);
border-radius: 5px;
}
</style>
</head>
<body>
<div class="container">
<h1>Hello NW.js World!</h1>
<div class="info">
<p>Node.js バージョン: <span id="node-version"></span></p>
<p>Chromium バージョン: <span id="chromium-version"></span></p>
<p>OS プラットフォーム: <span id="platform"></span></p>
</div>
<button onclick="showNotification()">通知を表示</button>
<button onclick="openDevTools()">開発者ツール</button>
<button onclick="minimizeWindow()">最小化</button>
<button onclick="closeApp()">アプリを終了</button>
</div>
<script>
// Node.jsモジュールの直接使用
const os = require('os');
const path = require('path');
const fs = require('fs');
// バージョン情報の表示
document.getElementById('node-version').textContent = process.version;
document.getElementById('chromium-version').textContent = process.versions.chromium;
document.getElementById('platform').textContent = os.platform();
// 通知機能
function showNotification() {
new Notification('NW.js 通知', {
body: 'Hello from NW.js アプリケーション!',
icon: 'app.png'
});
}
// 開発者ツールを開く
function openDevTools() {
nw.Window.get().showDevTools();
}
// ウィンドウの最小化
function minimizeWindow() {
nw.Window.get().minimize();
}
// アプリケーション終了
function closeApp() {
nw.App.quit();
}
// ファイルシステムアクセスの例
function readConfigFile() {
const configPath = path.join(__dirname, 'config.json');
try {
const config = fs.readFileSync(configPath, 'utf8');
console.log('設定ファイル:', JSON.parse(config));
} catch (error) {
console.log('設定ファイルが見つかりません');
}
}
// アプリ起動時の初期化
readConfigFile();
</script>
</body>
</html>
ファイル操作とシステム連携
<!-- file-manager.html -->
<!DOCTYPE html>
<html>
<head>
<title>File Manager - NW.js</title>
<meta charset="utf-8">
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
}
.toolbar {
margin-bottom: 20px;
padding: 10px;
background: #f0f0f0;
border-radius: 5px;
}
.file-list {
border: 1px solid #ddd;
height: 400px;
overflow-y: auto;
padding: 10px;
}
.file-item {
padding: 5px;
border-bottom: 1px solid #eee;
cursor: pointer;
}
.file-item:hover {
background: #f5f5f5;
}
.directory {
font-weight: bold;
color: #0066cc;
}
.file {
color: #333;
}
button {
margin: 5px;
padding: 8px 16px;
border: none;
background: #007cba;
color: white;
border-radius: 3px;
cursor: pointer;
}
</style>
</head>
<body>
<h1>NW.js ファイルマネージャー</h1>
<div class="toolbar">
<button onclick="selectDirectory()">フォルダを選択</button>
<button onclick="goHome()">ホームディレクトリ</button>
<button onclick="goUp()">上位フォルダ</button>
<button onclick="createFolder()">新規フォルダ</button>
<input type="file" id="fileInput" multiple style="display: none;">
<button onclick="document.getElementById('fileInput').click()">ファイル選択</button>
</div>
<div>現在のパス: <span id="currentPath"></span></div>
<div class="file-list" id="fileList">
<!-- ファイル一覧がここに表示されます -->
</div>
<div id="status"></div>
<script>
const fs = require('fs');
const path = require('path');
const os = require('os');
let currentDirectory = os.homedir();
// 初期化
updateFileList();
function updateFileList() {
const fileList = document.getElementById('fileList');
const currentPathSpan = document.getElementById('currentPath');
currentPathSpan.textContent = currentDirectory;
fileList.innerHTML = '';
try {
const items = fs.readdirSync(currentDirectory);
items.forEach(item => {
const itemPath = path.join(currentDirectory, item);
const stats = fs.statSync(itemPath);
const div = document.createElement('div');
div.className = 'file-item';
if (stats.isDirectory()) {
div.className += ' directory';
div.textContent = `📁 ${item}`;
div.onclick = () => {
currentDirectory = itemPath;
updateFileList();
};
} else {
div.className += ' file';
const size = (stats.size / 1024).toFixed(1);
div.textContent = `📄 ${item} (${size} KB)`;
div.onclick = () => openFile(itemPath);
}
fileList.appendChild(div);
});
} catch (error) {
fileList.innerHTML = `<div style="color: red;">エラー: ${error.message}</div>`;
}
}
function selectDirectory() {
const input = document.createElement('input');
input.type = 'file';
input.nwdirectory = true;
input.onchange = function() {
if (this.value) {
currentDirectory = this.value;
updateFileList();
}
};
input.click();
}
function goHome() {
currentDirectory = os.homedir();
updateFileList();
}
function goUp() {
const parentDir = path.dirname(currentDirectory);
if (parentDir !== currentDirectory) {
currentDirectory = parentDir;
updateFileList();
}
}
function createFolder() {
const folderName = prompt('新しいフォルダ名を入力してください:');
if (folderName) {
const newFolderPath = path.join(currentDirectory, folderName);
try {
fs.mkdirSync(newFolderPath);
updateFileList();
showStatus(`フォルダ "${folderName}" を作成しました`);
} catch (error) {
showStatus(`エラー: ${error.message}`, 'error');
}
}
}
function openFile(filePath) {
// OSのデフォルトアプリケーションでファイルを開く
nw.Shell.openItem(filePath);
}
function showStatus(message, type = 'info') {
const status = document.getElementById('status');
status.textContent = message;
status.style.color = type === 'error' ? 'red' : 'green';
setTimeout(() => {
status.textContent = '';
}, 3000);
}
// ファイル選択の処理
document.getElementById('fileInput').onchange = function() {
const files = Array.from(this.files);
console.log('選択されたファイル:', files.map(f => f.path));
showStatus(`${files.length}個のファイルが選択されました`);
};
// ドラッグ&ドロップサポート
document.addEventListener('dragover', function(e) {
e.preventDefault();
});
document.addEventListener('drop', function(e) {
e.preventDefault();
const files = Array.from(e.dataTransfer.files);
console.log('ドロップされたファイル:', files.map(f => f.path));
showStatus(`${files.length}個のファイルがドロップされました`);
});
</script>
</body>
</html>
システムトレイ統合とグローバルショートカット
// システムトレイとショートカット管理クラス
class NWJSSystemIntegration {
constructor() {
this.tray = null;
this.registeredShortcuts = [];
this.gui = nw;
this.win = this.gui.Window.get();
this.init();
}
init() {
// ウィンドウイベントの設定
this.win.on('minimize', () => {
console.log('ウィンドウが最小化されました');
});
this.win.on('close', () => {
if (this.tray) {
this.win.hide();
return false; // ウィンドウを閉じずに隠す
}
this.cleanup();
});
// 初期化完了
console.log('システム統合が初期化されました');
}
createTrayIcon() {
if (this.tray) {
console.log('トレイアイコンは既に作成されています');
return;
}
// トレイアイコンの作成
this.tray = new this.gui.Tray({
title: 'NW.js App',
icon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAA7EAAAOxAGVKw4bAAAAM0lEQVRYhe3QMQEAAAjDMPxr/hMXAIEP0lPaAgAAAAAAAAAAAAAAAAAAAAAAAAAA8P4BAEoAHpvQZd8AAAAASUVORK5CYII='
});
// トレイメニューの作成
const menu = new this.gui.Menu();
menu.append(new this.gui.MenuItem({
label: 'アプリを表示',
click: () => this.showWindow()
}));
menu.append(new this.gui.MenuItem({
label: '設定',
click: () => {
this.showWindow();
console.log('設定画面を開く(未実装)');
}
}));
menu.append(new this.gui.MenuItem({
type: 'separator'
}));
menu.append(new this.gui.MenuItem({
label: '終了',
click: () => this.exitApp()
}));
this.tray.menu = menu;
// トレイアイコンのクリックイベント
this.tray.on('click', () => {
this.showWindow();
});
console.log('トレイアイコンが作成されました');
}
registerGlobalShortcuts() {
const shortcuts = [
{
key: 'Ctrl+Shift+H',
action: () => {
if (this.win.isVisible()) {
this.win.hide();
} else {
this.showWindow();
}
}
},
{
key: 'Ctrl+Shift+Q',
action: () => this.exitApp()
},
{
key: 'Ctrl+Shift+N',
action: () => this.showNotification()
}
];
shortcuts.forEach(shortcut => {
try {
const globalShortcut = new this.gui.Shortcut({
key: shortcut.key,
active: shortcut.action,
failed: (msg) => {
console.error(`ショートカット "${shortcut.key}" の登録に失敗: ${msg}`);
}
});
this.gui.App.registerGlobalHotKey(globalShortcut);
this.registeredShortcuts.push(globalShortcut);
} catch (error) {
console.error(`ショートカット "${shortcut.key}" の登録エラー:`, error);
}
});
console.log(`${this.registeredShortcuts.length}個のショートカットが登録されました`);
}
showNotification() {
const notification = new Notification('NW.js システム通知', {
body: 'グローバルショートカットから通知が送信されました',
icon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAA7EAAAOxAGVKw4bAAAAM0lEQVRYhe3QMQEAAAjDMPxr/hMXAIEP0lPaAgAAAAAAAAAAAAAAAAAAAAAAAAAA8P4BAEoAHpvQZd8AAAAASUVORK5CYII=',
tag: 'nwjs-notification'
});
notification.onclick = () => {
this.showWindow();
};
}
showWindow() {
if (!this.win.isVisible()) {
this.win.show();
}
this.win.restore();
this.win.focus();
}
cleanup() {
// ショートカットの解除
this.registeredShortcuts.forEach(shortcut => {
try {
this.gui.App.unregisterGlobalHotKey(shortcut);
} catch (error) {
console.error('ショートカット解除エラー:', error);
}
});
// トレイアイコンの削除
if (this.tray) {
this.tray.remove();
this.tray = null;
}
console.log('システム統合がクリーンアップされました');
}
exitApp() {
this.cleanup();
this.gui.App.quit();
}
}
// 使用例
const systemIntegration = new NWJSSystemIntegration();
systemIntegration.createTrayIcon();
systemIntegration.registerGlobalShortcuts();
ネイティブメニューとウィンドウ管理
// ネイティブメニューとウィンドウ管理システム
class NWJSMenuManager {
constructor() {
this.gui = nw;
this.win = this.gui.Window.get();
this.menuBar = null;
this.createMenuBar();
}
createMenuBar() {
// メインメニューの作成
this.menuBar = new this.gui.Menu({ type: 'menubar' });
// ファイルメニュー
const fileMenu = new this.gui.Menu();
fileMenu.append(new this.gui.MenuItem({
label: '新規ウィンドウ',
accelerator: 'CmdOrCtrl+N',
click: () => this.createNewWindow()
}));
fileMenu.append(new this.gui.MenuItem({
type: 'separator'
}));
fileMenu.append(new this.gui.MenuItem({
label: '終了',
accelerator: 'CmdOrCtrl+Q',
click: () => this.gui.App.quit()
}));
// 表示メニュー
const viewMenu = new this.gui.Menu();
viewMenu.append(new this.gui.MenuItem({
label: 'フルスクリーン',
accelerator: 'F11',
click: () => this.toggleFullscreen()
}));
viewMenu.append(new this.gui.MenuItem({
label: '開発者ツール',
accelerator: 'F12',
click: () => this.win.showDevTools()
}));
viewMenu.append(new this.gui.MenuItem({
type: 'separator'
}));
viewMenu.append(new this.gui.MenuItem({
label: '最前面表示',
type: 'checkbox',
checked: this.win.isAlwaysOnTop,
click: () => this.toggleAlwaysOnTop()
}));
// ウィンドウメニュー
const windowMenu = new this.gui.Menu();
windowMenu.append(new this.gui.MenuItem({
label: '最小化',
accelerator: 'CmdOrCtrl+M',
click: () => this.win.minimize()
}));
windowMenu.append(new this.gui.MenuItem({
label: '最大化',
click: () => this.toggleMaximize()
}));
windowMenu.append(new this.gui.MenuItem({
type: 'separator'
}));
windowMenu.append(new this.gui.MenuItem({
label: '中央に配置',
click: () => this.centerWindow()
}));
// ヘルプメニュー
const helpMenu = new this.gui.Menu();
helpMenu.append(new this.gui.MenuItem({
label: 'NW.jsについて',
click: () => {
console.log(`NW.js ${process.versions.nw} - Node.js ${process.version}`);
}
}));
// メニューバーに追加
this.menuBar.append(new this.gui.MenuItem({ label: 'ファイル', submenu: fileMenu }));
this.menuBar.append(new this.gui.MenuItem({ label: '表示', submenu: viewMenu }));
this.menuBar.append(new this.gui.MenuItem({ label: 'ウィンドウ', submenu: windowMenu }));
this.menuBar.append(new this.gui.MenuItem({ label: 'ヘルプ', submenu: helpMenu }));
// メニューバーを適用
this.win.menu = this.menuBar;
}
createNewWindow() {
this.gui.Window.open('index.html', {
title: 'New Window',
width: 600,
height: 400,
position: 'center'
});
}
toggleFullscreen() {
if (this.win.isFullscreen) {
this.win.leaveFullscreen();
} else {
this.win.enterFullscreen();
}
}
toggleMaximize() {
if (this.win.isMaximized) {
this.win.unmaximize();
} else {
this.win.maximize();
}
}
toggleAlwaysOnTop() {
const newState = !this.win.isAlwaysOnTop;
this.win.setAlwaysOnTop(newState);
}
centerWindow() {
const screen = this.gui.Screen.screens[0];
const x = Math.round((screen.bounds.width - this.win.width) / 2);
const y = Math.round((screen.bounds.height - this.win.height) / 2);
this.win.moveTo(x, y);
}
}
// 使用例
const menuManager = new NWJSMenuManager();
パフォーマンス最適化
// パフォーマンス監視と最適化
class NWJSPerformanceOptimizer {
constructor() {
this.memoryMonitor = null;
this.performanceMetrics = {
memory: [],
cpu: [],
fps: []
};
this.init();
}
init() {
console.log('Performance Optimizer 初期化中...');
// メモリ監視の開始
this.startMemoryMonitoring();
// ガベージコレクション最適化
this.optimizeGarbageCollection();
console.log('Performance Optimizer 初期化完了');
}
startMemoryMonitoring() {
this.memoryMonitor = setInterval(() => {
const memInfo = process.memoryUsage();
this.performanceMetrics.memory.push({
timestamp: Date.now(),
rss: memInfo.rss,
heapTotal: memInfo.heapTotal,
heapUsed: memInfo.heapUsed,
external: memInfo.external
});
// 最大1000件のデータを保持
if (this.performanceMetrics.memory.length > 1000) {
this.performanceMetrics.memory.shift();
}
// メモリ使用量が閾値を超えた場合の警告
const memoryMB = memInfo.heapUsed / 1024 / 1024;
if (memoryMB > 500) { // 500MB超過時
console.warn(`高メモリ使用量検出: ${memoryMB.toFixed(2)} MB`);
this.triggerMemoryCleanup();
}
}, 5000); // 5秒間隔で監視
}
optimizeGarbageCollection() {
// V8エンジンのガベージコレクション設定
if (global.gc) {
// 定期的なガベージコレクション実行
setInterval(() => {
if (global.gc) {
global.gc();
console.log('ガベージコレクション実行');
}
}, 60000); // 1分間隔
}
}
triggerMemoryCleanup() {
console.log('メモリクリーンアップ開始...');
// DOMの不要な要素を削除
const unusedElements = document.querySelectorAll('.temp, .cached, [data-cleanup="true"]');
unusedElements.forEach(element => {
element.remove();
});
// Canvas要素のクリーンアップ
const canvases = document.querySelectorAll('canvas');
canvases.forEach(canvas => {
const ctx = canvas.getContext('2d');
if (ctx) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
});
// 強制ガベージコレクション
if (global.gc) {
global.gc();
}
console.log('メモリクリーンアップ完了');
}
getPerformanceReport() {
const latest = this.performanceMetrics.memory.slice(-1)[0];
return {
memory: {
current: latest ? (latest.heapUsed / 1024 / 1024).toFixed(2) + ' MB' : 'N/A',
peak: Math.max(...this.performanceMetrics.memory.map(m => m.heapUsed)) / 1024 / 1024,
average: this.performanceMetrics.memory.reduce((sum, m) => sum + m.heapUsed, 0)
/ this.performanceMetrics.memory.length / 1024 / 1024
},
uptime: process.uptime()
};
}
destroy() {
if (this.memoryMonitor) {
clearInterval(this.memoryMonitor);
this.memoryMonitor = null;
}
console.log('Performance Optimizer 停止');
}
}
// 使用例
const performanceOptimizer = new NWJSPerformanceOptimizer();
// パフォーマンスレポートの表示
setInterval(() => {
const report = performanceOptimizer.getPerformanceReport();
console.log('パフォーマンスレポート:', report);
}, 30000); // 30秒間隔
NW.jsは、WebテクノロジーとNode.jsの強力な組み合わせにより、デスクトップアプリケーション開発の新しい可能性を提供します。Electronとは異なるシンプルなアーキテクチャで、Web開発者が容易にネイティブアプリケーションを構築できる優れたフレームワークです。