不知不觉都 2021 年了,Node.js 的 LTS 已经到了 16.x, 这期间由于 Node.js 发展过程中基础类库的不完善,出现了各种生生不息的类库套娃封装,npm 包的数量扶摇直上,已经突破 170 万,断层式第一。
最近在响应 sindresorhus 大神的号召,陆续把一些类库升级为 ESM,期间重新审视 Egg 团队曾经沉淀下来的各种基础类库,也许需要说再见了。
使用场景:JavaScript 的类型判断一向被诟病,我们只能面对现实。
参赛选手:
我们封装了个 is-type-of,集成了大部分类型判断的场景。
https://www.npmjs.com/package/is-type-of
官方在 10.x 后提供了 util.types。
https://nodejs.org/api/util.html#util_util_types
技能演示:
const is = require('is-type-of');
is.regexp(/.*/);
is.asyncFunction(async function foo() {});
is.string(str);
VS
const types = require('util/types'); // 10.x 用 require('util').types
types.isRegExp(/.*/);
types.isAsyncFunction(async function foo() {});
// 一些基础的类型没有支持
typeof str === 'string';
替换指数:★★★★☆ 顺手为之
评委点评:
util.types
的判断逻辑不少,但类似判断 String 等方法需要用 typeof,记忆起来有点麻烦。
倾向于继续用 is-type-of
,推动它的底层大部分逻辑简化为 util,相当于简单的包了一层。
官方的 util
里面很多好东西,可以挖掘下。
使用场景:等待一段时间,类似其他语言的 sleep 函数。
参赛选手:
社区的 ko-sleep ,我们也在 mz-modules 里面简单封装了下。
https://www.npmjs.com/package/ko-sleep
https://www.npmjs.com/package/mz-modules
官方 16.x 后提供的 timers/promises 。
https://nodejs.org/api/timers.html#timers_timers_promises_api
技能演示:
const { sleep } = require('mz-modules);
await sleep('1s');
VS
const { setTimeout } = require('timers/promises');
await setTimeout(1000);
// 旧版本可以自己 promisify
await new Promise(resolve => {
setTimeout(resolve, 1000);
});
替换指数:★★★★★ 更待何时
评委点评:
sleep('10m')
。使用场景:日常的文件处理,如判断是否存在,写入文件,创建及删除目录等等。
虽然 Node.js 从第一个版本开始就有了 fs 模块,但老实说,真的一言难尽。
举个例子,创建和删除目录,这是一个非常典型的场景,谁不喜欢 mkdirp -p
和 rm -rf
呢?
但当年文件 API 非常不好用,跨平台兼容性也不咋滴。相信大家都遇到过在 Windows 下时删除文件夹时,经常会遇到:文件正在使用中 or 请先清空子目录文件
。在那个 npm 依赖还是马里亚纳海沟的年代,深一点的依赖连文件管理器都没法删除,因此经常需要祭出 rimraf 这些利器。
参赛选手:
我们封装了 mz 和 mz-modules 这 2 个模块,前者主要提供了 fs 等价的 Promise 版 API ,后者对 mkdirp 和 rimraf 进行了 Promise 封装。
https://www.npmjs.com/package/mz
https://www.npmjs.com/package/mz-modules
https://github.com/substack/node-mkdirp
官方在 10.x 后开始重视这块,14.x 后就好用了不少,派出了 fsPromise 这位代表。
https://nodejs.org/api/fs.html#fs_promises_api
技能演示:
// 常规文件操作
const { fs } = require('mz');
await fs.exists('/path/to/file');
await fs.readFile('/path/to/file');
await fs.writeFile('/path/to/file', 'some text');
// 目录操作
const { mkdirp, rimraf } = require('mz-modules');
await rimraf('/path/to/dir'); // 递归删除文件在 Windows 真的曾经很难很难。
await mkdirp('/path/to/dir'); // 早期的 Node.js 不支持递归创建和忽略已创建,即人民群众盼望着的 `mkdir -p`
VS
const fsPromise = require('fs/promises');
// 常规文件操作
await fsPromise.access('/path/to/file').then(() => true, () => false); // 这个比较恶心,exists 被 deprecate 了,只能判断 access,不存在会抛错。
await fsPromise.readFile('/path/to/file');
await fsPromise.writeFile('/path/to/file', 'some text');
// 目录操作
await fsPromise.rm('/path/to/dir', { force: true, recursive: true, maxRetries: 5 });
await fsPromise.mkdir('/path/to/dir', { recursive: true });
替换指数:★★★★★ 更待何时
评委点评:
虽迟但到,立刻马上现在就使用官方方案,Promise 你值得拥有。
绝大部分情况下,如果你不应该用 fs.readFileSync
这些同步方法,否则考虑剁手吧。
由于 rimraf 这名字大家经常记不住,老外还曾在 StackOverflow 讨论考古这是啥缩写,可惜没个结论。
使用场景:Stream 是 Node.js 新手最容易出问题的地方,尤其经常用到的 pipe:
如下这段代码,读取一个 zip 文件,然后解压,再写入文件,最后来个错误处理,很完美了是不?
await new Promise((resolve, reject) => {
fs.createReadStream('/path/to/src.zip')
.pipe(zlib.createGunzip())
.pipe(fs.createWriteStream('/path/to/target.js'))
.on('error', reject)
.on('finish', resolve);
});
然而,如果你实际使用的时候,一旦源文件不存在 or 不是 zip 格式等等情况下,进程立即 crash 掉。
是的,国内绝大部分的教程,都不会告诉你 pipe 时还需对每一个 stream 进行错误处理,丑到爆了,有没有!而且即使是老手,也经常容易踩坑。
await new Promise((resolve, reject) => {
fs.createReadStream('/path/to/src.zip')
.on('error', reject) // 如果需要不同阶段打印不同的错误信息,会更丑。
.pipe(zlib.createGunzip())
.on('error', reject)
.pipe(fs.createWriteStream('/path/to/target.js'))
.on('error', reject)
.on('finish', () => resolve());
});
参赛选手:
脑子不够,类库来凑:社区有 pump 这个库,不过由于 callback 风格的,所以我们封了个 mz-modules/pump 。
https://github.com/mafintosh/pump
https://www.npmjs.com/package/mz-modules
官方在 10.x 后推出的 stream.pipeline,注意不是 stream.pipe 。(贡献者就是 pump 作者)
https://nodejs.org/api/stream.html#stream_stream_pipeline_source_transforms_destination_callback
https://nodejs.org/api/stream.html#stream_readable_pipe_destination_options
技能演示:
const { pump } = require('mz-modules');
const fs = require('fs');
await pump(
fs.createReadStream('/path/to/src.zip'),
zlib.createGunzip(),
fs.createWriteStream('/path/to/target.js'),
);
VS
// 16.x 才支持,10.x 需要自己 promisify 下。
const { pipeline } = require('stream/promises');
const fs = require('fs/promises');
await pipeline(
fs.createReadStream('/path/to/src.zip'),
zlib.createGunzip(),
fs.createWriteStream('/path/to/target.js'),
);
替换指数:★★★★★ 更待何时
评委点评:
stream.pipe()
别再用了,这是一个倾向于让用户链式调用,但要么容易遗漏错误处理,要么写起来很别扭的 API。使用场景:发起一个 HTTP 请求,这是非常核心的能力之一。
可惜,官方的 http
库太底层太基础了,用起来往往需要大量的封装。譬如 302 后自动跳转、文件上传、响应结果解析等等。
曾经广受社区欢迎的 request 库去年宣布停止维护后,也引起了社区比较大的混乱,虽然提供了替代品建议。
参赛选手:
我们封装的 urllib,并在 Egg 里面内置,是和后端通讯的核心类库,稳定支撑了多年的双十一。
https://www.npmjs.com/package/urllib
官方刚推出的 undici (但没有内置到 Node.js 中)。
https://nodejs.medium.com/introducing-undici-4-1e321243e007
PS:对应配套的 Mock 库,如 nock 和 sinon 。
https://github.com/nock/nock
https://sinonjs.org/
技能演示:
const httpclient = require('urllib');
const result = await httpclient.request(url, {
method: 'post',
contentType: 'json', // 请求参数处理
dataType: 'json', // 响应结果处理
data: {
hello: 'world',
now: Date.now(),
},
});
console.log(result.status);
console.log(result.headers);
console.log(result.data); // 打印响应的 JSON
VS
const { request } = require('undici');
const result = await request(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
hello: 'world',
now: Date.now(),
}),
});
console.log(result.statusCode);
console.log(result.headers);
console.log(await result.body.json()); // 打印响应的 JSON,需要手动转换下
替换指数:★★★☆☆ 观望,未来可期
评委点评:
urllib
经历了多年的考验,非常的稳定,但内部代码有点旧了,未来底层可以切换掉,对用户无感。
undici
是官方出品,体积很小,API 设计也很现代化。
但 undici
还不够成熟,文档比较简陋,本想顺便示例下 Mock 的,结果看了半天文档和翻了源码,还是没看懂。
建议观望,小范围尝试。
使用场景:Assert 断言是一个常用的功能。
参赛选手:
power-assert 、chai(涵盖了 should 和 expect 这 2 个库)
https://github.com/power-assert-js/power-assert
https://www.chaijs.com/
官方 10.x 后推出了 assert 的 strict 增强模式,还提供了 CallTracker 来对函数调用进行计数。
https://nodejs.org/api/assert.html#assert_strict_assertion_mode
https://nodejs.org/api/assert.html#assert_class_assert_calltracker
技能演示:
主办方弃权,代码比较简单,但对比写出来挺麻烦的。
我们这边一直都用 power-assert
,参考阅读:《No API is the best API》。
最近试了下 assert/strict
感觉还可以,但某些场景还是无法覆盖,如 assert(arr[1] === 10)
这种取值后的。
值得关注的是 assert.rejects
、assert.match
等 API,还有个 CallTracker
是新出来的,有点类似 sinon 等 stub 库里面,验证某个函数有没有被调用。
替换指数:★★☆☆☆ 保持关注
评委点评:
萝卜青菜各有所爱。
非单测场景,如果用 assert 的场景,可以考虑改为 const assert = require('assert/strict');
单测场景,用到 power-assert 的情况下,保持观望,或者考虑下 jest 啥的内置断言能力。
使用场景:单元测试很重要,我们也会通过 测试覆盖率 来看单测的覆盖程度。
参赛选手:
社区目前的主流方案是通过 nyc 来对代码转译,通过构建期打桩的方式来采集代码执行情况,从而计算出覆盖率。
https://github.com/istanbuljs/nyc
V8 支持了 JS 代码覆盖率采集能力,并被 Node.js 10.x 开始集成,建议用 16.x。需使用 c8 来导出对应的报表。
https://v8.dev/blog/javascript-code-coverage
https://github.com/bcoe/c8
$ nyc mocha
$ open coverage/lcov-report/index.html
VS
$ c8 mocha
$ open coverage/index.html
后者还可以用来支持运行期的覆盖率采集:
# 代码里面定时触发覆盖率导出
const v8 = require('v8');
v8.takeCoverage();
# 启动应用,使用环境变量来通知 V8 启动采集
$ NODE_V8_COVERAGE=./coverage/tmp npm run dev
# 调用接口
$ curl http://localhost:7001
# 生成报告
$ c8 report -r html --all
之前在内网写过一篇:《代码覆盖率的运行期采集 - 论如何有理有据地怼测试同学验证不充分》回头有空放出来。
替换指数:★★★★☆ 推荐使用
评委点评:
nyc 会需要转译代码,导致执行速度变慢,而且有可能会影响到 TS 等 sourcemap 映射,导致排查问题麻烦。
官方的方案是使用了 V8 内置的代码覆盖率采集能力,因此推荐使用。
官方的方案,在进程退出等场景下不一定采集的到,有一些边缘的 case 暂时还比不过 nyc,不过问题不大。
注意:覆盖率采集 和 覆盖率报告生成 是 2 个阶段,后者是 c8 做的,不过 istanbuljs、nyc、c8 的作者都是同一人。
使用场景:调试日志的打印,通过环境变量来开启。
参赛选手:
官方内置 util.debuglog 的实现。
https://nodejs.org/api/util.html#util_util_debuglog_section_callback
debug 是早期大神 visionmedia 出品的,参考了 Node.js 的实现,增加了对浏览器的支持。
https://www.npmjs.com/package/debug
技能演示:
// https://www.npmjs.com/package/debug
const debug = require('debug')('egg-bin:test');
debug('launch application at %s', host);
// 通过环境变量 DEBUG 激活,支持通配。
$ DEBUG=egg-core,egg-bin:* node index.js
VS
// https://nodejs.org/api/util.html#util_util_debuglog_section_callback
const util = require('util');
const debug = util.debuglog('egg-bin:test');
debug('launch application at %s', host);
// 通过环境变量 NODE_DEBUG 激活,支持通配。
$ NODE_DEBUG=egg-core,egg-bin:* node index.js
替换指数:★★★★☆ 推荐使用
评委点评:
用法几乎没变化,触发的环境变量名修改下即可,同样的支持通配规则。
debug 其实是参考官方 Node.js 的实现,反而大受欢迎,被非常多的 Node.js 基础类库所使用。
如果你有用一些 Logger 库的话,logger.verbose()
其实也能覆盖这个场景。
使用场景:对某个过时的 API 保持兼容,同时打印 WARNING 信息提示用户升级。
参赛选手:deprecate vs util.deprecate
技能演示:
const deprecate = require('deprecate');
const fn = (...args) => {
deprecate('`app.get` is deprecated, please use `app.router.get` instead.');
return originFn(...args);
};
VS
const util = require('util');
const fn = util.deprecate(originFn, '`app.get` is deprecated, please use `app.router.get` instead.', 'DEP0001');
替换指数:★★★★★ 更待何时
评委点评:
建议直接用内置的 util 方法,自动代理了原函数,用起来更简单。
支持 --no-deprecation
和 --throw-deprecation
等参数来增强控制。
第三个参数定义错误码,相同的 CODE 只会打印一次 WARNING。
使用场景:经过 TS、Webpack 编译后的代码,执行时的错误堆栈,往往需要通过 sourcemap 还原为源文件对应的坐标,才能方便的定位问题。
参赛选手:
社区模块 source-map-support 。
https://www.npmjs.com/package/source-map-support
官方在 12.x 后支持的命令行参数 --enable-source-maps 。
https://nodejs.org/api/module.html#module_source_map_v3_support
技能演示:
初始化代码如下:
interface User {
name: string;
}
function sayHi(user?: User) {
if (!user) throw new Error('user is required');
console.log(`Hello ${user.name}`);
}
sayHi();
分别执行对应的命令:
# 编译测试代码,内嵌 sourcemap 方式
$ tsc --inlineSourceMap test.ts
# 执行运行,可以观察到报错行数是编译后的位置,而不是 TS 源码的位置。
$ node test.js
Error: user is required
at sayHi (./test.js:3:15)
# 引入 source-map-support 包进行解析
$ node -r source-map-support/register test.js
Error: user is required
at sayHi (./test.ts:6:20)
# Node.js 内置命令行参数
$ node --enable-source-maps test.js
Error: user is required
at sayHi (./test.ts:6:20)
替换指数:★★★★★ 更待何时
评委点评:
使用场景:fork 一个子进程也是常见的操作,但 child_process 太底层了,需要开发者自行处理跨平台问题,还需要自行处理执行输出。
参赛选手:
我们封装的 runscript 模块。
https://github.com/node-modules/runscript
sindresorhus 大神写的 execa 模块。
https://github.com/sindresorhus/execa
技能演示:
const runscript = require('runscript');
const { stdout, stderr } = await runscript('node index.js', { stdio: 'pipe' });
console.log(stdout);
console.error(stderr);
VS
const execa = require('execa');
const proc = execa.node('index.js', opts);
console.log(proc.pid);
// proc.kill();
// proc.cancel();
const { stdout, stderr, isCanceled, killed, exitCode } = await proc;
console.log(stdout);
console.error(stderr);
替换指数:★★★★★ 更待何时
评委点评:
两者对 child_process
包装了一层,很好的支持了跨平台的兼容性。
execa 的功能更全面一点,支持 fork,以及 kill 等操作。
遗憾是,官方选手目前没有计划优化和迎战。
使用场景:Node.js 的最初以及最大的使用场景,就是写命令行工具,因此它对应的测试很重要。
参赛选手:
我们封装的 coffee 模块,现在 Egg 体系的 CLI 测试全部基于它。
https://github.com/node-modules/coffee
我个人最新封装的 clet,试图解决 coffee 存在的一些问题,也是这篇文章的诱因之一。
https://github.com/node-modules/clet
技能演示:
const coffee = require('coffee');
describe('cli', () => {
it('should fork node cli', async () => {
return coffee.fork('/path/to/file.js')
.expect('stdout', '12\n')
.expect('stderr', /34/)
.expect('code', 0)
.end();
});
});
VS
import { runner, KEYS } from 'clet';
it('should works with boilerplate', async () => {
await runner()
.cwd(tmpDir, { init: true })
.spawn('npm init')
.stdin(/name:/, 'example') // wait for stdout, then respond
.stdin(/version:/, new Array(9).fill(KEYS.ENTER))
.stdout(/"name": "example"/) // validate stdout
.notStderr(/npm ERR/)
.file('package.json', { name: 'example', version: '1.0.0' }) // validate file
});
替换指数:★★★★☆ 推荐使用
评委点评:
熟悉 Egg 源码的同学,会对 coffee 非常眼熟,我们之前的 CLI 单测都是基于它的。
但 coffee 对 prompt 以及 web server 等长期运行的应用的测试,不是很在行,之前提供的都是一些比较黑的方式。
clet 是我最新实现的一版,有很多不错的特性,欢迎大家尝鲜。(预计最近会发 1.0 版本)
简单说,我们曾经维护的一些轮子,如 mz、mz-modules 等库,终于可以解甲归田了,大胆的说再见吧。
从总的趋势上来看,Node.js 官方在不断的听取和吸收社区的反馈,尤其是在 Promise 相关部分,对很多基础类库都进行了翻新。
过往我们遇到兼容性问题时,第一时间想起的是封装一个类库来屏蔽差异,因为给 Node.js 提 PR 的时效性不高,但时代变了,我们应该多考虑下沉。
同时也不要太顾忌那些已经过了 LTS 的老旧 Node.js 版本了,都什么年代了,那些还用着 yield 的库,果断重构发大版本吧。
是时候翻新下我们的认知了:
积极参与到 Node.js 官方的 API 讨论中,这是我们可以做到的,也应该做到的。
新开坑时,如果是兼容类型的,三思下,可以先写了救急,但建议同步给 Node.js 提 PR,咱最好只维护三年。
如果是旧的历史库,考虑基于官方的新能力,进行翻新,如 is-type-of 这种。
应该以身作则推动老旧版本的 Node.js 下线,封装的库勇敢发大版本。
我们之前维护的 https://github.com/node-modules 有 172 个库,需要说再见的只有本文提到的这几个么?
除了上面 Node.js 官方引入的新能力外,ECMA 等底层也可以关注下,像最近我们压测时就发现 Collator#compare
比 String#localeCompare
快 100 倍。
本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/s2Z3_pppuDs93DtNKlgUww
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。