根据github上的数据,去年最受欢迎的前端库为谷歌棋下的zx。今天我们就来聊一聊这个去年最火的前端库。

zx是什么

  zx 是谷歌实现的一个能在 node 中写 bash 的库。就像这样:

await $`echo "hello world"`;

  使用$``框起想要执行的命令,就可以直接执行 bash

  这个库的最大的便捷在于,node 和标准的 bash 都是我们非常熟悉的东西。只需要知道最基本的库的使用,就可以很快上手,写出功能很强大的工具。

基本使用

  只需要安装 zx 库,就可以便捷的在项目中使用了。

mkdir zx-demo
cd zx-demo
npm init -y
npm install zx --save
touch index.mjs

  需要一提的是,zx 命令执行的文件,需要以 .mjs 命名。

  在 .mjs 结尾的文件中,zx 可以允许在最外层使用 await,并提供了一系列常用的 node 库和函数,可以直接使用。

  现在打开刚刚创建的 index.mjs 文件,输入下面的指令,然后运行 npx zx index.mjs

await $`pwd`;
/*
输出
$ pwd
/Users/tang/Desktop/zx-demo
*/

  运行成功。

$`command`

  执行指令的两个飘折号``等同于ES6语法。你可以在内部直接使用${变量名}来嵌入命令。

var log = 'log';
await $`git ${log}`;

  这种使用方法需要注意的是,zx会自动给以${变量名}方式嵌入的内容加上括号,作为一个字符串。比如我要创建一个名为测试 测试的文件夹。在 bash 中直接执行是会失败的,如下:

mkdir 测试 测试
# 失败,创建了一个名为 测试 的文件夹,然后报错 mkdir: 测试: File exists
mkdir '测试 测试'
# 成功

  而使用zx则直接可以执行成功:

let name = '测试 测试';
await $`mkdir ${name}`;
// 输出:$ mkdir $'测试 测试' 成功

  除了这点,如果插入的变量是数组,zx 还会自动的解析数组,如下:

let flags = ['--oneline','--decorate','--color',
];
await $`git log ${flags}`;

  但是这个特性也会带来麻烦。当你直接键入命令的时候,zx 会强行解析。

let demo = 'git log';
await $`${demo}`; // 相当于在 bash 中执行 'git log'
/*
报错
$ $'git log'
/bin/bash: git log: command not found
Error: /bin/bash: git log: command not foundat file:///Users/tang/Desktop/zx-demo/index.mjs:2:8exit code: 127 (Command not found)
*/

  此时可以利用 zx 会解析数组的特性来这样操作。

let demo = `git log`;
await $`${demo.split(' ')}`;
// 执行成功

  $`command`的返回值是一个继承Promise的类。可以追踪该命令的输入输出错误退出码。还提供了两个方法,pipekill

class ProcessPromise<T> extends Promise<T> {readonly stdin: Writablereadonly stdout: Readablereadonly stderr: Readablereadonly exitCode: Promise<number>pipe(dest): ProcessPromise<T>kill(signal = 'SIGTERM'): Promise<void>
}

  其中 pipe 可以将进程的输出重定向,类似于 bash 中的管道语法。而 kill 则可以直接杀死进程。

// pipe
await $`cat file.txt`.pipe(process.stdout);
// kill
await $`npx service`.kill();

全局能力

  前文也提到过,zx 在执行以 .mjs 结尾的文件中,提供了便捷的能力,以及一些常用的函数和库。可以直接使用的库有:

  • chalk
  • fs
  • globby
  • os
  • path
  • minimist

关于这些库的作用和使用方法可以自行去 npm 上面查找,本文不做赘述。

  接下来重点聊一聊 zx 提供的两个非常有用的函数。

cd

  当你执行 npx zx index.mjs 时,默认的执行路径会是 index.mjs 文件的路径。而在文件中使用的每一个 bash,都会是以子进程的形式来执行的。所以,当使用 bash 来进行路径变更时,变更的是对应的子进程的路径,主进程的路径是不变的。

await $`pwd`; // 文件路径
await $`cd ../../`;
await $`pwd`; // 仍然是文件路径

  函数cd则可以改变主进程的路径。

await $`pwd`; // 文件路径
cd('../');
await $`pwd`; // ../文件路径

question

  question 函数是 readline 的包装,可以停住程序,等待用户输入敲击回车才完成。

let mustBeYse = await question('Do you like me ?');
// Do you like me ?

  question 还接受第二个参数,可以支持用户在输入的时候,使用 tab 键来补全。

// @type { choices: string[] }
let mustBeYse = await question('Do you like me ?', { choices: ['yes'] });

