分类
nodejs

node.js 里使用 fifo

0. 需求

前两天 Showman 遇到一个需求:

  1. 我们需要在服务器端录制视频
  2. 录制视频的过程主要由 node.js 控制,借助 puppeteer 操作浏览器
  3. 但是也会需要执行一些 shell 命令,此时为安全考虑,我们会启动一个封闭的临时环境给用户执行
  4. 这些封闭环境是用户进程间共用的,不会随时启动随时销毁
  5. 所以 node.js 就需要在其它环境里执行一些操作,返回内容,等待执行完毕后再继续下面的

于是我的同事就让我用 fifo。

1. 什么是 fifo

我以前没有用过 fifo,所以搜索了一下。

FIFO 特殊文件(同具名管道)与管道类似,只是可以用访问文件系统的方式来访问它。它可以被多个进程同时打开和读写。当进程通过 FIFO 交换数据时,内核将直接在内部交换数据,而不会写入到文件系统中。因此,FIFO 特殊文件在文件系统中没有内容;文件系统的入口(即文件)只是作为引用方式,让各进程能够使用文件名来访问管道。

原文:https://man7.org/linux/man-pages/man7/fifo.7.html

管道大家应该都知道,把 A 进程的输出直接输入到 B 进程里,加快处理速度。fifo 与管道的差别就是 fifo 可以通过文件路径直接访问,用起来更简单。

2. 在命令行里使用 fifo

创建 fifo,使用 mkfifo 命令:

mkfifo xxx.fifo

写入内容到 fifo:

echo "something" > xxx.fifo

读取 fifo:

cat xxx.fifo

因为 fifo 是管道,内容直接走内核,所以实际上硬盘上不会存储任何内容。如果我们在写入之后再 cat fifo,就不会得到任何内容。

3. 在 node.js 里使用 fifo

在 node.js 里使用 fifo 需要用 fs.opennet.Socket。因为我需要在执行完毕后继续下一步,所以进行了 Promise 封装:

try {
  // 为避免执行时间过长导致进程超时,不断输出些内容
  const interval = setInterval(() => {
    log('termlang is processing...');
  }, 3E4);
  await new Promise((resolve, reject) => {
    // 打开名为 $basename-$lineno.sp.fifo 的管道
    open('./$basename-$lineno.sp.fifo', constants.O_RDONLY | constants.O_NONBLOCK, (err, fd) => {
      if (err) {
        clearInterval(interval);
        reject(err);
      }
      const pipe = new Socket({fd});
      pipe.on('data', data => {
        data = data.toString();
        // 以输出内容包括 finished 或 errored 为结束标记
        if (/(finished|errored)/.test(data)) {
          resolve(data);
        }
      });
    });
  });
  clearInterval(interval);
} catch (err) {
  log(err);
}

4. 总结

作为半路出家的前端,我对系统、对 Linux 一直缺乏了解。所以类似管道这种东西,我一直也不太熟悉,这次算学会了一个新技能,记录分享一下。

分类
js

聊聊 NPM 里的版本号和依赖

好像一直没有写过版本号和依赖相关的内容,偶尔会有同学问,所以写一篇总结一下。

0. Semver

我们目前使用的版本规范通常基于 Semver,语义化版本。官方网站:语义化版本 2.0.0 | Semantic Versioning (semver.org)。按照其规则,版本号的结构应该是:

主(大)版本号.次(小)版本号.修正(补丁版本)号

其中,

  • 主版本号一般包含架构和 API 的变化。如果 API 出现重大变化,使得依赖它的软件要重构,那么就要体现在大版本号里。不过现在的代码仓库很少有破坏式重构,API 一般能够在至少 2、3 个大版本里保持稳定。所以主版本号变化一般出现在大型重构时,仓库内部的代码架构和组织形式出现重大变化,或者基于不同系统,就需要升级主版本了。
  • 次版本号一般表达功能变化。架构没有变化,原有 API 也基本维持不变,只是新增了功能。这个时候就会上调次版本号。
  • 修订号一般表示修复 bug。

0.1 年代版本号

