阅读视图

发现新文章,点击刷新页面。

尝试升级 WordPress 版本 和 PHP 版本,然后失败了

尝试把 WordPress 从 5.2 版本 升级到 6.6 版本,把 PHP 从 7.4 版本升级到 8.3 版本。

任务艰巨得完成不了。

非常的崩溃。


前提

我现在的线上的博客,以及线下的开发环境,以及(如果还在不为我知的某个角落存在的话)商用用环境,都是 PHP 7.4 的。

最早些时候,应该算是2015年,正式入坑 PHP ,那时候基本是公司用啥,我用啥。PHP 5.2 5.4 5.6 都用过了,尤其是 5.2 大坑一大堆,摔过很多次。然后到 2016 年的时候终于用上了革新的版本 PHP 7.0。真爽。再后来开始自己独立开发环境,直接开了 DAMP 的坑,最初也是用的 7.0 版本,然后就很随意的升到 7.3 版本,主要是当时开发负担少。然后博客站也都升级到了 PHP 7.3。

再后来 2021 年接了一个外包项目。虽然当时 PHP 8.0 已经发布了,但是貌似周边支持都不怎么地。为了保证开发速度,直接用了 PHP 7.4 版本。当时还涉及到前端开发,Node环境也是大更新,很早之前一直用的各种热门前端库基本都死绝了。博客和本地开发环境也都升级到了 PHP 7.4,博客的 WordPress 程序倒是没变,还是 5.2 版本(这里埋了个大坑),只是偶尔会从官网下载代码包然后手动更新对应的文件,以修复些潜在的安全问题。

现在是2024年年末,就连PHP 8.0 都已经停止维护快满一年了,最新版本 PHP 8.3 也已经发布快满一年了。WordPress 都到了 6.6 版本了,一堆 WordPress 插件都已经停止 5.2 版本的支持了。

想趁着有时间有机会,把 PHP 和 WordPress 都升级了。

天真了。


第一天

首先是把线上的代码全备份一遍。这个毫无工作量,我之前写了个备份代码,直接就把自己的整站扒下来。然后把备份的站再部署到本地的开发环境上,再改几个数据库字段,就完事了。打开后台,先禁用所有插件,以便升级之需。

Apache2 和 MySQL 不用动,因为用的一直都是 Docker 的最新版本,这几年也没什么巨大的兼容性变化。

PHP 这块我不知道算不算麻烦。我把 PHP 从 7.4 到 8.3 的所有 不向后兼容变更废弃功能 全都看了一遍。没多少,就几页,几分钟就看完了。个人感觉这么多变更,只有一条能实际影响到我日常开发工作,就是自 8.0 起不再支持 带有默认值的参数后面跟着一个必要的参数

screenshot_on_b85m_by_flameshot_at_2024-10-24_22-51-45.png

其实个人工作中也很少这么用,因为易读性有点烂。论性能来讲的话这么写性能也很差,只不过以前工作过的公司里就有很多人这么写,主要是为了防止其他同事调用函数忘记传参(空参也是参)。

其余的改动基本影响不到我。我使用的基本都是 PHP 的官方建议用法,最多也就是会遇到某些外部库被遗弃然后有个平替的情况,比如数据库接口啥的,即使出错了立刻就能发现。

于是很自信的先把 PHP 环境升级到了 8.3 。因为是基于 Docker 的所以也完全不用担心环境污染的问题。

然后就崩了,WordPress 就打不开了。

这倒是意料之中,毕竟当年 WordPress 4.3 版本当年连 PHP 7.0 都不支持。我现在用的是 5.2 版本,最高能支持的 PHP 版本也才 7.3 ……

Screenshot_2024-10-24_at_23-02-46.png

什么?7.3?但我已经用 PHP 7.4 跑了 WordPress 满 3 年了啊???

什么兼容性测试……

下载了个 wordpress-6.6.2 的包,按官方文档手动安装。 崩得一塌糊涂

全部删除了重新来,这回用官方的自动升级功能,直接从 5.2 升级 6.6。真神奇,WordPress 官网被墙了这么久了竟然能秒下 WordPress 的安装包,我也不知道他是走的什么渠道。PHP 就这点恶心,前台所有操作你都看不到任何细节,就像是在用 Windows 一样。

成功安装。然后 崩得一塌糊涂 。这回不仅崩得稀烂,而且由于没手工删除后台的旧文件,新旧文件混在一起,更是手足无措。

就这么搞了超过6个小时,一直干到后半夜三点多,搞不定。

放弃,睡觉。

然后严重失眠,抽搐。

screen_IMG_2148.png


第二天 白天

首先考虑下到底是 PHP 的问题还是 WordPress 的问题。

按理来讲我已经把 PHP 的升级文档都看完了,并没有什么会天塌一般的变更,但是 WordPress 这边的确天塌了。

先在 PHP 8.3 环境下运行一下我的其他项目看看,结果我的个人主页就崩了。

screenshot_on_b85m_by_flameshot_at_2024-10-24_23-33-27.png

我用的是 2023 年 3.1.48 版本 的 Smarty,其基础版本是给 PHP 7.0 做的,可能旧了吧。

下载了最新版本的 smarty-5.4.1 ,然后

screenshot_on_b85m_by_flameshot_at_2024-10-24_15-36-49.png

什么玩意? implode is Deprecated ,我怎么不知道?

又去重新看了一遍 PHP.net 官网的 implode 文档和 PHP 升级文档。根本没有 Deprecated 。

然后在网上搜了一下,发现是 Smarty 的锅。而且 Smarty 还在 join 和 implode 之间反复横跳。更恶心的是,implode 和 join 在 PHP 7.4/8.0 中已经声明并废弃了 先数组后分隔符 的用法,但是在 Smarty 中却是强制要求 先数组后分隔符 ?甚至官方在 issue 里来来了句 Smarty is Smarty and PHP is PHP. 有病吧。而且你就算有病,你特么连个文档都没有,谁知道你有这种抽风的设计啊。

你要知道之所以我还用 Smarty 这种超级古董,就是因为这是一种 靠谱的、前后端分离、完全后端渲染、仅需要 HTTP 和 PHP 环境、不需要臃肿框架和特定语法,的网页渲染模式,是提供给搜索引擎最靠谱的传统模式。没有网页模板系统的话,想写这种纯后端渲染前端显示的页面,就只能 php 和 html 代码混写,非常的恶心。

现在 Smarty 抽风了,真就不知道以后还怎么不依赖框架写这种页面。我搜了下,Laravel 的 Blade ,和 Symfony 框架下的 Twig,貌似也可以独立使用,但是我对 Laravel 和 Symfony 基本一无所知,作为一个 PHP 开发者真是有点丢人。

话题扯远了,回到刚才。

我目前是不太想动我的个人主页的,这 WordPress 是大头,是主要内容。主页只是个入口。虽然主页改动起来并不困难,代码量少,Smarty 抽风的部分比较好找。但主页这一块我其实并不满意,主要是多语言这块用的传参而不是独立页面,很受搜索引擎嫌弃,基本上没收录,收录的也搜不到。但我的确没有精力和欲望去重做。

另一方面,WordPress这边,崩溃得最多的部分其实是插件。WordPress的代码质量本来就很堪忧了,第三方插件更是三脚猫,各种天花乱坠的不规范语法,可以说基本上都不能在 PHP 8.3 上运行。

另外我虽然看了 不向后兼容变更废弃功能 ,但是 PHP 新版新增的语法糖也有点天花乱坠。我用的是最新版的 php-cs-fixer_v3.64.0 ,没配置好的话就会把那些插件的奇葩写法转写成 8.3 的语法糖,有时结果更是瞎眼,基本没有易读性。

综上考虑,先放弃 PHP 8.3 的升级。先把能通的条通。PHP 的升级难度应该不是最高的,但是底下这些撇不掉的小垃圾目前是必须要保且支持不到 PHP 8.3,没精力做修改。

放弃 PHP 8.3 继续用 PHP 7.4 。


第二天 晚上

看兼容性列表,WordPress 6.6 也是支持 PHP 7.4 的。

但是实际上 API 改动实在太大了,而 PHP 前端应用最恶心的一点,就是 出错了,不报错

即使开启了 define('WP_DEBUG', true); define( 'WP_DEBUG_LOG', true ); define( 'SAVEQUERIES', true ); ,也经常是

  • 功能好像开了但是没开
  • 功能好像崩了但是啥日志都没有
  • 功能正常使用但是页面上打了一堆不知道哪里来的错误日志

面对如此大的一个工程而大部分代码逻辑都是不可靠的。升级到 6.6 实在是消受不起。

放弃 WordPress 6.6 ,只升级到比较近的版本。

要不然试试 5.5 吧,毕竟我在用的一款插件,作者自评最高支持到 5.5。

下了个 WordPress 5.5 的安装包,装完了。崩,但是崩得没有 6.6 多。

主题 graphene

不开插件,只看主题,首先就是文章的评论显示不出来。检查后发现是评论的API变了,而我的主题 graphene 是 1.9.4.3 版本的,不支持 5.5 。

screenshot_on_b85m_by_flameshot_at_2024-10-25_00-48-04.png

有时候看其他人的代码就是折磨,代码里写法五花八门,空格TAB混着,一会拼接字符串,一会替换字符串。单引号双引号混着用,左边括号有空格,右边括号换行了。

说实话我都不知道这算不算改好了,反正现在是能显示出来。我更担心的是其实还有哪个不知道的角落还有错,但是看不到,毕竟 PHP 前端 出错了 不报错

官方倒是有个新版的 graphene,但是我现在用的这个主题就是我大量改动过的,因为 graphene 原版的代码实在是, 太错了 。好多代码完全不符合前端的理念。然后是颜色和界面也是要一点一点从设置里调,那复杂和麻烦程度,说真的我更乐意重写 HTML 和 CSS 。

我真的想过很多次自己做一个 WordPress 主题,这个想法可是足够老了。但是当时 WordPress 3 版本的主题文档就恶心到我了,真的超级麻烦。而到如今就 WordPress 现在的代码质量,我估计开发主题会更困难更恶心。

也是怪不得其他更轻量的博客程序能后追直上。

插件 Disable WordPress Core Updates

接下来是 Disable WordPress Core Updates 这个插件。

这个插件是为了禁用 WordPress 的界面更新的。但是其实只有一行有意义的代码:

add_filter( 'pre_site_transient_update_core', create_function( '$a', "return null;" ) );

首先 create_function 这个函数在 PHP 7.2 废弃,在 8.0 中删除,所以我改写成了。

add_filter('pre_site_transient_update_core', function ($a) { return null; });

然后就见证奇迹了。能用是能用,但是 pre_site_transient_update_core 这个字段我在整个 WordPress 代码中都没找到。为什么做一个 pre_site_transient_update_core add_filter 就能抑制 WordPress 界面提示升级? 魔法啊?

然后开启这个插件的时候,更新页面是崩的。

screenshot_on_b85m_by_flameshot_at_2024-10-24_21-14-55.png

魔法。

插件 NIX Gravatar Cache

接下来是 NIX Gravatar Cache 插件。

这是个把 Gravatar 头像缓存到本地服务器的插件,只不过早就死透了。我当时随便改了点代码对付着用,大部分时间没出错也就那样了。

首先是这段代码。(红色部分是我添加的改动)

screenshot_on_b85m_by_flameshot_at_2024-10-25_01-04-29.png

不能直接执行 wp_enqueue_scriptwp_enqueue_style ,要先执行个 add_action('wp_enqueue_scripts', 引用能调用那俩玩意的函数); 。外国人看这种超长的单词时不会眼花吗?

然后是这么一段代码。

screenshot_on_b85m_by_flameshot_at_2024-10-25_01-00-47.png

WordPress 的 register_啥啥啥_hook(__FILE__, array($this, '函数名')); 写法全都作废了,要改成 register_啥啥啥_hook(__FILE__, array($this, '函数名'));

接下来底下那一节:

  • 假如 路径不可写 且 路径为目录,报错

