【源码共读】跨平台打开 URL、文件、可执行文件 open

简介: 【源码共读】跨平台打开 URL、文件、可执行文件 open


open是一个跨平台的打开文件、URL、可执行文件的库,支持WindowsLinuxMac,当然这也意味着不能在浏览器环境中使用。


使用


首先看README中的介绍,为什么要使用open


  1. Actively maintained.(积极维护)
  2. Supports app arguments.(支持应用参数)
  3. Safer as it uses spawn instead of exec.(安全,因为它使用spawn而不是exec)
  4. Fixes most of the original node-open issues.(修复了大部分原始的node-open问题)
  5. Includes the latest xdg-open script for Linux.(包括适用于 Linux 的最新 xdg-open 脚本)
  6. Supports WSL paths to Windows apps.(支持 WSL 路径到 Windows 应用程序)


使用open非常简单,只需要传入一个路径即可:

const open = require('open');
// 通过默认的图片查看器打开图片,并等待这个程序关闭
await open('unicorn.png', {wait: true});
console.log('The image viewer app quit');
// 通过默认的浏览器打开指定的 URL
await open('https://sindresorhus.com');
// 在指定的浏览器中打开 URL。
await open('https://sindresorhus.com', {app: {name: 'firefox'}});
// 支持应用参数
await open('https://sindresorhus.com', {app: {name: 'google chrome', arguments: ['--incognito']}});
// 打开应用程序
await open.openApp('xcode');
// 打开应用程序,并传入参数
await open.openApp(open.apps.chrome, {arguments: ['--incognito']});

源码


源码三百多行,这次就不列出全部代码了,直接开始分析,首先看导出部分:

open.apps = apps;
open.openApp = openApp;
module.exports = open;

可以看到默认导出了一个open,通过上面的使用案例可知是一个函数,并且这个函数上面还挂了两个属性,这两个属性也是函数,但是不在我们今天的主要分析范围内,我们先看看open函数:

const open = (target, options) => {
   if (typeof target !== 'string') {
      throw new TypeError('Expected a `target`');
   }
   return baseOpen({
      ...options,
      target
   });
};

open函数接收两个参数,targetoptions


  • target是要打开的目标,必须是一个字符串;
  • options是可选参数,是一个对象,可以传入什么后面分析会看到。


open函数内部调用了baseOpen函数,这个函数是一个异步函数,返回一个Promise,这个函数的实现在下面:

const baseOpen = async options => {
   options = {
      wait: false,
      background: false,
      newInstance: false,
      allowNonzeroExitCode: false,
      ...options
   };
    // ...
}

从这个里可以看到options可选的参数有:waitbackgroundnewInstanceallowNonzeroExitCode,这四个参数都是布尔值。


继续往下:

if (Array.isArray(options.app)) {
    return pTryEach(options.app, singleApp => baseOpen({
        ...options,
        app: singleApp
    }));
}

这里判断了options.app是否是一个数组,如果是数组,就会调用pTryEach函数,并返回这个函数的返回值,这个函数的实现在下面:

const pTryEach = async (array, mapper) => {
   let latestError;
   for (const item of array) {
      try {
         return await mapper(item); // eslint-disable-line no-await-in-loop
      } catch (error) {
         latestError = error;
      }
   }
   throw latestError;
};

这个函数的作用是确保mapper函数中的异步函数有一个执行成功,如果都失败了,就会抛出最后一个错误,而这个mapper其实就是执行baseOpen函数,继续往下:

let {name: app, arguments: appArguments = []} = options.app || {};
appArguments = [...appArguments];
if (Array.isArray(app)) {
    return pTryEach(app, appName => baseOpen({
        ...options,
        app: {
            name: appName,
            arguments: appArguments
        }
    }));
}

这一步就是提取参数,然后判断app是否是一个数组,执行上面的逻辑,不过这一次是带参数的,继续往下:

let command;
const cliArguments = [];
const childProcessOptions = {};
if (platform === 'darwin') {
    command = 'open';
    if (options.wait) {
        cliArguments.push('--wait-apps');
    }
    if (options.background) {
        cliArguments.push('--background');
    }
    if (options.newInstance) {
        cliArguments.push('--new');
    }
    if (app) {
        cliArguments.push('-a', app);
    }
} else if (platform === 'win32' || (isWsl && !isDocker())) {
    // windows 环境处理逻辑
} else {
    // 其他环境处理逻辑
}