另一种流行的版本号规范是年代版本号,比如 Ubuntu,每年会发布两个版本,目前是 21.04,10月份会发布 21.10。偶数年的 .04 版本会最终成为 LTS,长期维护版本;奇数年的版本和 .10 版本则只维护半年,里面包含各种最新的软件组件,方便喜欢尝新的用户。

前端常见的软件和库,包括 node.js、Angular、Electron 也用这种方式确定版本号。

1. 依赖中指定版本号

我们在项目中可能会使用大量开源代码,这些开源代码通常都会使用包管理工具(比如 NPM,node package manager)安装和管理。

在 package.json 里,我们可以使用几个运算符告诉 NPM 我们希望怎么使用这些依赖:

  • 不写,repo: '1.0.0'。要求使用 1.0.0 版本的 repo,必须完全一致。
  • ~repo: ~1.0.0。要求大版本为 1,小版本为 0,修订版本不限制,比 0 高就可以。
  • ^repo: ^1.0.0。要求大版本为 1,小版本高于 0 就可以。

一般来说,使用 npm i repo 安装的依赖,默认规则是 ^;使用 npm i repo@version 安装的依赖,默认规则是写死。

2. 升级依赖

这个世界上不存在没有 Bug 的代码,也没有功能完善的代码。使用开源仓库,我们就要考虑升级依赖。一方面可以使用新功能,一方面可以解决 bug。

通常来说,直接使用 npm update 就能升级项目依赖,NPM 会按照(1)里设定的规则更新依赖。

3. 升级依赖的大版本

只使用 npm update 无法升级大版本。原因如上文所述,大版本可能包含破坏性的 API 更新,很容易导致 dev/build 失败,作为工具无法妥善处理,必须交给开发者手动完成。

很多同学因此不愿意升级大版本。但我建议大家还是要找机会做升级,尤其工具链,比如 webpack、babel。这些工具很多时候有时效性,长期不升级会导致各种问题。而且,升级工具链本身这也是我们偿还技术债务的好机会。

其实升级工具链的大版本并不复杂,大多数半天就搞定了;如果没有用到偏门功能,甚至可能直接升级就能跑。——通常来说,开源软件的作者面对使用频率高的功能,会比较保守;没人用的功能改动就会大刀阔斧一些。

升级依赖需要指定大版本,比如从 webpack 4 升级到 webpack 5,可以使用 npm i webpack@5,这样会安装大版本是 v5 的最新版本。这里有个小建议,公司的生产级别的项目,最好不要着急升级,等到 X.2 X.3 这样基本稳定的次版本发布后再升级,可以避免踩很多坑。

一般来说,开源仓库的官方也会提供迁移指南,比如 webpack v4 to v5。只要你有 v4 的配置经验,照着指南操作,大多数时候都能顺利完成。

4. 解决 npm audit 问题

开源仓库的安全问题日趋严重,GitHub 和 NPM 都会帮我们检查依赖,并且根据已知的安全问题列表发出警告。所以在安装依赖时,我们经常能看到类似下面这种警告信息:

found 5 moderate severity vulnerabilities
  run `npm audit fix` to fix them, or `npm audit` for details

这个时候,我们应该执行 npm audit 查看所有的审计结果,可能得到如下的报告:

┌───────────────┬──────────────────────────────────────────────────────────────┐
│ Moderate      │ Regular expression denial of service                         │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ Package       │ glob-parent                                                  │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ Patched in    │ >=5.1.2                                                      │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ Dependency of │ @vue/cli-service [dev]                                       │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ Path          │ @vue/cli-service > webpack-dev-server > chokidar >           │
│               │ glob-parent                                                  │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ More info     │ https://npmjs.com/advisories/1751                            │
└───────────────┴──────────────────────────────────────────────────────────────┘

这个报告说明有问题的包是 glob-parent,它由 webpack-dev-server 引入,又因为 @vue/cli-service 而最终成为项目的依赖。很明显,它是 @vue/cli 的组成部分,是前端工具链的一环。那么通常来说,它的危害层级很低,多半不会影响到项目整体安全性。

如果是 node.js 项目,就要小心了,服务器和后端都是安全重灾区,后果可能会很严重。这个时候,你可以往上翻,找到这些报告的最前面,如果可以修复,NPM 会告诉你应该运行什么命令更新问题依赖。如果不能修复,就要自己想办法了。