否则

  • 假如 创建目录(权限777)失败 且 路径不为目录,报错

就这烂判断条件看得我脑子都快炸了也没弄明白为什么这破玩意能在我线上服务器上跑几年没报错,而我本地开发环境却根本跑不通。

screenshot_on_b85m_by_flameshot_at_2024-10-25_01-19-42.png

而且这玩意讨厌就讨厌在于,确在我的测试环境下报错了,但是 WordPress 只是多了个 .php-error 的样式并且高出来 2em 的一节,但是一点错误日志都没有!!! 出错了 不报错

最后还是靠自己写 debug 代码定位的问题。

这 TM 都是些不该是问题的问题,竟然多得到处都是。

2天,一点有效进展都没有。唯一有效收获就是这些屎山不碰就没事,一碰能崩得全身是屎。

反而自己写的没有引用那些垃圾玩意的插件和程序,没发现啥大毛病。


第三天

2天没啥进展,给我干懵了。

俗话说没事别升级,升级必出事。

原本的想法是先试着升级,如果不能平滑升级的话,大不了全摧毁了,然后把整个博客文章用导入的方法塞回去重建。如果插件出问题了,大不了找找看是否有新的替代品。结果搜了一下,靠谱的插件基本没有,一大堆商业推广的插件,和一大堆复合性插件,而且这些插件无论对 5.x 版本 还是 6.x 版本的兼容性都乱七八糟,问题解决不了,还有可能引入更多的问题。主题这边则是更不想换,一方面本身现在用的主题就是我大量修改过的,因为网上的各种主题,仅安全性就一塌糊涂,更别说 HTML 标准了。我这主题还是专门针对 1366×768 分辨率优化过的,能在 小屏幕 150% 比例正常显示。让我再去改个新的,工作量也是太大。

现在基本上没辙了。

PHP 这边其实还行,而且版本活跃度比较稳定。

screenshot_on_b85m_by_flameshot_at_2024-10-25_22-09-00.png

https://packagist.org/php-statistics 能看出各版本使用占比都跟维护相关。但 7.4 版本 比 8.0 版本还受欢迎 属实乐了。

但是周边应用真的是质量山体滑坡。WordPress自 3.0 版本就开始崩,后面很多发展都很魔幻,就连编辑器都是靠社区兜底,到现在已经想不清楚这玩意的产品路线是啥了。

其他生态我也不清楚,毕竟作为一个 PHP 开发,我连 Laravel 和 Symfony 都没用过,商用产品都是用 ThinkPHP 应付的,当然最爽不过不依赖框架没有条条框框自己从头写。

但是像 Smarty 这种原行业标志都走奇葩路线了。可以说整个IT行业,基本上,正常的元老人物都退出舞台了,剩下的这些,刨去臭鱼烂虾,就只有偏执而扭曲了,假若走向歪路,那就没得旧,而这一点在开源社区上也极为明显(因为闭源商业的死不死没人关心),Godot 基金会开搞政治正确炮轰特朗普和动画头像用户,Linux 基金会直接开踢俄罗斯的代码贡献者。IT 行业现在就像是一个患了早期癌症的癌细胞轻微扩散病人,看似有救但却是谁都不想救。


结论

给我干哑火了,懵逼了,现在不知道咋整了。

The post 尝试升级 WordPress 版本 和 PHP 版本,然后失败了 first appeared on 石樱灯笼博客.

[Frightening Dream] 梦见求职怀旧「003」

「部分噩梦是因为本人在现实中遭受精神上的打击,使得心情低落,导致创伤后心理压力紧张综合征后,会在梦中体现。这种噩梦可能会引发一些疾病。」

——摘自维基百科「恶梦」词条。

声明:「本故事是做梦梦到的,如有雷同那你精神有问题」


章节一

找工作,出门去面试。

大下午,刚下完大暴雨,北京天黑得跟深夜一样。积水没过鞋。

面试地点在一个小区的四层小楼里。

天黑,小区里一个路人也没有。

到了楼下,从一个入口找到了一个特巨大的电梯,看着像货运电梯,但是保养得特干净,应该是被载人载货混用了。

电梯外边墙上一个大彩色海报,上面写着各楼层的用途:

  • 1楼,美食城(请从正门进入)
  • 2楼,酒吧(此电梯不到2楼)
  • 3楼,用棕色的胶带贴住了
  • 4楼,XXX幼儿园

我要去的是4楼,应该是个IT行业的公司,怎么是幼儿园?

我看看手机,离约定的面试时间还有一个多小时。不如直接上去看看是不是走错地方了。

坐电梯上到4楼,出电梯一看,楼层空无一人,有几个房间亮着灯,往里面看,装修的确像是个幼儿园,只不过里面都是空的。地上有些幼儿园家具和玩具,看起来以前是个幼儿园,只不过没在运营。

我继续往里走,看看能不能遇到个人,问问是什么情况,这是我等会要面试的地点吗?


章节二

突然从走廊对面呼啦啦上来好多人,挨个进房间开灯,好像在规划什么似的。我走进一看,领头的那个不是我以前的前同事W(代名)嘛!W也看到我了,赶紧互相打招呼。

W后面跟过来的人,我一看,这不都是以前我在公司时比我先离职的老前同事嘛!

我问他们这是在干啥,W说,老公司打算用个新名字开个子公司,把这里三楼四楼都租下来当公司了,人都是以前的老人,直接在这简单收拾一下就要开干了,今天就是过来看看四楼怎么分配工位比较合适,大家都过来看看,看完了之后在这团建,玩一下午,然后楼下应该是个美食城,还有酒吧,晚饭也直接在这解决,反正公司报销。

W问我:咋这么巧你咋上这来了?

我:我是来面试的,说是在这四楼。

W:那要是知道是你那还面个啥啊,直接来就行了。等会别走啊团建玩游戏直接带你一个。

随便看了一下,都是以前的老同事,互相寒暄一下,发觉之前出国的几个前同事都没来,还是满遗憾的。


章节三

不大一会,前同事们把最大的那个房间收拾出来了。原本应该是幼儿园的给小孩玩耍的活动教室,现在一堆大人在地上坐了一圈有说有笑。

我进屋,有的前同事看到我,一愣,「哎呦,老猫(代名)咋也来了」,我说我本来是来面试的,「那还面试啥啊你想要啥岗位等完事了直接跟我们说就行了。等会做游戏你可得在我们这一组啊。」

W:「那可不行,我先遇上的老猫,他就得跟我们一组了」

另一个前同事:「唉W你不能这样,你看我们组人少,你得给我们匀几个」

W:「那我把最后进屋那几个人匀给你!」

得,我还是个抢手货。

前同事M(代名。M以前是W的项目组组长)进屋,看到我,「呦,老猫来啦,你分哪个组了?」

W:「分咱们组了。」

M:「你给他手环了吗?」

W:「还没有呢。」

说罢,W拿出来个浅绿色的手环,直接套我手腕上了。「这是咱们公司的手环,也是门禁卡,你进门的时候刷这个手环,门就开了。另外今天做游戏咱们一个组的,手环颜色是一样的,都是浅绿色的。」

我看了一下周围,所有人手上都带着个手环,颜色一共四种,看来今天做游戏是分四组。


章节四

开始团建,做游戏。

第一个游戏,一个前同事竟然捧出来一摞 3DS 出来,第一个游戏竟然是要在 3DS 的一个排队游戏里比赛的。

然后果然我们组首推我第一个参赛……果不其然就拿了第一。怪不得每个组都想争取我当组员。

自己玩了第一个游戏,然后又看着老同事们继续玩。

我突然一愣,想起以前大家还在老公司的时候,开会啊,团建做游戏啊,都是有零食有饮料的,怎么现在没有了?

W:「这附近貌似还真没看到有超市,没买着。」

我:「我下楼去一楼那个美食城看看有没有吧。」

W:「你手环拿着呢吗?等会从3楼进来直接刷手环就能开门。」


章节五

我从货运电梯下楼,直接到了楼外,绕着整栋楼走了半圈,才看到整栋楼的正门。

天仍旧很黑,像深夜一般。

从正门进入,映入眼帘的,是一排排餐饮用的桌子,而大部分椅子则倒扣在桌子上,看起来像是很久没有用过一样。

继续往里走,看到有亮灯的柜台,有个人弯着摇拿着拖把在柜台后面拖地,也不像是在营业的样子。听到我的脚步后也没有站起来看我一眼的意思,弯着腰就回应了一句「还没到营业时间呢」,说罢就进里面的屋子了。

我四处看了一下。基本所有柜台都没有亮灯或者营业,都没有人在。

有个通向二楼的楼梯,但是整个楼梯都被椅子堆死了,根本不可能从那里上到二楼。

最里面有个柜台则是有个人坐在那玩手机,不过那个柜台连牌子也没有,看起来更像是值班的保安。

我过去问保安哪里能买东西,保安:「不营业。」

我又问从哪里能上楼,保安:「都说了不营业。」

我示意我其实是要去3楼4楼,保安竟然用嫌弃的眼光瞟了我一眼:「3楼4楼早就黄了好久了根本没东西,你上去干啥!」


章节六

没办法只能出大楼,然后走到大楼后门坐电梯再上去。

到了4楼,电梯打开,却发现整个楼层一丝光亮都没有。没有房间开灯,也没有人。我往里面走了一点,整个楼层除了我脚步声的回音之外,什么都没有。没开灯,房间里有什么也看不到。

很伤心的从电梯下楼,到楼下,外边又下起来大暴雨了。

门前积水很快就涨了上来,水深得能划船。


章节 Nostalgic PTSD

想拿手机看看几点了,却发现自己手腕上还套着手环,只不过在这乌七八黑的大黑天里,手环不是浅绿色的,是橙棕色的。

醒了。

The post [Frightening Dream] 梦见求职怀旧「003」 first appeared on 石樱灯笼博客.

给 WordPress 添加一个 RSS 友链阅读器

前情提要

前不久在 jeffer 的一篇文章看到写了一个wp的rss阅读插件,有点小心动。其实早在去年就和 thyuu 交流过这个wp的友链rss功能,当时老哥很快搞定了,还分享了实现代码。当时对rss不是很感冒,基本就是在友链页面翻翻经常逛的那几个,想着也不是每个人都有这个就感觉有点没必要,而且可以直接去看友链的公共聚合之类的,就没弄。直到现在,用了就感觉,欸 好像还挺方便的。

实现

需求是这样的:在wp原生链接基础上,读取不同分类链接中的 link_rss 数据然后解析为自定义 stdClass 返回并储存到 wp_options 表中(方便后期排序等操作),通过不同的链接分类,可以读取不同分类下的rss数据集,通过设置链接显示状态(visible)来限制已订阅链接。

基本理念就是读取和解析xml文件,不过这大千世界,rss种类也很多,面对多种数据结构需要手动去兼容返回。刚开始直接就问了kimi给了一套方案,用php自带的simplexml扩展来解析数据,试了 能用,不过需要自己手动兼容rss类型,就相对比较麻烦。后来想起 thyuu 之前用的wp原生功能 fetch_feed 能自动解析,效果感觉比 curl 好使..

后面尝试了两种不同输出的效果,虽然大差不差但还是wp原生的用起来更稳定(貌似),后面把数据缓存到 wp_option 表了,并挂载更新到了原生链接操作hook(增删改均可同步更新对应分类),添加 wp_schedule 定时清理所有分类缓存任务。

效果

综合效果还是不错的,如图。

一些请求被分割,一些请求失败(底部)

问题

主要有两个问题,一个是rss 抓取时间过长,另一个是抓取成功率问题。

  • 抓取时效性
  • 抓取成功率

关于 rss 抓取时效方面,从拉数据到缓存50+的链接需要反应大概2分钟左右。我问了kimi很多解决方案,什么异步、分块、多线程等等,效果都不太理想。

