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

1. 前言

第五次参与活动咯,如果你有时间(摸鱼),想学又不知道学什么的话,欢迎一同参与

如果你有一些钱不知道花在A还是B上,你先不作决定,没问题,因为钱还是你的。 但如果你有一些时间,不知道花在A上还是B上,不行,因为过了这段时间,这段时间就不是你的了。 ——《暗时间》

1.1 场景

在我们启动项目的时候,浏览器都会自动弹出并且显示项目内容,那么这一步到底是谁叫电脑做的呢?
如果你想直接知道原理以及具体代码可以直接跳到 3.5 baseOpen方法

1.2 你能学到

  1. 电脑打开浏览器的原理
  2. 具体的源码实现
  3. 调试源码

    2. 日常开发中是用什么打开浏览器的

    2.1 在webpack 中

    webpage提供了一个 dev-Server,使我们启动服务器后打开浏览器,在 webpack.config.js将其open属性设置为指定页面即可
    1. module.exports = {
    2. //...
    3. devServer: {
    4. open: ['/my-page'],
    5. },
    6. };
    open 接收一个数组—— 也就是可以一同打开多个指定页面,其余具体操作细节可以查看官方文档

一般我们开发项目都不会自己用 webpack 从头搭建一个了,而是使用脚手架

2.2 在vue-cli 中

  1. npx @vue/cli create vue3-project
  2. # npm i -g yarn
  3. # yarn serve 不会自动打开浏览器
  4. yarn serve
  5. # 添加 --open 参数后会自动打开浏览器
  6. yarn serve --open

2.3 create-react-app 中

  1. npx create-react-app react-project
  2. # 我的 open-analysis 项目中 react-project 文件夹
  3. # npm i -g yarn
  4. # 默认自动打开了浏览器
  5. yarn start

而以上三者打开浏览器共同用到到的第三方库就是 open,也就是我们今天的主角

  • create-react-app 引用的地方
  • vue-cli 引用的地方

    3. 理解源码

    3.1 先了解其原理

    读开源项目,先从readme 开始。

    为什么用 open

    Why?

    • Actively maintained.
    • Supports app arguments.
    • Safer as it uses spawn instead of exec.
    • Fixes most of the original node-open issues.
    • Includes the latest xdg-openscript for Linux.
    • Supports WSL paths to Windows apps.
  • 积极维护

  • 支持应用参数
  • 更安全,使用 spawn而不是exec (这里有关键词)
  • 修复了大多数node-open的问题
  • 包括适用Linux最新版xdf-open脚本
  • 支持Windows应用的WSL路径

这上面说到能够支持在不同系统上帮你打开浏览器,但是开发者在使用的时候只需要一种命令,那就是open

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

那么我的第一反应就是 用 Node.js获取系统类型,再分析获得的字符串,根据情况调用对应的方法。事实也是如此:

一句话概括open原理则是:针对不同的系统,使用Node.js的子进程child_process模块的spawn方法,调用系统的命令打开浏览器。

spawn 和 exec

这上面还提到了,更安全,因为使用 spawn 而不是 exec

两者的相同点

  1. 都用于开一个子进程执行指定命令
  2. 都可以自定义子进程的运行环境
  3. 都返回一个ChildProcess对象,所以他们都可以取得子进程的标准输入流标准输出流标准错误流

    两者的不同点

  4. 接收参数方式不同

    1. spawn使用了参数数组
    2. exec则直接接在命令后
  5. 子进程返回给Node的数据量

    1. spawn没有限制子进程可以返回给Node的数据大小,
    2. exec则在options配置对象中有maxBuffer参数限制,且默认为200K,如果超出,那么子进程将会被杀死,并报错:Error:maxBuffer exceeded,虽然可以手动调大maxBuffer参数,但是并不被推荐

      这或许与这两个 api设置的本意有关:spawn应用来运行返回大量数据的子进程,如图像处理,文件读取等。而exec则应用来运行只返回少量返回值的子进程,如只返回一个状态码。

  6. exec方法相比spawn方法,多提供了一个回调函数,可以更便捷得获取子进程输出。


看起来两个效果差不多,并且exec更为便携,但是可能就是在用于便携性的同时,也付出了maxBuffer的代价,大小超出子进程就会被直接杀死。而这,可能这就是安全性方面的问题

