分类
服务器端

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 的可能。

分类
puppeteer

在 Raspberry Pi 4B 上跑 Puppeteer

首先,我使用的是 Raspberry Pi 4B,安装的是官方 Debian 10(buster)系统,并且保持升级到最新版。

因为集成的 chromium 核心组件的关系,Puppeteer 一直无法跑在 Raspberry Pi 上,需要自己安装 Chromium Browser,然后修改 Puppeteer 启动的浏览器,以实现功能。这个一搜就能找到,比如 https://stackoverflow.com/questions/60129309/puppeteer-on-raspberry-pi-zero-w

但是我之前一直没能跑起来的问题在于,sudo apt install chromium-browser 会失败,报错找不到目标模块,只能装 chromium-codecs-ffmpeg,然后没用。

然后我受前几天完成 WSL 配置的启发,使用 apt search chromium 搜索名字接近的包,发现了真正原因:很简单,chromium-browser 当然是存在的,只是因为我当前系统配置的关系,它希望安装 stable 版本的软件,不愿意安装 testing 版本,所以不给装。

接下来我面临两个选择:

  1. 修改配置
  2. 试试 Chromium

方案二更容易尝试。于是

# 安装
sudo apt install chromium

# 查看路径
whereis chromium

修改 JS 代码:

const browser = await puppeteer.launch({
  executablePath: '/usr/bin/chromium',
  // 其它配置项
  // ....
}); 

再执行,成功。啊,终于搞定了手边所有平台跑 Puppeteer 了,哦耶。

分类
puppeteer

在 Windows 10 WSL 中使用 Puppeteer

使用 Puppeteer 很久了,基本上一直都在使用原生版本,macOS 或者 Ubuntu。在 Windows 10 上,我比较喜欢用 WSL,基本上所有其它脚本和程序都跑在 WSL 上,唯独 Puppeteer,我会专门弄一个目录,以便运行 Windows 版本。

前些日子去深圳办公,需要跑测试。我厂的测试基于 Perl 的 Test::Base,但 Windows 下的 Perl 不太好用(其实是我没仔细研究),远不如 WSL 开箱即用。所以就折腾了一下 WSL,竟然折腾好了,所以记一下

0. 升级到最新系统

第一步当然是各种升级,包括系统、WSL 和 node.js。我觉得这次之所以能成功,很大程度上得益于 Windows 10 的发展和 WSL 的升级。对了,Node.js v14 已经 LTS,建议一起升级。

sudo apt update
sudo apt install

# node.js 14
curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash - sudo apt-get install -y nodejs

# 在项目目录安装最新版 puppeteer
npm i puppeteer@5

1. 运行 puppeteer,查漏补缺

接下来随便运行一个包含了 puppeteer 的 JS,系统会提示缺少模块,但错误提示大概是找不到文件或者没有权限访问,存在一些迷惑性。仔细看错误提示,能找到缺少的模块名称,对我而言,大概是这些:

  • libnss3
  • libatk1.0-0
  • libatk-bridge2.0-0
  • libcups2
  • libxkbcommon0
  • libgtk-3-0

这里有个小技巧。错误提示可能是:找不到 libatk1.0.so.0,但是我用 sudo apt install libatk1.0 却无法安装,说找不到这个包(我之前就卡在这里)。此时,可以通过 apt search libatk 搜索,然后就能找到 libatk1.0-0 这个包,最后的 -0 对应的就是 .so.0,安装即可。其它包以此类推。

重复这个过程,直到所有包都安装成功。

2. 修改 JS,添加 --no-sandbox

继续运行,会报告另一个错误,大概意思是目前的版本存在各种问题,建议增加 --no-sandbox 选项。

修改 JS 文件,在 puppeteer.launch() 增加 --no-sandbox

const browser = puppeteer.launch({
  args: [
    '--no-sandbox',
  ],
});

3. 完成

现在 Puppeteer 应该可以正常工作了,不过因为缺少 UI 模块,所以暂时无法启动 headless: false,需要的话,还是用原生吧。

分类
测试

解决 mocha 测试时 `cannot use import statement outside a module` 错误,以及配置 travis

前些天同事突然发现一个库项目的测试无法运行,报错的内容大概是:cannot use import statement outside a module "should",即无法在模块外使用 import 导入内容。这个错误比较奇怪,简单 Google 之,基本上大家的解决方案都是使用 <script type="module"> 即在浏览器里用 ESM 加载 JS,这明显和我们的环境不同。

Node.js 当然也支持 ESM,不过这应该也不是问题症结。大体上我可以判断,因为测试集(JS 文件)用到 import 语法,而且用 mocha --require @babel/register 启动测试,所以应该是 Babel 没有正确转译导致的问题。

