【若川视野 x 源码共读】第13期 | open 打开浏览器

356 阅读8分钟

本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。

开发时总会遇到这样的场景:当项目启动后,浏览器就自动打开了页面。这是如何做到的?原理时啥,今天要学习open的源码来揭秘背后的故事~

1.参考资料

川哥的文章:

每次启动项目的服务,电脑竟然乖乖的帮我打开了浏览器,100行源码揭秘!

源码共读群友(前舟小哥)文章:【若川视野 x 源码共读】第13期|启动项目...打开浏览器...?谁帮我打开的?

川哥的代码:github.com/lxchuan12/o…

2.了解用法

readme翻译:我觉得好好看看readme,对接下来来读代码很有帮助,一些看不懂的地方,根据你已经掌握的信息可以猜测。

2.1 是什么,为什么?

这意味着在命令行工具和脚本中使用,而不是在浏览器中使用。

如果 Electron 需要它,请改用 shell.openPath()。

这个包不做任何安全保证。 如果您传入不受信任的输入,则由您来适当地清理它。

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

2.2 怎么用?

用法:

// 引入
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']});

2.3 什么原理

它在 macOS 上使用 open 命令,在 Windows 上启动,在其他平台上使用 xdg-open 命令。

下面是在mac上使用open命令打开川哥博客的截图:

2.4 相关api

2.4.1open

open(target, options?)
返回对生成的子进程的承诺(promise)。 
您通常不需要将它用于任何事情,但如果您想附加自定义事件侦听器或直接在生成的进程上执行其他操作,它会很有用。

参数:
(1)target: string类型,你想打开的东西。 可以是 URL、文件或可执行文件。
在文件类型的默认应用程序中打开。 例如,URL 在您的默认浏览器中打开。

(2)options:object类型,包含如下属性:

(2.1)wait:boolen类型,在履行承诺之前(promise状态fulfilled)等待打开的应用程序退出。 
如果为 false,则在打开应用程序时立即执行(fulfilled)。请注意,它等待应用程序退出,而不仅仅是窗口关闭。
在 Windows 上,您必须明确指定一个应用程序才能等待。

(2.2)background(仅mac): boolean类型,不要将应用程序置于前台。 

(2.3newInstance(仅mac):即使它已经在运行,也要打开应用程序的一个新实例。新实例总是在其他平台上打开。

(2.4)app: 要打开app的名字和参数,app的值可以是一个字符串;也可以是一个字符串数组,加上可选的参数数组;
也可以是一个对象数组,其中每一个对象由名字和参数组成{name:字符串或者字符串数组,arguments参数:字符串数组}

指定应用程序的名称以使用应用程序参数和可选的应用程序参数打开目标。 
app 可以是一组要尝试打开的应用程序,而 name 可以是一组要尝试的应用程序名称。
如果每个应用程序失败,都会抛出最后一个错误。

(2.5)allowNonzeroExitCode :boolean类型,当等待选项为真时,允许打开的应用程序以非零退出代码退出。
我们不建议设置此选项。 成功的约定是退出代码为零。

2.4.2 open.apps

一个包含常见应用程序自动检测二进制名称的对象。 有助于解决跨平台差异。
const open = require('open');

await open('https://google.com', {
	app: {
		name: open.apps.chrome
	}
});

2.4.3 open.openApp(name, options?)

1)name: 字符串类型,

应用程序名称取决于平台。 不要在可重用模块中对其进行硬编码。
例如,Chrome 在 macOS 上是 google chrome,
在 Linux 上是 google-chrome,在 Windows 上是 chrome。 
如果可能,请使用自动检测要使用的正确二进制文件的 open.apps。

您也可以传入应用程序的完整路径。 
例如,在 WSL 上,对于 Chrome 的 Windows 安装,
这可以是 /mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe。

app 参数依赖于 app。 检查应用程序的文档,了解它接受哪些参数。
(2)options : 对象类型,与 open 相同的选项,除了 app 和以下附加项:
 arguments: 字符串数组,传递给应用程序的参数。这些参数取决于应用程序。 检查应用程序的文档,了解它接受哪些参数。 

3.猜测实现方法

1.判断是在什么系统上,决定使用什么命令

2.判断要打开的资源是url,还是本地资源,还是可执行文件

3.根据参数使用默认的浏览器或者相关应用打开对应的资源

4.读源码

4.1 open

/open/index.js

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

	return baseOpen({
		...options,
		target
	});
};

open方法调用baseOpen:

