专栏首页云前端用NW.js构建跨平台桌面应用(2)-原生界面API

用NW.js构建跨平台桌面应用(2)-原生界面API

[I] 概述 - NW.js原生界面(Native UI)APIs

  • 要构建一个像样的桌面应用,除了由NodeJS处理底层功能,以及由Webkit来应付窗口GUI外,还需要诸如操作窗口、访问剪贴板或隐藏到系统托盘区等和系统图形界面交互的能力
  • 而前面提到的两者,要么无法访问GUI,要么受限于API边界,均无法提供
  • NW.js Native UI APIs 则在其他JS层的顶部提供了这些完整的功能

1.1 获取nw实例

  • 旧版本中可以用 var nw = require('nw.gui') 获取
  • 新版本中直接访问全局成员 nw 即可
//获取当前窗口
var currentWindow = nw.Window.get();//基本上所有的原生界面对象都继承自NodeJS中的EventEmitter
currentWindow.on('minimize', e=>alert('窗口已被最小化')); 

1.2 一些最佳实践

和web应用类似,如果引起某些错误,应用可能崩溃(并且可能没有异常会被抛出),所以一些好的习惯是:

  • 删除元素后,将其引用赋予null,以免不当的重用
  • 尽量重用元素,而不是重复创建
  • 不要重复指定元素
  • 不要更改UI对象的原型

1.3 几个主要的API

APIs

描述

App

设置应用基础功能,包括打开已绑定类型的本地文件、访问manifest文件、注册全局快捷键或退出应用等

Window

操作一个或多个窗口,响应窗口事件等

Screen

用一个单例对象,取得屏幕信息,并响应屏幕分辨率更改、增加屏幕等事件

Menu

用来创建窗口菜单、托盘菜单或右键菜单

File对话框

用文件对话框来打开文件或保存文件等

Tray

管理托盘状态图标

Clipboard

访问系统剪贴板

Shell

调用系统默认应用打开文件等

[II]. App API - 应用的核心

2.1 打开关联类型的文件

NW.js应用有多种办法打开文件,此处谈论的是打开关联的文件类型;也就是说如果我们开发一个文本编辑器,那么我们希望在系统中右键单击一个txt文件出现的“open with...”菜单中,能用我们的应用直接打开它

  • 事实上,当我们进行上述操作时,实际发生的是 nw path/to/app path/to/file.txt
  • 也可以同时打开多个文件 nw path/to/app path/to/a.txt path/to/b.txt
  • 为了访问到这些文件路径,需要使用App.argv属性,其返回一个参数数组
//如果要实际运行例子,需要打包,否则无法获取参数
var args = nw.App.argv;
if (args.length) {
   args.forEach(filepath=>{
       //检查文件是否合法并进行某些操作
   });
}
  • 以上的做法只在程序启动时运行一次
  • 在运行过程中,比如把文件拖放到应用图标上,会以同样的形式传递参数
  • 此时为了拦截到每个打开的文件,需要侦听open事件
//此时的参数是文件路径的字符串
nw.App.on('open', filepath=>{
   //操作文件
});

2.2 访问application data目录路径

所有操作系统都会提供一个默认的文件夹,用来关联每个用户及每个程序,以保存个人设置、应用支持文件,以及某些特定数据;为了避免在程序中硬编码每个平台的对应文件夹,可以用App.dataPath属性统一取得其路径

实际取得的值( 表示manifest文件中配置的应用名 ): - Win: $LOCALAPPDATA%/<name> - Linux: ~/.config/<name> - Mac: ~/Library/Application Support/<name>

//保存配置文件到`application data`目录的例子
var
   fs = require('fs')
   ,path = require('path')
   ,settings = {
       'show_sidebar': true,
       'show_icons': false
   }
   ,settingsFile = path.join(nw.App.dataPath, 'mySettings.json')