比如,你可以移除出问题的依赖,不过多半不可行。或者,我比较常用的方案是,在 GitHub 上找到依赖仓库,自己 fork 一份,然后升级其中的依赖,然后发布一个我个人的版本,接下来就用我自己的版本。等到官方修复后,再用回官方版本。

5. 总结

最后按惯例总结一下。依赖是我们软件产品的重要组成部分,它们的版本事关重大,必须给予足够的关注。至少,每个月要检查一次依赖的安全审计问题,如有需要,就升级依赖。

我们自己写代码的时候,也要遵守 Semver 的规定,适时上调版本号,让版本号能表示代码的发展情况。

分类
js

用 express.js 实现流式输出 HTTP 响应

0. 前言

我们先来总结一下客户端与服务器端的数据交互方式:

  1. ajax,即 XMLHttpRequest,最常见的数据交互方式,适用于较轻量级的一次性请求,比如用户登录、CRUD 等。
  2. SSE,服务器端事件,适用于有大量服务器端推送、且不需要从客户端发送数据的需求,比如盯股票行情
  3. WebSocket,适用于上下行数据都很多、都很频繁的情况,比如在线聊天、网络游戏等

单从技术角度来看,大概是这几个。根据需求变化,我也可以组合出更多方案。比如,类似 Showman 生成视频这种需求,耗时很长,对服务器资源占用很高,就比较适合:

  1. 发起请求,产生一条任务记录,进入队列
  2. 服务器上运行独立的进程,从队列中取任务出来执行,并记录日志
  3. 客户端不断检查任务进度

1. 需求

今天的需求,介于轻量一次性,与需要服务器长时间执行之间,即 FastTest 中的发布功能。这里,我们有以下需求:

  1. 管理员点击 Publish 按钮,开始发布静态网站
  2. 服务器需要先保存数据,然后调用 webpack 发布所有语言版本
  3. 只有少数管理员会使用此功能,服务器压力不大
  4. 管理员希望能了解发布进度,及每一步的状态

所以,提交任务后等待服务器慢慢跑就不合适;等待请求完成一次性看到所有结果也不合适;最合适的,就是基于长链接,不断获取返回信息,并在前端实时看到进度。也即是:流式输出 HTTP 响应。

2. 实现

2.1 后端

后端实现我选择 node.js + express.js,在后端领域,这两个我比较熟悉。express.js 是在 node.js 网络模块上进行封装得来,使用起来很简单,也支持原生 node.js 方法。

首先我们创建一个服务器:

const express = require('express');
const app = express();
const port = 3100;

app.listen(port, () => {
  console.log('FastTest Admin API at: ', port);
});

然后,我们在需要流式返回响应的接口里设置相应头 Content-type: application/octet-stream。接下来,只要我们不断向输出流写入内容就可以了。哦,对了,结束的时候,我们还要关闭输出流。

app.post('/data', async(req, res, next) => {
  res.setHeader('Content-type', 'application/octet-stream');
  
  // 第一次返回
  res.write('Local data saved. Start to build dist。files.\n');

  // 数次返回
  for (const item of items) {
    await doSomething(item);
    res.write(`${item} done successfully.\n`);
  }

  // 最后,全部完成
  res.write('All done.');
  // 关闭输出流
  res.end();
});

2.2 axios

axios 是很流行的 ajax 库,它进行了 Promise 封装,用起来很方便。这里我们要用 onDownloadProgress(即 axios 对 XMLHttpRequest.progress 的封装)获取下载进度,它会在每次服务器返回响应字符串的时候更新,我们只需要截取上次响应之后,这次响应新增的内容,即可。

function publish(data, onDownloadProgress) {
  return axios.post('/data', data, {
    onDownloadProgress,
  });
}