这一步是判断平台环境,platform是在最上面通过process中提取出来的;


这里主要是判断Mac环境,Mac环境下的命令是open,然后就是根据options中的参数来拼接命令;

下面就是Windows环境的判定:

if (platform === 'win32' || (isWsl && !isDocker())) {
    const mountPoint = await getWslDrivesMountPoint();
    command = isWsl ?
        `${mountPoint}c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe` :
        `${process.env.SYSTEMROOT}\System32\WindowsPowerShell\v1.0\powershell`;
    cliArguments.push(
        '-NoProfile',
        '-NonInteractive',
        '–ExecutionPolicy',
        'Bypass',
        '-EncodedCommand'
    );
    if (!isWsl) {
        childProcessOptions.windowsVerbatimArguments = true;
    }
    const encodedArguments = ['Start'];
    if (options.wait) {
        encodedArguments.push('-Wait');
    }
    if (app) {
        // Double quote with double quotes to ensure the inner quotes are passed through.
        // Inner quotes are delimited for PowerShell interpretation with backticks.
        encodedArguments.push(`"`"${app}`""`, '-ArgumentList');
        if (options.target) {
            appArguments.unshift(options.target);
        }
    } else if (options.target) {
        encodedArguments.push(`"${options.target}"`);
    }
    if (appArguments.length > 0) {
        appArguments = appArguments.map(arg => `"`"${arg}`""`);
        encodedArguments.push(appArguments.join(','));
    }
    // Using Base64-encoded command, accepted by PowerShell, to allow special characters.
    options.target = Buffer.from(encodedArguments.join(' '), 'utf16le').toString('base64');
}

这里主要是通过is-wsl来判断是否是wsl环境,wsl指的是Windows Subsystem for Linux,这个环境下的命令是powershell;


然后还用到了is-docker来判断是否是docker环境,这个环境下的命令也是powershell,但是这个环境下的命令是通过docker来执行的;


这两个可以参考:is-wslis-docker


这里最开始是通过getWslDrivesMountPoint来获取wsl环境下的mountPoint,这个函数的实现如下:

/**
Get the mount point for fixed drives in WSL.
@inner
@returns {string} The mount point.
*/
const getWslDrivesMountPoint = (() => {
   // Default value for "root" param
   // according to https://docs.microsoft.com/en-us/windows/wsl/wsl-config
   const defaultMountPoint = '/mnt/';
   let mountPoint;
   return async function () {
      if (mountPoint) {
         // Return memoized mount point value
         return mountPoint;
      }
      const configFilePath = '/etc/wsl.conf';
      let isConfigFileExists = false;
      try {
         await fs.access(configFilePath, fsConstants.F_OK);
         isConfigFileExists = true;
      } catch {}
      if (!isConfigFileExists) {
         return defaultMountPoint;
      }
      const configContent = await fs.readFile(configFilePath, {encoding: 'utf8'});
      const configMountPoint = /(?<!#.*)root\s*=\s*(?<mountPoint>.*)/g.exec(configContent);
      if (!configMountPoint) {
         return defaultMountPoint;
      }
      mountPoint = configMountPoint.groups.mountPoint.trim();
      mountPoint = mountPoint.endsWith('/') ? mountPoint : `${mountPoint}/`;
      return mountPoint;
   };
})();

这是一个IIFE,这里主要是通过/etc/wsl.conf来获取mountPoint,这个文件是wsl环境下的配置文件,如果没有这个文件,那么就返回默认的/mnt/


这个就不多说了,主要是通过fs来读取文件,然后通过正则来匹配root的值;


现在回到Windows环境的判定,获取到mountPoint后,就是拼接命令了:

command = isWsl ?
    `${mountPoint}c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe` :
    `${process.env.SYSTEMROOT}\System32\WindowsPowerShell\v1.0\powershell`;
cliArguments.push(
    '-NoProfile',
    '-NonInteractive',
    '–ExecutionPolicy',
    'Bypass',
    '-EncodedCommand'
);
if (!isWsl) {
    childProcessOptions.windowsVerbatimArguments = true;
}

可以看到如果是wsl环境,那么就是


mountPoint+c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe,否则就是process.env.SYSTEMROOT+\System32\WindowsPowerShell\v1.0\powershell


两个地址的区别就是wsl环境下的mountPoint/mnt/c/,而Windows环境下的mountPointC:

process.env.SYSTEMROOT指向的就是C:\Windows


然后就是cliArguments是直接push了一些参数,这些参数是powershell的参数;


最后就是childProcessOptions,这个是在环境判断之前定义的一个变量,后面会用到,继续往下:

const encodedArguments = ['Start'];
if (options.wait) {
    encodedArguments.push('-Wait');
}

Windowspowershell是通过Start命令来执行的,这里就是拼接了Start命令,如果options.waittrue,那么就会加上-Wait参数;


后面也没有看到上面mac环境下的一些参数配置的判断,继续往下:

if (app) {
    // Double quote with double quotes to ensure the inner quotes are passed through.
    // Inner quotes are delimited for PowerShell interpretation with backticks.
    encodedArguments.push(`"`"${app}`""`, '-ArgumentList');
    if (options.target) {
        appArguments.unshift(options.target);
    }
} else if (options.target) {
    encodedArguments.push(`"${options.target}"`);
}
if (appArguments.length > 0) {
    appArguments = appArguments.map(arg => `"`"${arg}`""`);
    encodedArguments.push(appArguments.join(','));
}
// Using Base64-encoded command, accepted by PowerShell, to allow special characters.
options.target = Buffer.from(encodedArguments.join(' '), 'utf16le').toString('base64');

app是通过options.app传入的,这个在上面是通过解构赋值的方式获取的;


如果有app,那么就会拼接app,并加上-ArgumentList参数;


如果有options.target,那么就会把options.target加到appArguments的第一个元素上;


如果没有app,那么就会直接拼接options.target,这个options.target就是我们传入的第一个参数


然后会将appArguments中的每一个元素都加上双引号,然后用逗号拼接起来,这个就是powershell的参数;


最后就是将encodedArguments通过utf16le编码成base64,然后赋值给options.target


还有最后一个else分支的判断,来看看:

if (app) {
    command = app;
} else {
    // When bundled by Webpack, there's no actual package file path and no local `xdg-open`.
    const isBundled = !__dirname || __dirname === '/';
    // Check if local `xdg-open` exists and is executable.
    let exeLocalXdgOpen = false;
    try {
        await fs.access(localXdgOpenPath, fsConstants.X_OK);
        exeLocalXdgOpen = true;
    } catch {}
    const useSystemXdgOpen = process.versions.electron ||
        platform === 'android' || isBundled || !exeLocalXdgOpen;
    command = useSystemXdgOpen ? 'xdg-open' : localXdgOpenPath;
}
if (appArguments.length > 0) {
    cliArguments.push(...appArguments);
}
if (!options.wait) {
    // `xdg-open` will block the process unless stdio is ignored
    // and it's detached from the parent even if it's unref'd.
    childProcessOptions.stdio = 'ignore';
    childProcessOptions.detached = true;
}

这里就是判断command的值,如果有app,那么就是app,就没有后面的事了;


如果没有app,那么情况就有点复杂了,首先是判断是否是Webpack打包环境;


注释写的很清楚,Webpack环境是在内存中进行的,所以没有实际的文件路径,也没有xdg-open


后面是检查是否有本地的xdg-open,如果有,那么就是用本地的xdg-open,否则就是用系统的xdg-open


xdg-open是一个命令行工具,用来打开文件或者URL,它会根据文件的类型,使用合适的程序打开;


这里是直接使用fs.access来判断是否有执行权限;


fs.accessfs模块中的一个方法,用来判断文件是否有指定的权限,如果有,就会调用回调函数,否则就会抛出异常;

fsConstants.X_OKfs模块中的一个常量,表示可执行权限;


最后就是判断是否有appArguments,如果有,就会将appArguments添加到cliArguments中;


最后就是判断是否有options.wait,如果没有,那么就会将childProcessOptions.stdio设置为ignore,并且设置childProcessOptions.detachedtrue


继续往下看:

if (options.target) {
    cliArguments.push(options.target);
}

这里就是判断是否有options.target,如果有,就会将options.target添加到cliArguments中;

if (platform === 'darwin' && appArguments.length > 0) {
    cliArguments.push('--args', ...appArguments);
}

这里是判断是否是macOS,如果是,那么就会将--argsappArguments添加到cliArguments中;

const subprocess = childProcess.spawn(command, cliArguments, childProcessOptions);

这里就是执行命令的地方了,这里面的三个参数分别是:命令、参数、子进程的配置,都是通过上面的环境判断代码得到的;

if (options.wait) {
    return new Promise((resolve, reject) => {
        subprocess.once('error', reject);
        subprocess.once('close', exitCode => {
            if (options.allowNonzeroExitCode && exitCode > 0) {
                reject(new Error(`Exited with code ${exitCode}`));
                return;
            }
            resolve(subprocess);
        });
    });
}
subprocess.unref();
return subprocess;

如果有options.wait,那么就会返回一个Promise,这个Promise会在子进程执行完毕后,调用resolve或者reject


如果没有options.wait,那么就会调用subprocess.unref(),这个方法会让子进程与父进程分离,父进程不会等待子进程执行完毕;


最后就是返回subprocess,这个subprocess就是子进程的实例;


总结


这篇文章主要是介绍了open模块的源码,这个模块主要是用来打开文件或者URL,它会根据文件的类型,使用合适的程序打开;


核心是通过child_process.spawn来执行命令,这个方法会根据不同的平台,执行不同的命令;



目录
相关文章
|
6天前
|
弹性计算 数据可视化 安全
云服务器ECS里文件的URL,如何查到呢?
云服务器ECS里文件的URL,如何查到呢?
59 0
|
9月前
|
JavaScript 前端开发
JS 下载 URL 链接文件(点击按钮、点击a标签、支持代理与非代理下载)
JS 下载 URL 链接文件(点击按钮、点击a标签、支持代理与非代理下载)
218 0
|
前端开发 JavaScript 数据格式
图片URL转file文件(前端+后端node.js)
图片URL转file文件(前端+后端node.js)
|
人工智能 Java 对象存储
Java获取阿里云图片临时URL与图片文件转换Base64编码方法
在使用阿里云人工智能产品服务时,有部分服务需要上传的参数中包含文件URL,当我们没有开通OSS服务时,可以使用临时URL服务、或部分服务支持Base64编码格式,此文章为生成临时URL-JavaSDK方案与图片文件转换Base64编码方案。
1817 0
|
6天前
|
Java 应用服务中间件
解决tomcat启动报错:无法在web.xml或使用此应用程序部署的jar文件中解析绝对的url [http:java.sun.com/jsp/jstl/core]
解决tomcat启动报错:无法在web.xml或使用此应用程序部署的jar文件中解析绝对的url [http:java.sun.com/jsp/jstl/core]
414 1
|
5月前
下载文件url为MultipartFile
下载文件url为MultipartFile
65 0
|
9月前
selenium--获取HTML源码断言和URL地址
selenium--获取HTML源码断言和URL地址
|
10月前
|
数据可视化 Python
Python Flask Echarts数据可视化图表实战晋级笔记(3)Blueprint蓝图解决单文件url分发
Python Flask Echarts数据可视化图表实战晋级笔记(3)Blueprint蓝图解决单文件url分发
74 0
Springboot Http文件的访问 Url 转换 MultipartFile ,File 转 MultipartFile
Springboot Http文件的访问 Url 转换 MultipartFile ,File 转 MultipartFile
664 0
|
11月前
|
监控 测试技术 API
【更新】Eolink Apikit 10.9.0 版本:接口测试支持通过 URL 请求大型文件,支持左右视图和全屏视图
本次更新后,会把 API 管理、自动化测试、API 监控中的环境和自定义函数数据进行合并统一管理。 1) 环境合并:各应用级环境合并成空间级环境后,直接罗列在空间级环境列表中,不进行去重,故可能会有重名环境需要大家按需处理。 2) 自定义函数合并:各应用自定义函数合并成空间级自定义函数后,在空间级自定义函数分组中会增加三个一级分组“API 管理函数”、“自动化测试函数”、“API 监控函数”,各应用自定义函数会置于对应的应用分组下,并且进行同名去重,保留最新编辑过的自定义函数。
88 0
【更新】Eolink Apikit 10.9.0 版本:接口测试支持通过 URL 请求大型文件,支持左右视图和全屏视图