分类
js

在任意上下文执行代码,`new Function` vs `eval()`

0. 需求及方向

我厂的 Navigator 扩展遇到一个需求:

  1. 动态生成一段 JS 在浏览器里运行
  2. 能重复生成、重复运行
  3. 能生成一段新代码,继续在上一次的环境里运行

其中(3)是新需求,(1)(2)已经比较完善地实现了。主要方案是使用 <script type="module">,这样加载的 JS 会自动执行,并且与全局环境相隔离,不会污染全局环境——这样就可以重复执行。

(3)的难点在于,上一段 JS 已经执行过了,环境也自然释放了,我要么把上一阶段 JS 的执行结果保存下来,要么把执行环境保存下来。前者需要对 Navlang 编译器做大幅度的修改;后者则有很大的实施难度。

经过研究和思考,我选择 保存环境 为攻关方向。即在上次执行结束前,在 window 上注册一个钩子函数,然后在后续追加执行的时候调用它,把后面的代码以函数的形式传进去,以便在上一次留下的环境里继续执行。

1. 问题:访问环境里的变量

添加钩子函数并不难,这样就行:

// 在上一段代码最后添加钩子函数
window.doNext = function (func) {
  func();
}

// 在新一段代码执行函数
window.doNext(function () {
  // 新生成的函数
});

这样做虽然看起来是在上一次的上下文环境中执行新的函数,但由于 JS 的闭包和作用域链特性,实际执行时,新函数并不能访问到上一次上下文的数据。换言之,目的并没有实现。

2. new Function() vs eval()

然后回到标题。所以我们必须在上一次的环境里重新构建函数,这并不困难,其实开发浏览器扩展时,因为要协调四、五个运行环境,所以把函数序列化传输,再重新构建执行非常常见。于是我直接操起 new Function(),然后失败了。

查阅 MDN,原来 new Function() 构造的函数,它的上下文会绑定在全局对象上(相当于 .call(null)),所以自然无法访问到上一次环境里的变量。

再看 eval()文档,似乎可行,于是换用之,果然有效。至此,问题解决。

3. 代码范例

function navlangExecuteFunction(func) {
  // 使用模版字符串构建异步函数
  eval(`(async function doNext() {
    // 捕获中间可能出现的问题
    try {
      // 执行真正的函数
      await (${func})();
      // 为兼容 node.js,mock 一个 process 来处理执行结束
      process.exit();
    } catch (e) {
       if (!e.message.startsWith('Exit code:')) {
         console.error(e);
         process.exit(1);
       }
    }
  })()`);
}
window.navlangExecuteFunction = navlangExecuteFunction;
const serializeFunction = (f) => {
  const serialized = f.toString();
  // Safari serializes async arrow functions with an invalid function keyword.
  // This needs to be removed in order for the function to be interpretable.
  const safariPrefix = 'async function ';
  if (serialized.startsWith(safariPrefix)) {
    const arrowIndex = serialized.indexOf('=>');
    const bracketIndex = serialized.indexOf('{');
    if (arrowIndex > -1 && (bracketIndex === -1 || arrowIndex < bracketIndex)) {
      return async ${serialized.slice(safariPrefix.length)};
    }
  }
  return serialized;
};

let nextStep = async function () {
  // 要执行的部分
}
nextStep = serializeFunction(nextStep);
navlangExecuteFunction(nextStep);

4. 可能带来的问题

MDN 明确建议大家 不要用 eval。理由如下:

  1. 不安全。因为 eval() 会在当前环境执行代码,意味着攻击者可能窃取任何当前环境的数据。
  2. 性能差。现代 JS 引擎会对代码进行大量优化,包括转成机器码等。eval() 会破坏这个过程,使得运行性能大大降低。

不过在我的场景下,这两个问题并不严重。一方面,被执行的追加代码都是由 Navlang 编译器生成,而不是任意第三方,它的安全性不会比其它的代码安全性低。另一方面,这个功能是帮助用户开发调试 Navlang 的,我们可以认为它大概率不会跑在性能敏感的环境里。