async function doPublish() {
  if (isLoading.value) {
    return;
  }

  isPublishing.value = true;
  message.value = status.value = null;

  try {
    const { cases, lang } = store.state;
    let offset = 0;
    await publish({ cases, lang }, ({ target: xhr }) => {
      // responseText 包含了从一开始到此刻的全部响应内容,所以我们需要从上次结束的位置截取,获得新增的内容
      const { responseText } = xhr;
      const chunk = responseText.substring(offset);
      // 记录这一次的结束位置
      offset = responseText.length;
      currentStatus.value = chunk;
    });
    status.value = true;
    currentStatus.value = '';
    message.value = 'Published successfully.';
  } catch (e) {
    message.value = 'Failed to publish. ' + e.message;
  }
  isPublishing.value = false;
}

2.3 Vue3

相对来说,Vue3 的部分最容易。这里我用了 animate.css 的 flash 效果,让信息更显眼。除此之外,就是简单的赋值。

<template lang="pug">
.alert.alert-info.mb-0.me-2.py-1.px-3.animated.flash.infinite.slower(
  v-if="currentStatus",
) {{currentStatus}}
</template>

3. 效果演示

流式输出效果演示

4. 部署

一般来说,我们很少会直接用 node.js 当服务器,多半会启动 node.js 服务,然后用 nginx 反向代理给用户访问。这里需要注意,nginx 默认会将响应内容存入缓冲区,然后批量返回给客户端。这会导致流式输出无效,变成常规的执行完毕后一次性输出。

所以我们必须修改 nginx 的配置:

# 仅展示有关配置
location /data {
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $http_host;
        proxy_pass http://localhost:3100;
        # 关闭代理缓冲区,所有响应实时输出
        proxy_buffering off;
}

5. 总结 & 扩展阅读

前些天我帮朋友做了个小项目,叫 FastTest。项目很简单,不过我主动扩展了技术选型,使用了非常多的技术,都是浅显入门的程度,非常适合用来做基础入门学习。本文也是从这个项目中提炼出来的。建议大家有空的时候看看,拉到本地,跑起来试试看效果。

文中的几段代码,分别位于:

项目的详细介绍请见:超全面全栈入门项目:fastest

扩展阅读

分类
技术

使用 Node.js 驱动 FFmpeg 在 Linux + vncserver 下完成视频录制

自动化录制屏幕有很多用途,比如生成教学视频、生成产品文档,等等。对比人工,自动化有很多好处:

  1. 避免创作者的设备和环境问题(比如邻居装修、麦克风不好等)
  2. 避免创作者的语言、发音问题(比如普通话不标准、不会说某种语言)
  3. 录制环境出现变化,可以方便的重录(比如换个背景图,界面有升级)
  4. 就像写博客一样,任何时候,拿出电脑或者手机都能编辑一段

所以目前研究这方面应用的很多,我厂也是。我近期就投入大量时间在这项工作上面,现在终于有所成果,写篇博客分享一下。

0. 准备环境

首先推荐大家使用 Linux。Linux 开源,有很多开源免费的工具可以完成各种操作,不仅可以录屏,还可以很容易地模拟各种用户操作,给我们留下大量开发空间。

建议选择有图形界面的 Linux 发行版,我尝试过 fedora 33 和 Debian 10 树莓派版,都很容易配置。如果使用纯命令行版本,然后自己完成安装图形界面,比如 gnome,再完成剩下来的配置,会很麻烦。

然后记得把系统更新到最新版,以规避可能遇到的问题。

1. 配置 vncserver

如果只能在主屏录制,这个产品的实用性就会大打折扣。所以我们选择用 vncserver 创建虚拟屏幕,然后在虚拟屏幕上完成录制。如果需要的话,也可以随时用 vnc viewer 之类的软件连上 VNC 实时查看效果,非常方便。

有些系统自带 vncserver,比如 Debian 10 树莓派,那就不用安装。我们选用 fedora 33,需要手动安装,这里推荐 TigerVNC,安装使用都很方便:

sudo dnf install tigervnc

安装完成后,使用:

vncserver :5 -geometry 1280x720

就可以创建虚拟显示器了。其中,:5 是显示器 id,可以顺延,比如 :6:7、甚至 :99,至于上限在哪里我暂时不知道。-geometry 1280x720 是设定显示器分辨率为 1280×720。

另外,还可以使用下面的命令查看和关闭显示器:

 vncserver -list
 vncserver -kill :5 

