分类
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

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

分类
ghost

Ubuntu 配置 Nginx + Ghost

按照惯例,买好机器,登录进去。建议是境外服务器,可以省掉备案环节。但是不要干坏事哦。

1. 更新系统

apt-get update
apt-get upgrade

2. 创建 ssh-key

ssh-keygen -t rsa -b 4096 -C "my-email@meathill.com"

3. 添加 authorized_keys

使用 ssh key 登录可以大大提升服务器的安全性。

首先,将你的电脑上的公 key 添加到服务器 ~/.ssh/authorized_keys。接着编辑服务器上的 /etc/ssh/sshd_config,禁用密码登录。

ChallengeResponseAuthentication no
PasswordAuthentication no

最后重启 ssh 服务:service ssh restart

4. 安装 Node.js

按照 NodeSource Node.js Binary Distributions 的指引,安装对应版本的 Node.js—— Ghost 要求 LTS,所以目前只能用 v12。

curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -
sudo apt-get install -y nodejs

5. 安装 MySQL/MariaDB

Ghost 默认使用 SQLite 作为数据库,不过生产环境中 SQLite 性能不够,因此建议直接安装并使用成熟可靠的数据库软件。这里建议使用 MariaDB,开源免费。不同版本的 MariaDB 源可以在 MariaDB Repositories 找到,我一般使用清华的源:

sudo apt-get install software-properties-common
sudo apt-key adv --fetch-keys 'https://mariadb.org/mariadb_release_signing_key.asc'
sudo add-apt-repository 'deb [arch=amd64,arm64,ppc64el] https://mirrors.ustc.edu.cn/mariadb/repo/10.5/ubuntu focal main'

# 添加完仓库和 key 之后,就可以安装了
sudo apt update
sudo apt install mariadb-server

MariaDB 的安装过程没有配置密码的环节,可以使用 重置 MariaDB root 密码 一文中介绍的方法先重置 root 密码,然后创建需要的用户和数据库。

create user 'ghost'@'localhost' identified by 'ghost';
create database `ghost`;
grant all on `ghost`.* to 'ghost'@'localhost';

6. 安装 ghost-cli

ghost-cli 是 ghost 提供的命令行管理工具,可以大大减少我们管理 Ghost 实例的时间。

# 安装
npm i ghost-cli -g

# 创建一个目录安装 ghost
cd /var/www
mkdir ghost
cd ghost

Ghost 要求我们不能用 root 用户维护实例,所以这时可能需要创建一个新用户,并赋予其 sudo 权限(方便操作目录权限之类的),我习惯命名为 ghost-admin:

adduser ghost-admin
usermod -aG sudo ghost-admin

接着,切换到 ghost-admin,安装 ghost

su ghost-admin
cd /var/www
# 改变权限
sudo chown -R ghost-admin:ghost-admin ghost
cd /var/www/ghost
ghost install

安装完成之后,执行 ghost ls,可以看到正在运行的 Ghost 实例,就是一切正常了:

+ sudo systemctl is-active ghost_fav-meathill-com
┌──────────────────┬────────────────┬─────────┬──────────────────────┬─────────────────────────┬──────┬─────────────────┐
│ Name             │ Location       │ Version │ Status               │ URL                     │ Port │ Process Manager │
├──────────────────┼────────────────┼─────────┼──────────────────────┼─────────────────────────┼──────┼─────────────────┤
│ my-ghost │ /var/www/ghost │ 3.31.5  │ running (production) │ http://my-ghost.com │ 2368 │ systemd         │
└──────────────────┴────────────────┴─────────┴──────────────────────┴─────────────────────────┴──────┴─────────────────┘

接下来,编辑 config.production.json,把数据库配置和域名配置都写进去,就基本可用了。

7. 安装并配置 Nginx

首先,配置源。目前 Ubuntu 20.04,代号 focal,命令如下:

deb https://nginx.org/packages/ubuntu/ focal nginx
deb-src https://nginx.org/packages/ubuntu/ focal nginx
sudo apt update
sudo apt install nginx

增加配置文件,并进行反向代理,即把外来的访问反向代理给 Ghost 服务:


cp /etc/nginx/site-available/default /etc/nginx/site-available/ghost.conf
ln -s /etc/nginx/site-available/ghost.conf /etc/nginx/site-enabled/ghost.conf

配置文件大体如下:

server {
    listen 80;
    listen [::]:80;

    server_name my-ghost.com;
    root /var/www/ghost/system/nginx-root; # Used for acme.sh SSL verification (https://acme.sh)

    location / {
        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://127.0.0.1:2368;

    }

    location ~ /.well-known {
        allow all;
    }

    client_max_body_size 50m;
}

最后重启 nginx 服务或者重新加载配置即可:

# 一般建议先检查一下配置有没有问题
nginx -t
service nginx restart

总结

这次主要更新了使用 ghost-cli 安装和配置的内容,服务器也改用 MariaDB,新的技术使得安装配置都更简单了。

