分类
技术

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

我一直想把录制教学视频的过程变得更稳定可控,而不需要依赖一时的状态。后者常常受到各种影响:比如家里狗叫了、孩子闹了、邻居装修了;或者录到一半突然遇到调不通的 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 作为主依赖安装,然后 webpack@5 对配置的要求和 webpack@4 不一样,所以就报错,不能继续编译。

分类
职业

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

群里有同学在抱怨,大意是:领导技术还没自己好;团队瞎搞领导也不管;老板不懂技术,天天催着赶活儿,不给 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。

分类
前端工具链 技术

初试 Webpack 5,解决合并 CSS 时多余的 JS

在长文分享《GitChat: 使用 Webpack 开发企业官网》中,我使用 Webpack 重构了官网开发工具链,效果很好,现在的开发效率也很高。

当时遗留下来一个问题:

  1. 我会把所有页面的 CSS 打包到一起。因为 CSS 尺寸不大,打包到一起利用浏览器缓存,可以改善后续页面的打开速度。
  2. 打包+抽取 CSS+合并,使用 mini-css-extract-plugin,配合 splitChunks。这里遵循的是 官方文档
  3. 打包完成后,除了包含所有样式的 screen.css,还会生成 screen.js,里面包含 webpack 模块的初始化代码,只有一行,几十个字节,但是很重要,必须在其它模块前先完成加载。

于是我就面临一个选择:

  1. 留着这个东西,会略微影响网页打开速度。因为它毕竟是个 JS 文件,要单独加载和运行。
  2. 想办法去掉它,比如利用 html-webpack-plugin 提供的各种钩子,写个插件,把它的内容提取出来,放进 html 或者跟 app.js 合并,等等。但是这个时机很难找,我尝试过一段时间,无果。

最后综合考虑成本和收益,以及看到这个 issue:https://github.com/webpack-contrib/mini-css-extract-plugin/issues/151 之后,我决定再等等,webpack 5 的时候再说。

最近 Webpack 5 终于发布了,我就来试试升级解决这个问题。

升级 Webpack 5

首先升级 Webpack@5 和 webpack-cli@4。新版本虽然底层变化很大,API 层面还是尽量保持了兼容性,所以配置文件可以不改或者小改(理论上)……果然编译就报错了,错误信息很扯:Error: Can't resolve './src',非常莫名其妙。Google 半天无果,后来我突然想起,假如 Webpack 配置信息没加载成功,那么它会默认使用 ./src/index.js 作为入口文件,是不是这个问题呢?

然后搜了半天,没有 dump configuration 的选项,只好仔细看文档。迁移指导里并没有关于配置文件的内容。那就看文档,然后发现,之前 Webpack@4,配置文件可以输出 Object、Promise、或者函数,所以项目的配置文件就是直接输出的 Promise;如今,虽然标题仍然是“Exporting a Promise”,但实际上 module.exports 导出的是函数,只是函数的返回值可以是 Promise。

这里嫌疑很大,所以我就试着改了一下,用函数包裹 Promise,果然可以了。

移除不必要的 screen.js

我会把所有样式都打包到 screen.css 里,这是从 HTML5 boilerplate 学到的。然后 Webpack 就会生成 screen.js,这是个多余的文件,只有坏处,前面说过。

我以为 Webpack@5 之后这个问题就自然解决了,结果看了一眼,还有,只好继续寻找解决方案。之前的关键词忘记了,issue 也没关注,所以多花了不少时间。所幸最终找到了:https://github.com/webpack/webpack/issues/7300#issuecomment-702840962

  1. 升级 webpack@5,webpack-cli@4,自不用说
  2. 升级 mini-css-extract-plugin@1,新版本包含了需要的补丁
  3. 如下修改代码
{
  optimization: {
    splitChunks: {
      cacheGroups: {
        styles: {
          type: 'css/mini-extract', // 重点在这里
          chunks: 'all',
          // If you need this uncomment
          // enforce: true,
        },
      },
    },
  },
}