1.1 测试

配置完成之后,可以用 Firefox 测试一下效果。

# 安装 firefox
sudo dnf install firefox

# 在指定虚拟显示器打开 firefox
DISPLAY=:5 firefox https://cn.bing.com

# 截图,可能需要安装 xwd
xwd -root -display :5 > screen.xwd

# 转换成 png,可能需要安装 ImageMagick
convert screen.xwd screen.png

然后把图片下载到本地,或者启动一个 http 服务器就能看到了。

1.2 关闭桌面

默认的 VNC server 会启动桌面,此时可能会要求我们登录什么的。我们在这套系统当中并不需要桌面,只要有显示器即可,所以可以修改 ~/.vnc/xstartup 禁用:

#!/bin/sh
 

unset SESSION_MANAGER
unset DBUS_SESSION_BUS_ADDRESS
# 把下面这行注释掉 
# exec /etc/X11/xinit/xinitr

2. 使用 FFmpeg 捕获屏幕内容

使用 FFmpeg 录屏比较简单,将输入源设置为指定显示器即可,formatx11grab,命令大体如下:

ffmpeg -y -f x11grab -video_size 1280x720 -framerate 30 -i :5.0+0,0 -c:v h264 a.mp4

其中,

  • -y 意思是自动覆盖前面生成的视频
  • :5.0+0,0 是使用刚才创建的 :5 显示器,用它的 0 号桌面,启动位置是 0,0 即左上角
  • -f x11grab 是使用 X server 抓取格式,Linux 下的图形界面一版是基于这个系统

注意,上面这条命令里参数的顺序很重要,否则可能遇到 Protocol not found 等错误。

3. 使用 node.js 驱动

最后只要用 node.js 的 child_process.spawn() 功能调用上面的命令即可。这段代码属于公司,我就不贴了,主要分享几点经验教训:

  1. 要用 spawn,因为录制过程中我们需要用输出来判断录制状态,exec 这种只能在结束时提供输出的没法用
  2. FFmpeg 会把输出输出到 stderr,理由不明,不过记得要用 stderr 来检查
  3. 录像完成,如果在命令行,按 q 或者 ctrl+C 都可以停止录像,并开始封装视频文件。在 node.js 里,我们可以调用 cp.kill('SIGINT') 。注意,调用之后,FFmpeg 子进程并没有立刻结束,它要把前面的录像进行封装,这个过程也是需要时间的,所以如果你接下来还要对视频文件进行操作,应该等待子进程彻底结束
  4. 判断录像开始的依据,我目前用的是:输出里包含 Output #0, mp4, to 'a.mp4'
  5. 判断录像结束,视频已经生成的依据,我用的是 cp.on('exit', onExit),然后在 onExit 里处理。注意,其它情况导致 ffpmeg 子进程退出时也会触发这个函数,所以我们必须检查 code。此时,在我的机器上,code 是 255,表示它是用户手动中止的,可以当作判断依据。

总结

剩下来的内容基本就是怎么驱动图形界面程序运行了。一般来说用 puppeteer 比较好,可以很容易的跟 node.js 联动,我厂的 showman 也是基于这个方案来实现的,最后贴一段视频,大家看下效果:

一段用上述技术生成的视频

参考阅读

分类
nodejs

使用 Node.js 驱动 FFMPEG 录屏

FFMPEG 功能非常强大,不仅能转换视频格式、压缩视频、添加字幕等,还能录制屏幕内容。使用 FFMPEG 录屏的方法很简单:

# Linux
ffmpeg -video_size 1024x768 -framerate 25 -f x11grab -i :0.0+100,200 output.mp4 

# macOS
ffmpeg -f avfoundation -list_devices true -i "" 

# Windows
ffmpeg -f gdigrab -framerate 30 -offset_x 10 -offset_y 20 -video_size 640x480 -show_region 1 -i desktop output.mkv 

更详细的介绍可以参考官方文档,这里不做太多摘抄。

使用 Node.js child_process 可以调用外部程序执行操作,详细的用法可以参考官方文档。大概来说,分为:

  1. exec/execFile 调用程序,当执行完成,一次性获得结果
  2. spawn 调用程序,过程中流式获得输出