分类
html

使用 <input type=”file”> 上传 ZIP/RAR 文件

上传文件要用到 <input type="file">,这个元素有个 accept 属性,可以用来筛选文件类型,方便用户选择。按照 MDN 的说法,这个属性的值支持以下几种形式:

  1. 合法的文件扩展名,大小写不敏感,以 . 作为开头,比如 .jpg.pdf
  2. 合法的 MIME type,不需要扩展名
  3. 媒体文件,audio/* 适配任意声音文件,video/* 适配任意视频,image/* 适配任意图片

同时,属性值可以使用 , 连接,表示“或”的意思。注意,这里只能是单独的 ,,不能有空格,不然有空格的部分会失效。所以,比如,一个上传图片、以及 PDF 的元素就可以写成:

<input type="file" accept="image/*,.pdf">

使用扩展名比较方便,但是扩展名太多不方便管理,比如 .jpg.jpeg,容易漏掉,所以我更喜欢 MIME type。那么压缩文件的 MIME type 是什么呢?经过一番搜索和尝试,是:.zip,.rar,application/x-rar-compressed,application/zip,application/x-zip-compressed,application/octet-stream

分类
js 技术

基于 @vue/cli 的项目配置 browserslist

前些日子虽然写了 最近折腾 @babel/preset-env 的一些小心得,但其实没有正确的理解和配置 browserslist,所以今天问题又来了。

分类
js

Nuxt.js 支持 core-js 3

Vue CLI 升级到 v4 之后,将内部的 core-js 依赖升级到 v3,关于 core-js v3 和 core-js v2 之间的区别,我在 最近折腾 @babel/preset-env 的一些小心得 里简单介绍过。

升级完 Vue CLI 之后,在调用 nuxt generate 生成静态页,就会报错,因为 Nuxt.js 默认使用 core-js 2。这个时候,如果 Nuxt.js 版本在 2.6.0 之后,就只需要修改 nuxt.config.js 里的配置,指定 core-js 的版本:

module.exports = {
  build: {
    babel: {
      presets({ isServer }) {
        return [
          [
            require.resolve('@nuxt/babel-preset-app'),
            // require.resolve('@nuxt/babel-preset-app-edge'), // For nuxt-edge users
            {
              buildTarget: isServer ? 'server' : 'client',
              corejs: { version: 3 }
            }
          ]
        ]
      }
    }
  }
}

参考文档:https://nuxtjs.org/guide/release-notes#v2.6.0

说起来 Nuxt.js,用它发布静态页比想象中复杂,如果你想快速掌握这个技能,不妨看下我的这本小书:

分类
js

解决 Firefox 下的 race 问题

我厂有几个产品,需要从后端获取大量的信息,为了让用户能够近乎实时的看到这些信息,大部分数据都是通过 WebSocket 发给前端。这些产品在 Chrome 下表现正常,但是在 Firefox 下经常把数据格式搞乱,最终渲染失败。

因为 Firefox DevTools 没法解析 WebSocket 数据,而且市场占有率比较低,所以我一直没有解决这个问题。前几天终于把最小可复现实例搞出来,正准备研究,结果同事已经修好了。

预览版的 Firefox 终于可以在 DevTools 里查看 WebSocket 每一帧的数据,所以她尝试看了一下,发现从解析二进制数据的角度来看,Firefox 应该没问题。于是又回到代码,发现了一个可能产生 race 的点:

if (data instanceof Blob) {
  data = await new Response(data).arrayBuffer();
}

因为服务器返回的数据是二进制,所以我需要进行一次转换,把它变成 ArrayBuffer,然后再通过 TextDecoder 转换成文本,然后处理。Response.arrayBuffer 返回的是 Promise,所以我就很自然的用 await,并且在 Chrome 上运行良好。

但是在 Firefox 里,某些帧会后发先完成转换,a b c 变成 a c b,于是数据格式错乱,无法正常解析。我怀疑 Chrome 并没有真的把这一步保留到用户,而是同时存了两份数据,这样转换的时候直接给出数据就好,所以是微任务,不走 Event loop,不会产生 race。而 Firefox 则是实时转换,所以是宏任务,所以出问题。

我尝试去翻了一下源码,无奈平时没看过,所以没能找到证据。如果有哪位同学刚好知道,可以在评论里告诉我。

分类
招聘

代友招聘:每日优鲜 BI 部门招前端技术经理

有个关系很好的朋友近期加盟每日优鲜,负责 BI 系统,据他描述,组内有三名初级前端,需要一个高级前端做技术经理,负责带团队。

薪资范围:30K~40K,16个月

工作地点:北京望京

分类
招聘

面试题:如何理解加班

前阵子参加 SF 的征文活动 一起分享你的故事,我的文章 我的编程职业生涯 有幸得到大家的支持,最终得奖:汤青松老师的《PHP Web 安全开发实战》。书前两天寄到了,还没来得及细看,也没法写书评,所以想了想,再分享一段职场经验吧。