然后重新编译,screen.js 就不见了,非常爽。

分类
前端

Webpack 5 发布,Chrome 86 开始支持本地文件系统

早上起来日常刷技术新闻,看到两个对我和我厂影响比较大的消息,简单写几句。

Webpack 5 发布

Webpack 5 于今天(北京时间10月11日,美国时间10月10日)发布。这是个大版本更新,会有很多破坏性变更。所以不要轻易升级,否则可能遭遇各种问题。不过,对于像我这种三天两头主动升级依赖的人来说,是否可以平滑升级还未为可知。

这次的升级主要有以下几点变化:

  • 使用持久缓存提升性能
  • 通过替换更好的算法和缺省值,提升长期缓存的效果
  • 使用更好的 Tree Shaking 算法和代码生成方式,减少打包后的提及
  • 提升 web 平台的兼容性
  • 在不引入破坏性变更的前提下,清理掉为实现 v4 功能而遗留下的奇怪的内部架构
  • 现在的破坏性变更是为将来实现更多的功能打好基础,让我们可以在 v5 版停留尽可能长的时间
  • (我加的)多页面 css 合并的时候,不再需要 all.js

更详细的内容大家请移步官网了解:Webpack 5 release (2020-10-10)。英语苦手的同学稍微等两天,估计中译版和各种解读版也会很快问世。

我只有两个建议:

  1. 尽快升级你的项目到 webpack 5
  2. 不要再学/用以前的版本了

Chrome 86 开始支持本地文件系统

很久很久以前,我在上一家公司做创业项目肉大师(Web 创作工具),不小心踏入了 FileSystem API 这个大坑。还留下一篇长文《HTML5的File API应用》,可能是为数不多的中文资料。

时隔八年,如今文件系统 API 终于有了比较靠谱的实现,并且被 Chrome 正式支持。一般来说,Chrome 的统治地位会帮助这个 API 成为事实标准,所以如果你对操作本地文件有需求,那么就可以开始使用这个 API 了,将来它会慢慢普及到其它浏览器上。

简单来说,这个 API 允许用户选择若干文件或者目录,相当于用户主动授权某些文件或目录给当前网站,然后 JS 就可以从文件里读内容,或者把内容写入文件。当然,从安全角度出发,网页不能任意访问文件,一定要用户主动选择。

对于我厂来说,这意味着 QA 产品可以更容易的编辑、保存文件,可以大大提升用户体验。一些 Web 工具也可以直接保存内容到用户本地,感觉网页生态更强大了。

更详细的内容可以阅读这两篇文章:

总结

学无止境,勤为径苦作舟吧。

分类
工具

使用 site-validator 验证网站链接

网站死链的问题比较常见,坏处就不多说了,大家应该都明白。想解决这个问题并不容易,主要难点在发现死链。如果是纯静态网站就比较简单,直接查服务器的 404 日志即可;如果是动态的,比如 SPA,就比较难搞。

所以我做了这个项目:site-validator。其实非常简单,用 puppeteer 访问网站,找出所有链接,一级一级点下去,直到把一个网站的链接都点一遍为止。记录下所有的死链,输出,然后排查。

代码简单,但是确实很实用,帮我厂找到不少死链,很好的提升了搜索表现。将来考虑再加一些新功能进去,比如集成 lighthouse 审计之类的。有兴趣的同学可以下载使用,也欢迎提意见和 issue 哦。

分类
招聘

代友招聘:广州-移动(Android)开发-网赚产品

广州奇异果,需要移动开发一枚,做网赚类产品,主攻 Android,能同时搞定 iOS 最好。有意者请联系我,谢谢。

下面是 JD:

工作职责:

负责 Android 平台的软件产品的开发,对整个单独 app 项目负责