服务进程与子进程

  zx最棒的地方就在于他可以方便的开多个子进程。下面我愉快的起三个服务,三个服务的输出都会转到主进程。

$`npx serve`;
$`npx serve`;
$`npx serve`;
/*
输出
npx: 90 安装成功,用时 8.892 秒
npx: 90 安装成功,用时 8.957 秒
npx: 90 安装成功,用时 9.047 秒
INFO: Accepting connections at http://localhost:3000
INFO: Accepting connections at http://localhost:52857
INFO: Accepting connections at http://localhost:52858
*/

  此时如果我退出主进程,子进程的服务也会全部停止。

  在上文中,使用$`npx serve`的时候,没有加上 await。是因为类似于服务这种需要手动关闭的进程,如果使用 await 将永远执行不到下一步。

await $`npx serve`;
console.log(1); // 此时这一行永远不会执行到。

  如果你需要监听一个服务进程启动成功了,再去执行下一步的操作,可以使用for await 去监听日志信息。

let serve = $`npx serve -p 5000`;
for await (let chunk of serve.stdout) {// 启动服务成功if (chunk.includes('Accepting connections')) break;
}
// 拉取服务信息
await $`curl http://localhost:5000`;
serve.kill('SIGINT');

尝试实践

  了解了初步的使用方法之后,来实现一个小小的脚本。在实际的按迭代的开发工作中,每次开发之后都会清理掉所有的本地迭代分支。就写一个清空除了master 分支以外分支的脚本。

  首先需要获取到本地所有需要删除的分支名。首先需要执行 git branch 获取到所有的本地分支,获取的信息是一个字符串。
字符串中的分支名按照换行符隔开的,因此可以使用 split('\n') 把字符串按照分支分割成数组。

  接着需要摘除分支中的 master 分支和当前所在的分支。因为当前所在的分支是无法使用 git branch -d 来进行删除的。

