NW.js

ChromiumとNode.jsベースのデスクトップアプリフレームワーク。DOM APIとNode.js APIの直接統合により、Webアプリをデスクトップアプリとしてパッケージ化。Electronとの違いはメイン・レンダラープロセスの分離がない点。

デスクトップJavaScriptNode.jsChromiumクロスプラットフォームハイブリッド

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.

ホームページ:https://nwjs.io
スター40,961
ウォッチ1,559
フォーク3,882
作成日:2012年1月4日
言語:JavaScript
ライセンス:MIT License

トピックス

desktopjavascriptnode-webkitnodejsnwjsweb-application-framework

スター履歴

nwjs/nw.js Star History
データ取得日時: 2025/7/19 10:37

フレームワーク

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開発者が容易にネイティブアプリケーションを構築できる優れたフレームワークです。