最近在用nwjs开发桌面应用,遇到了一些问题,为方便日后参考,所以在此把它们记录下来。之所以没用 electronjs,是因为公司产品主要面向教育行业工作人群,他们中XP系统的使用者,占了很大一部分比例。

一、nwjs 客户端无法播放视频、音频

在做nwjs客户端开发时,发现客户端无法播放视频、音频。查资料说,这是由于MP3编码属于专利编码,非开源授权的,所以在nw.js中默认不支持MP3的播放,需要手动启用才行。

访问它的 github主页,找到对应本地版本号的 ffmpeg.dll 压缩包,并下载它。

解压后,在nwjs对应的目录,覆盖 ffmpeg.dll 文件,即可!

二、windows XP 的兼容性问题

由于公司产品主要面向教育行业工作人群,因此,这款基于nwjs开发的产品需要兼容XP。但当在XP系统进行测试时,结果发现无法启动:

XP系统无法兼容

针对这个问题,官网给出了 解决方案

Google ended the support for XP in Chrome M50. NW.js LTS is based on the last compatible version of Chrome. So please use the LTS version (0.14.x) for XP support.

大致的意思是说,Google 在 Chrome50 这个版本终止了对 xp系统的支持。而 NW.js LTS 版本是基于Chrome浏览器最后一个兼容版本,因此,要兼容 XP,请使用 0.14.x 这种长期支持(Long Term Support - LTS)版本。

并且,从 下载列表 页面可以看出,0.14.70.14.x 的最后一个版本。

所以,如果要兼容XP系统,并且最大利用 NW.js 的新特性,你可以使用 0.14.7 版本。

三、nw is not defined

默认情况下,启动nw客户端后,nw会在运行环境注入一个全局变量 nw,通过该变量,我们可以使用nw的相关API。

但我们在代码中使用 nw.Window.get().maximize() 来实现窗口最大化时,直接报错 nw is not defined。并且,如果按 F12 开启开发者工具,输入 nw,也会直接报 nw is not defined:

nw未定义报错

有人也遇到类似的 问题,不过它是改变 windows location 遇到的。

解决方案是在 package.json 文件加入 "node-remote": "<all_urls>" 字段,即:

// package.json

{
...
"node-remote": "<all_urls>",
...
}

四、顶部栏拖动

由于默认的顶部栏都比较简陋(见图的上半部分):

nw头部

通常情况我们会使用前端技术(html、css、js)来自定义顶部栏,再结合 nw 窗口操作的API(最大化、最小化、还原、关闭),便能完成默认顶部栏的功能。

但是,nw 貌似没有提供窗口拖动的API,这时,你只需要给自定义顶部栏容器(假设类名为 top)应用一行样式,即可实现拖动:

.top {
-webkit-app-region: drag;
}

不过要注意,如果配置文件中使用 frame 模式,即 "frame": true,则自定义的顶部栏仍然无法拖动。

// package.json

{
"window": {
...
"frame": true, // 无法拖动
"frame": fasle
}
}

但还要注意,上面的样式可能会导致右边控制窗口的API(最大化、最小化、还原、关闭)无法使用。此时,你还需要对该控制区域禁用拖动:

.window-control {
-webkit-app-region: no-drag;
}

五、版本提示

本地开发环境下,启动 nw 客户端时,另外一个窗口有如下提示:

版本提示dev版

或者,打包完成后,生产环境下,运行 nw 客户端,也有类似英文的提示:

版本提示build版

这是因为之前装过高版本的 nwjs,存在缓存问题。所以。。。

解决方案是:找到 C:\Users\Administrator\AppData\Local 这个路径,把 nwjs目录 以及 nw生成的相关应用程序目录(比如你利用nw生成的程序名为 app.exe,则删除app文件夹)全部删除。

六、nw客户端不能启动

有的时候,本地开发环境下,在关闭了 nw客户端,希望通过命令行再次重启 nw客户端,会出现 nw客户端 无法启动的情况。

还有的时候,打包后的生产环境下,在关闭 nw客户端 后,希望双击 xx.exe 再次重启 nw客户端,也会出现 nw客户端 无法启动的情况。

