Skip to content
On this page

open

open 跨平台打开 URL、文件、可执行文件等内容。

目前前端常见的 cli 工具自动打开浏览器就是使用这个库来实现的。

为什么使用它?

  • 积极维护。
  • 支持应用参数。
  • 更安全,因为它使用spawn而不是exec.
  • 修复了大部分原始node-open问题。
  • 包括适用于 Linux 的最新xdg-open脚本。
  • 支持 Windows 应用程序的 WSL 路径。

实现原理

针对不同的系统,使用Node.js的子进程 child_process 模块的spawn方法,在 target 文件类型的默认应用程序中打开。例如,URL 在默认浏览器中打开。

shell
# macOS
open URL

# Windows
start URL

# Linux
xdg-open URL

用法

js
const open = require('open');


// Opens the image in the default image viewer and waits for the opened app to quit.
await open('unicorn.png', {wait: true});
console.log('The image viewer app quit');

// Opens the URL in the default browser.
await open('https://sindresorhus.com');

// Opens the URL in a specified browser.
await open('https://sindresorhus.com', {app: {name: 'firefox'}});

// Specify app arguments.
await open('https://sindresorhus.com', {app: {name: 'google chrome', arguments: ['--incognito']}});

// Open an app
await open.openApp('xcode');

// Open an app with arguments
await open.openApp(open.apps.chrome, {arguments: ['--incognito']});

open 函数

js
const open = (target, options) => {
  // 传递的 target 必须是个字符串
	if (typeof target !== 'string') {
		throw new TypeError('Expected a `target`');
	}

  // 调用 baseOpen
	return baseOpen({
		...options,
		target
	});
};

openApp 函数

js
const openApp = (name, options) => {
  // name 必须是个 string
	if (typeof name !== 'string') {
		throw new TypeError('Expected a `name`');
	}

	const {arguments: appArguments = []} = options || {};
  // appArguments 参数必须是数组或者 null
	if (appArguments !== undefined && appArguments !== null && !Array.isArray(appArguments)) {
		throw new TypeError('Expected `appArguments` as Array type');
	}

  // 调用 baseOpen
	return baseOpen({
		...options,
		app: {
			name,
			arguments: appArguments
		}
	});
};

baseOpen 函数

删减后的代码,只看 windows 的实现过程。

js
// open('https://sindresorhus.com')
const baseOpen = async options => {
  // 初始化且合并传入参数
	options = {
		wait: false,
		background: false,
		newInstance: false,
		allowNonzeroExitCode: false,
		...options
	};

  // 拿到应用程序名称和启动参数
	let {name: app, arguments: appArguments = []} = options.app || {};
	appArguments = [...appArguments];

	let command; // 命令行
	const cliArguments = []; // 命令行参数
	const childProcessOptions = {}; // 子进程执行参数

	if (platform === 'win32' || (isWsl && !isDocker())) {
    // windows 系统或者是 windows 子系统

    // 获取 windows 子系统中固定驱动器的装载点目录
		const mountPoint = await getWslDrivesMountPoint();

    // 获取执行的命令行
		command = isWsl ?
			`${mountPoint}c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe` :
			`${process.env.SYSTEMROOT}\\System32\\WindowsPowerShell\\v1.0\\powershell`;

    // 命令行参数
    // https://learn.microsoft.com/zh-cn/powershell/scripting/samples/sample-scripts-for-administration?view=powershell-7.3
		cliArguments.push(
			'-NoProfile', // 不运行Windows PowerShell配置文件
			'-NonInteractive', // 此开关用于创建不应要求用户输入的会话
			'–ExecutionPolicy', // 查看 PowerShell 会话的有效执行策略
			'Bypass', // 执行策略不需要区域检查
			'-EncodedCommand' // 接受命令的 Base64-encoded 字符串版本
		);

		if (!isWsl) {
      // 在Windows上不能引用或转义参数
			childProcessOptions.windowsVerbatimArguments = true;
		}

    // window 对应的启动命令 Start 及参数
		const encodedArguments = ['Start'];

		if (options.wait) {
      // 是否等待打开的应用程序退出
			encodedArguments.push('-Wait');
		}

		if (app) {
			// 带双引号的双引号,以确保传递内部引号。
			// 使用反引号分隔PowerShell解释的内部引号。
			encodedArguments.push(`"\`"${app}\`""`, '-ArgumentList');
			if (options.target) {
        // 将需要打开的目标放到应用启动参数的第一位
				appArguments.unshift(options.target);
			}
		} else if (options.target) {
      // 没有需要启动的应用名称,就直接将要打开的目标放到 Start 的参数数组中
			encodedArguments.push(`"${options.target}"`);
		}

		if (appArguments.length > 0) {
      // 拼接应用程序的启动参数
			appArguments = appArguments.map(arg => `"\`"${arg}\`""`);
			encodedArguments.push(appArguments.join(','));
		}

		// 使用PowerShell接受的Base64编码命令允许特殊字符
    // 将应用程序的启动参数转为 Buffer 类型
		// encodedArguments: ['Start', '"https://sindresorhus.com"'] => UwB0AGEAcgB0ACAAIgBoAHQAdABwAHMAOgAvAC8AcwBpAG4AZAByAGUAcwBvAHIAaAB1AHMALgBjAG8AbQAiAA==
		options.target = Buffer.from(encodedArguments.join(' '), 'utf16le').toString('base64');
	}

	if (options.target) {
    // 将应用程序的启动参数加入命令行的启动参数中
		cliArguments.push(options.target);
	}

  // 子进程执行
	/**
	 * command: "C:\\WINDOWS\\System32\\WindowsPowerShell\\v1.0\\powershell"
	 * cliArguments: ["-NoProfile", "-NonInteractive", "–ExecutionPolicy", "Bypass", "-EncodedCommand","UwB0AGEAcgB0ACAAIgBoAHQAdABwAHMAOgAvAC8AcwBpAG4AZAByAGUAcwBvAHIAaAB1AHMALgBjAG8AbQAiAA=="]
	 * childProcessOptions: {windowsVerbatimArguments: true}
	 */
	const subprocess = childProcess.spawn(command, cliArguments, childProcessOptions);

  // 为了防止父进程的退出,导致子进程退出
	subprocess.unref();

	return subprocess;
};