解决方案

  1. maxBuffer这个传一个足够大的参数
  2. 直接使用spawn,放弃使用exec

显然 open 源码中直接用的是第二个解决方案


接下来正式开始进入源码的世界

3.2 环境准备

  1. # 克隆官方项目
  2. git clone https://github.com/sindresorhus/open.git
  3. # npm i -g yarn
  4. cd open
  5. yarn

3.3 测试demo

新建一个文件夹examples用于存储测试demo,

  1. // open/examples/index.js
  2. (async () => {
  3. const open = require("../index.js");
  4. await open("https://juejin.cn/user/2164280112722760");
  5. })();

在终端执行命令:node examples/index.js
image.png
然后就开始 debug

3.4 open 方法

跟随断点,我们来到 open/index.js的第227行左右,找到调用的open方法

  1. const open = (target, options) => {
  2. if (typeof target !== 'string') {
  3. throw new TypeError('Expected a `target`');
  4. }
  5. return baseOpen({
  6. ...options,
  7. target
  8. });
  9. };

其返回一个函数baseOpen的调用结果,我们再跟着进去查看

3.5 baseOpen 方法

这个方法非常的长,这里就不完全粘代码过来了,这是仓库代码位置,也就是72行到225行。
从前面的介绍中可以得知关键处必有spawn方法,我们Ctrl+F快速定位关键处:
image.png

可以发现这里接收三个参数commandcliArgumentschildProcessOptions
我们找到这三个变量定义的地方(剩下分析都写在注释)

  1. const childProcess = require('child_process');
  2. const {platform, arch} = process; //获取当前机器的platform信息,arch下面这个方法未涉及
  3. const baseOpen = async options => {
  4. options = {
  5. wait: false,
  6. background: false,
  7. newInstance: false,
  8. allowNonzeroExitCode: false,
  9. ...options
  10. };
  11. //... 此处省略部分代码
  12. //源代码101行左右位置
  13. let command; //命令
  14. const cliArguments = []; //命令携带参数
  15. const childProcessOptions = {}; //子进程选项
  16. if (platform === 'darwin') {//Darwin 是MacOSX 操作环境的操作系统成份
  17. command = 'open';//使用mac系统的open命令
  18. // ...对 mac 系统的其他操作
  19. } else if (platform === 'win32' || (isWsl && !isDocker())) {//windows || windows子系统
  20. // ...对windows的
  21. const encodedArguments = ['Start'];//用windows系统下的start命令
  22. //...
  23. } else {
  24. if (app) {
  25. command = app;
  26. } else {
  27. //... linux系统
  28. const useSystemXdgOpen = process.versions.electron ||
  29. platform === 'android' || isBundled || !exeLocalXdgOpen;
  30. command = useSystemXdgOpen ? 'xdg-open' : localXdgOpenPath;
  31. }
  32. if (appArguments.length > 0) { //根据命令携带参数处理
  33. cliArguments.push(...appArguments);
  34. }
  35. //...
  36. }
  37. const subprocess = childProcess.spawn(command, cliArguments, childProcessOptions);
  38. //...
  39. subprocess.unref();
  40. return subprocess;
  41. };

4. 总结 & 收获

4.1 原理 && 不同系统打开浏览器的命令

  • 综上所述,分析得知,open原理就是:先通过Node.jsprocess获取当前机器platform信息,再根据机器信息使用Node.jschild_process模块中的spawn方法,调用对应操作系统打开浏览器的命令
    1. # mac 用open
    2. open https://juejin.cn/user/2164280112722760
    3. # win 用start
    4. start https://juejin.cn/user/2164280112722760
    5. # linux 用 xdg-open
    6. xdg-open https://juejin.cn/user/2164280112722760

    4.2 知其然

    启动项目时候打开服务器是如此的频繁,然而在此之前我从未了解过,这下可算是其然

    工作中常用的知识,做到知其然知其所以然

4.3 读源码的技巧

尽管全部的代码很长,但如果只是想了解其原理,达到知其然的效果,实际上并不需要全部阅读,只需要跟随断点以及几个关键词即可找到关键代码,即可大大降低阅读难度


🌊如果有所帮助,欢迎点赞关注,一起进步⛵

5. 学习资源