检查 Babel 的相关配置,发现同事为了能同时编译现代浏览器和 IE 两个版本的库,.babelrc 大概是这样的:

{
  "env": {
    "default": {
      "presets": [],
    },
    "withie": {
      "presets": [],
    }
  }
}

猜测 mocha 走了 default 分支,然后没有转译,所以出错。解决方案就是添加 node 分支,以当前 node 版本为 target,这样该转译就转译,不转译就用原生,性能更好,修改好的配置大概是这样的:

{
  "env": {
    "default": {"presets": []},
    "withie": {"presets": []},
    "node": {
      "presets": [
        [
          "@babel/preset-env",
          {
            "targets": {
              "node": "current"
            },
            "useBuiltIns": false
          }
        ]
      ]
    }
  }
}

使用时,需要增加环境变量用来切换配置:BABEL_ENV=node mocha --require @babel/register。比较奇怪的是,其他脚本使用 BROWSERSLIST_ENV 切换,这里只能使用 BABEL_ENV,我暂时不知道为什么。

修改之后的脚本就可以正常测试了。接下来我打算给它加上 Travis,这样就能自动 lint + 测试,比较方便控制质量。加 Travis 很简单,拷过来一个 .travis.yml 改吧改吧就行了,但是第一次运行失败了,而且是超时。经过研究,原来 mocha 从 v4 开始,完成测试后不会自动退出,除非手动指定,方法是增加 --exit 参数。

所以最终的测试脚本是(其它配置略去):

{
  "scripts": {
    "test": "BABEL_ENV=node mocha --require @babel/register --exit",
  }
}

最终 Travis 配置是:

sudo: required
  dist: trusty
 

  language: node_js
  node_js:
    - 14
 

  branches:
    only:
      - master
 

  cache:
    directories:
      - ~/.npm # cache npm's cache
      - ~/npm # cache latest npm
 

  install:
    - npm ci
 

  script:
    - npm run lint
    - npm run test
分类
php

PHP 8.0 发布——JIT 到来,性能大幅提升,一堆语法糖

早上起来,得知 PHP 8 正式发布了,作为曾经的半个 PHP 程序员,当然要去看一看。官方的 Release note 在这里,建议做 PHP 开发的各位同学都看一看:

PHP 8 Released!

接下来聊聊我的想法。

JIT

作为一个大版本,PHP 8 一定要有一些非常大的变化,JIT 就是这个非常大的变化。

JIT 是 just-in-time 的简写,意思是在运行时将部分代码编译成机器码,以便反复使用。执行机器码的速度会比执行一般的解释型代码快很多,所以 JIT 通常意味着可以大大提升语言的运行速度。

PHP 8 引入了两种 JIT 编译引擎,Tracing JIT 和 Function JIT,其中最值得期待的是 Tracing JIT。在基准测试中,速度有 3 倍提升;在一些长时间运行的应用当中,也有 1.5~2 倍的提升。参考下图,可惜,这个基于 WordPress 的博客提升只有一倍。

PHP 8 JIT 性能表现
PHP 8 JIT 性能表现

所有的功能都要以性能为基础,PHP 从 v7 开始就很努力地提升性能,加上它的功能一直封装的很好,所以我一直觉得 PHP 是服务器端开发最好的语言。

一堆语法糖

不知道是不是受了同为 Web 开发语言的 JS 的影响,v7 之后的 PHP 非常放飞,每个版本都引入一堆新语法和语法糖,什么箭头函数、类型系统,基本上只要有用,都给加上。v8 也不例外,有一些语法已经到了我看不懂的程度了……

比如这个 Attributes,我就没太明白,暂时把它理解成装饰器:

// PHP 7
class PostsController
{
    /**
     * @Route("/api/posts/{id}", methods={"GET"})
     */
    public function get($id) { /* ... */ }
}

// PHP 8
class PostsController
{
    #[Route("/api/posts/{id}", methods: ["GET"])]
    public function get($id) { /* ... */ }
}

Laravel

顺便说一下,前些日子 Laravel 也发布了 v8。不过不太一样的地方是,Laravel 的版本号跟 node.js、Ubuntu 采用同一种策略,即每年一个大版本,只有偶数版本会长期支持(LTS)。

总结

活到老学到老,大家加油。

分类
技术

设计并实现自动生成前端编程教学视频的小语言(一 想法篇)

我一直想把录制教学视频的过程变得更稳定可控,而不需要依赖一时的状态。后者常常受到各种影响:比如家里狗叫了、孩子闹了、邻居装修了;或者录到一半突然遇到调不通的 bug;又或者只有 10分钟想录一段,但找不到感觉;等等。