另外,对我厂的 Navigator 产品而言,这样的方案还会让 GC 变得比较难执行。不过一样从 便利开发 的角度出发,我觉得性价比完全 OK。

5. 总结

这个需求比较特殊,涉及到 JS 函数的很多性质,包括运行时优化的知识,还是蛮值得大家琢磨的。

另外就是所谓“尽信书不如无书”。eval() 的确存在一些问题,不能轻易使用,但是当需求摆在面前,经过充分的分析确认之后,我们该用还是要用。

分类
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 超时保护,避免某些暂时没遇到的问题破坏功能。

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

分类
js

函数,栈,try…catch,以及异步

前两天《记一个 `try…catch` 异步函数的坑》发出后,有同学表示对最后一句不解:

异步函数的调用可能并不在当前栈,也无法被 try…catch 在当前栈捕获到错误。

要解释清楚,一两行是不够的,于是写这篇文章展开解释一下。(这篇文章是基础向。)

函数与栈

首先来看这样一段代码:

function a() {
  // ...
}

function b() {
  // 代码1 
  a();
  // 代码2
}

function c() {
  // 代码3
  b()
  // 代码4
}

c();

它会怎么执行呢?简单来说是这样:

  1. 构建一个栈
  2. c 推入栈,开始执行 代码3
  3. b 推入栈,开始执行 代码1
  4. 将 a 推入栈,开始执行
  5. a 执行完,出栈
  6. 开始执行 代码 2
  7. b 执行完,出栈
  8. 开始执行 代码 4
  9. c 执行完,出栈

栈是一个先入后出的数据结构,你可以把它当成一个桶,先放进去的东西会被后放进去的东西压在下面,需要先把上层的东西,也就是后放进去的东西拿出来才能拿到先放进去的东西。

发生错误

如果代码执行中发生错误,就会在错误处中断,并逐个出栈。此时我们不仅能看到错误本身,还能看到错误发生处的完整栈信息,对 debug 很有帮助。

try...catch

异步函数出现之前,try...catch 只能捕获当前栈的错误。比如,同样是上面那段代码,稍微改动一下:

// 之前定义 a b c 的代码不变

function d() {
  throw new Error('oh my god');
}

try {
  c();
} catch (e) {
  console.error(e);
}

d();

这段代码里,c() 执行时如果有错误,可以被 try...catch 捕获到;但是 d() 位于另一个栈(或同一个栈的另一个堆叠),就无法捕获。

异步 callback & Promise

Promise 成为规范被纳入标准之前,我们处理异步操作时需要使用回调(callback)。比如侦听事件:

try {
  $('.btn').on('click', function (e) {
    console.log('hello');
  });
} catch (e) {
  console.error(e);
}

请注意:这里添加侦听函数的代码,也就是 $().on() 的部分,是在当前栈执行的;而用户点击后执行的操作,即 console.log('hello') 的部分,是在将来某个事件调用栈里执行的。所以,如果后面的函数里有问题,那么如上面所示的代码是无法捕获到错误的。

Promise 也一样。比如下面这段代码:

new Promise((resolve) => {
  // 执行完后回调
  func0(resolve);
})
  .then(() => {
    return func1();
  })
  .then(() => {
    return func2();
  });

这里的 func0func1func2 三个函数都是在不同调用栈里执行的,所以如果你在最外面 try...catch,无法捕获到错误。

这样会给我们编写代码带来一些困难。比如,有时候我们需要同时发起好几个网络请求,有些会成功有些会失败。我们并不知道每个失败请求对应的构造函数是怎么执行的,只能依靠请求内容进行判断,就比较麻烦。

这个问题,在异步函数中得到了解决。

异步函数 async function

这个变化主要是 await 带来的。

前篇文章所说,在不使用 await 的情况下,try...catch 只会捕获当前调用栈的错误。对于异步函数来说,它在当前栈会返回 Promise 实例,然后就顺利结束,不会抛出任何错误。所以 try...catch 也无法捕获任何错误。

