从remote-git-tags源码中,学习child_process子进程,promisify方法实现

188 阅读7分钟

前言

提起源码,相信大家无不头疼,每一次打开github想要拜读大神的开源,总是不知道从何下手,怎么调试,从哪里开始,每一次在不知所云的文件中怀疑自己的技术水平,怀抱着对技术的热爱,源码是技术人永远要去过的一道门槛,这一次,遇到了川神,他从易到难找寻了众多适合新手的源码库,带领我们从零开始,分享他的经验,分享他的学习历程,很荣幸参与到这次活动中来.

1. 简介

Get tags from a remote Git repo

本次学习的源码库为remote-git-tags,仅有22行代码,该库的作用就是,获取远程仓库的tags,并转换为ES6中Map的格式.

通过执行 **git ls-remote --tags repoUrl(仓库地址)**获取 tags信息

使用场景是通过查找tags的版本标签(如v.0.1.0,v.0.2.0 等),选择,切换特定的版本,进行后续的操作.

2. 使用

Install

 npm install remote-git-tags

Usage

import remoteGitTags from 'remote-git-tags';

console.log(await remoteGitTags('https://github.com/sindresorhus/remote-git-tags'));
//=> Map {'v1.0.0' => '69e308412e2a5cffa692951f0274091ef23e0e32', …}

API

remoteGitTags(repoUrl)

Returns a Promise<Map<string, string>> with the Git tags as keys and their commit SHA as values.

repoUrl

Type: string

The URL to the Git repo.

3. 源码

3.1.阅读起步package.json

在每次阅读源码时,都需要找到每个库的入口位置,可以从script命令中,很容易找到起步位置的信息

//package.json
{
  	//以什么模块加载文件,默认为CommonJS加载,设置module可以使用ES6模块
		"type": "module",
  	// 使用时暴露出的使用文件
		"exports": "./index.js",
  	// 指定Node的版本
		"engines": {
			"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
			},
  	// 启动命令,鼠标放上去可以看到"调试",启动调试
		"scripts": {
				"test": "xo && ava"
			},
  	// 打包的文件,如果是文件夹就会包含其中所有文件,类似白名单的功能
		"files": [
			"index.js"
			],
}

Node 中,加载 .js 后缀的文件,一般会一 CommonJS 的模块方式.如果文件后缀为 .cjs 则使用CommonJS 模块的方式加载,如果使用了 .mjs 的方式后缀,则会使用 ES6 模块的方式加载.

如果在所有目录中,一旦有packahge.json,其中指定了type属性,且值为module的话,那么在解析**.js后缀的文件时,会以ES6模块的方式解析,不指定默认还是会以CommonJS**的文件方式解析.

3.2源码

// index.js
import {promisify} from 'node:util';
import childProcess from 'node:child_process';

const execFile = promisify(childProcess.execFile);