好处

加入我厂后,见识到各种小语言的威力;另一方面,计算机语言经过祛魅,我也不觉得有多难实现。所以我希望能够把录制教学视频的过程语言化,这样会带来几个好处:

  1. 想写就写:哪怕只有几分钟,写上一个小节,或者修改几个错字,都可以;
  2. 想录就录:直接在服务器上生成,不需要考虑周围环境;
  3. 方便多语言:文字翻译后,重录生成其它语言即可;
  4. 方便修改和升级:大部分错误都是口误,或者细节出入,从头录必然不合适,目前来看大部分视频都通过字幕处理。而语言重新录制一遍即可;想补充内容,也很容易,尤其是 API 或者最佳实践变化,需要修改大小 N 处,从语言生成就更具优势。

技术环境与选择

能够实现这个小语言,自然需要依靠整个技术环境的成熟与健全。大概有这么几项:

语音合成

语音合成(TTS)经过 AI 加成,效果相较于过去提升不少,足够为用户接受。再加上难度不大,所以支持的平台很多,价格也不贵,随便选一家即可。

录屏

首先可以选择 OBS。除了 UI 之外,它也提供 API,不过语言只有 Python 或 Lua,我都不是很熟。用它的好处是支持场景配置,我们可以先配好几组场景,然后在需要的地方切换。

如果是 macOS 可以使用 aperture-node,它借助 macOS 的原生 API 实现录屏,性能非常好。不过兼容性不行,考虑到新推出的 M1 芯片性能好功耗低,买一台 Mac mini 专门用来跑生成也不错。

也可以选择 ffmpeg,好处是什么平台都能跑,坏处是什么平台都一般。

效果演示

有 webpack-dev-server 在,效果演示不成问题。

代码编写

目前最大的挑战就在这里——我还不太确定怎么实现代码的自动输入。初步考虑使用 AppleScript 配合 VSCode,如果不行的话就浏览器里跑 VSCode online,然后用 JS。

基础设计

  1. 既然要做教学视频(tutorial),我又很喜欢东南亚,那么语言就叫 tutolang 吧,tuto = 拖拖车,便宜又方便。
  2. 因为最终的目的是生成视频,所以它应该是个声明式语言
  3. 语言教程不能只从上到下顺代码,得能够找到特定位置输入代码然后讲解。这个部分考虑再三之后通过 git 来做最为简单直接。
  4. 目前来看,从前端三个语言的角度生成视频应该是问题不大,后面如何整合其它视频过程需要再考虑。

其它

语言本身肯定要开源,编译器计划也直接 MIT,随便用。然后提供一组自动化的编辑、生成、转码的基础设施,作为服务收费。

分类
前端工具链

解决“Error: Rule can only have one resource source (provided resource and test + include + exclude)”

又有一台服务器到期,不想续了,所以把东西往另一台服务器上搬。其中有一个小服务,用来存储 CI 测试失败的截图。里面有用到 font-awesome,但只用一个图标,太浪费。所以这次就顺手换成了 bootstrap-icons,然后顺便更新依赖,结果,再编译的时候,就报错:

ERROR  Error: Rule can only have one resource source (provided resource and test + include + exclude) in {
   "exclude": [
     null
   ],
   "use": [
     {
       "loader": "/Users/meathill/Projects/mini-store-admin/node_modules/@vue/cli-plugin-babel/node_modules/cache-loader/dist/cjs.js",
       "options": {
         "cacheDirectory": "/Users/meathill/Projects/mini-store-admin/node_modules/.cache/babel-loader",
         "cacheIdentifier": "219fb45a"
       },
       "ident": "clonedRuleSet-38[0].rules[0].use[0]"
     },
     {
       "loader": "/Users/meathill/Projects/mini-store-admin/node_modules/babel-loader/lib/index.js",
       "options": "undefined",
       "ident": "undefined"
     }
   ]
 }

非常诡异,按照错误栈点进去,从代码可以推断是生成的 webpack config 有问题,在 babel-loader 配置里既包含 resource 又包含 exclude。但是,这个项目是通过 @vue/cli 创建的,所以它的配置也是 @vue/cli-service 自动生成的,我并没有修改过。而且只有这个项目有问题,其它项目,同样使用 @vue/cli 创建,但是没有更新依赖,就没问题。

Google 之,有一些关于这个错误的讨论,但几乎都是用户自己写 webpack.config.js 没写好出问题。有人建议 rm -rf node_modules & rm package-lock.json & npm i,即重装所有依赖,我试了两次,也不行。