我的录屏对时间点有非常高的要求,力求准确。所以我只能选择 spawn,然后通过检查日志输出得到准确的录制开始时间。

所以,我的代码大概如此:

class FfmpegRecorder {
  // 其它函数略去
  startRecording(args) {
    this.isFileReady = new Promise((resolve, reject) => {
      this._isFileReadyResolve = resolve;
      this._isFileReadyReject = reject;
    });
    this.recording = spawn(this.ffmpeg, args);
    
    this.recording.stdout.on('data', this.onStdData);
    this.recording.stderr.on('data', this.onStdErr);
  }
  stopRecording() {
    this.recording.kill('SIGINT');
    const path = resolve(process.cwd(), 'a.mp4');
    return path;
  }

  onStdData(data) {
    data = data.toString();
    this.stdout += data;
    if (this.stdout.indexOf('Output #0, mp4, to \'a.mp4\'') !== -1) {
      this._isFileReadyResolve();
      this._isFileReadyReject = null;
    }
  }
  onStdErr(data) {
    data = data.toString();
    this.stderr += data;
    if (this.stderr.indexOf('Output #0, mp4, to \'a.mp4\'') !== -1) {
      this._isFileReadyResolve();
      this._isFileReadyReject = null;
    }
  }
}

根据我在命令行里直接跑 ffmpeg 的结果,它会先初始化,然后开始录屏。录屏开始时,会输出 Output #0, mp4, to xxx.mp4 这样的日志,所以我就会反复检查 stdoutstderr,直到关键日志出现,然后告诉外面的程序开始录制了。

这里比较奇怪的是,日志输出应该是正常的,走 stdout 通道,结果只能从 stderr 通道获取。我为防万一,两边都留下了同样的代码。可能我对 Linux 理解不够,将来再研究一下为什么会这样吧。

上面的代码忽略了 onErroronExit 部分,有兴趣的同学请等我开源(公司代码)。

在 Linux,stopRecording(),即 kill('SIGINT')(相当于按下 Ctrl+C)之后,FFMPEG 会终止录屏,并且生成可以播放的视频文件。但是在 Windows,只会留下一个无法播放的文件。

通过观察命令行直接运行 ffmpeg 的结果和 node.js 保存的结果,我发现缺失了结束录制后处理视频文件的部分。实际上 FFMPEG 录屏时只会记录视频内容,录制结束后才会生成影片的meta 信息,而播放器必须读取后者才可以正常播放。所以我猜测在 Windows 下 kill('SIGINT') 会直接彻底杀死进程,而不仅仅是发送一个信号给 FFMPEG,并让它完成后续的工作。

做出判断后,我尝试用按下 q 的方式通知 FFMPEG 停止工作,并等待 5s。果然成功,于是我构建了新类,继承前面的 FfmpegRecorder,大概代码如下:

const endTag = /kb\/s:\d+.\d{2}/m;

class WindowsRecorder extend FfmpegRecorder {
  stopRecording() {
    // 向 child process 输入 `q`
    this.recording.stdin.setEncoding('utf8');
    this.recording.stdin.write('q');
    const p = new Promise((resolve, reject) => {
      this._stopResolve = resolve;
      this._stopReject = reject;
    });
    // 设置一个超时保护 15s
    setTimeout(() => {
      this._stopReject();
    }, 15E3);
    return p;
  }
  onStdData(data) {
    super.onStdData(data);
    if (this._stopResolve && endTag.test(this.stdout)) {
      const path = resolve(process.cwd(), 'a.mp4');
      this._stopResolve(path);
    }
  }
  onStdErr(data) {
    super.onStdErr(data);
    if (this._stopResolve && endTag.test(this.stderr)) {
      const path = resolve(process.cwd(), 'a.mp4');
      this._stopResolve(path);
    }
  }
}

为了功能完善,我没有选择等待固定的时间,而是继续检查日志,直到发现 endTag 标记。另外,我也留下了 15s 超时保护,避免某些暂时没遇到的问题破坏功能。

至此,功能基本稳定完成。

分类
linux

解决 WSL Ubuntu 20.04 下使用 apt 源安装 node.js 的问题