export default async function remoteGitTags(repoUrl) {
	const {stdout} = await execFile('git', ['ls-remote', '--tags', repoUrl]);
	const tags = new Map();

	for (const line of stdout.trim().split('\n')) {
		const [hash, tagReference] = line.split('\t');

		// Strip off the indicator of dereferenced tags so we can override the
		// previous entry which points at the tag hash and not the commit hash
		// `refs/tags/v9.6.0^{}` → `v9.6.0`
		const tagName = tagReference.replace(/^refs\/tags\//, '').replace(/\^{}$/, '');

		tags.set(tagName, hash);
	}

	return tags;
}

这里源码部分非常短,在仅有22行代码,看上去非常简单,整体思路就是,使用 child_process 中的 execFile 方法,创建独立的子进程,自动执行了 git ls-remote --tags repoUrl(需要获取tags的网址) 其中使用了 nodeutil 方法 promisify ,将 execFIle 中的回调方法返回 Promise 的方式进行优化,最后获取所有的tags的标签后,使用Map的数据结构返回,其中 promisify 是一个关键的技术点.

3.3 node:util引用node的util方法

以往使用node中的util方法时我们的操作经常是这样

const util = require('util')

现在可以 node:xxx 的方式引用node的一些核心模块,没见过的写法QAQ,学无止境...

3.4 childProcess 中exec与execFile的使用与区别

childProcessnode中创建子进程的方法,其中有4个方法,分别是 spawn , exec , execFile , fork .本文中使用了execFile,其中execexecFile都是执行的非node应用,且都以回调的方式返回了结果.

不同点是,exec是执行了一段shell命令,而execFile是执行了一个应用.

举例来说,echoUNIX系统的一个自带命令,我们直接可以在命令行执行:

echo Hello World

结果,在命令行中会打印出hello world.

  1. 通过exec实现

    const childProcess = require('child_process') childProcess.exec('echo Hello World',(err,stdout)=>{ console.log(stdout) //执行后输出 Hello World })

执行后,我们会发现,exec的参数,第一个就是shell的命令

  1. 通过execFile实现

    const childProcess = require('child_process') childProcess.execFile('exho',[ 'Hello','World' ],(err,stdout)=>{ console.log(stdout) //执行后输出 Hello World })

execFile类似于执行了名为echo的应用,然后传入参数。execFlie会在process.env.PATH的路径中依次寻找是否有名为'echo'的应用,找到后就会执行。默认的process.env.PATH路径中包含了'usr/local/bin',而这个'usr/local/bin'目录中就存在了这个名为'echo'的程序,传入Hello和World两个参数,执行后返回。

  1. 安全问题区别

exec是执行一段shell命令,如果有人在exec命令使用了不好的命令,就会达到类似sql注入的情况,如

// 为人津津乐道的 rm -rf ,这种动辄删库跑路的安全问题
const childProcess = require('child_process')
childProcess.exec('echo Hello World;rm -rf')

exec使用echo命令的执行等级很高,会出现安全问题,execFile则不同了.

const childProcess = require('child_process')
childProcess.execFile('echo',[ 'Hello','World',';rm -rf'])

在传入参数的同时,会检测传入实参执行的安全性,如果存在安全性问题,会抛出异常。除了execFile外,spawnfork也都不能直接执行shell,因此安全性较高.

3.5 promisify 异步函数promise化

  1. node中的回调函数

node中,有很多函数都是异步执行,在函数中,往往需要在最后一个参数传入一个回调函数,来执行异步取得的结果,如

const fs = require('fs')
fs.readFile('./test.js','utf-8',(err,data)=>{
	if(err) console.log(err)
  else console.log(data)
})
  1. 使用promise优化

    const fs = require('fs') function readFile(){ //返回一个Proimise对象,在调用readFile时可以直接使用then获取回调中的data和err return new Promise((resolve,reject)=>{ fs.readFile('./test.js','utf-8',(err,data)=>{ if(err) reject(err) else resolve(data) }) }) }

    readFile().then((data)=>console.log(data)).catch((err)=>console.log(err))

3.封装通用的promisify函数

const fs = require('fs')

function promisify(fn){
  // fn 就是需要promise化的函数,return出去一个新的函数
	return function(...args){
    //导出Promise对象
  	return new Promnise((resolve,reject)=>{
      //在这里创建callback函数,fn调用
    		const callback = (err,...values)=>{
          //在调用catch时传入err
    			if(err) reject(err)
          //在调用then将正确的data传入
          else resolve(values)
    		}
        //执行fn,并将结果以参数的形式传入callback
        fn.apply(null,[...args,callback])
    })
  }
}


const readFile = promisify(fs.readFile)

readFile('./test.js','utf-8')
	.then((data)=>console.log(data))
	.catch((err)=>console.log(err))

总结

  1. remote-git-tags 是通过子进程 child_process中execFile的方法,执行了 git ls-remote tags命令,拿到返回的tags后,使用ES6的Map结构返回

  2. 学会了 require('node:xxxx')的引用node原生模块的方法

  3. 学会了child_process子进程的应用,还有exec与execFile的区别,exec关于安全性的弊端

  4. 封装了promisify工厂函数,使回调函数promise化

源码阅读感受

本文在为若川大神的带领下,本人源码阅读记录,非常感谢川神不辞辛苦为我们想从源码方面进步,却始终不得要领的小白主持着这个活动,这里再次感谢川神的坚持.仅有22行的源码阅读,却能收获很多,事实证明阅读源码是一个非常提升技术的方式,通过简单的源码库,增加阅读方法,为以后积累经验,相信有一天,任何源码库都可以上手.

如果有想要和我一样,需要从各种开源库中,阅读,学习各种源码的知识,请加入我们的活动吧...下面是川神的联系方式,还有我们的活动地址...

最近组织了源码共读活动,感兴趣的可以加我微信 ruochuan12 参与,长期交流学习。

作者:常以若川为名混迹于江湖。欢迎加我微信ruochuan12。前端路上 | 所知甚少,唯善学。
关注公众号若川视野,每周一起学源码,学会看源码,进阶高级前端。
若川的博客segmentfault若川视野专栏,开通了若川视野专栏,欢迎关注~
掘金专栏,欢迎关注~
知乎若川视野专栏,开通了若川视野专栏,欢迎关注~
github blog,求个star^_^~

参考文章

  1. 川神的原文,辅助阅读,体验更佳

  2. git tag 的作用及使用方法

  3. package.json文件中各个属性的含义及作用

  4. package.json中exports作用

  5. package.json中files的作用

  6. nodejs中的子进程,深入解析child_process模块和cluster模块

  7. Nodejs进阶:如何玩转子进程(child_process)

  8. promise的异步改造

原文地址

www.yuque.com/womaoni-nlw…