抓取成功率方面,我试了下当链接超过一定量时就会出现抓取失败的情况,有些报错是数据没完全接收。针对这一情况,首先尝试了 curl_multi 多线程处理,基本没效果。然后尝试将rss链接集分块请求处理,效果不理想。

综上所述,目前还是用的默认 fetch_feed 做的分块请求处理。有没有大佬来指点一二,这种数据应该怎么处理以性能最大化?

闲聊

tmd我前几天手贱给笔记本换硅脂,中毒了去看什么硅脂视频,tmd就是一坨大便艹。换完直接屏幕闪屏,一旦请求gpu上来就开始闪,以为gpu核心出问题了,换核显结果一样。。网上搜了一圈发现可能是显示器线材问题,于是外接显示器最后发现是笔记本显示器的问题。。。这tmd百思不得其解

最要命的是,换了硅脂,笔记本发热降频???我tmd合着搁着绕圈子呢,何况之前根本不降频,温度比这更高高负载时间也比这更长,我干,到底什么原因。我现在就怕这核心被我搞了,但按理来说我拆装非常小心所以不应该啊,哎日了狗,黑神话也搁置了,全怪自己手贱啊,非要去换sb硅脂,这就叫没事找事!!

如何在 VSCode Remote 里监控远程资源

快速开始

在使用 VSCode 的过程中,你可以通过使用 Remote-SSH 插件来连接到远程服务器,然后使用 Monitor Pro 插件对远程服务器的资源进行监控。

  1. 安装和配置 Remote-SSH 插件

    • 打开 VSCode,点击左侧栏的扩展按钮。
    • 在扩展商店搜索并安装“Remote-SSH”插件。
  2. 连接到远程服务器

    • 安装完成后,在 VSCode 的终端中输入相关命令来连接到远程服务器。例如,可以使用ssh 用户名@远程服务器地址命令进行连接。
  3. 使用 Monitor Pro 插件进行资源监控

    • 安装 Monitor Pro 插件。这个插件可以帮助你实时跟踪重要的系统指标,并提供直观的展现方式。
    • 安装后,该插件会自动启用,并且你可以通过 VSCode 的状态栏查看当前的资源使用情况,如 CPU、内存、磁盘等。
  4. 刷新设置

    • Monitor Pro 插件允许你设置更新资源指标的刷新间隔,确保能够及时获取最新的资源使用数据。

如何安装和配置 Monitor Pro 插件?

安装 Monitor Pro 插件

在 VSCode 界面的左上角,点击“扩展”(或按 Ctrl + Shift + X),然后在搜索框中输入“Monitor Pro”。

找到“Monitor Pro”插件后,点击“安装”按钮即可完成安装。

配置 Monitor Pro 插件

安装完成后,会在 VSCode 的活动栏中看到一个新图标,点击它以启动 Monitor Pro 插件。

根据具体需求,可以在 Monitor Pro 的设置页面中进行相应的配置,比如调整监控项等。

部分支持的远程资源监控指标

  1. CPU 使用率:监控 CPU 的利用百分比。
  2. 内存使用率:监测系统的内存使用情况。
  3. 网络使用率:跟踪网络的使用情况。
  4. 文件系统使用率:监控文件系统的使用情况。
  5. 电池百分比和充电状态:监测设备的电池百分比和充电状态。

最佳实践

  1. 实时跟踪关键指标:Monitor Pro 是一个全面的资源监控工具,可以帮助你实时跟踪重要的系统指标。因此,建议定期检查这些指标以确保系统的稳定性和性能。

  2. 直观展现方式:Monitor Pro 提供直观的展现方式,这有助于快速识别问题所在。利用这一点,可以及时发现并解决潜在的性能瓶颈。

  3. 内存优化:虽然 Monitor Pro 主要关注的是资源监控,但结合其他工具如 Memory Monitor 和 Allocation Tracker,可以进一步优化内存使用。例如,通过 Memory Monitor 查看整个应用所占用的内存,并注意 GC(垃圾回收)操作的频率和时机,以避免内存抖动等危险信号。

  4. 定制化配置:根据你的具体需求,对 Monitor Pro 进行定制化配置。例如,可以选择监控特定的资源,从而更有效地管理和优化资源使用。

This message is used to verify that this feed (feedId:42331815237783574) belongs to me (userId:55156152962822144). Join me in enjoying the next generation information browser https://follow.is.

银弹飞过先锋大厦

TL;DR 本文13200+字,全文阅读约需20分钟。其中2680字的示例部分为ChatGPT生成,因此可以说本文GPT含量20%,不过该部分详细内容与本文主旨关系不大,且由GPT-4生成,可略过不看。本文简要回顾了从软件及计算机诞生到当前大模型AI时代的软件工程发展历史,并试图从软件工程历史和ChatGPT实践案例中探讨在大模型时代的软件开发模式。

介绍一个多人划线标记功能

想法

灵感来源于某次逛公众号文章的时候,在文章中偶然看到了一个下划线,经过它时还会显示多少人划线标记。感觉这个功能其实对博文也挺方便的,因为都有评论系统,感觉可以通过评论用户信息做一个多人划线标记功能,自己留作标记的同时也方便其他浏览文章的博友发现和标记文章主要相关内容。

