Разработка десктоп-приложения на NW.js
NW.js (ранее node-webkit) — фреймворк для десктоп-приложений, объединяющий Node.js и Chromium в одном процессе. В отличие от Electron, где main и renderer разделены, в NW.js Node.js API доступно прямо в DOM-контексте страницы. Это упрощает некоторые сценарии, но создаёт другие проблемы с безопасностью.
Ключевое отличие от Electron
В Electron: renderer → IPC → main → Node.js API.
В NW.js: renderer напрямую вызывает require('fs'), require('path') и т.д.
// В NW.js это работает прямо в браузерном коде
const fs = require('fs');
const os = require('os');
document.getElementById('hostname').textContent = os.hostname();
fs.readFile('/etc/hosts', 'utf8', (err, data) => {
document.getElementById('hosts').textContent = data;
});
Для небольших утилитарных приложений это действительно удобнее. Для сложных приложений — сложнее рассуждать о безопасности.
Создание проекта
npm init -y
npm install nw --save-dev # или nw-builder для production сборок
{
"name": "my-nw-app",
"main": "index.html",
"window": {
"title": "My Application",
"width": 1200,
"height": 800,
"min_width": 800,
"min_height": 600,
"icon": "icons/icon.png",
"frame": true,
"resizable": true
},
"nodejs": true,
"node-remote": "",
"chromium-args": "--enable-features=WebRTC-H264WithOpenH264FFmpeg"
}
Поле main в package.json — точка входа, здесь это HTML-файл, а не JS.
Структура проекта
my-app/
├── package.json # конфигурация NW.js
├── index.html # главное окно
├── js/
│ ├── app.js # логика приложения
│ └── native.js # Node.js интеграция
├── css/
│ └── style.css
└── icons/
└── icon.png
Пример: файловый менеджер
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>File Browser</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<div id="toolbar">
<button id="btn-open">Открыть папку</button>
<span id="current-path"></span>
</div>
<div id="file-list"></div>
<script src="js/app.js"></script>
</body>
</html>
// js/app.js — Node.js API прямо в renderer
const fs = require('fs');
const path = require('path');
// NW.js предоставляет nw объект в глобальном контексте
const { dialog } = nw;
let currentPath = require('os').homedir();
function renderFiles(dirPath) {
document.getElementById('current-path').textContent = dirPath;
const list = document.getElementById('file-list');
list.innerHTML = '';
try {
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
entries.sort((a, b) => {
if (a.isDirectory() !== b.isDirectory()) return a.isDirectory() ? -1 : 1;
return a.name.localeCompare(b.name);
});
entries.forEach(entry => {
const item = document.createElement('div');
item.className = `file-item ${entry.isDirectory() ? 'dir' : 'file'}`;
item.textContent = entry.name;
item.addEventListener('dblclick', () => {
if (entry.isDirectory()) {
currentPath = path.join(dirPath, entry.name);
renderFiles(currentPath);
} else {
nw.Shell.openItem(path.join(dirPath, entry.name));
}
});
list.appendChild(item);
});
} catch (err) {
list.innerHTML = `<div class="error">${err.message}</div>`;
}
}
document.getElementById('btn-open').addEventListener('click', () => {
const input = document.createElement('input');
input.type = 'file';
input.setAttribute('nwdirectory', ''); // NW.js расширение для выбора папки
input.addEventListener('change', () => {
currentPath = input.value;
renderFiles(currentPath);
});
input.click();
});
renderFiles(currentPath);
Нативное меню
// js/menu.js
const menu = new nw.Menu({ type: 'menubar' });
const fileMenu = new nw.Menu();
fileMenu.append(new nw.MenuItem({
label: 'Открыть',
key: 'o',
modifiers: 'ctrl',
click: () => openFolder()
}));
fileMenu.append(new nw.MenuItem({ type: 'separator' }));
fileMenu.append(new nw.MenuItem({
label: 'Выход',
key: 'q',
modifiers: 'ctrl',
click: () => nw.App.quit()
}));
menu.append(new nw.MenuItem({ label: 'Файл', submenu: fileMenu }));
// Применить к окну (только macOS)
if (process.platform === 'darwin') {
nw.Window.get().menu = menu;
}
Системный трей
const tray = new nw.Tray({
title: 'My App',
icon: 'icons/tray-16.png',
iconsAreTemplates: false // macOS
});
const trayMenu = new nw.Menu();
trayMenu.append(new nw.MenuItem({
label: 'Показать',
click: () => nw.Window.get().show()
}));
trayMenu.append(new nw.MenuItem({
label: 'Выход',
click: () => nw.App.quit()
}));
tray.menu = trayMenu;
tray.on('click', () => {
const win = nw.Window.get();
win.isVisible() ? win.hide() : win.show();
});
Сборка дистрибутива
# Установка nw-builder
npm install nw-builder --save-dev
// build.js
const NwBuilder = require('nw-builder');
const nw = new NwBuilder({
files: ['./src/**/**', './package.json'],
version: '0.89.0',
platforms: ['win64', 'osx64', 'linux64'],
buildDir: './release',
buildType: 'default',
macIcns: './icons/icon.icns',
winIco: './icons/icon.ico',
zip: true
});
nw.build().then(() => {
console.log('Build complete');
}).catch(console.error);
Версии NW.js: Normal vs SDK
NW.js доступен в двух версиях:
- Normal — для production, без DevTools по умолчанию, меньше размер
- SDK — с DevTools, для разработки и отладки
{
"devDependencies": {
"nw": "0.89.0-sdk"
}
}
Когда NW.js имеет смысл
NW.js стоит рассмотреть для: портирования существующего веб-приложения без переработки архитектуры, быстрых внутренних инструментов где security model менее критична, проектов где команда работала с node-webkit раньше и знает его особенности.
Для новых production-приложений Electron или Tauri имеют более активное сообщество, лучшую документацию и более предсказуемую модель безопасности.