查看 Windows 任务管理器 你会发现,虽然通过 命令行 或者 关闭按钮 关闭了 nw客户端,但它的进程却没有结束。所以,会出现 nw客户端 无法启动的情况。

最开始的解决方案是,手动在 Windows 任务管理器 面板结束有关应用程序的全部进程。

后面发现,可能与本地存在多个 nw版本 有关!

解决方案是:找到 C:\Users\Administrator\AppData\Local 这个路径,把 nwjs目录 以及 nw生成的相关应用程序目录(比如你利用nw生成的程序名为 app.exe,则删除app文件夹)全部删除。

另外,如果可以的话,尽可能在打包时,使用命令行去下载更新 nw的运行包(该操作,非必须)。

七、无法使用 flash 播放

在 nw 中嵌入 flash 时,会出现 “无法加载插件”,从而导致flash不能正常播放:

flash播放失败

之所以出现这种情况,是因为 nw 没有找到对应的 flash 插件。

解决方案:windows 系统下,通过 C:\Windows\System32\Macromed\Flash 打开 Flash 插件的目录,找到 pepflashplayerxx_xx_x_x_xxx.dll (其中 x 字母代表你本地安装的版本号)这个文件。

然后,在 nw 开发目录找到 sdk 包,并在这个 sdk 包的根目录新建 PepperFlash 文件夹,并把 pepflashplayerxx_xx_x_x_xxx.dll 文件放入其中。

最后,在项目开发根目录,找到 package.json 配置文件,指定 flash 插件的路径:

// package.json`

{
"chromium-args": "--ppapi-flash-path=PepperFlash/pepflashplayer64_25_0_0_171.dll",
...
}

八、nw打包后,文件资源无法正常读取

最初开发完打包时,我们是把主程序和文件资源打包在一起,这显然不符合客户端程序开发的规范,会出现两个问题:

  • 如果文件资源很多,会导致打包的执行文件也很大
  • 主程序无法读取其他文件资源(这显然不行的,比如一个播放器,难道只能播放它自身包含的视频?)

但如果我们不把文件资源和主程序打包在一起,那么,又会导致文件资源无法正常读取(找不到资源路径)的问题。一般情况下,我们会把文件资源放在主程序的根目录。那么,要解决这个路径问题,其实,最关键的是,要在主程序读取文件资源时,首先需要找到主程序路径:

function getNwRunPath() {
let installPath = '';
let { execPath } = process;

if (execPath.includes('MacOS')) {
installPath = execPath.replace('nwjs/0.14.7-sdk/osx64/nwjs.app/Contents/Versions/50.0.2661.102/nwjs Helper.app/' +
'Contents/MacOS/nwjs Helper',
'')
} else if (execPath.includes('nw.exe')) {
installPath = execPath.replace('nwjs\\0.14.7-sdk\\win64\\nw.exe', '')
} else {
installPath = execPath.substring(0, execPath.lastIndexOf('\\') + 1)
}

return installPath;
}

上面函数可得到主程序的运行路径,然后,我们再通过主程序路径,去找对应的文件资源目录则非常容易了。

九、nwjs应用图标无法正常显示

9.1 本地开发时,工具栏图标显示不正常

由于该nwjs项目在本地开发及浏览器端打包方面是基于 webpack 的,所以,我是先使用 webpack-dev-server 来启动服务,再通过nodejs去执行 nw-sdk 包中的 nw.exe,从而启动nw客户端。

在没设置工具栏图标之前,是这样的(最后两个分别是 nw客户端、nw客户端开发者工具):

开发环境工具栏图标1

我们需要在项目根目录的配置文件设置nwjs运行的的图标(你也可以设置宽高或者其他属性):

// package.json

{
...
"window": {
"width": 1024,
"height": 768,
"min_width": 1024,
"min_height": 768,
...
"icon": "src/res/images/app/app.png"
}
}

设置完后,再次启动服务,就得到预期的效果:

开发环境工具栏图标2

9.2 打包后,工具栏图标和预览图标无法正常显示

