详解NodeJs中《open》库唤起浏览器原理

3,901 阅读6分钟

本文正在参与技术专题征文Node.js进阶之路,点击查看详情

源码学习总是带着些枯燥的,但长期来看坚持看源码是受益无穷的。

But,if you are very 投入,yet very 刺激! ----- 愣锤

open库是什么?

open是NodeJs的一个跨平台的用于打开URL文件可执行文件的库。比如常见的很多库都依赖他唤起浏览器并打开指定URL。本文也将会基于8.4.0来结束open的基本使用和实现原理,不扯闲篇,开始吧!!!下面先看send的使用示例:

  • 安装
# 安装
npm install open -S
  • NodeJs环境下使用send唤起浏览器
// 使用默认浏览器打开百度
open('https://www.baidu.com');

// 使用火狐浏览器打开百度
open('https://www.baidu.com', {
  app: {
    name: 'firefox',
  },
});

唤起默认浏览器的效果图如下,我的默认浏览器是chrome

image.png

除了可以唤起浏览器外,还可以唤起图片查看程序等,具体就不多说了。那么知道了如何使用send之后,还是老规矩,知其然、知其所以然。我们通过send的源码来分析一下是如何唤起浏览器的。

open唤起浏览器原理

小伙们可能使用的操作系统不同,有人是windows有人是macOS等等,那么我们先回忆下在这些操作系统中是如何调用系统命令的呢?这时候小伙伴说了,这还不简单,直接终端键入命令呗!比如常见的命令像cd进入指定路径,ls查看资源列表等等。那么要提问了,在windowsmacOS中唤起软件用哪个命令呢?

操作系统中提供了唤起程序的命令,比如MacOSIOS中可以在终端使用open命令唤起appwindows系统中使用利用PowerShell命令借助powershell唤起程序,下面看例子:

# mac的terminal终端
open https://www.baidu.com # 使用默认浏览器打开百度
open -a "google chrome" https://www.baidu.com # 指定谷歌浏览器打开百度

# windows系统终端
PowerShell Start https://www.baidu.com # 使用默认浏览器打开百度

其实讲到这里,我想小伙伴们即使之前不知道,现在也基本上能思考库该库的大致实现了!!!

该库的核心实现就是判断不同的操作系统,利用node的子进程执行不同的系统命令。下面我们先看该库的主体代码吧:

/**
 * 对外暴露的方法,打开程序
 */
