open是一个跨平台的打开文件、URL、可执行文件的库,支持Windows、Linux、Mac,当然这也意味着不能在浏览器环境中使用。
使用
首先看README中的介绍,为什么要使用open:
- Actively maintained.(积极维护)
- Supports app arguments.(支持应用参数)
- Safer as it uses
spawninstead ofexec.(安全,因为它使用spawn而不是exec)- Fixes most of the original node-open issues.(修复了大部分原始的
node-open问题)- Includes the latest xdg-open script for Linux.(包括适用于 Linux 的最新 xdg-open 脚本)
- 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函数接收两个参数,target和options:
target是要打开的目标,必须是一个字符串;options是可选参数,是一个对象,可以传入什么后面分析会看到。
open函数内部调用了baseOpen函数,这个函数是一个异步函数,返回一个Promise,这个函数的实现在下面:
const baseOpen = async options => { options = { wait: false, background: false, newInstance: false, allowNonzeroExitCode: false, ...options }; // ... }
从这个里可以看到options可选的参数有:wait、background、newInstance、allowNonzeroExitCode,这四个参数都是布尔值。
继续往下:
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来执行的;
这里最开始是通过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环境下的mountPoint是C:;
process.env.SYSTEMROOT指向的就是C:\Windows;
然后就是cliArguments是直接push了一些参数,这些参数是powershell的参数;
最后就是childProcessOptions,这个是在环境判断之前定义的一个变量,后面会用到,继续往下:
const encodedArguments = ['Start']; if (options.wait) { encodedArguments.push('-Wait'); }
Windows的powershell是通过Start命令来执行的,这里就是拼接了Start命令,如果options.wait为true,那么就会加上-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.access是fs模块中的一个方法,用来判断文件是否有指定的权限,如果有,就会调用回调函数,否则就会抛出异常;
fsConstants.X_OK是fs模块中的一个常量,表示可执行权限;
最后就是判断是否有appArguments,如果有,就会将appArguments添加到cliArguments中;
最后就是判断是否有options.wait,如果没有,那么就会将childProcessOptions.stdio设置为ignore,并且设置childProcessOptions.detached为true;
继续往下看:
if (options.target) { cliArguments.push(options.target); }
这里就是判断是否有options.target,如果有,就会将options.target添加到cliArguments中;
if (platform === 'darwin' && appArguments.length > 0) { cliArguments.push('--args', ...appArguments); }
这里是判断是否是macOS,如果是,那么就会将--args和appArguments添加到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来执行命令,这个方法会根据不同的平台,执行不同的命令;
- child_process: nodejs.org/api/child_p…