;
fs.writeFileSync(settingsFile, JSON.stringify(settings));
fs.readFile(settingsFile, 'utf8', (err, data)=>{
   var saved = JSON.parse(data);
   console.log(saved);
});

2.3 访问manifest文件

正如上一篇中介绍的,应用使用package.json作为主配置文件,尽管可以藉由Node.js取得其引用,但更方便的方法是使用App中的属性

var manifestData = nw.App.manifest;
alert(manifestData.name);

2.4 关闭应用

如果以NW.js应用正常的生命周期来理解,应用打开的所有窗口都依次关闭后,整个应用才能退出;不过有两种方法可以干预这一进程:

App.closeAllWindows()

该方法会触发各子窗口的close事件,从而提供了执行清理动作的机会:

//index.html(第一个窗口,主窗口)
window.open('settings.html');
var currWin = nw.Window.get();
currWin.on('close', function() {
  nw.App.closeAllWindows();
  this.close(true);
});//settings.html(第二个窗口,设置窗口)
var currWin = nw.Window.get();
currWin.on('close', function() {
  //退出之前,在这里保存设置数据
  this.close(true);
});

App.quit()

与上一种方法不同,该方法不发出任何关闭信号,程序直接退出

2.5 注册系统层级的快捷键

利用App.registerGlobalHotKey()方法,并结合 Shortcut API(http://docs.nwjs.io/en/latest/References/Shortcut/),可以注册系统层级的组合快捷键,即便是在应用失去焦点、最小化或缩至托盘后,这些快捷键仍能生效

nw.App.registerGlobalHotKey(
   new nw.Shortcut({
       key: 'Ctrl+Alt+A',
       active: function() { //组合键被正确按下时的回调
           nw.Window.get().show(); //显示当前窗口
           console.log('按下了 ', this.ke);
       }
   })
);
  • 注意尽量不要和其他快捷键冲突,比如'Ctrl+C'
  • 除了用active回调,也可以用同名事件 shortcut.on('active', function() {...})
  • 可以用 App.unregisterGlobalHotKey(shortcut)解除注册

2.6 完整的App文档

[文档地址](http://docs.nwjs.io/en/latest/References/App/)

[III]. Window API - 操作NW.js窗口

在NW.js中,Window API 只不过是对DOM中window对象的一层包装,很多(并非所有)方法和属性继承了后者的用法,同时window对象也是 Node.js EventEmitter 的实例,可以对move或resize等实现事件监听

3.1 实例化

//取得当前窗口
var currWin = nw.Window.get();//向get()方法中传递一个DOM引用,取得其他窗口
var win = nw.Window.get( window.open('other.html') );//也可以用 nw.Window.open(url[,options][,callback]) 方法
var win = nw.Window.open('other.html', {
   // options中的选项和manifest中 [相关参数](http://docs.nwjs.io/en/latest/References/Manifest%20Format/#window-subfields) 一致
   position: 'center',
   width: 600,
   height: 500,
   focus: true,
   
   //也有几个额外定义的选项
   'new-instance': true, //在新的Webkit进程中打开窗口
   'inject-js-start': 'path/to/js', //在文档loaded前注入的脚本
   'inject-js-end': 'path/to/js' //在文档unloaded前注入的脚本
});

优化窗口显示时机

NW.js窗口显示后,代码执行等后台工作还需要一段时间,为了更好等用户体验,可以有意先隐藏窗口

{
   "window": {
       "show": false
   }
}
//html
window.onload = function() {
   nw.Window.get().show();
}

原始的window对象

开头提过:“在NW.js中,Window API 只不过是对DOM中window对象的一层包装”,但很多功能受限无法访问,为了获得原始的引用,可以使用Window.window

var currWin = nw.Window.get();
var nav = currWin.window.navigator;
var lang = nav.language;
console.log(lang);  //zh-CN

3.2 位置和尺寸

在nw.Window实例中,最常见的属性包括 width, height, x, y 等,前两者不言自明,x,y则表示窗口之于屏幕的绝对位置

var currWin = nw.Window.get();
console.log({
  x: currWin.x,
  y: currWin.y,
  width: currWin.width,
  height: currWin.height
});
setTimeout(function() {
  currWin.x = 100;
  currWin.width = 600;
});

可以用以下方法限制窗口大小;但 max/min 不能和 setResizable(false) 方法同时使用,否则会无效

win.setResizable(bool);
win.setMaximumSize(maxW, maxH);
win.setMinimumSize(minW, minY);

可以用如下方法设定窗口位置,其中to设定绝对值,by设定相对偏移量

win.setPosition(posiStr); //有效参数为 null | 'center' | 'mouse'
win.moveTo(x, y);
win.moveBy(x, y);
win.resizeTo(w, h);
win.resizeBy(w, h);

窗口位置或尺寸变化时,触发以下事件

win.on('move', (x,y)=>console.log(x, y));
win.on('resize', (w,h)=>console.log(w, h));

3.3 改变窗口状态

每个桌面窗口都有几种不同的状态:minimized,maximized,hidden,focused,blurclosed;可以用以下方法设置:

var other = nw.Window.get().open('other.html');other.minimize();
other.restore(); //从最小化恢复other.show();
other.hide();other.maximize();
other.unmaximize();other.focus();
  • 应成对使用以上方法并仔细测试,否则会在不同平台引起差异;比如在Mac上用win.open()也可以恢复一个最小化的窗口,但在Win8.1上则只会打开一个黑色的窗口
  • 可以监听'minimize','restore','maximize','unmaximize','focus','blur'几个事件

3.4 全屏

可以简单的设置isFullScreen属性:

win.isFullScreen = true;

也可以调用方法并监听事件:

win.enterFullScreen();
win.leaveFullScreen();
win.toggleFullScreen();
win.on('enter-fullscreen', function(){...});
win.on('leave-fullscreen', function(){...});

3.5 Kiosk模式

一种特殊的全屏模式,也有人称之为展台模式,就是类似网吧或取号机等场合那种不能轻易退出的定制模式

  • 在Linux或Windows系统中,如果有键盘,还可以用Alt+F4,Ctrl+Alt+Del等组合键退出
  • 在Mac系统基本相当于完全锁定了
  • 对应的方法为 win.enterKioskMode(), win.leaveKioskMode(), win.toggleKioskMode()
  • 对应的事件仍是 'enter-fullscreen' 和 'leave-fullscreen'
  • 也可以在在manifest配置(http://docs.nwjs.io/en/latest/References/Manifest%20Format/#kiosk)中启用Kiosk模式

3.6 无边框窗口和可拖动区域

可以将窗口的边框禁用,从而更大程度的自定义窗口

//package.json
{
   "name": "My App",
   "main": "index.html",
   "window": {
       "frame": false
       /* "fullscreen": true */ //应避免同时这样设置,否则将造成屏幕边缘无法捕获鼠标
   }
}

同时,一旦设置为无边框,就无法拖动窗口了,除非自己设置一个可拖动区域

<div class="draggable">
   可拖动区域
   <a href="javascript:;">不被拖动干扰的链接</a>
</div>
<style>
.draggable {
   -webkit-app-region: drag;
   -webkit-user-select: none;
}
.draggable>a {
   -webkit-app-region: no-drag;
}
</style>

3.7 任务栏图标

当窗口失去焦点或最小化时,任务栏或Dock图标是吸引用户注意的重要途径,相关的API包括:

win.setShowInTaskbar(bool); //显示或隐藏图标
win.setBadgeLabel(label); //设置图标相关的文本标签,在Linux下一般无效
win.setProgressBar(num); //0到1//Mac上,参数为-1就跳一次,为1就一直跳直到用户点击
//Windows上,图标和窗口同时闪动参数指定的次数
//Linux上,在非激活状态下,非0的参数才会生效
win.requestAttention(number|bool); 

3.8 关闭窗口

前面用到过的 win.close([fouce]) 方法及相关的事件,可以用来在窗口关闭前方便的做收尾工作;需要注意的是这个过程也会减慢窗口的关闭,可以先隐藏窗口以提供比较好的用户体验:

win.on('close', function(type) {
   this.hide(); //先隐藏
   swtich (type) {
       case 'quit': //从菜单、任务栏图标或快捷键关闭
           break;
       case undefined: //点击窗口关闭按钮
           break;
   }
   //save data ...
   this.close(true); //
});
//调用close()并不会立即关闭窗口,直到回调内的close(true),这样就很好的提供了很好的关闭流程
win.close();

当有多个窗口时,closed事件也可以用来清理窗口的实例引用等

var otherWin = nw.Window.get().open('other.html');
otherWin.on('closed', function() {
  otherWin = null;
});

3.9 完整的Window文档

[文档地址](http://docs.nwjs.io/en/latest/References/Window/)

[IV]. Screen API

nw.Screen.Init(); //实例化Screen的单例对象,只需一次
var screens = nw.Screen.screens; //获得屏幕数组,保护一个或多个screen对象//每个屏幕对象包含这些信息:
/*
screen {
   id: int,   // 物理屏幕分辨率
   bounds: {
       x: int,
       y: int,
       width: int,
       height: int
   },   // 可用区域
   work_area: {
       x: int,
       y: int,
       width: int,
       height: int
   },   scaleFactor: float,
   isBuiltIn: bool,
   rotation: int,
   touchSupport: int
}
*///可以方便的利用这些信息改善用户体验
var currWin = nw.Window.get();
var myScreen = screens[0];
var x = myScreen.work_area.width - currWin.width;
var y = myScreen.work_area.y;
currWin.moveTo(x, y); //窗口贴到了右上角//当连接投影仪时,分辨率有可能发生改变
nw.Screen.on('displayBoundsChanged', function(newScreen) {
  //参数 newScreen 包含了新尺寸屏幕的所有信息
  x = newScreen.work_area.width - currWin.width;
  currWin.moveTo(x, y);
});

[完整的文档地址](http://docs.nwjs.io/en/latest/References/Screen/)

[V]. Menu API - 菜单栏和右键中的菜单

NW.js中,共有三种类型的菜单:

  • 上下文菜单:右键单击应用内的元素时
  • 窗口菜单:在Windows或Linux中,每个窗口上方都可以有自己的菜单栏;==在Mac中,同一应用的所有窗口在系统的任务栏中共享一套菜单==
  • 托盘菜单:在系统任务栏的右侧,一般都有托盘区域,点击其中图标出现的就是托盘菜单

4.1 上下文菜单

var menu = new nw.Menu(); //实例化一个菜单
menu.append(new nw.MenuItem({ //添加若干菜单项
 label: 'Item A',
 icon: 'xxx.png',
 tooltip: 'hello world!',
 enabled: true,
 key: 'A', //快捷键的主键,仅在菜单打开时有效;如果同时为document监听了keyup,会同时生效
 modifiers: 'ctrl-shift', //快捷键的修饰键
 click: function() { //点击事件回调,也可以用 menuitem.on('click', callback) 的方式
   alert('点击了 "Item A"');
 }
}));
menu.append(new nw.MenuItem({
   label: 'Item B',
   checked: false,
   type: 'checkbox' //类型2:点击后菜单项前面有对勾效果
}));
menu.append(new nw.MenuItem({
   type: 'separator' //类型3:分割线
}));
menu.append(new nw.MenuItem({
   label: 'Item C',
   type: 'normal', //类型1: 普通
   iconIsTemplate: true, //仅对Mac生效,系统自动根据 dark/light 等状态改变其样式
   submenu: new nw.Menu(...) //子菜单
}));
document.querySelector('#area').addEventListener('contextmenu', function(ev) {
 ev.preventDefault();
 menu.popup(ev.x, ev.y); //右键时弹出菜单
 return false;
}, false);

menu实例中一些其他的方法:

  • menu.items: 一个由 MenuItem 组成的数组,包含了其持有的所有菜单项
  • menu.insert(item, i)
  • menu.remove(item)
  • menu.removeAt(i)

4.2 窗口菜单

窗口菜单的绝大多数用法和上下文菜单相同,几个不同点在于:

  1. 必须指定type为menubar
var winMenu = new nw.Menu({type: 'menubar'});
  1. 如果应用要部署到Mac系统,还需要激活系统内建菜单(应用、编辑和窗口)
var os = require('os');
if (os.platform() === 'darwin') {
    winMenu.createMacBuiltin("配置中的应用名称", {
        hideEdit: true,
        hideWindow: false
    });
}
  1. 所有 windowMenu 下的一级菜单都必须有子菜单,不能为空
var mitem1 = new nw.MenuItem({
    label: 'm1',
    submenu: new nw.Menu
});
mitem1.submenu.append(new nw.MenuItem({label: 'aaa1'}));
mitem1.submenu.append(new nw.MenuItem({label: 'bbb1'}));
winMenu.append(mitem1);

var mitem2 = new nw.MenuItem({
    label: 'm2',
    submenu: new nw.Menu
});
mitem2.submenu.append(new nw.MenuItem({label: 'aaa2'}));
mitem2.submenu.append(new nw.MenuItem({label: 'bbb2'}));
mitem2.submenu.append(new nw.MenuItem({
    label: '退出这个程序', 
    click: e=>nw.App.quit()
}));
winMenu.append(mitem2);
  1. 将菜单赋值给窗口的menu属性
var currWin = nw.Window.get();
currWin.menu = winMenu;

[完整的文档地址](http://docs.nwjs.io/en/latest/References/Menu/)

[V]. Tray API - 管理托盘状态图标

托盘区一般处在系统状态栏的右侧,一些长时间运行的应用或服务的图标被安置在此处,以免都挤在任务栏中过于拥挤。(这些图标在不同平台叫法不同,Mac中叫做 Status Item, 一些Linux中叫做 Status Icon, Windows 中叫做 System Tray Icon)

// 创建一个托盘图标
var tray = new nw.Tray({ title: 'Tray', icon: 'img/icon.png' });// 添加菜单
var menu = new nw.Menu();
menu.append(new nw.MenuItem({ type: 'checkbox', label: 'box1' }));
tray.menu = menu;// 移除图标
tray.remove();
tray = null;
  • 把实例放在全局作用域,以防被gc后图标消失
  • Mac中的托盘图标没有右键点击的行为,如果在menu上绑定了click行为,将被默认的显示菜单行为覆盖
  • Mac中的高分屏,可判断 window.devicePixelRatio>1后动态指定,或将2x图标和原始图标文件打包,[参考这里](http://www.cnblogs.com/lovelylife/p/6226314.html)

[完整的文档地址](http://docs.nwjs.io/en/latest/References/Tray/)

[VI]. 文件对话框 - 打开或保存文件

在浏览器里,文件对话框可以上传下载文件。在NW.js里,同样的操作只是传递文件路径字符串而已,而非拷贝其内容;同时一些浏览器中的安全限制被解除,并赋予其一些增强的能力,从而使用户体验更接近原生应用

<input type="file" />
<a href="javascript:;" id="file-trigger">打开文件对话框</a><script>
var fipt = document.querySelector('[type=file]');
fipt.addEventListener('change', function(e) {
   var path = this.value;
   alert(path);
   fipt.value = '';
});
document.getElementById('file-trigger').addEventListener('click', function() {
   fipt.click(); //如果要自定义样式或自动触发,也可以直接调用
});
</script>

6.1 各种文件对话框

  • 允许一次选择多个文件 <input type="file" multiple />
  • 过滤文件类型 <input type="file" accept=".doc,.xml,application/msword" />
  • 选择一个目录 <input type="file" nwdirectory />
  • 保存文件 <input type="file" nwsaveas[=默认路径] />
  • 默认路径,必须写成目标平台的格式 <input type="file" nwworkingdir="C:\Documents" />

6.2 用拖动打开文件

<div id="drag_file_area" style="width: 300px;height: 300px;background-color: gray;">拖动文件到这里</div><script>
window.ondragover = window.ondrop = function(e) {
   e.preventDefault()
};
var dragfile = document.getElementById('drag_file_area');
dragfile.ondragenter = function() {this.style.opacity = .5}
dragfile.ondragleave = function() {this.style.opacity = 1}
dragfile.ondrop = function(e) {
   e.preventDefault();
   this.style.opacity = 1;
   let {files} = e.dataTransfer;
   let paths = [].map.call(files, file=>file.path);
   alert(paths);
};
</script>

[VII]. Clipboard API - 访问系统剪贴板

// 获取单例
var clipboard = nw.Clipboard.get();// 从剪贴板读取
var text = clipboard.get('text');
console.log(text);// 写入剪贴板
clipboard.set('I love NW.js :)', 'text');// 清空
clipboard.clear();

[完整的文档地址](http://docs.nwjs.io/en/latest/References/Clipboard/)

[VIII]. Shell API - 调用系统默认应用

  • Shell.openExternal(URI): 用系统默认的浏览器或邮件程序打开URI
  • Shell.openItem(file_path): 用系统默认的关联程序打开一个文件,如果没有指定,则打开"Open with"对话框
  • Shell.showItemInFolder(path): 用资源管理器或finder打开指定的目录

[完整的文档地址](http://docs.nwjs.io/en/latest/References/Shell/)

* 原创文章转载请注明出处

本文分享自微信公众号 - 云前端(fewelife),作者:lua

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2017-02-27

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • [译]JS的内存管理及4种常见的内存泄漏

    原文:https://blog.sessionstack.com/how-javascript-works-memory-management-how-to-h...

    江米小枣
  • 用MobX管理状态(ES5实例描述)-2.可观察的类型

    用observable.shallowObject(value)方法可以实现“浅观察”,只自动响应“浅层”的子属性

    江米小枣
  • [译] 在 GitLab 中使用 Issue 面板的 4 种方式

    原文地址:https://about.gitlab.com/2018/08/02/4-ways-to-use-gitlab-issue-boards/

    江米小枣
  • shell脚本中各种括号的区别以及用法

    最近学到了shell脚本编程,觉得脚本中的不同括号有不同的用处,以及有些括号的格式也有特殊要求,下面我就总结一下各种括号的用法。

    用户4877748
  • ol中闪烁点动画的实现

    实现如图的动画,可以用两种思路: 1.overlay+css3动画实现; 2.canvas动画实现。

    lzugis
  • Shell 中的中括号用法总结

    需要注意的是 [ 与 ] 与操作数之间一定要有一个空格,否则会报错。比如下面这样就会报错:

    用户1558438
  • 微信小程序体验3D物理引擎-ammo.js

    点击体验3D物理引擎bullet的javascript版本。源码参考了:https://github.com/THISISAGOODNAME/learn-amm...

    周星星9527
  • Java11震撼发布了,我们该怎么办?

    Java11已经发布了,我们今天聊聊大家还停留在哪个版本呢?大家对于新版本的迅速的发布有什么想说的呢?

    好好学java
  • 纯粹依靠位操作实现整数加法运算

    Jerry Wang
  • 批量导出某个简书用户的所有文章列表和文章超链接

    虽然简书提供了批量下载文章的功能,但是下载到本地的文章都是markdown格式的,不包含文章的链接,这不满足我的需求。

    Jerry Wang

扫码关注云+社区

领取腾讯云代金券