【源码共读】第13期 | open 打开浏览器

624 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第2天,点击查看活动详情

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

【若川视野 x 源码共读】第13期 | open 打开浏览器 点击了解本期详情一起参与

今天阅读的库是:open

image-20221024144927456

使用场景

当我们使用脚手架创建vue或者react项目后,启动项目,命令行会帮助我们自动打开浏览器,访问对应的项目地址。那么,这个打开的动作是怎么实现的呢,我们来一探究竟。

vite为例,文档中也有提及使用了open这个库

image-20221024145632862

源码分析

首先,我们看下readme

为什么使用这个库呢,这个库有什么优势

支持跨平台执行命令

image-20221024145832828

根据说明,我们可能有如下疑问:

1.为什么要用spawn代替exec,他们之间有什么异同
2.怎么做到跨平台使用
3.open的原理是什么
4.node判断当前平台

整个文件目录很简单

image-20221024161715237

源码应该是在index.js

我们可以写一个简单的测试用例并打断点

(async () => {
	const open = require("../open/index.js");
	await open("https://www.juejin.cn");
})();

image-20221024165108661

可以看出,最终返回的是baseOpen,我们来看下baseOpen的实现

const baseOpen = async (options) => {
	options = {
		wait: false,
		background: false,
		newInstance: false,
		allowNonzeroExitCode: false,
		...options,
	};
	/*
		省略...
		唤醒外部应用
	*/

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

	// 根据不同平台构建command
	if (platform === "darwin") {
		// mac
		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平台 或  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;
			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
	);

	/*
		省略...
	*/

	subprocess.unref();

	return subprocess;
};

原理:

  • 根据platform判断当前的系统
  • 不同的系统拼接对应的命令
  • 使用childProcess.spawn来执行命令,唤起应用

总结

open这个库是根据node js中的 process.platform参数来识别出不同的平台,根据平台构建对应的指令,再调用 childProcess.spawn来执行命令,从而唤起对应的应用。

  1. 为什么要用spawn代替exec,他们之间有什么异同

    • child_process.exec 使用子进程执行命令,缓存子进程的输出,并将子进程的输出以回调函数参数的形式一次性返回。但是,当程序过大时,会崩溃

    • spawn 会返回一个带有stdoutstderr流的对象。你可以通过stdout流来读取子进程返回给node js的数据。

  2. 怎么做到跨平台使用

    • process.platform参数来识别出不同的平台,构建对应的命令