我正在参与掘金会员专属活动-源码共读第一期, 点击参与
open
是一个跨平台的打开文件、URL、可执行文件的库,支持
Windows
、
Linux
、
Mac
,当然这也意味着不能在浏览器环境中使用。
源码地址: github.com/sindresorhu…
首先看
README
中的介绍,为什么要使用
open
:
spawn
instead of
exec
.(安全,因为它使用
spawn
而不是
exec
)
node-open
问题)
使用
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…