const open = (target, options) => {
  if (typeof target !== 'string') {
    throw new TypeError('Expected a `target`');
  }

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

module.exports = open;

这里无非是判断下参数类型,然后调用baseOpen方法传入用户参数唤起程序。下面看下baseOpen的实现:

// 获取系统环境相关参数
const { platform, arch } = process;

const baseOpen = async options => {
  // 合并初始化参数,省略部分参数初始化的代码
  options = {
    wait: false,
    background: false,
    newInstance: false,
    allowNonzeroExitCode: false,
    ...options
  };
	
  let command;
  const cliArguments = [];
  const childProcessOptions = {};
	
  /**
   * MacOS、IOS系统
   */
  if (platform === 'darwin') {
    // ....
  }
	
  /**
   * windows系统、linux下的windows系统
   */
  else if (platform === 'win32' || (isWsl && !isDocker())) {
    // ....
  }
	
  // linux或其他系统
  else {}

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

  if (platform === 'darwin' && appArguments.length > 0) {
    cliArguments.push('--args', ...appArguments);
  }
	
  // 利用spawn开启一个子进程,执行command命令,传入对应的cliArguments参数
  const subprocess = childProcess.spawn(command, cliArguments, childProcessOptions);

  if (options.wait) {
    // ...
  }

  // 父进程不再等待子进程的退出,而是父进程独立于子进程进行退出
  subprocess.unref();

  return subprocess;
}

这里的大体结构可以看到:

  • 判断当前系统环境进行不同的处理,生成对应的唤起程序的系统命令
    • macOS系统
    • windos系统
    • linux系统
  • 利用node子进程执行唤起程序的系统命令

这里有需要特殊关注的点是spawn的子进程开启部分:

const subprocess = childProcess.spawn(command, cliArguments, {
  // 忽略子进程中的文件描述符
  stdio: 'ignore',
  // 让子进程在父进程退出后继续运行
  detached: true,
});

// 父进程不再等待子进程退出
subprocess.unref();

扩展知识:spawn开启子进程时的参数detached: true,让子进程在父进程退出后继续运行,subprocess.unref();让父进程不再等待子进程退出再退出。

接下来我们分别看下各种系统下具体做了什么处理逻辑吧:

  • windows中的参数实现:
/**
 * windows系统、linux下的windows系统
 */
else if (platform === 'win32' || (isWsl && !isDocker())) {
  // 获取linux下的windows系统的驱动器入口地址
  const mountPoint = await getWslDrivesMountPoint();

  /**
   * 根据不同的windows系统获取powershell程序的路径
   *  - isWsl判断是否是在linux系统的windows子系统中运行的进程
   *  - process.env.SYSTEMROOT用于获取系统路径, EG: C:\Windows
   */
  command = isWsl
    ?  `${mountPoint}c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe`
    :  `${process.env.SYSTEMROOT}\\System32\\WindowsPowerShell\\v1.0\\powershell`;

  /**
   * 可以在cmd终端输入PowserShell -Help查看相关参数的帮助文档:
   * -NoProfile 不使用用户配置文件
   * -NonInteractive 不向用户显示交互式提示
   * –ExecutionPolicy Bypass 设置会话的默认执行策略为Bypass
   * -EncodedCommand 命令接受Base64的编码字符串。用于支持参数中使用一些特殊字符
   */
  cliArguments.push(
    '-NoProfile',
    '-NonInteractive',
    '–ExecutionPolicy',
    'Bypass',
    '-EncodedCommand'
  );

  if (!isWsl) {
    childProcessOptions.windowsVerbatimArguments = true;
  }

  // 需要编码的参数
  const encodedArguments = ['Start'];

  if (options.wait) {
    encodedArguments.push('-Wait');
  }

  // 如果指定了启动的app,则使用指定的app,否则使用默认的启动程序
  // 当Start命令后面直接拼接options.target参数时,此时因为没有指定app,所以系统会使用默认的启动程序
  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.
  // 对需要编码的参数使用Buffer转换成base64的字符串
  options.target = Buffer.from(
    encodedArguments.join(' '),
    'utf16le',
  ).toString('base64');
}

详细的实现基本放在了注释里面,但是要注意的是windows系统中通过通过powershell程序打开程序时传入的参数-EncodedCommand可以让参数执行base64的数据,这样便可以支持复杂的字符。base64字符数据的转换利用了Buffer对象。

  • MacOS系统下的实现
/**
 * MacOS、IOS系统
 */
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);
  }
}

macOS下也是拼接参数,且逻辑编辑简单,没什么好说的。linux下就不多赘述了,有兴趣的可以翻阅看看。那么总结一下open库原理的核心实现如下:

const { spawn } = require('child_process');

const { platform } = process;

function open(target) {
  let command;
  const commandArgs = [];

  if (platform === 'darwin') {
    command = 'open';
    commandArgs.push(target);
  } else if (platform === 'win32') {
    const startArgs = [];
    command = 'PowerShell';
    commandArgs.push(
      '-NoProfile',
      '-NonInteractive',
      '–ExecutionPolicy',
      'Bypass',
      '-EncodedCommand',
    );
    startArgs.push('Start');
    startArgs.push(target);
    commandArgs.push(
      Buffer.from(startArgs.join(' '), 'utf16le').toString('base64'),
    );
  }

  spawn(command, commandArgs).unref();
}

// 测试,唤起效果
open('https://www.baidu.com');

在终端运行node脚本,测试下效果:

# 终端运行该脚本
# node index.js

image.png

原理总结

open库实现打开应用的原理,都是利用node子进程直接或间接执行系统的启动命令:

  • windows系统或者linux下的windows系统,利用powershell程序传入Start命令和启动参数唤起程序,直接利用PowerShell命令也可以。
  • MacOSIOS系统下利用open系统命令。
  • linux下通过xdg-open唤起。

结束语

如果你喜欢这篇文章,欢迎小伙伴们❤️❤️❤️点赞👍👍收藏👍👍转发❤️❤️❤️哈~~~同时推荐你阅读我的其他文章: