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

132 阅读2分钟

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

介绍

  • 有些项目在启动后会默认打开对应地址的浏览器,webpack 和脚手架也可以通过配置是否自动打开, 其原理实际是使用了 open

  • open 原理:针对不同的系统,使用 Node.js 子进程 child_process 模块的 spawn 方法,调用系统命令打开浏览器

Usage

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"] });

webpack 中配置

// 文件中配置
module.exports = {
  // ...
  devServer: {
    open: true,
    // ...
  }
}

// 命令行配置
npx webpack serve --open
npx webpack serve --no-open

脚手架中配置

vue-cli

  • packages/@vue/cli-shared-utils/lib/openBrowser.js
const open = require("open");
// ...

// Fallback to open
// (It will always open new tab)
try {
  var options = { app: browser, url: true };
  open(url, options).catch(() => {}); // Prevent `unhandledRejection` error.
  return true;
} catch (err) {
  return false;
}

create-react-app

  • packages/react-dev-utils/openBrowser.js
const open = require("open");
// ...

// Fallback to open
// (It will always open new tab)
try {
  var options = { app: browser, wait: false, url: true };
  open(url, options).catch(() => {}); // Prevent `unhandledRejection` error.
  return true;
} catch (err) {
  return false;
}

源码

const childProcess = require("child_process");

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

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

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 = {};

  // const {platform, arch} = process;
  // 设备判断
  if (platform === "darwin") {
    command = "open";
    // ...
  } else 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"];
    // ...

    // 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 {
      // ...
    }

    if (appArguments.length > 0) {
      cliArguments.push(...appArguments);
    }
    // ...
  }

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

  if (platform === "darwin" && appArguments.length > 0) {
    cliArguments.push("--args", ...appArguments);
  }

  // Point 关键 childProcess.spawn
  const subprocess = childProcess.spawn(
    command,
    cliArguments,
    childProcessOptions
  );

  // ...

  subprocess.unref();

  return subprocess;
};

child_process.spawn(command[, args][, options])

  • 详见

  • command <string> 要运行的命令

  • args <string>[] 字符串参数列表

  • options <Object>

    detached <boolean> 准备子进程独立于其父进程运行
    stdio <Array> | <string> 子进程的标准输入输出配置

收获

  • 了解 child_process