相关文章推荐

最近遇到一个小需求,希望写一个 Windows 可执行文件,做一些监听 U 盘插入事件的事情。由于不需要 GUI,electron 就用不到了。不过仍然需要打包 Node.js 执行环境进来。

一番搜索之后找到了 nexe 这个库。简单介绍一下它的几个关键特性:

  • 支持命令行和编程式调用两种使用方式
  • 支持选择构建目标为不同操作系统
  • 支持打包不同 Node.js 版本
  • 下面我们看一下它的使用方法。

    内置打包工具

    首先默认使用的打包工具是 Fusebox,但是也支持通过 bundle 选项切换成其他构建工具。例如我们使用自定义的构建方法:

    // build.js
    const nexe = require('nexe');
    nexe.compile({
        output: 'native-build',
        target: 'win32-x64-8.9.4',
        bundle: './nexe-bundle.js',
        silent: false
    

    在构建方法中,我们就可以使用任意构建工具编译 JS 文件了,只要最后返回最终结果。 这里我们使用 Webpack 4:

    // nexe-bundle.js
    const webpack = require('pify')(require("webpack"))
    const fs = require('fs')
    module.exports.createBundle = function (options) {
        return webpack({
            entry: options.input,
            target: 'node',
            output: { filename: 'dist/tmp.js' }
        }).then(() => {
            const result = fs.readFileSync('./dist/tmp.js').toString()
            fs.unlinkSync('./dist/tmp.js')
            return result
    

    选择目标平台

    nexe 支持 Mac Windows Linux 平台下的构建。通过 target: 'win32-x64-8.9.4' 我们可以选择操作系统以及 Node.js 版本。 全部可用的列表在这里

    首次构建时会下载目标平台下的运行环境,并进行缓存。

    需要注意的是,由于某些第三方库依赖当前运行环境,所以要想编译跨平台的程序最好在目标平台上进行构建。 例如在 Mac 上想要构建 Windows 下的 exe,最好安装虚拟机,比如 Parallel Desktop。

    node-gyp

    Node.js 虽然强大,但是在使用某些平台底层的功能时,还是需要依赖 C++ 编写的组件。在 Node.js 中,可以通过 node-gyp 将这些原生代码编译成 node 模块,在运行时很方便地进行调用。

    node-usb-detection 为例,在 binding.gyp 文件中:

    "targets": [ "target_name": "detection", "sources": [ "src/detection.cpp", "src/detection.h", "src/deviceList.cpp" "include_dirs" : [ "<!(node -e \"require('nan')\")" 'conditions': [ ['OS=="win"', 'sources': [ "src/detection_win.cpp" 'include_dirs+': # Not needed now ['OS=="mac"', 'sources': [ "src/detection_mac.cpp" "libraries": [ "-framework", "IOKit"

    更详细的例子可以参考 Node.js addons 文档

    Windows 上的可怕经历

    在 Windows 虚拟机上的编译经历可谓困难重重。

    首先按照 node-gyp 的安装说明,执行:

    npm install --global --production windows-build-tools

    这一步会安装 Python 和 VS 构建工具。这时候可以检查下 C:\Program Files (x86)\MSBuild\Microsoft.Cpp\v4.0 下是否有 v140 也就是 VS2015 的构建工具。

    一切顺利的话就可以开始安装 npm 依赖了,很多使用了 node-gyp 的第三方依赖此时会进行 prebuilt-install,开始构建 node addon。

    运行时如果出现如下错误 MSB4019

    error MSB4019: The imported project “C:\Microsoft.Cpp.Default.props” was not found. Confirm that the path in the declaration is correct, a nd that the file exists on disk.

    需要在 CMD 中设置环境变量,这里我们设置成之前安装好的 VS2015 的路径。相关 ISSUE

    SET VCTargetsPath=C:\Program Files (x86)\MSBuild\Microsoft.Cpp\v4.0\v140

    如果出现如下错误 MSB8036

    MSB8036: The Windows SDK version 8.1 was not found. Install the required version of Windows SDK

    则需要配置 npm 环境变量,相关 ISSUE

    npm config set msvs_version 2015

    总之,遇到问题可以先在 MS 官方的指导意见 中查找,能少走一些弯路。

    示例:监听 U 盘插拔

    回到我们最初的需求,希望监听 U 盘的插拔。

    启动监听后,进程不会退出,类似 DOS 中的 pause 语句。 在监听到 add 事件时,回调函数会传入插入 USB 设备的对象。

    const usbDetect = require('usb-detection');
    usbDetect.startMonitoring();
    usbDetect.on('add', device => {});

    值得注意的是在这个设备对象中,是不包含挂载点的,更多的是一些底层设备信息。 一些更高层次得操作,例如试图获取挂载点,并没有提供,相关ISSUE

    locationId: 0, vendorId: 5824, productId: 1155, deviceName: 'Teensy USB Serial (COM3)', manufacturer: 'PJRC.COM, LLC.', serialNumber: '', deviceAddress: 11

    为了获取当前得挂载路径,不得不借助其他库,例如 drivelist。 通过定时遍历当前所有驱动设备,我们能找出其中的 USB 存储设备,得到其挂载点。 之所以使用定时器是因为挂载需要时间,触发 USB 设备的 add 事件时还没有挂载好。

    const drivelist = require('drivelist');
    let checkUSBIntervalID;
    checkUSBIntervalID = setInterval(() => {
        drivelist.list((error, drives) => {
            if (error) {
                throw error;
            drives.forEach(drive => {
                if (!drive.isSystem && drive.isRemovable
                    && drive.mountpoints.length) {
                    clearInterval(checkUSBIntervalID);
                    scanDrive(drive);
    }, 1000);

    得到了挂载点信息,就可以使用 fs 模块进行文件操作了:

    mountpoints: [ { path: '/Volumes/KINGSTON', label: 'KINGSTON' } ],

    查找 node addon

    最后我们关注一下 nexe 中的一个技术细节。

    使用 node-gyp 编译之后得到 node addon,可以通过 bindings 模块使用。 例如之前介绍过的 usb-detection 是这么使用的:

    var detection = require('bindings')('detection.node');

    那么 nexe 是如何解决运行时 node addon 路径查找的呢?

    按照官方文档的说法,编译之后的 .node 文件会生成在 build/Release/ 目录下,解析路径时也会默认从这里开始查找。

    Next, invoke the node-gyp build command to generate the compiled addon.node file. This will be put into the build/Release/ directory.

    nexe 利用 Fusebox 的转译插件(类似 Babel),对代码中的 bindings 路径进行了重写

     
    推荐文章