const baseOpen = async options => {
  // 给参数里面加一些共通的属性
	options = {
		wait: false,
		background: false,
		newInstance: false,
		allowNonzeroExitCode: false,
		...options
	};

  // 如果app参数是一个数组,则遍历这个数组,使用这些应用打开资源
	if (Array.isArray(options.app)) {
		return pTryEach(options.app, singleApp => baseOpen({
			...options,
			app: singleApp
		}));
	}

  // 获取应用名和应用参数
	let {name: app, arguments: appArguments = []} = options.app || {};
	appArguments = [...appArguments];

  // 如果应用名是一个数组,则使用这些应用打开资源
	if (Array.isArray(app)) {
		return pTryEach(app, appName => baseOpen({
			...options,
			app: {
				name: appName,
				arguments: appArguments
			}
		}));
	}

  // 命令和参数
	let command;
	const cliArguments = [];
	const childProcessOptions = {};

  // 如果是mac
	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系统或者linux上运行的windows
    
    // 获取 WSL 中固定驱动器的挂载点。
		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');
	} 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;
      // xdg-open
			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;
		}
	}

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

	if (platform === 'darwin' && appArguments.length > 0) {
		cliArguments.push('--args', ...appArguments);
	}
  // 衍生新进程
	const subprocess = childProcess.spawn(command, cliArguments, childProcessOptions);

	if (options.wait) {
		return new Promise((resolve, reject) => {
      // 应用程序的进程一旦运行出错,就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 退出,则使用 subprocess.unref() 方法。 
  // 这样做会使父进程的事件循环不将子进程包括在其引用计数中,从而允许父进程独立于子进程退出,除非在子进程和父进程之间建立了 IPC 通道。
	subprocess.unref();

	return subprocess;
};

关键点在于:

(1)使用platform判读系统类型,从而识别出该使用什么命令,命令应该有哪些参数

(2)使用childProcess.spawn()方法创建应用

4.2 openApp

打开应用,调用了baseOpen方法:

const openApp = (name, options) => {
	if (typeof name !== 'string') {
		throw new TypeError('Expected a `name`');
	}

	const {arguments: appArguments = []} = options || {};
	if (appArguments !== undefined && appArguments !== null && !Array.isArray(appArguments)) {
		throw new TypeError('Expected `appArguments` as Array type');
	}

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

4.3 apps

给app定义一些属性,主要是给chrome,firefox, edge浏览器定义属性:

const apps = {};

defineLazyProperty(apps, 'chrome', () => detectPlatformBinary({
	darwin: 'google chrome',
	win32: 'chrome',
	linux: ['google-chrome', 'google-chrome-stable', 'chromium']
}, {
	wsl: {
		ia32: '/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe',
		x64: ['/mnt/c/Program Files/Google/Chrome/Application/chrome.exe', '/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe']
	}
}));

defineLazyProperty(apps, 'firefox', () => detectPlatformBinary({
	darwin: 'firefox',
	win32: 'C:\Program Files\Mozilla Firefox\firefox.exe',
	linux: 'firefox'
}, {
	wsl: '/mnt/c/Program Files/Mozilla Firefox/firefox.exe'
}));

defineLazyProperty(apps, 'edge', () => detectPlatformBinary({
	darwin: 'microsoft edge',
	win32: 'msedge',
	linux: ['microsoft-edge', 'microsoft-edge-dev']
}, {
	wsl: '/mnt/c/Program Files (x86)/Microsoft/Edge/Application/msedge.exe'
}));

open.apps = apps;

detectPlatformBinary:

function detectArchBinary(binary) {
	if (typeof binary === 'string' || Array.isArray(binary)) {
		return binary;
	}

	const {[arch]: archBinary} = binary;

	if (!archBinary) {
		throw new Error(`${arch} is not supported`);
	}

	return archBinary;
}

function detectPlatformBinary({[platform]: platformBinary}, {wsl}) {
  // linux下的相关系统
	if (wsl && isWsl) {
		return detectArchBinary(wsl);
	}
	// 没有传系统
	if (!platformBinary) {
		throw new Error(`${platform} is not supported`);
	}

	return detectArchBinary(platformBinary);
}

5.调试验证

我的VScode不支持自动debug, 按照川哥文档中给的vs code debug文档地址配置一下:

配置一下,保存,新建一个终端:

进入到open中:

进入到baseOpen:

判断系统类型:

childProcess.spawn方法:

最后,打开川哥的博客

6.总结和收获

总结:open的原理是首先通过node.js的process模块的platform判断当前操作系统;然后通过node.js中child_process模块的spawn方法来调用对应操作系统打开浏览器或app的命令。

收获:

(1)了解了相关的依赖用法:

is-wsl: 检查进程是否在适用于 Linux 的 Windows 子系统中运行(Windows 上的 Bash)

is-docker: 检查进程是否在 Docker 容器内运行

child_process.spawn(): child_process.spawn() 方法使用给定的 command 和 args 中的命令行参数衍生新进程

define-lazy-prop:在对象上定义惰性计算的属性

process.platform :属性返回标识运行 Node.js 进程的操作系统平台的字符串

(2)了解open基本原理

(3)了解vs-code node debug配置

学完本期源码,您可以思考如下问题;

1.webpack配置文件中devServer选项的open属性作用是什么?应如何配置?

2.vue-cli运行命令时 --open参数的作用是什么?、

3.node.js子进程模块child_process的spawn和exec方法有什么异同?

4.如何用node.js判断当前的操作系统?

5.简述open的原理?