随着 Ubuntu 20.04 发布,各大平台都适配发布了对应版本的系统,Windows WSL 也不例外。如果你是新系统,直接在 Microsoft Store 里搜索并安装 Ubuntu 即可;如果你是老系统,已经装过以前的版本,那么需要先卸载再安装,如果直接安装 Ubuntu 20.04 会有多个不同版本的 Ubuntu 共存。

装完系统后,接着安装其它软件。我现在比较喜欢用包管理工具安装软件,因为容易更新,而我又是更新爱好者。所以按图索骥,找到 node.js 的二进制包安装指引,复制执行:curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash - ,结果报错:gpg: can't connect to the agent: IPC connect call failed

经过搜索,得知这是 WSL 版 Ubuntu 20.04 的问题,与 WSL1 有一些不兼容,在 WSL2 上就没这个问题了。解决方案是装一些工具:

sudo add-apt-repository ppa:rafaeldtinoco/lp1871129
sudo apt update
sudo apt install libc6=2.31-0ubuntu8+lp1871129~1 libc6-dev=2.31-0ubuntu8+lp1871129~1 libc-dev-bin=2.31-0ubuntu8+lp1871129~1 -y --allow-downgrades
sudo apt-mark hold libc6

然后问题就解决了。

这个 issue 里还记录了一些别的方案,包括上面方案的修正版,不过我用起来没问题,也就没继续往下看。感兴趣的同学可以研究一下。

分类
npm

几步简单的操作解决 `npm audit` vulnerabilities

NPM 是 JavaScript 生态最重要的组成部分,我们的项目中会大量使用 NPM 安装第三方包(安装后就称为“项目依赖”)解决问题。这些第三方包也会带来他们的依赖,最终一个项目里可能安装成百上千个依赖。

有道是:没有完美的代码,代码里一定藏有隐患。所以用的依赖多了,其中有问题的概率也提升了,三五不时的,npm 就会提示我们:found N low/medium/high severity vulnerabilities

npm 提供命令 npm audit fix,理论上可以修复这些隐患,但在实际操作中,以我的经验来看,并不容易生效。我猜测可能是因为依赖间的复杂关系,想彻底解决并升级不太容易。所以我一般是这样做的:

分类
nodejs

解决“[ERR_PACKAGE_PATH_NOT_EXPORTED]: No “exports” main resolved”

周末例行升级系统,今天打开项目,npm run dev,就报这个错误。检查代码,没变化,依赖也没变化。因为错误位置在 main.js,尝试给它加上 exports,无果。

Google 之,发现一个非常新的 issue:https://github.com/babel/babel/issues/11216,3天前,来自 @babel/babel 仓库,多半是了。

点进去一看,原来 node.js 从 13.10.1 之后,对 package.json 里的 exports 属性解读出现问题,继而导致 Babel 抛出错误。最简单的解决方法就是升级 Babel 到 7.8.4。

升级后问题解决。

分类
js

在 Node.js 12 中使用 ESM

Node.js 12 之后开始支持 ECMAScript Modules(简称ESM),不过并不是默认开启或者自动切换。坦率地说我也卡了一阵子才搞清楚怎么直接使用。简单记一下吧。

分类
nodejs

Node.js 里使用 Promise 的小技巧

Node.js 8 的时候,引入了 util.promisify() 方法,可以把 node-like 的回调函数改造成返回 Promise 实例的方法,我当时还写了篇博文《Node.js 8 中的 util.promisify》小记。

所以我现在写 Node.js 基本都是这种风格:

const fs = require('fs');
const {promisify} = require('util');

const readFile = promisify(fs.readFile);
const writeFile = promisify(fs.writeFile);

刚才在推上看到两位大佬聊起这个话题,发现可以这么搞:

去查了一下 Node.js 的文档,发现这是 v10 新增的 API,升级之后即可使用。

我比较喜欢这么做:

const {promises: {readFile, writeFile}} = require('fs');

(async () => {
  let content = await readFile('1.txt', 'utf8');
  content = doSthToContent(content);
  await writeFile('2.txt', content, 'utf8');
  console.log('ok');
})();