聊聊去年最火的前端库zx

2024-03-13 13:48

本文主要是介绍聊聊去年最火的前端库zx,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

  根据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的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/805077

相关文章

Vue3 的 shallowRef 和 shallowReactive:优化性能

大家对 Vue3 的 ref 和 reactive 都很熟悉,那么对 shallowRef 和 shallowReactive 是否了解呢? 在编程和数据结构中,“shallow”(浅层)通常指对数据结构的最外层进行操作,而不递归地处理其内部或嵌套的数据。这种处理方式关注的是数据结构的第一层属性或元素,而忽略更深层次的嵌套内容。 1. 浅层与深层的对比 1.1 浅层(Shallow) 定义

这15个Vue指令,让你的项目开发爽到爆

1. V-Hotkey 仓库地址: github.com/Dafrok/v-ho… Demo: 戳这里 https://dafrok.github.io/v-hotkey 安装: npm install --save v-hotkey 这个指令可以给组件绑定一个或多个快捷键。你想要通过按下 Escape 键后隐藏某个组件,按住 Control 和回车键再显示它吗?小菜一碟: <template

【 html+css 绚丽Loading 】000046 三才归元阵

前言:哈喽,大家好,今天给大家分享html+css 绚丽Loading!并提供具体代码帮助大家深入理解,彻底掌握!创作不易,如果能帮助到大家或者给大家一些灵感和启发,欢迎收藏+关注哦 💕 目录 📚一、效果📚二、信息💡1.简介:💡2.外观描述:💡3.使用方式:💡4.战斗方式:💡5.提升:💡6.传说: 📚三、源代码,上代码,可以直接复制使用🎥效果🗂️目录✍️

【前端学习】AntV G6-08 深入图形与图形分组、自定义节点、节点动画(下)

【课程链接】 AntV G6:深入图形与图形分组、自定义节点、节点动画(下)_哔哩哔哩_bilibili 本章十吾老师讲解了一个复杂的自定义节点中,应该怎样去计算和绘制图形,如何给一个图形制作不间断的动画,以及在鼠标事件之后产生动画。(有点难,需要好好理解) <!DOCTYPE html><html><head><meta charset="UTF-8"><title>06

计算机毕业设计 大学志愿填报系统 Java+SpringBoot+Vue 前后端分离 文档报告 代码讲解 安装调试

🍊作者:计算机编程-吉哥 🍊简介:专业从事JavaWeb程序开发,微信小程序开发,定制化项目、 源码、代码讲解、文档撰写、ppt制作。做自己喜欢的事,生活就是快乐的。 🍊心愿:点赞 👍 收藏 ⭐评论 📝 🍅 文末获取源码联系 👇🏻 精彩专栏推荐订阅 👇🏻 不然下次找不到哟~Java毕业设计项目~热门选题推荐《1000套》 目录 1.技术选型 2.开发工具 3.功能

Vue3项目开发——新闻发布管理系统(六)

文章目录 八、首页设计开发1、页面设计2、登录访问拦截实现3、用户基本信息显示①封装用户基本信息获取接口②用户基本信息存储③用户基本信息调用④用户基本信息动态渲染 4、退出功能实现①注册点击事件②添加退出功能③数据清理 5、代码下载 八、首页设计开发 登录成功后,系统就进入了首页。接下来,也就进行首页的开发了。 1、页面设计 系统页面主要分为三部分,左侧为系统的菜单栏,右侧

【VUE】跨域问题的概念,以及解决方法。

目录 1.跨域概念 2.解决方法 2.1 配置网络请求代理 2.2 使用@CrossOrigin 注解 2.3 通过配置文件实现跨域 2.4 添加 CorsWebFilter 来解决跨域问题 1.跨域概念 跨域问题是由于浏览器实施了同源策略,该策略要求请求的域名、协议和端口必须与提供资源的服务相同。如果不相同,则需要服务器显式地允许这种跨域请求。一般在springbo

HTML提交表单给python

python 代码 from flask import Flask, request, render_template, redirect, url_forapp = Flask(__name__)@app.route('/')def form():# 渲染表单页面return render_template('./index.html')@app.route('/submit_form',

Java 后端接口入参 - 联合前端VUE 使用AES完成入参出参加密解密

加密效果: 解密后的数据就是正常数据: 后端:使用的是spring-cloud框架,在gateway模块进行操作 <dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId><version>30.0-jre</version></dependency> 编写一个AES加密

vue2 组件通信

props + emits props:用于接收父组件传递给子组件的数据。可以定义期望从父组件接收的数据结构和类型。‘子组件不可更改该数据’emits:用于定义组件可以向父组件发出的事件。这允许父组件监听子组件的事件并作出响应。(比如数据更新) props检查属性 属性名类型描述默认值typeFunction指定 prop 应该是什么类型,如 String, Number, Boolean,