开始尝试、推翻、再尝试、再推翻。后来怀疑到 webpack ,在 package-lock.json 里查找之,发现安装的版本竟然是 5.1.0,而没有更新过依赖,可以正常编译的项目里都是 4.x。那基本可以确认了。

  1. 先删掉 node_modulespackage-lock.json
  2. 手动在 package.jsondevDependencies 里添加 "webpack": "^4.44.2"
  3. 重新安装全部依赖: npm i
  4. 尝试编译,npm run build,发现问题解决

总结

我猜问题是这样的:某些新版本的库要求 webpack@5,更新依赖时,根据依赖选择的规则,就以 webpack@5 作为主依赖安装。然而 @vue/cli 依赖 webpack@4,它自带的 webpack 配置无法兼容 webpack@5 ,于是就报错,不能继续编译。如果你也在使用 @vue/cli,那么请不要贸然升级 webpack@5。

分类
职业

改变你的环境,或者选择适合你的环境——聊聊“被”管理

群里有同学在抱怨,大意是:领导技术还没自己好;团队瞎搞领导也不管;老板不懂技术,天天催着赶活儿,不给 code review 等内训的时间;感受不到成长;等等。

坦率地说,年轻的我也有这种想法,而且很重(可惜我 2011 年之前的博客遗失了,不然可以翻出来给大家晒晒)。不过随着年龄愈大,尤其是参与创业的这些年,学会站在不同角度看问题,换用不同的思维模式之后,我的想法变了。

年轻的时候,我非常喜欢《倚天屠龙记》开篇何足道挑战少林寺那一段:一位猛人,来到不可一世的少林寺,打遍少林无敌手,少林寺高层一筹莫展。突然出现一位不在编的僧人,把他锤跑了。

换成现实世界,就是:我在公司是个普普通通的开发人员,除了前后桌,根本没人知道我是谁。突然有一天公司遇到重大技术难题,无人可解。这个时候我潇洒的戴上假发,上去把问题解决了。老板痛哭流涕,把前任总监就地免职,任命我为新的 CTO。

然则这并不会真的发生。现实世界里,不管你再看不起你公司(下面简称贵司)和你公司的技术,它会被一项技术拖死的可能性也微乎其微。它能够活着,多半靠的是你眼里不咋样的老板所赐。

决定你在公司地位的,大部分时间也不是技术能力。你的技术领导,可能来得比你早,刚好排到这个位置。虽然他技术不如你(只是可能),但是对于公司来说,够用了。老板不懂技术,不可能把他挪走把你放上去。

至于其它能力,比如情商、沟通能力、业务理解能力,其实也都和技术差不多。有优先级之分,但是都没有一票赞成/否决权。

所以选公司、选团队的时候,要先看行业、商业模式,这种赛道型的内容。比如我厂,做的是非常高端的 2B 软件,属于“有的公司自己搞不定,要花钱请外面的高人来搞定”这种需求。所以对技术的要求就很高,相应的,技术人员的天花板就很高。

如果你喜欢技术,就最好来这种公司,因为你的技术可以很容易的折现,并且上不封顶(理论上……)

相反,如果你不是特别喜欢技术,写代码更多是为了谋生,只是恰巧选择了程序员这份工作。那就应该选择一个模式相对基础,公司比较稳定,技术在其中主要做支持的公司。比如我的第一家公司 201——IT 资讯门户,对技术的要求是网页能打开,不要挂。主要挣钱手段是养编辑写文章,养销售卖广告。

在这种公司里你可以每年进步一点点,但工作稳定有保障。如果你有技术梦想,来到 201,然后发现领导的技术不如你,要求老板换你上。那不行,是你不对,你没有理解公司的商业模式。


总结一下,就是:尽量找到合适你的公司。老板不傻,领导不笨,大多数时候只是你们不合适。

分类
js

JavaScript 获取正则表达式中子表达式的个数

正如标题所示,我厂有这么一个需求。我不会,老板鄙视我后丢过来一个链接:stackoverflow: Count the capture groups in a qr regex?

看不太懂 Perl,但是这个思路很棒。所以改写成 JS 版,并记录如下:

function countCapturingGroups(r){
  r = new RegExp(`|${r.source}`);
  const result = ''.match(r);
  return result.length - 1;
}

const result = countCapturingGroups(/fo(.)b(..)/);
console.log(result); // 2