但是添加 await 之后,情况就不同了。这个时候,try...catch 会捕获整个 Promise 里所有的错误,即使函数位于不同调用栈,也可以正确捕获并且显示在一起,方便调试。

分类
js

记一个 `try…catch` 异步函数的坑

前几天遇到一个问题:想捕获异步函数的错误,但是捕获不到。我的代码大概是这样:

try {
  (async () => {
    // 一大堆异步函数
  })();
} catch (e) {
  if (e.message.startsWith('my error:') {
    // 我的错误,提示一下即可
  } else {
    // 某种不知名错误,继续抛出
    throw new Error(e.message);
  }
}

不知道读者是否发现问题,我当时是左看右看没看出来。后来只好写最小用例缩小问题范围,终于找到问题所在:异步函数前面缺少 await。于是想通了,上面的代码其实被引擎解释成:

  1. try 一个函数
  2. 这个函数的返回值是一个 Promise
  3. Promise 是正常对象,所以 try 成功
  4. 至于 Promise 里的函数是否失败,不关 try 的事,所以自然捕获不到

所以正确的写法是:

// 外面先套一层,不然不能用 await
(async () => {
  try {
    // 再加上 `await`
    await (async () => {
      // 异步函数体
    })();
  } catch (e) {
    // ....
  }
})();

这个 await 至关重要。这是异步函数的一大特性,即 try...catch 可以捕获整个异步操作前后所有栈的异常,而不仅仅是当前函数的异常。同时我们也要注意,异步函数的调用可能并不在当前栈,也无法被 try...catch 在当前栈捕获到错误。

分类
许愿

Hello,2021

2020 可能是史无前例的不受欢迎的一年,无论是微博、朋友圈、Twitter,极少看到有人怀念它的。我不禁为 2020 感到惋惜,明明不是它的罪过。

不过我还是要说,在我经历过的 36 个年头里,2020 年是体感速度最快的一年,我现在都还能回忆起年初的种种,好像一睁眼一闭眼,就 2021 了,还有很多想做的事情没做……好吧,还是总结一下吧。

回顾 2020

先说身体。去年 4 月 28 日,早上起床突然发现脚踝肿了,怀疑是痛风,于是去医院看诊。医生觉得不太像是痛风,还是扭伤的可能性大。但是检查结果血糖没控制好,骨外科的大夫说糖尿病患者可能会出现随机的关节痛。哎,果然还是走上了糖尿病患者的老路。

跟老婆商量了一下,正好疫情逐步得到控制,健身房也即将开放,于是我们办了健身卡开始健身。我也找医生换了新药控制食欲。开始效果很明显,食欲下降明显,几乎吃两口就饱了。体重下降很快,最低到 105+kg。可能是太过冒进,身体也出现一些不适,于是 8 月份去住了一周院调整。后来减了一些运动量,加了一些食量,目前感觉好多了,体重稳定在 106~108之间。

开始锻炼之后,心肺功能和肌肉都得到了不小的加强,基本学会自由泳。

总结一下:

  1. 终于有一年体重达标了,可喜可贺!
  2. 利拉鲁肽对我非常有效,不过半年过去,药效已经明显下降。
  3. 低血糖不好,控糖的目的一是血糖稳定,二是稳定在合适的范围内。
  4. 无氧运动加强肌肉,提升基础代谢;有氧运动减重

side project 部分就比较惨,书没进展,肉大师没捡起来,新接的视频也没录完……春节前一定要录完,加油。

博客大概写了 69 篇,还行吧,草稿箱里还有几篇没写完。访问量相比去年提升很多,坚持还是有用的。今年继续努力。

旅游彻底黄了,一年没出门。哦,不对,去了几次深圳……

终于下定决心把车换了,老车送回北京给表姐开,换了一辆亚洲龙(Avalon)代步。去年告别了很多人,包括 Avalon JS 框架的作者。新车挺好的,未来一时半会儿不会再换了。

NAS 的机器弄出来了,没顾上搞系统,今年继续吧。

2021 计划

  1. 坚持锻炼,把体重降到 105 kg
  2. 去年捡起来游泳,今年希望捡起来篮球
  3. 复活肉大师,把书写完
  4. 录完欠(签)下的教学视频
  5. 把前面设想的教学视频小语言做出来,然后再录 1~2 套视频
  6. 努力写博客,去年大概挣了 $20,希望今年够得上提款
  7. 旅游方面,如果国境线如果能开放,希望可以出去一次
  8. 感谢老板支持,去年年底终于把 Navlang + 扩展发出去了。那么今年要再努力一把:
    1. 上线 Chrome Web Store 和其它浏览器应用商店
    2. 能够收获 100+ 用户
    3. 完成付费使用功能
  9. 搞定一个 Side Project

去年身体状况从五月折腾到八月,期间想了很多,觉得还是要拼一拼,主要是不拼也未必能好。所以今年还要多努力!加油!

分类
js

使用 File System Access API 在浏览器里操作本地文件

如《Webpack 5 发布,Chrome 86 开始支持本地文件系统》一文所述,Chrome 86 开始,浏览器正式支持操作本地文件。接下来结合最近的使用,分享下用法。

0. 准备工作:理清概念

首先,我们要先搞清楚一些概念。实际上,让浏览器操作本地文件是开发者一直在努力并且不停在探索的方向,所以历史上有很多方案,存在很多类似但其实并不一样的 API,大家在学习的时候一定要搞清楚,不要弄混。

最早登场的是 File API,代表功能是 FileReader(参考:《使用 Promise 封装 FileReader》)。这个 API 最大的进步在于,我们可以在浏览器里读取和操作二进制文件,然后通过 <a download="file.ext"> 下载到本地。如此,浏览器作为工具平台的价值大大提高。

接下来,激进的 Google Chrome 提出并实现了 File System API。这个 API 试图在浏览器里创建一个独立的文件环境,让开发者可以在里面任意操作文件和目录,如果能做好,那么是一个非常好的抽象。可惜步子不仅大、而且偏,最终失败。我总结原因有二:

  • 一方面“独立的文件环境”,即无法操作系统本地文件,那么其实没什么价值……
  • 另外,当时浏览器的其它限制没有突破——没有包管理、没有 babel、甚至没有 Promise,IE 仍然大量存在,开发难度极大。

所以最终这套方案死得悄无声息。《HTML5的File API应用》,这篇博客可能是为数不多的中文分享。

接下来是 Chrome Extension、Chrome App、Chrome OS 里的 File (System) API。这几个产品都是 Google 私有,不用考虑其他浏览器厂商,所以可以放开手脚随便搞。这里大家需要注意的是,因为 Google 的产品策略一向是说关就关,所以大家要留心常看文档,别学了一半 API 没了,比如:Extension 的 chrome.fileSystem 就已经弃用了

最后,也就是今天的主角,File System Access API。这套方案应该是未来的主角。它提供了比较稳妥的本地文件交互模式,即保证了实用价值,又保障了用户的数据安全,明显是前辈 File System API 的继任者。

它的设计思路也不复杂:

  1. 要求用户手动选择文件或者目录,以获取文件或目录的控制权限
  2. 选择文件或目录后,获取到 FileHandle,后续的操作经由它来进行
  3. FileHandleserializable 对象,所以可以通过序列化和反序列化实现跨 session 的存储(即刷新后还能用)

好,下面看代码。

1. 读取本地文件

这段代码可以比较完整的演示 window.showOpenFilePicker API 的用法:

// 使用 `try...catch` 可以捕获用户取消选择时抛出的错误,如果你对错误不在意,不捕获也行
try {
  const [handle] = await showOpenFilePicker({
    multiple: false, // 只选择一个文件
    types: [
      {
        description: 'Navlang Files',
        accept: {
          'text/x-navlang': '.nav',
        },
      },
    ],
    excludeAcceptAllOption: true,
  });
} catch (e) {
  if (e.message.indexOf('The user aborted a request') === -1) {
    console.error(e);
    return;
  }
}

// 如果没有选择文件,就不需要继续执行了
if (!handle) {
  return;
}

// 这里的 options 用来声明对文件的权限,能否写入
const options = {
  writable: true,
  mode: 'readwrite',
};
// 然后向用户要求权限
if ((await handle.queryPermission(options)) !== 'granted'
  && (await handle.requestPermission(options)) !== 'granted') {
  alert('Please grant permissions to read & write this file.');
  return;
}

// 前面获取的是 FileHandle,需要转换 File 才能用
const file = await handle.getFile();
// 接下来,`file` 就是普通 File 实例,你想怎么处理都可以,比如,获取文本内容
const code = await file.text();

2. 保存本地文件

前面说过,FileHandle 可以序列化,也即可以进行持久化存储。所以我们只需要把对应的 FileHandle 存下来,然后保存即可。

if (data.file) {
  const writable = await data.file.createWritable();
  await writable.write(data.code);
  await writable.close();
}

如果之前没有获取过 FileHandle,则可以通过 window.showSaveFilePicker 来获取:

try {
  const file = await showSaveFilePicker(filePickerOptions);
} catch (e) {
  if (e.message.indexOf('The user aborted a request.') === -1) {
    console.error(e);
  }
  return;
}
// 然后接前面的代码
const writable = await file.createWritable();
await writable.write(data.code);
await writable.close();

这个功能现在有一点小问题,不知道是不是 Chrome 实现不太稳定,如果你打开开发者工具,然后钩上“Pause on caught exceptions”,那么保存时会暂停数次,并提示错误。不用理会,直接继续执行即可。我猜测这个过程本来应该由浏览器自动捕获并重试,直到超时保护或者写入成功,但是现在会错误地抛出来。

3. 总结

File System Access API 不仅可以操作文件,还可以操作目录,操作目录的方式和文件相仿,我就不详细举例了,大家可以看下后面的参考链接,或者等我用到目录、踩了坑再来分享。

这个 API 对前端来说意义不小。有了这个功能,Web 可以提供更完整的功能链路,从打开、到编辑、到保存,一套到底。虽然目前只有 Chrome 支持,但还是建议大家尽快把它用起来。


参考链接:

分类
chrome

Chrome 扩展里实现 SSO

周五打算给客户发版,结果在这里卡了大半天,写篇博客记录下。

0. SSO 的实现

SSO,Single sign-on,单点登录,即统一处理用户登录、提供用户身份凭据的功能。使用 SSO,可以只维护一套用户体系,容易开发维护;对用户来说,只需要登录一次就能使用该开发商的全部产品,也很轻松方便。

一般来说,SSO 的流程是:

  1. 用户使用 A产品,域名是 pa.mydomain.com,登录服务(S)位于 login.mydomain.com
  2. 用户使用提供服务的 A产品,A产品需要登录,用户选择登录
  3. 来到登录服务,完成登录
  4. S服务将用户指回 A产品,返回的 URL 里包含一个 token
  5. A产品拿到 token,请求 S服务,验证 token,获取部分用户信息(比如邮箱,一般只用来展示
  6. A产品生成自己所需的身份凭据,并以此验证用户身份

我厂的产品也是这么实现的。

1. Chrome 扩展遇到的问题

本地调试一切正常,但是加载成扩展之后,从登录服务跳回扩展会遇到 ERR_BLOCKED_BY_CLIENT 错误,URL 也被重定向到 chrome://invalid/。我在这里卡了很久,主要是不知道该怎么定位问题和搜索答案。

后来经过反复尝试,我终于发现,只有从登录页面跳转回去插件页面的时候,即 location.href='chrome-extension://{id}/ui/index.html 的时候,才会报错,所以立刻换用 chrome extension href ERR_BLOCKED_BY_CLIENT 作为关键词,立刻找到了答案:redirect to chrome-extension:// results in ERR_BLOCKED_BY_CLIENT

然后阅读文档:Manifest – Web Accessible Resources(可由 Web 访问的资源),得知需要在 manifest.json 里添加对应的配置:

{
  ...  "web_accessible_resources": [
    "ui/index.html"
  ],
  ...
}

添加后 SSO 就正常了。

2. 后记

不过我没想明白的是,这个配置意义何在?配置写在扩展里,防止 web 访问扩展里的文件,似乎并没有什么帮助,也没什么安全性的顾虑。也许是我还没遇到吧。

分类
chrome

让 Chrome API 支持 Promise

Chrome API 都是回调型,连续使用非常不方便,希望能改成 Promise 型。Chrome 本身不提供 promisify,不过可以自己写一个:

export default function promisify(original) {
  return function (...args) {
    return new Promise((resolve, reject) => {
      original(...args, (...results) => {
        if (chrome.runtime.lastError) {
          reject(chrome.runtime.lastError.message);
        } else {
          resolve(...results);
        }
      });
    });
  }
}

这里有几个注意事项:

  1. 参数使用 ...args 进行拆解和合并,方便调用
  2. 需要检查 runtime.lastError,不然出错的时候浏览器会报错,影响体验

使用的时候,我比较喜欢只修改需要使用的函数,不打算 promisify 全部函数,大概是这样:

import promisify from './index';

/* global chrome */

export const update = promisify(chrome.tabs.update);
export const remove = promisify(chrome.tabs.remove);
export const get = promisify(chrome.tabs.get);
// 我觉得 `close tab` 看起来更合理
export const close = remove;
// 封装一个 `goto` 的快捷方式
export const goto = function (tabId, url) {
  return update(tabId, {url});
}
// 全部导入,好处是简单,坏处是不方便 tree-shaking
import * as ChromeTabs from '../chrome-promisify/chrome.tabs';

ChromeTabs.goto(tabId, 'https://blog.meathill.com');

// 需要哪个导入哪个
import {goto} from '../chrome-promisify/chrome.tabs';

goto(tabId, 'https://github.com/meathill');
分类
服务器端

在树莓派上启用 PostgreSQL 对外服务

以前写过一篇笔记《树莓派4 安装 OpenResty + PostgreSQL》,记录如何在树莓派上装 PostgreSQL,不过那时候只是为了在上面做开发,没有考虑过对外服务。如今为了能够在别的机器上做开发,所以要想办法配置一下对外服务。

0. 系统

  • Raspberry Pi 4B
  • Debian 10 buster 更新到最新
  • 如上篇文章所述安装和配置 PostgrSQL

1. 判断本地运行状态。

# 查看服务状态
sudo service --status-all
# [ + ]  postgresql

# 查看端口
sudo netstat -plunt | grep postgres
# tcp        0      0 127.0.0.1:5432            0.0.0.0:*               LISTEN      6629/postgres

服务在运行,端口也在侦听,直接连接,失败,被服务器拒绝。

2. 安装防火墙工具调整规则

猜测可能跟防火墙有关,iptables 我不熟,所以安装 ufw 帮忙:

# 安装
sudo apt install ufw

# 启动端口
sudo ufw allow 5432
sudo ufw allow from 10.0.0.10 # 我的 iMac

3. 修改侦听端口

修改防火墙后还是连不上。使用 Telnet 工具可以本地连接,但不能远程连接,推断应该是侦听端口的问题。回去仔细看了一下端口状态,觉得应该是端口没配好,所以修改配置,侦听 0.0.0.0,然后重启 PostgreSQL 服务,再连接就成功了。

listen_addresses = '0.0.0.0' 
port = 5432
host    all             all              0.0.0.0/0                       md5
host    all             all              ::/0                            md5

参考链接:

分类
服务器端

LeanCloud 笔记

慢慢记。

慎用 await Promise.all(items.map(item => ....))

很容易造成 409 too many requests 问题。

最好用

const newItems = [];
for (const item of items) {
  item = await doSomeAsyncJob();
  newItems.push(item);
}

Pointer 时尽量用 query

取单一对象的时候,方法有很多,比如 createWithoutData + fetch。不过如果如果对象内部属性有 Pointer,且我们希望一次性把 Pointer 取回来的话,最好用 query,因为只有它支持 .include(),可以一次性拉取全部需要的数据,减少请求次数,减少发生 too many requests 的可能。