当开发完成,通过 nw-builder 打包后,在打包目录中启动 exe 文件,会发现工具栏图标和预览图标显示的是 nwjs(node-webkit) 的默认图标:

生产环境图标1

并且,当把打包的目录或者 exe 文件复制到其他文件夹,不仅工具栏图标和预览图标,exe 文件的图标也变成了 nwjs(node-webkit) 的默认图标,这显然是无法接受的。

这就需要在 nw-builder 打包之前,将图标复制到 nwjs 要打包的目录(webpack编译后的文件夹一般为 dist 目录):

const fs = require('fs');
const path = require('path');
const distDir = path.resolve(__dirname, 'dist');
const appIconUrl = path.resolve('./', 'src/res/images/app-old/app.png');

const buildConfig = {
name: 'app',
window: {
icon: 'app.png'
},
main: 'index.html'
};

fs.copyFile(appIconUrl, path.resolve('./', 'dist/app.png'), () => {
// 写入nw运行的配置文件
fs.writeFileSync(path.resolve(distDir, 'package.json'), JSON.stringify(buildConfig));

// nw-builder 打包操作
...
});

再次打包后发现,发现工具栏图标变成我们自定义的了,但是,但是右侧预览图标还是一直显示 nwjs(node-webkit) 的默认图标。

生产环境图标2

和工具栏图标不同,对于右侧预览图,最坑爹(最坑爹!)的是,由于打包后文件目录存在缓存,这就导致不是每一次打包后,exe应用图标都会及时按照每次修改的源码进行更新。换言之,即使改对了,打包后生成的应用图标可能还是原来的。

因此,在排查问题上非常耗时!!为了这个问题,几乎折腾了一天了!!!

再查找各方资料,经多次尝试后发现,nw-builder 配置文件(假设为 nwbuild.config.js)中对 windows 系统的应用图标(ico格式,假设为 app.ico),必须是 256*256 尺寸这么一个尺寸的图标(网上资料都说要 128 * 128 尺寸,其实根本不对!!!):

// nwbuild.config.js

module.exports = {
...
winIco: './src/res/images/app/app.ico'
}

终于,再更新图标后,应用图标、底部栏图标、右侧预览图标,都显示成我们自定义的图标。并且,把打包目录或者 exe 文件复制到其他文件夹,也显示的是我们自定义的图标。

生产环境图标3

后来针对上面的缓存问题,我想了这么两个方案:

  • 尽可能在每次修改时,换掉图标的名称及生成路径(不一定会生效)
  • 将修改后整个项目的源码复制到其他新文件夹,在新文件夹重新编译打包(完全规避了缓存问题。但每次都要按照一堆依赖,比较繁琐)

另外,图标转换其他格式的方式如下:

  • 将 png 转换为 ico 格式,可通过这个站点:icoconverter ,转换时,Sizes 选项选择 256 pixels 的规格,Bit depth 选项则选择 32 bits 的规格。

  • 将 png 转换为 icns 格式,则可通过这个站点:cloudconvert-png-to-icns

9.3 更改图标其他方案

如果不想用程序代码来更换图标,你也可以使用桌面应用资源编译器工具。其中,目前主流的,应该是 resourcehacker

十、无法使用 alert、prompt、confirm等系统弹框

在程序中使用 alertprompt 等系统弹框,会导致nw程序奔溃。但在某些同事的电脑上,则不会出现该问题。经过多次尝试,暂无解决方案…

十一、draftJs光标错乱的问题

由于项目中涉及文本输入,所以用到了 facebook 的一个富文本react组件 - draft-js 。但一开始出现光标错乱的问题,见图:

输入光标错乱

可以看到,输入第一个字符串正常,但输入第二个字符时,光标会跳到最前面。当尝试删除这些字符时,它们的顺序又正常了。对于这种问题,一开始百思不得其解,因为,我在其他项目里(浏览器端)该组件功能是没问题的。

后来发现,是由于我在webpack中设置了 nwjs 在 nodejs 环境下运行。解决方法很简单,因为 nodejs 环境的全部变量是 global,只需要针对该环境把 getSelection 方法重新赋值即可:

global.getSelection = window.getSelection.bind(window);
输入光标正常

相关参考