它的原理是这样的。构建一个新正则,包含两部分:空字符和目标正则。空字符正则会完成与目标字符串的匹配,保证有结果(不然的话就会返回 null。接下来 | 会保证后面的正则也是有效的,可以生成包含子表达式结果的数组。

我们知道,结果是个类数组,结构大约是:

  1. 全部匹配字符串
  2. 0~N 子表达式结果
  3. 其它一些属性

所以用其长度 – 1 就能获得子表达式的个数。从功耗上来说,这个应该是很节省了。

分类
vue

升级 Vue@2 项目到 Vue@3

这篇主要是笔记。(我估计会是第一篇,因为只迁移了一个项目)

1. 安装新包

只记录必须重装的:

npm i vue@3 vue-loader@16.0.0-beta.8 vue-router@4.0.0-beta.13 @vue/compiler-sfc

2. 修改 Webpack 配置

// v2
const VueLoaderPlugin = require('vue-loader/lib/plugin');
// v3
const {VueLoaderPlugin} = require('vue-loader');

// for DefinePlugin
{
  plugins: [
    new DefinePlugin({
      __VUE_OPTIONS_API__: true,
      __VUE_PROD_DEVTOOLS__: false,
    }),
  ],
}

3. 修改入口文件

没有 new Vue({}) 了,取而代之的是 Vue.createApp({}),后者还支持 tree-shaking。

也不需要注册 Vue-router 了,直接 app.use(router) 就好。所以传统的入口文件就要修改为:

// v2
import Vue from 'vue';
import VueRouter from 'vue-router';
import App from './app';
import 'bootstrap/dist/css/bootstrap.min.css';
import '@/styl/index.styl';
import router from './router';

Vue.use(VueRouter);

Vue.config.productionTip = false;

new Vue({
  router,
  ...App,
}).$mount('#app');

// v3
import {createApp} from 'vue';
import App from './app';
import 'bootstrap/dist/css/bootstrap.min.css';
import '@/styl/index.styl';
import router from './router';

const app = createApp({
  ...App,
});
app.use(router);
app.mount('#app');

4. 修改 router

Vue-router 的变化很大,建议大家好好看看 迁移手册。就我厂这个项目而言,主要是三个变化:

  1. 使用支持 tree-shaking 的函数 createRouter
  2. 修改 history: createWebHistory()
  3. 使用渲染函数 h 替换之前渲染方式
// 加载方式
import {h} from 'vue';
import {
  createRouter,
  createWebHistory,
  createWebHashHistory,
  RouterView,
} from 'vue-router';

const routes = [
  {
    path: '/',
    name: 'home',
    component: {
      // vue-router v3
      render(createElement) {
        return createElement('router-view');
      }

      // vue-router v4
      render() {
        return h(RouterView);
      },
    },
    children: components,
  },
  // ....
];

const router = createRouter({
  // vue-router v3
  mode: process.env.NODE_ENV === 'production' ? 'history' : 'hash',
  // vue-router v4
  history: process.env.NODE_ENV === 'production'
    ? createWebHistory()
    : createWebHashHistory(),
  scrollBehavior: (to) => {
    if (to.hash && !/^#/.test(to.hash)) {
      return {selector: to.hash};
    }
    // 这里有个小改动,x => left, y => top,简单提一下
    return {top: 0};
  },
  routes,
});

5. 自定义组件 v-model 修改

  • prop: value => modelValue
  • event: input => `update:modelValue`

6. 一些小修改

  • beforeDestroy => beforeUnmount

7. createApp 与 Application,与 Component

v2 时,我们可以通过 new Vue({}) 初始化 Vue 实例。这个阶段,Vue 默认有一个全局对象 + 若干个实例,除了 local 的,就是全局的。

v3 时,引入了 Application(应用)的概念,在全局和组件之间,增加了一个新的层级。这样一来,我们就可以在同一个 Web 产品中,使用 Application 来划分命令、组件、mixins 的范围。应该会增加代码的强壮程度(虽然我暂时还没用到)。

不过,迁移代码的时候,也要注意。以前我们可能 new 一个实例,调用它的 methods;现在不行了,要这样做:

// v2
const ins = new Vue({});
ins.doSomething();

// v3
const app = createApp({});
const vm = app.mount('$el');
vm.doSomething();

8. 新的响应式 API

v3 最大的变化就是重构了响应式实现,所以新增了不少响应式 API。同时,也会检查开发者的代码,如果发现不需要响应式的地方用到响应式对象,就会提示开发者,因为响应式会增加系统开销。

这个时候可以用 markRawtoRaw 方法来修改对象,撤销之前附加在上面的响应式属性,提高访问效率。

其它 API 还很多,后面慢慢更新吧。

9. Devtool 和 SourceMap

遗憾的是,目前 Vue Devtool 无法检测到 Vue。老项目的 SourceMap 也完全不生效,无法正常对 SFC 进行 debug。