const branchBash = await $`git branch`;
const localBranchNames =branchBash.stdout.split('\n').filter(branchName => !branchName.includes('master') && !branchName.includes('*')).map(filteredBranchName => filteredBranchName.trim());

  最后执行删除分支的命令,然后抓到无法删除分支的错误信息,提示给用户。

  try {const deleteBranch = await $`git branch -d ${localBranchNames}`;} catch(p) {let remainBranch = p.stderr.match(/\'(\S+?)\'/gi);console.log(`remain local branches: ${remainBranch}`);}

  最后使用 zx 执行文件,就可以敲击下回车清空所有的本地分支了。

深入一下

  zx 的源码非常的少,两个代码相关的文件加起来总才共五百多行,这还包括了 markdownxml 的解析功能。如果去掉诸如执行命令解析错误匹配以及一些格式化输入的代码,整个 bash 核心功能相关的代码才100行左右。

  zxbash 相关的功能本质上是在 node 的 child_process 模块上一个封装。正如库本身声明的那样, 它是一个为了更好编写脚本的工具(A tool for writing better scripts)。因此深入源码前,可以带上这三个有趣的问题:

  1. 为什么 zx 要使用 `` 的方式去执行$函数。
  2. $`command`是如何解析 command,处理引号的问题的。
  3. 为什么一个核心功能仅为包装了一下 node 功能的库,能够成为2021年最受欢迎的前端库。

  zx 库 代码文件有 zx.mjsindex.mjs。其中 zx.mjs 是用来解析用 zx 执行命令行文件的。这个文件的代码可以跳过,只看 index.mjs 功能的部分。以下为了文章内容的连贯性和阅读的方便性,会省略部分不重要的解析处理代码,具体代码以源码为准。

我们首先来看核心的$函数是怎么出入输入的 command 的:

function $(pieces, ...args) {let cmd = pieces[0], i = 0while (i < args.length) {let s// 转换数组类型的入参if (Array.isArray(args[i])) {s = args[i].map(x => $.quote(substitute(x))).join(' ')} else {s = $.quote(substitute(args[i]))}cmd += s + pieces[++i]}
}

  先来回顾一下$函数的使用方法,$`command`。这里使用的是带标签的模板字符串语法。这种方式可以快速的提取在``中插入的变量,也这样就能方便的实现前文提到过的自动转换数组自动加上引号的功能。

接下来看下解析转换的 substitute$.quote 函数。

function substitute(arg) {if (arg instanceof ProcessOutput) {return arg.stdout.replace(/\n$/, '')}return `${arg}`
}

  函数 substitute 很简单,就是如果输入变量为一个 ProcessOutput 类,会解析成类的 stdout 输出返回。

function quote(arg) {if (/^[a-z0-9/_.-]+$/i.test(arg) || arg === '') {return arg}// 如果其中包含空格会走这里return `$'`+ arg.replace(/\\/g, '\\\\').replace(/'/g, '\\\'').replace(/\f/g, '\\f').replace(/\n/g, '\\n').replace(/\r/g, '\\r').replace(/\t/g, '\\t').replace(/\v/g, '\\v').replace(/\0/g, '\\0')+ `'`
}

  quote 部分也很简单,如果输入变量仅包含 /^[a-z0-9/_.-]+$/i 或者为 空字符串,就直接返回。如果有其它内容,则会加上 $’’,把这个输入作为一个字符串返回。这也就是前文提到的过的现象的原因:

let name = '测试 测试';
await $`mkdir ${name}`;
// 输出:$ mkdir $'测试 测试' 成功let demo = 'git log';
await $`${demo}`;
// 报错

  这么设计的原因在我和库提的一个issue中有讨论过原因,主音是为了安全(Main idea for $ to be safe)。至此,三个问题中的两个已经解决了,我们接下来看完$函数中的剩余内容。

let resolve, reject
let promise = new ProcessPromise((...args) => [resolve, reject] = args)
promise._run = () => { /* 暂时省略中间内容 */ }
setTimeout(promise._run, 0) // 确保所有的协程已经开始
return promise

  剩下的就是生成了一个 ProcessPromise 类的实例,取出其中的 resolve, reject ,声明了一个_run 函数,然后把实例返回了。总的$函数的逻辑就是这么简单。我们来过一下 ProcessPromise 类。

class ProcessPromise extends Promise {get stdin() {this._run()return this.child.stdin}get stdout() {this._run()return this.child.stdout}get stderr() {this._run()return this.child.stderr}get exitCode() {return this.then(p => p.exitCode).catch(p => p.exitCode)}then(onfulfilled, onrejected) {if (this._run) this._run()return super.then(onfulfilled, onrejected)}pipe() {}kill() {}
}

  类 ProcessPromise 继承了 Promise,重写了它的 then 方法,去执行 _run。然后声明了文档提供的属性和方法。本质上就是一个取值需要的工具类。现在我们回到 _run 函数。

import {spawn} from 'child_process'let child = spawn(prefix + cmd, {cwd: process.cwd(),shell: typeof shell === 'string' ? shell : true,stdio: [promise._inheritStdin ? 'inherit' : 'pipe', 'pipe', 'pipe'],windowsHide: true,
})child.on('exit', code => {child.on('close', () => {/* 省略错误处理 */})
})let onStderr = data => {if (verbose) process.stderr.write(data)stderr += datacombined += data
}
child.stderr.on('data', onStderr)

  可以看到 _run 函数中核心的功能就在这里。每次使用$函数去执行一个 bash 本质上是使用 spawn 分出去一个以当前主线程路径的子线程。去监听它的日志错误退出

写在最后

  文章的最后,来聊聊前文提出的三个问题中的最后一个问题。我写代码到现在为止一直在思考一件事情,想要做一个成功的程序员,到哪一点才是最重要的。我一直有一个观点,程序中蕴含的哲学思想,是远远要比现在大多数程序员在努力追求的诸如算法底层炫酷的代码是要重要的多的。

  比如操作系统是一个伟大的创作,但是如果学习和明白了操作系统中核心的设计理念,用任何语言,一般水平的程序员也能写出简易的操作系统中的核心功能。但是操作系统中,线程池文件任务调度这些高度抽象的哲学设计和它们成功解决的问题,这应该才是操作系统最伟大的地方。

  就像 zx 库一样,它的核心功能代码几乎不到100行,而且基本上是对 node 功能的一个封装,但是却能成为2021最受欢迎的前端库,不正是因为它巧妙的设计和解决的问题吗。

聊聊去年最火的前端库zx相关推荐

  1. 白嫖我常用的 11 个超火的前端必备在线工具,终于有时间上班摸鱼了

    大家好,我是你们的 猫哥,一个不喜欢吃鱼.又不喜欢喵 的超级猫 ~ 前言 猫哥是一个常年混迹在 GitHub 上的猫星人,所以发现了不少好的前端开源项目,在此分享给大家. 公众号:前端GitHub,专 ...

  2. 目前国内可用的前端库及Zdir自建前端库

    xiaoz一直在用BootCDN提供的前端库,但就在前几天发现网站打开非常缓慢,F12分析发现原来是BootCDN的锅,目前BootCDN的全部地址被重定向到CloudFlare,这次怕是凉凉了.收集 ...

  3. scum官方服务器维护时间,在等公测的这段时间里聊聊最近很火的scum吧

    原标题:在等公测的这段时间里聊聊最近很火的scum吧 没有明日之后的日子实在是难熬,没有办法,楼主只好寻觅一下新的游戏寄托. 听说最近STEAM上有一款名叫<SCUM>的游戏非常火爆,出现 ...

  4. React前端库,table中的每列内容,进行强制换行的解决方法

    1.问题描述: 管理系统的前端实现用的是React+ant design,但是React的table,每列的值,如果用<br />或者/r/n 来实现,则还是换行无效.如下图,SN值是多个 ...

  5. 理解前端框架、前端库,两者有什么区别

    一.前端框架(Framework).前端库(Library)两者的概念和区别是什么 前端框架和库都是前端开发中常见的概念,它们的区别在于它们所解决的问题不同. 1.1.前端框架(Framework)是 ...

  6. 火盈云库分享创始人陌路老师的采访和软件介绍

    互联网是一个神奇的行业,大门不出二门不迈,一根网线一台电脑就可以赚钱.它给我们创造了前所未有的商业机会,让成千上万有梦想,敢想敢干的人通互联网获得了巨大的成功!正因为如此,无数新人涌入这个行业,希望在 ...

  7. 火盈云库陌路老师接受采访探讨软件的发展前程和过程

    互联网是一个神奇的行业,大门不出二门不迈,一根网线一台电脑就可以赚钱.它给我们创造了前所未有的商业机会,让成千上万有梦想,敢想敢干的人通互联网获得了巨大的成功!正因为如此,无数新人涌入这个行业,希望在 ...

  8. 【HTML③】JavaScript基础、使用JavaScript制作网页特效、使用前端库及UI框架快速开发

    [HTML③]JavaScript基础.使用JavaScript制作网页特效.使用前端库及UI框架快速开发 八.JavaScript基础 1.JavaScript JavaScript简称JS是用于制 ...

  9. 技术实践 | 聊聊网易云信的信令网络库实践

    导读:信令作为实时音视频技术架构中的重要一环,是对建立实时音视频通信起到关键桥梁性的作用. 文|丁永锋 网易云信资深客户端开发工程师 本文将从信令的概念着手,分享在网易云信新一代音视频技术架构下,信令 ...

最新文章

  1. 关于JS面向对象、设计模式、以及继承的问题总结
  2. 1.11 双向循环神经网络-深度学习第五课《序列模型》-Stanford吴恩达教授
  3. spark sql定义RDD、DataFrame与DataSet
  4. Oreo易支付程序开源源码分享发行版V1.3
  5. python 太灵活_Python中的灵活参数
  6. 图论法求解经典面试题:NxN匹马,N个赛道,求最快前M匹马,至少需要几次比赛?
  7. 小项目正确的操作姿势,碾压工薪阶层
  8. LOTO 示波器 两种电流探头的对比实测
  9. google浏览器缓存文件在哪里
  10. 各大邮箱网址用哪个好?企业内部邮箱哪个比较好用?
  11. 职业人需要直面的7个问题(转载)
  12. opencv中加入contrib模块
  13. SSH(Struts,Spring,Hibernate )和SSM(SpringMVC,Spring,MyBatis )的区别,抽丝剥茧的给你讲清楚
  14. 7-7 The Whimper of Universe (30 分)
  15. 郎平卸任后首次接受采访,说出执教8年的最大感受
  16. 数据分析入门宝藏!《Python数据分析-从入门到实践》
  17. 清除Trojan.PSW.WoWar.qq等木马
  18. CSS 实现内阴影的方法
  19. 2023年“华数杯”国际数学建模比赛ICM-问题B:社会稳定预警研究背景-多思路详解(含代码/数据)
  20. 分析了 6000 款 App,竟有这么多佳软神器没用过

热门文章

  1. Tkinter 布局管理器(一):pack
  2. 致远a8-v5-6.0协同管理软件_高危漏洞利用预警:近期利用“致远OA任意文件写入漏洞”的攻击较多...
  3. Python编程错误:参数错误([_ctypes.COMError: (-2147024809, ‘参数错误。‘, (None, None, None, 0, ...
  4. Android中动态生成ListView及SimpleAdapter的使用
  5. Zookeeper安装配置(单机)
  6. Qt之简约按钮导航栏
  7. SQL SERVER 的垂死挣扎
  8. 将sentinel.jar做成windows服务
  9. C# 汉字转拼音首字母大写
  10. 欧盟对谷歌开出第三张天价罚单 累计罚款总额达93.7亿美元