众所周知我目前用的平台是 wordpress,所以该功能也是基于 php 环境下进行开发的,由于标记数据储存在本地而非数据库中,可以灵活应用于各类基于php框架的博客系统上(如typecho、zblog、emlog等)。唯一一点是使用了 wordpress 的文章 id 来区分不同文章页面(关于这点其实可以使用加密 location.pathname 发送到后端作为文章别名区分,后续版本可能以此更新 已更新(默认url别名),执行标记后请勿随意修改url:*切换请求标识符(postID)可能导致清空本地记录继而误删远程数据)若使用其他平台需要修改前端请求中的pid参数(现可选携带参数:postId 初始化,默认 location.pathname

功能简介

划线标记功能结构由文章—用户—标记三部分组成,通过wordpress文章id区分:同一篇文章下可以理论上可以存在不限量标记,但同一个用户(md5邮件区分)默认情况下仅能存在3个标记(可携带自定义参数初始化标记)。而同一个标记(内容)仅能在该文章中存在一次(若通过某种手段重复标记某段已存在于远程记录时,后端会返回标记已存在的错误信息),另若当前选中文本在当前段落中存在多个相同字符时,前端会阻止用户提交标记。

本着只要能把功能复现就行的想法,在持续一周左右的时间后正式开始测试,目前该功能已集成到2BLOG主题最新的 #v1.3.9.2 版本中,后续将对其进行持续迭代更新,下面讲下简单实现思路。

实现思路

前端直接用的 getSelection api 执行选中标记操作(之前本想做跨浏览器兼容,后面想想太麻烦,先实现可以用了再说,反正又不是上线的东西 后面可以慢慢迭代无所叼谓),用户信息存了本地cookie(刚开始做的内容储存,后来改为用户本地校验内容)。

后端用的东西基本和我之前那篇实现gpt的思路基本是一致的,都是本地储存。唯一只是多了部分用户校验,因为涉及到用户新增和删除操作问题(毕竟所有人在评论后都可以对文章进行标记或删除)暂时只能先这么弄(虽然目前配置了文章标记次数限制,不过并没有做黑白名单限制)。

WordPress 简单实现 chatGPT 文章摘要

WordPress 简单实现 chatGPT 文章摘要

灵感来源于之前在浏览 HEO 博文时候偶然看到文章前有一段 AI 摘要,第三人称以打字形式来简述文章内容还是蛮酷的~ 于是拟了个把这个功能集成到 2BLOG ...

2BROEAR 21/03/2024 | 3192 views.

用户校验

目前做了两个信息校验,

一个是最基本的邮件mail(明文验证,其实刚开始的方案是有md5来做,后来因为新增了显示标记用户 gravatar 头像的需求所以不得不放弃该方案,取而代之的只能是远程明文对比验证。但是,为了防止出现邮件明文暴露后造成伪造请求的情况,在执行部分敏感操作时必须携带储存在用户本地的 timestamp 时间戳(明文)与储存在远程服务器中的ts(加密,防止远程ts暴露后获取到对应的本地ts明文)进行校验,通过后再放行相关操作。

但ts交互验证有一个明显的坏处就是:如果标记浏览器与执行操作的浏览器环境不同(即本地cookie无相关ts记录)哪怕当前是用户本人操作也无法通过远程ts验证,目前应对这种情况目前做了用户操作提示与浏览器user-agent记录(可查看对比记录),暂无具体替代解决方案。

其他

这个功能目前处于测试阶段,使用过程中有任何bug欢迎反馈哟/doge。(*另附一些常用的初始化参数如下(所有参数值均为默认值!具体参数等内容可前往 github 查看)

// 携带参数初始化
new marker.init({
    static: {
        postId: window.location.pathname, // 页面标识符(可选,文章唯一ID)
        apiUrl: "mark.php", // 后端 mark.php 文件地址(可选,缺省同一目录)
        md5Url: "md5.js", // 用户 mid 初始化 md5 资源(可选,缺省同一目录)
        avatar: "//gravatar.cn/", // 用户头像 cdn(可选,默认 cravatar)
    },
    class: {
        blackList: ['chatGPT','article_index','ibox'], //'', 'chatGPT,article_index',
    },
    element: {
        effectsArea: document.querySelector('.content'), // 可选区域(可配合 blackList 使用)
        commentArea: document.querySelector('#vcomments textarea'), // 评论区域
        commentInfo: {
            userNick: document.querySelector('input[name=nick]'), // 评论用户
            userMail: document.querySelector('input[name=mail]'), // 评论邮箱
        }
    },
});

插件

现已集成插件到 github:

2Broear/marker: a local-storage(php) based original javascript api marking-off plugin. (github.com)

todos

一些預計添加或修復的待辦事項

  • ❓ 約束後端請求頻率,(新增延迟文件锁),修復并發請求失敗但返回已完成問題。问了一圈 gpt 没解决,暂时在前端做请求限制与延时。
    • ❌使用 localStorage 修复了授权用户操作失败(弱网、并发)后再次访问时自动更新本地/远程记录。bug:标记后立即刷新页面会导致本地记录(已记录)无法匹配远程(等待返回)被删除,后续刷新页面后导致远程记录(已返回)无法匹配本地记录(已删除)被删除。(可在切换评论系统后,通过读取本地记录仍可执行标记删除等操作)
    • ✅更新方案(v1.3.9.5):标记或删除后等待远程响应,返回后再写入(补全)或删除(更新)本地记录对应逻辑如下:
      • 新增标记:(未响应,新增本地记录)—>刷新页面(未响应,删除本地记录)—>返回数据(已响应,对比本地记录)—>远程记录中 未找到 本地记录—>新增本地记录
      • 删除标记:(未响应,删除本地记录)—>刷新页面(未响应,新增本地记录)—>返回数据(已响应,对比本地记录)—>本地记录中 不存在 远程记录—>删除本地记录
  • ✅ 修复本地、远程标记请求上限多步验证。
  • ✅ 新增更多自定义初始化携带参数配置。
  • ✅ 新增自定义标记文本注释功能。
  • ✅ 支持多人重复(未注释)标记,并显示(前三)标记用户头像。
  • ✅ 更新UI/UE,更好的适配暗黑模式
  • ✅ 支持 SSE 服务端 api 推流(递增显示标记➕动画)bug: 每次接收数据后输出时未即使更新data

使用 Docker 和 pnpm 优化打包 Nuxt

本文将指导你如何为一个结合了 Prisma 和 Nuxt.js 的全栈项目创建优化后的 Docker 镜像,并使用 pnpm 作为包管理器。

我的项目最终镜像大小从 1.12GB 缩减到了 160.21MB。

我的项目构成

Nuxt.js 是一个基于 Vue.js 的服务器端渲染应用框架,非常适合于构建现代化的 Web 应用。

我的项目直接采用 Nuxt 构建全栈项目。

  • Nuxt3
  • Prisma
  • PNPM

开始构建

首先,我们将使用 node:20-alpine 这个更轻量级的基础镜像来减小最终镜像的大小。Alpine Linux 因其安全、简单且体积小而广受欢迎。

多阶段构建是减少 Docker 镜像大小的有效策略之一。我们将使用三个阶段来构建我们的镜像。

第一阶段:构建依赖项

1
2
3
4
5
6
7
8
9
10
11
ARG NODE_VERSION=node:20-alpineFROM $NODE_VERSION AS dependency-baseWORKDIR /appRUN npm install -g pnpmCOPY package.json pnpm-lock.yaml ./RUN pnpm install --frozen-lockfile`

这一阶段负责安装我们项目的依赖项。我们使用了 pnpm 来代替 npm,pnpm 在缓存和磁盘使用上更为高效。

大部分项目也用 pnpm 而不是 npm 作为包管理工具了。

第二阶段:构建应用程序

1
2
3
4
5
FROM dependency-base AS production-baseCOPY . .RUN pnpm run build

在这一阶段,我们复制了项目代码并执行构建命令。这里的构建指的是 Nuxt.js 的构建过程,它会生成静态文件和服务器端渲染所需的资源。

第三阶段:生成生产镜像

1
2
3
4
5
6
7
8
9
10
11
12
13
14
FROM $NODE_VERSION AS productionCOPY --from=production-base /app/.output /app/.outputENV NUXT_HOST=0.0.0.0 \    NUXT_APP_VERSION=latest \    DATABASE_URL=file:./db.sqlite \    NODE_ENV=productionWORKDIR /appEXPOSE 3000CMD ["node", "/app/.output/server/index.mjs"]

最后,我们创建了适用于生产环境的镜像。这个镜像仅包含用于运行应用程序的必要文件,减少了不必要的层,使得镜像尽可能地保持精简。

我们还定义了一些环境变量,比如 NUXT_HOSTDATABASE_URL,这些是 Nuxt.js 应用和 Prisma 所需要的。其中,DATABASE_URL 被设置为使用项目根目录下的 SQLite 文件作为数据库。

最终通过暴露端口 3000 并指定启动命令来运行 Nuxt.js 应用程序。

不同构建方式的镜像大小比较

分别为:

  • 3 步构建
  • 2 步构建
  • 直接构建

a3c345aaa51a4b8b802c25bc9d3591c0.png

Dockerfile 总览

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# Use a smaller base imageARG NODE_VERSION=node:20-alpine# Stage 1: Build dependenciesFROM $NODE_VERSION AS dependency-base# Create app directoryWORKDIR /app# Install pnpmRUN npm install -g pnpm# Copy the package filesCOPY package.json pnpm-lock.yaml ./# Install dependencies using pnpmRUN pnpm install --frozen-lockfile# Stage 2: Build the applicationFROM dependency-base AS production-base# Copy the source codeCOPY . .# Build the applicationRUN pnpm run build# Stage 3: Production imageFROM $NODE_VERSION AS production# Copy built assets from previous stageCOPY --from=production-base /app/.output /app/.output# Define environment variablesENV NUXT_HOST=0.0.0.0 \    NUXT_APP_VERSION=latest \    DATABASE_URL=file:./db.sqlite \    NODE_ENV=production# Set the working directoryWORKDIR /appEXPOSE 3000# Start the appCMD ["node", "/app/.output/server/index.mjs"]

使用 PNPM 的情况下,Jest 解决 ESM 依赖库的报错问题

环境

  • NX
  • PNPM
  • lodash-es
  • Jest

从 karma 转移到 Jest 遇到了如下报错

主要原因是 “node_modules” 文件夹中 ESM(ECMAScript Modules) 库不被 Jest 支持。

鉴于 Jest ESM 支持还在几乎不可用的试验阶段,而目前我主要是在公司项目上迁移到 Jest。所以本文主要采用 transformIgnorePatternsmoduleNameMapper 两种配置来解决这个问题。

11c629a593c4c8484b6cb8ca44d6aa5f.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Test suite failed to runJest encountered an unexpected tokenJest failed to parse a file. This happens e.g. when your code or its dependencies use non-standard JavaScript syntax, or when Jest is not configured to support such syntax.Out of the box Jest supports Babel, which will be used to transform your files into valid JS based on your Babel configuration.By default "node_modules" folder is ignored by transformers.Here's what you can do:    • If you are trying to use ECMAScript Modules, see https://jestjs.io/docs/ecmascript-modules for how to enable it.    • If you are trying to use TypeScript, see https://jestjs.io/docs/getting-started#using-typescript    • To have some of your "node_modules" files transformed, you can specify a custom "transformIgnorePatterns" in your config.    • If you need a custom transformation specify a "transform" option in your config.    • If you simply want to mock your non-JS modules (e.g. binary assets) you can stub them out with the "moduleNameMapper" config option.You'll find more details and examples of these config options in the docs:https://jestjs.io/docs/configurationFor information about custom transformations, see:https://jestjs.io/docs/code-transformation

以下配置主要以 lodash-es 作为参考。

transformIgnorePatterns

官方文档的解释是:正则表达式模式字符串的数组,在转换之前与所有源文件路径匹配。如果文件路径与任何模式匹配,则不会对其进行转换。
transformIgnorePatterns 用于指定在进行代码转换时应该忽略的文件或文件夹。

而在 NX 默认的 Jest 配置中,配置为 node_modules/(?!.*\\.mjs$)
这个正则表达式的含义是,匹配以 node_modules/ 开头的文件夹路径,但排除那些以 .mjs 为扩展名的文件夹路径。?! 是一个否定预查,表示不匹配这样的文件夹路径。

1
transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],

以上配置意思就是将会把以 .mjs 为扩展名的文件从 ESM 转换为 CommonJS,以支持 Jest。

添加转换 lodash-es

顺便支持一下 PNPM

1
2
3
4
5
6
7
const esModules = ['.*\\.mjs$', 'lodash-es'].join('|');export default {    ...    transformIgnorePatterns: [`node_modules/(?!.pnpm|${esModules})`],    ...}

转换后 failed 数量从 15 减少到 11,但是这么做会有一个转换的过程会有额外的支出,需要 51s。不过第一次转换完后貌似就会缓存然后就不用转换了。

ef4e6aeef369b021b707664f9c03549a.png

支出更少的方法 moduleNameMapper

这种方法需要库本身有对应的 CommonJS,就不需要转换了。可以跑到 12s

1
2
3
4
5
6
7
export default {    ...    moduleNameMapper: {        '^lodash-es$': 'lodash',    },  ...}

e87d8ad99b64c8f836a8c1777ec217bf.png

最终配置参考如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/* eslint-disable */const esModules = ['.*\\.mjs$'].join('|');export default {  displayName: 'pc',  preset: '../../jest.preset.js',  setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],  coverageDirectory: '../../coverage/apps/pc',  moduleNameMapper: {    '^lodash-es$': 'lodash',  },  transform: {    '^.+\\.(ts|mjs|js|html)$': [      'jest-preset-angular',      {        tsconfig: '<rootDir>/tsconfig.spec.json',        stringifyContentPathRegex: '\\.(html|svg)$',      },    ],  },  transformIgnorePatterns: [`node_modules/(?!.pnpm|${esModules})`],  snapshotSerializers: [    'jest-preset-angular/build/serializers/no-ng-attributes',    'jest-preset-angular/build/serializers/ng-snapshot',    'jest-preset-angular/build/serializers/html-comment',  ],};

参考

  1. Jest setup "SyntaxError: Unexpected token export"
  2. Configuring Jest · Jest
  3. ECMAScript Modules · Jest
  4. Configuring Jest · Jest

如何构建一个受保护的网站

前段时间,我出于兴趣试着做了一个需要登录鉴权才能访问的个人网站,最终以 Docusaurus 为内容框架,Next.js 做中间件,Vercel 托管网站,再加上 Auth0 作为鉴权解决方案,实现了一个基本免费的方案。这里做一个分享。

基本的目标是发布一系列文字内容为中心的网页,并限制访问这些页面必须先鉴权同时确认身份信息在白名单上,防止被任意用户访问或黑客破解。

从这个角度看,鉴权的功能实际上是一个包装层,首先要解决的是内容生产的问题。由于我最近主要开发的网站,包括 Pulsar 官方网站开源小镇都是用 Docusaurus 生成的。所以这部分的选型,我就直接选定了 Docusaurus 框架,不再赘述。

实际上,“网页”也不是一个强需求,如果能很好的生成带水印或加密的 PDF 文件,也是一种解决方案。我于是调研了 PandocSphinx 这两种工具,但是它们都有自己的学习成本,并且生成 PDF 基本要先走一遭 Latex 的生态,实在是折腾不来,最终放弃。

选定 Docusaurus 以后,我就开始折腾起了鉴权的问题。

由于实在不想自己折腾一个服务器,加上之前一直是用 Vercel 来托管静态网站,我首先考虑在能够开放源码和内容的托管 Docusaurus 框架生成的网站的基础上,加上 client-side 的保护。

Attempt: Client-side 的鉴权方案

要做具体的鉴权方案,首先要考虑的是选择哪个鉴权软件或解决方案。由于 Vercel 的模板有 Auth0 的示例,加上 Auth0 确实上手非常快,所以我很快选定了 Auth0 作为解决方案。从结果来看,这个选择也是很合理的。

中间我还调研尝试过 Supabase 和 Logto 等解决方案。但是 Supabase 实在是太复杂了,开发者体验非常差;Logto 还没有供应商提供 SaaS 服务,我不太可能再去折腾一套部署,否则为什么不用 Apache Web Server 或者 Nginx 里面写点逻辑呢?

Auth0 client-side 的鉴权,结合 Docusaurus 基于 React 框架开发,可以找到 @auth0/auth0-react 集成库,对应的方案大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// src/theme/Root.tsx
import {Auth0Provider} from "@auth0/auth0-react";

export default function Root({children}): JSX.Element {
const history = useHistory();
const {siteConfig} = useDocusaurusContext();
const {BUILD, NEXT_PUBLIC_AUTH0_CLIENT_ID, NEXT_PUBLIC_AUTH0_DOMAIN} = siteConfig.customFields;
return (BUILD > 0) ? <>{children}</> : (
<Auth0Provider
clientId={`${NEXT_PUBLIC_AUTH0_CLIENT_ID}`}
domain={`${NEXT_PUBLIC_AUTH0_DOMAIN}`}
redirectUri={resolveRedirectUri()}
useRefreshTokens={true}
cacheLocation={"localstorage"}
onRedirectCallback={(appState?: AppState): void => {
if (appState) {
history.push(appState.location);
}
}}
>
<Authenticator children={children}/>
</Auth0Provider>
);
}

上面这段代码有很多细节,但是核心是用 Auth0Provider 组件把 Docusaurus 的页面元素全部包括进来。这样,浏览器访问对应页面的时候,得到的就会是一个自定义的 Authenticator 生成的页面,比如在这个尝试里,我把它做成一个带有 LOGO 和登录按钮的页面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
export default function Authenticator({children}): JSX.Element {
const {isAuthenticated, error, user, loginWithRedirect} = useAuth0();

if (error) {
return <Errors code={500} message={error.message}/>;
}

if (!isAuthenticated) {
const location = useLocation();
return <>
<Head>
<meta property="og:description" content="Login page"/>
<meta charSet="utf-8"/>
<title>登录</title>
</Head>
<div className={styles.container}>
<main className={styles.main}>
<h1 className={styles.title}>
网站名称
</h1>

<p className={styles.description}>
<Button variant={'contained'} onClick={() => loginWithRedirect({
appState: {
location: location,
returnTo: location.pathname,
}
})}>
点击登录
</Button>
</p>
</main>
</div>
</>
}

if (isWhitelistUsers(user)) {
return <>{children}</>;
}

return <Errors code={403} message={`Unregistered user: ${JSON.stringify(user)}`}/>;
}

但是,这样的方法会有两个问题。

第一个问题相对好解决,记录在 easyops-cn/docusaurus-search-local#210 上。由于生成的页面内容 HTML 文件里全都是登录页,所以这里提到的本地搜索插件建立索引的时候就无法取得正确的内容。

对于 Algolia 这样的搜索专业解决方案来说,它会提供爬取 bypass 的途径,用户配置鉴权过程的 client secret 就可以正确获取页面内容。

对于依赖 HTML 文件内容的本地搜索插件来说,最终我选择的解决方案是上面代码里 BUILD 配置体现出来的两阶段构建。第一次先把 Auth0Provider 的逻辑给禁用掉,生成包含内容的 HTML 文件,以此继续生成正确的 search-index.json 索引文件。把这个文件拷贝出来,启用 Auth0Provider 再次生成网站,再用拷贝出来的索引文件覆盖这次生成的无效索引文件,就可以绕过这个问题。

第二个问题就不好解决了。在解决第一个问题的时候,我尝试了这样一个解决方案:

1
2
3
4
5
6
import useIsBrowser from "@docusaurus/useIsBrowser";

export default function Root({children}): JSX.Element {
const isBrowser = useIsBrowser();
return isBrowser ? (<Auth0Provider /* ... */> ... </Auth0Provider>) : <>{children}</>;
}

这样,因为 isBrowser 在构建阶段一定是 false 所以生成索引正常工作。实际访问的时候,isBrowser 变成 false 于是用户会被导向登录页。

但是,熟悉 React 的同学应该可以看到这里一个明显的问题。isBrowser 被设置成 true 要在 React 渲染逻辑加载完毕之后,所以用户会有一个明显的先能访问页面,然后被弹出去要求登录的体验。

更加要命的是第一次访问的时候其实已经把页面内容发送到浏览器了,随后弹出去的逻辑只是 JS 代码的逻辑。这样,只要知道从开发者工具里捞传输内容,甚至直接用 wget 或者 curl 等工具访问页面的用户,就可以得到页面的源代码,从而知道页面的内容。

即使不用 isBrowser 的方案,回到两阶段构建的方案,也会有类似的问题。

这一次,在用户鉴权之前,他只能看到登录页,没法接收到实际页面的内容。所以这个路径上是被很好的保护的。但是,我的好奇心让我不由得探究:如果页面上没有实际内容,那么用户鉴权之后到底是怎么看到实际内容的呢?

怀着这样的疑问,我在 docusaurus 的论坛上发起了一个讨论。最终,我发现了实际内容被编码到 JS 文件里,鉴权成功后时间内容会从 JS 文件里被调用加载上来。

这里就有一个问题了。JS 文件作为静态文件,也是被托管到 Vercel 的服务器上的,只要有路径,谁都可以访问。而且访问静态资源不会走 React 框架的逻辑,自然也就没什么鉴权的说法。实际上,我在未鉴权的情况下,直接访问 JS 文件是可行的。

那么问题就是用户能不能知道 JS 文件的路径是什么呢?Docusaurus 生成的 JS 文件,是十六进制的数字编号,并没有什么特别的规律。如果用户在正常情况下无法轻易破解 JS 文件路径,那么这个解决方案也是可行的。

可惜的是,我自己在知道框架工作逻辑的情况下,不到半个小时就破解了这个规律。实际上,就算只访问登录页,也可以看到引用了一个带 runtime-main 字样的 JS 文件。打开这个文件,找到固定特征位置的 content files 映射表,取出两个分段里相同的键对应的值 $a$b 再拼成 $a.$b.js 就是对应内容的 JS 文件。这样,随便写个爬虫就可以把页面内容全爬下来了。

虽然对于文本内容的读者来说,大部分是不会做也不知道可以做这样的事的,但是我强迫症上来了,就总想着找个更干净的解决方法。

有读者可能会问,既然如此,鉴权软件提供这种集成有什么用呢?实际上,鉴权(Authentication)和授权(authorization)是不一样的。鉴权主要是解析用户身份,获取对应的用户信息,也就是访客是谁。对于任何人都可以访问的页面来说,也有根据访客身份做定制化展示的需求。相反,授权是在取得用户信息之后,判断对应用户是否具有访问对应资源或执行对应操作的过程。因此,哪怕不做权限控制,只是支持用户登录获取用户信息,也是有价值的。

Vercel Edge Middleware

可以看到,只要试图在 client-side 解决问题,就一定解决不了访问静态资源无法保护的问题,因为访问静态资源的整个逻辑根本不走用户定义的 JS 代码,而是由服务器完成的。实际上,用户访问网页,也是服务器把对应的静态资源返回给浏览器,浏览器加载对应逻辑渲染得到的。

要想拦截这个过程,只能在 server-side 处理入站请求的时候动手脚,这也是 Docusaurus 的主要维护者 @slober 在上面讨论中的建议。由于我不想自己维护服务器,而且已经多次将网站托管在 Vercel 平台上,这个生态相对比较熟悉,于是我先尝试寻找 Vercel 平台提供的解决方案。

幸运的是,Vercel 提供了 Edge Middleware 的方案来支持这类需求。

Edge Middleware location within Vercel infrastructure.

上图展示了 Edge Middleware 在 Vercel 平台上的位置:浏览器的请求首先会经过 Edge Middleware 处理,再被转发到实际托管资源的服务器进程上。

这样,我就可以在最前面拦截所有针对内容的请求,重定向到鉴权页面,在鉴权返回以后根据用户信息做权限验证,只放过在白名单上的用户。

不过,实际实现的时候又遇到了很多问题。

第一个问题是 Edge Middleware 对框架的差别待遇。

上面 Edge Middleware 的文档页上说明了,针对 Next.js 框架和其他框架托管的网站,能够使用的 Edge Middleware 的功能是不一样的。简单来说,只有在 Next.js 框架下才能使用 Next.js 的 Request/Response 等 next/server 包里的定义。如果强行在 Docusaurus 里使用,会直接报错无法渲染。

这样一样,Auth0 针对 Edge Middleware 的集成 @auth0/nextjs-auth0 就不能用了,因为它依赖了 Next.js 定义的 Request/Response 接口。

有读者可能会说,这样的话 fallback 到 auth0.js 手写一个集成不就好了吗?

一方面,手写一个这样的集成还是很麻烦的,尤其是写对 session cache 那一大坨逻辑,以及手动解析各种 wire protocol 传输的数据,非常枯燥。我是终端用户,我的目的是拿好趁手的工具尽快解决我的问题,而不是跑偏去做一个我的依赖项,除非实在是无路可走。

另一方面,即使想要这么做,Edge Middleware 在 Docusaurus 框架的集成上还是有奇怪的问题

这一度让我考虑完全切换到 Next.js 框架下,随便找个内容渲染框架把内容丢上去算逑。于是我就遇到了第二个问题。

第二个问题是 Auth0 的 Next.js 集成默认 bypass 了对 Next.js 静态资源的鉴权要求。

或许这里有一些前端的哲学,但是我的需求确实被阻碍了。

有些开发者可能会说,静态资源就是可以被随意访问的,如果你的资源需要被保护,那可以用一个后端服务保护,或者丢到数据库里要求带着账号密码访问。但是我的一个基本诉求就是尽可能不要自己维护一个服务器。如果说这话的人能帮我像在某电商公司那样给我提供网关服务和数据库服务,接我的需求帮我解决,那我自然很乐得按照这种分工方案来实施。

我在 nextjs-auth0 的仓库里记了一个报告,但是看起来维护者不是很能理解我的需求。所幸最后我也没用 Next.js 的解决方案,静态文件路径不在被硬编码 bypass 的 /_next 路径下,也就没了这个问题。

Docusaurus + Next.js 的最终方案

出于以上问题的考虑,加上 Next.js 原生的内容渲染框架实在是没有跟 Docusaurus 能打的选择,最终我选择的是 Docusaurus 框架生成静态网站,然后用 Next.js 框架托管这些静态文件的方案。这样,就能跟 Next.js 生态的其他插件尤其是 @auth0/nextjs-auth0 集成上了。

实际项目结构说来也简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
.
├── LICENSE
├── README.md
├── gateway
│   ├── README.md
│   ├── middleware.ts
│   ├── next-env.d.ts
│   ├── next.config.js
│   ├── package.json
│   ├── pages
│   ├── public
│   └── tsconfig.json
├── package.json
├── source
│   ├── README.md
│   ├── babel.config.js
│   ├── build
│   ├── docs
│   ├── docusaurus.config.js
│   ├── package.json
│   ├── sidebars.js
│   ├── src
│   ├── static
│   └── tsconfig.json
├── tools
│   ├── build.sh
│   ├── check-build.sh
│   ├── devel.sh
│   └── env.sh
├── vercel.json
└── yarn.lock

核心构建逻辑如下:

1
2
3
4
5
6
7
8
9
10
pushd ./source
yarn build
popd

rm -rf ./gateway/public
cp -va ./source/build ./gateway/public

pushd ./gateway
yarn build
popd

其中 Docusaurus 框架管理的 source 目录按照正常 Docusaurus 网站的方式开发。Next.js 框架管理的 gateway 目录,需要提供 Auth0 集成的两个文件。

第一个是创建 pages/api/auth/[auth0].js 文件以支持 Auth0 的鉴权流程的一系列 API 接口,默认会生成以下端点:

  • /api/auth/login
  • /api/auth/logout
  • /api/auth/callback
  • /api/auth/me
  • /api/auth/401

为了处理用户鉴权成功但是权限验证失败的情况,我加了一个 /api/auth/403 端点,最终代码如下:

1
2
3
4
5
6
7
8
9
10
11
import { handleAuth } from '@auth0/nextjs-auth0';

export default handleAuth({
403: (req, res) => {
const user = JSON.parse((req.query.user as string));
res.status(403).json({
error: 'forbidden',
...user,
})
}
});

第二个是创建 middleware.ts 文件定义中间件处理逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import {getSession, withMiddlewareAuthRequired} from "@auth0/nextjs-auth0/edge"
import {NextResponse} from "next/server"
import {Claims} from "@auth0/nextjs-auth0"
import path from "path-browserify";

export default withMiddlewareAuthRequired(async function middleware(req) {
const res = NextResponse.next()
const {pathname, origin} = req.nextUrl

const session = await getSession(req, res)
if (!session?.user) {
return NextResponse.rewrite(new URL('/api/auth/401', origin), {status: 401})
}

const user = session.user
if (isWhitelistUser(user)) {
if (path.extname(pathname)) return
if (pathname.startsWith('/api')) return

// WHAT: specially handle / or /docs case.
//
// WHY: Next.js will intercept these urls and result in a 404 page.
// We should append an index.html segment to succeed in the first Docusaurus pages loading.
// Then the Docusaurus SPA will take over the control flow.
return NextResponse.redirect(new URL(path.resolve(pathname, 'index.html'), origin))
}

const forbidden = new URL(`/api/auth/403?user=${encodeURIComponent(JSON.stringify(user))}`, origin)
return NextResponse.rewrite(forbidden)
});

除了检验鉴权后用户的访问权限,还有就是处理 Docusaurus 生成的页面,变成 Next.js 的静态资源以后,首次加载如果不是直接访问 HTML 文件,会因为误入 Next.js 框架的解析逻辑而 404 的问题。

具体配置 nextjs-auth0 集成的方法,可以参考官方文档示例。虽然有上面提到的静态文件被无情 bypass 的设计问题,但是总的来说这个集成的文档体验是相当好的。

最终效果,是当你访问任何受保护链接(即除了几个鉴权相关 API 端点以外的链接,包括静态文件)的时候,都会被重定向到 Auth0 提供的鉴权登录页,成功登录后会根据你是否在用户白名单上,分别导向实际页面内容,或者 403 页面。出于内容保护的考虑,这里就不插入对应的动态图演示了。

最终费用是 $0 开销。

应用框架是开源的,无需付费。Vercel 的免费计划对于个人网站来说完全够用,Auth0 的免费额度也是没什么问题的,主要限制在只支持两个内置集成,我实际也只需要 GitHub 和 Google 这两个,甚至只要 GitHub 也行,这样就没有付费的理由了。

Bonus: Logout Button

大家司空见惯的登出按钮其实也是需要开发的。否则一旦成功登录,除非你知道登出的 API 接口是啥,Session 记录会让你在清理缓存之前永远也无法退出。

Docusaurus 最适合做登出按钮的位置就是导航栏右上角,但是 Docusaurus 的导航栏是框架内定义的,预定义的类型里没有点击按钮回调的类型,只有点击图标跳转页面的能力。

一开始,我做了一个点击登出图标后跳转到登出页面,在登出页面里做了登出按钮,但是这个体验就很别扭。所幸我发现了 Docusaurus 上游针对自定义导航栏项目的没有记录在文档的支持,做了一个流畅的登出按钮。

相关代码如下。

第一步,需要定义好登出按钮的组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// @site/src/components/LogoutButtonNavbarItem
import React from "react"
import clsx from "clsx"
import NavbarNavLink from '@theme/NavbarItem/NavbarNavLink'
import useSWR from 'swr'

const fetcher = url => fetch(url).then(r => {
if (r.ok) return r.json();
throw {status: r.status}
})

export default function LogoutButtonNavbarItem(props: {
mobile?: boolean;
position,
}): JSX.Element | null {
const {data} = useSWR('/api/auth/me', fetcher, {
shouldRetryOnError: (error) => {
// specially handle that Docusaurus returns a 404 page
return error.status != 404 && !`${error}`.startsWith('SyntaxError')
}
})

if (!data) return <></>

if (props.mobile) {
return <li className="menu__list-item">
<NavbarNavLink
onClick={() => {
window.location.href = "/api/auth/logout";
}}
className={clsx('menu__link', 'header-logout-link')}
aria-label={'logout button'}
/>
</li>
}

return <NavbarNavLink
onClick={() => {
window.location.href = "/api/auth/logout";
}}
className={clsx('navbar__item navbar__link', 'header-logout-link')}
aria-label={'logout button'}
/>
}

内容没有太多好说,为了在展示上跟点击图标有一样的样式效果,内容基本是抄的内置 DefaultNavbarItem 组件的逻辑。功能上,通过访问 /api/auth/me 端点来确认是否是用户登录状态。

第二步,将组件挂载到导航栏项目登记表里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// @site/src/theme/NavBarItem/ComponentTypes.tsx
import DefaultNavbarItem from '@theme/NavbarItem/DefaultNavbarItem';
import DropdownNavbarItem from '@theme/NavbarItem/DropdownNavbarItem';
import LocaleDropdownNavbarItem from '@theme/NavbarItem/LocaleDropdownNavbarItem';
import SearchNavbarItem from '@theme/NavbarItem/SearchNavbarItem';
import HtmlNavbarItem from '@theme/NavbarItem/HtmlNavbarItem';
import DocNavbarItem from '@theme/NavbarItem/DocNavbarItem';
import DocSidebarNavbarItem from '@theme/NavbarItem/DocSidebarNavbarItem';
import DocsVersionNavbarItem from '@theme/NavbarItem/DocsVersionNavbarItem';
import DocsVersionDropdownNavbarItem from '@theme/NavbarItem/DocsVersionDropdownNavbarItem';

import LogoutButtonNavbarItem from '@site/src/components/LogoutButtonNavbarItem';

import type {ComponentTypesObject} from '@theme/NavbarItem/ComponentTypes';

const ComponentTypes: ComponentTypesObject = {
default: DefaultNavbarItem,
localeDropdown: LocaleDropdownNavbarItem,
search: SearchNavbarItem,
dropdown: DropdownNavbarItem,
html: HtmlNavbarItem,
doc: DocNavbarItem,
docSidebar: DocSidebarNavbarItem,
docsVersion: DocsVersionNavbarItem,
docsVersionDropdown: DocsVersionDropdownNavbarItem,

'custom-logout-button': LogoutButtonNavbarItem,
};

export default ComponentTypes

这里,src/theme 是个特殊路径,会覆盖 Docusaurus 框架对应路径的主题组件,对应功能文件在此

最后,将注册名为 custom-logout-button 的导航栏组件添加到站点配置文件里,注意自定义导航栏组件注册名必须以 custom- 开头:

1
2
3
4
5
6
7
8
9
10
// docusaurus.config.js
const config = {
themeConfig: ({
navbar: {
items: [
{type: 'custom-logout-button', position: 'right'},
]
}
}),
}

这里唯一没有提供源码的是关联到样式类型 header-logout-link 上的登出按钮样式,这个可以在网上找到自己喜欢的 SVG 图片用一下的方式添加到 src/css/custom.css 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.header-logout-link:hover {
opacity: 0.6;
}

.header-logout-link::before {
content: '';
width: 24px;
height: 24px;
display: flex;
background: url("data:image/svg+xml...") no-repeat;
}

[data-theme='dark'] .header-logout-link::before {
filter: invert(100%);
background: url("data:image/svg+xml...") no-repeat;
}

Bonus: SVG Editor

编辑 LOGO 和按钮 SVG 文件的时候,深深感受到了一个好的 SVG Editor 的价值。别的也不多说了,上连接:

【前端十万个为什么】V0

为什么用 Composition API

c2f54277b2fb37d52596daa96202d926.png

  1. 使逻辑分离更容易:使用可组合函数可以将组件的逻辑分解为多个较小的部分,不再限制于 Options API 中组织代码,更容易重用有状态逻辑。
  2. 灵活性和代码重用性:可以提取和重用共享逻辑,分离关注点,使代码更加模块化和易于维护。
  3. 实现组件的复用和组合:将逻辑拆分为可重用的部分,使用可组合函数组合组件,避免重复编写代码,提高代码的重用性,减少重复和不一致性的风险。
  4. 更好的可读性和可理解性:每个可组合函数封装了特定方面的行为、方便推理和测试、有助于团队合作,使代码结构化和有组织。
  5. 更好的类型推断:使用变量和标准 JavaScript 函数处理组件逻辑。更容易在使用静态类型系统(如 TypeScript)构建大型 Vue 应用程序时进行类型推断

为什么解构 Proxy 会失去效应性

等待补坑

为什么 Vue 中解构 props 会失去响应性

在 Vue 3 中,当你解构 props 时,可能会丧失响应性。这意味着对 props 的更改不会触发组件的更新。

原因是 Vue 的响应性系统依赖 Proxy 来跟踪对象属性的更改。当组件接收到一个对象作为 props 时,Vue 会为该对象的每个属性设置响应性的 getter 和 setter。这使得 Vue 能够检测属性何时更改并相应地更新组件。

当你解构对象 props 时,实际上是创建了一个不再具有响应性的新对象。Vue 为原始对象创建的响应性 getter 和 setter 不会转移到新对象上。

参考

  1. Vue3 如果解构 props 会失去起响应性导致 setup 里一堆 pros.xxx 怎么办? - 这似谁的小鹿的回答 - 知乎
  2. Vue Tip: Destructure Props in Composition API Without Losing …
  3. How To Destructure Props In Vue 3 Without Losing Reactivity | by Nicky Christensen | Medium

为什么 Vue 项目很少用 RxJS

一句话解释

这是因为 Vue 希望成为一个轻量且灵活的框架,允许开发者选择他们喜欢的工具和库。虽然 RxJS 是一个强大的响应式编程库,但 Vue 采用了不同的方法,提供了自己的响应式系统。

详情点

  • 设计理念不同:Vue 注重简单和直观,便于响应式编程;而 RxJS 功能更强、更复杂,适合异步和事件驱动编程。
  • 库的大小与复杂性:RxJS 库大且学习曲线陡,若作为 Vue 的默认依赖,会增加框架大小和开发复杂性,与 Vue 的轻量和灵活理念不符。
  • 灵活性:Vue 设计为灵活且适应不同的项目需求。不将自己绑定到特定的响应式库(如 RxJS),允许开发人员选择最适合他们需求的工具和库。这种灵活性使开发人员能够无缝地将 RxJS 或其他任何库集成到 Vue 项目中。
  • 学习曲线:Vue 拥有平缓的学习曲线,特别是对于初级开发。通过提供自己的响应式系统,Vue 可以提供更简单、更渐进的学习体验。

参考

  1. Introduction to VueJS and RxJS - This Dot Labs
  2. Integrating RxJS with Vue.js | DigitalOcean
  3. Reactive Programming: The Good and the Bad | goodguydaniel.com
  4. A better practice to implement HTTP client in Vue with RxJS for enterprise Apps | by Pawel Woltschkow | Medium
  5. You might not want Rxjs

JavaScript 总结、比较 V2

Promise 与 RxJS Observables 的区别

Promise

  • Promise 是 JavaScript 中内置的,不需要任何额外的库。
  • Promise 表示可能现在或将来可用的单个值。
  • Promise 是急切的,也就是说一旦 Promise 被解析,.then()回调会立即执行。
  • Promise 只能发出单个值。
  • Promise 非常适合处理产生单个结果的简单异步操作。

RxJS Observables

  • Observables 是 RxJS 库的一部分,需要额外安装依赖。
  • Observable 表示可以随时间发出的值流。
  • Observable 是惰性的,也就是说在订阅之前不会执行任何操作。
  • Observable 可以发出多个值,包括零个或多个值。
  • 可以使用各种 RxJS 操作符对 Observable 进行转换和组合,以创建新的定制流。
  • Observable 非常适合处理复杂的异步操作,例如实时数据流或事件驱动编程。

参考

  1. JavaScript Theory: Promise vs Observable - Medium
  2. angular - What is the difference between Promises and Observables? - Stack Overflow
  3. JavaScript Promises vs. RxJS Observables

模版语法的简单实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const name = 'Nexmoe';const message = 'My name is {{name}} and I\'m {{getAge(20)}} years old.';function getAge(age) {  return age;}const replacedMessage = message.replace(/\{\{(.*?)\}\}/g, (match, variableOrFunction) => {  const trimmedValue = variableOrFunction.trim();  if (trimmedValue.includes('(')) {  // 如果占位符包含括号,则表示为带参数的函数替换    const [functionName, ...args] = trimmedValue.split(/\(|\)/).filter(Boolean);    const func = eval(functionName);    return func(...args);  } else {  // 否则为变量替换    return eval(trimmedValue);  }});onsole.log(replacedMessage);

先检查占位符中是否包含括号,如果包含括号,则表示是一个带参数的函数调用。使用split方法和正则表达式来解析函数名和参数,并将其存储在functionNameargs变量中。然后,使用eval函数将函数名转换为实际的函数对象,并使用扩展运算符 (...) 将参数作为参数列表传递给函数。函数执行后,将返回值作为替换后的字符串返回。

如果占位符不包含括号,则表示是一个变量。直接使用eval函数将变量名转换为实际的变量值,并返回其值作为替换后的字符串。

⚠️ 注意:使用eval函数执行代码具有一定的安全风险,因为它可以执行任意的 JavaScript 代码。有相当多的建议建议不使用eval。准备过段时间研究研究不用eval的方法。

MVVM 是什么

MVVM 代表 Model-View-ViewModel,在 MVVM 中,Model 表示应用程序的数据和业务逻辑,View 表示用户界面,ViewModel 充当 Model 和 View 之间的中介。

模型(Model)

  • 模型代表应用程序中的数据和业务逻辑。
  • 它可以是从服务器获取的数据、本地存储的数据或通过其他方式获取的数据。
  • 模型通常实现了一些方法来操作、存储和管理数据。
  • 对应的是组件中的 data、props 属性。

视图(View)

  • 视图是用户界面的可见部分。
  • 它负责展示数据给用户,并接收用户的交互操作。
  • 在 Vue.js 中,视图通常由 Vue 组件表示,可以包含 HTML 模板和样式。

视图模型(ViewModel)

  • 视图模型是连接模型和视图的中间层。
  • 视图模型通常包含了与视图相关的数据、计算属性和方法,以及与模型交互的逻辑。
  • 通过双向绑定(data-binding)将视图和模型连接起来。当模型中的数据发生变化时,视图会自动更新。通过 DOM 事件监听,当用户在视图中输入数据或进行其他交互操作时,视图模型会自动更新模型中的数据。

优势

  • 分离关注点:将数据逻辑与视图逻辑分离,使代码更易于维护和测试。
  • 提高开发效率:通过双向数据绑定和声明式编程风格,减少了手动操作 DOM 的代码量。
  • 可重用性:通过组件化的方式,视图和视图模型可以在不同的应用程序中进行复用。
  • 响应式更新:当模型中的数据发生变化时,视图自动更新,提供了更好的用户体验。

参考

  1. 为什么尤雨溪尤大说 VUE 没有完全遵循 MVVM? - 知乎
  2. Vue 的 MVVM 思想(包含三个常见面试题) - 掘金
  3. MVC,MVP 和 MVVM 的图示 - 阮一峰的网络日志
  4. Getting Started - vue.js
  5. Comparing Vue.js to new JavaScript frameworks - LogRocket Blog

MVC 是什么

MVC 这个概念已经存在很久了,用了这么多年,今天了解一下概念做个总结。

MVC(Model-View-Controller)设计模式将应用程序中的对象分为三个角色:模型(Model)、视图(View)和控制器(Controller)。该模式不仅定义了对象在应用程序中的角色,还定义了对象之间的通信方式。每种类型的对象都通过抽象边界与其他类型的对象分离,并在这些边界上与其他类型的对象进行通信。应用程序中某种 MVC 类型的对象的集合有时被称为层,例如模型层。

848723f97c7a1b862e10abe0445da348.png

模型(Model)

  • 封装应用程序特定的数据,并定义操作和处理数据的逻辑。
  • 可以表示应用程序中的实体,如游戏中的角色或地址簿中的联系人。
  • 可以与其他模型对象建立关联,形成对象图。
  • 应该存储应用程序的持久状态数据。
  • 不应与呈现数据和用户界面相关的视图对象直接连接。

视图(View)

  • 用户可见的对象,负责显示数据和响应用户操作。
  • 知道如何绘制自身,并可以与用户进行交互。
  • 通常通过控制器对象从模型对象中获取数据进行展示和编辑。
  • 在 MVC 应用程序中与模型对象解耦,提供一致性和重用性。

控制器(Controller)

  • 充当视图对象和模型对象之间的中介。
  • 负责处理用户操作,并将其传递给模型层进行数据处理和更新。
  • 可以执行应用程序的设置和协调任务,管理其他对象的生命周期。
  • 在模型对象发生变化时,将新的模型数据传递给视图对象进行显示。

优势

  • 提供良好的应用程序设计,使对象更具可重用性和接口定义明确性。
  • 支持应用程序的可扩展性,易于添加新功能和模块。
  • 分离关注点,使代码更易于维护和测试。
  • 应用程序的模型层、视图层和控制层之间保持了清晰的分离,实现了代码的结构化和职责的明确划分,从而提高了应用程序的可维护性和可扩展性。

参考

  1. https://developer.apple.com/library/archive/documentation/General/Conceptual/DevPedia-CocoaCore/MVC.html

为什么 Bun 这么快

JavaScriptCore 引擎

Bun 使用 JavaScriptCore 引擎,这是 Safari 浏览器使用的引擎,而不是基于 Chromium 的浏览器和 Node.js 使用的 V8 引擎。JavaScriptCore 引擎经过了针对更快启动时间的优化,这有助于 Bun 的速度。

性能分析和优化

大量的性能优化。Bun 的初衷就是要快。[2]

Zig 语言

Bun 利用 Zig 语言进行低级内存控制和消除隐藏控制流。Zig 的设计原则注重性能,通过利用 Zig,Bun 可以实现更好的内存管理和控制,从而提高速度 [2]

参考

  1. Bun 1.0 | Bun Blog
  2. A first look at Bun: is it really 3x faster than Node.js and Deno? - DEV Community

自建 Sentry 使用 script 启用时无效

最近在服务器上面自建了 Sentry。

用 script 方法加入到网站后,始终没有效果,然后在控制台中发现了下面的报错。

The Sentry loader you are trying to use isn’t working anymore, check your configuration.

于是去 Github 上找了下,看着应该是国内网络的问题。

在 sentry/sentry.conf.py 内添加下面内容可以解决。

1
JS_SDK_LOADER_DEFAULT_SDK_URL = "https://browser.sentry-cdn.com/%s/bundle.tracing.replay.debug.min.js"

然后记得重启 Sentry 的 Docker 服务

1
2
sudo docker compose restartsudo docker compose up -d

动画 PNG(APNG) 转 GIF 并无限循环

今天在网上找了些 PNG 格式的动态表情包我是不会告诉你是我是在 LINE 偷的表情包的,于是了解到是 APNG 这种格式。由于微信和 QQ 不支持 APNG,所以就把 APNG 转为 GIF 了,在使用 APNG 转换成 GIF 后,发现在微信上只能播放一次,就产生了如何批量修改 GIF 的循环次数的问题。

所以准备简单介绍一下 APNG。并提供了一个在线工具,可以将 APNG 批量转换为 GIF,但是该工具不能实现无限循环。所以分享了一个批量修改 GIF 循环次数的方法,使用了 Node.js 和批处理脚本两种不同的实现方式。方便 Node 开发者和使用 Windows 的普通用户直接批量处理。

APNG 是什么?

APNG(Animated Portable Network Graphics)是 PNG 的位图动画扩展,可以实现 PNG 格式的动态图片效果。APNG 相比于 GIF 在图片质量和细节表现方面更有优势,而且随着越来越多的浏览器对 APNG 的支持,它有望成为下一代动态图的标准之一。主要有以下区别:

  1. 图片质量:GIF 最多支持 256 种颜色,并且不支持 Alpha 透明通道,这导致 GIF 在色彩丰富的图片和含有半透明效果的图片上质量较差。而 APNG 可以支持更高质量的图片,包括更多的颜色和 Alpha 透明通道,使得动画效果更加细腻。

  2. 构成原理:APNG 和 GIF 都是由多帧构成的动画,但是 APNG 的构成原理允许超自然向下兼容。APNG 的第一帧是标准的 PNG 图片,即使浏览器不认识 APNG 后面的动画数据,也可以无障碍显示第一帧。而如果浏览器支持 APNG,就可以播放后面的帧,实现动画效果。

  3. 浏览器支持:从 Chrome 59 开始,Chrome 浏览器开始支持 APNG,使得大部分浏览器都能显示 APNG 动画。唯独 IE 浏览器不支持 APNG。

更多内容请参考:https://xtaolink.cn/268.html

APNG 批量转 GIF

该工具可以批量将 APNG 转为 GIF,不过不能无限循环。

https://cdkm.com/cn/png-to-gif

批量修改 GIF 为无限循环

bat(普通用户请使用该方法)

下面是使用批处理脚本(.bat)来实现相同的功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@echo offsetlocal enabledelayedexpansionset "directoryPath=C:\path\to\directory"for /r "%directoryPath%" %%f in (*.gif) do (    echo Modifying %%~nxf    call :modifyGif "%%f")exit /b:modifyGifset "filePath=%~1"set /p data=<"%filePath%"set "index=!data:~0,16!"set "modifiedData=!data:~0,16!!data:~16,1!!data:~17,1!!data:~19!"echo.!modifiedData!>"%filePath%"exit /b

请将C:\path\to\directory替换为实际的目录路径。将上述代码保存为.bat文件,双击运行即可。脚本将遍历指定目录下的所有.gif文件,并对其进行修改。

请注意,批处理脚本的功能相对有限,无法直接读取二进制文件。上述脚本通过读取文件的第一行来模拟读取文件内容。在修改文件时,它直接将修改后的数据写入文件,而不进行二进制操作。这种方法可能不适用于所有情况,尤其是处理大型文件时可能会有性能问题。如果需要更复杂的二进制文件处理,请考虑使用其他编程语言或工具来实现。

Node(Nexmoe 使用的该方法)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
const fs = require('fs');const path = require('path');function unlimitedGifRepetitions(path) {  const data = fs.readFileSync(path);  const index = data.indexOf(Buffer.from([0x21, 0xFF, 0x0B]));  if (index < 0) {    throw new Error(`Cannot find Gif Application Extension in ${path}`);  }  data[index + 16] = 255;  data[index + 17] = 255;  return data;}function batchModifyGifFilesInDirectory(directoryPath) {  fs.readdir(directoryPath, (err, files) => {    if (err) {      console.error('Error reading directory:', err);      return;    }    files.forEach(file => {      const filePath = path.join(directoryPath, file);      const fileExtension = path.extname(file);      if (fileExtension === '.gif') {        try {          const modifiedData = unlimitedGifRepetitions(filePath);          fs.writeFileSync(filePath, modifiedData);          console.log(`Modified ${file}`);        } catch (error) {          console.error(`Error modifying ${file}:`, error);        }      }    });  });}const directoryPath = './path/to/directory';batchModifyGifFilesInDirectory(directoryPath);

请注意,上述代码使用了 Node.js 的文件系统模块(fs)来读取和写入文件。此外,需要将./path/to/directory替换为实际的目录路径。在执行该脚本之前,请确保已经安装了 Node.js。

该脚本将批量遍历指定目录下的所有文件,并对后缀名为.gif的文件调用unlimitedGifRepetitions函数进行修改。修改后的数据将写回原始文件。在控制台输出中,你可以看到每个已修改的文件的信息或任何出现的错误信息。

更多内容参考:https://www.b612.me/golang/232.html

更好的工具

这个批处理工具可以将多个 APNG 文件批量转换为 GIF 文件,并且可以对转换后的 GIF 文件批量设置为无限循环。

https://github.com/nexmoe/batch-apng2gif

用 VS Code 管理服务器,我有独特的服务器管理方式

自从 Typora 开始收费之后,我彻底转向使用 VS Code(Visual Studio Code) 进行文章撰写。作为一款 Markdown 编辑器,它已经完全满足了我的需求。

然而,我开始思考是否可以将 Visual Studio Code 用作服务器管理器。在此之前,我一直使用 FinalShell 进行服务器管理,这款工具具备以下特点,也是我所喜欢的:

  • 简单的服务器资源监控
  • 服务器文件浏览
  • 终端功能
  • 多服务器管理

当 VS Code 发布 Remote SSH 功能时,我并没有太过关注。该功能的初衷是用于远程开发,而我并没有远程开发的需求。然而,当我开始进行远程开发时,我决定尝试使用 Remote SSH。结果令我惊喜地发现,Remote SSH 非常适合用于服务器管理工作。

那么,VS Code 有哪些特点,并且满足了我哪些需求呢?

VS Code 的特点

多服务器管理

通过在你需要访问的远程服务器中配置好 SSH Key,便可以轻松地设置 Remote SSH,并指定服务器的 IP 地址,从而直接通过 Remote SSH 访问服务器。

1
2
3
4
5
6
7
Host server1    HostName 服务器 1 的 IP 地址    User 用户名Host server2    HostName 服务器 2 的 IP 地址    User 用户名

通过这样的配置,然后就可以方便地在 VS Code 中访问和管理多个服务器。只需单击服务器列表中的相应服务器,即可快速连接到目标服务器,并在远程环境中执行所需的操作。

文件浏览器

和正常一个项目的使用一样,VS Code 可以打开服务器的一个文件夹。然后我们就可以进行各种各样的文件浏览器的常规操作。还可以使用搜索功能快速查找和定位特定的文件。

我通常就会直接把账号目录直接打开。

同时还可以直接集成 VS Code 本身拥有的强大的文件编辑能力。

从此告别 lsmkdirtouchvi 等操作。

方便快捷的终端体验

可以同时打开多个终端实例,并在它们之间切换。每个终端实例都可以独立运行命令,并保留其输出历史记录。

还可以在编辑器的多个标签页或分割视图中同时打开不同的终端实例,以便同时执行不同的命令。

除此之外,在 VS Code 中,可以通过右键单击文件或文件夹,选择"在终端中打开",快速打开终端并自动切换到对应的路径。再也不用痛苦的到处 cd 了。

Docker GUI 管理

由于我大部分服务都是放在 Docker 上,所以对于 Docker 的使用是非常高频的。

如果你在服务器上使用 Docker 进行容器化管理,VS Code 通常就会推荐你安装 Docker 扩展,然后就可以并以图形化界面的形式管理和操作 Docker 容器。可以方便地查看和管理容器、镜像、网络和卷等 Docker 资源,执行常见的 Docker 命令,以及监控容器的状态和日志。

这基本覆盖了大部分 Docker 常用的操作,也让我大部分时间不再需要使用 Portainer 或是命令行了。

Git GUI 集成

VS Code 提供了强大的 Git GUI 集成功能,可以在代码仓库上进行版本控制和协作。可以直接通过 GUI 进行查看提交历史、比较文件差异、切换分支、合并代码,以及推送和拉取代码等操作。

丰富的个性化主题、插件生态

如果本身就使用 VS Code,便可以直接继承自己的审美。同时 VS Code 比其他终端管理软件拥有更为丰富丰富的个性化主题选项,可以根据自己的喜好和习惯选择适合的主题。也具有丰富的插件社区。

其它

除此之外,VS Code 本身就具有跨平台支持、多语言支持等特性,而且它完全免费。

如何使用 VS Code 进行服务器管理

你需要满足如下条件

  • 能够运行 VS Code 的电脑
  • 正版的 VS Code
  • 生成 SSH Key 并完成配置
  • Remote SSH 插件

其实你需要的并不多,核心是 VS Code 中的 Remote SSH 插件。

配置 SSH Key

你需要在你的电脑上生成 SSH Key,并将公钥配置到服务器上以实现免密登录。你可以在网上查找相关教程了解如何生成和配置 SSH Key。

配置 Remote SSH

通过 这个链接 安装 Remote SSH 后。

你可以按照以下步骤来打开和配置 SSH 主机:

  1. 在 VS Code 左下方找到 Remote SSH 的图标按钮。
  2. 点击该按钮,然后选择 “Connect to Host” 选项。
  3. 再次点击 “Configure SSH Hosts”。

就可以在 VS Code 中进行配置,指定服务器的 IP 地址和用户名。示例配置如下:

1
2
3
4
5
6
7
Host server1    HostName 服务器 1 的 IP 地址    User 用户名Host server2    HostName 服务器 2 的 IP 地址    User 用户名

完成配置后便可以在左下角直接连接服务器了。

插件以及其他推荐

Monitor Pro 插件

资源监控,由我开发。

Monitor Pro 是一款资源监控工具,实时跟踪系统指标。监测 CPU、内存、网络、文件系统使用率,电池百分比和充电状态。可自定义顺序和刷新间隔,提供高占用警报。适用于开发人员、系统管理员和普通用户。

https://marketplace.visualstudio.com/items?itemName=nexmoe.monitor-pro

Docker 插件

如果你在使用 Docker 容器来部署应用程序,这个插件可以帮助你在 VS Code 中管理和调试 Docker 容器。

Wakatime 插件

统计你在服务器上的摸鱼时间。

zsh 与 ohmyzsh

使用 ohmyzsh 以及它的生态,为你的命令行集成自动补全和纠错功能。

累积布局偏移优化 CLS 完全指南

什么是布局偏移

一个十几秒的短视频解释清楚。

更详细的解释是:布局偏移指的是在网页上发生突然变化时,页面中的内容位置发生意外移动的现象。这种情况常常让人感到困扰,因为它会导致阅读中断或误操作。布局偏移通常是由于资源异步加载或动态添加到页面上的 DOM 元素导致的。可能的原因包括具有未知尺寸的图像或视频、字体与其备用字体渲染大小不同,或者第三方广告或小部件动态调整大小。

难受的是,网站在开发过程中的功能通常与用户体验有很大不同。个性化或第三方内容在开发中的行为通常与生产环境中不同,测试图像通常已经存在于开发者的浏览器缓存中,本地运行的 API 调用通常非常快,延迟几乎不可察觉。

什么是 CLS

累积布局偏移 CLS(Cumulative Layout Shift)是一个指标。

是对页面整个生命周期中发生的每个意外布局变化的最大布局变化分数的度量。

CLS 通过测量实际用户遇到布局偏移的频率来帮助解决布局偏移问题。它可以帮助开发者了解布局偏移在真实用户中发生的情况,从而采取相应的措施进行修复。

为什么要优化 CLS

布局偏移是一个非常影响用户体验的问题,通过上面那一个简短的视频也能理解。

布局偏移通常会导致意外点击、页面方向的迷失,最终导致用户受挫。用户往往不会逗留太久。有时也会使用户不按照预计的产品流程走。

通常优化好布局偏移能够很好的提高用户粘性、用户停留时间等指标。

Yahoo! JAPAN News 通过降低 CLS 0.2 分,得到如下成果。

如何降低 CLS

图片等媒体元素占位

在图像、视频等媒体资源元素中始终包含宽度和高度大小属性。或用 CSS 中的 min-heightaspect-ratio 或类似的方式保留所需的空间。

aspect-ratio

可以用来直接指定当前元素的比例。

https://developer.mozilla.org/zh-CN/docs/Web/CSS/aspect-ratio

对浏览器的支持:

padding-bottom

如果考虑浏览器支持问题仍然可以考虑使用目前一个被广泛接受的基解决方案 “Padding-Top Hack”。这个解决方案需要一个父元素和一个绝对型的子元素。然后计算出长宽比的百分比来设置为 padding-top。例如:

1
2
3
<div class="container">  <img class="media" src="..." alt="..."></div>
1
2
3
4
5
6
7
8
9
10
.container {  position: relative;  width: 100%;  padding-top: 56.25%; /* 16:9 Aspect Ratio */}.media {  position: absolute;  top: 0;}

使用不易产生偏移的 CSS

其中 transfrom 表现很好,以下举几个例子。
用例可以在这里找到:https://play.tailwindcss.com/26PxFA6UVI

zoom VS transform: scale

zoom 会撑大页面并向右偏移时,transform: scale 只是在原地放大。

margin VS transform: translate

margin 造成父元素变大,transform: translate 只是让当前元素移动。

border VS box-shadow

border 会撑起父元素,而 box-shadow 并不会。

小心你的懒加载

懒加载会引起布局的偏移,如果你在有懒加载长列表的里进行跳转,请小心!
无动画进行跳转,能够一定程度上避免该问题。

小心使用 transition: all

在页面首次加载或者跳转页面时,transition: all 可能会导致元素的 padding 等从参数为 0 开始渲染,照成页面的抖动。

这都是痛:
Commit:表格以及友情链接图标抖动
Commit:修复导航栏抖动问题

标签顺序导致的偏移问题

由于在移动端上优先展示主要内容,因此侧边栏的 markup 位于主要内容的后面;而在更大的屏幕上,则通过设置 CSS order 的方式进行排序,将主要内容移到中间(即第二列),伪代码如下:

1
2
3
4
5
6
7
8
9
export default function MainLayout(props) {  return (    <Container>      <Main className={css`@media screen and (min-width: breakpoint) { order: 0 }`} />      <Left className={css`@media screen and (min-width: breakpoint) { order: -1 }`} />      <Right className={css`@media screen and (min-width: breakpoint) { order: 1 }`} />    </Container>  )}

浏览器在首次绘制时并没有完整解析 DOM、只知道 <Main /> 的存在、但不知道 <Left /> 或者 <Right /> 的存在,才因此将 <Main /> 渲染进第一列而不是第二列;直到第二次绘制时,浏览器才将 <Main /> 渲染进第二列、将 <Left /> 渲染进第一列。

Chrome 并不是一次完整解析 HTML 的,在以下两种情况下,Chrome 会暂停解析、开始渲染和绘制:

  1. Chrome 解析器在读取了 65535 字节的 HTML 后暂停
  2. Chrome 在遇到 <script> 标签后,会继续读取约 50 个「Token」之后暂停

详细了解请看:优化博客的累计布局偏移(CLS)问题

网页跳转与前进后退缓存

默认情况下,所有浏览器都使用 bfcache,但由于各种原因,有些站点不适合使用 bfcache。有关如何测试和识别阻止 bfcache 使用的任何问题的更多详细信息,请阅读 bfcache 文章

在你离开后,bfcache 将页面保存在浏览器内存中很短的一段时间,所以如果你返回它们,那么它们将完全恢复为你离开时的样子。这意味着完全加载的页面立即可用,而不会出现任何变化。

现在的 SPA 应用也能很轻易的保证路由跳转页面布局的一致性。记住始终保持你的目录和导航栏在页面的固定位置。

字体

在下载和渲染网络字体之前,通常有两种处理方式:

  1. 使用网络字体替代备用字体(FOUT——未样式化文本的闪烁)。
  2. 使用备用字体显示“不可见”文本,直到网络字体可用并且文本可见(FOIT——不可见文本的闪烁)。

着两种方式都可能导致布局变化。即使文本是不可见的,它仍然使用备用字体进行布局。这意味着使用该字体的文本块以及周围的内容在网络字体加载时会发生布局变化,与 FOUT 的可见字体完全相同。

以下方法可以帮助你最小化这种问题:

  1. 使用 font-display: optional 可以避免重新布局,因为只有在初始布局时网络字体可用时才会使用它。
  2. 使用匹配度高的备用字体。例如,使用 font-family: "Google Sans", sans-serif; 将确保在加载"Google Sans"字体时使用浏览器的无衬线备用字体。如果只使用 font-family: "Google Sans" 而不指定备用字体,将使用默认字体,而在 Chrome 上默认字体是"Times",它是比默认无衬线字体的匹配度更差。
  3. 使用新的 size-adjustascent-overridedescent-overrideline-gap-override API 来尽量减小备用字体和网络字体之间的大小差异,详细信息请参阅“Improved font fallbacks”文章。
  4. 使用 Font Loading API 可以减少获取所需字体的时间。
  5. 使用 <link rel=preload> 尽早加载关键的网络字体。预加载的字体有更高的机会达到首次绘制,这样就不会发生布局变化。
  6. 阅读有关字体最佳实践的“Best practices for fonts”文章。

使用真正的骨架屏

骨架屏好坏示例

测量 CLS 分数

生产阶段

实验阶段

Lighthouse in DevTools

能够针对移动设备和桌面设备生成网页的实际性能报告,并能够提供关于如何改进相应网页的建议。

在本地开发期间从 DevTools 运行 Lighthouse 非常方便。

PageSpeed Insights

应该就是在线版的 Lighthouse。

Performance in DevTools

性能选项卡在 Chrome 的 DevTools 配置文件的所有页面行为在一段时间内记录。时间轴上会出现一个标记为“Experience”的图层,突出显示布局的变化和发生变化的元素。

Web Vitals extension

最好将 Web vital 扩展视为查找性能问题的抽查工具,而不是全面的调试工具——这是 Chrome 的 DevTools 中的性能选项卡的工作。

结语

作为一个对自己项目有较高要求的人,平常几乎都会接触到布局偏移优化或者 Lighthouse,只不过之前自己瞎折腾的时候还没有 CLS 这个概念,现在算是对 CLS 有了较为清晰的概念了。
CLS 作为一个非常基础的优化指标,在用户体验上非常重要,任何项目都应该针对 CLS 做优化。

如有勘误,请及时指出,感谢!

参考

  1. https://web.dev/cls/
  2. https://web.dev/optimize-cls
  3. https://developers.google.com/publisher-tag/guides/minimize-layout-shift
  4. https://web.dev/yahoo-japan-news/
  5. https://addyosmani.com/blog/infinite-scroll-without-layout-shifts/
  6. https://blog.skk.moe/post/fix-blog-cls/
  7. https://developer.mozilla.org/en-US/docs/Web/CSS/aspect-ratio
❌