前言
提起源码,相信大家无不头疼,每一次打开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的网址) 其中使用了 node 的 util 方法 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的使用与区别
childProcess是node中创建子进程的方法,其中有4个方法,分别是 spawn , exec , execFile , fork .本文中使用了execFile,其中exec与execFile都是执行的非node应用,且都以回调的方式返回了结果.
不同点是,exec是执行了一段shell命令,而execFile是执行了一个应用.
举例来说,echo是UNIX系统的一个自带命令,我们直接可以在命令行执行:
echo Hello World
结果,在命令行中会打印出hello world.
-
通过exec实现
const childProcess = require('child_process') childProcess.exec('echo Hello World',(err,stdout)=>{ console.log(stdout) //执行后输出 Hello World })
执行后,我们会发现,exec的参数,第一个就是shell的命令
-
通过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两个参数,执行后返回。
- 安全问题区别
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外,spawn和fork也都不能直接执行shell,因此安全性较高.
3.5 promisify 异步函数promise化
- node中的回调函数
在node中,有很多函数都是异步执行,在函数中,往往需要在最后一个参数传入一个回调函数,来执行异步取得的结果,如
const fs = require('fs')
fs.readFile('./test.js','utf-8',(err,data)=>{
if(err) console.log(err)
else console.log(data)
})
-
使用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))
总结
-
remote-git-tags 是通过子进程 child_process中execFile的方法,执行了 git ls-remote tags命令,拿到返回的tags后,使用ES6的Map结构返回
-
学会了 require('node:xxxx')的引用node原生模块的方法
-
学会了child_process子进程的应用,还有exec与execFile的区别,exec关于安全性的弊端
-
封装了promisify工厂函数,使回调函数promise化
源码阅读感受
本文在为若川大神的带领下,本人源码阅读记录,非常感谢川神不辞辛苦为我们想从源码方面进步,却始终不得要领的小白主持着这个活动,这里再次感谢川神的坚持.仅有22行的源码阅读,却能收获很多,事实证明阅读源码是一个非常提升技术的方式,通过简单的源码库,增加阅读方法,为以后积累经验,相信有一天,任何源码库都可以上手.
如果有想要和我一样,需要从各种开源库中,阅读,学习各种源码的知识,请加入我们的活动吧...下面是川神的联系方式,还有我们的活动地址...
最近组织了源码共读活动,感兴趣的可以加我微信 ruochuan12 参与,长期交流学习。
作者:常以若川为名混迹于江湖。欢迎加我微信ruochuan12。前端路上 | 所知甚少,唯善学。
关注公众号若川视野,每周一起学源码,学会看源码,进阶高级前端。
若川的博客segmentfault若川视野专栏,开通了若川视野专栏,欢迎关注~
掘金专栏,欢迎关注~
知乎若川视野专栏,开通了若川视野专栏,欢迎关注~
github blog,求个star^_^~
参考文章
原文地址