任职资格:

  • 3 年以上 Android 开发经验,具有多个成熟产品开发经验,能独立承担 Android 产品开发工作,有互联网行业工作经验优先考虑;
  • 熟悉 Android OS 体系结构,熟悉 Android SDK,熟悉 Android 常见应用实现机制,对 Android 应用结构有深刻的认识;
  • 熟悉 MVC 模式,熟悉跨进程应用交互方式;
  • 丰富的手机 UI 设计经验,熟悉网络编程、多线程、图形界面编程、熟悉 TCP/UDP、HTTP 协议;
  • 了解 Linux 基本命令,了解 NDK 基础;
  • 思路清晰,善于思考,能独立分析和解决问题;责任心强,具备良好的团队合作精神和承受压力的能力;
  • 熟悉软件工程,具有良好的代码编写规范和书写文档的习惯;
  • 有很强的自学能力,喜欢钻研技术。
分类
pouchdb

PouchDB 使用笔记

0. 缘由

我厂的 QA 产品是一个比较重的单页应用,支持用户在本地维护测试用例、执行测试、记录结果,所以需要相对比较复杂的数据存储支持。

首先排除 cookie;localStorage 因为空间有限,也排除了。FileSystem API 不知道目前是个什么状态,不过似乎很少听到它的消息,多半好不到哪儿去,而且明显数据库更适合我,所以最后选定了 IndexedDB

IndexedDB 可以使用不超 1/3 的硬盘空间,大部分场合应该都足够用了。不过 IndexedDB 的 API 层级相对较低,换言之,就是对普通开发者而言,不好用。所以,我选择之前听说过的 PouchDB 作为功能层,提升开发效率。

1. PouchDB 核心功能

首先是存储数取。PouchDB 使用 key-value 存储,然后在上面提供一层类 SQL 查询封装。这套架构已经被 Google 证明性能最好、扩展性最强,比起传统的关系数据库,比如 MySQL,它更容易横向扩展,天生分布式(参考自 Teahour 90 – 和 PingCAP CTO 畅谈数据库和编程语言)。

它的存储结构很像 LeanCloud,每次存储都会生成一个对象,对象里有若干属性。开发中可以很容易的增删属性,表的概念和列的概念在这里并不突出。

下一个重点是同步。PouchDB 完全兼容 CouchDB 协议,而 CouchDB 设计成可以很方便地把数据从一个实例同步到另一个实例。所以我本地使用了 PouchDB,将来就可以利用一个中心仓库在几个不同的实例之间同步代码,或者在本地的几个不同浏览器之间同步代码。

PouchDB 还提供版本管理功能,这点和我之前用过的其它数据库都不太一样。当我们修改对象时,就会生成一个新的版本,还可以回退到之前的版本。这个功能的核心场景我暂时还没太想出来,不过考虑到我们的产品是测试用例编辑器,似乎这个版本管理还的确可以派上些用场。

2. 实例

考虑到将来筛选(query)的需求,每个 PouchDB 实例应该对应一个 MySQL 表,只存放一类内容。

比如 QA,那么就是一个 pouch 实例存储测试用例,一个存储日志。测试用例可能要保留 5+ 个版本历史,日志就不需要版本历史。还有一些别的存储需求,比如测试用例所属的项目(类似打 tag),因为结构非常简单,我考虑用 localStorage 直接存,就不走 PouchDB 了。

3. 同步

PouchDB 实例之间的同步非常简单,只需要一行代码:

const db = new PouchDB('mydb');
// 假设本机启动了 pouchdb-server,端口是 5984
// live 表示当本地 db 发生变化时,自动推送变化到 server
db.replicate.to('http://localhost:5984', {live: true});

// 还可以使用 .sync() 确保 server 的数据能回到本地
// 这对开发过程中修改数据非常有用
db.sync(remoteDB, {live: true});

更完整的文档参考:Replication

4. 最佳实践

官方有一个最佳实践指导,刚开始用,很多地方不是很能理解,不过多看几遍总有好处:12 pro tips for better code with PouchDB

5. 官方文档

链接在此,建议认真阅读。