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

分类
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,随便用。然后提供一组自动化的编辑、生成、转码的基础设施,作为服务收费。

分类
前端工具链 技术

初试 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 工具也可以直接保存内容到用户本地,感觉网页生态更强大了。

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

总结

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

分类
前端

Boostrap 发布图表库 Bootrap Icons 1.0,该准备切换到 SVG 了

Bootstrap 团队一边开发 Bootstrap 5,一边发布了 Bootstrap Icons 1.0(以下简称 BI)。官方全文见此:Bootstrap Icons v1.0.0。这个版本里包含 1100 个图标,涵盖范围很广,全免费(MIT),可以自由使用、修改。

真正引起我注意的是,这个仓库不再使用 Webfont,而是全部使用 SVG。官方文档 介绍了三种使用方式:

  1. 直接嵌入 SVG 代码
  2. <svg> 中引用 BI 库并配合 <use> 使用
  3. 通过 bootstrap-icons/icons/*.svg 直接使用

如果你非常怀念 Webfont 方式,可以用(3)结合 CSS background-image 的方式近似的模拟。

其实,GitHub 很早以前就开始从 Webfont(Icon font) 向 SVG 切换了,他们还特地写了篇博客解释这件事情:Delivering Octicons with SVG,在里面列举了六个理由:

  1. Icon font 本身存在一些渲染问题,Webkit 内核的浏览器会使其边缘模糊,不利于辨识
  2. Icon font 一般会延后加载,而字体在完成加载之前无法判定大小,所以加载完成之后,需要重新计算布局并且重新进行渲染,造成不必要的性能损耗
  3. 可用性。参考这篇 Slide:Death to Icon Fonts,每十个人当中就有一个人存在阅读障碍,他们很难使用普通字体,必须使用专为他们设计的特殊字体,而这个时候,icon font 就会变成没有意义的方块。对使用阅读器的人更甚:icon font 是一种 hack,通常来说,我们并不会真的写文字进去,所以在他们看起来,图标会变得完全不可读。
  4. 合适的图标尺寸
  5. 方便重构字体文件
  6. 方便制作动画

所以,如今 Bootstrap 放弃 webfont,使用 SVG 作为主要格式,应该说是大势所趋。这几年使用 Webfont,的确很方便,给开发带来很大的效率提升,但与此同时,也存在不少问题:

  1. fontawesome 体积越来越大,现在一组字体要 1M+,加载很耗时,但是拆分非常困难,即使只用几个图标,也需要加载整个字体库
  2. 无法对图标进行稍微复杂的操作,比如双色,或者特定部位的动画
  3. 放大之后,图标比较丑,也不好加工
  4. 前面说过的那些问题

所以未来我和我厂也要慢慢从 webfont 切回 SVG。这样也有助于提升我厂产品的可用性。其实,互联网行业的可用性一直都做得不错,软件开发目前也算是残障人士最有机会获得体面收入的机会,所以,我辈仍需努力呀。

分类
前端

零宽空格的问题与使用

今天有同学在群里提问:

“我的请求路径是这样的”
“路径上面自动多加东西了,好奇怪”

我的第一反应是“零宽空格”,然后该同学试着手动敲了一遍 url 地址,问题果然解决了。看起来,就是因为原来的 URL 里含有零宽空格,直接复制过来,虽然看起来没问题,但是在发起请求时,非标准字符被 encode 之后就出错了。

我们姑且不管为啥他的复制来源里会有零宽空格,聊一聊什么是“零宽空格”,以及“零宽空格”能干什么。

零宽空格定义

零宽空格是空格的一种空格,但是它的宽度为零,即不显示,所以看起来跟没有一样。我们可以在浏览器里启动开发者工具,然后切换到 Console 面板,输入以下代码:

> a = '\u200b'; // 即零宽空格
"" // 其实是有内容的,只是看不到
> a.length
1 // 长度为 1,说明有东西
> encodeURIComponent(a)
"%E2%80%8B" // encode 之后跟截图里一样,破案了

维基百科的解释:

零宽空格(zero-width space, ZWSP)是一种不可打印的Unicode字符,用于可能需要换行处。

它的用法:

在HTML页面中,零宽空格可以替代 <wbr>。但是在一些网页浏览器(例如 Internet Explorer的版本6或以下)不支持零宽空格的功能。

MDN 上没有零宽空格的定义,但是有 <wbr> 的内容。之前我也写过一篇文章:使用 <wbr> 解决长 URL 的换行问题,里面介绍了 HTML 换行算法,以及我选择 <wbr> 的思路,建议大家看一下。

用途一:在特定位置换行

比如一首古诗:

锄禾日当午,汗滴禾下土。谁知盘中餐,粒粒皆辛苦。

我们有时候会希望:

  1. 在宽度足够的时候,放一行
  2. 如果宽度不够,就在标点符号处换行

这个时候,我们可以先设置这段文字 word-break: keep-all,避免在汉字后断句换行;然后在每个标点后面加上零宽空格,这样,一行的时候就不会看到奇怪的空格,而宽度不够的时候,又能根据 white-space 属性正常换行。

用途二:特殊标记

我厂有一个产品,要输出大量日志,包含大量数字。为方便阅读,需要给数字添加千位分隔符;为了方便复制,又希望剪贴板上的是纯数字,不要千位分隔符;但是如果本来就是千位分隔符的,比如在别的软件里格式化的数字,就原样复制,不需要去掉千位分隔符。

这个时候就可以用到零宽空格。我先找出来足够长的数字,然后添加千位分隔符,然后在两头加上零宽空格。这样在用户眼里,看到的是千位分隔过的数字;等他们复制的时候,我就检查两端的零宽空格,如果有的话,就复原数字;如果没有的话,就原样返回。

其它零宽字符

除了零宽空格之外,还有很多零宽字符,可以用来在页面中加入特殊标记,或者实现一些控制功能。大家如果发现 url encode 之后的内容和之前肉眼看到的不符,那么多半是存在零宽字符,可以试着干掉它们,多半问题就能解决。