阅读视图

登珠海凤凰山凤凰顶

✇雅余
作者Jeff

昨天到中山走岐澳古驿道不够过瘾,一路都是疯狂的蚊子,扛不住,走到三分之一就下撤了。今天找了一条新的轨迹,趁天气好爬到凤凰山的凤凰顶看看风景。路上发生两次轨迹偏离,第一次发生在上山路上,绕了一下找回正确轨迹,第二次下山直接走偏300米,同时下降200多海拔,想着也不会迷路,就干脆自己钻林子找路迹。全程7.43公里,最高爬升440海拔左右,从普陀寺对面的东坑爬至凤凰顶再回出发点,一个小环线。虽然今天黄色高温预警,但山里还是非常凉快。

登珠海凤凰山凤凰顶-雅余
轨迹已分享到“两步路”
登珠海凤凰山凤凰顶-雅余
进山前的路口

走进机耕道几百米,城市的喧闹声就基本听不到了。一路上只有泥土和花的味道,让人心情舒畅。

登珠海凤凰山凤凰顶-雅余
200海拔前基本都是30-40坡度硬化后的机耕道
登珠海凤凰山凤凰顶-雅余
眺望回对面山
登珠海凤凰山凤凰顶-雅余
山棯子

山棯子,小时候吃过不少这个果子。成熟的果子是紫黑色浆果的,可直接吃,也可酿酒,是鸟类喜欢的食物。

登珠海凤凰山凤凰顶-雅余
木荷花

木荷是山茶科,属大乔木,最高可达25米。这边山上挺多见,树冠开满了花,很好看。木荷为中国珍贵的用材树种,树干通直,材质坚韧。

登珠海凤凰山凤凰顶-雅余
眺望市区
登珠海凤凰山凤凰顶-雅余
很美,可以看到水库和大海
登珠海凤凰山凤凰顶-雅余
白花灯笼

白花灯笼是唇形科大青属灌木植物,白花灯笼因其花萼膨大似灯笼,花冠淡红色或白色稍带紫色而得名。

登珠海凤凰山凤凰顶-雅余
顶峰的观景台
登珠海凤凰山凤凰顶-雅余
顶峰能看到更完整的水库,可惜一片乌云遮挡了阳光
登珠海凤凰山凤凰顶-雅余
有阳光的山更好看
登珠海凤凰山凤凰顶-雅余
下次要去远处山上的气象站
登珠海凤凰山凤凰顶-雅余
眺望珠海地标“日月贝”
登珠海凤凰山凤凰顶-雅余
市区方向
登珠海凤凰山凤凰顶-雅余
钻林子
登珠海凤凰山凤凰顶-雅余
钻林子是一件开心的事情
登珠海凤凰山凤凰顶-雅余
被大片绿树环绕
登珠海凤凰山凤凰顶-雅余
下山后看到小溪,溪水还不多
登珠海凤凰山凤凰顶-雅余
出山口有一片水塘

登珠海凤凰山凤凰顶-雅余

By 理光 GR3

  •  

静风说15岁了

不知不觉这个博客已经诞生15年了,回想当初博客建立时的情景仿佛就在昨天。那时候谷歌还可以访问,韩寒还在写博客,天涯社区还没有关闭,微信还没有诞生,我还在用按键手机。现如今移动互联网极速生长,网络中心化愈加严重,中文网站逐渐消亡。不管网络环境如何变化,我仍然在写博客,博客更真实更纯粹地记录了我的生活,它已经是我密不可分的一部分了。

这些年为什么会一直在写,因为我有「表达欲」和「记录欲」,发生在生活里的小事,我总想记下来,可能它并没有特别的意义,想着记下来等以后再看。偶尔翻看写过的文章,总会感叹幸好有了这个博客,不然好多事情好多想法就不记得了。

这个博客的内容比较杂,没有特别的主题,也没有「垂直领域」,生活里发生了什么就会写什么,读了一本书、看了一部电影、去过一个地方、学到一点知识、冒出一点想法和思考,这些我会通通写到博客里。写的多且杂,文笔不好,深度也不够,通常都是想到哪写到哪,不润色,不修改,直抒胸臆后「写完就走」,大部分文章没什么可读性,读者估计也不爱看,可是我还是忍不住记录这些东西。

建站之初,我花了很大的精力和时间来鼓捣这个博客:加入Google Analytics,每天看有多少流量;添加这样和那样的功能,只要自己喜欢觉得读者也需要;想着怎么才能赚钱,接入Google AdSense;为了增加流量,把文章同步到各大平台。现在对这些都已经看淡,只想有个可以写字的地方,把博客简单地当作生活的笔记本。今年把域名续费了十年,打算一直写下去,继续用博客记录自己的生活和思考。

  •  

《跳出幼教看幼教》的目录

目录:      
      
   我为什么撰写这本书
     引言
       我看幼儿教育的立场与视域
       “育人为本”还是“儿童为本”
       面对幼儿教育的悖论
       被内卷的幼儿园教师
       对过度强调自主游戏的质疑
       对幼儿园园本课程的质疑
       对幼儿园淡化、弱化集体教学活动的质疑
       对幼儿园禁止幼儿识字的质疑
       对“幼儿园教师要读懂儿童这本书”的质疑
       对“幼儿园教师都要成为研究者”的质疑
       对幼儿园教师自创玩具的质疑
       要给幼儿园园长和教师松绑
       成也萧何,败也萧何(一)
       成也萧何,败也萧何(二)
    

  要学点哲学、历史、逻辑和科学
     引言
     立场与视角
       从生态学立场、视角看幼儿教育
       宏观、中观、微观和时序(一)
       宏观、中观、微观和时序(二)
       宏观、中观、微观和时序(三)
       从多种学科的视角看幼儿教育
     哲学与文化
       从人学的立场、视角看幼儿教育(一)
       从人学的立场、视角看幼儿教育(二)
       从教育人类学立场、视角看幼儿教育
       幼儿教育的文化适宜性
       中华优秀传统文化的内涵
       从中华文化与欧美文化的差异看幼儿教育
       东西方文化的主流价值观——集体主义VS个人主义
       我国幼儿教育的根本问题
       从儒家学说的哲学基础中庸之道看幼儿教育
       幼儿教育中的隐性文化逻辑
       传承中华优秀传统文化,警惕文化殖民和自殖民
       中华文化与幼儿创造性教育
     历史
       从历史的立场、视角看幼儿教育
       近数十年来美国政府导向下的幼儿教育
       美国课程标准化运动的历史借鉴
       美国的“儿童发展适宜性教育”
       近数十年来联合国教科文组织对幼儿教育做了些什么
       近数十年来经合组织对幼儿教育做了些什么
       近数十年来我国幼儿教育的理论与实践
       对我国近数十年来幼儿园课程改革的反思
     逻辑
       幼儿教育学的逻辑起点
       幼儿教育学的逻辑推演
       幼儿园课程编制和实施的逻辑一致性
       形式逻辑下的游戏与教学之间的关系
       从培养非逻辑思维的视角看幼儿教育
     科学
       要懂一点科学
       从AI的立场、视角看幼儿教育
       颠覆、降维和迭代
       培养能与AI合作、共享的人
       数智赋能是提升幼儿教育质量的关键
       AI赋能STEM教育,开启创新教育新篇章
       挑战与机遇并存
       面对AI时代,幼儿教育做些什么
      
     幼儿教育为什么
     引言
       全球化对我国幼儿教育的利弊权衡
       当今我国幼儿教育应该关注的主要问题
       我国的幼儿教育理应传承与弘扬中华优秀传统文化
       我国的幼儿教育为了什么
       陈鹤琴先生“活教育”的目的论
       我国幼儿园教育的魂、道、术
       不同的幼儿教育目的,推衍不同的幼儿教育实践(一)
       不同的幼儿教育目的,推衍不同的幼儿教育实践(二)
       道德与幼儿道德教育
       ——从东西方文化比较的视角审视道德与道德教育
       德育为先,立德树人
       集体主义与个人主义
       ——从东西方文化比较的视角审视幼儿教育的理念和实践
       规矩与自由
       ——从东西方文化比较的视角审视幼儿教育的理念和实践
       要为入小学做好准备
      
   幼儿教育做什么
     引言
     能够学的和应该学的
       教育内容选择的永恒标准是“真”“善”“美”
       国家意志主要决定幼儿应该学习什么
       幼儿发展水平主要决定幼儿能够学习什么
       幼儿教育中的关键经验与关键概念
       “教得对”,才有“教得好”和“教得实”
     德育
       要把中华优秀传统文化的基因嵌入幼儿的脑中
       将德育融入幼儿语言教育
       将德育融入幼儿数学教育
       将德育融入幼儿科学教育
       将德育融入幼儿体育
       将德育融入幼儿艺术教育
       将德育融入幼儿社会教育
       幼儿德育与心理健康教育
     各领域的幼儿教育
       从健康的立场、视角看幼儿教育
       从语言的立场、视角看幼儿教育
       早期阅读拉开了儿童之间的学习差距(一)
       早期阅读拉开了儿童之间的学习差距(二)
       从数学的立场、视角看幼儿教育
       早期逻辑思维是未来人才素养的重要根基
       从科学的立场、视角看幼儿教育
       早期科学启蒙是面向未来教育的基石
       从艺术的立场、视角看幼儿教育
       从社会学习的立场、视角看幼儿教育
       领域(学科)一般性与领域(学科)特殊性
      
    幼儿教育怎么做
     引言
     适时、适度
       从生物学的立场、视角看幼儿教育
       从遗传学的立场、视角看幼儿教育
       从儿童发展理论的立场、视角看幼儿教育(一)
       从儿童发展理论的立场、视角看幼儿教育(二)
       从儿童发展理论的立场、视角看幼儿教育(三)
       从学习理论的立场、视角看幼儿教育
       从卫生学的立场、视角看幼儿教育
       从诠释学的立场、视角看幼儿教育(一)
       从诠释学的立场、视角看幼儿教育(二)
     有效、愉悦
       游戏与教学的关系
       幼儿教育中的游戏与教学
       以游戏为主的幼儿教育活动
       以教学为主的幼儿教育活动
       教学是发展的源泉
       确保幼儿园教学有效性的起码条件
       分久必合,合久必分
       学习的核心——主动或被动
     环境创设
       从传统的环境定义出发,看幼儿教育环境的创设
       从几种生态学立场、视角看幼儿教育环境创设
       从人类发展生态学立场、视角看幼儿教育环境创设
       幼儿教育环境创设实务的要点
       教师专业成长
       一个教师专业成长中的世界性难题
       从教师专业发展和成长的立场、视角看幼儿教育
       幼儿园教师的德行
       幼儿园教师与课程资源
       幼儿园教师应做该做的、能够做的事情
      
  幼儿教育做得怎么样
     引言
       幼儿教育的“质量话语”与“超越质量”
       对幼儿教育质量评价的重新思考
       经合组织对学前教育质量的评价和监测
       对盲目引进国外认证标准的质疑
       从幼儿教育质量评价的视角看理论与实践的脱节
       教育目的达成度是幼儿教育评价的根本标准
       从接受“他评”到自主参与评价(一)
       从接受“他评”到自主参与评价(二)
       幼儿教育评价中的“学习故事”
      
     后记
      
     附录:与本书有关的我撰写过的主要书籍
  •  

薛定谔的电瓶

人不能太闲,我就是吃饱了撑的,车的电瓶有点亏电,但是还能正常启动,只不过门上的投影灯和天使之翼熄火以后亮不了几秒就灭,昨天婶子他们在晒麦子,我就把车挪过道底下,想着正好给电瓶充充电,于是让我爹找邻居借来了电瓶充电器,他说是给拖拉机用的,我也没多想直接插上开始充,起初一切正常,结果早晨起来去看,电瓶一点电都没了,车灯一会闪烁一下,雨刮器也慢速摆动,上网一查,好像是电瓶彻底亏电了。

好吧,下单了新的电瓶,不知道换上是不是能好,有些东西你不想、不碰就挺好,你要是心血来潮修理它、那等来的只会是「薛定谔的电瓶」。

上次这么无语还是我觉得电脑有点脏…记得那天的账单让我瞠目炸舌…

  •  

写了个网页版的提词器软件,就叫“CoCo提词器”

演播室配了俩提词器,题词软件不太好用,于是去年在网上魔改的一个提词器用了一年多,我们的主持人说用着还可以,但是看着臃肿和逻辑混乱的代码,心痒痒,要不我重新写个,顺便挑战下自己。

最近一直在使用 Trae 来淘汰 vscode,要不挑战下,看看 AI 能不能帮我写一个比较实用的工具软件。

经过好几天的研究和迭代,共改进了 40 多个版本,一步一步的实现出来了,当然中间还需要在 Ai 写代码的基础上帮 Ai 改代码,尤其是优化和逻辑的控制。比起之前用的那个上几千行代码的提词器,这个提词器仅仅就不到 600 多行的代码。

目前,基础功能已经完善,后续的话,打算做个快捷键配置界面,用于配置快捷键,比如有些题词的无线遥控器,需要映射对应的按键,才能正常工作。

主打的就是离线使用,当然也可以上传到自己的网站上。

目前版本是 0.1,后续随着功能的叠加,逐步升级吧,当然这个小工具是免费开源的。

下载地址:CoCo提词器_CoCoTeleprompter_v0.1.html

以后的更新全部放在这里,同时也同步到 Github 上,项目地址不出意外的话是 https://github.com/yefengs/CoCoTeleprompter/

使用说明:键盘空格为播放/暂停,方向键⬆⬇翻页⬅➡速度。

版本更新记录

  •  

制作了款 wordpress 主题 Cooooo

上一个博客主题《Memorable-lit》,缝缝补补也将就用了六七年,况且,博客也是将近一年多没有更新,好多篇博文静静地躺在了草稿箱里……

这次呢,想着借机写一款主题,形式依然是我喜欢单栏。由于很久不写代码,看到代码很生疏了,外加当前前端技术迭代太快了,好多HTML、css3的新特性和js的ES5、ES6,我处于完全看不懂的状态,这个主题的由来是我平时浏览网站的时候,看到别人好看的风格样式,就扒拉扒拉下来了,有的懒得动脑子,就直接参考和借鉴过来。整个主题是使用字节推出的 Trae CN 编辑器,本地电脑搭建环境,外加用上Trae 的 Ai 来写代码,效率是离谱的高,在这个主题绝大部分代码是使用 Ai 的写的。开发效率是有点高,尤其是在解决一些很复杂的逻辑时,你只需告诉 Ai 程序的基本逻辑,它便生成可用的代码,在前端库的选择, 当然我选择的是 jQuery,其实前端JS可以用原生来实现,但是用着jQuery很顺手,代码简单,一行代码就能搞定的,没必要绕弯。别人都在用高级的Hexo、Hugo 等高级,而我依然选择的是 wordpress,可能懒得折腾、懒得写作时用Markdown,也可能是保持了习惯和旧状态,新特性对我没有吸引力吧。

年初一段时间博客感觉被黑了,博客的插件里出现了一些奇奇怪怪的代码,感觉是木马后门之类的代码,之前博客的版本为4.8,也是年久失修的状态,可能存在能利用的漏洞,导致的博客沦陷,当前,wordpress 最新的版本为 6.8.1,借机新主题就在最新的版本上测试开发,前前后后感觉写了近两个月,部分代码也是参考了大佬们的代码,修修补补,也算是正常跑了起来。

当前博客跑在Nginx + Mysql + php 8.1 下,对于 2 核2G 的云主机来说够用,甚至剩余的算力可以跑跑 docker 里的小应用,整体相对比较顺畅。

这次换主题也顺便修修花、剪剪草和施施肥,对于改造博客我也是下了很大的决心,好在一点点雏形出现到基本能用的状态,也是可喜可贺。当然主题若有问题,欢迎评论区提出,当然这个主题足够完善并且很有必要的话,可能也会开源哦。

博客我加个视频播放器、音乐播放器和全景播放器,想着给博友们分享一些我外出拍摄的一些照片和内容吧。

  •  

京东和美团你选谁?

这几天京东和美团为抢夺外卖市场掐起架了,4月21日,京东一则《致全体外卖骑手兄弟们的公开信》让业内顿时炸锅。其 […]
  •  

尝试做了一个vscode的博客md文件管理工具

全程使用cursor和copilot协助,typescript和vscode插件开发本身一点不懂😂

唧唧歪歪

之前有尝试过在vscode里写博客的文章,虽然vscode的编辑器很好用,插件也很多了,但是文件夹里的文件默认都是按照文件名排序的,想找文章非常不便。以前没有什么办法,因为找不到相关的插件,自己也不会插件开发。现在有AI工具的帮助,自己实现插件就变得可以尝试了。

成品

在花了2,3个小时的不断与AI对话,调整,debug,最后做出了一版Blog文章管理插件,取名BlogMan,支持按照时间排序文章,按照tag和年份分类文章,且排序和分类方式可配。

插件会根据yaml的文件头,将文章的标题,tag, 时间等信息解析出来显示,并按照相关内容进行排序。文章的标题后会显示文章的时间,鼠标放在标题上还会显示文章的tag信息。

切换为年份分组后就会按照年份进行文章分类。

切换为标签分组后就会按照标签进行文章分类。

~~

现在还没有上架到vscode的商店,因为还不知道怎么上架,后面有空再研究研究好了,先自己用用看。不过AI真的开始改变编程开发了,细思极恐啊。

  •  

又到主机续费时

博客所在的阿里云虚机又到年度续费时了,同往常一样价格,没有优惠,但是价格后面还要加个括号(优惠88),天哪,捡 […]
  •  

国庆香港citywalk

国庆打卡香港,感受文化的交织与碰撞

趁着国庆,一个人从深圳福田口岸到香港,整一个Citywalk,两天时间第一天暴走37000步,第二天27000步,把香港主要的热门地点都转了转,感受很明显,和内地的任何一个城市都完全不一样,整个香港的感觉是冲突融合碰撞。

城市建设

香港的高密度建筑+高密度路网的布局,市区的每栋楼都挨在一起,基本上完全没有间隙,而且一栋或者两栋楼的宽度就是一条路。路都很窄,大量的单行道,每条路都没有非机动车道,除了主干道,其他很多路都没有红绿灯,都靠观察通过。虽然路很窄,但是路上的车开的都很快,通行效率还蛮高的。
机动车道路面水泥路面为主,而现在内地的各省市基本已经逐渐变为沥青路面了。人行道香港主要也是水泥铺装,而内地基本上都是人行道砖,目前走遍的各个城市基本都是一般。
香港的过街天桥很发达,有很多互联多个路口,连接公交地铁,同时还会连接路两边的楼栋,非常方便,通行效率也很高。不过这个应该也只有香港才能做到,其他很少有地方楼栋和道路挨的如此的近。

香港路上能看到不少在建设或者装修的大楼,建设过程中,脚手架都是用竹子做的,不知道是什么原因,内地现在应该都是钢制脚手架了。

公共交通

香港的公共交通,地铁+公交+有轨电车(叮叮车)的组合,车次都非常的密,经常路上一溜排的公交车,除了地铁稍贵,公交和有轨电车应该完全是靠补贴存在的。
公共交通主要覆盖的是主干道,很多支路还是只能靠走,因为路网密倒是不太远。
坐在叮叮车上,晃晃悠悠看遍路两边的感觉真的挺好的,3HK$坐多远都行。

商业

香港的商业让我感觉特别的割裂,一遍有着很多现代化的商业综合体,宽敞,现代化;
另一方面,街上的单面大多门脸开间特别小,每间店铺都有种家门口五金店塞得满满的感觉。
大概是这两年经济不景气,非常多的门面都倒闭关门招租中,即使在最繁华的尖沙咀和铜锣湾也不例外。
另外香港遍地的药店,特别的多,卖的东西也很杂。同时还有很多专门的类似于摊位一条街的存在,卖各种各样大小东西的地方,让我想起了我小时候去的城隍庙。

物价

香港的物价总体上感觉是内地的2倍到3倍,总的来说应该还是人工和房租贵,路边食室随便吃点6-70HK,电子产品等批量化的工业产品之类的倒是不贵些。香港的住宿很贵,条件稍可的都要超过1000了,寸土寸金啊。
最不能理解的还是矿泉水和饮料,超级贵,同时价格悬殊极大,便利店都是十几HK,而不少药店却很便宜,最便宜的我看到是2.5;香港的蜜雪冰城柠檬水是9HK, 所以在大多时候,比矿泉水还便宜。

汽车

香港路上,公交车看到的全是油车,私家车则是各种各样,油车的话从跑车,老爷车到一般的代步车,电车的话能明显看出来的就只有特斯拉和比亚迪了,其他的或许也有,我认不出来。
在香港竟然真的有开大牛,小牛代步的,果然有钱,不考虑油钱和舒适度,只要帅就行了。因为香港是右舵,所以都靠左行驶,刚过去还不太适应。
香港骑摩托车的挺多的,车看起来也都还不错,路上也有专门的摩托车停车位。在香港开车,起步都超级快,每个路口是都油门嘶吼的声音,无论是公交车还是私家车,都是一样。

人文

在香港路上走着,粤语,普通话,日语,韩语,以及带着各种口音的英语,还有我听不出来的语言混杂。从外貌上看,路上碰到的欧美白人,印度人,日韩,其他东南亚人都很多,有不少看得出是旅游的,另外不少则明显就在香港工作。据说香港的外国人占比超过10%。
至于香港本地人和内地过去的游客,从穿衣打扮以及精气神上就是能看出来不同,能够明显看出香港本地人和内地游客,至于具体差异在哪,我描述不清楚。
香港街头还能看到卖电视天线,唱片DVD的小店,同时也还有不少公共电话亭和邮箱,但各处却又wifi覆盖,让我感觉特别的冲突。既现代化,又复古的感觉,很拧巴。全球各地的人又在这里汇集,感觉到不同的文化在这里交织与碰撞。

景点

收费的景点去了坐了太平山的缆车和摩天台上,只要20块的摩天轮也去坐了,就是那一个多小时的队排的不值,又热又晒,还浪费了宝贵的时间。剩下的就是免费的景点和一些热门地段,星光大道,坚尼地城海边,旺角,铜锣湾,尖沙咀啥的都有去,主打一个暴走。

其他

现在从深圳去香港是真的方便,我是从福田口岸过去的,全程自助过关,刷两次港澳通行证就可以了,过关就可以坐上港铁,直达中环附近。

  •  

使用echarts.js生成足迹地图(优化版)

较pyecharts可无缝集成至主题中,无需手动生成更新

之前使用pyecharts制作生成足迹地图过于简陋繁琐,如果需要更新地图,需要手动填写再更新生成,最后替换原有地图,过于繁琐麻烦。最近又好好研究了一番echarts.js,算是让足迹地图的嵌入变得稍微优雅了一些。

地图数据

echarts.js绘图需要GeoJson数据作为绘图数据源,搜索网上,主要都是依赖阿里云的altas平台提供的数据源,不过这里提供的数据都是按照省份分割存储的,没有市级的,又不想做地图的下穿等更复杂的操作,索性写了个小脚本,将GeoJson数据做了下重新拼合,制作了一个按照市级分割的GeoJson数据源。制作好的数据放到的github仓库:https://github.com/wherelse/GeoMapData_ChinaCityProcess 需要市级分割地图的朋友可以下载或者调用china_city.json

主题修改

  • 在header中引入echarts.js:
<script src="https://cdn.jsdelivr.net/npm/echarts@5.5.1/dist/echarts.min.js"></script>
  • 在需要渲染出地图的位置渲染标签:
<body onresize="resizeFresh()">
<div id="footmap" style="width:95%; height:500px; margin:auto; top:30px"></div>
</body>
  • 在主题文件尾部添加echarts初始化及刷新script:
    <script type="text/javascript">
      var chartDom = document.getElementById("footmap");
      var myChart = echarts.init(chartDom);
      var option;

      myChart.showLoading();
      fetch(
        "https://raw.githubusercontent.com/wherelse/GeoMapData_ChinaCityProcess/master/china_city.json",
        {
          method: "get",
        }
      )
        .then((response) => response.json())
        .then(function (ChinaCityJson) {
          myChart.hideLoading();
          echarts.registerMap("China", ChinaCityJson, {});

          option = {
            title: {
              text: "我的足迹",
              subtext:
                "FootPrint in China\n\n感谢来源于阿里云altas平台的地图数据",
              sublink: "http://datav.aliyun.com/tools/atlas/",
              left: "right",
            },

            visualMap: [
              {
                min: 0,
                max: 1,
                show: false,
                inRange: {
                  // 选中范围中的视觉配置
                  color: ["white","#00AAFF"], // 定义了图形颜色映射的颜色列表,
                },
              },
            ],

            series: [
              {
                name: "China Footprint",
                type: "map",
                map: "China",
                roam: true,
                emphasis: {
                  label: {
                    show: true,
                  },
                },
                data:[
                  {name:"北京市", value: 1},
                  {name:"天津市", value: 1},
                  {name:"南京市", value: 1},
                  //在这里添加更新足迹城市
                ]
              },
            ],
          };
          myChart.setOption(option);
        });

     //窗口大小变化时候,进行刷新页面操作,防止样式混乱
     var x=window.innerWidth;
     function resizeFresh(){
         if(x!=window.innerWidth)
             location.reload();
     }
    </script>
  • 根据自己的足迹更新echarts初始化中的series data中的城市目录即可。渲染效果可参考本博客关于页面
  •  

VCS+Verdi仿真Xilinx FPGA Vivado工程

在使用过VCS配合Verdi进行波形仿真之后,再也无法忍受vivado那缓慢的仿真与卡顿的界面,Verdi追踪信号更是极快加速问题定位。不过FPGA的IP不能像普通Verilog IP一样直接使用VCS进行编译仿真,需要调用一些Vivado IP Library才可以,下面分享一下如何使用VCS进行FPGA工程波形前仿真。

环境配置

需要使用VCS+Verdi进行仿真,这两个必须要是安装好的,这个参考其他教程。然后就是Vivado,开发 Xilinx FPGA这个也是必备的。这些都有安装后,基础的环境就算是OK了。

FPGA工程基础准备

在工程中,调用的vivado IP核,都需要完成调用和预综合,在对应的IP文件目录生成对应的xxx_sim_netlist.v,这个一般是放在/工程目录/sources/new/ip/xxx 中。这些需要先Ready,是后面在VCS编译过程中调用到这些IP的基础。工程中调用的vivado原语则不用处理,正常使用即可。然后是准备好工程中所有使用到的.v文件,除了testbench以外,其他文件都不需要做特殊的修改。testbench文件需要额外添加以下内容,用于dump fsdb波形和mem等内容:

initial begin
	$fsdbDumpfile("xxx.fsdb"); //xxx根据需要替换为文件名
	$fsdbDumpvars();
end

filelist准备

VCS和Verdi的仿真和波形查看都基于filelist进行维护,而不是和Vivado一样的图形化界面。需要将所有调用的.v文件,lib文件等的目录和文件名整理到filelist文件中,VCS和Verdi会自动根据文件列表分析结构层次。

  1. V文件列表整理:可以使用下面的脚本,一件生成当前目录及子目录下所有文件的filelist

    find . -name "*.v" -exec ls -dl \{\} \; | awk '{print $9}' >> flist.f
    
  2. ip sim netlist列表整理,根据项目中调用的IP情况,将所有调用的IP sim netlist添加到filelist中。

  3. Vivado LIb文件添加,为了能够正常仿真IP和原语,还需要在filelist中添加额外的Vivado库文件,这些文件都存放在vivado的安装目录中,一般必须添加unisim库和glbl库,如果有调用serdes源语或者GT收发器,则还需要调用额外的.vp加密库。目录参考:xxx/xilinx/vivado/$vivado version$/data/srcxxx/xilinx/vivado/$vivado version$/data/secureip/

run脚本准备

用于配置VCS编译选项,调用filelist,可以写一个简单run文件,也可以写一个makefile作为管理,这里以一个简单run文件为例:
vcs -R -full64 -debug_all -debug_region+cell+encrypt -f flist.f
仿真开始后,就可以打开Verdi查看已经完成部分的仿真波形,快捷迅速的追踪信号,仿真速度也会比Vivado自带仿真快很多。

  •  

求手机梯子

现在出门打球前必须看黄历了,脚伤好了去打球,结果右手小拇指又给扭伤了。 下午在电梯里碰到许久不见的老师,打完招 […]
  •  

运动虽好,不宜过量

前几天可能是打气排球太猛了,导致右脚前掌有些酸痛。现在这些球友水平越来越高了,不使点力根本扣不死。 之前就有一 […]
  •  

莫名在年龄上的焦虑

真的,时间过得飞快,转瞬即逝的感觉,熟不知自己已经步入34岁了。

那年我觉得我还年轻,我觉得26、27岁算个什么,虽然我初婚在17年,离后至今,很多事情都是历历在目,已经过去6年之久,这6年我收获了什么,光秃秃的额头?蜡黄的皮肤?油腻的脸颊?还是那日渐瘫痪的意志?

自己一直是独居,去年房子装修出来,有了自己的安身之所,不再频繁搬家,不再为熟悉而又陌生的房屋担忧。装修住进去的房子虽然安置了自己喜欢的家具家电,但自己内心越来越觉得空,躺在偌大的沙发上发呆。难道自己今天就这样?明天这样?后天这样?大后天也是这样?

这几年陆陆续续相亲,相亲,相亲,一直在相亲,每一段都是不了了之。我也渴望有个伴侣,有时很真的很害怕孤独,真的害怕,自己性格内向,甚至孤僻,但是每每遇到感觉不错的人时,总是打退堂鼓,总是不主动,总是在犹豫,总是不知所措,总是没有结局。羡慕同事和妻子的你侬我侬,羡慕同事对象送的礼物,羡慕他们有人可以说话,羡慕他们共同有事情可做,羡慕他们回家有人等……其实我心里早有答案,我知道有个人在等我,知道她的样貌,她优雅知性,懂我,而我只想把我好的一面给她,懂她、爱她、惜她……

……

回头发现自己真的不再年轻。

  •  

利用pyecharts制作自己的足迹地图

之前有在高德和百度地图的软件上看到自己的足迹地图,不过这两个软件因为我使用的原因,数据都不完整。很多之前去过的地方并没有标记,或者只是坐火车坐高铁路过而已的地方也都记录了下来。然后就萌生了自己能不能做一个属于自己的足迹地图,且能够方便的嵌入在博客中。

尝试

在一番搜索研究,看了看已有的开源项目,首先看到了基于echarts的方案。仔细研究了一番,发现常规的geojson文件都是按照省份划分,如果想要精确到市级,要么做点击下穿,要么要自己花时间去做城市级的json数据整合。一番搜索没有找到现成的数据,又不想花额外的时间,随放弃。

研究

又经过一番搜索,发现了pyecharts,echarts的python开源版本,echarts本身则是基于JavaScript的。pyecharts的官方示例直接就带了一个全国市级地图示例,这不正是我想要的吗。不过这个示例还有不少额外的标记,不太需要,经过又一番搜寻和文档阅读,首先找到了Python-pyecharts生成精确到市的地图, 经过一番魔改修改成了这样,成功把各种标识全部关闭:

from pyecharts import options as opts
from pyecharts.charts import Map
finished=["北京市", "天津市", "南京市", "合肥市","六安市","安庆市","黄山市","池州市","淮南市","焦作市","西安市","福州市","杭州市","绍兴市","苏州市"]#对应地图中的名字
citylist=[]
for each in finished:
    citylist.append([each,100])
map_data = (
    Map()
    .add("中国地图", citylist, "china-cities", is_map_symbol_show=False,)
    .set_series_opts(label_opts=opts.LabelOpts(is_show=False))
    .set_global_opts(
        title_opts=opts.TitleOpts(title="我的城市足迹",is_show=False),
        legend_opts=opts.LegendOpts(is_show=False),  # 隐藏图例
        visualmap_opts=opts.VisualMapOpts(is_show=False),
    )
)
map_data.render("citymap_footprint.html")

完善一下

但是这个时候,pyecharts生成的html文件,地图只在左上角的角落里,缩放之后更是直接被截断。按pyecharts学习2--自适应屏幕居中显示这个博客内容又处理了一番,唉嘿,输出的文件是我想要的样子了,一个全屏可自适应的,除了足迹以外无标记地图。

效果

<iframe src= "/citymap_footprint.html" style= "border:none; width: 100%; height: 50vh; "  scrolling="no"></iframe>

最后使用iframe嵌入到文章或者博客主题中,完美,效果如下:

🚅我的城市足迹

我最后是将我的足迹嵌入到了关于页面,毫无违和感,非常合适。可放大缩小,以后更新也只需要替换一下iframe嵌入的html文件即可。

  •  

2023to2024

这是我在博客写下每年愿望的第四个年头,2024又有什么期待呢。

我的2023总结

回看23年初时,写下的几个23年的愿望,现在有实现的吗?

2023年的几个愿望:
1.工作精进,技能增长 (√)
2.多出去走走转转看看 (...√)
3.加强锻炼,身体健康 (x)
4.坚持内容输出,提升自己的输出能力(√)

第一条,工作中有了新的变化,也能力上有提升,算是完成了,详细的下面细说。
第二条,下半年的周末,只要没有雨雪,基本每周都会出去走走,但是现在回头看来,基本没有走出市区,走出最常生活的范围。
第三条,今年算是荒废的一年,基本没有什么锻炼,虽然没有经常生病,但是感觉体能是真的差了不少。
第四条,2023年一年总共写了9篇或长或短的博客,还搭了memos时不时输出一下碎碎念,算是圆满。

2023中我的工作

从去年毕业到现在,毕业也已有一年半的时间,但也算是挺短的。年初部门因为很多人离职,就剩下一群应届生,换了大领导,完全不一样的行事风格。新领导比旧领导要push不少,但是愿意带我们新人,给予机会和帮助。这一年学了不少东西,虽然有些零碎不成系统,但是回看年初的时候工作中的自己已不可同日耳语。同时今年的公司,裁员降本不断,行业整体也不景气,不知道到2024会如何。同时我也从开始基本不加班,到现在加班多了,加班真的会降低生活质量,这算是这一年中工作中相当难受的点。

2023中我的生活

生活上,最大的变化是新家装修完之后,我自己一个人住了,而不是和去年一样和爸妈住一起。之前我还经常做饭,但是自己一个人住后加上经常加班,家里没菜,做饭变得异常麻烦起来,就不自己做饭了,之前还会准备第二天的午饭,带到公司去热,因为不做饭也就不再带了。但是一个人住,对于我这个享受独处的人来说,也是一种释放,每天少了许多我爸的啰嗦和负能量。
另一个是因为周内经常加班,到家一般都是9点多了,周内属于自己的生活时间基本所剩无几,回家之后基本打开电脑随便翻翻网页,就洗漱上床,刷会手机睡觉了。周末则就是拿起相机,出去瞎转,去寻找值得记录的风景。
这一年的生活,如果用一个词来总结,我大概只能用平淡来总结。

2023中的买买买

2023年为我自己花钱最贵的两个,一个是电脑,一个是相机,也算是工作后对之前多年愿望的满足吧。电脑现在有点后悔,稍微配的有些性能过剩了,工作之后已是电子阳痿状态😭,对很多游戏完全没有之前那样想玩的欲望。相机则是刚刚到手,希望后面多拍多用,毕竟拍的越多,用的越多,钱花的越值。
如果说我认为买的最值的,我则是认为一个是新买的小斜挎包,一个是Airpods pro。新包买回来之后基本每天出门都背着,包超级轻且背起来贴合身体,但是容量相当可观,出门所需的东西基本都能塞得下。Airpods pro则是佩戴舒适度超级好,基本无感,作为一个地铁通勤人真的是太有用了。能让我每天通勤过程中,隔绝于地铁那震耳欲聋的噪声之外,拥有一段本失去的时光。

我的2024愿望

经过一番短时间的碎碎念,可能遗漏了不少我在2023的所想,但是还是决定停下来,写下我在2024的愿望。

  1. 工作多学习提升少加班,生活多些滋味。
  2. 运动起来,跑步最简单,就从它开始吧。
  3. 坚持内容输出,多写长文,多记录,尝试更多形式,无论文字,图片还是视频。
  4. 多出去走走,走的更多更远,见更多山河,攀更多山峰。
  5. 精打细算,尽可能多存些钱。

新的一年,开始~~

  •  

入手索尼A7C2

SONY A7C2 让我爱不释手

碎碎念

大概6年前,我刚刚对摄影有了些认知的我买了索尼a6300,6300陪伴我度过了大学,研究生,到了工作时期。作为一代神机,真的很能打,有些规格即使到了现在仍然能和有些相机打的有来有回。兜兜转转的几年间,我用6300学习了摄影最基本的知识,摸清了我到底喜欢用相机拍什么。但是毕竟是发布于7年前的老机器了,用起来算是充满了年代感,microusb的充电接口,容量超小的电池,难用的菜单,稀烂的屏幕,都在不断的提示着我。

心路历程

在今年,索尼发布的A7C2在发布前就让我眼前一亮,相比一代有了不少的提升,该有的拨轮,快捷按键都不少,整体的规格基本和A7M4也基本一致。同时那超小的体积和相对轻的重量,和我在使用的6300基本一致,是可以轻松放到我日常通勤的斜跨包当中,随时带着就走,而不用顾忌该如何带走。在几年的相机使用过程中,“能经常带出去拍照的相机才是最好的相机”,是我不断提醒自己,同时又在相机的使用中不断被验证的一句话。

因为工作了,有了自己的可支配收入,买一台全画幅的相机有了足够的预算。然而A7C2一发布就缺货,溢价不断,直到最近12月,价格才逐渐下降,回归到正常水平。在这几个月的纠结犹豫后,想买的心使用没有平复,终于下定决心把6300卖了买A7C2。



上周末,去线下实体店原价入手了银色的A7C2单机,为了搭配机子,在闲鱼淘了一个适马35F2镜头作为挂机镜头。还配了一个银色的底板,提升了握持手感,同时也和机身的颜色很搭。



新机体验

新机子拿到手,手感确实和6300差不多,但是屏幕,电池续航,拍照防抖,AI对焦,菜单这些都提升巨大,尤其是拍照防抖,轻松用0.xs的快门在晚上拍照不会糊片,这个真的太爽了,之前完全不敢想。同时新的菜单比索尼之前的祖传菜单真是好用太多。

同时更高的像素加上全幅,拍出的成片也是能明显感到差别,真的是爱不释手的感觉。另外新的创意外观也是比较好用,有的时候不想修图调色就直接开创意外观直出就可以了。看网上还可以导入自定义lut,暂时还没有尝试,什么时候尝试一下。

样片时间

最后放几张用新相机拍的照片,传递一下拥有新相机的快乐,嘿嘿😁:







  •  

【杂语周刊 vol.03】本末倒置 进展缓慢

回顾本周没有特别重要的事情,只是配合他人工作。

新的小组成立了,我担任组长,对本组的工作还是有些模糊,抽空写了份工作职责和任务分工,感觉不够理想,先临时交给领到了,还的细化再细化。

8号是中国记者节,上周做的视频就仓促发布了,没有精雕细磨,还是比较仓促的。我觉得我只是帮大家打杂,并没有实际参与拍摄和剪辑工作。

周六周天,加了两天班,把单位网站末班重新整理了下,本月底必须完成网站迁移工作,否则某中心的某个主任说要上告上级领导,说我们工作开展缓慢,问题是你得提供一个能用,基本功能能用系统,这实现不小,那也不行,平台是服务于人,别本末倒置。

时间过得非常快,每天有非常多的工作和任务,总是压的自己喘不过气,说是时间挤挤总是有的,但对于我来说,确实有点牵强。大家都在找我,问我在哪,什么时候来单位……我说快了,快了,至于多快,反正很慢。

最近工作状态不好,是得调整调整了……

下周有个活动,需要出差,让我与同事参与,想想也是很兴奋的……

  •  

【杂语周刊 vol.02】意外摔伤 新增家电

今天双十一战线太长了,从10月20好开始了,直到11月12日,总觉的优惠力度不太大。

房子装修好了,也入住了,自4月份开始,9月入住,前前后后折腾了大半年,智能家居也上了,自己组的2.5G网络也部署好了,目前针对我的2.5G网络和智能家居,我另起博文介绍。

 

单位院里子看到的残月和枯黄摇曳欲坠的树叶
单位院里子看到的残月和枯黄摇曳欲坠的树叶

今年618把大部分的家具和家电配齐之后,一直没看到合适的电视,刚开始看好的的小米的ES Pro 75寸,在雷鸟和小米之间犹豫,后来因为其他家具还没备齐,618上价格也没啥竞争力,外加电视并不是刚需,于是拖着一直没买,进了客厅,只见一面大白墙。

今年双11,我正在瞎逛的时候,京东和天猫都开始推小米电视 S Pro 75 Mini LED ,什么MiniLED、75寸、2200nits 的亮度、144Hz高刷、ΔE≈1 、4GB+64GB,就这配置居然5999,果断下单。使用了几天,感觉真心 不错,得益于MiniLED面板,效果和OLED有一拼,黑的地方完全黑,亮的地方很亮。播放我NAS里的4K 120帧的双子杀手,真心丝般顺滑。

本周工作也是比较繁忙的,周末去办公室加班,躺在椅子上,根本无心工作,刷抖音、淘宝,一下子好几个小时没了,工作还是没有做完。

最可气的是,周六晚上过马路,一个没注意不小心被路口固定路障桶的螺钉绊倒了,手机也摔坏了,胳膊和腿也磨破皮了,下巴也磕掉了点皮,还有iPod耳机充电仓也丢了,可谓是损失惨重,14 Pro的外屏和钢化膜全碎,手机左下角天线接缝处也摔裂了,心痛心痛。只能说是安慰自己是舍财免财,舍财免灾。

这周也觉得挺忙的,工作只会永远干不完,真的干不完。

  •  

部署 FreshRSS 内容聚合RSS订阅器

 

前年,我找一个 名叫 Lilina 的RSS订阅软件,部署在自己的服务上,我还写了一篇教程(点击查看 自建Rss订阅器),由于作者已经停更了,部署在php7.4及php8.0环境下存在报错,我也尝试修复了报错,也尝试汉化了部分功能,但是,Lilina 或多或少存在一些问题,比如拉取订阅源的时候非常慢,并且数据是以Json格式保存在数据目录下,无论是易用性,还是可靠性,不够完美,前些天逛博客,看到有人用一款名叫FreshRSS的开源订阅软件(基于PHP开发),简单试了下,感觉还可以,我看网上好多教程都是部署在Docker中,虽然宝塔中有Docker,服务器本身环境明明部署了PHP和MySQL,为什么还要开个Docker浪费性能呢。于是研究老半天安装流程,发现宝塔部署很Easy。

首先看看我部署的效果 https://rss.yefengs.com/

由于FreshRSS是基于 PHP + MySQL(可以选择数据库类型,当然MySQL好维护和管理),安装和博客安装别无二法。

部署流程

1.程序包下载

最新Releases发行版  https://github.com/FreshRSS/FreshRSS/releases

当前最新版1.22.0版 https://github.com/FreshRSS/FreshRSS/archive/refs/tags/1.22.0.zip

2.简易部署流程

我们在宝塔上部署,首先新建站点(PHP + MySQL),将源码上传至网站根目录并解压,其次最主要的是配置网站的运行目录,我们来到宝塔网站设置中,找到“网站目录”,在运行目录中选择运行目录为/p,如图所示,然后保存即可。

接下来,打开新建的网站,按照提示,选择中文,一路下一步,在选择数据库时选择MySQL,填入刚才新建网站时数据库的配置信息,直到创建管理员

安装完成之后,就可以添加订阅了(左上角管理订阅那里的+号),可以看看后台的设置信息,程序还是比较简洁的。想不用登录查看订阅的内容,来到设置中心,找到管理 -> 认证 ,找到“允许匿名阅读”启用即可。

想让FreshRSS自动刷新订阅,可以参考宝塔的计划任务来实现,依据自己需求,设置执行周期即可,执行代码如下,具体路径可参照自己网绝对路径即可设置。

php /home/wwwroot/yourdomain.com/app/actualize_script.php > /tmp/FreshRSS.log 2>&1

  •  

新房装修分享

新家装修自去年9月初开始,收到去年疫情管控以及自己本身拖拉的影响,拖拖拉拉到了今年8月才基本结束,在今年的9月总算住了进来。装修真是一个费钱,费心,不断妥协的过程,这一年下来,工作的收入基本全砸在了里面,额外还从爸妈那里花了不少钱。不过总算是结束了,用文字和图片的形式记录一下装修的亮点、心得与踩坑。

户型改造

首先是布局改造,整体变动不大,一个是把次卫门口处的洗手池位置改进了厨房,用来放冰箱,不然冰箱没有合适的位置来放。另一个是把原来主卧的门封了起来,把书房和主卧打通,做成了套房的形式。这样减少了客厅开间里门的数量,让电视背景墙更大,整体性更好。同时也把客厅与阳台设计成了一个整体,显得空间更大。

门头抬高

在拆改的过程中,把家里的所有门洞的抬高到了梁,正常门只有约2m高,抬高后的门大概约2.3m高左右。从现在装修完来看,效果相当不错,因为门洞拉高之后,门显得细长一些,有拉伸层高的观感,显得家里层高更高一下。同时也不会像有的人家里加的门楣板那种,尤其是开门之后,看起来整体性并不好。

水电改造

水电改造相比原来的毛坯加了非常多的点位,主要注意的是厨房的厨下用来安装厨下净水器,洗碗机,以及吸烟烟机,冰箱插座的位置需要在设计阶段安排好,避免后续冲突。阳台的家政柜中和水池下柜分别预留了充电插座,以及洗衣机,烘干机,扫地机器人的供电插座点位。在两个卫生间门口留了装小夜灯的插座/86盒位置,卫生间内预留只能马桶以及智能镜柜的供电插座或者供电线。在各个房间的窗户顶位置预留了电动窗帘的点位。剩下的地方基本上能想到后续可能使用到的顺手的地方都留的插座,比如沙发,电视柜(尽可能多,大部分人家中这里的小电器都很集中),房间角落(用来接电风扇之类的)。除了这些需要特别注意的地方,其他都是按照设计师给的点位去做的。
最后完工安装插座总共装了60多个,从目前入住的使用感受来看,点位留的很充足,用起来很顺手。
对了还有记得给开关留零线,这样如果想做智能家居时,只需要换个开关就行,而且不挑。

网络布局

新家的网络布局是完全我一手规划的,在每个房间都有留网线,在客厅和书房有做了特别的规划和加强。设计的思路主要是全屋2.5G网口接入,客厅与书房两台路由器做有线mesh组网。客厅电视柜位置有预留三个网口,一个网络做单独的IPTV接入网口,另外两个网口做主路由(可选)的来程和去程。然后进入交换机中再连接到各个房间中。书房设计了三个网口,一个用来接电脑,一个用来接mesh路由和NAS,还有一个备用。两个路由组网,WiFi覆盖家中的面积还是非常OK的,基本没有死角。同时两个卧室如果需要接网线也有网口可以接,在需要提升网络稳定性可以连接。

卫生间设计

卫生间的设计次卫比较简单,就是马桶,花洒,以及为了解决原次卧门口洗手池位置被移走,放在卫生间中的一个超小洗手池。主卧花的心思比较多,做了下沉式的淋浴间和玻璃隔断,而不是传统的淋浴房。同时为了解决平时不想洗衣服的困扰,还在马桶上方设计了一个壁挂洗烘洗衣机,用来洗平时的贴身衣物。两个卫生间都是用的60*120cm的大砖上墙,最后小效果非常简洁,美缝完之后,一体性非常好。唯一的缺点可能就是贴砖的工费比较贵了。

净水器与管线机

管线机预埋

管线机真的是解决喝水难题的重要发明,要多少温度的水,点一下就可以立刻出来。以前尤其是夏天的时候,烧开水然后再等着喝水实在是太痛苦了。管线机如果想效果好的话最好再水电阶段就做好预埋,我装修的时候就是因为没有考虑好,导致现在管线机的电源线拖在外面。最好的预埋方式就是从厨房橱柜预埋PE净水管到餐边柜,然后餐边柜中预留一个插座,然后预埋50管到安装位置,这样电源线以及净水管都可以隐藏起来。

净水机选择

净水器的话一定要选择支持零陈水的,不然使用管线机这种场景,喝的都是高TDS值的陈水。

书房设计

书房做的比较简单,主要是作为书房和活动室来布置的,硬装上只做了一组书柜,剩下就都没做了。我在书房放了一个2m*0.8m的电动升降书桌。书桌旁边布置了一个矮柜,用来放我的NAS以及其他的小的东西。

书房与厨房外挂门

因为户型改动的原因,导致书房和厨房如果安装传统的门打开后都会干涉正常的行走动线,最后在设计师的推荐下选择了外挂吊柜门,安装后左右推拉不占用空间,看起来也不丑,黑色也算是家中色彩的一抹点缀。

全屋定制

全屋定制需要细心的对设计图纸,有没有哪里和家中的其他设计有冲突或者干涉,如果不仔细对图纸非常容易踩坑。另外全屋定制也是价格相对不透明的一个品类 ,不同的板材,加工工艺差价很大。如果追求质感强烈建议用烤漆工艺,但是真的贵。不过最最重要的是最后的落地安装,如果选择用全屋定制,一定要去工地看看落地细节,不然设计的再好,最后安装一塌糊涂都白搭,很多安装工人的技术以及对细节的重视和追求都非常差劲。

扫地机器人

扫地机器人的选择上现在竞争很激烈和透明,反而没有太多要注意的。只需要事先规划好放扫地机器人的位置,留好电源和上下水,尤其是上下水,这点很重要,有上下水扫地机器人基本就不太用去维护,只需要过一段时间换下尘袋,清理下基站就可以了。另外家中的家具尽量选择机器人可以进的高度,这样家中就基本不会有卫生死角。

智能电动窗帘

电动窗帘一个注意的就是要在水电改造时预留插座,另一个则是做窗帘盒要做宽一些,不然安装后两个窗帘可能会蹭到一起。另外一个点,就是电动窗帘买的时候不要从旗舰店买,会贵很多,拼多多的店铺包安装的价格会便宜很多。我买的杜亚的电动窗帘就是这种情况,拼多多能便宜三分之一到一半。

杂项

如果是壁挂空调,有条件的话可以提前规划一下做管路背出,这样安装出来会显得很简洁好看。

最后
再放些装修完的一些照片:

  •  

斜挎包挑选记

之前一直有两个包常用于日常通勤和外出出行,一个是多年前买的小米的10L小背包,一个是去年在拼多多买的高仿NIID R1斜挎包。
小米小背包
小米小背包从买到现在一直在使用,包很轻,容量却相当可观,背个iPad Pro加上水杯,伞,再加上轻薄外套都不在话。说实话,算是爱不释手的类型,但是也因为包本身很轻,所以都是薄薄的布料,当我出行的时候想带相机[Ps: 相机是索尼α6300,相机加上镜头一斤多]时,就不敢往里放,害怕磕碰。同时也因为是双肩包,同时分类收纳隔层也不多,外出的时候其实不是很方便,取东西或者放钥匙等都不好用,所以我基本上只在需要背比较多东西且不在意磕碰的时候才会使用,过去一年中上班通勤需要带饭的时候一般我都会使用它。另外小米小背包真的很便宜,当时买的大概才20元左右,用了这么多年,一点坏的迹象都没有。
R1
高仿NIID R1斜挎包,去年四十多购于拼多多,皮质质感的表面,但是除此之外做工很拉垮,内里也很松散,不过正版300+的价格,40多的东西也不能有什么品质的要求。这个包的容量也不算小,放个iPad Pro +雨伞水杯也没问题,但是衣服是放不下的。这个包内部有不少分隔,不过由于设计不是很合理,加上买的盗版,内衬松散,放东西用起来很难受,我基本只用来主袋放伞,前袋放钥匙和公交卡,其他空间都是浪费的。这个包的内衬还算稍微有些厚,所以我想带相机出门的时候一般会把相机放在包里,但是,放了相机之后,基本上其他的东西都不太放得下了,背的感觉也会变差很多。

最近因为这个斜挎包用着不是特别合用,就萌生了换一个新的斜挎包的想法,心中理想的包型是内部分隔合理,体积不是很大,背在身上看起来要相对贴合。这样基本能满足我日常通勤的需要,能够包正常需要随身携带的东西收纳清楚同时方便取拿,不浪费空间。同时最好在需要的时候能够放下我的相机。确定了这个目标,心理大概有个筛选条件了,经过一番搜索比较,发现满足我需求的包大概都落在长度在30cm左右,容量在5-10L这样。在淘宝,京东,拼多多一番搜索之后,将目光放在了这几个之中:
[1] 马可·莱登单肩包

这个包体积和我之前的斜挎包差不多,但是内部的隔层看起来要合理很多,但是缺少前拉链,拿东西都需要翻开上盖才行。整体的设计看起来感觉还可以。价格淘宝上约150RMB。

[2] 光影行星星云斜挎包

这款包的设计和之前的包有很多相似之处,甚至前袋都是一样的倾斜设计,倾斜设计一点不好用。然后体积和容积也是基本差不多,基本被我PASS了。这款的价格正版价格大概200+,仿版约80RMB左右。

[3] tomtoc 斜挎包


这款包体积和容积相较之前的也差不多,这个包的外形设计更加简洁,同时前袋设计整齐,内衬的设计看起来也划分也算是比较合理,是我心中的备选之一。这款的价格RMB200+接近300。

[4] Bellroy Lite Sling

这款包的体积和容积稍小,同时包很轻薄。这个的前袋设计也是整齐的,取拿东西应该也比较方便。因为包的设计是轻薄类型,内衬的质感就差了些,不过分隔看起来还是比较科学的。这款的设计也算是比较喜欢,这款国内仿版拼多多50RMB左右,正版则是要700+RMB,实在消费不起。

Bellroy这个品牌还有其他的几个型号,设计和质感从评论中看起来我也中意,不过价格来到1000+,而且国内并没有仿版,只得放弃,暂时消费水平还没有到在一个包上花费上千元。

目前还在[3]和[4]中纠结,准备是先买一个Bellroy Lite Sling试试看,毕竟便宜,如果感觉不喜欢,就在双11买一个tomtoc 的斜挎包。

新包到手

拼多多仿品Bellroy Lite Sling到手,银色的布料观感还可以,还带点反光质感。布料比较薄但是强度还不错。主袋拉链比较顺滑,副带拉链比较涩。包整体很轻。算是能完美符合我通勤时使用的小包,但是有时出门时想塞个相机就装不下了。能够完美符合我通勤+外出拍照随身携带相机的小包据需寻找中。

  •  

更新证书备忘录

博客和导航站使用的都是阿里云免费SSL证书,有效期都是一年。时间一久容易忘记怎么操作,这次写个备忘录,以备以后 […]
  •  

我还在

正如好多博友说的,一年多没更了。很多人在问:你还在吗?我知道大家在想什么,譬如是不是要关站了?十年之约还能不能 […]
  •  

verible-verilog-format 使用指南

Verible 是一套 SystemVerilog / verilog 开发工具,包括解析器、样式检查器、格式化程序和语言服务器。这里为主要分享关于格式化工具verible-verilog-format的使用。用来格式化verilog代码,实现代码风格统一。

verible 项目托管于github,项目地址:https://github.com/chipsalliance/verible

安装

verible可以自行编译安装,也可以下载已经编译好的可执行文件。下载路径见github release。下载完成后,将下载到的压缩包解压,在文件夹的子目录verible-bin/中就可以找到 verible-verilog-format 了。

使用方法

使用命令调用:

verible-verilog-format -- [options] filename.v

举例:以原位替换的方式,按照默认规则进行verilog代码格式化, inplace及为原位替换选项。

verible-verilog-format --inplace testbench.v

其他可用配置参数可以参考,以为为部分选项的,全部选项见:https://chipsalliance.github.io/verible/verilog_format.html:

--column_limit 
(Target line length limit to stay under when formatting.);default: 100;
行代码长度限制,默认最长100个字符。 使用示例: --column_limit=200 设置最长字符数为200个

--indentation_spaces 
(Each indentation level adds this many spaces.);default: 2;
代码层级缩进深度,默认为2字符 使用示例:--indentation_spaces=4 设置代码层级缩进为4字符 

--assignment_statement_alignment 
(Format various assignments:{align,flush-left,preserve,infer}); default: infer;
赋值语句对齐方式:{对齐、左对齐、保留、推断} 默认值:推断;   

--case_items_alignment 
(Format case items:{align,flush-left,preserve,infer}); default: infer;
case语句格式化对齐方式:{对齐、左对齐、保留、推断} 默认值:推断;
      
--class_member_variable_alignment 
(Format class member variables:{align,flush-left,preserve,infer}); default: infer;
类成员变量格式化对齐方式:{对齐、左对齐、保留、推断} 默认值:推断;    

--compact_indexing_and_selections 
(Use compact binary expressions inside indexing / bit selection operators); default: true;
使用紧凑的二进制表达式索引/位选择运算符; 默认值:true;    

--distribution_items_alignment 
(Aligh distribution items: {align,flush-left,preserve,infer}); default: infer;
distribution对齐方式:{对齐、左对齐、保留、推断}); 默认值:推断;
    
--enum_assignment_statement_alignment 
 (Format assignments with enums: {align,flush-left,preserve,infer}); default: infer;
 枚举对齐方式:{对齐、左对齐、保留、推断}); 默认值:推断;
     
--expand_coverpoints 
 (If true, always expand coverpoints.); default: false;
 
--formal_parameters_alignment 
 (Format formal parameters: {align,flush-left,preserve,infer}); default: infer;
格式形参对齐方式:{对齐、左对齐、保留、推断} 默认值:推断;    

--formal_parameters_indentation 
 (Indent formal parameters: {indent,wrap});default: wrap;
形参缩进方式{indent,wrap};默认:换行;
    
--module_net_variable_alignment 
(Format net/variable declarations: {align,flush-left,preserve,infer}); default: infer;
wire声明对齐方式:{align,flush-left,preserve,infer}); 默认值:推断;
    
--named_parameter_alignment 
(Format named actual parameters: {align,flush-left,preserve,infer}); default: infer;
命名参数对齐方式:{对齐、左对齐、保留、推断} 默认值:推断; 
    
--named_parameter_indentation 
(Indent named parameter assignments:{indent,wrap}); default: wrap;
命名参数缩进方式:{缩进,换行}); 默认:换行;
    
--named_port_alignment 
(Format named port connections:{align,flush-left,preserve,infer}); default: infer;
端口名称对齐方式:{对齐、左对齐、保留、推断}); 默认值:推断;
   
--named_port_indentation 
(Indent named port connections: {indent,wrap});default: wrap;
端口名称缩进方式:{缩进,换行}); 默认:换行;
    
--port_declarations_alignment 
(Format port declarations:{align,flush-left,preserve,infer}); default: infer;
端口声明格式:{对齐、左对齐、保留、推断} 默认值:推断;
    
--port_declarations_indentation 
(Indent port declarations: {indent,wrap});default: wrap;
端口声明缩进方式:{缩进,换行}); 默认:换行;
    
--port_declarations_right_align_packed_dimensions
(If true, packed dimensions in contexts with enabled alignment are aligned to the right.); default: false;
对齐的上下文中的尺寸将向右对齐。默认值:false;
    
--port_declarations_right_align_unpacked_dimensions 
(If true, unpacked dimensions in contexts with enabled alignment are aligned to the right.); default: false;
    
--struct_union_members_alignment 
(Format struct/union members: {align,flush-left,preserve,infer}); default: infer;
结构体,联合成员变量对齐方式: 默认:推断
    
--try_wrap_long_lines (If true, let the formatter attempt to optimize line
 wrapping decisions where wrapping is needed, else leave them unformatted.
 This is a short-term measure to reduce risk-of-harm.); default: false;
    
--wrap_end_else_clauses 
(Split end and else keywords into separate lines); default: false;
将 end 和 else 关键字分成单独的行;默认值:false;

--inplace (If true, overwrite the input file on successful conditions.);default: false;
原位替换为格式化后结果,默认:false   

根据filelist文件批量处理

为了方便使用,使用python写了一个基于filelist.f进行批量处理的脚本, 可以根据filelist进行代码批量格式化,有需要的可以自取:

https://github.com/wherelse/verilog-formatter

https://circuitcove.com/tools-verible/ 
https://chipsalliance.github.io/verible/verilog_format.html

  •  

时光不停留

时光不停留,岁月催人老。自诩发质很好的我,今天早晨发现已经有一根白头发了,大概太劳累?一直以为我的发质遗传自妈 […]
  •  

想换一个微信头像

许久没更。博客差点被遗忘了,收到主机续费的通知邮件才想起它。这次阿里云良心发现,续费价格尚能接受。 现在的微信 […]
  •  

远离生产线之后

社会不断进步,人类也离生产线越来越远,大部分一线工作都由工厂机械化地完成。在这种情况下,人们经手的东西几乎都不是人类的直接生产物。这便会产生一些问题。

人类在制造一件事物时是有情感的。过去的大部分人造事物都是“有情”的,它们经过人们所谓“低效”的手工劳作制得——如磨镜匠将镜子磨得光亮,烧彩人将釉彩烧得出神入化,虽然人的生产低效,但至少有人的意蕴在里面,这样一来那个时代的人所接触的事物大多是带有人文之色彩的,因此这份被赋予在器物中的情感,这份人文色彩便被传承了下去。可现在这个时代,一切都变了。朱光潜先生在《 谈在卢浮宫所得的一个感想》中提到,杭州织锦、美国钢铁房屋与湘绣、兰斯大教寺的区别便在于一个是人亲手所得,一个是机械制造;一个是“有情”,一个是“无情”。在这个人们在追求效率的同时远离了情感,远离了人性本身。这样制造出来东西固然高效, 但是失去了人的意蕴。一旦我们经手的东西都是机械化的、失去了人之色彩的东西,久而久之我们便习惯了这些事物的“无情”,这也诱导着我们人类逐渐失去了独属于人的那份”低效“却奇异的创造力,失去了人类的气息。

这份气息的失去是很危险的。它往小的说会导致人心越来越浮躁,人们越来越追求高效率,制造出更多的“十字街头”;往大了说,我们甚至会失去我们的价值。人便是要做人,便是要发出独属于人的那一份光,若是我们人都盲目追求效率而忽略了那份光,那还有谁会关心人类的价值?

因此,远离生产线之后,我们应当深思人类何去何从,应当怎么去将人的色彩保留下来。

  •  

2024浏阳跨年烟火

✇遐说
作者Dorad
2024年最后一个周末,三五好友,驱车前往浏阳凑热闹,看到了人生最美的烟火!
  •  

2024年终总结与展望

迟到的年终总结……

2024年,又是一场洗礼,也又是一场蜕变。

关于学业

这一年,我的初中生涯结束了。我在中考取得了优异的成绩,但自从到了高中以后成绩下滑严重。或许是因为在新的环境中,总有种种不适应。每日十来个小时的在校时间导致回家的我疲惫不堪,但是迫于压力我不得不继续学习。这种高强度的学习导致了我身体上的不适应;每日的周回顾,一月一次的月考……五花八门的考试使我难以喘过气来,这导致了我心理上的不适应。我经历了长达一个月的低谷期,直至十二月底我方能调整好心态,一步一步向前迈进。

我在这一个月里经历了前所未有的挫败:成绩下滑到班级十名以外。尤为讽刺的是,我每日把时间花在理科上,我不选的文科仅仅花了一个晚上恶补。最后政治、地理考得比物化生都要好。这是让我极为崩溃的点。这个月,我经历了强烈的自我否定,有时我甚至怀疑我是否适合学理科。

但人不可总是自我否定,这样会陷入内耗,会直接导致精神上的崩溃,更会致使人在相当一段时间内沉沦。我便是如此沉沦许久,每天都带着满满的负能量;这种压力真的会把人压垮。后来我在与父母的沟通中逐渐意识到,光说自己没用是无济于事的,重要的是拿出行动。于是我最终接受了这一切,并尝试改变这一切。

我静下来仔细反思了我失败的原因。这时候,我想我收获了更优的学习方法、更好的学习态度,但更重要的是我还收获了面对这断壁残垣不言弃的勇气吧。

这种勇气的获得使我突然觉得我这一年没有白活。中考所谓的“成功”,并不能使我取得实质上的进步,反而滋长了我的虚荣心与那超标的“自尊”。影视飓风公司一直秉承着“无限进步”的理念;我现在逐渐明白,这个词并不是鸡汤,它体现了一种积极面对生活的态度,一种在面对困难时不把它看作“失败”,而是把它看成成长的机会的进取心。面对2025年的学习挑战,我坚信自己能顶住压力,无限进步。

关于心灵

这一年,我心灵的洗礼尤为丰厚。我在中秋诗会中感受到了青春,在乡下面朝生命的旷野;我在《平凡的世界》中看到了平凡人生活的多艰,在《额尔古纳河右岸》中则目睹了一个民族的衰落与终结。“山不让尘,川不辞盈。”新的一年,要让自己变得更加宽广些、达观些,为自己找到一片独处的、静谧的角落,如同庄夫子之于天地之间,朱自清先生之于荷塘之畔。

关于他人与世界

这一年,我认识了更多的人,也经历了更多的事。我承认有时复杂的人际关系会让人感到懊恼,但在这种即使充满学习压力也不失活力的环境里,我收获了许多欢乐。也希望在新的一年,我能更好地接受这个环境,并做好适应。

这一年,这个世界千变万化。AI技术持续发展、中东地区局势紧张、特朗普胜选总统……局势风云变幻,有许许多多的事已然记不清了。无论怎样,我始终相信,一切都会变得更好的。

“辞暮尔尔,烟火年年。”2025年,相信自己,相信世界,相信未来,相信相信的力量。

  •  

2025你好

✇遐说
作者Dorad
转眼就2025了,成年后的时光总是在不经意间溜走。
  •  

涅槃

一本书,一串文字,可以在一个人的生命中留下浓墨重彩的一笔。我,或许不仅仅是我,从书中获得涅槃。

那是一个深冬。当时的我难以从生活中的一些琐事中走出来,心不在焉地飘在雪地里。雪地中留下一道道或深或浅的鞋印,我路过痕迹,而又留下痕迹。回首一望,看到我留下的浅浅的足迹,不禁浮想:我就这样浅薄吗?就这样浅浅地度过自己的一生吗?再回想起最近和同学的争执,和父母的不和,我心绪烦乱,加快脚步上楼。

回到家。打了个寒颤,我泡了杯热茶,有意无意地抓起一篇小说,是《喝汤的声音》。小说中主人公哈喇泊命运坎坷波折。他浅薄过,他也深沉过,他更尝试过实现过自己的价值,在烧水和敲钟的岗位上恪尽职守。作为一名生活在边陲小镇的穷人,他曾将要放弃生活的希望,但最终总是在“喝汤的声音”中涅槃。“这声音初始像穿越幽谷的强风,带着股气吞山河的力量。”穿越幽谷,气吞山河,生命便是这样的磅礴,这样的富有气概。这是书本中生命的经历,而再回望自己生命的经历,我感到自己的那些所谓“经历”是那样微不足道。我仿佛从书中获得涅槃,是那种“气吞山河的力量”令我鼓起勇气去继续生命的伟大创造。

那是否还有一批人和我一样,从书中汲取力量呢?

不由得,我开始大胆地想象“喝汤的声音”背后各种各样的声音。在不同的时空、不同的社会环境下,不同的人们读到这篇小说后会有不同的声音。他们或许因那次屠杀而发出愤恨的声音,或许因主人公的悲惨境遇而发出深沉而惋惜的声音,又或许因那“气吞山河的力量”发出昂扬的声音。这一个个声音的激发,便是书本带给读者的涅槃。

抬起头,饮一口热茶下肚,一阵热气涌入心田;慷慨激昂的我仍对这番文字意犹未尽。简单翻阅几页,我发现了书中的主人公还有一个家族背景:他的祖父祖母曾经历过“海兰泡惨案”。当时遇害的人有几千多名,所以现代社会也会有很多人是那些遇害之人的子孙亲属,他们必定与哈喇泊一样都对当时的沙皇统治者抱有无限的愤恨。这是读者情感上的共鸣,他们经历的相似性使得书本被赋予了崭新的意义。这时,我开始意识到——或许书本的意义并不仅限于此。如果读者们在涅槃后加深了对书本的理解,那是否也赋予了书本一种生命的价值呢?

缓缓踱出门外,看着漫天飞雪纷纷而下,我静静倚在窗前,仿佛经历一场洗礼。渐渐地,我眼中的雪花逐渐抽象,抽象成一片片思绪;而雪地就如同一本大书,默默接受着这思绪进入,浸入。我想如同这雪花一样形状各异,或许每个人的思绪都不同,而这些不同的思绪创造了一本局部多样化却整体统一的“书”,这使得书本不仅存于纸面,而是有了生命的色彩。那是书自身的涅槃。

渐渐我明白,或许读书的过程,便是读者与书的双向奔赴。读者可以从书中获得重生的力量,而书本在读者赋予它生命的意义后也就完成了涅槃。作为不同的个体,人们观察世界的角度不同,他们对书中事物的理解便也不同,于是打动他们,推动他们涅槃的点便不同。这样一来,在每个读过它的人的眼中,它的价值象征不同;而读的人多了,它不同角度的价值便汇聚在一起,成为一种生命价值的聚合体。此时的书似乎真的获得了生命,而我们读者的作用便在于将这种生命的价值一直传递下去,以一页页书的形式传递给下一名读者。

既然书本是生命价值的载体,那么不妨读一读书吧,这样或许能体会到他人对生命的理解,又或许能从中获得独属于自己对生命的感受呢……

窗外的雪愈下愈大,我慢慢顺着楼梯下楼。雪纷纷扬扬下着,我抬头,寻那一片片雪花。

“有人在我身边/往来穿梭/留下一部分他们/带走一部分我。”这是生命的涅槃,也是书本的涅槃;理解这一切后,又是生命的涅槃……

  •  

生命的旷野

落日西沉,炊烟袅袅。

村里的环境恬静而舒适,深秋的风凉爽而不凛冽。铁栅栏里是一条不长不短的小径,通往一座矮矮的房子。小径边上有一座小假山和一个小水塘,尚且算作修饰。在小屋的后面可以隐约看见远远的旷野。这是一片广阔之境。

坐在小方桌旁,等待饭菜上桌。夹起一口喷香出锅的鸡,配一瓶可乐,一群人围成一顿简单而丰满的晚餐。

夜幕降临,华灯初上。徘徊于小路边,看明亮的小屋,看轮廓模糊的假山,看路旁的小灯发出的点点微光……抬眼,城市里难以见到的点点繁星次第铺展;烟花绽放,破旧的瓦房洒下了昏黄的灯光。再将视野放远,便又是那片若隐若现的旷野。

几个人,一张桌子,一个小院……生命不再困在一个小小的城市里,而是去面对乡下的广阔天地。

不知出于什么想法,这里的一切使我内心有种自由之感,不能说太强烈,但是存在。我意识到我在某种程度上触及到了被繁重生活所蒙蔽的内心深处的快乐与自由。

我说城里人啊,别局限在那狭窄的格子里,去看看外面的世界吧。活着不仅仅是为了“谋生”,更是为了遵从内心的自由,听那栅栏门前的声声犬吠,看那旷野之上的点点繁星,探索那摆脱纷繁复杂的纯洁的别样世界。人常言“生命如旷野”,人的内心本来就是一片旷野,只是现实逼迫我们用些所谓的正确去将那本属于我们的纯真隐匿起来罢了。所以请坚信,只要你愿意去探索,去创造,那片独属于你的苍茫旷野必定会得到解脱,热烈而奔放。

此间别处也一样奔忙,只是他们脚步声里多了些天宽地广。——毛不易《此间别处》

在天宽地广的旷野中,我们与新的自己相逢。

乡下

  •  

青海出差

✇遐说
作者Dorad
此前心心念念想到青海玩耍,没想到第一次到是因为出差。
  •  

东京游记 Vol. 1 - First Impression

拖了几个月时间,记录一下今年六月底、七月初赴日本东京的旅程。

我一直很想去日本旅游,但是也一直犯懒,想到要办签证、订机票、订酒店、计划行程就一个头两个大。好在🐦平时看似爱犯懒,但是聊到出门玩她是一点也不困,所以这次旅程基本上就是🐦全程操办了,我只负责提出一些想去想吃的需求。

出国旅游三要素:签证、机票、住宿,简单介绍下:

  • 签证这块,是在携程上面代办的三年多次,RMB 538。我是北京领区,感觉材料准备起来算是比较简易的。
  • 机票这块,对比了好几家航司,最终选择了全日空,应该是属于服务较好的选择。价格亦实惠,双人往返~4k RMB。
  • 住宿纠结了很久。因为东京有点类似香港,住宿很贵。挑了很久没有中意的酒店,亦或者酒店中意、价格太贵。最后还是选择了在 Airbnb 上定了一家民宿,位于池袋,据介绍在一个安静的街区(一开始我并不相信,因为池袋出了名的吵,哪有安静的街区。不过后面发现我错了,日本全是安静的街区)。选择民宿另一个原因是希望体验下住在真正的日本居民区是什么感觉。

至于做攻略,我几乎是 0️⃣ 贡献,全靠🐦一人搞定~


航班是全日空从广州白云直飞东京羽田。白云机场出关时被海关稍稍问了下去日本做什么、为什么现在去之类的,如实回答即可:去旅游,因为决定在最近休年假。

从羽田机场入关,手续也并不复杂,大致就是拿着护照签证在一个机器上扫一扫这样,就这样丝滑地入境日本~到的时候差不多已经天黑了。

过关后,没有太多逗留,即坐上 MO 东京单轨电车至滨松町站,转 JY 山手线直达池袋。 池袋西口北果然不负盛名:中国人极多!地铁站内外到处都是说中文的人。从地铁站到住处,步行约 10min,这个过程逐渐有了对池袋的第一印象:灯红酒绿的地方。

整条路上,举着牌子站在路边招揽生意的女生随处可见。主要的行人是社畜模样的年轻人。

我以为我要住在这样的街区,没想到拖着行李箱左转右转,真的来到了一个宁静的街区。其静谧,显得一两公里开外的池袋站仿佛是另一个世界。

所下榻的民宿叫做 Ikebukuro Manga House「池袋漫画屋」。民宿总共三层,确实每层都有书柜,放满了漫画。因为价格比酒店便宜不少,一开始我没报什么希望,但是到了实地发现出乎意料地不错,干净整洁,面积也并不觉得局促。浴缸、洗衣机、微波炉等设备一应俱全。推荐!

第一天舟车劳顿,放下行李后即去池袋站附近觅食,找了家看起来主要是卖海鲜的日料店落座,吃了些刺身什么的。并且第一次真正领教了日式英语。店员反反复复地问我要什么「得凛可」,我是绞尽脑汁也不知道她在说啥,最后她只好把菜单翻到饮品的页面,我这才反应过来:原来是问我要什么 drink!把我尴尬笑了。

吃完回住处已是午夜,洗洗睡了。


第二天起床后在下雨,还好有带伞。 路去地铁站的路上找了家 711 打发了早餐,顺便仔细看了下住的街区。日本的街面很整洁干净,但是也很窄,绿化比较少。日本的一切好像都很迷你,道路也是,很多地方一条车道比一辆车宽不了多少,停车位也小得可怜,逐渐开始佩服日本人的车技。

此外貌似东京正值东京都知事选举,看到了一个有点离谱的候选人广告:

便利店、自动贩卖机随处可见,但是,没有垃圾桶。日本执行严格的垃圾分类,路上要是临时买瓶水、买个小吃吃了之后怎么处理垃圾会很头疼,也许最终只能自己带回住处处理。

住宅区的街面安静祥和。由于很多是带小院子的一户建住宅,家家户户都把自己的宅子打理得漂漂亮亮,走在路上赏心悦目,审美确实有基本盘在。

东京是一座传统与现代、西方与东方完美融合的城市,处处都能看到寺院、高级商场、街头艺术的混搭。很容易理解许多赛博朋克会把东京作为大场景。

以上便构成了我对东京的第一印象。接下来计划用几篇日志,回顾下这段旅行。

  •  

青春

青春,在那一刻被具象化了。
昨天的中秋鹅池诗会,对我可以说是一种解脱。沉浸在歌声中,积压已久的压抑得到释放。灯影漾漾的水波,初来懵懂的身影,一切的一切仿佛可以定格在那一晚、那一个时辰。
正式进入高中,有各种各样的不顺心。千头万绪的人际关系,被动的、无节奏的学习状态,完全失调的作息……我难以从这些之中解脱出来。争分夺秒的学习中,我感受到了前所未有的疲惫,更多的是一种无奈。在我的心中压着一团火,我不知道这是什么火,只是为它不能释放而感到无比的心酸,甚至气愤。
一瞬间,我质疑过:什么是青春?青春就是浸在题海中,心里憋着难以言表的辛酸吗……
我不知道。我不能回答。
来诗会之前,我仍然难以从考试的失利中解脱出来。出于一种心理安慰,我带了一些作业。在那一刻,我屈服了,或许青春就是这样压抑,这样抽象而难以言表。
然而,事情发生了转机。生命或许在一些时刻会迸发出一种具象的、放荡不羁的激情,在诗会上的我即是如此。一曲《少年游》的激昂,一片《将进酒》的豪迈,一首《琵琶行》的清雅……青春的魅力在那一刻迸发出来。我放下了作业,尽情欣赏这一切的交织错杂,享受着那月明星稀的夜。
二晚开始后,回到教室学习。外面班级的一首《知否知否》又激起了我内心的波涛。望着眼前的作业,听着那袅袅的余音,我第一次真正感受到了青春的模样——
或许青春是题海中的徜徉、是压力万重的学业,但是青春不应仅限于此。有时候我们不应当仅仅坐在死气沉沉的教室里,更应该到这个周围的世界去看看,去看看阳光明媚、星光灿烂,去看看迢迢牵牛星,去品味、去感受那中秋长夜中富有青春色彩的点点滴滴。生命之美,莫过于此。
无奋斗,不青春。而无激情、无奔放,不也同样不青春么?我认为如此,我也一定会践行自己的青春誓言,去燃烧、去释放内心的青春之火。
一次的诗会或许微不足道,但在我眼中是激发,是创造。我忘不掉一群人的青春交织在一起的样子;那一刹青春之花的盛放,我也铭记于心。

  •  

买一辆小车 | 揽件日志

说起来,一直对车这种东西不是非常感冒。我觉得车和房一样具有资产性质,里面蕴含了别样的意味 —— 似乎大家都会根据住的房子、开的车来对人进行评判,我不喜欢这种评判。另一方面,潜意识里觉得自己还没到买车的年纪,还有点停留在学生时期,觉得「买车」这件事情显得有点太成熟了。

开始想要买车,主要是切身体会到了有车后带来的自由感;去年从北京南下到广州,也为买车这件事创造了可行性。

在此之前,虽然拿着驾照,但是五六年下来基本不怎么摸车。去年年初到广州后,偶尔会觉得自己的生活半径有点窄,并且公共交通出行还是有点麻烦。有一两次,突发奇想去租了辆便宜的油车,开着去周边游玩了一圈,感觉十分自由从容,不用拖着行李赶地铁大巴,也会轻松很多。相比起公共交通的「赶路」,开车自驾则更像是「旅途」。

再后来,由于工作的关系,有机会借用公司的一辆小鹏 G6 开了大约八九百公里,G6 基本上代表当时最领先的新能源车智能化体验。过程中高强度地使用 XNGP 辅助驾驶,着实体会到了新技术对驾驶这件事的增益,激动得我回来后甚至发了条朋友圈盛赞 G6。不过最终我选择的车并不是 G6,这是后话。

朋友圈盛赞小鹏 G6

买车、不买车、买车、不买车……两种念头一直萦绕在脑中。买吧,我其实又不是刚需;不买吧,又真的很想拥有一辆自己的车。

当心中种下一粒种子,它自然会发芽长大的。我早该料到的,到了这个阶段,只会是以买收场,我不太能抵制住自己的物欲。不知不觉间,逛街时我会对路上的车多看两眼,也逐渐构建起我的审美来:我喜欢偏轿跑型的、精致或者具时尚感的,尤其不喜欢大车。色彩方面则是喜欢浅色。

在某个奇妙的周末,某种奇妙的磁场,把我吸进了天环广场的 Tesla 展厅(但是最后我买的也不是 Tesla 哈哈哈)。说起来这还是我生平第一次走进汽车店,整个十分僵硬,不知道要看什么,也不知道要问什么😂。好在 Tesla 的销售很专业很随和,最终以约了一周后 Model Y 试驾结束。

约试驾,这是个非常重要的节点。从这一刻起,我开始真的在「选车」了。


我第一次试驾,其实不知道要试什么,所以 Tesla 的试驾试得比较草率,基本上是沿着天环外面的路开了一圈,迷迷糊糊地听销售讲了一些特斯拉的优点,诸如销量大、质量好、久经市场验证之类的。但是这些我其实没太大实感。倒是感觉 Tesla 开起来比较颠,坐在后排的🐦也有同感。

最重要的是,如前文所说,我很在意的辅助驾驶这一块,目前的 Tesla 在国内约等于一片空白。虽然 FSD 在美国大杀四方了,但是在中国还是不能用;只有 EAP,which costs 3.2w RMB 并且只提供少得可怜的一些功能 —— 这些功能在国产新势力车上早就标配了。

正由于对智能化这块的需求,直接过滤掉了比亚迪之类的传统车企,眼光逐渐转向几家新势力:小鹏,小米,华为等。

后来实在是选不出来了,到小红书上发了个求助贴,没成想后面就水到渠成了。


说起来我对小红书很有好感。比起微博,小红书更友善,更少阴阳,信息也更有用。这条求助贴快速累计了一千多条回复,并且大多都是认真的分析。虽说网友们各有各的喜好,因此分析结论也是大不相同,但只要是基于合理的逻辑、理性地分析,总还是有参考价值的。比如大家会特别强调车子的用料品质、不同能源形式的使用体验、外观、驾驶感受、智能化,甚至到公司是不是靠谱等等。

小红书求助贴

帖子里的几个牌子以及车型确实是我最纠结的几款,基本是各有优缺。我正在评论区跟各路网友畅聊,小红书突然蹦出来一条私信:

姐妹姐妹特斯拉车主兼特斯拉前销售 现蔚来销售给你合理的推荐 广东省内可以上门试驾

但神奇的是,让我做决定的正是一条私信,所以说呀,机会留给主动出击的人。一开始我也是本着收集信息的角度询问了一些比较基础的东西。但这位销售很积极地给我约了上门试驾。


话说蔚来的试驾体验真的很好。约好的上门试驾时间,很准时到楼下等我。整个试驾过程展示、讲解也都很专业,交流起来很轻松。作为几十万的商品,我当然希望购买体验对得起价格,但是最重要的还得是车子自身。

试驾的是一辆蔚来 ET5T,同温层蓝色。我几乎第一眼看到试驾车就种草了,非常协调、非常漂亮。不仅是外观,内饰也简洁大方,坐起来特别宽敞。尤其令我心动是换电体验。上面说过,我试过小鹏 G6 开大几百公里,整个过程中体验最不好的就是充电,需要到处找充电桩、排队、等待漫长的充电。但是蔚来在我的住处几公里开外就有一座换电站,换电只需要三五分钟,这个体验堪比油车了。更多的用车体验后面再说。

现在回想,试驾结束后基本上「买什么车」这件事我从直觉上已经有答案了,再收集更多信息、看更多评测,其实都是在巩固我已经形成的结论。


但是到正式下定中间还有一个插曲。某天我和🐦偶然去到了另一家蔚来门店,跟另一位销售聊上了。这位销售特别热情地推荐一款展车(也是 ET5T,但是是展车,便宜 2 万元,并且已经预选了配置不能更改)给我,确实挺实惠。但是他让我很快地做决定,因为展车一般都很抢手,搞得我有点懵懵的。最后正当稀里糊涂地要下定时,发现被人先一步抢走了😂。

尴尬地是,这位销售自认为自己跟我们搭上了线,但他自己又知道我们之前有跟另一位销售对接,于是总是让我们搞一些我自己觉得不是很磊落的操作,比如用一个新的手机号注册,以避免之前的销售知道新的动向。我一方面不喜欢搞这些小动作,另一方面单纯觉得麻烦,于是有意远离了这位销售。不过,后来还是通过这位销售,给自己要到了更多的一些优惠。


下定的过程是很顺畅的。我和之前带我们试驾的销售约在广州珠江新城的 NIO House,基本上只细聊了一下价格、优惠之类的细节就直接下定并锁单了。以下是我的车的配置细节。

下定之后,就是大约两三周的等待。四月底下定,五一回老家参加了好哥们儿的婚礼,回到广州就去提了车。这期间真是天天打开 App 欣赏我的未来小车,看着它排产、生产、下产线、运输、抵达交付中心,这时候 App 上传了几张验车照片,好生把我激动了一把,终于看见实际的车了!


给大家欣赏几张实车美照:

是不是美爆!可惜照片还是少了点,我一定要开去更多更漂亮的地方,拍更多漂亮的照片。


最后一部分,我回顾一下当时我的选择思路及三个月开下来是不是有所变化。分为外观/驾驶/补能/服务这几个部分吧。

  • 外观。外观属于萝卜青菜各有所爱,但是由于这里是我的博客,所以我要说我的车是最美的。一开始我会觉得 ET5 更好看一些,因为是轿车,背部线条更流畅精致。后来逐渐了解了旅行车这个品类,开始喜欢上了 ET5T 这种旅行车外观。并且,旅行车也是外形与实用的完美平衡,后排空间凭空就多出了很高一截。几个月下来,越来越喜欢。我同事说得对:

    买那辆你停好车还会回头看几眼的就对了。

  • 驾驶。实话说,驾驶感我是一窍不通的,所以网上大家聊的很多的诸如底盘素质、加速能力等等,我不懂也不太在乎。从数字上来说,似乎 ET5T 是不错的:4.0s 的百公里加速应该已经是很快的了。但是 again,用不太到也不太在意。但是智能驾驶这块不一样,我很懂。NOP+ 的可用性已经非常高了,再加上我工作就是搞这个的,对于智驾的性能边界了解得很清楚,这使得我跟 NOP+ 的配合非常流畅。数据说话:目前我开了 3537 公里,其中有 1838 公里是智能驾驶 —— 52% 的比例是相当高的,这也侧面证明了其可用性。另外还有自动泊车、遥控泊车、召唤等等,这些功能也会极大地提升我的幸福感。蔚来的智驾声量比较小,但是其实该有都有,创造的用户价值一点不小。

  • 补能。因为有换电,蔚来补能体验是独一档的。我举一个例子:端午节我和🐦回家过节,往返是 800 公里的高速,好巧不巧还遇上了事故堵车。中后段我们经过的每个服务区都是大排长龙 —— 全是排队等充电桩的电车,以我估计,每台车不等两三小时是充不上电的。对比之下, 我驱车直入服务区换电站,5 分钟即从 0 到满电出发,毫无拖泥带水,走的时候还看见旁边的充电苦主幽怨地看着我,这个情绪价值是极高的。另一方面,蔚来也同样可以快充。所以我对换电完全是支持的态度:在充电达不到 5min 一台车的速度时,我选择换电;当一台车 5 分钟充满的日子真的来临时再转移不迟。而我相信,后者还要很久才能成真。提车至今,我只为了消磨时间充了一次电,其余全是换电。换电让蔚来的补能体验达到油车水准,甚至超过油车 —— 参见我的一条朋友圈:
  • 服务这块,也是蔚来立足的杀手锏之一。我自己其实用得较少,只体验到了从试驾到提车的全流程。如我前面所述,整个过程是顺畅、愉悦的。从销售到排产到交付,每个环节都有专人对接,中间什么时候该做什么都安排得清晰明了,对于我这种 J 人是很友好的。蔚来的整个服务体系:从售后专属服务群,到 NIO House 这些车主专属权益做得都非常周全。我想,大家之所以觉得蔚来车主总是吹蔚来,还是有原因的:因为车主确实被伺候得很好。也举一个例子:之前与🐦去日本玩,我们就选择自己开车去机场,然后通过蔚来驾享服务,让服务小哥帮我们把车从机场开回家停着;回国时反之,让驾享小哥提前把车从家里开到机场,我们下飞机直接开自己的车回家。这样的服务体验实在是太极致了。

所以,我最终导出的结论是 —— 蔚来是六边形战士,而我的车,是完美的车😝。

三个月过去了,每每想到自己靠自己赚钱买了一辆车,还是会觉得很不可思议。我觉得买车的本身就是一趟小小的旅程,而买到的车又会带我踏上更多的旅程。这既让我充满期待,又让我庆幸,至少我还有一些想要得到和想要去看的东西,这是生命力的来源。

  •  

毕业,上高中了

初中毕业。

回望初中三年,我感慨万分。初一初二参与各种各样的活动,生活丰富多彩;而初三则立即投入了紧张的备考中去。这三年有低沉,有难过;有欢笑,有喜悦;有得知难以入团时的一份遗憾,更有中考出分时的一分惊喜。

三年来,我的生活总体可用两个词形容:欢快,充实。

可现在,我要与母校蚌埠六中正式告别,与初中生活正式告别……

告别的同时,也代表着高中生活的开始。我就读于一所省重点高中——蚌埠第二中学。在这所学府,我也渴望过上欢快而充实的生活。也就是说,这两个词语不仅仅是我对过去的总结与怀念,更是我对未来的期待与憧憬。

录取通知书封面

录取通知书内容

“少年不惧岁月长,彼方尚有荣光在。”

高考加油!

  •  

关于文字与文化的关系

文字,自古以来就是文化的见证。潜意识里,我们将文字当做文化的一种极端重要的象征,仿佛有了文字便有了文化。可是读了费孝通先生的《乡土中国》后,我对文字与文化的关系有了新的认识:文字并不等同于文化,只是文化的一种体现。它可以在一定程度上阐释文化并便捷人与人之间的交流,实质上是一种传播文化并推动文化交流的工具。

在这里请思考,文化究竟是什么?它包罗万象,博大精深——诗词歌赋、琴棋书画、地理人文等等,无不属于文化的范畴。诗词有唐诗、宋词、元曲的延续,书法有王羲之、颜真卿、赵孟頫等人的接力;科技有《九章算术》《齐民要术》《天工开物》的发展,而文字也有甲骨文、小篆、行书、楷书的传承。从这些方面来看,文字只是文化的一个小小的分支,只是文化的一部分。

但是不可否认的是,文字是文化至关重要的一部分,它起到传播文化并推动文化交流的作用。我们可以通过新文化运动的进程理解这一点。当时是20世纪初期,报刊、杂志等蓬勃发展,先进文化也以文字的形式通过这些报刊传入中国。通过报刊文字,中国人了解了无政府主义、马克思主义等新思想、新文化;这体现了文字对文化的传播作用。而在救亡图存的风波中,先进知识分子们通过上文所说的报刊等了解到新文化后,他们进行了广泛的讨论。提倡各种思想文化的均有代表,此时文字对于这些思想文化的交流便至关重要。比如,这期间不乏旧势力与新文化的争斗,比如林纾、马其昶为代表的桐城一派便与陈独秀为首的新文化展开了激烈斗争。这期间文字成为两股势力交锋的重要载体,如此便体现了文字对文化交流的推动作用。

讨论文字与文化的关系,我们还要提到它们的载体——人。所谓“文盲”,即是这些人中的一部分。根据相关资料,文盲是指不识字并且不会写字的成年人。在现代社会,有相当一部分人认为文盲便是“笨蛋”,便是不懂得文化;其实不是。“文盲”的问题在于文字认识方面的缺陷,而不是对文化缺乏认识,更不是智力方面的低下。

在新文化运动的后期,工人运动渐渐走上历史舞台,而且扮演起了主力军的角色。为什么呢?我们可以这样理解:在知识分子们经过广泛交流形成共识后,他们便将这交流成果传达给工人们。工人们虽不了解文字,但理解文化,他们懂得救国的迫切性;因此,他们便立刻投入了艰苦卓绝的斗争当中。这一系列的过程说明工人们虽是“文盲”,但也对中国国情与文化有着深刻的认识。这个现象进一步证明了:文字只是文化的一种体现,说到底也仅仅是一种交流工具,它决不能成为判断一个人是否具有文化认同的依据。不了解文字的人,也可以理解文化;但文字的重要性,毋庸置疑。

综上,我认为我们应当对文字与文化的关系抱有更加清醒的认识。我们不应抵触甚至鄙视那些所谓的“文盲”;但是不可否认的是,文字是需要我们去学习并加深理解的。通过学习文字,我们或许能够更加深刻地体会文化内涵,领悟民族精神。

  •  

zotero批量修改条目语言解决”等”和”et al”混排问题

✇遐说
作者Dorad
在使用 Zotero 进行参考文献管理时,当采用 GB/T-7714 标准进行中英文混排时,可能会遇到“等”和“et al”的问题。本文提供了详细的分步指南,介绍如何通过修改语言设置来解决此问题。该指南包括使用 JavaScript 代码的具体步骤,以及运行截图以帮助用户可视化该过程。
  •  

一些想法

最近看了些先秦时期的一些讲义书籍,发现法家和儒家实际上是两种不同的思维方式。

首先,我发现以孔子为代表的儒学,作为后来主流的学派,它提倡的是“仁”。

君子去仁,恶乎成名?(《里仁》)

上面这则言论中,要求“君子”一定要做到仁。而如何成为君子?实际上还是要做到仁。在孔子眼中,如果人与人之间都是一种仁爱、友善的关系,那么人间便是一种互相宽容、相互成就的情形。

躬自厚而薄责于人。(《卫灵公》)

己所不欲,勿施于人。(《卫灵公》)

对于上面两则,试想一下:如果每一个人都对自己严格,而对别人宽松,那这个世间还需要法律吗?人们自己就是自己的“法”,约束着自己的行为,完全不需要外力的约束。

因此,儒家强调的是自己对自己的约束,它实际上是基于整个社会普遍具有较高境界的理想状态。因此,它可以作为统治者统治人民的一种指导思想,但这种思想并不实用,因为人们的境界尚且达不到。而这时就需要法家的法治来制约。

法律是一种非常具象化的东西,它不像“仁”这样抽象,因此更具有普遍约束力。即使人们不懂得很多知识,也知道法不可违——这便是法的优势。很明显,法家是基于整个社会实际的情况来创立的指导思想,即强调法律对人的约束。它是一种实用的思想理念,即使不那么崇高,但具有很强的制服力。

综上,正因两者是不同的思路,所以在古代社会扮演的角色不同。儒家更像是一种宏观大方向的体现,法家则是一种具体细节的约束。但不可否认的是,两者都非常重要,缺一不可。

这是些不成熟的想法,敬请批评指正。

  •  

下雪了

谁能想到,2024年第一场雪下在晚冬,这样一个平淡的日子。

看到那一片片雪白的冰晶,连绵不断地抚摸着大地,一个个小白点若隐若现。望着苍茫的天空,我才真正意识到:新的一年要来了。

不知道为何,今年年初很多人都喜好庆祝元旦——包括我也加入了这一阵营。那日晚上跨年的气氛甚至不亚于春节的喜庆,人们纷纷走上街头,逛街,放烟花……好不热闹。可是,我实际上更加盼望的,终究是2月10日——那在我们中国人眼中真正的新年。

此刻我的内心有一分坦然,但实际上更多的是期待。可眼前还有一个拦路虎令我一直放不下——年前的关键性考试。紧张的筹备浇灭了我的激情,但没有压灭我内心的那分渴望。

其实,我本应该坦然,毕竟风风雨雨都过来了。这一个学期,我不知经历了多少次大考小考,感觉我仿佛熬过了一整个学年还多。这半年发生的事情太多了,到现在我甚至不能够从紧张的状态之中完全解脱出来。而眼前的考试,我不能够置之不问。

现在只想一件事:严阵以待,静候新年

  •  

468. 网站磁盘满 流量超

放假,带孩子出门逛了一圈,回来刚想记下行程,发现博客没法访问了。本想搁置下,毕竟不是计划内的,不过总在心里绕着放不下,只好先行处理。大概也是性格的某种固执,稍有点哪里不舒服,就会上头。

又是小白,只好去问空间服务商,打回来说是磁盘超了,再问说要自己查。一头雾水,当初用了最简单的wordpress,官方最直白的主题,连插件也是能不用就不用,就是因为技术的事情,实在搞不明白。虽然现在有很多途经可以搜索,摸着自己干,可是真累,一不小心还容易弄得不可收拾。

不过客服默认是有开发人员的,让到管理后台找大文件,再纠缠,就给了句,要么就是被黑了。可是一个个人小博客,黑个什么劲儿呢?

开始以为可能是留下的朝花,因为大多数文章都是摘录,保不齐得罪哪个网站,或者前辈。本想多保留一段,算是个回忆。不过如果太麻烦,嘀咕着要不还是删掉算数。不过打开文件管理器一看,没占多少。

倒是主站这里出了问题,耐心地一层一层看,总算在awstats里面发现了端倪,无数个文件,有两个还超过100M。100…… 整个网站都没这么大,这是干什么用的,搜了一遍,又找了一遍各种插件,心想让别人动手,软件干活,总比自己人肉删安全些。

结果都不管用,后来看到个文章说大概是日志文件,也没什么其他办法,心一横,删吧。不过还是谨慎地先删了几个试试,没什么影响,一气算弄干净了。看看磁盘占用,一下子减掉了一大半。

不过流量那边,还是红色,客服说到下个月会自动恢复,好在月底了,候着吧。

一回神,太太叫吃饭了,起个大早,弄了这么个事儿。

  •  

467. 《繁花》数则

王家卫导演,《繁花》电视剧和小说的关系,可以参照《东邪西毒》和金庸《射雕》。不无联系,但为负数,因为确实少,且修改了大多数人物关系。一贯风格。

多年前到静安图书馆,面聆金宇澄先生关于《繁花》的讲座。提到一句,王家卫导演已经在着手改编,具体会怎样,金先生笑笑。

好像已经广为人知。电视剧剪辑前,金先生笑,没办法看。网传是和许子东先生说的,八卦一个,才知道许先生的太太是陈燕华,果真男才女貌。

和金宇澄先生近旁朋友聊天,滔滔不绝表达了自己对小说的崇敬。那边静静地说,觉得电视剧比小说好,时代和氛围的把握,王导外,地球不做第二人想。确实如此。

金先生在讲座上也表达过类似观点,谁改都是改,不如让王导来,至少有个品质的底线。看《繁花》改编的沪上话剧和评弹,深以为是。

一直认为,无论叙事还是语言,繁花是现代经典,也是上海代表,前者足矣名列前茅,后者则是前无古人。

也是这次聊天的棒喝,可能很多人想法并不一样。当年讲座,抱了一大批书请金先生签名,分批送给和上海有若即若离关系的朋友,想听听他们意见。后来一无音讯。笑笑,不响。

需要反思。大概也是金宇澄先生自己的一个警醒,既是毕生功力,也还在不断打磨。不仅是吴语地区的作品,应该是一个时代的代表。

相似之处,感觉电视剧和小说都是匆匆结尾,各种续貂都无济于事。金宇澄先生一个观点,老了就是要受苦的,就是莫名其妙收场,现实如此。

当年有无数讨论电影主角的话题,沪生和阿宝是梁朝伟和张震(金城武),小毛用张晋或内地某武行,李李是张曼玉的,其余女生则依王导班底,相当契合原著。

讲座上血往上涌,观众提问站起说,依原著描述,金先生如果现身影视剧,当出演搭救李李的澳门大佬。那时还有火云邪神的造型,亦正亦邪,又独具文人气质。

  •  

金钱与人际关系

金钱问题,是一个敏感的话题。有时,只要不谈钱,大家都是朋友;但是一谈钱,多半会引起争端,甚至反目成仇。

我曾见证过这样一件事。一个人在网上卖书,他一开始没有提出价格问题,只是把书的样品发了出来,简要概括了书的内容,并扬言自己书的品质如何好,自己多么辛苦,希望大家的支持;下面评论果真赞不绝口。但过了几天,一谈到价格了,说:“50元!“网友纷纷议论,有人甚至质问:“你这个书怎么那么贵!”这个人说:“我辛辛苦苦做出的书,当然要工本费!”此时,评论区的“喷子”也坐不住了:“你要卖书你直说,还搞这一套!我早知道你这样的人不是什么东西!什么破书,我不买了!”

这个事例或许有些夸张,但是确实一定程度上反映了这个社会下金钱与人际关系上的矛盾。

那么,这种矛盾为什么会存在呢?我想问题的根源在于:贫富差距的存在。(即发展的不平衡不充分性)

假若这个世界上每个人都一样富有,那么金钱就没有存在的价值了,在都能各取所需的情况下,人人都能友好相处;而假若人们都一样贫穷,大家或许都不在乎钱,反正都没钱,一起干就完了!(当然只是假设:这种金钱已经存在而人们还都一样贫穷的情况,现实中不可能存在;反之亦然。)

但只要贫富差距一存在,人际关系就不一样了。在交往中,人们多少会产生由于金钱多少而产生的嫉妒心理;这样的心理若不去激发,或许还能正常交流;但如果一去“点燃”它,那就一发不可收拾。所以所谓“见利忘义”“见钱眼开”都是这么来的;“忘义”和“眼开”都是人际关系的崩塌、道德伦理的沦陷。

但是,由于社会发展程度的局限,这种矛盾或许目前不会解决,甚至会长期存在。革命尚未成功,同志仍须努力。

  •  

2023年终总结与展望

又是一年过去,做些总结吧。

关于我

我在这一年中似乎又成长了许多。我有幸参加了共青团蚌埠市第十七次代表大会。在本次大会中,我受益良多,学到了很多关于团的知识,还参加了换届选举的投票工作。我今后必定不会辜负团对我的信任与期望,为团工作继续贡献力量。

共青团蚌埠市第十七次代表大会

关于学业

这一年,我生活的主基调或许就是学业。

去年的我在年终总结说“现在不是拼尽全力的时候”,现在基本拼尽全力了。我甚至抽不出一点时间做些别的事情。自从进入九年级重新分班后,我的学习压力骤增。首先是客观课程的调整:原来音美信息课都取消了,没有丝毫喘息的机会;而再加上重新分班的同学个个都是高手,竞争压力可想而知,稍有懈怠就看不见名次了。综上种种原因之下,我再没有闲情逸致练书法,写程序之类,更别提四个月没更的博客(开学后再也没更过)。

这一年的学习中,我把重心更多地转向数理化,不像原来那样全面发展。这导致的后果就是我的名次在第三次月考滑到了二十来名(语文150分满分的试卷考了110分)。没有办法,现在对数学物理的重视程度越来越高;但就这两门学科而言,我七八年级基本上是混过来的,难题做的少之又少。我不得不补这个大窟窿。所以这也导致我的语文英语没有长足的进步,文笔甚至没有了七八年级的水平。

继续加油!

关于网站

不比去年,我甚至还有些许精力打理博客——今年彻底摆烂,网站甚至被博彩网站抢劫,于是我将所有的服务都换成了各种平台的托管(白嫖且安心),评论系统也换回了由netlify托管的twikoo。但是这种调整也只是我学习之路中的小插曲而已。

关于技术

去年曾说过“前端后端双管齐下”,现在想想也是可笑。我想现在我是“数学物理双管齐下”,至于技术……

关于世界

这一年,疫情终于散去,整个社会也恢复了生气。今年的世界也发生了许多事情,令我印象最深的是李克强总理的逝世。总理,一路走好!

今年,2023,平淡而充实的一年。现在内心格外平静,期待在新的一年,万事皆胜意,新的曙光如约而至。

曙光

  •  

466. 装修规范

提起这个话题,总是已经受了很多委屈。

这个行业很有意思,从商品房开始,就是各种纠缠。几乎每个城市,每个家庭,每个人,一提起这个话题,都是摇头。从兴高采烈开始,历经精疲力尽,到垂头丧气。

内卷,和互联网,更加剧了这种不美好的体验。因为必须低价引流,全盘承诺,美化过程。待到实际操作,再利用丰富的经验,拷打业主的耐心和意志,并取得最终的胜利。

暑期过后,利用两次策展的间隙,妻考察了数家从某网请来的装修公司,并最终选择了一位,较留下来经验相对丰富,实施内容和有效回复都比较满意的,做一次厨卫改造。

商谈了所有细节后,签了闭口合同。才知道还有这么个名词,大意是说后面不会加钱,引申为进来的工人光干活,不多废话。当然很美好,虽然稍微多花那么一些。

显然是冒傻气,所谓国情,才会出这么个名词,再如定,订,ding在购房里面都是不同含义。充分体现出这个民族的智慧和,很多小心思。合同,这种高于一切的信用产物,显然是压不住的。

刚进场,拆旧师傅还没干货,说要加钱,因为建筑垃圾要走地下车库,再上去人工费力。妻都呆住了,说了半天,然后电话过来抱怨。脑子很清醒,让她先离开,然后群内和队长说了两句,按合同闭口。然后和这位拆旧师傅说,加钱找队长。消停了。

这个行业的运行规则,慢慢有所了解,大概是一个人接单,类似于前台。然后手头会有一些各种行业的师傅,比如泥瓦工,电工,大多还都兼通,然后逐个分包。

模式是不错,但一来管理很难象公司那样上传下效,大多怨言过去均是一句话,和师傅商量。二来价格互相之间压得很低,导致师傅到了现场,如鱼得水,各种伎俩层出不穷。

很好奇的是,居然这么多年,这么多投诉和纠纷。直到现在,几乎没什么大的改善,也是国人之迷思。

  •  

评论|如何看待面青地项目允许博士后变更依托单位

✇遐说
作者Dorad
近日,国家自然科学基金委党组会议审议通过了《国家自然科学基金部分人才资助政策调整方案》。根据方案要求,自2023年7月4日起,取消面上项目、青年科学基金项目、地区科学基金项目不允许博士后变更依托单位的限制,对于经审核符合相关规定、且变更后单位可以支撑保障其正常开展研究工作的,允许其变更依托单位。
  •  

465. 是时候暂别了 朝花 读书声 饭否

筹备听雨雅集的这段日子里,也想了一些自己坚持的一些事情。博客,摘录网站-朝花,摘录公众号-读书声,饭否的摘录分享,想的是初衷,和未来的打算。前者是发自内心的冲动,想用摘录,来说自己的一些观点。后者却是从未想过,甚至为了页面干净,排除了各种商业广告。
 
每天晚上在网上的闲逛和摘录,现在想来,有时是出于内心,但也有些日子,会敷衍一些。仿佛觉得有人在看,而不能无所事事。偶尔想起这点,还会有些迷茫,这样是为了什么?为了几个点赞,转发,带来的虚荣感么?五十知天命,生活中却仍有太多看不懂的人和事,但最看不懂的,还是自己。
 
不知道什么时候才能豁达。
 
另外一件事,批评比建设容易,且能获得更多认同。无论是在底层还是中层,在做这种类公众媒体的时候,是既欣喜,又不愿意看到的事情。涨粉和获赞最多的几次,都是谐谑地批评,看到数字和内容,心底的矛盾油然而生。
 
于社会发展而言,需要批评,更需要做一些对社会有意的事情,尽力避开国家这个词眼。以前觉得每个人做好一点,这个社会就前进一点只是鸡汤,个人的努力太容易被淹没。
 
但当自己把一张废餐巾纸,放进口袋,一直带到家里扔进垃圾桶时,会有一种异样的感觉。我这一代人,多多少少还残存着立德立功立言的使命感,虽然接近被磨圆的年岁,所剩无几。
 
突然间理解兼济天下和独善其身,其实是一回事。不赘。
 
另,本想也暂停博客,想想还有些呓语,那么多朋友,看时间吧。
 

 
朝花 是时候暂别了
 
是时候暂别了,全站文章会保留,
五十再创业,发现心底热情并未消退。
批评的声音,能获得更多关注,
只是于社会前进意义甚小。
 
个人博客:http://www.winature.com/
听雨雅集公众号:tingyuyaji2020

  •  

464. 闪亮的日子 丨听雨雅集① 收官

友圈记录

本想做些总结,待要开口却发现不知从何说起。公司同仁灵光一现,说你的朋友圈发文,有时间线,不就是最真实的感受么。细想的确如此,后续总结是给自己看的,当时的各种感受,自然在文字间有所流露,流水如下。

0908

周五早市,见见老友,祝贺外都询问下一步。和前辈请教,多勉励警策,铭记于心。午后蒙中福王总批示,让出十二月初档期,成全听雨雅集之四季设想,感恩知遇。为补前回缺憾,展商只接受好友和好友推荐,需审,见谅。

0903

收官,最大的收获是一批挚友,和团队,感激之情言辞无从达意,来日方长,尽全力,不辜负。认真算来,呗美王总是我该学习的前辈,无论在商业方向,还是网络逐浪,初衷助行业出路不改,愿出一份薄力。

0903

今日收尾,连夜和团队沟通后,达成共识,高柜师友精品,凡标价者一律七折展销。仅想实证两点,上海有藏家群体,中福以精品平价。也建议展商尝试善终,前期观望的藏家群体,在慢慢向雅集聚拢,此条可随意转发。

0902

李老师昨天开玩笑,问我有没有什么限定或者不能讲的,会影响里面的古玉商家吗?会不会走出不出中福,我说李老师随意,其实邀请的这些朋友,大部分都是由收藏爱好,而进入商家的。指出东西不对,难过外会感到高兴,因为眼力上了一个层次。

0902

昨晚回到家,发现皮鞋底磨坏了。看看手环步数:28288,能凑一个半马。却觉不出累,翻翻朋友圈,看到好友们的图,躺在床上傻笑。得到太多师友的建议和帮助,也错过很多和新朋友好好聊天的机会,招待不周,见谅!

0901

如果说这三天上海最值得打卡的地方,应该是上海博物馆。4日青铜、雕塑、陶瓷、印章、货币,还有,钟爱的玉器馆,将闭长待迁。公益讲解场次图是博物馆曹老师制作,从最早的手写,表格截屏,到如今的大片感,用心。不要错过。

0831

明日开展,恭候师友莅临指导。有个很强烈的感受,当真正不计得失做一件事情时,会有很多朋友帮忙。有时觉得缺憾愧对,却得到更多的鼓励。年轻时一些想法不敢试错落地,其实错失了太多贵人和师友。

0829

太熟悉的朋友,反而不知道从何说起。太多的事情澎湃而来,失去头绪。数十年的交往,还能在一起喝喝酒,当为人生的眷顾。有段茅台掉价,周兄对此味颇有心得,三番五次循循善诱。粗钝如我,也慢慢品出酒和人生的好坏。

0829

昨日商谈展柜布局,晚间盯着小朋友做图,头一歪就睡着了。只好拖延到上午通知各位,见谅。报到布展确定,31日周四下午1时起,6时止,诸兄内部交流。需1日进场的,需早间9:45联系带入。余见备忘录和物料图,不赘。

0827

临时加绣纪念T恤,公司小朋友居然电脑生成印稿,哭笑不得,杨兄友情救场。今日再抽核部分展商展品,明日确定展位,不再预留。还望各位细选展品,现场会有雅集同仁巡看,如建议收起部分,也请谅解配合。

0823

自己开始喜欢古玉时,一直想搞明白,经验和望气以外,有没有什么确定的点,可以明白工艺和断代。慢慢找到对岸的网站和书籍,知道杨建芳先生的团队在做,不过系统理论,就是科学,并不容易掌握。偶遇杨门李老师讲学,颇多获益。

0819

最想,也最难把控的,是开门度。每天数十个电话微信,大多婉拒,挫折感很重。甚至碰到以他人图片,取展位资格,发现一瞬,既笑且苦,何德何能。尽力弥补漏网,友圈偶尔看到朋友的推介,始有过河小卒之感。

0817

印象中,轻舟兄是一种厚积薄发式的横空出世,各个形制的藏品都是仓储式的量。而抱着去粗存菁理念的轻舟兄,如今有了自己的艺术馆,令人羡煞!此次预览轻舟兄自行提供摄图,光影真义,公司制图小姑娘追摹良久。

0815

结识篆刻罗老师的时候,恰逢自己开始志愿讲解印章馆,人和人还是有着某种缘分的。间歇请教些问题,和太太两人涵养极好,对于门外汉的胡搅蛮缠,微笑以对。注册商标时慌乱用了个印稿,现在想起来,应该请篆刻家起草。

0813

陈兄说起早年喜欢古玉,就去报了个班,动刀治玉,对料对工有了新的认识。心生敬佩,把一件事儿往深里钻的朋友,身边颇为不少,各有建树。这次说起办听雨雅集,马上提醒注册,当时后背发凉,枉做了这些年的广告公司,致谢!

0811

因为也做公司,顾对组织效率极为敏感。即便以挑剔的眼光看,中福王总团队执行力保持在很高水准。凡沟通必有结果,用对方立场考虑问题,也是愿意做听雨雅集的初因。只是于个人的褒奖,汗颜而不敢当,全赖师友协助。

0809

先睹为快③ 华堂主人 :徽章孙慧荣 沪上文玩代表,作为晚辈,无论是灵石路轩昂的形象,还是中福城高亢的声音,都有些不敢近前。不过交往多了,其实和音量一样,是个很爽朗的性格。聊记闲事一两则,至今引以为省。

0809

先睹为快② 古缘堂主人:老魏 结识已久,早年每周藏宝楼早市会面,互相就很了解喜好和品味。展会请益良多,倒张不开口道谢了。圈内诸友,意向参展、预留柜台、或此前联络时间待定者,还请尽早确定,顿首。

0806

尽力说服身边的藏家,把沉淀下来的自珍,这次拿出来公开展示。只是说服别人,向来拙于此道,只能很直率地表白。却顺利得出奇,得到了很多前辈师友的赞同,幸福得有些飘飘然。自然不辞其任,抛砖引玉,先睹为快。

0803

前期确定参与师友,已经逐一联系确认;邀请后待定的玉友,不便过于打扰,如仍有意向,可私聊联系我。英雄帖发出后,圈内新添加朋友,如有意共襄雅聚,展位优先。十日后广而告之,柜台按次序发放,不再保留。

0802

办展比想象中难,头回上轿处处需着力,比如意外的商标注册和商用字体问题,满头冷汗。但比预计的又顺利很多,因时有师友指点,不胜感激。不知不觉展期进入倒计时,诸事逐一落定,但求无过,不贪有功,果彻因源。

0718

逐一邀请师友,也很开心得到诸多鼓励和建议,譬若不可过度自得其乐。设计和文案很容易陷入自我欣赏而导致语焉不详。譬如柜台、包间是展厅名词,房间是选择入住,避免因极简而造成混淆。闻过则改。

0716

沟通大概是自己的弱项,人众容易社恐,独面则会话痨到不知所云。筹备阶段二者兼有,一路请益,所获良多,很多事情在闲谈间慢慢有了轮廓,全赖师友相助。只是喜好古玉初心不改,方案基本定型,望师友不吝指正。

0707

肖斯塔科维奇第二爵士组曲的Waltz II,压抑纵放。铜管迷离低沉,弦乐堕入角落奏出舞曲。铁幕时期的旋律华美至极,又孕育着落魄的忧郁。广播里响起一时记不得曲名,好在微信有了听音的功能,也用在库布里克《大开眼戒》的片首片尾。

0705

知识分享后的获益是,有师友指出缺失错漏。这件龙沟行文时未敢确定材质,只猜测是玉质提油。午后玉友凌兄发来消息,曾在展厅请教策展的上海文物商店,回答是,起初也不很明确,后来做了光谱测试,发现是翡翠。日有一得,致谢!

近读青铜器史,才发现海内三宝之大克鼎,上博镇馆之一,另有冷僻名曰膳夫鼎。这位“克”厨外征番邦,内聆民意,按周礼列鼎制度,权重堪比王侯。可见大鹅厨师受宠拥兵,古已有之。抓住胃就拿下心,不独是说给女生听的。

请暑假赋闲在家的高中生,画张简易交通图,晚上交上来居然不是涂鸦,一张powerpoint生成再转存的jpg。说本来让ChatGPT生成3d、过于魔幻作罢,明天让妹妹试试彩绘,看看会有什么新花样。

0702

逐一邀请师友,也很开心得到诸多鼓励和建议,譬若不可过度自得其乐。设计和文案很容易陷入自我欣赏而导致语焉不详。譬如柜台、包间是展厅名词,房间是选择入住,避免因极简而造成混淆。闻过则改。

0701

禁不住师友撺掇,试试能不能做一次古玉交流会。心头本也有这个念想,又有便利条件沟通场地和藏家,时机凑巧。英谚有云,总要有人做,为什么不是我?这些年得师友照顾良多,此次当尽力而为,不负友朋。

再次致谢诸位师友,来日方长!

不足之处,如我后补。

 

  •  

收到明信片啦!

✇遐说
作者Dorad
万万没想到,这么些年过去了,居然还能再收到明信片! 近期收到明信片两张,分别来自王云子同学和关关&六六!非常喜欢,十分感谢!
  •  

尝试配置PicGo-Core日志输出等级

✇遐说
作者Dorad
开发notion2markdown-action的时候,想修改[PicGo-Core]()的日志等级,但查询官方文档后未得知设置方法。 好在该项目是开源的,所以在查看源码后,得知了其配置方法,故尝试进行配置。
  •  

为土地,为正义,为和平

细读《艾青诗选》,我想其中有三个关键词:土地、正义、和平。

土地

“为什么我的眼里常含泪水?因为我对这土地爱得深沉……”(《我爱这土地》)

艾青的这句诗想必是妇孺皆知,这也足见他对土地深深的爱。为什么艾青如此深爱这片土地?我想他爱的恐怕不仅仅是土地本身,而是其中蕴含的中华文明的伟大足迹。五千年前,泱泱华夏在东亚这片土地上焕发出了蓬勃生机,而这土地便记录了这千百年来人们辛勤劳作的历史。农民的一滴滴汗水洒在土地上,这些汗水都在土地上沉淀下来,迄今仍不断地释放出能量——这能量便源于上百个世纪以前的先民的不懈努力。艾青爱的便是这生机勃勃的能量,爱的便是这经久不衰的勤劳的精神,爱的便是这代代相承的中华文明!

正义

“正义是属于他们的;耻辱的将变成光荣;束缚的也得了解放;……”(《九百个》)

这句诗是艾青为赞颂陈胜吴广起义而书写的。作为一名出身中等地主家庭的人,艾青不顾自己的出身,毅然决然地为如陈胜吴广那样地位低下的农民发声,这是与他所出身的地主阶级相矛盾的。他的正义感促使他越过了阶级围成的藩篱,促使他“六亲不认”似的反抗他所出身的地主阶级,那是有多么伟大的精神,多么宽广的心胸!而就是这样的精神造就了他的伟大,使他的诗句有了非凡的意义。

和平

“让我们不再走了吧,也不要回到避难所去!我们应该有一个钢盔,每人应该戴上自己的钢盔。”(《梦》)

这句话中,“避难所”“钢盔”等词都展现除了战争的气息,这与作者写的背景有关。1937年发生了七七事变,而在事变发生之前,作者便有了战争的预感,于是写下了这篇文章。果然,全民族全面抗战在这一年拉开序幕。这首诗中并没有对战争的场面进行描写,但是处处都透露出战争的惨烈。那么,这首诗写作的目的是什么呢?我想是为了反对战争,呼吁和平。

和平是世界的主旋律,但是艾青生活的那个时代却是少有的战争年代。艾青是一个和平主义者,他痛恨战争不仅仅因为战争会伤害人民,还由于战乱对于一个国家的损失、对于一个民族的打击是巨大的。

中国的苦痛与灾难,像这雪夜一样广阔而漫长呀!
雪落在中国的土地上,寒冷在封锁着中国呀……(《雪落在中国的土地上》)

这些文字便表明了,中国的战乱让作者感到极度痛苦、不满。作者的不满便映射了当时全中国人民的低迷、沮丧。因此战争带来的不仅仅是物质上的贫乏,更多的是精神上的打击。这样一来,艾青对和平的期望甚至渴望是不言而喻的。

为土地,为正义,为和平……艾青为了这些写下了一篇篇动人的诗篇。他对土地的热爱,心中满腔的正义,对和平的殷殷期盼跃然纸上。这样的一份诗集打造出了一个鲜活的、有血有肉的诗人形象,他激励着我们当代青年不断前进。

  •  

A Cultural Journey

Recently, I have been to Beijing for 4 days. It was my first time to get there. Before the trip, I was so excited that I had a great preparation. The night before the first day, I didn’t sleep well because I thought about how to have fun there all night!

After four hours’ ride on the train, we arrived at Beijing. It is such a big city but there are few high buildings. Why is that? That is because there are some quadrangle dwellings which are not very high. They are very important to this city. They are a symbol of the Old Beijing.

After a great lunch, we went to the Great Wall. It is such a long wall that we can’t see the edge of it! After having a search on the Internet, we found it is over 20 thousand kilometers long! That is fantastic! We took a deep breath and started to go. Although I didn’t sleep well last night, I didn’t feel tired at all. We ate nothing but climb from afternoon to evening. We got such a strong feeling of joy and satisfaction when we found we had 20 thousand meters’ walk! We walked one thousandth of the length of the Great Wall!

the Great Wall

On the second day, we went to the National Museum. There are so many people there! After we get into the museum, we went to see the Ancient China. There are many valuable cultural relics there such as Houmuwu ding, Four-goat Square Zun, etc. Before I could only see them on the history books, but this time I was able to watch the real things in front of my eyes! I took a lot of photos to review the relics.

That night, we tried something different — Copper Pot Instant Pork. That was so delicious that we ate four plates of meat. We got very full at last but still want to eat more!

Next day, we went to the Tiananmen Square in the morning. It is a large and beautiful square. We could clearly see the big photo of Chairman Mao on the wall. We all kept serious and thought a lot. Without Mao and other leaders, we wouldn’t live a happy life at all!

In the afternoon, we got to Sanlian Bookstore and read there for several hours. We found many good books there and bought a few of them. Maybe it is the best bookstore I’ve ever been!

Sanlian Bookstore

The last day, we had a look at Qianmen Street. We bought some snacks there and saw something interesting. After there hours’ walk, we felt bored and went back to the hotel. Then we had a lunch and went back to Bengbu in the afternoon.

It was a nice cultural journey to Beijing. We reached many places of interest, enjoyed the dietary culture and also had a good time at the bookstore. This way, I have a deeper understanding of the Chinese culture. As a middle school student in the new era, I will study widely and try my best to help realize the China Dream.

  •  

超实用的铁路地图网站

✇遐说
作者Dorad
分享两个铁路信息查询网站,依次是China Railway Map(以下简称CRG)和OpenRailwayMap(ORM)。China Railway Map(CRG)适合查询车次轨迹;简化版铁路信息。OpenRailwayMap(ORM)适合查询全球铁路分布;轨道详细信息;城市轨道交通信息。
  •  

463. 高速收费刺客

刺客一词,借用于超市柜台旁的小玩意儿。因为伸手可得,往往会在结账时带上一件,也不会在意价格。但偶尔会在一堆零钱价格的东西中,埋藏两三高价物品,如被刺客袭击。

譬若有次超市购物,结账时女儿要个棒棒糖,选中那个最大的。想来普通的半圆钱,这颗也不会贵。因为流水单需要保留扫码开发票,就揣起来了。到家细看了下,这颗糖居然是十几圆,放在总价里不显眼,心底一抽。暗暗问候了几句作罢,也只能如此。

大规模为人所知,大概是超市的冰柜,标价牌一字排开,其实不能一一对应。按照惯常思维,取出结账,临时发现价格匪夷所思,碍于收银员眼神,或者后续排队者,再或者是实在不想再跑老远送回去,付账后苦笑被刺。虽然被曝光,但看周边超市的格局,似乎依然如此,仿佛是另一种惯例。

没想到还有高速刺客。

周中因听雨雅集古玉展一事,到常州拜访几位藏家。去程收费131元,惊了一下,上海到常州不过170余公里,怎么这么贵了?不料回程只66元,一半的费用,百思不得其解。回忆起来,都是高德导航,印象中往返是不同的高速标号。和朋友说起,问是不是过苏锡交界的长隧道了,才明白过来。不过重新查询导航软件,未发现有这个费用。

暗暗心惊,江浙沪高速网络发达,条条大路通罗马,出门少做准备,大多交给导航。加之分段收费改为汇总计价后,如非有心,不会计量每段费用。车流如织,实为生财之道。

只是此刺客非彼刺客,超市如果顾及钱袋,放下面子,不,揭下超市的面子,有重新来过的机会。高速完全不能退货,所谓来都来了…… 大概率来说,纠缠投诉也不太会有作用。

路是人走出来的,还真是。

  •  

9月丨听雨雅集 上海古玉交流会 展品预览 ①

写在前面

办这次上海古玉交流会,有两个初衷,第一个是尽量开门度高,所以到现在为止,都是在雅集成员集体讨论后,才通过邀请名单。

第二个就是,劝说相交已久的藏家,把沉淀下去的自珍,拿出来向公众展示。即便这样显然是在向非盈利方向发展,但自然而然已经走到这一步,不辞其任。

以为这件事会很难,说服别人向来不是长项,时常劝着劝着,会觉得对方占着逻辑的高处,而自己实在拙于讲道理,非但乖乖交了白旗,偶尔还会助敌为虐。

顺利得出乎意料,在很直白的表达这个想法后,几乎得到了百分之百的赞同,在飘飘然之际,有些前辈还会提出建议,自告奋勇去邀请其他藏家,幸福得有点脑晕。

在经济环境并不好的情况下,如果暂时要在商业上停滞的话,也可以好好读书,好好看看东西。
那么,利用展前这个月,在能及的情况下,介绍一部分参展的藏品,也为上海古玉交流会做个热身。

文中所有古玉,均会在现场展示。

从自己开始吧。

龟鹤遐龄 聼雨斋藏品

龟为四灵之一,
《述异记》云:龟千年生毛,寿五千年谓之神龟,万年谓之灵龟。
民间素以龟鹤为长寿瑞物,鹤寿千年,龟龄万载,
以此系带,意在添岁增寿。
— 《中国玉器赏鉴》 薛贵笙

此纹饰始建于宋,元代流行,
明清皆有仿制。
龟鹤同龄 乃同享高寿之意

《韵会》:“龟为甲虫之长。”
龟寿万年,是长寿之象征;
鹤是仙禽, 《崔豹古今》 “鹤干年则变苍,
又二千岁则变黑,所谓玄鹤也。”

巧色玛瑙,中心隆起,倭角,
外表利用黄褐色玛瑙皮凸雕灵龟,
口吐灵芝状祥云。
龟灵献祥瑞,俏色留天成。

立意独特的巧雕,保留自然之灵气,
寥寥几刀,宛若人工天成。
观之灵动,妙趣横生;
思之含蓄,意味无穷。
形神合一,精妙绝伦。

形态生动,云纹“品”字形,
同类“龟吐云纹”的饰物在出土元代玉器与金器中均有发现,
上海松林区西林塔出土过一件元代青玉龟吐云纹带饰,
北京地区元代铁可墓中出土过一件“龟云四合”金饰。

该专题系列共计二十余件,
均会在本次古玉展中亮相,
期待与您的相遇!

  •  

江河的味道

历史的车轮持续转动,而江河的产生、壮大与衰亡也不断交替。来自万古洪流的江河,孕育了人类,孕育了中华文明。

万古的江河一直存在着,江河的味道也从未消失,甚至历久弥新。

江河的味道包含了人类的发展。几千年前的祖先们在黄河、长江两条大江大河附近生存繁衍,在这里他们由打砸石器到有目的地间接敲制石器,由采集植物、猎捕野生动物到种植农作物和驯养家畜。此时,江河在他们的生活中便扮演着重要的角色:人的生存需要水,作物的生长也需要水,猎物的冲洗、烹饪更需要水。江河融入了祖先们的生活,融入了那个伟大的时代。

江河的味道沉淀着丰厚的文化。唐宋时期诗词发展鼎盛,人们在不同的地区书写着不同的盛世华章。而有关江河湖海的诗句尤为丰富。“白日依山尽,黄河入海流”“飞流直下三千尺,疑是银河落九天”……这些波澜壮阔的诗句留下了文人们的名字,也留存着江河的味道。江河融入了文人的生活,融入了那个繁荣的时代。

江河的味道积累着变革的浪花。近代以来中国共产党不屈不挠,坚决与国民党反动派进行不懈的斗争。红军长征途中,我党的英勇机智与坚毅果断巧妙地运用在了江河之上。四渡赤水的举措搞得国军晕头转向,血战湘江的冲击硬是为中国的未来杀出了一条血路。中国人民的血洒在了江河之中,江河的味道也添上了浓重的一笔。江河融入了中国人民的生活,融入了这个崭新的、开天辟地的新时代。

江河的味道无时无刻不在沉淀、积累和发展。江河和每个时代息息相关,伴随着无数时代的兴起与衰落。它能够穿越时空,跟随历史的发展寻找永恒。它是万古以来历史的见证者,它的味道必定是五味杂陈的。

江河的味道,蕴含一切;江河的味道,孕育了过去,孕育着未来。

  •  

新冠首阳

✇遐说
作者Dorad
作为武汉地区的父老乡亲,近日喜提首阳,谨此记录。
  •  

共饮一江水

说来真是惭愧,作为土生土长的安徽人,却从未到本省的博物院一览。

这次,我来到了安徽博物院。我意外地发现这里正在举行特展。那便是共饮一江水——三星堆·长江流域青铜文明特展。好奇心驱使我买了一张特展的票,想追随先人的足迹,一览三星堆文明的壮美。

在游览的开始,展出的一种面具引起了我的注意。那是一种别具一格的装饰品,面具上有着犀利的双眼、高耸的鼻梁和细长的嘴。浓厚的眉毛和精心刻画的大耳朵都让人感到奇特。这种类人又不是人的面具到底蕴含着什么含义?它背后的历史渊源又是怎样的?

巴蜀文化面具

我将目光转移到了旁边的文字介绍,“巴蜀并辉”几个大字闪耀在我的眼前。

“古蜀文化以成都平原为核心,在商代、西周时期先后绽放出璀璨的三星堆文化、十二桥文化。巴文化以川东、渝东峡江地区为中心,与古蜀文化并辉共融。春秋战国时期,巴蜀文化更加展现出异彩纷呈的繁荣景象。”

至此我才知晓,三星堆的巴蜀文化并不是一种一元的、单一的文化,而是巴和古蜀两种文化融合而成的结果。

此后,展馆展示了古蜀时期的一系列文物,有的来自十二桥文化的金沙遗址,有的来自古蜀国开明王朝时期(即中原的战国时期)蜀王的墓葬,还有的是巴蜀文化与中原文化融合时的文物,那样的文物有巴蜀文化的特征,也已经有了中原文化的痕迹。

器皿、利剑、铜灯、图腾……不难看出,当时巴蜀文化发展得已经非常繁荣,他们有城池、有社会阶级的划分,图腾的精美并不亚于中原的甲骨文。古蜀国甚至是除了中原的商朝以外最强盛的一个帝国。而当它在被战国时期秦王朝吞并以后,则与中原文化完美地融合起来,构成了我们今天多元一体的中华文明。

伯各铜卣

不知不觉,我看到了结语,可是我却迟迟不能平静。若没有这样一个文明的存在,若没有巴蜀文化与中原文化的融合,很难想象我们今天的中国文化是什么样子。三星堆文化被永远地留存在了历史之中,可我却觉得它时时刻刻存在于我的眼前,生生不息地活在我们中华文明的文化与精神之中,永不泯灭。

巴蜀人、中原人,我们共饮一江水,齐心协力,在历史的漫漫长路中不断前进,共同繁荣!

  •  

462. 做了一次展览的策展

一段忙到不可开交,因为朋友的怂恿和自己的好奇心,开始筹备两个月后,不,一个半月后的展览。很多事情做起来,才发现和想象的不一样,当然,大多数是复杂程度增加,如果非要用数值来形容,五倍以上是妥妥的。

起因是多年一起交流古玉的朋友,说既然已经在古玩城喝茶了两年,为什么不索性办一次古玉雅集?场地方也熟悉了,工作人员大多也认识了。心里一动,倒是一直有这个念头,因为自己开始喜欢古董,喜欢古玉的时候,最迷茫的就是满世界的这些古玩,到底哪些是真,哪些是假,这种分辨,除了多看博物馆,多看书之外,其实很需要一些真正做古玩的,有诚信的人的帮助,需要可以经常看到对的东西而非假货赝品。

毕竟书上的知识,在现实面前显得苍白,博物馆的精品,只能看不能摸,没有一个体量和重量感,也不能翻来覆去地朝夕相处。眼睛里去伪存真是个很痛苦的过程,这大概就是学习古玉最大的门槛,很漫长的一个过程。待到自己心有所悟,偶尔和朋友交流时,也经常心有所慨,如果早些遇到他,或者他遇到我,可以少走不少弯路。

也是本次古玉雅集的初衷,召集一帮喜爱古玉收藏古玉的老友,身边有聚集了一批想要了解学习的新朋友,那么在尽力保证藏品和交流品的真实之后,是否会有比较好的化学反应,也请了朋友开两次公益课,具体入微的从科学分析入门,尽力避免玄乎的望气断代,那个对新手来说太难了。

麻烦的事情就在于,沟通场地,沟通藏家,沟通老师,而自己本身是偏内向的人,做这些事情多少有点勉为其难。但这一路下来,虽然有些累,实在心有所得,很多念头和建议,都在聊天沟通中有了新的进展,日有所进,是很开心的事情。

博客里有很多技术和推广方面的朋友,所以有两件事情也咨询下大家,先行谢过。

一是我们想做一个鉴定的网上小数据库,简单来说,就是收藏者送古玉到我们雅集来鉴定,如果是真品,那我们会给他出具一张证书。证书上会有个编码或者二维码(这个比较方便),然后通过扫描这个二维码会导到浏览器或者小程序(好像是潮流,也更便捷),展示出来的是关于这个藏品的五六张照片,和我们的文字鉴定结果。其实宝石鉴定这些流程和查询已经很成熟,我们试着在古玉方面也尝试一下。

二是这次上海古玉展的费用,全部是我个人承担,公益性质,因此除了朋友圈口口相传,和场地方的一些固定宣传渠道外,可能没有更多的钱去投广告。所以也请教做推广的博友,是否有适合的渠道?自己可以说确实是好酒,但确实也需要让受众知道,毕竟酒香也怕巷子深。如果有愿意做些传统文化方面赞助,或者热心公益活动的支持,则不胜感谢!

非常感谢大家!

  •  

461. 有什么简单的方法记住青铜器的名称和用途?【转载】

这篇问答很棒,深入浅出,基本把青铜器的分类交代清楚了,看了下可以注明出处可以转载,转过来看看,主要是自己学习备用。
作者:信古斋主人
链接:https://www.zhihu.com/question/40396015/answer/86380525
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

简单的方法真没有。根据用途分类,逐类而记,大概比死记硬背要好些吧。当然最好的办法就是多看实物。

根据马衡先生《中国金石学概要》中的讲法,我们可以将历代铜器分为礼乐器、度量衡、钱币、符玺、服御器、古兵这六种。其中又以礼乐器与兵器为高古青铜之大宗。以下就按这六个门类简单讲讲。

一、礼乐器

礼乐器又可分为礼器与乐器两类。礼器按照功用可分为食器、酒器、水器这三大类;乐器种类比较少。

1、食器

有鼎、鬲、甗、豆、敦、簋、簠、盨等。这其中又可分为蒸煮食物所用的炊器及盛放食物所用的食器。

鼎:这个不用多说,都知道是什么。

西周 大盂鼎 最典型的鼎式之一 三足两耳 圆腹垂倾 三足之间可添柴置火 两耳便于把持

鬲(音li 四声):《尔雅·释器》:“鼎款足者谓之鬲。”款者,空也。鼎足中空者,谓之鬲。很多人以为鬲都是无耳的,其实有耳无耳并非判断鼎鬲的标准,足部是否中空才是其根本区别所在。足部中空是为了增加所盛食物的受热面积。

一组馆藏的青铜鬲。前三器为无耳鬲,末为有耳鬲。

甗(音yan 三声):蒸器。可以简单理解为在鬲上边增加一个铜锅(甑),甑与鬲之间用一个带很多小孔的铜片(箄)隔开。在鬲中放水,甑中放食物,水蒸气通过箄将甑中的食物煮熟,其原理和今天的蒸锅类似。

商代 青铜饕餮纹甗

中间隔片箄的图片找不到,放个网上找的仿品图片吧,反正也是按照真的做的。水蒸气就是通过中间的小孔将上部的食物蒸熟。《世说新语》中记载:“陈元方季方炊。。。忘著箄,饭落釜中成糜。”就是在讲用甗蒸饭时忘了放中间的隔片,饭落入水中,煮成了稀饭。。

鼎、鬲、甗这三样属于彝器中较常见的炊器。至于战国以后流行的鍪、釜之类的平民用炊器,就不展开细说了。

豆:《说文·豆部》:“豆,古食肉器也。”可知豆这种器物是用来盛放肉酱的。《国语·周语》:“觞酒豆肉箪食。”具体形象可以参考我这个回答:

什么是假腹豆? – 信古斋的回答

敦(音dui 四声):为盛放黍稷之器。这个器型产生比较晚,最早见于春秋中期。

造型特别好记。器盖与器身扣在一起成一个球,盖子上也有足,揭开后放置在桌上也可以单独作为餐具使用。

簋(音gui 三声):盛黍稷之器。簋与鼎一样,属于先秦时期最重要的礼器之一。祭祀时用鼎用簋的数量直接与贵族的等级挂钩。天子用九鼎八簋,诸侯七鼎六簋,大夫五鼎四簋,元士三鼎一簋。

簠(音fu 三声):与簋一样,为盛黍稷之器。二者的区别在于造型。《说文·竹部》:“簋,黍稷方器也。”“簠,黍稷圜器也。”认为方形的为簋,圆形的为簠;按照出土器物的自铭,许慎正好将这二者给弄反了。

河南省博陈列的鼎簋组合 七鼎六簋

“虢季”青铜簋

象首纹青铜簠

圆为簋,方为簠。还是很好记的。

盨(音xu 一声):这个器型是从簋变化来的,流行时间很短。

河南省博 “虢季”青铜盨 它很像一个簋簠的复合体,方形圆角,盖上有四足,翻过来可以作为食器使用。

豆、敦、簋、簠、盨这几样属于彝器中较常见的食器,用来装已经蒸煮好了的食物。除了用来装调味品肉酱的豆,其他几种装黍稷的大多有盖,这是由于黍稷尚温的缘故。

以上这些都是用来做饭和吃饭的。看着很多,其实很好记。做饭的就三样,鼎、鬲是用来煮食物的,甗是用来蒸食物的。装食物的有五样,其中最重要的是簋,这几样器型都很有特点,结合实物很容易记住。

2、酒器

有尊、罍、壶、卣、觥、盉、爵、觚、觯、角、斝等。《礼记》之中凡是盛酒之器皆曰尊,饮酒之器皆曰爵。实际情况很复杂,有不少酒器的功能争议很大。其中尊、罍、壶、卣为盛酒之器,觥有盛酒、饮酒两种功能,爵、觚、觯、角、斝为饮酒之器,盉为调酒之器。

尊:最重要的盛酒器之一。古人讲青铜器,概称“尊彝”;尊在汉语中表示的是尊重、尊崇之意,可见古人对尊的重视。尊作为一种酒器,其形圆,硕腹侈口。还有很多动物形的青铜酒器,也一概称之为尊。

商代 龙虎尊

鼎鼎大名的四羊方尊

商代 象尊 其实这种动物形的酒器具体该不该叫尊,是有待商榷的。

这几张图片的来源:

青铜系列之 尊

北京一位藏家老师的公众号文章,推荐

罍(音lei 三声):《诗经》中常常提到的酒器。“我姑酌彼金罍,维以不永怀。”金罍即青铜罍。罍的容量一般来说要大于尊。

我们四川出的神器 象首耳兽面纹铜罍 西周彭州竹瓦寺窖藏所出,一共出了俩,一在国博一在川博

壶:这个从字面上就很好理解了。脖子细,肚子大而垂倾的一般叫做壶。

西周中期 十三年兴壶

太原金胜村赵卿墓出土方壶

卣(音you 三声):卣之制如壶,差小而有提梁,俗谓之提梁卣。

西周神器 伯格卣 好像是在陕博

尊、罍、壶、卣,这四种属于较常见的盛酒之器。

觥(音gong 一声):王国维谓觥兼饮酒盛酒之用,而马衡认为饮酒之觥与盛酒之觥并非同一种器物。《诗经·卷耳》有“我姑酌彼兕觥”一句,酌谓以勺取之,故知其为盛酒之觥;而《诗经·七月》中有“称彼兕觥”之句,“称”犹“举”也,所以说称彼兕觥,与举爵扬觯同意,都是执器直接饮酒。所以觥有盛酒、饮酒两种用途。

弗利尔美术馆藏 商代 青铜兕觥

上面那个太凶猛 这个是南博所藏的萌萌的小觥

觥的造型很类似于一个带盖的铜匜,这二者容易搞混,一会后面写水器的时候再说。

爵:这也是大家所熟知的一种酒器。二柱三足一鋬耳,前有流,后有尾。铭文一般在鋬内,也有在柱上的。

西周青铜爵 铭文在鋬内

燕侯旨爵 铭文“旨作”,在柱上

角:和爵造型差不多,但没有柱,前后皆是尖角,所见大部分都是带盖的。

10年佳士得拍卖的 带盖青铜角 整体和爵差不多,就是没有俩突起的铜柱,前后都是尖角

斝(音jia 三声):青铜斝和爵也很像,三足两柱一鋬耳,区别就是口部没有流,整体是呈圆形的。

盘龙城出土 二里头青铜斝

觚(音gu 一声):也是一种酒器(酒器是主流意见,亦有学者认为是单纯的祭祀礼器),很像我们今天的花瓶,宋代以后很多收藏家都用觚来插花,所以也叫花觚。其实这个名称是宋代金石学家给定的,目前出土的青铜觚没有一件是自铭为觚的,反而有一件自铭为“同”。所以此器型名称还有待商榷。

故宫博物院藏 商代青铜觚 确实很适合拿来插花

觯(音zhi 四声):觯是一种很典型的饮酒用器。一般尺寸都不大,双手持之饮酒。《礼记·礼器》云:“

尊者举觯,卑者举角。”

商代 鸮纹铜觯

鸮即猫头鹰,商人所崇拜的战神

爵、角、斝、觚、觯,一般认为属于青铜器中较常见的饮酒之器。

盉(音he 二声):这个其实说不清楚能不能算在酒器里边,大部分学者认为它是用来调和酒味的。《说文·皿部》云:“盉,调味也。”清代金石学家端方在陕西得一新出土的铜禁,其上陈列一尊、二卣、一爵、一觚、四觯、一角、一斝、一盉,前者全是酒器,而盉与这些酒器同列,故结合《说文》的记载推测为调和酒味的用具。

陕博所藏鸟盖青铜盉

霸国墓地出土 青铜盉

以上这些都是用来盛酒和饮酒的。看着很复杂,实际上也很复杂。尊、罍、壶、卣,是用来装酒的;爵、觚、觯、角、斝,是直接用来饮酒的;觥既可盛酒,也可饮酒;盉是用来调味的。

3、水器

有盘、匜、钅和、鑑等。水器很简单,盘、匜、钅和这三样通常同组所出,属于沃盥之礼所用的礼器;鑑则是用来装水或装冰的大铜盆。

之前看到山西那边一古玩商发的图,刚出的一整套青铜水器。

盘、匜(音yi 二声):古代在祭祀和燕飨之前,都要先行沃盥之礼,即参与祭祀和宴会的贵族用专门的礼器来洗手。古人洗手还是非常讲究的,用青铜匜装水,泻水于手,底下用青铜盘承接用过的污水。盘一般有双耳三足(或圈足),匜则与上边发过的觥非常相似,只是没有盖。

工艺非常复杂的青铜盘匜

西周 青铜盘

西周 青铜匜

钅和(音he 二声):这个字电脑打不出来,左边一个钅,右边一个和。这种器具宋代人定名为舟,但根据近年新出土的考古材料,应该定名为“钅和”。用途不明,古人多以为酒器,但由于多与盘匜同出,定为水器比较恰当。

我之前收藏的一只很漂亮的春秋散虺纹青铜钅和。。╮(╯_╰)╭ 。。可惜年前缺米,转给一武汉的藏家了

鑑(音jian 四声):《说文·金部》云:“鑑,大盆也。”想象一个装水的大盆子,就是它了。

上博 交龙纹大铜鑑

鑑也有用来装冰的。最著名的无如曾侯乙墓出土的冰鑑了。

曾侯乙墓出土 青铜冰鑑

再说乐器。乐有八音,金、石、丝、竹、匏、土、革、木,金居其首。传世青铜乐器种类其实不多,无非钟、鼓、錞于、铎、钲、句鑃、铙这几类。

钟:这个大家都非常熟悉的,青铜编钟。当然里边可以细分出编钟、甬钟、鎛钟、钮钟等好几种,差别都不大,非专业研究者不必去细分。

著名的曾侯乙编钟

鼓:鼓这个东西,一般是以革木制成,中原地区极少见有铜鼓传世。但是在先秦两汉时期,云南、贵州、广西这些属于滇人、越人等少数民族聚居之处却留存有大量铜鼓,其形制比较奇特。

广西藤县出土 汉代铜鼓

錞于(音chunyu 二声):《周礼·地官》载鼓人“以金錞和鼓”,錞于作为乐器,常常被用来与鼓相配合。《太平预览》引《乐书》云:“錞于者以铜为之,其形象铲,顶大,腹揳,口弇,上以伏兽为鼻,内悬子铃铜舌,凡作乐振而鸣之,与鼓相和。”今天考古发掘所见的錞于,其外形与古文记载相合,但内里大多不见铜舌。

战国 虎钮铜錞于

铎(音duo 二声)、钲(音zheng 一声)、句鑃(音gou 一声 diao 四声)、铙(音nao 二声):这四样形制都差不多,只是有大小长短的不同。形似编钟,但是是倒过来插在座子上敲击的(铎除外,铎是在孔内插木柄,里边有个小坠,手执摇动发声的)。

先秦 青铜铎

国博藏 战国 青铜钲

淹城博物馆藏 青铜句鑃一组

南博藏 兽面纹大铜铙

以上即为青铜器中较常见的几类乐器,除铜鼓多见于西南文化圈,钟、錞于、铎、钲、句鑃、铙这六件皆为主流文化圈内流行的打击乐器,且造型都差不多,很不容易分辨。

礼乐器大概就这些。总结一下,有鼎、鬲、甗、豆、敦、簋、簠、盨、尊、罍、壶、卣、觥、盉、爵、觚、觯、角、斝、盘、匜、钅和、鑑、钟、鼓、錞于、铎、钲、句鑃、铙。好像真的很多很难记的样子。。不过按照食器、酒器、水器、乐器这几个大的分类,平时再多去博物馆看看实物,没多久也就对古代青铜器很熟悉了。

二、兵器

《左传•成公十三年》:“国之大事,在祀与戎。”实际上,存世量最大的青铜器并非最受重视的宗庙彝器,而是用以杀伐的兵器。兵器按照使用方法,可分为句兵(即勾兵)、刺兵、短兵、射兵、斧钺、甲胄这几类。

1、句兵

有戈、戟。句兵,即勾兵,主要用于横击,钩杀敌人,在车战中非常有效。车上的武士只需横持勾兵,在两车相交之时即可借助车的冲力钩杀敌人。

戈:《考工记》曰:“戈广二寸,内倍之,胡三之,援四之。”早期戈无胡,晚期戈均由内、胡、援三部分组成。

戈的结构名称

早期的戈无胡

三年吕不韦戈 三年相邦吕不韦造寺工讋丞义工沱

刃口锋锐 杀人有如切菜

戟:戈装上矛刺即为戟。《说文》曰:“戈,平头戟也。戟,有枝兵也。”早期有戈、刺铸成一体的戟,晚期基本都是戈、刺分装的。

西周早期 侯戟

此器的出处没找到 应该不是中原王朝的兵器

晚期戟是这个样子的 就是一矛刺一戈组装到一起

还专门有一种组装的戟 一矛刺 多把戈 这样的叫多果戟 这又是曾侯乙墓出土的 为仪仗用戟 无益于实战

2、刺兵

有矛、铍、铩等。刺兵,顾名思义,为一种专门用于刺击的长兵器。

矛:与今天的矛区别不大,捅杀。

秦 寺工矛

吴王夫差矛

铍(音pi 一声):是一种将剑身加于长柄上的杀器。一般是护卫所用的兵器。《左传》中记载,专诸刺王僚时,“抽剑刺王,铍交于胸”,吴王僚的护卫即是手持长铍,挟持专诸上前献鱼。

战国 青铜铍 看着很像剑,与剑不同的是后边是装在长柄上的。

铩(音sha 一声):铍上加镡即为铩。

上面钩起的即是镡。可以用来格挡敌人的攻击

目前存世较多的是这种铜铁复合的铩。铩身用铁铸成,镡用青铜铸成。

3、短兵

有刀、剑、匕首等。短兵即武士随身所佩,手持格斗用的兵器。在车战占据主导地位的春秋时代以前并不常用,到了战国时期方才大兴。

刀:早期的刀和今天的刀并没什么两样。反而是汉代兴起的环首刀与今天的刀大不相同。

新干大洋洲出土 商代青铜刀

商代 管銎刀

汉代 青铜环首刀 刀身是直的

剑:也没啥可说的,大家都认识。

东周 青铜剑

匕首:更好理解了,就是短剑。

刺客专用

4、射兵

有弓弩、矢镞两种。其实就是一种,射远之兵曰矢,发矢之兵曰弩。弓弩主体结构为木质,出土时早已朽坏。好在兵马俑坑出土的铜车马上带有一具保存非常完好的铜弩模型。

此车名为戎立车,是皇帝出行车队中的护卫车。御者佩剑一把,弩一具。

秦始皇兵马俑里的铜马车为什么是四匹马? – 信古斋的回答

顺便安利一下之前答的一个铜车马的问题,这个答案没人看真是好忧伤。。

一般我们现在能够看到的弩机构件就是这样的了。木质的弩身完全朽坏,只余下铜质的扳机与铜匣组件。

5、斧钺

有斧、钺、斤、戚、斨等。其实都是一种东西,钺为大斧,斤为伐木斧,戚即为钺,斨为方銎斧。钺为先秦时期军事权力的象征,最早其实是斩首用的刑具。《史记·周本纪》载武王克商后,“以黄钺斩纣头”、“(对纣王的宠姬)斩以玄钺”,都是以钺砍头之意。

很魔性的商代亚丑钺

管銎斧,即《说文》所谓之斨

6、甲胄

铜质甲胄出土数量很少,比较常见的只有青铜胄(头盔)这一种。兵马俑坑出土过一套石质的铠甲,还有一面非常罕见的青铜彩绘盾牌。

战国时期 青铜胄

秦兵马俑坑出土 石质铠甲

兵马俑坑出土 彩绘青铜盾牌 也是铜车马上所配的防具

之前在成都本地所收的一副滇文化青铜臂铠。本来还有一副正面的身铠,可惜碎为十余块,卖家索值甚昂,就只买了这副臂铠。身臂分离,想想也真是可惜

兵器大概就是这些。比礼乐器要好记很多,冷兵器时代的杀戮之器总是没有什么太大改变的,也没有那么多生僻字。大概就是戈、戟、矛、铍、铩、刀、剑、匕、弓、弩、矢、斧、钺、斤、戚、斨、胄、盾这几样。

彝器与兵器是青铜器中的大类,公众普遍关注的也就是这两类。剩下的钱币、度量衡、符玺都没什么好讲的,就只有服御器非常麻烦。带钩、铜镜、车马器,尤其是车马器,各种小零件特多,字又特别难认,估计也没谁对这些东西感兴趣。。就这样吧。。 (´・ω・`) 大家看完了不要忘记点个赞啊。毕竟码字配图还是很辛苦的。。

  •  

460. 公版文档

家里两把宜家的椅子,被小家伙们摇得快散架了。这倒不是说宜家不耐用,从螺丝结构来说,宜家的东西相当能打,退一步来说,在性价比和轻便性的前提下,是不二选择。

之所以敢这样说,因为快十年过去,当年的宜家风之下选择的一些日用家具,依旧坚挺。所以每每看到,宜家是欧美初入职场,工资不高情况下的过渡选择,都觉得是友商手笔。是骡子是马,拉出来遛遛,无论是板材还是五金件,亦或是组装出来的样式,都吊打一众异议者。

说实话,摇起来的话,不管是螺丝结构,还是传统的卯隼,四边形样式总是经不起折腾,光从这一点上质疑宜家,本身也是说不过去的。

有点扯远了,说回两把椅子,前段不知为什么,大概是帮孩子做模型的缘故,视频号突然推荐了两款油性粘料,号称吊打502。有点心动,很想把椅子拆开,重新上胶再装一遍,看看是否能救回来。

等胶到手,才发现面对一堆组件组成的椅子,不知从何开始安装。当然可以自己琢磨,也许更有乐趣,不过呢,应该不属于实用的中年人。用关键词宜家椅子搜了一遍,排名靠前的是几家付费文档网站。

不以为然,也不想付这笔钱。此前有过找打印机说明书的经验 — 直奔官网。

先拍图搜图,找出型号,官网查询后说明众多,一时没有看到有文档。于是客服聊了下,感觉是机器人,但很明白地解决了问题,就在商品详情页的下方,详细pdf供下载浏览。

先不说是否侵权,单就任何文档均有价,就已经失却互联网的初衷了。

  •  

书法里的文化

笔、墨、纸、砚,自古以来便是文人生活不可或缺的一部分。而恰恰由于不可或缺,书法便承载着中国文化最厚重的底色。

小时初学书法之时,总是把字写得很粗,每一幅“作品”都不像是字,而是一幅幅油墨画。稍长一点,我便开始明白:字写的好不在于多么粗、多么清晰明显,它代表的是一种视觉的美感。我开始将一笔一划都写得粗细分明,而不是统一的厚重。那样的作品和我之前的“画”比起来,顺眼多了;但我却一直想不通,看起来更加舒展、有神的原因。

直到现在,我仿佛明白了一部分。做人或许就要这样,不能所有事情都浓墨重彩、大张旗鼓,我们平日更多的是默默的付出。平凡地做事,有时反而更能创造出价值。

在学习书法的过程中,我曾欣赏过许多书法作品:写得唯美,但总是感觉欠缺了点什么。直到我看到了王羲之的《兰亭集序》,我才真正为那精湛的技艺所折服;与此同时,我也发现了它与那些“欠缺”的作品的差别之处。那些“欠缺”的作品,每个字的转折之处多为圆滑,棱角少之又少,这让人觉得字缺乏“精神”;而《兰亭集序》则不同,王羲之的字并不失圆滑,但是他能在适当的地方采取适当的棱角,使字变得鲜活生动。而这些棱角的存在,使每个字都有了“精神”,进而使一幅作品凝聚着一股劲。它不缺乏柔和,但也不失刚烈,柔中带刚。

反观现代社会,这种既和气、不乏柔和,但又有着一种骨气、一种精神的人越来越少了。大多数人要么是极度圆滑,毫无底线;要么是极度自信,刚愎自用。他们并没有把握好柔与刚的相对平衡。我们不应该缺柔,也不应该少刚。柔中带刚,是我们应有的样子。虽然我们不知道王羲之的具体性格,但我从他的字中能看出,他便是那样柔中带刚的人。

在一幅书法作品中,留白一般会比黑字的部分要多。这便说明,一幅真正美的作品,它不一定满篇皆是黑色的笔墨,而更多的却应该是那片缄默的白色。我想这种现象恰恰表明了一种无为的美;那是我们中国传统的道学带来的影响,体现了古人较高的思想境界。

书法

或许传习书法的人越来越少了,但我认为它值得我们去学习、传承与发扬。因为,书法不仅仅是一种美感的外在体现,它已经成为了中华民族的一种为人处世的态度,一种思想智慧的结晶,一种植根于华夏的深刻而有力的文化。

  •  

GRASS斜坡单元分割插件(r.slopeunits) windows移植方案

✇遐说
作者Dorad
该插件由意大利@Massimiliano基于GRASS平台开发,用于快速划分斜坡单元。 原作者只适配了在Ubuntu下的操作,而本人尝试使用后发现并不好使,故想办法移植到Windows下使用。由于花费了不少心思摸索,故记录,方便下次查阅。
  •  

AI工具-效率提升神器-使用体验

✇遐说
作者Dorad
ChatGPT发布到现在,仅过去了四个月。而衍生出的各类产品、插件和竞品,井喷式地爆发着。 此文记录个人使用GPT相关产品的个人体验,以及一些个人使用建议。
  •  

论精神世界与现实世界

在这个信息时代,玩游戏并沉迷于游戏的人越来越多。有的人说他们不思进取,也有人说他们是精神贫瘠。这些说法都带有片面性,并且较为主观。

我以为本质上,游戏这个事物无罪。书籍,是一个有机的精神世界;而游戏则同样是另外一个精神世界:生活中其中的“网民”在这个世界中乐此不疲。所以说,这个世界的存在是没有错的。可既然游戏世界的存在没有错,为什么读书是值得肯定的,而玩游戏则是颓废、不思进取呢?

作为地球上的一个生命,我们的躯体是存在于现实世界中的,和动物一样要谋生。而这些过于日日夜夜玩游戏、依赖精神世界的人则常常忘记了现实世界的谋生手段,不知道去竞争,参加现实世界中的较量。这样的人不在现实世界中奋斗,便不能获得任何劳动成果,通俗的讲就是赚不到钱。这样一来,他们便无法在社会上生存下去,于是受到人们的斥责。

同样是精神世界,不同的精神世界的性质不同。许多人爱读书,“书”这个精神世界往往会对现实世界有一定的益处,能让我们更好地生存,因而人常言适度地看书是很好的;而游戏世界便不同了。游戏世界的一切都是虚构出来的,对现实世界没有任何辅助促进意义。所以说,同样是精神世界,读书很好,而玩游戏则成了玩物丧志,这是通过看精神世界的事物能否应用于现实生活来评判的。但有一点是一致的:无论是哪种精神世界,都不能过度沉迷其中。毕竟沉迷于游戏的人叫网瘾,而沉迷于读书无法自拔的叫作书呆子!

而精神世界的存在本身也是有意义的。一个人如果天天忙于人情世故,一点理想和追求都没有,那他活着还有什么意义?就拿学生学习来举例,如果每天二十四小时不停地学,从不休息;只知道脚踏实地,不知道仰望星空、思考人生与理想,毫无精神世界可言,那久而久之岂不是要抑郁?

总而言之,我们应该客观地理解现实世界与精神世界的关系,不过分沉迷于精神世界,也不应总为现实世界的琐事而烦恼。同时,慎重地选取精神世界,尽量选取对现实生活有一定价值、意义的。将这些融会贯通,生活会轻松很多!

  •  

GitHub 自动翻译 GitHub Action

简介 github-translator 是我最近做的一个小工具。它是一款将非英文的 GitHub issue 和 GitHub discussion 自动翻译成英文的 GitHub Action。https://github.com/lizheming/github-translate-action 已启用该 Action,感兴趣的同学可以直接上仓库测试一下。 使用 使用其实很简单,在你的项目仓库中新建 .github/workflows/translate.yml 文件,并添加如下内容。它实现了当有 issue 或者 discussion 创建或者修改时会自动翻译并将翻译内容追加到原始的内容后面。 name:'translator'on:issues:types:[opened, edited]issue_comment:types:[created, edited]discussion:types:[created, edited]discussion_comment:types:[created, edited]jobs:translate:permissions:issues:writediscussions:writeruns-on:ubuntu-lateststeps:- uses:actions/checkout@v3- uses:lizheming/github-translate-actionenv:GITHUB_TOKEN:${{ secrets.GITHUB_TOKEN }}with:IS_MODIFY_TITLE:trueAPPEND_TRANSLATION:true起因 我一直在维护的评论系统 Waline 自身定位为国际化项目,所以一直都在思考如何为非中文的用户提供更多的资料。由于我们的项目中文用户还是占绝大多数,所以我并不想改变大部分用户的习惯强制大家使用英文在 GitHub issue/discussion 交流,于是就有了自动翻译的想法。 我找了相关的一些工具之后,最终 dromara/issues-translate-action 进入了我的视野。它基本上符合我的诉求,基于 GitHub Action 当有用户发布 issue 的时候就会执行翻译脚本并将翻译内容作为新评论发布出去。 但发布新评论是有提醒的,这个和我想要不打扰用户的初衷相悖。而且发布新评论上下文看起来会不太流畅,我更期望的是基于原始内容修改。于是乎就 Fork 过来准备增加一个配置项改造下。 结果在改造的过程中发现原作者的代码写的比较乱,所有的逻辑都在一个文件里写的过程式代码。强迫症的我就将其进行重构,对代码进行简单的函数拆分。 在阅读代码的过程中发现它使用的是作者自荐的一个账号作为机器人发布评论。如果使用者想要用自己项目的账号的话就需要自己去新建个第三方账号,比较麻烦。之前我一直都知道 GitHub Action 会注入一个机器人的自动令牌来方便我们对 GitHub 进行操作,所以我尝试简化了第三方机器人令牌的操作,直接使用 GitHub Action 令牌让流程变得非常简单。 除了 GitHub issue,GitHub discussion 也会有很多用户的内容产生,GitHub Action 本身是支持 discussion 的相关事件触发的。 不过官方却默认没有提供 GitHub discussion 的 RESTful 操作接口,仅提供了 GraphQL 的操作 API。之前一直没有尝试过 GraphQL,而 GitHub Action 又不太支持本地调试,试错的成本比较高。好在经过一段时间的摸索后总算是搞定了。 使用 GraphQL 的过程中踩了一个坑,在修改 discussion 的接口中需要提交 discussion_id。按照之前 RESTful 接口的操作经验,我惯性的认为这个 discussion_id 就是我们 github discussion url 上的 id。结果执行给我报了个错: Error: Request failed due to following response errors: - Could not resolve to a node with the global id of '8' 由于根本没想到这个 id 的值有问题,一直认为是哪里的权限或者流程有问题查了半天。最后在《Automate your process with GitHub》的一个举例启发下才想着是不是这个 id 有问题,看了下数据发现有一个 node_id 才恍然大悟。后面就一马平川了,当接口调通的那一刻还是非常开心的! 将翻译内容追加到内容里的话,有一个点不好解决,当用户再次编辑内容的时候,我如何知道哪些是用户的原始内容,哪些是机翻的内容。这里我取了个巧,在内容中插入了一段固定文案的 HTML 注释。这段注释作为分隔符分隔原始内容和翻译内容,同时在注释中做好说明,让用户不要修改。这样就解决了再编辑的翻译问题。 原始内容 <!--This is a translation content dividing line, the content below is generated by machine, please do not modify the content below--> 翻译内容 于是乎「github-translator」这个项目就诞生了!
  •  

生命不可辜负

人生天地之间,若白驹过隙,忽然而已。

有段时间,我彷徨,怅惘。我不知道我该做什么,不知道我活着的意义所在。

深秋。满目的金黄之下掩不住接下来的衰败灭亡,草已枯,木将折。眼见着枫叶在风的摧残下一片一片地落下,我于心不忍却无可奈何。坐在路边,这车水马龙、熙熙攘攘的城市之中,我低下头来,百感交集。

我这样的人,以后有什么前途?我能干出什么大事业?

似乎,我认为我就是别人所说的“小镇做题家”——除了做题,别的什么都不会。在历史的长河中,我只是沧海一粟,是再平凡不过的生命。我不相信我能做出什么创举——就是做出了贡献,与前人们相比也不值一提。

风愈来愈烈,叶子一下子席卷走了不少。我的心情更是跌到了最低谷:绝望、无力、不知所措……“这难道就是我的人生吗?”

不知何时,一片树叶落在我头上,我试图把它弄下来,可它就如一只牢牢的钉子,仿佛钉在我头上似的,死活不愿下来。我竟使出了好大的力气,才使它落下。“真是执着!”我想着。

忽然之间,我竟从树叶身上得到些许启发。它作为一枚叶子,竟然尽它微薄之力坚持与大风作斗争,不屈不挠,附在我头上都不愿落地。尽管在这历史长流中,他只是四季更替之子,世间轮回之果,可它却仍然尽自己的微薄之力换取一线生机。

既然给予了我们生活,我们便要珍惜;既然给予了我们生命,我们就要让这个生命有价值、有意义。

我尝试着摆脱“生活自理奇差”诸如此类的标签,每天坚持做些家务。曾经连晾衣服都做不好的我,短短几个月竟然连做饭都可以从容自如。

生活技能提高了,我仿佛对未来有了信心。即使我如此渺小,如此不值一提,但我要尽我所能,做满天星辰中那颗平凡而耀眼的星!

周遭太暗,人生从无坦途;生命不可辜负,不负时代、不负韶华,做自己的那束光!

建筑

  •  

自然

静静地,品味自然。

溪涧中,传来阵阵鸟鸣;露水点缀枝头,蝉儿芽尖鸣叫。天空之中,云彩的身躯如仙人一般如真似幻,变化多端;搭配着那洁净的云彩,白鹤显得缥缈无比,看的见却似乎摸不着,就如那“所谓伊人”一般,富有“在水一方”的距离感。水中的鱼儿漫无目的地游行,悠然自由、不受拘束。那天空之碧蓝与溪水之青绿交相辉映,皆是曼妙而澄澈,仿佛是从未被墨水沾染似的。

溪水的尽头,出现了一片小潭。这潭水并不很深,一眼见底;甚至能看见潭中的石头,在阳光的照射下熠熠发光。纯天然石头的表面并不很光滑,但却能发出如此耀眼之光亮,这是我没有想到的;或许这是自然的伟力造就的吧。

自然,美妙绝伦。

在林子之中,总有几棵小苗子,长得茁壮;也总有几棵奄奄一息的老树,可怜得只剩树干。自然之中生老病死无时无刻不在发生着,这是常态。一朵花折了,一束草又获得了新生;一只老鸟咽了气,一只雏燕却展翅高飞……这些生命都是短暂的、渺小的,在自然面前是微不足道的;但正是这些短暂生命的存活与轮回交替,造就了这永恒的、生机勃勃的自然。

有人曾经希望长生不老,这无疑是行不通的。我们人便来自于。自然,最终注定要归于自然;这是自然轮回的一部分。而长生不老的想法与自然轮回之规律相悖,那么必定不会成功。作为短暂的生命,作为自然轮回的一部分,冷静地看待生存与死亡,是非常有必要的。

自然,轮回不止。

现代的我们,常生活在城市中——这喧嚣的地方使我们变得浮躁无比。人们的生活节奏、生活质量等方面都发生了或多或少的改变,但改变最大的却是人们的心。21世纪的人们不断追求名利、追求钱财,在追求世俗的过程中却迷失了自己。而走进自然,便是帮助你回归本心的最合适的方法。到自然中去看看吧,呼吸一口新鲜空气,听着鸟鸣、看着树木芳草,你可以忘掉世俗的一切。到那时,或许便会真切地体会到“鸢飞戾天者,望峰息心;经纶世务者,窥谷忘反”的真正含义了。

自然,回归本心。

自然,那样美妙,那样纯净,那样哲思,那样自然。

  •  

断点调试之压缩造成的血案

前段时间组里的小伙伴让我帮忙排查一个线上问题,我觉得排查流程比较有意思,想着记录一下看看是否能对其它同学有所帮助,遂有此文。 事情的起因是前几天线上突然收到一个报警,错误内容是 TypeError: C.fn is not a function。相关同学尝试排查无果后又回滚了最近上线的变更也没有排查到问题。虽然最终确认了复现路径,但是在本地却无法复现。 🔍 初步排查 在线上复现该错误后,点击错误堆栈的文件跳转,快速定位到线上出错的代码。由于线上都是压缩过的代码,这里我们可以点击左下角的 {} 进行代码美化。 经过美化后我们可以看出来,应该就是 189624 行出了问题。我们直接尝试在这一行上打断点,之后会发现代码会在这块疯狂打转。这是因为它处于一个 for 循环中。仔细观察不难看出代码其实上是 this.head 这个链的递归执行,每次执行完当前 C 都会被赋值成链的下一个值,并执行该值对应的 fn() 方法。也就是问题是这个链上的某个值没有 fn() 方法,最终导致了这个报错。 大概确认问题后,我们需要看一下最终这个 C 的值是什么。由于处在循环当中,一次一次的点击下一步实在是麻烦。由于我们有明确的目标,所以我们可以尝试添加条件断点,让只有符合我们条件的断点才停下来,否则都忽略正常执行。 在 189624 行右键点击 Add conditional breakpoint… 选项,并输入 typeof C.fn !== 'function' 作为条件表达式。这样我们就实现了一个仅在 C.fn 不是一个方法的时候才会触发的条件断点。 条件断点触发后,我们可以在控制台中基于断点时的上下文输出变量进行调试。可以从下左图我们可以清晰的看到,此时的 C.fn 的确是不存在的。 由于刚才我们已知 this.head 应该是一条链,依次执行链上的方法。所以理论上来说链上的每个元素都是一样的。于是乎我就尝试输出了 this.head 链上所有的元素想看一下这个链到底是什么样子的。模拟代码里的循环我也在控制台尝试写了下,发现输出的结果如下左图展示。在链的最后一个元素就是我们有问题的元素。 而之前我们已知的是在本地开发环境是无法复现这个问题的,所以我照猫画虎在本地同样的位置也输出了一下 this.head 链,结果见上右图。发现和线上输出的,除了最后这个有问题的元素,其它的输出基本是一样的。 看来问题的原因就在于线上的代码执行在链上增加了这么一个玩意导致的,而本地由于没有这个多余的元素所以没有触发问题。 🐞 确认问题 找到原因后我就想着从代码层面捋一下是哪里给增加了这么个玩意。由于之前的代码中可以明显的看到 i.prototype.finish 的字样,初步猜测这应该是一个类的定义。于是乎就想看看这个类是在哪里实例化执行的。 通过刚报错时的压缩后的代码,我们可以看到报错的模块是”protobuf.js“这个模块。于是乎我在项目和依赖中查找是哪个模块依赖了它,最终查到了是我们内部使用的一个 IM 消息模块有用到。 之后在具体的依赖模块中搜索 .finish() 相关字样,查到了最终的调用在如下地方。serialize() 方法会调用 Request.encode() 方法,它返回一个 $Writer 基类的实例,而 $Writer 就是 protobuf.js 模块中的 Writer 基类。Request.encode() 方法实例化完 Writer 基类后会执行一系列的成员函数,执行完毕后会返回 Writer 实例,并调用它的 finish() 方法。 了解执行流程之后,我就顺着 Request.encode(req).finish() 这一句开始向上对 Request.encode() 方法进行断点(下左图)。如下图先尝试在末尾断点输出 o.head(o 是压缩后指向 Writer 实例的变量),发现此时已经存在异常链元素了(下右图)。 中间的代码稍微打了下断点发现也依旧如此。最终在头部断点处发现了端倪。尝试在开头增加断电之后,发现在 120274 行执行完毕之后 o.head 链上就已经存在了异常数据了。 那我们尝试翻看下代码看一下 o.create() 方法具体干了什么。从下图左我们可以看到 Writer.create() 本质其实就是 Writer 基类的实例化工厂方法。而下图中可以看到 Writer 的构造方法对一些成员属性赋了初值。其中关键的 this.head 的初值是一个 Op 基类的实例。下图右可以看到 Op 基类的构造方法中也是赋了一些初值。同时我们可以看到 function noop() {} 实际上就是一个空方法。也就是说 this.head 默认指向了一个空方法实例化的 Op 对象。 乍一看整个流程其实非常简单,本质上构造函数内都是一些简单的赋值操作,不会有什么问题。于是乎还是按照链路依次向上排查问题。因为上一趴我们排查到执行完 Writer.create() 工厂方法后就有问题了,所以这里我们需要对 Writer 的构造函数进行断点排查。 尝试如下图在构造方法末尾断点后,输出 this.head 链,发现此时已经有异常数据了。而这个时候不过只是做了初值的操作而已,这怎么就能出问题了呢?由于断点情况下我能在当前上下文中进行调试,所以此时我尝试自己执行一下 Op 基类的实例化操作(见下图)。这时候发现确实它的 next 属性不对,是我们要找的问题元素! 此时此刻,我感觉我们已经越来越接近真相了! 如下图左我们在 f 变量上 hover 一会儿,会出现它的定义处链接,点击后会直接跳转到它的定义处下图右(其实就离的不太远)。 大家可能也都注意到了,我们刚才看的代码中 this.next 明明是定义成 undefined 怎么这里给定义成 g 了?而这个 g 又对上了 189456 行 g = s.base64,所以我们才看到 this.head.next 的值这么奇怪。而我们尝试看一下引用的 protobuf.js 代码,发现代码里 this.next 虽然是等于 g 但是它并没有关联到 u.base64 上。 由于我之前有解决过一些压缩再压缩后代码异常的 Case,所以至此我基本上可以断定,由于 protobuf.js 在我们的依赖中是引入的压缩后的代码,而压缩后的代码再走压缩导致了变量指向出现错乱从而导致的问题。这也侧面印证了为什么只有线上可以,本地无法复现的原因。因为本地是没有走压缩的。 🛠 如何解决 找到问题后有两种解决方法。一是正向的去查找压缩工具造成这个问题的原因;二是反向的去规避该问题,我们不引入压缩后的代码而是正常引入未压缩的代码,最终统一由项目进行压缩处理。 这两种方法都能解决问题。而第一种需要的时间会比较久,所以我们先采用了第二种方法临时解决一下。由于该依赖包不是我们维护的,我们只能使用 patch-package 给模块打补丁的方式进行修复。它的功能是在安装完依赖后会根据我们的 diff 文件对依赖进行修改。 这里我们的修改比较简单,找到我们依赖模块引入 protobuf.min.js 的地方,将其修改成 protobuf.js 即可。 🗒 后记 undefined 在压缩后就变成了 g 这个初步猜想应该是本地想要定义一个没有定义的变量,这样就是 undefined 了。我尝试克隆了下 protobuf.js 仓库进行了尝试,发现应该是 UglifyJS 中配置了 marguel.eval 导致有这个特性。 以上就是压缩造成的血案完整的排查经过,整个的过程总结一下有以下几个经验可以供大家参考: 除了单步断点,我们还有条件断点、日志断点等多种断点方式帮助我们排查问题,合理使用会加速我们排查问题的速度。 断点后当前 JS 环境会停留在当时的上下文中,我们可以在控制台执行、输出我们想要的当时环境的数据帮助排查。 控制台中我们也可以 hover 查看定义位置,进行定义间快速跳转。 压缩后的代码不可怕,我们可以通过源码对比,无法压缩的关键字进行定位查找。 只要是可以复现的问题,那都不是问题! 最后祝大家开工大吉,新的一年没有 Bug!
  •  

你不知道的前端新特性

有些你不知道是正常的……因为他们基本都没怎么被浏览器实现 🥶 CSS Toggles https://tabatkins.github.io/css-toggle/ 纯 CSS 实现状态切换一般使用 Checkbox 或者 Radio 配合选择器来实现。实现起来麻烦不说,而且 CheckBox/Radio 的位置限制了你可控制的范围,用起来很不方便。 iframe { width: 100%; height: 300px; border: 1px solid #EFEFEF; } 交互中越来越多依赖状态,比如 Tab, 弹窗, Summary 等,所以有了原生的状态切换草案。目前还是草案中,不过已经有对应的 Polyfill 了 https://github.com/oddbird/css-toggles toggle-root:定义该元素可切换状态 <toggle-root> = <custom-ident> [ <toggle-states> [at <toggle-value>]? || <toggle-overflow> || group || self ]? <toggle-states> = <integer [1,∞]> | '[' <custom-ident>{2,} ']' <toggle-value> = <integer [0,∞]> | <custom-ident> <toggle-overflow> = cycle | cycle-on | sticky // mode // mode 1 at 0 cycle wide // mode 3 at 0 // mode [light dark] at light toggle-overflow 定义设置的值超出之后的行为,针对数字类型有效 cycle / cycle-on: 比最小值小则为最大值,比最大值大则为 0 / 1 sticky: 最小值 <= value <= 最大值 self 定义触发元素和可切换元素的查找关系: wide: 任意 narrow: 必须是父子关系 toggle-rigger:定义该元素为 的切换触发器 <toggle-trigger> = <custom-ident> <trigger-action>? <trigger-action> = [prev | next] <integer [1,∞]>? | set <toggle-value> // mode // mode next 1 // mode prev 1 // mode next 2 // mode 2 // mode set light :toggle():根据 值选择元素 toggle:当可切换元素和切换触发器元素为同一个时,可使用 toggle 进行简写 toggle-group:指定该元素为 的组内元素 我们可以通过 Element.toggles() 来获取可切换元素的所有切换状态枚举,也通过 Element.addEventListener('togglechange', ...) 获取当前可切换元素的当前状态。 更多示例见:https://toggles.oddbird.net PopUp API https://open-ui.org/components/popup.research.explainer 如果要自己从 0 开始写一个弹窗是比较麻烦的,需要考虑很多事情:全屏浮层,内容居中,页面滚动失效,遮罩点击关闭,ESC 按下关闭… 所以就有好多组件封装,虽然原生已经有 <dialog> 标签可以干类似的事情了。但毕竟还是有点原始,所以 Chrome 就将 PopUp 原生实现了~~(真卷啊)~~。 popup: auto | hint | manual 默认为 auto,指定多弹窗的关系 popuptoggletarget:指向带有 popup 属性的元素 id,用来切换 popup 元素的显隐 popupshowtarget:指向带有 popup 属性的元素 id,用来显示 popup 元素 popuphidetarget:指向带有 popup 属性的元素 id,用来隐藏 popup 元素 Element.showPopUp() Element.hidePopUp() 目前仅最新的 Chromium 生效 :-) 还有些问题~ https://chromestatus.com/feature/5463833265045504 CSS 作用域 https://www.w3.org/TR/css-scoping-1/ 以往我们要实现 CSS 作用域,组件之间样式不互相影响,一般就是 BEM 命名或者 CSS Module, CSS in JS 之流最终生成带 hash 的唯一选择器伪实现,亦或是使用 Shadow DOM 这种高成本的完美实现。 现在我们能直接使用 @``scope 来实现样式隔离了! 图来自:https://weibo.com/1708684567/LzHEY1wGm 不用太多介绍,简单好用~ structuredClone https://developer.mozilla.org/en-US/docs/Web/API/structuredClone 原生的深拷贝方法,可以对结构化数据进行深拷贝,避免 JSON.parse(JSON.stringify()) 的尴尬。 structuredClone(value: any, { transfer?: any[] }) 不允许克隆Error、Function和DOM对象,如果对象中含有,将抛出DATA_CLONE_ERR异常。 不保留RegExp 对象的 lastIndex 字段。 不保留属性描述符,setters 以及 getters(以及其他类似元数据的功能)。例如,如果一个对象用属性描述符标记为 read-only,它将会被复制为 read-write。 不保留原型链。 Navigation API SPA 的基石路由管理,早有 History API 支持,但因为本质是历史记录的管理,缺少一些切换后的控制等功能,所以 Chrome 从新提出了 Navigation API 来专门实现路由管理。 navigation.addEventListener('navigate', navigateEvent => { if (shouldNotIntercept(navigateEvent)) return; const url = new URL(navigateEvent.destination.url); if (url.pathname === '/') { navigateEvent.intercept({handler: loadIndexPage}); } else if (url.pathname === '/cats/') { navigateEvent.intercept({handler: loadCatsPage}); } }); navigateEvent包含以下信息: canIntercept 是否支持拦截,跨域等无法拦截场景会返回 false destination.url 跳转目标地址 hashChange是否是锚点跳转 userInitiated 是否是由页面内 <a> 标签触发的跳转,为 false 表示是浏览器前进后退等触发的跳转 downloadRequest是否是由具有download属性的链接带来的跳转 formData表单跳转时对应提交的表单数据,可以针对 Form 表单拦截后发送数据 navigationType枚举值"reload", "push","replace"或"traverse"(类似 history.goBack())之一。如果是"traverse",则无法通过preventDefault()阻止跳转 signal 提供给拦截 handler 中异步请求使用,方便当跳转终端后同步中断请求 scroll()控制跳转后滚动,在异步 handler 中比较有用,可能会多次滚动 function shouldNotIntercept(navigationEvent) { return ( !navigationEvent.canIntercept || navigationEvent.hashChange || navigationEvent.downloadRequest || navigationEvent.formData ); } signal 和 scroll() 的例子: navigation.addEventListener('navigate', navigateEvent => { if (shouldNotIntercept(navigateEvent)) return; const url = new URL(navigateEvent.destination.url); if (url.pathname.startsWith('/articles/')) { navigateEvent.intercept({ async handler() { // The URL has already changed, so quickly show a placeholder. renderArticlePagePlaceholder(); // Then fetch the real data. const articleContentURL = new URL( '/get-article-content', location.href ); articleContentURL.searchParams.set('path', url.pathname); const response = await fetch(articleContentURL, { signal: navigateEvent.signal, }); const articleContent = await response.json(); renderArticlePage(articleContent); navigateEvent.scroll(); const secondaryContent = await getSecondaryContent(url.pathname); addSecondaryContent(secondaryContent); }, }); } }); // navigate const { committed, finished } = navigation.navigate('/articles/hello-world'); 设置好跳转拦截回调后,我们就能正常使用 navigation.navigate() 进行跳转了。返回两个 Promise 对象,分别对应 Navigate 完成的状态 committed,以及导航拦截的回调 Handler 结束的状态 finished。 比起 History API 惊喜的是,我们可以通过 navigation.entries() 获取当前所有的历史记录。通过 navigation.currentEntry 返回当前的记录。 navigation.entries() 获取的只能是同域的历史记录,跨域的无法获取。 兼容性:https://caniuse.com/mdn-api_navigation_navigate URLPattern https://developer.mozilla.org/en-US/docs/Web/API/URL_Pattern_API 可以算是原生版的 path-to-regexp ,用来做 URL 格式匹配解析的。最开始是因为 service worker 场景有解析拦截的资源请求地址的需求创造,但适用所有 URL 解析场景。 虽然 URLPattern 可以解析完整的域名,但一般 hostname 相关可以用 URL 解析,query 可以使用 URLSearchParams 解析。所以其实用的比较多的场景还是 pathname 的解析。 const pattern = new URLPattern({ pathname: '/books/:id(\\d+)' }); console.log(pattern.test('https://example.com/books/123')); // true console.log(pattern.exec('https://example.com/books/123').pathname.groups); // { id: '123' } console.log(pattern.test('https://example.com/books/detail')); // false 使用 :<group> 来为当前匹配内容分组 {}是非捕获组,相当于正则中的 (?:),可以在后面增加{}?表示可选,不加的话其实可有可无 (正则表达式)也可以通过正则进行精确匹配,可以跟在命名分组的后面,相当于(?<group>正则表达式)。内部正则关键字需要做转义处理。 * 表示贪婪匹配,独立使用相当于正则 .*,也可以搭配在前几个规则后使用,例如 :id* + 相当于 {1,} 不可独立使用 使用 URLPattern 而不是自己使用正则解析的一个好处就是它会帮助我们把 URL 规范化之后再进行解析,而不是简单的做一个字符串的正则转换匹配。 目前仅 Chrome 系兼容性还行,Node 也暂时还没有跟上版本。不过有对应的 Polyfill 了已经。 图片 https://developer.mozilla.org/en-US/docs/Web/CSS/aspect-ratio 如果需要按比例显示图片,一般会保持比例设置 DOM 尺寸后使用 background-image 或者使用 <img> 来展示,比较不便。所以增加了 aspectio-ratio 属性直接支持设置图片显示的比例。 配合 object-fit 属性指定图片比例不对的时候填充模式,食用更加。 img { aspect-ratio: 16 / 9; object-fit: contain; } https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#attr-loading object-fit 的兄弟属性 object-position:用于指定图片的展示区域 https://developer.mozilla.org/en-US/docs/Web/CSS/object-position 为了性能优化我们一般都会为图片增加懒加载的支持,这个官方也做了原生的支持。 <img src="image.jpg" alt="..." loading="lazy"> https://developer.mozilla.org/en-US/docs/Learn/HTML/Multimedia_and_embedding/Responsive_images 为了更好的性能优化,我们一般会从图片尺寸和图片格式上对图片资源进行处理,此为响应式图片。 关于图片格式,静图有 jpg,png,webp,avif,jpeg XL 这些格式。动图有 gif,apng,webp,avif之这些格式。avif 是基于视频编码 AV1 衍生的图片格式。针对不同的浏览器适配不同的格式,我们可以使用 <picture> 进行图片渲染。 <picture> <source type="image/avif" srcset="....avif" /> <img src="....webp" loading="lazy" /> </picture> 除了格式之外,我们还可以按照分辨率来设置,图片尺寸也不在话下。综合如下: <style> img { width: 320px; aspect-ratio: 320 / 240; object-fit: contain; } </style> <picture> <source type="image/avif" srcset="...320x240.avif 1x ...640x480.avif 2x ...960x720.avif 3x" /> <img srcset="...320x240.webp 1x ...640x480.webp 2x ...960x720.webp 3x" src="....webp" loading="lazy" /> </picture> 参考资料: The Future of CSS: CSS Toggles 有哪些以往需要使用javascript实现的功能现在可以直接使用html或者css实现? - 知乎 Scope Proposal & Explainer Proposal for CSS @when | CSS-Tricks JS 深拷贝的原生终结者 structuredClone API - 掘金 https://developer.chrome.com/docs/web-platform/navigation-api/ URLPattern brings routing to the web platform https://web.dev/learn/design/picture-element/
  •  

清除 useEffect 副作用

在 React 组件中,我们会在 useEffect() 中执行方法,并返回一个函数用于清除它带来的副作用影响。以下是我们业务中的一个场景,该自定义 Hooks 用于每隔 2s 调用接口更新数据。 import { useState, useEffect } from 'react'; export function useFetchDataInterval(fetchData) { const [list, setList] = useState([]); useEffect(() => { const id = setInterval(async () => { const data = await fetchData(); setList(list => list.concat(data)); }, 2000); return () => clearInterval(id); }, [fetchData]); return list; } 🐚 问题 该方法的问题在于没有考虑到 fetchData() 方法的执行时间,如果它的执行时间超过 2s 的话,那就会造成轮询任务的堆积。而且后续也有需求把这个定时时间动态化,由服务端下发间隔时间,降低服务端压力。 所以这里我们可以考虑使用 setTimeout 来替换 setInterval。由于每次都是上一次请求完成之后再设置延迟时间,确保了他们不会堆积。以下是修改后的代码。 import { useState, useEffect } from 'react'; export function useFetchDataInterval(fetchData) { const [list, setList] = useState([]); useEffect(() => { let id; async function getList() { const data = await fetchData(); setList(list => list.concat(data)); id = setTimeout(getList, 2000); } getList(); return () => clearTimeout(id); }, [fetchData]); return list; } 不过改成 setTimeout 之后会引来新的问题。由于下一次的 setTimeout 执行需要等待 fetchData() 完成之后才会执行。如果在 fetchData() 还没有结束的时候我们就卸载组件的话,此时 clearTimeout() 只能无意义的清除当前执行时的回调,fetchData() 后调用 getList() 创建的新的延迟回调还是会继续执行。 在线示例:CodeSandbox 可以看到在点击按钮隐藏组件之后,接口请求次数还是在继续增加着。那么要如何解决这个问题?以下提供了几种解决方案。 🌟如何解决 { async function getList() { id = setTimeout(async () = { const data = await fetchData(); setList(list = list.concat(data)); getList(); }, 2000); } getList(); return () = clearTimeout(id); }); return list; } ``` -- 🐋 Promise Effect 该问题的原因是 Promise 执行过程中,无法取消后续还没有定义的 setTimeout() 导致的。所以最开始想到的就是我们不应该直接对 timeoutID 进行记录,而是应该向上记录整个逻辑的 Promise 对象。当 Promise 执行完成之后我们再清除 timeout,保证我们每次都能确切的清除掉任务。 在线示例:CodeSandbox import { useState, useEffect } from 'react'; export function useFetchDataInterval(fetchData) { const [list, setList] = useState([]); useEffect(() => { let getListPromise; async function getList() { const data = await fetchData(); setList((list) => list.concat(data)); return setTimeout(() => { getListPromise = getList(); }, 2000); } getListPromise = getList(); return () => { getListPromise.then((id) => clearTimeout(id)); }; }, [fetchData]); return list; } 🐳 AbortController 上面的方案能比较好的解决问题,但是在组件卸载的时候 Promise 任务还在执行,会造成资源的浪费。其实我们换个思路想一下,Promise 异步请求对于组件来说应该也是副作用,也是需要”清除“的。只要清除了 Promise 任务,后续的流程自然不会执行,就不会有这个问题了。 清除 Promise 目前可以利用 AbortController 来实现,我们通过在卸载回调中执行 controller.abort() 方法,最终让代码走到 Reject 逻辑中,阻止了后续的代码执行。 在线示例:CodeSandbox import { useState, useEffect } from 'react'; function fetchDataWithAbort({ fetchData, signal }) { if (signal.aborted) { return Promise.reject("aborted"); } return new Promise((resolve, reject) => { fetchData().then(resolve, reject); signal.addEventListener("aborted", () => { reject("aborted"); }); }); } function useFetchDataInterval(fetchData) { const [list, setList] = useState([]); useEffect(() => { let id; const controller = new AbortController(); async function getList() { try { const data = await fetchDataWithAbort({ fetchData, signal: controller.signal }); setList(list => list.concat(data)); id = setTimeout(getList, 2000); } catch(e) { console.error(e); } } getList(); return () => { clearTimeout(id); controller.abort(); }; }, [fetchData]); return list; } 🐬 状态标记 上面一种方案,我们的本质是让异步请求抛错,中断了后续代码的执行。那是不是我设置一个标记变量,标记是非卸载状态才执行后续的逻辑也可以呢?所以该方案应运而生。 定义了一个 unmounted 变量,如果在卸载回调中标记其为 true。在异步任务后判断如果 unmounted === true 的话就不走后续的逻辑来实现类似的效果。 在线示例:CodeSandbox import { useState, useEffect } from 'react'; export function useFetchDataInterval(fetchData) { const [list, setList] = useState([]); useEffect(() => { let id; let unmounted; async function getList() { const data = await fetchData(); if(unmounted) { return; } setList(list => list.concat(data)); id = setTimeout(getList, 2000); } getList(); return () => { unmounted = true; clearTimeout(id); } }, [fetchData]); return list; } 🎃 后记 问题的本质是一个长时间的异步任务在过程中的时候组件卸载后如何清除后续的副作用。 这个其实不仅仅局限在本文的 Case 中,我们大家平常经常写的在 useEffect 中请求接口,返回后更新 State 的逻辑也会存在类似的问题。 只是由于在一个已卸载组件中 setState 并没有什么效果,在用户层面无感知。而且 React 会帮助我们识别该场景,如果已卸载组件再做 setState 操作的话,会有 Warning 提示。 再加上一般异步请求都比较快,所以大家也不会注意到这个问题。 所以大家还有什么其他的解决方法解决这个问题吗?欢迎评论留言~ 注: 题图来自《How To Call Web APIs with the useEffect Hook in React》
  •  

如何制作 Figma 插件

Figma 是一款专业的在线 UI 设计工具,它因为个人使用免费、在线跨平台、多人协作、蓬勃的 Figma Community 社区组织而广受欢迎。目前设计团队都在使用 Figma 进行 UI 设计交付。 能被 SaaS 化的终将被 SaaS 化 Figma 本身是 Web 服务,其客户端也是使用 Electron 进行的封装。所以它的插件系统是前端友好型,和日常前端开发没有什么太大的区别。 插件原理 Figma 的插件也采用了双线程的架构。UI 线程能获得完整 Web 的能力,但是无法直接操作 Figma;主线程则相反,可以通过 Figma API 对数据进行操作,但无完整的 Web 能力,仅有 JS 执行以及 Figma API 支持。两者通过 postMessage 进行通信。 根据官方文章描述,通过 WebAssembly 版的 QuickJS 来实现主线程的沙箱执行,UI 线程则是通过 iframe 执行。 采用这种方案的原因主要是既想保证代码隔离,但是又希望能方便的操作 Figma 数据。 根据官博文章描述,之前也曾有尝试使用 Web 原生的沙盒 API Realms 来实现主线程的沙盒执行,但因为该 API 的一些安全漏洞还是回退回了 QuickJS 的实现。 了解了插件的原理之后,下面我就以帮助设计师同学快速插入占位图的插件 Placeholder 为例,带大家一步一步的了解如何进行 Figma 插件开发。 需求整理 在进行插件开发之前,我们捋一捋我们需要实现的功能。http://placeimg.com/ 是一个专门用来生成占位图的网站,我们将利用该网站提供的服务制作一个生成指定大小的占位图并插入到 Sketch 画板中的功能。插件会提供一个面板,可以让使用者输入尺寸、分类等可选项,同时提供插入按钮,点击后会在画板插入一张图片图层。 项目结构 在 Figma 客户端中按照如上操作即可完成插件的初始化。除了默认的三个例子之外,官方也有一个示例插件的仓库,也可以参考。 https://github.com/figma/plugin-samples Figma 插件默认推荐使用 TypeScript 开发,官方提供了完善的 TypeScript 类型支持。以默认的带 UI 的模板为例,初始化后进入文件夹 npm i 安装依赖后执行 npm run build 编译完成后点击插件即可看到效果。 . ├── README.md ├── code.js ├── code.ts ├── manifest.json ├── package-lock.json ├── package.json ├── tsconfig.json └── ui.html manifest.json 可以看到整体的接口和大多数 JS 项目一样,其中 manifest.json 用来记录插件的信息。manifest.json 这个文件大家可以理解为是 Figma 插件的 package.json 文件。我们来看看默认生成的 manifest.json。 { "name": "figma-placeimg", "id": "1117699210834344763", "api": "1.0.0", "main": "code.js", "editorType": [ "figma" ], "ui": "ui.html" } 其中重点的是 main 和 ui 两个字段: main:指定插件的入口文件,该文件中的代码会运行在主进程中的沙箱里。 ui: 指定插件的 UI 代码文件,该文件中的代码会运行在 iframe 中。实际上,UI 代码文件的内容会作为字符串传递给 figma 内置变量 __html__,在沙箱内可以通过 figma.showUI(__html__) 创建 iframe。 这里注意到是将UI代码文件中的内容作为字符串注入到主线程中,类似 <iframe srcdoc="__html__" />。这就导致了我们无法直接引用插件中的其他资源,所有插件内依赖的资源都需要内嵌到最终的字符串中。 ui 字段也支持指定多个文件,当指定多个文件的时候会注入 __uiFiles__ 对象来映射文件。 manifest.json 中还支持通过 menu 字段定义插件的菜单。如果不想写 UI 也可以通过parameters指定支持的指令,直接通过输入指令来操作也是可以的。更多的配置可以查看官方文档 Plugin Manifest。 插件开发 一些基本原理了解清楚之后我们就可以进行插件的开发了。首先我们需要用户点击插件菜单之后打开一个面板,该面板可以配置尺寸、分类等基础信息。 <link rel="stylesheet" href="https://unpkg.com/figma-plugin-ds@1.0.1/dist/figma-plugin-ds.css"> <style> .content { display: flex; } .icon--swap { animation: rotate 1s linear infinite; } .hide { display: none; } @keyframes rotate { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } </style> <div id="app"> <div class="field"> <label for="" class="label">请输入图片尺寸:</label> <div class="content" style="padding-left: 10px;"> <div class="input"> <input type="input" class="input__field" placeholder="宽" name="width"> </div> <div class="label" style="flex:0;">×</div> <div class="input"> <input type="input" class="input__field" placeholder="高" name="height"> </div> </div> </div> <div class="field"> <label for="" class="label">请选择图片分类:</label> <div class="content"> <div class="radio"> <input id="radioButton1" type="radio" class="radio__button" value="any" name="category" checked> <label for="radioButton1" class="radio__label">全部</label> </div> <div class="radio"> <input id="radioButton2" type="radio" class="radio__button" value="animals" name="category" > <label for="radioButton2" class="radio__label">动物</label> </div> <div class="radio"> <input id="radioButton3" type="radio" class="radio__button" value="arch" name="category" > <label for="radioButton3" class="radio__label">建筑</label> </div> <div class="radio"> <input id="radioButton4" type="radio" class="radio__button" value="nature" name="category" > <label for="radioButton4" class="radio__label">自然</label> </div> <div class="radio"> <input id="radioButton5" type="radio" class="radio__button" value="people" name="category" > <label for="radioButton5" class="radio__label">人物</label> </div> <div class="radio"> <input id="radioButton6" type="radio" class="radio__button" value="tech" name="category" > <label for="radioButton6" class="radio__label">科技</label> </div> </div> </div> <div class="field"> <label for="" class="label">请选择图片滤镜:</label> <div class="content"> <div class="radio"> <input id="radioButton7" type="radio" class="radio__button" value="none" name="filter" checked> <label for="radioButton7" class="radio__label">正常</label> </div> <div class="radio"> <input id="radioButton8" type="radio" class="radio__button" value="grayscale" name="filter" > <label for="radioButton8" class="radio__label">黑白照</label> </div> <div class="radio"> <input id="radioButton9" type="radio" class="radio__button" value="sepia" name="filter" > <label for="radioButton9" class="radio__label">老照片</label> </div> </div> </div> <div class="field" style="padding:0 10px;"> <div id="create" class="icon-button" style="width: 100%;"> <div class="icon icon--image"></div> <div class="type type--small type--medium type--inverse">插入</div> </div> <div class="icon-button loading hide" style="width: 100%;"> <div class="icon icon--swap"></div> </div> </div> </div> 官方文档里有推荐 https://github.com/thomas-lowry/figma-plugin-ds 这个仓库,提供 Figma 的 UI 库组件,会让你的插件显的更加“原生”。 由于之前说过所有的资源都需要内嵌到 html 中,所以我使用了 CDN 地址的形式引入了样式文件。另外由于功能比较简单,这里也没有使用 React 等框架去进行开发。官方模板中有 React 模板可以参考 https://github.com/figma/plugin-samples/tree/master/webpack-react 获取图片 UI 完成之后接下来我们需要实现功能。我们需要将图片下载下来插入到 Figma 图层中。由于主线程没有网络能力,所以这部分工作需要在 UI 线程中完成,再通过 postMessage 传递回主线程中完成后续操作。具体的代码如下: <script> async function loadImage(url) { const resp = await fetch('http://localhost:3000/' + url); const buffer = await resp.arrayBuffer(); return new Uint8Array(buffer); } document.getElementById('create').onclick = async (e) => { const width = parseInt(document.querySelector('input[name="width"]').value); const height = parseInt(document.querySelector('input[name="height"]').value); const category = document.querySelector('input[name="category"]:checked').value; const filter = document.querySelector('input[name="filter"]:checked').value; const loading = document.querySelector('.icon-button.loading'); e.target.classList.add('hide'); loading.classList.remove('hide'); const imgBytes = await loadImage(`https://placeimg.com/${width}/${height}/${category}/${filter}`); parent.postMessage({ pluginMessage: { type: 'insert', bytes: imgBytes, width: width, height: height } }, '*'); loading.classList.add('hide'); e.target.classList.remove('hide'); } </script> 由于 UI 线程是一个纯 Web 环境,当我们使用 XMLHttpRequest 或者 fetch 发送请求的时候,肯定会碰到跨域的问题。按照文档 https://www.figma.com/plugin-docs/making-network-requests/ 提供的解决办法,我们只能依靠服务端加层代理来解决。 当你的插件没有 UI 面板的时候,如何进行网络请求?按照文档所说,我们是可以设置 figma.ui.show() 的第二个参数,将其设置成 visible: false 的形式创建 iframe 获取数据。 // code.ts function fetch(url, options) { const html = `<script> fetch(${url}, ${JSON.stringify(options)}).then(resp => resp.json()).then(resp => parent.sendMessage({ pluginMessage: { type: 'networkRequest', data: resp } }); </script>`; return new Promise(resolve => { figma.ui.on('message', msg => msg.type === 'networkRequest' && resolve(msg.data) ); figma.ui.show(html, { visible: false }); }); } 插入图片 由于只有主线程才能操作 Figma 数据,所以需要在 UI 线程 postMessage 传递数据到主线程中继续进行操作。 主线程中的步骤就比较简单了,使用 Figma API 创建好矩形并将图片填充即可完成图片的插入。 我们可以通过设置 figma.currentPage.selection 设置选中项,并使用 figma.viewport.scrollAndZoomIntoView 将刚插入的数据滚动到视野中。 figma.ui.onmessage = msg => { if (msg.type === 'insert') { const rectNode = figma.createRectangle(); const image = figma.createImage(msg.bytes); rectNode.name = 'Image'; rectNode.resize(msg.width, msg.height); rectNode.fills = [{ imageHash: image.hash, scaleMode: 'FILL', scalingFactor: 0.5, type: 'IMAGE' }]; figma.currentPage.appendChild(rectNode); figma.currentPage.selection = [rectNode]; figma.viewport.scrollAndZoomIntoView([rectNode]); } figma.closePlugin(); }; 除了需要显示的调用 figma.ui.show 来展示 UI 之外,在执行完插件后需要显示的调用 figma.closePlugin() 告知 Figma 进行关闭插件操作。 优化插件 上面我们实现了配置宽高然后插入一张图片。但有时候我们会先插入一个矩形占位,之后才会将其替换成图片。所以我们可以优化下操作步骤,当选中到一个矩形的时候,自动获取到它的尺寸,然后点击插入后会直接插入到该矩形中。 // code.ts function initSelectionState() { if (figma.currentPage.selection.length === 1 && figma.currentPage.selection[0].type === 'RECTANGLE') { const rectNode = figma.currentPage.selection[0]; figma.ui.postMessage({ type: 'update', width: rectNode.width, height: rectNode.height }); } } figma.on('selectionchange', initSelectionState); initSelectionState(); 通过在主线程中监听 selectionchange 事件,我们能实时获取到当前选中的元素。我们将尺寸信息发送到 UI 线程后让其填充到输入框中称为默认值。 window.onmessage = function(e) { if (e.data.pluginMessage.type === 'update') { document.querySelector('input[name="width"]').value = e.data.pluginMessage.width; document.querySelector('input[name="height"]').value = e.data.pluginMessage.height; } } 最后再插入的时候,我们也需要判断如果有选中矩形的话则优先使用选中的矩形,而不是新增矩形。 let rectNode: RectangleNode; if (figma.currentPage.selection.length === 1 && figma.currentPage.selection[0].type === 'RECTANGLE') { rectNode = figma.currentPage.selection[0]; } else { rectNode = figma.createRectangle(); } // const rectNode = figma.createRectangle(); 插件发布 最终我们的插件的主体功能就开发完毕了。下面我们就可以进行插件的发布了。我们可以直接通过插件管理中 Publish 操作进行发布。 和 Chrome 插件有点类似,Figma 插件支持发布到社区,也支持发布到组织。支持发布到多个组织中。发布到组织不需要审核,但只有该组织的同学和文件可使用。发布到社区的需要由 Figma 官方审核。 插件调试 由于是 Web 技术向,所以 Figma 的插件调试非常简单。直接 Command + Shift + I 打开控制台即可。 不过比较麻烦的是热更新的支持不太好。之前页面资源需要编译到 html 问价中的方式也不太友好。所以有人就想到了** iframe 套娃**来解决 UI 的更新问题。 简单来说就是通过在 UI 线程中再嵌套一个在线页面,UI 线程作为主线程和新的 iframe 的消息中转。这样相当于将插件在线化,回到了纯 Web 开发模式了,热更新自然就没有什么问题了。 不过这仅能解决 UI 线程的热更新问题,主线程如果有变化还需要重新更新插件解决。基于上面的方案,其实我们能做的更“绝”一点。我们可以将主线程变成一个壳,具体的业务代码由 iframe 下发,通过这种方式来解决主线程的更新问题。 // ui.html parent.postMessage({ pluginMessage: { type: 'MAIN_CODE', code: 'console.log(figma)' } }); // code.ts figma.ui.onmessage = (msg) => { msg.type === 'MAIN_CODE' && eval(msg.code); } 后记 通过示例讲述了如何开发一个 Figma 插件,包含了获取 Figma 数据信息,操作 Figma 文件等双向操作。基于以上简单操作我们可以完成更多有意义的事情帮助我们更好的开发。比如快速导出多尺寸图片、导出图标自动发布到 npm 等… 以上示例代码已发布到 GitHub 中,欢迎参考。 https://github.com/lizheming/figma-placeimg
  •  

豆瓣书影音同步 GitHub Action

2023-07-12 更新:《关于豆瓣图片无法直接使用的说明》 简介 doumark-action 是我前段时间造的一个轮子。它是一款 GitHub Action,支持在 GitHub 中同步你的豆瓣书影音数据到本地的文件或者 Notion 中。我利用它,定时同步我的豆瓣观影数据到我的博客仓库中,并利用 Hugo 读取文件数据渲染成页面,观影 是最终的效果。 使用 使用其实很简单,在你的博客仓库中新建 .github/workflows/douban.yml 文件,以观影为例添加如下内容。它实现了每小时自动抓取你的豆瓣观影记录并更新到文件中,如果发现文件有更新则触发 commit 提交。 name:doubanon:schedule:- cron:"30 * * * *"jobs:douban:name:Douban mark data syncruns-on:ubuntu-lateststeps:- name:Checkoutuses:actions/checkout@v2- name:movieuses:lizheming/doumark-action@masterwith:id:lizhemingtype:movieformat:csvdir:./douban- name:Commituses:EndBug/add-and-commit@v8with:message: 'chore:update douban data'add:'./douban'该 workflow 总共分为三步,第一步初始化 Git 仓库;第二步调用 doumark-action 同步豆瓣账号 lizheming 的 movie 类型数据到 ./douban 文件夹下,并保存为 csv 格式文件;最后一步则是当 ./douban 文件夹下有更新则调用插件提交修改。 Notion 如果是要同步到 Notion 中会稍微复杂一点。需要先准备好 Notion Token 并初始化好页面。 我们可以在 My Integrations 里创建机器人得到 NOTION_TOKEN。 电影 | 阅读 | 音乐 基于这三个模板点击右上角的 Duplicate 按钮渲染复制页面。 复制后的页面右上角选择右上角的 Share - Invite 将第一步创建的机器人加入,这样机器人就有权限更新你的页面数据。 # .github/workflows/douban.ymlname:doubanon:schedule:- cron:"30 * * * *"jobs:douban:name:Douban mark data syncruns-on:ubuntu-lateststeps:- name:movieuses:lizheming/doumark-action@masterwith:id:lizhemingtype:movieformat:notiondir:xxxxnotion_token:${{ secrets.notion_token }}其中 format 需要为 notion,dir 为 Notion 页面 ID,Notion 页面 URL 第一个随机字符即为页面的 ID。 渲染 数据已经有了,剩下的就是我们需要读取该数据源的数据,并渲染出页面。除了数据渲染之外,我还给自己增加了筛选查找的需求,所以我在头部还渲染了一些筛选项。 {{$movies := getCSV "," "douban/movie.csv" }} {{$scratch := newScratch}} {{$scratch.Add "genres" slice}} {{range $idx, $movie := $movies}} {{if ne $idx 0}} {{$scratch.Set "genres" (union ($scratch.Get "genres") (split (index $movie 7) ","))}} {{end}} {{end}} <div class="sc-ksluID gFnzgG"> <!--分类筛选--> <div class="sc-bdnxRM jvCTkj"> <a href="javascript:void 0;" class="sc-gtsrHT kEoOHR" data-search="genres" data-method="contain" data-value="">全部</a> {{range $genre := $scratch.Get "genres"}} <a href="javascript:void 0;" class="sc-gtsrHT dvtjjf" data-search="genres" data-method="contain" data-value="{{$genre}}">{{$genre}}</a> {{end}} </div> <!--时间筛选--> <div class="sc-bdnxRM jvCTkj"> <a href="javascript:void 0;" class="sc-gtsrHT kEoOHR" data-search="year" data-method="equal" data-value="">全部</a> {{range $year := (seq 2022 -1 2009)}} <a href="javascript:void 0;" class="sc-gtsrHT dvtjjf" data-search="year" data-method="equal" data-value="{{$year}}">{{$year}}</a> {{end}} </div> <!--评分筛选--> <div class="sc-bdnxRM jvCTkj"> <a href="javascript:void 0;" class="sc-gtsrHT kEoOHR" data-search="star" data-method="equal" data-value="">全部</a> <a href="javascript:void 0;" class="sc-gtsrHT dvtjjf" data-search="star" data-method="equal" data-value="5">五星</a> <a href="javascript:void 0;" class="sc-gtsrHT dvtjjf" data-search="star" data-method="equal" data-value="4">四星</a> <a href="javascript:void 0;" class="sc-gtsrHT dvtjjf" data-search="star" data-method="equal" data-value="3">三星</a> <a href="javascript:void 0;" class="sc-gtsrHT dvtjjf" data-search="star" data-method="equal" data-value="2">二星</a> <a href="javascript:void 0;" class="sc-gtsrHT dvtjjf" data-search="star" data-method="equal" data-value="1">一星</a> <a href="javascript:void 0;" class="sc-gtsrHT dvtjjf" data-search="star" data-method="equal" data-value="0">零星</a> </div> <!--排序规则--> <div class="sc-bdnxRM jvCTkj sort-by"> <a href="javascript:void 0;" class="sort-by-item active" data-order="time"> 观影时间排序 </a> <a href="javascript:void 0;" class="sort-by-item" data-order="rating"> 网友评分排序 </a> </div> <!-影片列表--> <div class="sc-dIsUp fIuTG"> {{range $idx, $movie := $movies}} <!--排除第一行表头--> {{if ne $idx 0 }} <div class="sc-gKAaRy dfdORB" data-year="{{index $movie 9}}" data-star="{{index $movie 8}}" data-rating="{{index $movie 6}}" data-genres="{{index $movie 7}}" > <a href="{{index $movie 5}}" target="_blank"> <div class="sc-hKFxyN HPRth"> <div class="lazyload-wrapper "> <img class="lazy" data-src="https://dou.img.lithub.cc/movie/{{ index (findRE `\d+` (index $movie 5)) 0 }}.jpg" referrer-policy="no-referrer" loading="lazy" alt="{{index $movie 1}}" width="150" height="220"> </div> </div> <div class="sc-iCoGMd kMthTr">{{index $movie 1}}</div> <div class="sc-fujyAs eysHZq"> <span class="sc-jSFjdj jcTaHb"> {{range $star := (seq 0 2 8)}} <svg viewBox="0 0 24 24" width="24" height="24" class="sc-dlnjwi {{if gt (index $movie 6) $star}}lhtmRw{{else}}gaztka{{end}}"> <path fill="none" d="M0 0h24v24H0z"></path> <path fill="currentColor" d="M12 18.26l-7.053 3.948 1.575-7.928L.587 8.792l8.027-.952L12 .5l3.386 7.34 8.027.952-5.935 5.488 1.575 7.928z"></path> </svg> {{end}} </span> <span class="sc-pNWdM iibjPt">{{index $movie 6}}</span> </div> </a> </div> {{end}} {{end}} </div> </div> 整体的布局我使用了 Flex 布局,增加了图片懒加载。 搜索 使用 CSS 的属性选择器,可以非常简单的实现搜索的功能。事先将数据通过属性挂载在 DIV 上,通过 [data-year^=2022][data-genres*=喜剧] 就可以查询到 2022 年看过的喜剧片了! function search(e) { // 隐藏全部电影 document.querySelectorAll('.dfdORB').forEach(item => item.classList.add('hide')); // 移除当前筛选项之前的选项 document.querySelector(`.dvtjjf.active[data-search="${e.target.dataset.search}"]`)?.classList.remove('active'); // 如果选择的是非全部选项,则高亮该选项 if(e.target.dataset.value) { e.target.classList.add('active'); } // 找到所有筛选项的值 const searchItems = document.querySelectorAll('.dvtjjf.active'); // 根据筛选值拼接 CSS 选择器,JSON 数据类型的需要使用 *=,其它的需要使用 ^= const attributes = Array.from(searchItems, searchItem => { const property = `data-${searchItem.dataset.search}`; const logic = searchItem.dataset.method === 'contain' ? '*' : '^'; const value = searchItem.dataset.method === 'contain' ? `${searchItem.dataset.value}` : searchItem.dataset.value; return `[${property}${logic}='${value}']`; }); const selector = `.dfdORB${attributes.join('')}`; // 找到目标元素对其进行展现操作 document.querySelectorAll(selector).forEach(item => item.classList.remove('hide')); } window.addEventListener('click', function(e) { if(e.target.classList.contains('sc-gtsrHT')) { e.preventDefault(); search(e); } }); 排序 由于我使用了 Flex 布局,所以排序这个实行实际上是可以通过 Flex 的 order 属性来实现的。这样做的好处就是我不需要真的去修改 DOM 结构,只需要生成或者删除 CSS 就好了。 function sort(e) { const sortBy = e.target.dataset.order; const style = document.createElement('style'); style.classList.add('sort-order-style'); document.querySelector('style.sort-order-style')?.remove(); document.querySelector('.sort-by-item.active')?.classList.remove('active'); e.target.classList.add('active'); if(sortBy === 'rating') { const movies = Array.from(document.querySelectorAll('.dfdORB')); movies.sort((movieA, movieB) => { const ratingA = parseFloat(movieA.dataset.rating) || 0; const ratingB = parseFloat(movieB.dataset.rating) || 0; if(ratingA === ratingB) { return 0; } return ratingA > ratingB ? -1 : 1; }); const stylesheet = movies.map((movie, idx) => `.dfdORB[data-rating="${movie.dataset.rating}"] { order: ${idx}; }`).join('\r\n'); style.innerHTML = stylesheet; document.body.appendChild(style); } } window.addEventListener('click', function(e) { if(e.target.classList.contains('sort-by-item')) { e.preventDefault(); sort(e); } }); 起因 很早以前我就养成了看完电影就要上豆瓣上标记一下的习惯,并在每年年末的时候统计一下。为了满足自己的需求,很早之前我写过一款 Chrome 插件,用于统计豆瓣电影记录,具体可以看这篇文章《豆瓣电影统计插件For Chrome》。 在后来无意间知道了牧风老师开发的布克牧为,用户同步豆瓣记录数据并支持在第三方网站中挂件展示。所以我为我的博客增加了观影页面,用来展示我看过的电影。后来,每当我和朋友聊电影,想要推荐之前看过的电影给他们的时候,它也成为了重要的查找入口。 布克牧为的第三方挂件样式很好看,但筛选功能偏弱,仅支持分类的筛选。对于我有搜索和统计的需求其实没办法很好的满足。再加之最近布克牧为时长不出数据,变的不太稳定,导致我又有了重新造轮子的想法。 自从博客切换成 Hugo 之后,我对 SSG(Server Side Generate) 就非常的痴迷,连评论都是使用 SSG 的方式渲染到页面上的,具体可以查看我之前写的这篇文章《静态博客如何高性能插入评论》。于是关于这次的功能理所当然我也想使用类似的方式。 所以最开始我是写了个独立的服务,该服务会定时抓取数据并更新到数据库中,同时提供了 API 用于获取数据。在博客中则去调用该接口获取到数据后渲染页面。后来因为需要找一个第三方定义任务服务,用于定时触发抓取任务接口。更新数据后还需要调用博客的构建触发器,同时又觉得每次构建的时候都需要花时间去请求一次接口有点浪费,就一直在思考有没有其它的方式。 其实 Hugo 除了支持 JSON 接口的数据读取之外,也支持本地 CSV 文件的数据读取。直接读取从库中的表格文件获取到数据能减少不必要的网络请求,而表格文件更新的时候会自动触发 Git 操作从何触发博客的构建任务。所以最终就想到了 GitHub Action 的方案,通过免费的 GitHub Action 触发 CSV 文件的更新,最终触发构建更新。 于是乎「doumark-action」这个项目就诞生了!
  •  

Eureka 主题性能优化小结

我在之前的文章 《Hugo 主题 Eureka 自定义》 中有讲到我现在用的博客主题就是 Eureka。不过主题虽然好看,但是性能跑分却比较低。遂趁着周末时间给优化了一下,遂有本文。 打开控制台看了下资源的加载,之前没注意,这会才发现首页竟然后 10M 这么多资源要加载,怪不得性能不好呢。 JS 资源 JS 资源中大头是 FontAwesome,主题中直接使用了引用了所有图标的集成版地址 @fortawesome/fontawesome-free/js/all.min.js,该资源有 1.2M。但其实在主题中根本没有使用到如此之多的图标,完全可以按需加载优化。 参考《Using Font Awesome Icons in Hugo》 中的优化方法。通过关键词查找收集了主题中用到的图标,下载下来后通过模板语法直接在构建阶段把所有的 SVG 内联到 HTML 中。 不过我发现在首页中会有大量的重复图标,使用该方法后会有重复的 SVG 内容内联到 HTML 中。所以我再上述方法的基础之上再次尝试优化,将所有的 SVG 图标合并到一个文件中,每个使用的地方使用 <use href="#<icon>" /> 来进行引用。 首先我们还是像之前那样,把所有的图标下载下来。区别是通过 <symbol> 将图标转成图元,方便后续使用 <use> 进行复用。 // deno run --allow-net --allow-write fontsvg.ts import * as path from "https://deno.land/std/path/mod.ts"; const __dirname = new URL('.', import.meta.url).pathname; const icons = [ "calendar-alt", "calendar", "star-half-alt", "comment", "clock", "bars", "search", "moon", "sun", "adjust", "globe", "th-list", "folder", "caret-right", "edit", "user", "pen", "book", "rss" ]; const baseUrl = 'https://cdn.jsdelivr.net/gh/FortAwesome/Font-Awesome@5.x/svgs/solid'; const toDefs = (id: string, svgText: string) => svgText .replace(/<svg.+viewBox=['"](\d+) (\d+) (\d+) (\d+)[^>]+>/, `<symbol id="${id}" viewBox="$1 $2 $3 $4">`) .replace('</svg>', '</symbol>') .replace('<path', '<path fill="currentColor"') .replace(/<!--.+?-->/, ''); const iconTexts = await Promise.all(icons.map(async icon => { const text = await fetch(`${baseUrl}/${icon}.svg`).then(resp => resp.text()); const match = text.match(/(viewBox="\d+ \d+ \d+ \d+")/); if(!match) { throw Error('match error'); } Deno.writeTextFile(path.join(__dirname, `./fontawesome/${icon}.svg`), `<svg ${match[1]}><use href="#${icon}" /></svg>`); return toDefs(icon, text); })); Deno.writeTextFile(path.join(__dirname, './fontawesome/all.svg'), `<svg width=0 height=0 viewBox="0 0 0 0">${iconTexts.join('\r\n')}</svg>`); 之后我们需要在 header 中加载 all.svg。在主题 header.html 开头增加如下代码: {{ $svg := resources.Get (print "fontawesome/all.svg") }} {{ $svg.Content | safeHTML }} 还是像引文中的方式一样,我们定义一个 Partial,所有使用的地方可以直接使用这个 Partial 内联图标 SVG。 <!--layouts/partials/fontawesome.html--> <span class="inline-svg svg-inline--fa fa-w-14 {{.class}}"> {{ $svg := resources.Get (print "fontawesome/" .icon ".svg") }} {{ $svg.Content | safeHTML }} </span> 最后我们在使用的地方只需要使用如下 partial 命令即可完成图标的嵌入。修改 calendar 为对应的图标名称可以实现内嵌对应的图标。 {{ partial "fontawesome.html" (dict "icon" "calendar") }} 这么优化之后首页 HTML 文档的体积有着显著的改善,从 89.7k 降低至 77.3k。不过由于内联的图标都变成了不重复的内容,压缩率反而降低了,这倒是我没有想到的。Vercel 使用的是 Brotil 压缩方式,原本基于引文的方式压缩后的体积是 20.4k,优化后压缩后的体积反而增加到了 22.1k。不过 2k 不到的体积增长,倒是还能接受。 解决了大头之后,JS 资源还剩下 highlight.min.js 和 eureka.min.js,前者是代码高亮使用,后者是主题对应的 JS 脚本。由于我在首页实际上是没有代码高亮的需求,所以我将 highlight.js 相关的资源做了判断,仅在详情页的时候再做加载。 而针对 eureka.min.js 这种小文件,我们可以考虑将其内联到 HTML 中减少一个请求。不过该优化在 HTTP/2 场景并不是一个最佳实践,诸君请适度使用。 {{- $eurekaJS := resources.Get "js/eureka.js" | resources.ExecuteAsTemplate "js/eureka.js" . | minify }} <script defer src="data:application/javascript;base64,{{ $eurekaJS.Content | base64Encode }}"></script> 最后其实还剩下百度统计的请求资源,这个参考以下两篇文章也是可以做类似的优化的,虽然两篇文章讲的都是 Google Analytics 但是原理都差不太多。不过目前这种程度我也能接受了,就没有再继续尝试下去,之后有空再参考优化一下。 《本博客零散优化点汇总》 《使用 Cloudflare Workers 加速 Google Analytics》 CSS 资源 CSS 资源中大头是 eureka.min.css,高达 4M 的体积一看就知道它用了原子类 CSS 库 TailWind(笑哭。毕竟正经人谁能写出 4M 的 CSS 文件,特别还是这么简单的一款主题。 我对原子类 CSS 写法一直不太感冒的原因主要有两点,一个是本质它把 CSS 的功能转嫁到了 HTML class 属性上,看着那些纷繁复杂又臭又长的 class 令人脑壳疼。再一个就是因为它的体积问题。 好在 TailWind CSS 提供了优化选项,通过遍历配置中的文件查找所有可能用到的 class 节省体积。具体的话可以参考文档。由于是静态分析 class,所以不能出现动态拼接,也不能出现变量之类的替代。 将配置开启之后,eureka.min.css 文件从初始的 4M 优化成了 21.4k,Brotil 压缩后体积是 5.2k,整个人都神清气爽了有没有。 初次之外,网站还加载了一款代码高亮主题 solarized-light.min.css 以及一款自定义字体。代码高亮样式则按照 JS 优化策略一样,仅针对详情页再加载。而自定义字体我看了下会加载一款中文的 Web Font,用于提供给全站使用。所以也没有做动态切片等体积优化处理,每次都会加载 2M 的字体资源。考虑到该需求是纯美化场景,系统默认的衬线体也还可以,遂直接将该自定义字体移除解决。 图片资源 图片也是比较中的资源加载灾区,有 3M 的图片资源加载。由于之前没有特别在意这块,很多场景为了方便直接原图就放上来了,也没有做图片的处理。所以这次就使用常规的图片资源处理手段对图片进行了优化处理,主要是图片压缩以及 LazyLoad。 图片处理这块就是很正常的手段了,没有什么值得说的。图片压缩主要是使用了 https://tinypng.com,LazyLoad 则是用了苏卡卡推荐的 vanilla-lazyload。 除了体积的优化之外,图片还可以对它进行格式和尺寸进行优化。现在比较推荐使用 <picture> 的写法渲染图片,内部存放不同的格式的图片,浏览器会根据是否支持选择对应的格式展示。 <picture> <source srcset="image.webp" type="image/webp"> <img src="image.jpg" alt="my image"> </picture> 剩下的就是如何获取 webp 的图片了,网上有比较多使用 cwebp 手动转成 webp 图片的教程,我就不多赘述了。除此之外,Hugo 本身似乎也支持做个格式的转换(https://discourse.gohugo.io/t/image-conversion-without-resizing/32429)。 {{ $i := resources.Get "image.jpg" }} {{ $resizeOptions := printf "%dx%d webp" $i.Width $i.Height }} {{ $i = $i.Resize $resizeOptions }} 最后一种方式,也是我比较推荐的方式,是使用外部的 CDN 存储服务。这些外部服务都会有通过 URL 动态转换和裁剪的能力。 除了更好的格式,对图片的裁剪也很重要。实际上 Hugo 本身也有非常多的图片处理方法用于图片优化,主要是裁剪和滤镜。我们可以利用 Hugo 本身的功能,也可以使用外部 CDN 存储服务,基于他们的动态裁剪能力来实现。 不过我的只是我的文章中图片地址五花八门,有本地的也有各种外部 CDN 的,使用那种方式都比较麻烦。所以暂时就没有处理格式的事情了, 如果有需要的可以参考一下。 2022-07-02 更新 最终我采用了外部 CDN 的方式,将博客中所有的外链图片重新抓取下来整理上传到又拍云,并对文章图片进行了整体的清洗,一些老图无法访问的就直接指向一个 404 的图片了。 同时开启了又拍云的 Webp 自适应功能,无需修改图片链接地址,又拍云 CDN 会自动根据浏览器是否支持来返回 Webp 图片,轻松全站支持 Webp 图片访问。最终的优化效果也非常明显,整体图片体积再次缩小了 3 倍左右。 总结 在各种优化之下,最终首页的资源加载从之前的 10M 缩减到了现在的 361k,加载速度已经令我比较满意了。之后我再抽空处理下整站的图片资源。
  •  

Apple TV × NAS | 揽件日志

空虚的生活需要精致物件填充,甜蜜的消费主义陷阱虽然是陷阱但确实很甜。好久不见,这里是「揽件日志」。

接近半年时间我都没购入什么大件,少有的上千的物品都是买给猫的(智能猫砂盆,烘猫箱…),甚至 iPhone 13 也忍住没买。过年返京年终奖一发,手里多了点闲钱,实在是没忍住,下手了第六代 Apple TV 和一台 Synology DS220+ NAS,今天就来说说感受。

Apple TV: 最「果味儿」的影音体验

设备图片我就不放了哈,自己去官网看。一个小黑盒子,和其它电视盒子没啥两样。带一个遥控器,一根 Lightning 线(给遥控器充电)和一根电源线。很简朴。开机,联网,激活,然后就是非常简洁的主界面。苹果主推的 Apple TV+ 和 Apple Fitness+ 占据了屏幕上半部分主要的可视空间,然后下半部分是 App 列表。

方方面面的细致评测我懒得做懒得说。我说它「果味儿」,主要是三个方面:对 iOS 用户来讲非常熟悉的、流畅的操作逻辑;与其它苹果设备的深度整合;赏心悦目又好用的各种 App。

举几个例子。不知道大家有没有用过那款 iPod,那个带有 Click Wheel 的经典款?巧了我也没用过,但这次 Apple TV 附带的遥控器让我们有机会体验到那种有趣高效的交互。简单来说,这次的遥控器既有实体的按键,但也是可触控的,你可以在遥控器上面滑动,TV 上相应的焦点也会改变,十分顺滑。苹果甚至在配备了触摸遥控器的机型上修改了键盘布局,由 QWERTY 键盘变成了直接的 26 个字母罗列,想必是对新遥控器的输入速度很有信心。

流畅的体验不止于此,苹果通过自己的软硬件生态做到了只有它能做到的流畅:Apple TV 是可以用 iPhone 做遥控器的。当与 iPhone 配对之后控制中心会出现一个新按钮,点开就可以完全使用 iPhone 来遥控电视,当然也支持滑动手势,不需要下载什么 App,整合程度很深。另外 TV 上需要输入文字的地方,比如输密码,搜索内容,都可以使用 iPhone 输入。说起来这么自然的方案,第一次用上还是会觉得,啊,舒坦。另外和 HomeKit 的联动,支持 Siri 等等我就不说了。

App 方面可能见仁见智。因为 Apple TV 没有进入国区,所以是不能用国区的 Apple ID 下载 App 的,爱奇艺、优酷、腾讯等等也只能下载美区商店里的海外版,内容和国内不同。这对许多用户来说是硬伤,但具体到我就觉得还好,一者是我很少看这几个平台的视频,二者还能保底用 AirPlay 投屏。我更关心的如何播放我自己的收藏品,以及看 B 站,这两个需求在 Apple TV 上刚好能很好地解决。

前者我的选择是 Infuse,一个横跨苹果所有平台的播放器。它首先实力超群,对各种格式、编码都有很好的支持,基本上解决了所有播放需求;其次颜值非常非常高。TV 端不好截屏,给你们看看 Mac 端的效果,TV 端基本类似。

这些元数据、海报都是 Infuse 自己匹配抓取的,只要文件名稍微符合格式,抓取正确率很高。有些人可能疑惑,用 Emby、Plex 之类的媒体库也能达成相似的效果,为什么不用呢?有这么几点原因:

  • 我的文件都存在后文所说的 NAS,通过 SMB 在家里局域网内分享,Infuse 是上手即用的,不需要部署任何别的服务;
  • Infuse 抓取的元数据可以通过 iCloud 同步,我的所有设备都能立刻获得相同的优质体验;
  • Infuse 颜值真的很高

后者我的选择是 MiaoProject,一个 Bilibili 的第三方客户端。它的数据是从电视端的云视听小电视来的,但同样是颜值很高,而且支持弹幕,应该是 Apple TV 上最好的 B 站播放器,说不定是所有 TV 端上最好的 B 站播放器。

虽然很主观也不全面,个人体验 Apple TV 一段时间下来总结起来就是我在推特上说的一句话:

从小米电视换 Apple TV,让我体会到了当年从安卓换 iPhone 的喜悦。

Synology DS220+: 好玩、可靠、自由的文件服务

NAS 是我心水很久的东西,但一直都觉得没什么刚需。在此之前我的数据都存在一块 4T 的移动硬盘里插在树莓派上,通过 SMB 分享到局域网使用。但是近两个月频繁掉盘,我没找到原因(兴许是供电的问题),不安感逐渐增强起来:这可能不是长久之计。

通过调研、学习、对比之后,我入手了 Synology DS220+ 以及两块 8T 的希捷企业级硬盘,组 SHR(群晖自有的 RAID 方案,可以认为是 RAID 1 与 RAID 5 的加强版)使用,自认为是比较稳定、中长期内够用的配置。NAS 的细节太多,我推荐少数派出品的一本入门手册给大家:家用 NAS 入门指南,手册是基于群晖 DSM 6.x 的,但是内容仍然有很强的参考性。

我开始是有点担心买回来吃灰的,因为在我的认知里 NAS 不过就是增加了一个管理面板的 Linux 服务器,顶多是稳定性稍微好一点,这样一来我家里的任何一台电脑都能胜任。到手探索了一段时间后发现群晖基于「存储」这个核心做了很多衍生,完全创造了新的价值。短短几天我就更新了认知:群晖 NAS 给用户提供的是「好玩、自由、可靠的文件服务」。

「好玩」两部分组成,一是群晖提供的 DSM 系统中集成的大量开箱可用的服务、套件,二是 Docker 支持带来了无限可能性。

文件共享服务器这个核心功能确实大多数的 Linux 都能做到的,群晖使用的也是 SMB、NFS、AFP 这些标准协议,但群晖 NAS 的优势在于这些服务开箱即用,支持十分全面,免去了很多繁琐的运维工作。基于文件存储的衍生服务才是群晖的独门绝技,这里我主要说的是 Synology Drive,Synology Photos 这两项服务。

Synology Drive 可以理解为自托管的类似 Dropbox 的服务,它在文件存储的基础上增加了同步、共享、协作等等特性。与 OneDrive、Dropbox 等类似,它也支持将远程存储映射到本地目录,并且官方支持 Windows、Mac、Linux(Ubuntu) 几大平台,对 Geek 相当友好。增量同步是有的,按需同步目前在 Windows 上支持,据官方表示今年 Q2 会上 Mac 平台。Synology Drive 还提供了 Web 界面,可以浏览文件,甚至也支持在网页端创建在线协作文档,就类似于 Google Docs 那种,个人用自必不说,对几人十几人的小团队来说也是堪用的。

Synology Photos 我还没有体验太多,大致是一套类似于 Google Photos 的服务,也支持基于 AI 的内容识别、照片归类等等。据深度使用过的人说,体验甚至比 Google Photos 更好,因为他不会压缩你的图片,空间限制也就是你的硬盘限制。

群晖 DSM 上还有很多优质套件,比如 Video Station,Audio Station 等,以及 Nginx,MySQL,PHP 等开发套件,应该是够技术达人们好好把玩一番了。还有一个套件是 Docker,我举两个用例。

国内环境里,全面使用 NAS 作为个人存储解决方案最大的阻碍只有一个:公网 IP。没有公网 IP 的情况下要从外网访问 NAS 只能走群晖的 QuickConnect,叫「Quick」,其实很不「Quick」。FRP 这样的内网穿透服务使我们可以用一个有公网 IP 的 VPS 作为中转进而访问内网的 NAS。FRP,是可以通过 Docker 容器部署的,在群晖 NAS 上,部署只是点几下鼠标的工作量。基于此,我的 NAS 真正地随处可用了,它现在已经完全取代 OneDrive 成为了我所有文件的存取点。

阿里云盘大家都知道,不限速,资源也逐渐地在多起来。如果能在 Apple TV 上直接串流阿里云盘的内容是不是很美好?可惜阿里云盘并没有 TV 客户端。但是有人基于阿里云盘的 API 开发了 aliyundrive-webdav,可以直接把阿里云盘转换成一个 WebDAV 服务器,同样支持 Docker 部署在 NAS 上,这样在 Apple TV 上通过 Infuse 连上这个 WebDAV 服务器,你的阿里云盘立即就变成了一个巨大的媒体库,可以直接在 TV 上串流播放,爽不爽?

举这俩例子是想说:DSM 对 Docker 的支持进一步让 NAS 的可玩性变得接近无限。

说完了「好玩」,接下来稍微聊聊「可靠」、「自由」。如果钻牛角尖,前者公有云(百度云、阿里云盘之类的)私有云(NAS)都不能完全做到;后者公有云别想了,私有云反倒是有一定可能。

单纯论数据存储可靠性,我想公有云显然是更强的,毕竟企业级的多副本、灾备措施是本地 NAS 不能及的。这一点上可以通过 RAID 来多少提升一些数据可靠性,目前我的选择是 SHR,一块盘损毁不会丢失数据,算是比较保险的方案,在我的风险承受能力下,我称之为是「可靠」的。

如果我们把数据主权纳入考虑,会发现公有云陡然多了一层风险:数据审查。我想大家都体验过存在百度云上的视频被夹,那一刻我不知道你是什么感受?一笑了之?我是感到了恐惧与愤怒。数据是我的数据,你天天扫我数据,甚至还直接删除。虽然是监管要求也怪不得你云盘,但我就是不喜欢。谁没有点私密的东西不想让任何第三方知道呢?

NAS 则让我从这种风险中解脱。数据存在硬盘上,硬盘放在家里。虽然说不上是绝对的安全(毕竟警察也可以没收我的 NAS),但是比起把数据放在某某云盘上可是安心多了。


本期揽件日志就到这里。我们拔高一点,上文的两个物件核心价值都来源于数据:NAS 让我以更稳定、可靠、可用的方式存储数据,Apple TV 则让我以最舒服的方式享受我的部分数据。数据到底多重要?现在,数据就是一切。把数据从公有云上撤下来、尽力保持数据私有,然后借助 NAS 获得不输公有云的可用性与可连接性,这是我当下的决定。

  •  

拯救者R9000P氮化镓电源适配器测评

✇遐说
作者Dorad
由于拯救者R9000P原装300W的电源适配器过于笨重,本习武之人难以驾驭,故一直在寻找合适的氮化镓充电器。先后使用了原装300W、安述240W氮化镓和安述330W氮化镓等电源适配器,在此分享使用心得。
  •  

养一只猫

今年年初毕业后正式开始了社会人生活。我从小学四年级开始在学校寄宿,算起来集体生活已经过了十六七年,如今和🐦租房,明明只是十几平米的单间也显得空旷。于是时不时和🐦商量,「要不养只猫吧」!

🐦同样喜欢小动物,也是满口答应。然后我们就盘算着要去网上做做功课,要提前买一些猫猫用品,要怎么样布置家里让猫猫住的舒心……但始终没有迈出去接一只猫回家的这一步。就这么从春天盘算到了冬天。

这将近一年里眼睁睁看着几个朋友先后养了猫。更有甚者每晚给我发自家小猫的视频,或可爱或顽皮,说是会在每晚跳上床要抱抱,看得我好生羡慕。终于下定决心:养只猫吧!

于是开始在网上寻找靠谱的猫舍,也向朋友请教经验。我是不介意外貌什么的,但听说品种猫性格会比较好一些,所以比较偏好买一只品种猫;🐦则觉得领养代替购买更好,所以更偏好领养。不过我们都同意,接一只猫猫回家主要还是看缘分的。

缘分很快就来了。某天一个朋友转了篇公众号推送给我,是通州的一只小流浪找领养。说是小流浪,其实也由那时的主人接回去照顾了一段时间,做了驱虫,打了第一针疫苗,也即将绝育。那是个多猫家庭,原住民和这只猫猫相处不是很好,而且原住民患癫痫,不能受刺激,所以才会想为小猫找领养。

小猫叫豆花,是黄白相间的长毛男生。我俩觉得这机会不可多得,赶紧微信联系,想上门去看看猫,对对眼缘。于是在一个周末,我俩来到了通州。原主人家里暖气过剩,我俩穿着羽绒服戴着口罩,一边冒汗一边尝试接触豆花。豆花在笼子里,似乎有点不领情。听原主人说,豆花很胆小,说不定会对我们哈气,让我们也别介意,可能熟悉了就好了。实际见到豆花,倒没有哈气,但是躲得远远的,不太搭理我们。原主人见我们有点灰心,甚至安慰起来:「面试失败也没关系,养猫还是看缘分的」。

我俩虽觉得希望不大,不过还是认真考虑了一晚,向原主人回复说我们想领养他。想不到的是原主人说更愿意把豆花交给我们。据原主人说,并不是因为鸟也是个 LO 娘的原因,而是因为别人家里已经有猫猫了,原主人不确定豆花能不能跟原住民好好相处。于是一周后,待豆花做完了绝育,我们把他接回了家。


刚到家的豆花怕得不行,一打开航空箱立刻钻进床底深处,睁大眼睛盯着我们,不出声也不动,不吃不喝。我们一边担心一边觉得惊讶:猫猫怎么能像个雕塑一样,纹丝不动地呆这么久!?

豆花会在熄灯后悄悄出来探索环境,我俩也被弄得神经紧张。一旦听到床下有个什么动静,我俩大气不敢出,生怕吓到豆花。只能在被子里互相掐对方,意思是:「听!豆花有动作!」就这么到凌晨才睡觉。

我们专门买了个监控摄像头,带夜视那种,偷窥豆花。豆花喜欢东闻闻西蹭蹭,也喜欢跳上飘窗看窗外。几天后,虽然还是胆怯,豆花敢在我们睡着或不在家时出来吃东西、喝水了。拉屎倒是到家后第一晚就拉了,会用猫砂盆,拉完会认真埋好,猫砂盆里总是堆起小山。

向原主人报告豆花近况后,原主人说,豆花能吃能喝我们就可以大胆点试着摸摸他了。豆花胆小,不会打人。我决定试试。

我钻进床底。他果真不打人,稍稍躲了一下,就任我摸了。我知道他还是很警觉,随时准备逃走或者反击,也知道这是建立信任的关键一步,于是尽量慢慢温柔地摸他。豆花逐渐放松下来,喉咙里也开始发出呼噜声,我就知道成了。

后面几天里我维持着不会吓到他的程度和豆花接触,取得了他的信任。现在的豆花已经是会主动走到我脚边躺下,甚至翻身露出肚皮求摸摸的猫猫了。家里添置了猫爬架放在飘窗,如今那是豆花最爱的地方,每天下午固定在猫爬架上晒太阳。晒太阳的豆花很懒,睡得七倒八歪,丝毫不顾形象。

猫果真是敏感细腻的生物。饲主与猫的关系很难说是主人与奴仆的关系,而我也不全同意是反过来的主子与铲屎官的关系。我更多地悟出了一种相互尊重。猫不曾失去自尊,他有自己对待世界的一套方式,对人亲疏全看人是否友善待他。要和猫好好相处,其中的秘诀之一便是不要强迫猫,慢慢让猫知道他与你的相处是自然而安全的。偶尔试着伸出友好的手接近他,缘分到了他自然也会过来,用毛茸茸的脸蹭蹭你。

  •  

Hugo 主题 Eureka 自定义

今天有网友邮件我咨询我现在的主题 Eureka 的一些自定义配置,他想参考一下。由于我的博客仓库是私有的,所以就写一篇文章简单整理一下。 Eureka 是前段时间群友推荐给我的,纯白的朴素风格同时提供了暗色模式瞬间我就喜欢上了。将其 clone 到 Hugo 博客目录 themes/hugo-eureka 下,config.toml 中配置 theme = "hugo-eureka" 即可使用上该款主题。为了方便主题的更新,我将我所有自定义的模板都放在了 layouts 目录下。Hugo 会将主题目录和 layouts 目录下的文件进行合并,并优先使用 layouts 目录中的同名文件。这样之后我只需要单纯的更新 thtmes/hugo-eureka 目录即可。 首页 相较于 Eureka 主题的默认首页,我个人还是比较喜欢传统博客的两栏布局,左侧显示模块列表,右侧显示文章列表,所以我需要自定义首页模板。拷贝以下内容创建 layouts/index.html 文件即可实现同款。 {{ define "main" }} <div class="pl-scrollbar"> <div class="w-full max-w-screen-xl lg:px-4 xl:px-8 mx-auto"> <div class="max-w-screen-xl mx-auto" style="padding-top: 3rem"> <div class="bg-local bg-cover"> <img class="day" src="/banner-day.png" /> <img class="dark" src="/banner.png" /> </div> </div> <!-- <article class="mx-6 my-7"> <h1 class="font-bold text-3xl text-primary-text"></h1> </article> --> <div class="grid grid-cols-2 lg:grid-cols-8 gap-4 lg:pt-12"> <div class="col-span-2 sidebar"> <div class="widget bg-secondary-bg rounded p-6"> <h2 class="widget-title">最新文章</h2> <ul class="widget-list"> {{- $recent := default 5 .Site.Params.numberOfRecentPosts }} {{- $posts := where (where .Site.RegularPages "Permalink" "!=" .Permalink) "Type" "in" .Site.Params.mainSections }} {{- range first $recent $posts }} <li> <a href="{{ .RelPermalink }}" class="nav-link">{{ .Title }}</a> </li> {{- end }} </ul> </div> <div class="widget bg-secondary-bg rounded p-6"> {{ $walineURL := .Site.Params.comment.waline.serverURL }} <h2 class="widget-title ">最近回复</h2> <ul class="widget-list recentcomments"> {{ $resp := getJSON $walineURL "/comment?type=recent&count=10" }} {{ range first 10 $resp }} <li class="recentcomments"> <a href="{{.Site.BaseURL}}{{ .url }}">{{ .nick }}</a>:{{ .comment | safeHTML | truncate 22 }} </li> {{ end }} </ul> </div> <div class="widget bg-secondary-bg rounded p-6"> <h2 class="widget-title">友情链接</h2> <ul class="widget-list"> {{ range .Site.Menus.friends }} <li> <a href="{{ .URL }}">{{ .Name }}</a> </li> {{ end }} </ul> </div> <div class="widget bg-secondary-bg rounded p-6"> <h2 class="widget-title">管理</h2> <ul class="widget-list"> <li> <a href="/admin">🛠 后台管理</a> </li> <li> <a href="{{ .Site.Params.comment.waline.serverURL }}/ui">💬 评论管理</a> </li> </ul> </div> </div> <div class="col-span-2 lg:col-span-6 bg-secondary-bg rounded px-6 py-8"> <div class="bg-secondary-bg rounded overflow-hidden px-4 divide-y"> {{ range .Paginator.Pages }} <div class="px-2 py-6"> {{ partial "components/summary-plain.html" . }} </div> {{ end }} </div> {{ template "_internal/pagination.html" . }} </div> </div> </div> </div> {{ end }} 其中顶部还增加了一组暗色模式切换的横幅图片,添加以下 CSS 内容至 layouts/partials/custom-head.html 文件中,不存在的话需要新建。 .widget + .widget { margin-top: 1rem; } .widget-title { font-weight: bold; margin-bottom: 1rem; } .widget-list li { font-size: 0.9rem; } .bg-cover img { opacity: 1; transition: all .5s ease-in-out; } .bg-cover img.dark { opacity: 0; height: 0; } .dark .bg-cover img.day { opacity: 0; height: 0; } .dark .bg-cover img.dark { opacity: 1; height: auto; } 左侧的模块中,评论是使用了本人自研的 Waline 评论系统并进行了一定的改造,具体可参见我之前的文章《静态博客如何高性能插入评论》。当然也可以直接使用 Waline 自带的最近评论挂件。 友情链接则是在 config.toml 中按照如下格式进行配置的。 [[menu.friends]] name = "童欧巴博客" url = "https://hungryturbo.com/" weight = 20 [[menu.friends]] identifier = "QingXu" name = "QingXu" url = "https://blog.qingxu.live" weight = 19 [[menu.friends]] identifier = "蜘蛛抱蛋" name = "蜘蛛抱蛋" url = "https://blog.zzbd.org/" weight = 18 后台管理则是使用了 forestry 提供的服务,它支持提供在线后台进行文章、页面和其它配置的管理。评论管理则是链接到了 Waline 服务的后台面板中。 Metadata Eureka 主题的文章 metadata 显示分为列表页和详情页两个,分别对应 post_metadata.html 和 post_metadata_full.html 两个文件。我们在 layouts/partials/ 目录下新建这两个文件用来覆盖主题默认的文件。 2021-03-13 更新: 更新后的 Eureka 统一使用了 components/post-metadata.html 显示文章的 metadata,代码和之前的 layouts/partials/post_metadata.html 是一致的。 {{/* layouts/partials/components/post_metadata.html */}} <div class="flex flex-wrap flex-row items-center my-2 text-tertiary-text"> <div class="mr-6 my-2"> <i class="fas fa-calendar mr-1"></i> <span>{{ .Date.Format (.Site.Params.dateFormat | default "2006-01-02") }}</span> </div> {{- $slug := printf "/%s.html" .Slug}} {{- $commentsData := (partialCached "utils/get-comments.html" .)}} {{- $comments := slice }} {{- range where $commentsData "url" "==" $slug}} {{$comments = $comments | append .}} {{- end}} {{- $count := len $comments}} <div class="mr-6 my-2"> <a href="{{ .Permalink }}#waline-comments" title="{{ .Title }}"> <i class="fas fa-comment mr-1"></i> <span>{{- if gt $count 0}}{{$count}} 条评论{{else}}暂无评论{{end -}}</span> </a> </div> <div class="mr-6 my-2"> <i class="fas fa-clock mr-1"></i> <span>{{ i18n "readingTime" . }}</span> </div> {{ with .GetTerms "categories" }} <div class="mr-6 my-2"> <i class="fas fa-folder mr-1"></i> {{ range $index, $value := . }} {{ if gt $index 0 }} <span>, </span> {{ end -}} <a href="{{ .Permalink }}" class="hover:text-eureka">{{ .LinkTitle }}</a> {{ end }} </div> {{ end }} {{ with .GetTerms "series" }} <div class="mr-6 my-2"> <i class="fas fa-th-list mr-1"></i> {{ range $index, $value := . }} {{ if gt $index 0 }} <span>, </span> {{ end -}} <a href="{{ .Permalink }}" class="hover:text-eureka">{{ .LinkTitle }}</a> {{ end }} </div> {{ end }} </div> post_metadata.html 主要是增加了评论条数的显示,而 post_metadata_full.html 中还增加了 Markdown 原文链接的显示。关于如何生成 Markdown 原文链接,可以参考我之前的文章《Hugo 之旅》。 {{/* layouts/partials/post_metadata_full.html */}} <div class="flex flex-wrap flex-row items-center my-2 text-tertiary-text"> <div class="mr-6 my-2"> <i class="fas fa-calendar mr-1"></i> <span>{{ .Date.Format (.Site.Params.dateFormat | default "2006-01-02") }}</span> </div> {{$resp := getJSON "https://imerd.comment.lithub.cc/comment?type=count&url=https://imnerd.org/" .Slug ".html" }} <div class="mr-6 my-2"> <a href="{{ .Permalink }}#waline-comments" title="{{ .Title }}"> <i class="fas fa-comment mr-1"></i> <span>{{- if gt $resp 0}}{{$resp}} 条评论{{else}}暂无评论{{end -}}</span> </a> </div> {{ if eq .Type "posts" -}} {{ with .OutputFormats.Get "MarkDown" -}} <div class="mr-6 my-2"> <a href="{{ .Permalink }}"> <i class="fas fa-book mr-1"></i> <span>阅读Markdown格式</span> </a> </div> {{- end }} {{ end }} <div class="mr-6 my-2"> <a href="{{ .Permalink }}"> <i class="fas fa-pen mr-1"></i> <span>{{ .WordCount }} 字</span> </a> </div> <div class="mr-6 my-2"> <i class="fas fa-clock mr-1"></i> <span>{{ i18n "readingTime" . }}</span> </div> {{ with .GetTerms "categories" }} <div class="mr-6 my-2"> <i class="fas fa-folder mr-1"></i> {{ range $index, $value := . }} {{ if gt $index 0 }} <span>, </span> {{ end -}} <a href="{{ .Permalink }}" class="hover:text-eureka">{{ .LinkTitle }}</a> {{ end }} </div> {{ end }} {{ with .GetTerms "series" }} <div class="mr-6 my-2"> <i class="fas fa-th-list mr-1"></i> {{ range $index, $value := . }} {{ if gt $index 0 }} <span>, </span> {{ end -}} <a href="{{ .Permalink }}" class="hover:text-eureka">{{ .LinkTitle }}</a> {{ end }} </div> {{ end }} </div> 搜索框 搜索也是博客比较重要的功能,为了方便我在顶部增加了搜索框。创建 layouts/partials/header.html 文件用来覆盖默认的头部模板。 {{/* layouts/partials/header.html */}} <script> let storageColorScheme = localStorage.getItem("lightDarkMode") {{- if eq .Site.Params.colorScheme "light" }} if ((storageColorScheme == 'Auto' && window.matchMedia("(prefers-color-scheme: dark)").matches) || storageColorScheme == "Dark") { document.getElementsByTagName('html')[0].classList.add('dark') } {{- else if eq .Site.Params.colorScheme "dark" }} if ((storageColorScheme == 'Auto' && window.matchMedia("(prefers-color-scheme: light)").matches) || storageColorScheme == "Light") { document.getElementsByTagName('html')[0].classList.remove('dark') } {{- else }} if (((storageColorScheme == 'Auto' || storageColorScheme == null) && window.matchMedia("(prefers-color-scheme: dark)").matches) || storageColorScheme == "Dark") { document.getElementsByTagName('html')[0].classList.add('dark') } {{- end }} </script> <nav class="flex items-center justify-between flex-wrap px-4 py-4 md:py-0"> <a href="{{ "/" | relLangURL }}" class="mr-6 text-primary-text text-xl font-bold">{{ .Site.Title }}</a> <button id="navbar-btn" class="md:hidden flex items-center px-3 py-2" aria-label="Open Navbar"> <i class="fas fa-bars"></i> </button> <div id="target" class="hidden block md:flex md:flex-grow md:justify-between md:items-center w-full md:w-auto text-primary-text z-20"> <div class="md:flex md:h-16 text-sm md:flex-grow pb-4 md:pb-0 border-b md:border-b-0"> {{- $relPermalink := .RelPermalink }} {{- range .Site.Menus.main }} {{- $url := .URL | relLangURL }} <a href="{{ $url }}" class="block mt-4 md:inline-block md:mt-0 md:h-(16-4px) md:leading-(16-4px) box-border md:border-t-2 md:border-b-2 {{ if hasPrefix $relPermalink $url }} selected-menu-item {{ else }} border-transparent {{ end }} mr-4">{{ .Name }}</a> {{- end }} </div> <div class="flex"> <div class="search-container relative pt-4 md:pt-0"> <div class="search"> <form role="search" class="search-form" action="/search.html" method="get"> <label> <input name="q" type="text" placeholder="搜索 ..." class="search-field"> </label> <button> <i class="fas fa-search"></i> </button> </form> </div> </div> <div class="relative pl-4 pt-4 md:pt-0"> <div class="cursor-pointer hover:text-eureka" id="lightDarkMode"> {{- if eq .Site.Params.colorScheme "dark" }} <i class="fas fa-moon"></i> {{- else if eq .Site.Params.colorScheme "light" }} <i class="fas fa-sun"></i> {{- else }} <i class="fas fa-adjust"></i> {{- end }} </div> <div class="fixed hidden inset-0 opacity-0 h-full w-full cursor-default z-30" id="is-open"> </div> <div class="absolute flex flex-col left-0 md:left-auto right-auto md:right-0 hidden bg-secondary-bg w-48 rounded py-2 border border-tertiary-bg cursor-pointer z-40" id='lightDarkOptions'> <span class="px-4 py-1 hover:text-eureka" name="Light">{{i18n "light"}}</span> <span class="px-4 py-1 hover:text-eureka" name="Dark">{{i18n "dark"}}</span> <span class="px-4 py-1 hover:text-eureka" name="Auto">{{i18n "auto"}}</span> </div> </div> {{- if .IsTranslated }} <div class="relative pt-4 pl-4 md:pt-0"> <div class="cursor-pointer hover:text-eureka" id="languageMode"> <i class="fas fa-globe"></i> <span class="pl-1">{{ .Language.LanguageName }}</span> </div> <div class="fixed hidden inset-0 opacity-0 h-full w-full cursor-default z-30" id="is-open-lang"> </div> <div class="absolute flex flex-col left-0 md:left-auto right-auto md:right-0 hidden bg-secondary-bg w-48 rounded py-2 border border-tertiary-bg cursor-pointer z-40" id='languageOptions'> <a class="px-4 py-1 hover:text-eureka" href="{{ .Permalink }}">{{ .Language.LanguageName }}</a> {{- range .Translations }} <a class="px-4 py-1 hover:text-eureka" href="{{ .Permalink }}">{{ .Language.LanguageName }}</a> {{- end }} </div> </div> {{- end }} </div> </div> <div class="fixed hidden inset-0 opacity-0 h-full w-full cursor-default z-0" id="is-open-mobile"> </div> </nav> <script> let element = document.getElementById('lightDarkMode') {{- if eq .Site.Params.colorScheme "light" }} if (storageColorScheme == 'Auto') { element.firstElementChild.classList.remove('fa-sun') element.firstElementChild.setAttribute("data-icon", 'adjust') element.firstElementChild.classList.add('fa-adjust') document.addEventListener('DOMContentLoaded', () => { switchMode('Auto') }) } else if (storageColorScheme == "Dark") { element.firstElementChild.classList.remove('fa-sun') element.firstElementChild.setAttribute("data-icon", 'moon') element.firstElementChild.classList.add('fa-moon') } {{- else if eq .Site.Params.colorScheme "dark" }} if (storageColorScheme == 'Auto') { element.firstElementChild.classList.remove('fa-moon') element.firstElementChild.setAttribute("data-icon", 'adjust') element.firstElementChild.classList.add('fa-adjust') document.addEventListener('DOMContentLoaded', () => { switchMode('Auto') }) } else if (storageColorScheme == "Light") { element.firstElementChild.classList.remove('fa-moon') element.firstElementChild.setAttribute("data-icon", 'sun') element.firstElementChild.classList.add('fa-sun') } {{- else }} if (storageColorScheme == null || storageColorScheme == 'Auto') { document.addEventListener('DOMContentLoaded', () => { switchMode('Auto') }) } else if (storageColorScheme == "Light") { element.firstElementChild.classList.remove('fa-adjust') element.firstElementChild.setAttribute("data-icon", 'sun') element.firstElementChild.classList.add('fa-sun') } else if (storageColorScheme == "Dark") { element.firstElementChild.classList.remove('fa-adjust') element.firstElementChild.setAttribute("data-icon", 'moon') element.firstElementChild.classList.add('fa-moon') } {{- end }} document.addEventListener('DOMContentLoaded', () => { getcolorscheme(); switchBurger(); {{- if .IsTranslated }} switchLanguage() {{- end }} }); </script> 大部分的内容都是 Eureka 主题提供的,除了增加了 #search-container 搜索框部分。为了让搜索框更美观一点,我在 layouts/partials/custom-head.html 中自定义了一些样式。 .search-container { margin-top: -0.3rem; } .search-container .search { border: 1px solid #e2e8f0; border-radius: 4px; } .search-container input { padding-left: 1rem; line-height: 2rem; outline: none; background: transparent; } .search-container button { font-size: 0.8rem; margin-right: 0.5rem; color: #e2e8f0; } 最终搜索框跳转至单独的搜索页面。关于如何给 Hugo 博客添加搜索功能,可查看我之前的文章 《Hugo 之旅》。我这边提供一下我的搜索结果页模板。 {{/* layouts/_default/search.html */}} {{ define "main" }} <div class="w-full max-w-screen-xl lg:px-4 xl:px-8 mx-auto"> <article class="mx-6 my-8"> <h1 id="search-count" class="font-bold text-3xl text-primary-text"></h1> </article> <div id="search-result" class="bg-secondary-bg rounded overflow-hidden px-4 divide-y"> </div> </div> <script src="https://cdnjs.cloudflare.com/ajax/libs/fuse.js/3.2.0/fuse.min.js"></script> <script> document.addEventListener('DOMContentLoaded', async () => { const qs = new URLSearchParams(location.search); const searchResult = document.querySelector('#search-result'); const searchCount = document.querySelector('#search-count'); const fuseOptions = { shouldSort: true, includeMatches: true, threshold: 0.0, tokenize: true, location: 0, distance: 100, maxPatternLength: 32, minMatchCharLength: 1, keys: [{ name: "title", weight: 0.8 }, { name: "summary", weight: 0.5 }, { name: "tags", weight: 0.3 }, { name: "date", weight: 0.3 }, ] }; let fuse = null async function getFuse() { if (fuse == null) { const resp = await fetch('/index.json', { method: 'get' }) const indexData = await resp.json() fuse = new Fuse(indexData, fuseOptions); } return fuse } function render(items) { console.log(items); return items.map(item => { item = item.item return ` <div class="px-2 py-6"> <div class="flex flex-col-reverse lg:flex-row justify-between"> <div class="w-full lg:w-2/3"> <div class="my-2"> <div class="mb-4"> <a href="${item.permalink}" class="font-bold text-xl hover:text-eureka">${item.title}</a> </div> <div class="content"> ${item.summary}<p class="more"> <a href="${item.permalink}" title="${item.title}">阅读全文<span class="meta-nav">→</span></a> </p> </div> </div> <div class="flex flex-wrap flex-row items-center my-2 text-tertiary-text"> <div class="mr-6 my-2"> <i class="fas fa-calendar mr-1"></i> <span>${item.date}</span> </div> <div class="mr-6 my-2"> <a href="${item.permalink}#waline-comments" title="${item.title}"> <i class="fas fa-comment mr-1"></i> <span>${item.comments > 0 ? item.comments + ' 条评论' : '暂无评论'}</span> </a> </div> <div class="mr-6 my-2"> <i class="fas fa-clock mr-1"></i> <span>${item.time}分钟阅读时长</span> </div> </div> </div> <div class="w-full lg:w-1/3 mb-4 lg:mb-0 lg:ml-8"> ${item.featuredImage ? `<img src="${item.featuredImage}" class="w-full" alt="Featured Image">` : ''}</div> </div> </div>`; }).join(''); } function updateDOM(html, keyword, number) { document.title = document.title.replace(/包含关键词.*?文章/, `包含关键词 ${keyword}的文章`) searchResult.innerHTML = html searchCount.innerHTML = `共查询到 ${number}篇文章` } async function search(searchString) { console.log(searchString); let result = []; if(searchString) { const fuse = await getFuse() result = fuse.search(searchString) } const html = render(result) updateDOM(html, searchString, result.length) } document.querySelectorAll('input[name="q"]').forEach(el => el.value = qs.get('q')); search(qs.get('q') || '') window.blogSearch = function(keyword) { if(!keyword) { return; } history.pushState('', '', location.pathname + '?q=' + encodeURIComponent(keyword)); document.querySelectorAll('input[name="q"]').forEach(el => el.value = keyword); search(keyword); } }) </script> {{ end }} 归档 之前使用 Typecho 的时候有一个归档插件会按照年月列表展示文章,所以我在 Hugo 中按照之前的格式实现了一下。按照如下内容新建 layouts/_default/archive.html 文件,并新建文章 content/日志.md,文章内容为空即可,在文章的 meta 数据中指定 layout: archive 来映射到该模板。 {{/* layouts/_default/archive.html */}} {{ define "main" }} {{ $hasToc := and (in .TableOfContents "<li>" ) (.Params.toc) }} {{ $hasSidebar := or ($hasToc) (.Params.series) }} <div class="grid grid-cols-2 lg:grid-cols-8 gap-4 lg:pt-12"> <div class="col-span-2 {{ if not $hasSidebar }} {{- print "lg:col-start-2" -}} {{ end }} lg:col-span-6 bg-secondary-bg rounded px-6 py-8"> <h1 class="font-bold text-3xl text-primary-text">{{ .Title }}</h1> <div class="flex flex-wrap flex-row items-center my-2 text-tertiary-text"> <div class="mr-6 my-2"> <i class="fas fa-calendar mr-1"></i> <span>{{ .Date.Format (.Site.Params.dateFormat | default "2006-01-02") }}</span> </div> </div> {{ $featured := partial "utils/get-featured" . }} {{ with $featured }} <div class="my-4"> {{ . }} </div> {{ end }} <div class="content"> <script type='text/javascript' src="https://lib.baomitu.com/jquery/1.11.1/jquery.min.js"></script> <style type="text/css">.car-collapse .car-yearmonth { cursor: s-resize; } </style> <script type="text/javascript"> /* <![CDATA[ */ jQuery(document).ready(function() { jQuery('.car-collapse').find('.car-monthlisting').hide(); jQuery('.car-collapse').find('.car-monthlisting:first').show(); jQuery('.car-collapse').find('.car-yearmonth').click(function() { jQuery(this).next('ul').slideToggle('fast'); }); jQuery('.car-collapse').find('.car-toggler').click(function() { if ( '展开全部' == jQuery(this).text() ) { jQuery(this).parent('.car-container').find('.car-monthlisting').show(); jQuery(this).text('折叠全部'); } else { jQuery(this).parent('.car-container').find('.car-monthlisting').hide(); jQuery(this).text('展开全部'); } return false; }); }); /* ]]> */ </script> <div class="car-container car-collapse"> <a href="#" class="car-toggler">展开全部</a> <ul class="car-list"> {{ range (.Site.RegularPages.GroupByDate "01月 2006") }} <li> <span class="car-yearmonth">{{ .Key }} <span title="Post Count">({{ len .Pages }})</span></span> <ul class="car-monthlisting"> {{ range .Pages }} <li> {{ .Date.Format "02"}}: <a href="{{ .Permalink }}">{{ .Title }} </a> <!--<span title="Comment Count">(0)</span>--> </li> {{ end }} </ul> </li> {{ end }} </ul> </div> </div> </div> </div> {{ end }} 统计 屈屈的博客中还有一个统计页面,我觉得挺有意思的,于是也在我的博客中复刻了一下。按照如下内容新建 layouts/_default/stats.html 文件,并新建文章 content/统计.md,文章内容为空即可,在文章的 meta 数据中指定 layout: stats 来映射到该模板。 {{/* layouts/_default/stats.html */}} {{ define "main" }} {{- $.Scratch.Add "stats" slice -}} {{- range .Site.RegularPages -}} {{- $.Scratch.Add "stats" (dict "title" .Title "slug" .Slug "year" (.Date.Format "2006") "month" (.Date.Format "2006-01") "hour" (.Date.Format "15") "week" (.Date.Format "Monday") "count" .WordCount) -}} {{- end -}} {{ $hasToc := and (in .TableOfContents "<li>" ) (.Params.toc) }} {{ $hasSidebar := or ($hasToc) (.Params.series) }} <style> .chart { margin-top: 15px; width: 100%; height: 350px; } </style> <div class="grid grid-cols-2 lg:grid-cols-8 gap-4 lg:pt-12"> <div class="col-span-2 {{ if not $hasSidebar }} {{- print "lg:col-start-2" -}} {{ end }} lg:col-span-6 bg-secondary-bg rounded px-6 py-8"> <h1 class="font-bold text-3xl text-primary-text">{{ .Title }}</h1> <div class="flex flex-wrap flex-row items-center my-2 text-tertiary-text"> <div class="mr-6 my-2"> <i class="fas fa-calendar mr-1"></i> <span>{{ .Date.Format (.Site.Params.dateFormat | default "2006-01-02") }}</span> </div> </div> {{ $featured := partial "utils/get-featured" . }} {{ with $featured }} <div class="my-4"> {{ . }} </div> {{ end }} <div class="content"> {{ .Content }} </div> </div> {{ if $hasSidebar}} <div class="col-span-2"> {{ if .GetTerms "series" }} {{ partial "components/post-series.html" . }} {{ end }} {{ if $hasToc }} {{ partial "components/post-toc.html" . }} {{ end }} </div> {{ end }} </div> <script src="https://lib.baomitu.com/echarts/5.0.0/echarts.min.js"></script> <script> const data = {{- $.Scratch.Get "stats" -}}; function showChart(id, title, type, d) { var chart = echarts.init(document.getElementById(id)); var xData = []; var yData = []; d.forEach(function(item) { xData.push(item[0]); yData.push(item[1]); }); var option = { title : { text : title }, tooltip : { trigger : 'axis' }, xAxis : [ { type : 'category', data : xData } ], yAxis : [ { type : 'value' } ], grid : { x : 35, y : 45, x2 : 35, y2 : 35 }, series : [ { type : 'bar', name : type, data : yData, markLine : { data : [ { type : 'average', name : '平均值' }], itemStyle : { normal : { color : '#4087bd' } } }, itemStyle : { normal : { color : '#87cefa' } } }] }; chart.setOption(option); } window.addEventListener('load', function() { basicInfo(); yearStats(); monthStats(); hourStats(); weekStats(); }); function basicInfo() { const articles = {{ len (where .Site.RegularPages "Section" "posts") }}; const pages = data.length - articles; const comments = data.reduce((count, article) => count + article.comments, 0); const words = data.reduce((count, article) => count + article.count, 0); document.querySelector('#basic-info').innerHTML = ` <span>文章:<strong><a href="/">${articles}</a></strong> 篇</span>;<span>页面:<strong><a href="/">${pages}</a></strong> 篇</span>;<span>总字数:<strong>${words}</strong></span>; `; }; function yearStats() { const yearGroup = {}; data.forEach(article => { const year = parseInt(article.year); if(!yearGroup.hasOwnProperty(year)) { yearGroup[year] = 0; } yearGroup[year] += 1; }); const d = []; for(let i = 2009; i <= (new Date().getFullYear()); i++) { d.push([i, yearGroup[i] || 0]); } showChart('year-stat', '文章数 - 按年统计', '文章数', d); } function monthStats() { const monthGroup = {}; data.forEach(article => { if(!monthGroup.hasOwnProperty(article.month)) { monthGroup[article.month] = 0; } monthGroup[article.month] += 1; }); const d = []; for(let year = 2009; year <= (new Date().getFullYear()); year++) { for(let month = 1; month < 13; month++) { const text = `${year}-${month < 10 ? '0' + month : month}`; d.push([text, monthGroup[text] || 0]); } } showChart('month-stat', '文章数 - 按月统计', '文章数', d); } function hourStats() { const hourGroup = {}; data.forEach(article => { const hour = parseInt(article.hour); if(!hourGroup.hasOwnProperty(hour)) { hourGroup[hour] = 0; } hourGroup[hour] += 1; }); const d = [ ['00:00-01:00'], ['01:00-02:00'], ['02:00-03:00'], ['03:00-04:00'], ['04:00-05:00'], ['05:00-06:00'], ['06:00-07:00'], ['07:00-08:00'], ['08:00-09:00'], ['09:00-10:00'], ['10:00-11:00'], ['11:00-12:00'], ['12:00-13:00'], ['13:00-14:00'], ['14:00-15:00'], ['15:00-16:00'], ['16:00-17:00'], ['17:00-18:00'], ['18:00-19:00'], ['19:00-20:00'], ['20:00-21:00'], ['21:00-22:00'], ['22:00-23:00'], ['23:00-24:00'] ].map((item, key) => { item[1] = hourGroup[key] || 0; return item; }); showChart('hour-stat', '文章数 - 按时段统计', '文章数', d); } function weekStats() { const weekGroup = {}; data.forEach(article => { if(!weekGroup.hasOwnProperty(article.week)) { weekGroup[article.week] = 0; } weekGroup[article.week] += 1; }); const d = [ ['星期一', weekGroup.Monday], ['星期二', weekGroup.Tuesday], ['星期三', weekGroup.Wednesday], ['星期四', weekGroup.Thursday], ['星期五', weekGroup.Friday], ['星期六', weekGroup.Saturday], ['星期日', weekGroup.Sunday] ]; showChart('weekday-stat', '文章数 - 按星期几统计', '文章数', d); } </script> {{ end }} 其它 除了以上这些,我的博客中改动最大的当属评论这块,但这块定制型比较高,一般玩家就不推荐了,感兴趣的还是去看我之前的《静态博客如何高性能插入评论》一文。除此之外,我还修改了 footer.html 修改了底部显示文案,增加了网页统计脚本;基于自研的 wxhermit 增加了微信分享自定义相关功能;文章页目录底部增加了个人公众号的展示。由于这些内容都比较简单且定制化内容程度比较高,就不一一展示了,感兴趣的朋友可以自行查看源码查阅。
  •  

一道面试题让你更加了解事件队列

今天在群里聊天,突然有人放出了一道面试题。经过群里一番讨论,最终解题思路慢慢完善起来,我这里就整理一下群内解题的思路。 该题定义了一个同步函数对传入的数组进行遍历乘二操作,同时每执行一次就会给 executeCount 累加。最终我们需要实现一个 batcher 函数,使用其对该同步函数包装后,实现每次调用依旧返回预期的二倍结果,同时还需要保证 executeCount 执行次数为1。 let executeCount = 0 const fn = nums => { executeCount++ return nums.map(x => x * 2) } const batcher = f => { // todo 实现 batcher 函数 } const batchedFn = batcher(fn); const main = async () => { const [r1, r2, r3] = await Promise.all([ batchedFn([1,2,3]), batchedFn([4,5]), batchedFn([7,8,9]) ]); //满足以下 test case assert(r1).tobe([2, 4, 6]) assert(r2).tobe([8, 10]) assert(r3).tobe([14, 16, 18]) assert(executeCount).tobe(1) } 抖机灵解法 拿到题目的第一时间,我就想到了抖机灵的方法。直接面向用例编程,执行完之后重置下 executeCount 就好了。 const batcher = f => { return nums => { try { return f(nums) } finally { executeCount = 1 } } } 当然除非你不在乎这次面试,否则一般不建议你用这种抖机灵的方法回答面试官(不要问我为什么知道)。由于 executeCount 的值和 fn() 函数的调用次数呈正相关,所以这道理也就换成了我们需要实现 batcher() 方法返回新的包装函数,该函数会被调用多次,但最终只会执行一次 fn() 函数。 setTimeout 解法 由于题干中使用了 Promise.all(),我们自然而然想到使用异步去解决。也就是每次调用的时候会把所以的传参存下来,直到最后的时候再执行 fn() 返回对应的结果。问题在于什么时候触发开始执行呢?自然而然我们想到了类似 debounce 的方式使用 setTimeout 增加延迟时间。 const batcher = f => { let nums = []; const p = new Promise(resolve => setTimeout(_ => resolve(f(nums)), 100)); return arr => { let start = nums.length; nums = nums.concat(arr); let end = nums.length; return p.then(ret => ret.slice(start, end)); }; }; 这里的难点在于预先定义了一个 Promise 在 100ms 之后才会 resolve。返回的函数本质只是将参数推入到 nums 数组中,待 100ms 后触发 resolve 返回统一执行 fn() 后的结果并获取对应于当前调用的结果片段。 后来有群友反馈,实际上不用定义 100ms 直接 0ms 也是可以的。由于 setTimeout 是在 UI 渲染结束之后才会执行的宏任务,所以理论上来说 setTimeout() 的最小间隔值无法设置为 0。它的最小值和浏览器的刷新频率有关系,根据 MDN 描述,它的最小值一般为 4ms。所以理论上它设置 0ms 和 100ms 效果是差不多的,都类似于 debounce 的效果。 Promise 解法 那么如何能实现延迟 0ms 执行呢?我们知道除了宏任务之外 JS 还有微任务,微任务队列是在 JS 主线程执行完成之后立即执行的事件队列。Promise 的回调就会存储在微任务队列中。于是我们将 setTimeout 修改成了 Promise.resolve(),最终发现也是可以实现同样的效果。 const batcher = f => { let nums = []; const p = Promise.resolve().then(_ => f(nums)); return arr => { let start = nums.length; nums = nums.concat(arr); let end = nums.length; return p.then(ret => ret.slice(start, end)); }; }; 由于 Promise 的微任务队列效果将 _ => f(nums) 推入微任务队列,待主线程的三次 batcherFn() 调用都执行完成之后才会执行。之后 p 的状态变为 fulfilled 后继续完成最终 slice 的操作。 **2020-03-17:**感谢 @kricsleo 帮忙指出由于存在副作用多次调用会存在问题,并提供了优化版本。 const batcher = (f) => { let nums = []; let p; return (nums) => { if(!p) { p = Promise.resolve().then(_ => f(nums)); } const start = nums.length; nums = nums.concat(arr); const end = nums.length; return p.then(ret => { nums = []; p = null; return ret.slice(start, end); }); }; }; 后记 最终分析下来,其实这道理的本质就是要通过某些方法将 fn() 函数的执行后置到主线程执行完毕,至于是使用宏任务还是微任务队列,就看具体的需求了。除了 setTimeout() 之外,还有 setInterval(), requestAnimationFrame() 都是宏任务队列。而微任务队列里除了有 Promise 之外,还有 MutationObserver。关于宏任务和微任务队列相关的,感兴趣的可以看看《微任务、宏任务与Event-Loop》这篇文章。
  •  

Maverick 的命运

我在 2019 年 12 月自己写了一个静态博客生成器,名叫 Maverick,使用它实现了我的个人博客完全自主化,一度是我最自豪的作品之一。但它现在久缺维护,似乎已经走到了生命的尽头。

想来这事很有意思。2016 年前后我开始接触个人博客,从公开的服务(简书,Medium,etc.)到自托管的博客程序(WordPress,Typecho,Hexo,etc.)统统玩了一个遍,最终停留在 Typecho 上,输出了一些内容,也写了一两个还算过得去也有人用的模板和若干插件。到 2019 年达到了一个小高潮:终于决定要自己写一个静态博客程序。

那时候我刚大四毕业,整天有无穷的精力和时间探索学习。和很多后来被证明是伟大创新的项目一样,我从自己的观察总结出发,写了面向自己需求的 Maverick。我承认,不是没有一瞬间,我希望这个项目被很多人用上,因为它真的解决了我许多需求:比如编写博客时图片引用的问题,比如对许多有用功能(RSS,搜索,图片排版等)的原生实现。事实上我也确实用它作为我的博客引擎直到今年。当 Maverick 第一版问世时,我心想:

写个静态博客生成器也不是那么难嘛。

现在再思考这句话,技术上依然是没问题的。一个静态博客生成器要做的无非是解析本地文件,生成对应的 HTML 网页罢了。可是,想写一个「大家都能用的」工具,其难度非凡。

虽然我时常对友人自夸说,Maverick 用起来可太舒服了,我觉得我是天才,只可惜没人用。后来明白,我至少搞错了两件事情。

第一,我混淆了「自己需要」和「大家需要」这两件事情。这便是技术人士想做产品的主要陷阱之一。基本功能(模板,插件)支持不全面,但却在一些偏门功能上过度设计,这让 Maverick 并算不上合格的工具。不过,对于一个非科班、没什么经验的学生来说,这些方面的问题是可以原谅,并且也是可以改进的。

第二,我沉迷在「发布」的快感中,却没有做到长期稳定的迭代与维护。毕竟一个所谓的开源项目,「发布」意味着 star,意味着热闹的评论区,意味着大量的夸赞,最重要的,意味着令人愉快的成就感;而「维护」意味着什么呢?意味着枯燥地修一个又一个的 bug,意味着没有任何背景知识的用户不着边际的反馈,甚至意味着一些没来由的批评与攻击。

上文中第二点更致命。因为它可以扩展到方方面面,其实说的就是做工作是否有长性,是否扎实。


现在回头看 Maverick 的代码设计,自然是非常糟糕。但我不会觉得羞愧,因为那就是当时的我能做到的最好。但人在进步,Maverick 却还是小孩子的模样,这是我觉得惭愧的地方。现在我忙于工作,更没有时间去维护 Maverick。我大概要归档这个项目了,让它作为我成长过程中的历史资料吧。

上周末花了些时间,重新写了这个博客的代码,业已上线。这一次一切都变得更加简约,更加面向自己,并且暂时也没有大张旗鼓放出来供大家把玩的意向。我已经认识到,开源不只是开源而已,还有面向用户与社区的责任。这个责任心不是被强加的,而是我认为一个有开源志向的作者应当具备的。

在这里 shout out to 那些多年坚持维护、更新,或成为整个数字世界基石、或成为每个长情用户心头好的项目:刚过完 30 岁生日的 VIM,可能是被侵权最多的项目 FFmpeg,最近又开始更新的 Typecho... 数不胜数,这个世界因为这些项目变得更好,hopefully,会持续变好。

  •  

不用备案也能支持微信自定义分享

我们知道,在微信中打开网页,使用右上角的 ... 分享给朋友/朋友圈,是可以使用 JS SDK 自定义分享卡片文案的。为了让分享内容能够更好的受到监管,从早期会自动读取网页内第一张大图到后期使用 JS SDK 自定义分享,再到后期需要做域名绑定关联,自定义分享卡片内容的流程变的越来越复杂。 目前如果你的网站想要增加微信自定义分享文案的支持,需要准备以下两件事情: 确保你的网站域名已备案,并被添加到了一个已认证的公众号的“JS安全域名”中。 提供服务端支持,用于与微信交互获取 access token 和 jsapi ticket 并计算获得最终的 signature,用于在前端调用微信 JS SDK 时进行校验。 具体的流程可以参见微信开放文档。可以看到要想实现自定义分享文案,除了开发流程之外,你还需要域名备案和公众号认证,这两个做过的人肯定知道会有多头疼了。而最蛋疼的是,我的博客域名后缀 .org 目前是不支持备案的,难道就没有办法了吗? wxhermit 为了能让未备案网站也能自定义分享文案,我开发了 wxhermit 这个项目。它的原理非常简单,基本就是在已备案域名下 <iframe> 嵌套展示未备案域名,并通过 postMessage 通信,将自定义分享的文案传递到父页面。最终实现了任意网站分享自定义的需求。 当然它的本质还是使用基于最开始的备案域名网站进行分享。由于微信分享需要提供已认证公众号并绑定已备案域名,一个已认证公众号只能绑定至多 5 个安全域名,条件颇为苛刻。针对 5 个以上的域名,部分域名无法备案的情况,要自定义微信分享的文案就非常麻烦。本方案比较好的迂回解决该问题。 如何使用 在服务端使用 Docker 启动服务。其中 WECHAT_ID 和 WECHAT_SECRET 是在微信公众号后台开发-基本配置中获取的“开发者ID”和“开发者密码”。而 ALLOW_HOST_LIST 是为了避免服务被滥用,允许开发者配置允许使用内嵌服务的网站。可以使用逗号拼接多个域名,例如 imnerd.org,eming.li。不在该列表中的域名会直接跳转会源地址。 docker run \ -e WECHAT_ID=<WECHAT_ID> \ -e WECHAT_SECRET=<WECHAT_SECRET> \ -e ALLOW_HOST_LIST=<ALLOW_HOST_LIST> \ -p 8360:8360 lizheming/wxhermit 而对于需要使用该服务的网站,需要在页面中增加以下代码用于自定义分享文案。其中 wxhermit 是固定值,其它的为自定义文案内容。 <script> if (window.parent !== window) { window.parent.postMessage({ type: 'wxhermit', title: '自定义分享的标题', desc: '自定义分享的描述', imgUrl: '自定义分享的封面图' }, '*'); } </script> 配置好后就可以在微信使用 <domain>/?url=<url> 来访问了,其中 <domain> 是你的已绑定的安全域名,<url> 则是在 ALLOW_HOST_LIST 中配置的可使用域名下的网址。 后记 通过代理的形式很好的解决了我未备案域名需要自定义分享的问题。为了能让体验更自然,我在我的网站中增加了在微信中自动跳转至该嵌套页面的逻辑。 <script> if(/micromessenger/i.test(navigator.userAgent) && window.parent === window) { location.href = 'https://wechat.75.team/?url=' + encodeURIComponent(location.href); } </script> 不过它的缺点也很明显,本质相当于将所有的域名都挂靠在某个安全域名之下。所以在微信下拉显示网站地址的时候都还是显示该安全域名。而且子域如果出现内容问题的话风险也全部在该安全域名上,所以建议是 ALLOW_HOST_LIST 配置个人可控域名。 除了我的这种方案之外,也有配置 <meta> 信息通过 Safari 调用系统的分享功能设置封面图和文案的方式,以及通过 QQ 浏览器分享自动获取页面第一张大图的形式自定义分享卡片。不过它们在可定制和确定性上都要稍微弱一些,可以根据实际情况选择使用。
  •  

pyeemd安装教程--win10 wsl + ubuntu

✇遐说
作者Dorad

想在 python 环境下调用 pyeemd, 进行 ceemd 分解。

调研发现 pyeemd 仅是 libeemd 项目的 python 封装, 而且是采用动态链接库的形式调用的。而该项目依赖 GSL 科学计算库,且 libeemd 目前只适配了 linux 环境,故选择使用 WSL 进行安装。

  •  

再见了,所有的 Evangelion

惭愧,半年没更新了。一方面是因为生活逐渐丰富起来,很难有学生时代那样大块的空闲时间用来思考和行文;另一方面也是自己觉得「可发表」的阈值有所变高。前者自然是无可奈何,后者则并不一定是好事。事实上后者正是博客这个形态的一个弊端:这样以文章为单位的媒介会自然而然地让作者产生畏惧。我决定把这个畏惧暂时放在脑后,摘录一些发表在微博或推特上的只言片语过来(真的不是凑数的借口😝)。毕竟文本长短都是表达。

生活切片

2021.08.14。EVA 最终篇终于有了可供观赏的资源,立刻看了。第一感是:唔,终于结束了。从高中时代至今我等这部最终篇已经等了太多年,自己也从十几岁的少年等成了社畜。从完结篇的角度看,《终》为整个系列画上了很圆满的句号,整部片子的告别意味很浓,大量用了老镜头,最后与每一位角色说再见,甚至是闪过了 TV 版所有标题作为告别,真让人怀念。电影终于交代了一切,从开始的动机到最终的结局。

或许是庵野真的厌倦了做这个系列,他从旧剧场版开始就一直在告诉观众的是,向前走吧,活在现实中吧,活在当下和未来吧,活在没有 EVA 的世界里吧。这部电影是与角色们,与庵野自己,与所有观众的最后一次促膝长谈。再见了,所有的 Evangelion。


2021.08.12。最近的幸福莫过于下班后和🐦一起吃宵夜看这部纪录片。那时候屋子里乱乱的,电视直接放在地上。我们横七竖八地趴在地毯上,一边看一边聊一边笑。都觉得二手书,绘本,还有阅读本身都是很棒很棒的东西。


2021.07.29。25岁这一年自己和周遭似乎都经历了天翻地覆的变化,但仔细想想,又似乎一切如旧。但有一点是非常确定的:我正在经历我最好的一段时光。自身的状态不错,也很幸运地有人陪伴身边。与十年前的我相比,最大的收获莫过于逐渐领悟了活在当下、珍惜身边人的重要性,就还蛮开心的。


2021.07.25。回形针,滴滴,再加上这两天网暴失利奥运选手和绑架国产品牌捐款的事件,所谓事不过三,这已经短期内四次让我见识到网民的暴徒本质。

不论是极端的「爱国」还是极端的「恨国」,极端的右还是极端的左,他们都是同一群人,有同样的丑陋内核。无法开放思想、换位思考的人永远只能陷入自己的世界,输出给外界的只能是肮脏可怖的东西。


2021.07.23。You are what you read 确有其事,如今刷微博也得列入「read」范畴,吞垃圾的风险颇高。故还是建议多摄入些出版物,毕竟两种媒介都是垃圾的可能性要小些。


2021.05.07。我从来不觉得外婆会离去。我还小时她牵着我去河边捡石头,到我长大,她还是在给我做不变的、好吃的卤肉,她好像一直这么老,一直有一双暖和的大大的手,自然也会永远地,在这世上等我回家,等我去看她。

去年和今年外婆两次病重我恰好都在家,两次我都把她抬上救护车,但两次都挺了过来。我越发觉得,老太太生命力真是顽强,应该还能在世上好久吧。

但是她越来越瘦,越来越虚弱了。五一假期我回到康定,想去看看外婆住过的老房子,但老房子已不在。我只好站在老房子的位置,呆呆地看着对面不变的山。小时候我就趴在老房子的窗前看着这座山,外婆则会给我端来撒了白糖的油炸土豆丁。

到新都桥的酒店后我终于情绪失控,忍不住哭了出来。女朋友手足无措,只好默默地抱着我。

今天父亲来电说外婆刚离世了,我坐在工位上发了好久的呆,然后收拾东西回家。路上我想起几天前回家带女朋友去看外婆,那时她已十分虚弱,神志不清。

“阿婆!看,这是小江!”

外婆慢慢地握住女朋友的手,“哦小江啊!小江身体好哇!”

外婆的旅程到此就是终点,但世界延续,生生不息。

精神食粮

  • 《诗翁彼豆故事集》《神奇的魁地奇球》《神奇动物在哪里》三本书向麻瓜们介绍了巫师世界的一些有趣知识,尽管其中一些在我们巫师看来是常识了。邓布利多的批注超有趣就是了!
  • 《从一到无穷大》,花了五六个晚上读完了这本奇书。关于空间与宇宙的两章实在是给我很大的震撼。一本 1947 年的科普书在 2021 年还能给一个勉强算受过现代高等教育的普通青年如此大的触动,内容还不乏最先进、发展最快的科学领域,不得不说真是一个奇迹。虽然书中一些事实数据已经与现在的公认结论有所出入,但人类对基础科学的孜孜以求本身就有无穷的美感。
  • 《石黑一雄诺贝尔奖获奖演说》,最近一两年很迷石黑一雄,也是多亏了诺贝尔奖,否则我都不知道这人。
  • 《大萝卜和难挑的鳄梨》,《爱吃沙拉的狮子》,周末独自在家适合读村上碎碎念,村上的絮叨也挺好玩的!
  • 《金银岛》,让我想起了一大堆加勒比海盗的情节,海盗、叛变、寻宝、荒岛这些元素真是一个比一个有吸引力。
  • 《山本耀司:我投下一枚炸弹》,说实话没咋看明白。
  • 《美国众神》,可能读得太草率了 主线明白了 支线没理清。我还是适合哈利波特一点。
  • 《克拉拉与太阳》,本来读完觉得是一本很温柔的书,但看了豆瓣热评我有点不确定了…
  • 《弃猫》,好短的一篇文章,村上和他父亲的关系好像也引起了一些我的共鸣。
  • 《离线·共生》,终于收到了离线的新刊,很激动,毕竟是我的科技文艺白月光。
  • 《失落的卫星》,中亚大地跃然纸上!不过土库曼斯坦的一篇确实让我有点惊讶
  • 《千万别用Futura》,倒是不知道 Futura 背后有这么故事……
  • 《一个叫欧维的男人决定去死》,此前是知道电影,这次是读了原著。确实是十分暖心,舒服的故事。读的时候是初春,适合配上一杯热红茶,听这首歌
  • 《潮骚》。一本好读的小书,大概花了一个半小时读完,很沉浸的体验。可能也是因为今天的背景音乐总是与故事合拍。平时因为想看的书太多,许多书我都一目十行;但这本书让我不得不逐字逐句地读,细细体会。里面种种描绘实在是太细腻了……伴着窗外的风雨,我好像也身处这个质朴纯粹的歌岛。好喜欢这种能让我感受到辽阔开朗和生命力的作品❤️。

好,以上是一些碎碎念,和最近读的一些书。那么,周末愉快,下次再见。

  •  

咸九高速-幕阜山扶贫攻坚重点项目

✇遐说
作者Dorad
咸九高速,全称为咸宁(通山)至九江(武宁)高速公路。该项目起于县南林桥镇南林桥枢纽互通立交,接已建成的咸通高速公路和G56杭瑞高速公路通山段,经杨芳林乡、厦铺镇、闯王镇、九管会,止于九宫山二号隧道与通武高速公路江西段相接,路线全长约64.7公里。其咸宁段为46.7km,江西段约18km,项目总投资599034万元。该项目采用四车道高速公路建设标准建设,设计速度100公里/小时,路基宽度26米。全线设置南林桥互通、厦铺互通、九宫山互通等3处互通立交,估算总造价超百亿元,建设期为48个月。已于2021年1月开工建设,预计通车时间为2024年。
  •  

SameSite 那些事

在《Web 安全漏洞之 CSRF》中我们了解到,CSRF 的本质实际上是利用了 Cookie 会自动在请求中携带的特性,诱使用户在第三方站点发起请求的行为。除了文中说的一些解决方式之外,标准还专门为 Cookie 增加了 SameSite 属性,用来规避该问题。Chrome 于 2015 年 6 月支持了该属性,Firefox 和 Safari 紧随其后也增加了支持。SameSite 属性有以下几个值: SameSite=None:无论是否跨站都会发送 Cookie SameSite=Lax:允许部分第三方请求携带 Cookie SameSite=Strict:仅允许同站请求携带 Cookie,即当前网页 URL 与请求目标 URL 完全一致 该属性适合所有在网页下的请求,包括但不限于网页中的 JS 脚本、图片、iframe、接口等页面内的请求。可以看到 None 是最宽松的,和之前的行为无异。而 Lax 和 Strict 都针对跨站的情况下做了限制。其中 Strict 最为严格,不允许任何跨站情况下携带该 Cookie。Lax 则相对宽松一点,允许了一些显式跳转后的 GET 行为携带。以下是一个带有 SameSite 属性的标准 Cookie 响应示例: Set-Cookie: name=lizheming; SameSite=None; Secure 需要注意的是,浏览器做了仅针对 HTTPS 域名才支持 SameSite=None 配置。所以如果你要设置 SameSite=None 的话,则必须还要携带 Secure 属性才行。 Same Site Same Site 直译过来就是同站,它和我们之前说的同域 Same Origin 是不同的。两者的区别主要在于判断的标准是不一样的。一个 URL 主要有以下几个部分组成: 可以看到同域的判断比较严格,需要 protocol, hostname, port 三部分完全一致。相对而言,Cookie 中的同站判断就比较宽松,主要是根据 Mozilla 维护的公共后缀表(Pulic Suffix List)使用有效顶级域名(eTLD)+1的规则查找得到的一级域名是否相同来判断是否是同站请求。 例如 .org 是在 PSL 中记录的有效顶级域名,imnerd.org 则是一级域名。所以 https://blog.imnerd.org 和 https://www.imnerd.org 是同站域名。而 .github.io 也是在 PSL 中记录的有效顶级域名,所以 https://lizheming.github.io 和 https://blog.github.io 得到的一级域名是不一样的,他们两个是跨域请求。 在类似 GitHub/GitLab Pages, Netlify, Vercel 这种提供子域名给用户建站的第三方服务中,eTLD 的这种同站判断特性往往非常有用。通过将原本是一级域的域名添加到 eTLD 列表中,从而让浏览器认为配有用户名的完整域名才是一级域,有效解决了不同用户站点的 Cookie 共享的问题。 eTLD eTLD 的全称是 effective Top-Level Domain,它与我们往常理解的 Top-Level Domain 顶级域名有所区别。eTLD 记录在之前提到的 PSL 文件中。而 TLD 也有一个记录的列表,那就是 Root Zone Database。RZD 中记录了所有的根域列表,其中不乏一些奇奇怪怪五花八门的后缀。 eTLD 的出现主要是为了解决 .com.cn, .com.hk, .co.jp 这种看起来像是一级域名的但其实需要作为顶级域名存在的场景。这里还可以分享一个有趣的事情,2020年5月份出现了一起阿里云所有 ac.cn 后缀网站解析全部挂掉的事件。原因就是 ac.cn 是中科院申请在册的 eTLD 域名。而阿里云的检测域名备案的脚本不了解规范,没有使用 PSL 列表去查找一级域名,而是使用了.分割的形式去查找的。最终所有 *.ac.cn 的域名由于 ac.cn 这个域名没有进行备案导致解析全部挂掉。而我们现在知道 ac.cn 这个域名是 eTLD 域名,它肯定是无法备案的。 Schemeful Same Site 在 Chrome 86/Firefox 79 中,浏览器增加了一个 Schemeful Same Site 的选项,将协议也增加到了 Same Site 的判断规则中。但是并不是完全的不等判断,可以理解是否有 SSL 的区别。例如 http:// 和 https:// 跨站,但 wss:// 和 https:// 则是同站,ws:// 和 http:/ 也算是同站。 Chrome 可以浏览器输入 chrome://flags/#schemeful-same-site 找到配置并开启。 Lax 我们知道互联网广告通过在固定域 Cookie 下标记用户 ID,记录用户的行为从何达到精准推荐的目的。随着全球隐私问题的整治,同时也是为了更好的规避 CSRF 问题,在 Chrome 80 中浏览器将默认的 SameSite 规则从 SameSite=None 修改为 SameSite=Lax。设置成 SameSite=Lax 之后页面内所有跨站情况下的资源请求都不会携带 Cookie。由于不会为跨站请求携带 Cookie,所以 CSRF 的跨站攻击也无从谈起,广告商也无法固定用户的 ID 来记录行为。 对用户来说这肯定是一件好事。但是对我们技术同学来说,这无疑是上游给我们设置的一个障碍。因为业务也确实会存在着多个域名的情况,并且需要在这些域名中进行 Cookie 传递。例如多站点使用 SSO 登录、接入统一的验证码服务、前端和服务端接口属于两个域名等等情况,都会因为这个修改受到影响。 这个修改影响面广泛,需要网站维护者花大量的时间去修改适配。而 Chrome 80 于 2020 年 2 月发布后全球就开始面临新冠疫情的影响。考虑到疫情问题后续的版本里又暂时先回退了这个特性(相关链接),最终是在 Chrome 86 进行了全量操作。 针对因为此次特性受到影响的网站,可以选择以下一些适配办法: 使用 JWT 等其它非 Cookie 的通信方式 为 Cookie 增加 SameSite=None;Secure 属性配置 所有的跨域接口增加 Nginx 代理,使其和页面保持同域 每一种方法都需要一些取舍。第一种更换 Cookie 的方式改造成本非常高,特别是在有外部业务对接的情况下基本不可能。第三种方式通过将跨域变为同域的转发方式可能会带来线上流量的成倍增加,也是需要考虑的因素。第二种设置成 None 看起来是比较简单的办法,不过也有着诸多的限制。 SameSite=None;Secure 由于仅支持 HTTPS 页面,所以如果有 HTTP 的场景需要考虑跳转至 HTTPS 或者选择其他方案; 由于 SameSite 属性是后来才加入的,一些老浏览器(其实就是 IE)会忽略带有这些属性的 Cookie,所以需要同时下发未配置 SameSite 属性和配置 SameSite 属性的两条 Set-Cookie 响应头,这样支持和不支持的会各取所需; 在 Safari 的某些版本中会将 SamteSite=None 等同于 SameSite=Strict 所以部分 Safari 场景需要特殊处理不进行下发(相关链接); 综上使用代理转发的方式是我比较推荐的方式,除了不那么绿色之外兼容问题处理还是不错的。 SameParty SameSite=Lax 断了我们跨站传递 Cookie 的念想,但实际业务上确实有这种场景。例如 Google 自己就有非常多的域名,这些域名如果都需要共享登录 Cookie 的话可能就会非常困难了。针对这种某个实体拥有多个域名需要共享 Cookie 的情况,就有人(那其实就是 Google 的同学)提出了 SameParty 的概念。 该提案提出了 SameParty 新的 Cookie 属性,当标记了这个属性的 Cookie 可以在同一个主域下进行共享。那如何定义不同的域名属于同一主域呢?主要是依赖了另外一个特性 first-party-set 第一方集合。它规定在每个域名下的该 URL /.well-known/first-party-set 可以返回一个第一方域名的配置文件。在这个文件中你可以定义当前域名从属于哪个第一方域名,该第一方域名下有哪些成员域名等配置。 当然使用固定 URL 会产生额外的请求,对页面的响应造成影响。也可以直接使用 Sec-First-Party-Set 响应头直接指定归属的第一方域名。 不过 W3C TAG 小组已经强烈拒绝了该提案(来源)。W3C 认为该提案重新定义了网站沙箱的边界,带来的影响可能不仅仅只是 Cookie 共享这么简单,包括麦克风、摄像头、地理信息等隐私设置都需要去重新评估影响。 同时该提案可能会和用户的预期不一致,如果 Google 和 Youtube 被定义成第一方网站进行共享的话,那 Google 就能很轻松的获取到用户在 Youtube 上的行为,可能用户并不想要这样。 W3C TAG 小组全称是 Technical Architecture Group,即 W3C 技术架构组。TAG 是 W3C 专注于 Web 架构管理的特殊小组。其使命是为 Web 架构的设计原则寻求共识,且在必要时梳理并澄清这些设计原则,帮助协调 W3C 内部及外部跨越不同技术的架构定义与研发工作。基本可以认为它是 Web 基础规范定义的小组。另外万维网之父 Tim Berners-Lee 也在 TAG 小组中。 不过 W3C 说的有理没理,都阻挡不了 Chrome 去实现这个功能。在 Chrome 89 中已经增加了 SameParty 的相关逻辑,只是目前没有默认开启。目前在 DevTools 中是可以看到 Cookie 的 SameParty 属性列的。Edge 由于使用了 Chromium 也在同版本支持了该功能。只掌管了规范,没有掌管实现,当某一方浏览器实现了“霸权”的情况下,W3C 的处境就变得尴尬了起来。 FLoC SameSite 除了影响单实体多域名共享 Cookie 的情况,最大的问题其实就是互联网广告获取用户行为了。由于广告挂载页面和广告不在同域,所以广告无法获得用于标记用户 ID 从而对用户行为进行聚类。为了解决这个问题,有人(其实也是 Google 的同学)提出了 Federated Learning of Cohorts 同盟学习队列提案。 有别于之前使用 Cookie ID 标记直接将用户行为数据传递到广告商网站处理的方式。它提出了 document.interestCohort() 这个新的 API,将用户的行为在本地转换成了不带个人隐私的关键词,既规避了用户隐私问题,同时又解决了广告的精准投放问题。 不过这看似美好的东西却遭到了各大网站和浏览器的强力抵制,brav、Vivaldi、duckduckgo、GitHub 以及 Edge,Firefox,Safari(来源)都纷纷发表了拒绝支持的观点和行动。 社区主要的担心点在于,新的特性的增加可能会增加特征值为隐私嗅探提供了更广阔的入口。而且通过该 API 能获取到之前碍于权限无法程序获取的用户浏览数据。目前 Chrome 已经支持了这个功能,不过需要开启 Flag 才能支持。amIFLoCed 是一个用来检测你的浏览器是否开启了 FLoC 追踪特性的网站,可以使用它检测你的浏览器是否应用该特性。 后记 为了解决 CSRF 问题,Chrome 强推了 SameSite=Lax 作为默认配置。随之而来的,不仅是全球开发者的配合修改,还造成了已有场景的无法满足。而为了满足现有场景,又提出了 SameParty 和 FLoC 两个方案。这种行为不知能否成为浏览器的内卷行为? SameSite 属性本身是没有什么问题的,但个人认为它应该是一种 CSRF 问题的选择方案,浏览器将其默认修改成 SameSite=Lax 就有点难受了。大部分企业项目里都已经采用其他 CSRF 防范方式规避了该问题,而 Lax 配置又存在着兼容性问题,不能让我们完全免顾 CSRF 之忧。 随着全球隐私问题的白热化,不知道还有什么新的提案搞出来需要我们全球开发者为其买单。 参考资料: 封面图来源 《SameSite cookies explained》 《SameSite cookie recipes》 《Schemeful Same-Site》 《Understanding “same-site” and “same-origin”》 https://www.chromestatus.com/feature/5088147346030592 https://www.chromium.org/updates/same-site https://hacks.mozilla.org/2020/08/changes-to-samesite-cookie-behavior/
  •  

全国分省12.5米DEM数字高程数据(ALOS 12.5m)下载

✇遐说
作者Dorad

分享全国数字高程数据,ALOS 12.5m, 数据源: ASF, 该官方源可免费下载,下载教程在网络随处可以搜索到。

本文提供的是整理好的全国分省份12.5m数字高程,适合有一定搜集癖好或者是想省事的同学使用。

  •  

儿童节快乐

✇遐说
作者Dorad

又到一年儿童节,无法回去的童年,只能怀念。

那时候一无所有,却又无比快乐!

  •  

基于 Antd 封装业务 Upload 组件

前言 我们的后台系统都是基于 Antd Design 开发的。最近做的新系统里有比较多的场景需要使用到附件上传的功能,我们针对 Antd 的 <Upload /> 组件在项目里进行了业务的封装。过程中也碰到些问题,遂总结于本文中。 基本使用 我们主要是用到了它多文件上传和功能。 import React from 'react'; import { Upload } from 'antd'; return () => { const [fileList, setFileList] = useState([ { uid: '1', name: '1.txt', status: 'done', url: 'https://www.baidu.com', }, ]); const handleChange = info => { let fileList = info.fileList.slice(); fileList = fileList.map(file => { if (file.response) { // Component will show file.url as link file.url = file.response.url; } return file; }); setFileList(fileList); }; return ( <Upload action="https://www.mocky.io/v2/5cc8019d300000980a055e76" fileList={fileList} onChange={handleChange} /> ); }; 这是官方文档中提供的示例,我们可以通过 action 属性定义上传的地址,通过 onChange 获取上传后的文件地址以及 fileList 设置上传文件。其中 onChange 以及 fileList 参数类型如下。 import { RcFile as OriRcFile } from 'rc-upload/es/interface'; export interface UploadFile<T = any> { uid: string; size?: number; name: string; fileName?: string; lastModified?: number; lastModifiedDate?: Date; url?: string; status?: UploadFileStatus; percent?: number; thumbUrl?: string; originFileObj?: RcFile; response?: T; error?: any; linkProps?: any; type?: string; xhr?: T; preview?: string; } export interface RcFile extends OriRcFile { readonly lastModifiedDate: Date; } export type UploadFileStatus = 'error' | 'success' | 'done' | 'uploading' | 'removed'; export interface UploadChangeParam<T extends object = UploadFile> { // https://github.com/ant-design/ant-design/issues/14420 file: T; fileList: UploadFile[]; event?: { percent: number }; } UploadFile 是主要的类型,最外层是组件包装的一些数据,包括 status, percent 等用于记录下载状态字段。其中还有 response 字段,当下载完成 status === 'done' 的时候,该字段会存储服务端返回的相应数据。 需求描述 中后台场景会有大量的表单场景,其中我们的大部分附件提交都是在表单之中。当然我们的表单也是使用的 Antd 组件。它提供了类似于原生 <form /> 的一套模式,你不需要关心表单的交互,当使用 <Form.Item name="" /> 包裹之后,就会自动认为你是表单元素,在 onFinish 事件中可以达到所有提交后的表单数据。而通过initialValues 属性又可以对整个表单设置初值。简单的通过这两个属性就可以实现表单的大多数需求。 impoprt React from 'react'; import {Form, Input, Upload} from 'antd'; export default function() { const initialValues = { remark: 'hello', attachment: { attachmentNo: 1234, fileKey: 2345 } }; return ( <Form onFinish={onFinish} initialValues={initialValues}> <Form.Item name="remark" label="说明"> <Input.Textarea /> </Form> <Form.Item name="attachment" label="附件"> <Upload /> </Form> <Button>提交</Button> </Form> ); } 这套表单的方式让上层交互变的非常纯粹,所以我期望我们封装的组件也最好能适配这套逻辑。而这里的矛盾点在于,我们需要的是 UploadFile['response']['data'] 中的数据,但是当我们要给它赋值的时候,它接收的是 UploadFile[] 的数据格式。所以除了封装业务的配置之外,还需要将数据格式转换的逻辑封装进去。 思考实现 最开始我想的设想类似于下面这个 Demo,只需要定义 uploadFile2value 和 value2UploadFile 两个方法,用于处理数据的双向转换即可。 import { Upload } from 'antd'; const Upload = React.memo(({onChange}) => ( <Upload fileList={value2UploadFile} onChange={e => onChange(uploadFile2value(e.fileList))} /> )); 但是我没想到的是,文件上传是一个异步的过程,最终 onChange 接收到的 fileList 数据是一组多状态数据的集合,具体的状态列表如下。 export type UploadFileStatus = 'error' | 'success' | 'done' | 'uploading' | 'removed'; 根据预期效果,显然我想要的是 status=done 后的数据。而如果我在 uploadFile2value 中对数据做过滤仅将 status=done 的数据返回给出去的话,在之后的渲染中 initialValues 传过来的初始数据中则不包含其它状态的数据了,会导致传入的 fileList 数据异常。这样我们就陷入了一种死循环,刚上传文件文件状态是 uploading 然后被 onChange 过滤为空数据传出,之后空数据作为初始数据再次被传入上传中的文件状态丢失组件回复到初始状态…… 最终实现 后台维护组件库的小伙伴提醒了我,既然组件本身需要所有的数据,而外部只需要上传完成的数据,那我们可以考虑将所有的数据在组件内部自行维护,仅将外部组件需要的数据传递出去。当外部数据传入进来的时候,将其与内部数据做合并即可。 import React, { useEffect, useState } from 'react'; import { Upload } from 'antd'; const value2UploadFile = record => ({ uid: record.id, name: record.name, status: 'done', response: { code: 0, msg: '', data: record } }); function useUpload(files, onChange) { const [filePool,setFilePool] = useState([]); useEffect(() => { if(!Array.isArray(files) || files.length === 0) { return; } setFilePool(filePool => { const fileIds = filePool.filter(({status}) => status === 'done').map(file => file.response?.data?.id); const appendFiles = files.filter(({id}) => !fileIds.includes(id)).map(value2UploadFile); return [...filePool, ...appendFiles]; }); }, [files]); const handleUploadChange = ({fileList}) => { fileList.filter(({status, response}) => status === 'done' && response.code !== 0 ).forEach(file => { file.status = 'error'; }); setFilePool(fileList); const doneFiles = fileList.filter(({status}) => status === 'done').map(file => file.response.data); onChange(doneFiles); } return [filePool, handleUploadChange]; } export default function({value, onChange, ...props}) { const [filePool, onFileChange] = useUpload(value, onChange); return ( <Upload listType="picture" btnType="default" btnText="上传文件" {...props} fileList={filePool} onChange={handleUploadChange} withCredentials action="/api/file/upload" /> ); } 可以看到我们内部增加了 filePool 的状态用来存储数据,每次内部都会全量的存储待上传的文件列表,但是最终调用外部的 onChange 方法回传出去的时候则只会传出 status=done 的数据。而针对赋值的场景,我们鉴定了 files 的变化,根据最终返回数据的 id 获取到 fileIds 内部已存在的文件,然后再使用这个和传入的数据进行 diff 比较,查看是否有新增的数据。如果存在新增的数据则将其转换成组件需要的数据格式后更新文件列表。 通过以上操作,我们就将上传组件的逻辑封装在了内部组件中。甚至我们还能在内部增加当接口返回非 0 的 code 上传失败的时候我们会将组件数据状态修改为 error 而不是 done。最终外部组件不需要关心上传接口本身内部的逻辑,只需要关系上传之后得到的数据即可,达到了业务上传组件解耦的目的。
  •  

统一路由、菜单、面包屑和权限配置

我最近做的一个新项目是一个典型的中后台项目,采用的是 React + React Router + Antd 方案。正常情况下我们需要定义路由配置,在页面中定义面包屑的数据,页面写完之后需要在左侧菜单中增加页面的路由。写多了之后,我会觉得同一个路由的相关信息在不同的地方重复声明,实在是有点麻烦,为什么我们不统一在一个地方定义,然后各个使用的地方动态获取呢? 单独配置 首先我们看看每个功能单独定义是如何配置的,之后我们再总结规律整理成一份通用的配置。 路由和权限 路由我们使用了 react-router-config 进行了声明化的配置。 // router.ts import { RouteConfig } from 'react-router-config'; import DefaultLayout from './layouts/default'; import GoodsList from './pages/goods-list'; import GoodsItem from './pages/goods-item'; export const routes: RouteConfig[] = [ { component: DefaultLayout, routes: [ { path: '/goods', exact: true, title: '商品列表', component: GoodsList, }, { path: '/goods/:id', exact: true, title: '商品详情', component: GoodsItem, } ], }, ]; //app.tsx import React from 'react'; import { BrowserRouter as Router } from 'react-router-dom'; import { renderRoutes } from 'react-router-config'; import { routes } from './router'; export default function App() { return <Router>{renderRoutes(routes)}</Router>; }; 菜单 左侧导航菜单我们使用的是 <Menu /> 组件,大概的方式如下: //./layouts/default import React from 'react'; import { renderRoutes } from 'react-router-config'; import { Layout, Menu } from 'antd'; export default function({route}) { return ( <Layout> <Layout.Header> Header </Layout.Header> <Layout> <Layout.Sider> <Menu mode="inline"> <Menu.SubMenu title="商品管理"> <Menu.Item key="/goods">商品列表</Menu.Item> </Menu.SubMenu> </Menu> </Layout.Sider> <Layout.Content> {renderRoutes(route.routes)} </Layout.Content> </Layout> </Layout> ); } 权限 这里的权限主要指的是页面的权限。我们会请求一个服务端的权限列表接口,每个页面和功能都对应一个权限点,后台配置后告知我们该用户对应的权限列表。所以我们只需要记录每个页面对应的权限点,并在进入页面的时候判断下对应的权限点在不在返回的权限列表数据中即可。 而页面权限与页面是如此相关,所以我们惯性的会将页面的权限点与页面路由配置在一块,再在页面统一的父组件中进行权限点的判断。 // router.ts import { RouteConfig } from 'react-router-config'; import DefaultLayout from './layouts/default'; import GoodsList from './pages/goods-list'; import GoodsItem from './pages/goods-item'; export const routes: RouteConfig[] = [ { component: DefaultLayout, routes: [ { path: '/goods', exact: true, title: '商品列表', component: GoodsList, permission: 'goods', }, { path: '/goods/:id', exact: true, title: '商品详情', component: GoodsItem, permission: 'goods-item', } ], }, ]; // ./layouts/default import React, { useEffect, useMemo } from 'react'; import { useHistory, useLocation } from 'react-router-dom'; import { matchRoutes } from 'react-router-config'; export default function({route}) { const history = useHistory(); const location = useLocation(); const page = useMemo(() => matchRoutes(route.routes, location.pathname)?.[0]?.route, [ location.pathname, route.routes, ]); useEffect(() => { getPermissionList().then(permissions => { if(page.permission && !permissions.includes(page.permission)) { history.push('/no-permission'); } }) }, []); } 面包屑 面包屑则比较简单了,直接使用 <Breadcrumb /> 即可 //./pages/goods-item.tsx import React from 'react'; import { Link } from 'react-router-dom'; import { Breadcrumb } from 'antd'; export default function() { return ( <Breadcrumb> <Breadcrumb.Item> <Link to="/goods">商品列表</Link> </Breadcrumb.Item> <Breadcrumb.Item>商品详情</Breadcrumb.Item> </Breadcrumb> ); } 合并配置 通过上面的整理我们可以看到,所有的功能都是和配置相关,所有的配置都是对应路由的映射。虽然路由本身是平级的,但由于菜单和面包屑属于多级路由关系,所有我们的最终配置最好是多级嵌套,这样可以记录层级关系,生成菜单和面包屑比较方便。 最终我们定义的配置结构如下: //router-config.ts import type { RouterConfig } from 'react-router-config'; import GoodsList from './pages/goods-list'; import GoodsItem from './pages/goods-item'; export interface PathConfig extends RouterConfig { menu?: boolean; permission?: string; children?: PathConfig[]; } export const routers = [ { path: '/goods', exact: true, title: '商品列表', component: GoodsList, children: [ { path: '/goods/:id', exact: true, title: '商品详情', component: GoodsItem } ] } ]; 路由 基于上面的嵌套配置,我们需要定义一个 flatRouters() 方法将其进行打平,替换原来的配置即可。 //router.ts import { RouteConfig } from 'react-router-config'; import DefaultLayout from './layouts/default'; import { routers, PathConfig } from './router-config'; function flatRouters(routers: PathConfig[]): PathConfig[] { const results = []; for (let i = 0; i < routers.length; i++) { const { children, ...router } = routers[i]; results.push(router); if (Array.isArray(children)) { results.push(...routeFlat(children)); } } return results; } export const routes: RouteConfig[] = [ { component: DefaultLayout, routes: flatRouters(routers), }, ]; 菜单 菜单本身也是嵌套配置,将其正常渲染出来即可。 //./layouts/default import React from 'react'; import { renderRoutes } from 'react-router-config'; import { Layout, Menu } from 'antd'; const NavMenu: React.FC<{}> = () => ( <Menu mode="inline"> {routers.filter(({ menu }) => menu).map(({ title, path, children }) => ( Array.isArray(children) && children?.filter(({ menu }) => menu).length ? ( <Menu.SubMenu key={path} title={title} icon={icon}> {children.filter(({ menu }) => menu).map(({ title, path }) => ( <NavMenuItem key={path} title={title} path={path} /> ))} </Menu.SubMenu> ) : ( <NavMenuItem key={path} title={title} path={path} /> ) ))} </Menu> ); const NavMenuItem: React.FC<{path: string, title: string}> = ({path, title}) => ( <Menu.Item> {/^https?:\/\//.test(path) ? ( <a href={path} target="_blank" rel="noreferrer noopener">{title}</a> ) : ( <Link to={path}>{title}</Link> )} </Menu.Item> ); export default function({route}) { return ( <Layout> <Layout.Header> Header </Layout.Header> <Layout> <Layout.Sider> <NavMenu /> </Layout.Sider> <Layout.Content> {renderRoutes(route.routes)} </Layout.Content> </Layout> </Layout> ); }; 面包屑 面包屑的难点在于我们需要根据当前页面路由,不仅找到当前路由,还需要找到它的各种父级路由。 除了定义一个 findCrumb() 方法来查找路由之外,为了方便查找,还在配置上做了一些约定。 如果两个路由是父子关系,那么他们的路由路径也需要是包含关系。例如商品列表和商品详情是父子路由关系,商品列表的路径是 /goods,那么商品详情的路由则应该为 /goods/:id。 这样在递进匹配查找的过程中,只需要判断当前页面路由是否包含该路径即可,减小了查找的难度。 另外还有一个问题大家可能会注意到,商品详情的路由路径是 /goods/:id,由于带有命名参数,当前路由去做字符串匹配的话肯定是没办法匹配到的。所以需要对命名参数进行正则通配符化,方便做路径的匹配。 命名参数除了影响路径查找之外,还会影响面包屑的链接生成。 由于带有命名参数,我们不能在面包屑中直接使用该路径作为跳转路由。为此我们还需要写一个 stringify() 方法,通过当前路由获取到所有的参数列表,并对路径中的命名参数进行替换。 这也是为什么之前我们需要将父子路由的路径定义成包含关系。子路由在该条件下肯定会包含父级路径中所需要的参数,极大的方便我们父级路由的生成。 //src/components/breadcrumb.tsx import React, { useMemo } from 'react'; import { Breadcrumb as OBreadcrumb, BreadcrumbProps } from 'antd'; import { useHistory, useLocation, useParams } from 'react-router'; import Routers, { PathConfig } from '../router-config'; function findCrumb(routers: PathConfig[], pathname: string): PathConfig[] { const ret: PathConfig[] = []; const router = routers.filter(({ path }) => path !== '/').find(({ path }) => new RegExp(`^${path.replace(/\:[a-zA-Z]+/g, '.+?').replace(/\//g, '\\/')}`, 'i').test(pathname) ); if (!router) { return ret; } ret.push(router); if (Array.isArray(router.children)) { ret.push(...findCrumb(router.children, pathname)); } return ret; } function stringify(path: string, params: Record<string, string>) { return path.replace(/\:([a-zA-Z]+)/g, (placeholder, key) => params[key] || placeholder); } const Breadcrumb = React.memo<BreadcrumbProps>(props => { const history = useHistory(); const params = useParams(); const location = useLocation(); const routers: PathConfig[] = useMemo<PathConfig[]>( () => findCrumb(Routers, location.pathname).slice(1), [location.pathname] ); if (!routers.length || routers.length < 2) { return null; } const data = props.data ? props.data : routers.map(({ title: name, path }, idx) => ({ name, onClick: idx !== routers.length - 1 ? () => history.push(stringify(path, params)) : undefined, })); return ( <OBreadcrumb {...props}> {data.map(({name, onClick}) => ( <Breadcrumb.Item key={name}> <span onClick={onClick}>{name}</span> </Breadcrumb.Item> ))} </OBreadcrumb> ); }); export default Breadcrumb; 后记 至此我们的统一配置基本上就屡清楚了,我们发现只是简单的增加了几个属性,就让所有的配置统一到了一起。甚至我们可以更上一层楼,把 component 这个配置进行声明化,最终的配置如下: //router-config.json [ { path: "/goods", exact: true, title: "商品列表", component: "goods-list", children: [ { path: "/goods/:id", exact: true, title: "商品详情", component: "goods-item" } ] } ] //router-config.tsx import React from 'react'; import type { RouterConfig } from 'react-router-config'; import routerConfig from './router-config.json'; export interface PathConfig extends RouterConfig { menu?: boolean; permission?: string; children?: PathConfig[]; } export interface PathConfigRaw extends PathConfig { component?: string; children?: PathConfigRaw[]; } function Component(router: PathConfigRaw[]): PathConfig[] { return router.map(route => { if(route.component) { const LazyComponent = React.lazy(() => import(`./pages/${route.component}`)); route.component = ( <React.Suspense fallback="loading..."> <LazyComponent /> </React.Suspense> ); } if(Array.isArray(route.children)) { route.children = Component(route.children); } return route; }); } export const routers = Component(routerConfig); 将这些配置声明化,最大的好处是我们可以将其存储在后台配置中,通过后台菜单管理之类的功能对其进行各种管理配置。 当然这种统一配置也不一定适合所有的场景,大家还是要具体问题具体分析。比如有同事和我反馈说微前端的场景里可能就不是特别合适,不管怎么统一配置,主应用和子应用中可能都需要分别存在一些配置。主应用需要菜单,子应用需要路由,这种时候可能稍微拆分一下反而更倒是合适的。
  •  

南京行之游后感

前言 清明来了一次说走就走的旅行,去南京溜达了一圈。具体的每日见闻游记可见之前的几篇文章: 南京行-Day 1 南京行-Day 2 南京行-Day 3 虽然过程有喜有乐,不过从行程上来看准备确实做的不充足,吃了很多亏。好在我们也是比较佛系的人,景点进不去那就不打卡了吧,人多排队那就换别家吧。这趟出行虽然有囧事,有累点,不过整体来看还是不错的。 旅游 这次出门获得的宝贵经验,那就是出门旅行之前务必需要先搜索一下当地的景点是否需要预约,是否支持网上购票,提前做好准备避免到了景区发现无法进入的尴尬。 这里列举一下我掌握的南京的一些旅游景点的相关讯息,如果大家有要去南京玩的可参考一二: 南京大屠杀纪念馆不需要门票,但是需要提前预约,使用微信公众号”侵华日军南京大屠杀遇难同胞纪念馆“可以查看预约情况并预约。 总统府需要门票,可以使用携程等网上购票,具体链接可查看总统府官网: http://www.njztf.cn/business/index.sh。 玄武湖公园不需要门票,可以直接在携程上预约。 雨花台烈士陵园不需要门票,需要在携程等平台上提前预约。预约的时候有两个选项,分别是陵园预约和陵园+纪念馆套票预约。纪念馆还是值得一看的,第一次去建议选择套票预约。 吃住 我们住的地方是南京机场宾馆夫子庙店,设备比较新,房间内东西齐全,周边环境也不错。交通比较方便,离这些景区都不算太远。价格也还算 OK,最重要的是二楼就有南京当地的淮扬菜连锁店小厨娘,吃饭极其方便。除了隔音效果一般之外,给我的体验非常不错。推荐给有需要的同学。 这次说老实话也没有特地去吃一些经典的菜,毕竟说实话大部分品类南京大牌档都有了,而且品种多都能吃到。盐水鸭确实还不错,鸭子做的不膻不腻肉质紧实多汁。另外鸭血粉丝汤里面的鸭肠可真是好吃呀~ 出行 南京水杉很多,所以看起来树木都很笔直高大,看起来很养眼。比起其他地方,南京路上的红绿灯极其的多,这点可能和台湾类似,基本是没两步路就有斑马线和红绿灯配套。这个对行人比较友好,对开车的人就不知道是否方便了。 南京的路不算宽,路上的车辆也比较多,堵车情况从这几天来看似乎比起北京不逞多让,再加上在修地铁有些路口更是雪上加霜。到了景区附近更是车子动不动不了。 不过可能因为我们住的离去的地方都不远的缘故,打车还挺省钱的。小伙伴用美团打车基本上都只要几块钱就能搞定了,真是惊呆我了。
  •  

南京行-Day 3

今天是行程的最后一天,我们放弃了之前找的攻略路线,准备把之前两天没有逛到的总统府和雨花台补完下。这两个在小伙伴的努力下都已经提前预约了,应该是不能再出什么幺蛾子了。 例行早起收了一波蚂蚁森林的能量之后把行李收拾好和小伙伴一块把房退了。不过因为我们还要出去玩,行李就暂时先寄存在宾馆了。吸取了昨天总统府旁边的早餐店排队盛况的教训,我们决定在宾馆附近吃完早餐再出发。附近找了一家看着还算干净的早餐店,豪放的狂点了一番后发现总价只有昨天早饭的一半,而且还比它更好吃,惹得小伙伴们纷纷对我们今天的机智点赞! 二战总统府 打车去总统府,沿途依旧是熟悉的风景。可能是因为我们来的稍微晚了点,今天排队买票的人没有昨天那么多了。不过我们也没有功夫去称赞自己的机智了,我们昨天预定的时间是8:30-10:30,到地方一看就已经9:45了,赶紧汇入进去的人群。好在突然又开放了一个入口分流,让我们能按时进入景区,成功打卡南京行第二个景区。 注:人不要太多的总统府 本来小伙伴想着这种人文景点,豪放的请一个人工讲解会更有意思一点,奈何天不遂人愿,去咨询台一看人工讲解已然售罄。无奈的我们悄咪咪的尾随了一位讲解听了半路,有些讲解听起来着实有些意思。令我印象深刻的是太平天国的一幅画,洪秀全的妹妹站在比洪秀全更高处,表示了当时太平天国男女平等的观念,这个在那个年代还真是难得。 注:总统府天下为公匾额 参观完前院就到了蒋介石办公的地方了,这块人多地方窄,所有人比肩继踵的往前艰难行走着。上楼梯的时候前面的人踢到后面人的膝盖是在正常不过了,听小伙伴说甚至有小孩摔倒了一度造成了交通的堵塞。其实就是老式的办公室,说实在的没什么可看的,也不知为何吸引了这么多人。 出来后就是一片花园。看着花园的白色围墙,想着昨天只能在墙外面眼巴巴的望着游园的人群心生羡慕的我们,再次感慨提前做攻略的重要性。花园的风景真是不错,成荫的绿树和似锦繁花相得益彰。中间还参观了一些国父孙中山的纪念馆,发现他居然只做了三个月的大总统,但是却给我们带来了巨大的影响,不由心生佩服。 注:革命尚未成功,同志仍需努力 不得不说总统府真的很大,后面的后花园之前是乾隆下江南的行宫,每栋建筑都古色古香。特别是春天各种鲜花争群夺艳,让这三十元的门票花的真值。参观完继续遵循昨天的不要在景区旁吃饭定律,我们找了些稍微远一丢丢的地方,可能是对距离没有把控好,结果也是都要等位。一气之下我们想着干脆吃火锅吧,最后定了距离不太远的马路边边串串香。步行过去的时候发现路边的风景再次熟悉起来,原来定位的地方竟然就是昨天我们沿着珍珠河溜达去总统府路过的地方!真是造化弄人。 注:好看的绣球 据小伙伴说这家马路边边似乎少了很多经典菜色,不过他们家特有的糖蒜牛肉获得了我们的一致好评。酒足饭饱过后,我们打车朝雨花台出发了。路上司机师傅和我们一路吐槽旅游景点,比如夫子庙的南京特色小吃都是安徽人开的,南京只有小笼包压根没有汤包之类的,让我们大吃一惊。 雨花台烈士陵园 雨花台烈士陵园恢弘大气,我们没有完全逛完,驻足比较久的是雨花台烈士纪念馆。里面陈列了大量的烈士事迹,包括我们在《觉醒年代》中看到的邓中夏。每位烈士的事迹都一一驻足瞻仰后,一个多小时已经过去了。大部分人都是在二十几岁人生最美好的年华中去世的,有些甚至只有十几岁,看照片都只是孩子模样。他们基本都是集中在几个时间过世的,有些可能是因为同一事件被抓,有些则是因为同一事件被处决。看到那些年纪轻轻就英勇就义的烈士,有种说不清道不明的感觉油然而生,或疑惑,或震撼,或感慨,或哀叹。 注:雨花台烈士陵园 纪念馆的尽头有个漆黑的小房间,里面陈列了一些烈士相关的物品,扫描他们的二维码可以查看对应的烈士事迹。特别的是每个小物品都使用火焰灯光照射着,通过漆黑的镜面效果将这几簇火焰生生演化出燎原之势,观感效果极好,同时也非常具有寓意。 注:只有9簇火焰的燎原效果 雨花台烈士陵园又非常肃穆庄严。花了一个多小时浏览完纪念馆,我们来到了纪念碑前。小伙伴买了朵小菊花代表我们的心意在纪念碑前为烈士献上了鲜花。看着时间也差不多要离开了,草草的游览了一下就离开了雨花台了。 返程 打车到宾馆拿了行李之后还有一点时间,就在附近找了个生煎包店解决晚饭。路上还碰到个叫”甜星“的好利来李鬼,本来以为只是个小面包店,结果为了防止火车上饿着买了点面包发现小票上居然印着苏州好利来的字样。怪不得在店里看到了好利来的半熟芝士,这就说得通了。回来一搜发现原来好利来苏州的店都改名叫甜星了,其它地方的也纷纷都改成当地的品牌名了。 打了出租车去车站,结果快到站计程车的表跳了一下。师傅应该是跳表钱看的,和我说 25。我看了下表疑惑的和他说:”不是 26 么?“真是老实人碰到老实人,分外尴尬。上车后还用手机写了一会游记,奈何太累了开始还是小憩,后来就沉沉的睡过去了。 到北京后已经是晚上十一点半了,如何回家顿时成为了难题。地铁停运只剩下增开的 4 号线,出租车早已人多车少车站工作人员已经不推荐排队等候了。无奈的我们只能想着出站走一段看看能不能打到车,结果滴滴打车等位都已经超过 200 位了。 出来车站没多远就有一些黑车在等着载客,随口问了下说是要 150 还得拼车,看这坐地起价的口气就不想坐。往前走了段我鬼使神差的建议在一个十字路口逆着人流方向走了一段,碰到一趟公车,想着人走估计是走不出这片拥堵区了,干脆坐公车坐几站出去了再打车吧。结果好家伙也不知是不是运气好,公车还真就能到住的地方附近。不过晚上太冷,最后一公里不太好解决,遂还是按照之前的思路尝试在后几站的公车站打车。 每过一个站我就重新更新下出发地点为后几站,就这么尝试了七八次之后,在我都已经不抱希望的情况下,居然被我打到车了!当时激动的心,颤抖的手,喜悦的心情估计周边人都能感受到了。而且非常幸运的是到站没多久师傅也到了,时间完美匹配!经过这么一个多小时的折腾,总算是顺利到家了。至此清明南京之行顺利结束!
  •  

南京行-Day 2

总统府 本以为昨晚吃一堑长一智,详细的查看了路线攻略,发现没有需要预约才能参观的地点后能开开心心的开启新的一天。没想到我们还是太低估了国内旅游的困难程度。早上把小伙伴催促起来之后已然过了九点,匆忙打了个车就去了本日的第一站—总统府。来了之后发现本日第一囧,那就是排队买票的人实在是太多了,我们是万万没戏了。 注:酒店风景 想着反正也排不上了,遂大众点评上找了一家旁边的早餐店准备慢悠悠的吃个早饭去下个景点。结果去了店里发现这排队的人也非常多,果断换了旁边一家人不太多的吃了鸭血粉丝汤和汤包,不过质量嘛景区的东西你懂的。吃饭的时候顺手查了一下总统府能否在网上购票,发现居然可以!不过遗憾的是只能定明天的票了,不过我们还是先买了想着明天再过来。 注:鸭血粉丝汤 南京大学 费了半天劲打车来到我们的第二站南京大学后发生了本日的第二囧,南大可能因为疫情的原因目前暂不开放!无奈的我们打卡了下不算好看的校门,在附近溜达了一圈后就灰溜溜的跑去我们的第三站古鸡鸣寺了。 注:槽点满满的南京大学校门 古鸡鸣寺 到了古鸡鸣寺发现人更是多,看到门口的人山人海我们就果断放弃了参观的想法。不过因为人实在是太多了,已经需要一堆城管采用人墙策略划分人流维护秩序了。好巧不巧小伙伴又很想上厕所,而上厕所的标识指向了古鸡鸣寺的路口处。结果小伙伴就不小心被挤入到了入寺的人群中,最后好不容易让城管帮忙给放出来了,这算是本日的第三囧了。万般无奈之下就去了就近的地铁站进站上了个厕所,结果反馈地铁站的厕所居然也是要排队的,真是令人崩溃! 注:人山人海的古鸡鸣寺入口 南京大牌档 一连打卡三个景点都没有去成让我们的兴致不是非常高,临近中午了就想着干脆找一家饭馆好好吃一顿排解一下。怀着好奇心想试试南京的南京大牌档会不会特别点,于是找了一家附近的南京大牌档准备溜达过去。沿着珍珠河我们一边溜达一边拍照,走着走着就发现路边的风景慢慢熟悉了起来。原来我定位的这个南京大牌档居然就在总统府的旁边啊!囧上加囧的事情是,店里排队的人可不比去古鸡鸣寺的人要少,只能果断放弃了。 江宴楼 询问了一遍附近的饭馆发现居然都需要等位之后,我们想着是不是打个车逃离总统府景区就可以正常吃上饭了?于是一番讨论后大众点评上找了家评分比较高且不需要等位的江宴楼。去了之后才发现和我平常吃饭的地方档次高了一个数量级,服(菜)务(品)也特别的周(昂)到(贵)。 注:好吃的江宴楼 夫子庙 在包间我们边吃边聊间解决了随意点的一些菜品,感觉我们又复活了!看到古鸡鸣寺那么多人之后,我们放弃了下一站位于古鸡鸣寺旁边的玄武湖公园,直奔夫子庙秦淮河景区了。吃饭的地方离夫子庙倒也不是很远,就想着溜达溜达消消食。夫子庙旁边的步行街到处都充满了熙熙攘攘的人群。按照导航的定位,我们跑到了夫子庙的出口处买了票并成功进入。进去之后发现原来夫子庙真的就是说的孔夫子和学业相关的事情。这才明白过来为什么去的路上听到有人说”我一个山东人跑南京来参观夫子庙?“这种问句了。 作为我们今日成功打卡的第一个景区,也是我们南京之行成功打卡的第一个景区,我们还是非常认真的游览了一遍。里面按照时间先后展览着一些和学生、学习、考试等相关的藏品。其中最有意思的当属于古代人在袜子上用小字撰写的小抄,怕不是和现在的小抄有着异曲同工之妙。除了这些之外还有很多明国时期的毕业证书、课本教材、护照等有意思的文字材料。夫子庙算是已经很成熟的商业景区了,内部的商业气息非常浓重,付费撞钟祝好运,付费挂许愿树、付费扔币许愿、付费平安福等等活动充斥着景区。 注:古代小抄 参观完后我们从景区的入口处出来,发现来夫子庙的人真的很多,只是我们是从出口处进来的。夫子庙入口前面就是秦淮河,上面有秦淮河画舫可以120(白)/140(夜)元坐船的活动,想着晚上坐船可能有灯光会更好看点于是就先过了。路上有用铜片手动压制铭牌的机器,给小伙伴买了一个。想着景区里打车不方便就慢慢走出了景区,结果发现了我们第五件囧事,我们居然沿着之前路过的时候大家疯狂吐槽的”为什么会有很多人从小区门口出来“的地方回到了我们去夫子庙的原点处!实打实的沿着夫子庙走了一圈,真是欲哭无泪。 二战小厨娘 经历了两个轮回圈之后我们已然是精疲力尽,赶紧打了个车回酒店歇息了。稍作整顿后我去了小伙伴那继续补习《觉醒年代》了。晚上则还是在昨天的小厨娘,甚至还是昨天的那个位置吃的。我们点了松鼠桂鱼和烤鸭,想着试试南京的烤鸭和北京的烤鸭到底有什么区别。吃完后本来我们也没有觉得烤鸭有多好吃,但是因为服务员和我们说我们的烤鸭是最后半只,且吃饭过程中一直听到服务员和顾客说烤鸭早就卖完了,莫名的给这烤鸭增加了一颗星。 注:小厨娘的烤鸭 夜游东水关遗址 吃完沿着宾馆旁边的城墙溜达了一圈,走到城墙尽头上去看了下,发现旁边的秦淮河里也有画舫在游河,发现真要去划船着实是没什么可以看的,画舫游船一事也就作罢。本意是想出来溜达找个水果店买些水果回去窝着看剧的。逛了一圈大家又都有写疲了,就在宾馆旁边的超市买了袋牛奶回宾馆歇息去了。 注:夜游东水关
  •  

南京行-Day 1

清明来了一次说走就走的旅行,突然之间决定和小伙伴一块去南京当回游客。本来我是想出去转转定了上海的,后来想想上海似乎也没有需要待好几天的必要,就在小伙伴的建议下选择了附近的南京。 我和小伙伴花了一个晚上的时间定好了酒店和出行时间,第二天早上用智行买的火车票。其中回程的车票因为只有一等座了就暂时先买了,后来智行还很温馨的提示我们有二等座车票了是否需要更换,果断更换后省了一晚酒店钱,同时也让我第一次体验到了智行这种第三方买票软件的优势。 出发 早早收拾好就出门赶地铁去了,想着给小伙伴带新口味的鸡蛋汉堡还路过了一下超市,结果也不知是起晚还是放假休息的缘故,鸡蛋汉堡的老板居然没有来上班,美好的愿望就这么落了个空。只能路上买了个煎饼留了一半给他们吃。 地铁上出行的人还是很多的,看来清明旅游区的人流量应该不会少。进车站后发现车展特地为清明出行开启了快速进站厅,上火车顿时方便了不少。上车后就在我焦急等待小伙伴的时候,他们总算赶着发车的尾巴上来了。上车后发现隔壁两个小孩在车上写作业,小伙伴也发表了他要带工作旅游的艰辛感慨,而我只能感谢他辛苦百忙之中抽空来陪我们旅游了。 注:火车上努力写作业的小朋友 许久不见分外想念,一路上有的没的聊着聊着就到地方了。当我们一边带着疑惑”不是不用扫健康码了吗“一边制作了苏州健康码后,我们碰到了第一个囧事。为了找出租车,我们居然按照火车站的指示牌绕着南站转了大半圈,也是不得不感慨造物主的脑回路。到了宾馆办好入住之后进屋发现居然还挺不错,东西应有尽有,设施也很整洁(不过隔音一般)。特别还碰到了宾馆服务员给房客发放清明节小礼物,顿时好感上升了不少。 注:路边盛开的景观花 由于已过中饭点,赶紧找了附近一家好评还不错的饭店小厨娘吃饭。它是南京一家连锁店,这家分店也是很神奇,居然开在了宾馆的二楼。我也是到了一楼之后才发现的,只能傻不愣登的赶紧跑二楼去了。为了节约用餐时间,趁着小伙伴还没到的时间我先把菜点了。菜口味还不错,就是忘记点招牌的盐水鸭了。而且糯米类的食物比较多,吃完已然是撑得不行了。 注:逛商场看到的可爱童装 南京大屠杀纪念馆 经过几个人轮番努力,总算打上了车去南京大屠杀纪念馆了。快到的时候我才发现我们的第二个囧事,那就是纪念馆是需要提前一天预约的,而不巧的是清明三天的时间都已经预约满了。在某人惋惜的眼神下,我们只好跑去附近的万达广场逛商场去了。不知是否是放假气息的带动,即使是逛着在帝都也能逛到的品牌店,我们依旧也很开心。甚至每个人都买到了自己称心的衣服,还不止一件! 注:蒙蒙细雨中送入云端的大厦 明瓦廊小吃街 逛完商场已然是到了晚上,打车去了小伙伴之前查到了一家南京很好喝的奶茶店拾叁茶明瓦廊店,想着买杯奶茶再在附近找家吃饭的地方就好了。在中江书香世家酒店下车后发现明瓦廊这条小巷似乎是小吃一条街,里面的小吃可太多了。特别是居然在去奶茶店的路上我们还看到了卖鸡蛋汉堡的!虽然比起在公司旁边吃的那家贵了很多,但看在排队的人那么多以及好奇心的驱使下,组织还是把我派出来排队购买了,而他们继续向奶茶店前进。当我看着门口无序的排队人群头大的时候,一个小妹妹非常优秀的和我达成了帮忙排队代购鸡蛋汉堡,她去其它地方排队帮我买好吃的的交易。结果买奶茶的,买其它好吃的都已经回来了,而我还在等鸡蛋汉堡…好在是没过一会都好了。真是到哪里都有排队、堵车的事情呀! 注:超多人排队的鸡蛋汉堡 等车的路上我们又买了点水果,想着干脆不吃晚饭,窝在宾馆边看剧边吃小吃了。回到宾馆发生了第三件囧事,宾馆只能刷卡到卡对应的楼层。而我和小伙伴不在一个楼层,当我想去他们楼层的时候发现过不去了。到一楼咨询了下之后发现需单独授权一下对应楼层,这里就想吐槽下办入住的时候服务员为什么不帮我们一步到位打通一下。 注:打卡到超好喝的茶饮品拾叁茶 吃了下鸡蛋汉堡发现用料确实要比我们在北京吃的扎实很多,带着一股鸡蛋的鲜嫩口感。不过我个人觉得在口味上还是北京的更胜一筹。北京的鸡蛋汉堡会使用榨菜来增加咸鲜味,同时辣酱口味使用的是剁椒而不是辣椒面也会更加符合我的口味。边吃边刷剧发现宾馆电视可以看《觉醒年代》,遂看了两集,正好在说一战后的巴黎合约事件,边看边搜下又恶补了一段历史知识。
  •  

Notion 记账与扩展

我第一次尝试记账是本科期间接触 MoneyWiz 时,那时落入了把玩 App 的邪道,重点没有放到记账上;后来使用过很多其他软件,自然是各有优劣,但是均无善终。自毕业开始生活状态和学生时期大不相同,全靠一点微薄薪水养活自己,因而有必要严加管理自己的财务状况。这才重拾记账。

如果你与我相同,目的是通过记账搞明白自己的钱花到哪里了,然后根据过往支出修正自己的消费行为,那这篇文章提到的方法或可作为参考。

关于记账

这次我没有直接奔向任何记账软件,而是首先思考了一下「记账」这件事。记账也属于自我量化的一部分,有人记账纯粹是为了了解自己,而别无他求;然而我猜更多人最终目的是想要增加账上余额。想要增加财富,无非开源、节流两种方法。

先说「开源」,一般来讲普通人的收入可以分为工资、副业、理财收入,但是对我(以及多数学生、初入社会的年轻人)来讲这三者都很固定,并不是可以随意调整的,因此其实没有必要考虑到记账体系中。

再说说「节流」,这才是记账的用武之地。财务状况一团糟的人最大的问题是「不知道钱花到哪里了」,记账则是通过记录支出,以期搞明白这个问题。但是值得注意的是:只是记账并不能达成这个目的,必须要搭配合适的回顾才能搞明白。这就隐含了对记账体系的一个要求:要支持对每笔支出归类,还要有灵活的统计回顾功能,这样才能根据过往支出调整未来支出。

以 MoneyWiz 为代表的记账软件实在过于繁复,包含了太多我不需要的东西,例如账户体系之类;而我真正想要的功能却缺失或者无法满足我的需求。因此为自己量身定制一个记账系统可能是正道所在。

接下来我会叙述如何使用 Notion 建立个人账本,并向你展示,它为何简洁、实用而强大。

用 Notion 记账

细细想来,我所需要的个人账本其实无比简单。一笔账,最基本的信息莫过于金额、描述、交易时间,这三者是随着支出自然产生的;为了进行统计回顾,还需要再加上一个信息,用来将各种支出归类。

在 MoneyWiz 中,每笔交易有「交易类别」属性。然而我认为更合适的支出标记方法不是分类,而是标签。这两者之间的差别在于分类是唯一的,一笔支出只能属于一个分类;而标签不是唯一的,一笔支出可以被打上若干标签。

用标签替代分类可以增加许多灵活性。例如咖啡与可乐,按照分类大概是 食品 > 饮料,然而按照标签,则可以是 食品、饮料、咖啡(可乐)、含咖啡因、下午茶 等等。其信息更加丰富,同样的信息量用分类系统实现则会无比麻烦。这样的灵活性能够在日后进行统计分析时带来诸多便利。

因而,我的账本中每笔支出不过四项信息:金额、描述、交易日期、交易标签

Notion 提供了一种名为 Database 的模块,如果你也是 Notion 用户,你一定听说过。就如其名,一个 Database 就是一个数据库,以行列组成。一行是一条数据,列则表示了数据的各种信息。Notion Database 的优点在于它强大、灵活又易于使用,

上述账本使用 Notion 建立,不过是 3 分钟的事情:

Notion 账本

为易于使用,金额的类型可选择为 Number,交易标签设置为 Multi-select,交易日期则是 Date。

Notion 在移动设备上使用有些不便,因此我每天固定晚上的某个时间一次性在电脑上录入当天的所有支出,时间粒度以「天」为单位,日常消费没有记也无伤大雅,晚上补上即可。

账本回顾与监控

对过往支出进行回顾,规划未来支出,监控当前已经花了多少钱,这样才能真正达到修正消费习惯、节约金钱的目的。这一步非常重要。

Notion Database 提供的 View 与 Filter 功能在这里大放异彩。View 是数据库的一个视图,Filter 则可以通过设定的条件过滤数据,只显示我们需要了解的数据记录。

我举一个例子。如果现在想要看看我除一日三餐外,在吃的上花了多少钱,应该怎么做?

首先,新建一个名为「非三餐食品支出」的 View,并在这个 View 中添加 Filter,设定为:

    交易标签 Contains 食
And 交易标签 Does not contain 三餐

也就是过滤出标签包含但不包含三餐的支出记录。设定好 Filter 后可以看到当前 View 的数据随即更新。

统计非三餐食品支出

一览无遗。利用 Filter 可以对支出进行相当灵活的归类筛选,这是 MoneyWiz 等现有记账软件不能实现、不便实现的。这是使用 Notion 的简洁、实用,又不失强大之处。

更棒的是,由于 Notion 是一个线上服务,基于其 API 可以实现更多有趣、实用的玩法。

我通过回顾以往的支出制定了本月的支出限额:总支出最多 ¥6000,非三餐食品支出最多 ¥2000,大额消费(金额高于 ¥150)小于 10 次。我基于开源的 Notion API 编写了一个 API,并基于新小组件实现了一个 iOS 14 小组件,在手机主屏上实时展示我当前的总支出、重点控制支出、大额消费次数及剩余预算,效果如上图所示,也相当不错。

结语

以上就是我一个多月以来使用 Notion 进行记账的实践细节。利用 Notion API 进行账目统计的实现目前还比较粗糙,就不开放了;小组件的代码在这里,如果你安装了新小组件 App 可以一试。但是新小组件目前 bug 很多,请随缘。

  •  

React Server Component 可能并没有那么香

前段时间 React 团队发布了一项用于解决 React 页面在多接口请求下的性能问题的解决方案 React Server Components。当然该方案目前还在草案阶段,官方也只是发了视频和一个示例 demo 来说明这个草案。 Server Components 官方在视频和 RFC 中说明了产生这个方案的主要原因是因为大量的 React 组件依赖数据请求才能做渲染。如果每个组件自己去请求数据的话会出现子组件要等父组件数据请求完成渲染子组件的时候才会开始去请求子组件的数据,也就是官方所谓的 WaterFall 数据请求队列的问题。而将数据请求放在一起请求又非常不便于维护。 既然组件需要数据才能渲染,那为什么接口不直接返回渲染后的组件呢?所以他们提出了 Server Components 的解决方案。我们暂且不管这其中的逻辑有没有道理,先来看看该方案的大体流程是怎样的。 方案的大概就是将 React 组件拆分成 Server 组件(.server.tsx)和 Client 组件(.client.tsx)两种类型。其中 Server 组件会在服务端直接渲染并返回。与 SSR 的区别是 Server Components 返回的是序列化的组件数据,而不是最终的 HTML。 可能带来的问题 通过接口将组件和组件的数据一并返回的方式带来了打包体积的优势,但是它真的能像 React Hooks 一样香吗?我觉得并不然。 接口返回 常规做法里前端 JS 中加载组件,接口返回组件需要的数据。而 React Server Components 中则是将二者合二为一,虽然在打包体积上有所优化,但是明显是把这体积转义到了接口返回中。特别是在类似列表这种有分页的请求中,这种劣势会更明显。明明组件只需要在初始的时候进行加载,但是因为被融合进接口里了,每次接口都会返回冗余的组件结构,这样也不知道是好还是不好。可能后续需要优化一下接口二次返回只返回数据会比较好。 服务器成本问题 这里所说的服务器成本有很多,首先是机器本身的成本。将客户端渲染行为迁移到服务端时候势必会增加服务端的压力,用户量上来之后这块的成本是成量级的在增加的。关于这个问题,官方提供的回复是随着服务器的成本降低势必 Server Components 带来的优势会抵消这块的劣势。 Question: This might become more expensive for applications. In the search demo, finding those search results plus rendering them on the server is a more expensive operation than just an API call sent from the client. Reply: We are moving some of the rendering to the server–so it’s true that your server will be doing more work than before. But server costs are constantly going down, and far more powerful than most consumer devices. I think React Server Components will give you the ability to make that tradeoff and choose where you best want the work to be done, on a per component basis. And that’s not something that’s easily possible today. via: 《RFC: React Server Components》 不过以目前我所在的业务情况来看,服务器的成本还是非常贵的,为了降低成本大家纷纷将逻辑下发到边缘计算甚至是客户端处理。一方面是为了节省成本,另一方面也是为了降低压力加快处理。 除了机器本身的成本之外,请求的成本也会增加。毕竟除了数据请求之外还要处理组件渲染,而且这块作为组件耦合不好进行拆分。相比较常规方案,使用 JS 文件加载组件到客户端,接口单纯返回数据,这块的时间成本增加了非常多。特别是常规方案中 JS 文件加载完之后是在浏览器中缓存的,后续的成本非常小。 体积问题可能还好,但是请求时间增加了这个可能就非常致命了。 心智负担 这点在 RFC 中也有说明。由于 Server Components 中无法使用 useState, useReduce, useEffect, DOM API 等方法,势必这会给使用者带来大量的心智负担。虽然官方说会使用工具让开发者做到无感,且会提供运行时报错,但是我相信光是想什么时候需要写 Server Componet 什么时候需要写 Client Component 就已经脑壳疼了吧,更别提还有个 Shared Component 了。 另外还有就是增加了跨端的流程之后,调试的成本也会变的非常高。别说很多人没有服务端的经验,就算是有相关经验的同学可能也没办法很好的在服务端进行快速定位。关于这个问题官方提供的说法是可以依赖内部的错误监控和日志服务。 回归问题的本质 让我们回归到问题的本质,React Server Component 的目的其实是为了解决接口请求分散在各组件中带来的子组件的数据请求需要等待父组件请求完成渲染子组件时才能开始请求的数据请求队列问题。那么除了 Server Component 之外没有其它的解决方案了吗?其实不然。 import React, {useState, useEffect} from 'react'; import ReactDOM from 'react-dom'; function App() { const [data, setData] = useState([]); useEffect(() => { fetchData.then(setData); }, []); return ( <div> {!data.length ? 'loading' : null} <Child data={data} /> </div> ); } function Child({data}) { const [childData, setData] = useState([]); useEffect(() => { fetchChildData.then(setData); }, []); if(!data.length) { return null; } return ( <div>{data.length + childData.length}</div> ); } ReactDOM.render(<App />, document.querySelector('#root')); 如示例代码所示,只要加载组件,但是在无数据情况下不返回 DOM 也是可以做到子组件的数据先请求而无需等待的。当然这种需要认为的在写法上进行优化,但我也仍然认为比大费周章的去做 Server Component 要好很多。 至于 Server Component 带来的打包体积优化这个问题,我觉得 RFC 里面的评论说的非常的好。”比起 83KB(gzip 后大概是 20KB)打包体积,我觉得在项目中为了格式化日期使用一个 83KB 的库这才是更大的问题。“ Removing a 83KB (20KB gzip) library isn’t a big deal, I would say the bigger problem here is that you’re using a 83KB library to format dates. via: 《RFC: React Server Component》 实际上官方列举的两点关于日期处理以及 Markdown 格式处理的库,可以看到都是针对于数据进行处理的需求。针对这种情况如果觉得这块的体积非常”贵“的话完全是可以让服务端将格式化后的数据返回,这样岂不是更小成本的解决了这个问题? 后记 看完 《RFC: React Server Component》 中所有的讨论,大部分人对 Server Component 还是持不赞成的态度的,认为它可能并没有像 React Hooks 那样解决业务中的实际痛点。就目前暴露的提案,我个人也觉得 Server Component 是弊大于利的。目前就期望官方如果要实现的话能解耦实现,不要影响未使用 Server Component 的 React 用户打包体积。 当然该提案我觉得不是没有好处,它最大的好处我个人认为是带来了 React 组件序列化的官方标准。为多端、多机、多语言之间实现 React 组件交流提供了基础。基于这套序列化方案,我们可以实现组件缓存存储,多机器并发渲染组件等。至于多语言实现也是在 RFC 讨论中大家比较关心的问题,通过这套序列化标准让其它语言去实现 React 组件也不是没有可能。
  •  

好耶!是树莓派🍓!

树莓派的到来实在太令我惊喜。之前和某人聊天时提起过我有想买一个树莓派搭 NAS 的事,说过也就过了,哪曾想她真的记下来,并且背着我火速下单了一套直接寄到刚搬进去的新住处。她以前玩过 Arduino,对树莓派也知道一些,准备和我一起折腾些有趣的东西。

好,你们可以开始羡慕了😉。

我知道淘宝上的卖家总爱搭配一些良莠不齐的配件一同出售,但是某人深得我心,给我买了主机和全套官方配件,包含已经烧写了官方 Raspbian 系统的 SD 卡。我并不需要图形界面,所以打算使用 Ubuntu Server 作为操作系统,因而只是把 SD 卡插上去开机草草看了一眼 Raspbian 系统长啥样,就直接上 Ubuntu 20.04 了~ 然后一顿操作按照自己的习惯配置好终端环境后,情况大概这样:

拿到手的是一台树莓派 4B。树莓派本身应该很多人都熟悉了,其实就是一个麻雀虽小五脏俱全的微型电脑。体积非常小巧,也很轻,在这样的身躯下其性能却足够进行一些小型折腾和实验。接口方面,包含 2×Mini-HDMI 接口,2×USB 2.0 接口,2×USB 3.0 接口,音频接口,RJ45 端口,应该说能满足日常的外设需求。此外,这款树莓派主板上提供了 40 个可用于自主扩展的端口,称为 GPIO(general-purpose input/output) 接口,这是树莓派超级强大的一个重要原因。套件中包含一个小风扇,用于散热,就依靠 GPIO 端口工作。

树莓派 GPIO 接口图

我按照说明将风扇的三根线分别接到了板子上编号 4(5V power)、6(Ground)、8(GPIO 14) 的三个端口上,风扇顺利启动。然而,这风扇贼吵……而且是常开的,搜了一圈,经过了若干次失败和尝试后终于实现了用 Python 根据 SoC 温度控制风扇启停。代码如下(需要先安装 RPi.GPIO 库)。

#!/usr/bin/python3

import time

try:
    import RPi.GPIO as GPIO
except RuntimeError:
    print("Error importing RPi.GPIO!")

def cpu_temp():
    with open("/sys/class/thermal/thermal_zone0/temp", 'r') as f:
        return float(f.read())/1000

def main():
    channel = 14
    GPIO.setmode(GPIO.BCM)
    GPIO.setwarnings(False)

    # close air fan first
    GPIO.setup(channel, GPIO.OUT, initial=GPIO.LOW)
    is_close = True

    while True:
        temp = cpu_temp()
        output = ' '.join([str(time.ctime()), str(temp)])

        if is_close:
            if temp >= 65:
                GPIO.output(channel, GPIO.HIGH)
                is_close = False
        else:
            if temp < 55:
                GPIO.output(channel, GPIO.LOW)
                is_close = True

        if is_close:
            output += ' fan off'
        else:
            output += ' fan on'

        with open('/var/log/autofan.log', 'a+') as f:
            print(output, file=f)

        time.sleep(2.0)

if __name__ == '__main__':
    main()

以上代码可以使用 systemd 作为开启自启的服务,然后就不用再管树莓派的温度问题了(发热真的很厉害)。


我宣布,如今我的个人主页、当前这个博客、个人Wiki 都由这台树莓派提供服务!

这是交给它的第一个重大任务。我连夜给它布置好了 LNMP 环境,然后使用 acme.sh 在上面申请好了 SSL 证书。得益于采用 Maverick 生成我的网站,站点的迁移简单到难以置信:大概就是改动部署脚本中的三个字母。

这也就是说,以后如果我搬家或者家里断网,这个网站也会跟着挂掉🤣。但是想想吧,这就是早期互联网的样子啊!颇有些文艺复兴的感觉,我很喜欢。

另外,我在树莓派上插了一块 4T 的硬盘,里面保存了我的多年原始积累,终于实现了我在世界任何角落进行艺术鉴赏的梦想。接下来的打算是让树莓派成为家庭智能中枢,大概包括和智能家居联动、作为 AirPlay 投屏服务器、作为 TimeMachine 备份服务器等等。

好久没有这种兴奋的感觉了!真好,感觉有无穷的可能性等待探索。我充满了期待。

  •  

倒带贰零贰零

疫情也好,求职也好,毕业也好,获得也好,失去也好,对现在的我而言仅仅是没有实感的一个个词汇。在我身上刻下印记的不是这些节点,而是过程。

我一直信奉的,要设法体验一切身为人的感觉,今年着实体验了不少。不论是「小镇青年」还是「都市职人」,不过是路径不同而已,该有的挣扎、愤怒、怅然、愉悦、欣喜,一个都不会少的。

一月份回家过年时我断然想不到今年会这么特殊。跟父母吃饭时多次说起:「若不是这个疫情,我恐怕不会有这么久与家人同住一个屋檐下的机会」。这是实话,尤其是想到过去十五年间我一直与家人聚少离多,这样的机会显得弥足珍贵。即使是身处在关系如此僵硬的家里,我还是能感觉到自己态度逐渐软化,因而才有了上一篇小镇青年的文章。我认真思考过:也许呆在家,留在那个小镇,是我的归宿。

可我还是打了一场败仗,无奈、激动、期待、紧张地,我还要留在北京。不亲身经历这个阶段,真的很难体会选择的难处。当我向尽力克制失望的父母,向我曾许下承诺的朋友告知我的最终决定时,尤其困难。这是 2020 年我最大的意难平。


八月底返校后几乎只忙于两样事务:毕业与求职,被这两座大山交替爆锤,后期甚至都麻木了。日复一日焦躁不安地写代码、写论文、投简历、笔试、面试,多少有些磨灭斗志。但结果还算好。结束那一刻固然欣喜,但我想这个过程最珍贵的收获不是录用信,而是求职季一同战斗的同伴间的情谊。这甚至称得上是「战友情」。到现在我仍然怀念拖着面试完疲惫的身体,开一瓶烧酒与相同疲惫的朋友一起聊天放松的日子。很多次跟自己说:「拿到这家我就结束秋招!」,想着大致有个工作养活自己就行了。终究还是靠着朋友的激励继续投下去。

因为听说过太多亲近的朋友甚至是同门师兄弟因为求职时的竞争关系而撕破脸皮的故事,我才十分珍惜能与我互通有无,「串通一气」的同伴。在千万求职大军中,我所认识的不过几十人。若在这样的格局下也要「内卷」,那么眼界实在太小。


这是今年的第二个主题:与人的联系。在学校的最后一个学期,同时也成为了我学生生涯最愉快的一个学期。我所期待的实验室氛围终于建立起来。学术上,真正有意义的学术分享会(不,不是组会)开展起来了,我们有意地不让老师参与,因而可以在分享会上肆无忌惮地提最白痴的问题,争论最琐碎的细节。事实证明这是值得的,不仅是对我,对刚进组的后辈而言更是如此。我一直对进组时无人领路只能自己摸索的事耿耿于怀,这让我无比孤独,走了太多弯路。今年在留学回来的大师兄带领下终于有所改变,我由衷开心。我想,同处一个师门,分享与交流实在是再重要不过了。

因为争取到了经费,我们屯了大量零食,饮料,以及……酒。与组内同学喝酒撒欢也是今年才有的事。在实验室呆到十点十一点左右,常常心照不宣地一人开一瓶烧酒,坐在一起喝酒聊天。也有过分的时候,比如在公共会议室里喝吐,喝到不省人事。都是太有趣的回忆。

也是因为认识了,或者说「重新认识了」,许多人。我越来越珍惜身边人,越来越容易与人共情,越来越想做一个有温度的人。拖更博客多少也与我逐渐现充有关。当现实生活令我满足时,我并不太想活在线上。


不久前出去吃饭时与朋友聊天,发现对方与我如此相似,有着几乎相同的困扰。回来后发了一条仅自己可见的微博:

今天出去喝酒,深聊之后发现一个和我情况(精神状态)和我非常像的人。我感觉很无助,因为自己也没有解决好,帮不了她。想了好久,跟她抱在一起,说我们一起加油吧。 ​​​ ​​​​

我也许是多虑了,因为对方比我更积极,有寻求相应的帮助。我的话,似乎一直以来也都能自己调节好。

我相信大部分的焦虑都来自期望与现实的不匹配,所以治本的方法是要搞明白「自己」的位置,并且在认识本质后坦然接受。这真的是太难的一个课题了。


直接新启一个文档,取名「倒带贰零贰零」来为自己的这一年定论,我有些底气不足。太多纵然逝去的想法,太多被时间抹平的情绪。在博客上假死时,我在微博与推特发了许多零星的东西,有琐碎的,也有相对长一些的。我一边翻看这些内容,一边敲出这些字。现在唯有一个想法:

请往前走,不要在此停留

  •  

2020 岁末总结

不知不觉,2020年都要过去了。今年因为疫情的原因,感觉时间过得特别的快,一不留神,一年就这么过去了。而今年发生的很多事情也都围绕着疫情在改变着。 🚑 疫情 每当你想尝试放松的时候,你都会被工作扼住命运的喉咙。今年要说什么对我的影响最大,非疫情莫属了。当我还在家里做着疫情很快会过去的美梦的时候,不知不觉就已经被疫情专题页的工作搞的日夜颠倒了。可能是因为丁香园疫情专题页的高流量,不知道为何我们突然之间也投入了大量的人力去开发疫情专题页了。整整持续了两个多月的高强度工作让我身心俱疲。 看看我当时发的状态,真是太丧了。印象比较深的是有一天搞到凌晨3点才休息,结果8点的时候就被领导电话叫起来说是线上有 Bug 赶紧看看。我… 😓真是棒呢! 截两个图留存下被大 Boss 直接跟项目带来的恐惧,名字就不留了,看头像懂得人应该都懂。 好在三月之后慢慢开始复工,回到公司之后状态慢慢的就恢复过来了。甚至感觉还能再来个疫情呢(大雾! 现在回过头来不得不感慨,之前一天 50 多次上线,每次上线都没走 QA 就上了,我头是有多铁啊!全程面向微信开发,产品微信发需求,设计微信发设计图,开发微信发上线记录。我们都有美好的未来……个鬼啊! 🌎 Drone 看过我之前文章的同学就知道,Drone 是我非常喜欢的一款 CI/CD 的工具。它的可扩展性非常高,适合用来在企业内部进行 CI/CD 服务的接入和部署推广。奈何内部环境限制,Gitlab 版本过低,网段隔离导致推广起来还挺费劲的。 不过两年后环境发生了很大的变化,Gitlab 版本升级上来了,网段隔离的问题也发现了解决方案。顺顺利利的就部署了起来。而且老板也想统一推广 CI/CD 这块,正好顺着这个风推广给大家了。 为了帮助大家能快速的接入 Drone,也开发了很多内部服务相关的插件,包括内部项目、容器上线的,内部 IM 消息通知的插件。而且高兴的是,除了我之外,公司内还有其它团队的同学也有在使用 Drone,写的插件对他们也很有帮助。 自己一直想推的事情总算有一点小小的进展,而且发现还有同好,真是很开心呢。 👩‍🎨 设计云 设计云是今年团队因为蓝湖收费产出的一款类似于蓝湖的设计交付工具。后续转手到了我这里进行开发维护。它给我最大的收获是打开了 Sketch 插件开发的大门,原来开发一款 Sketch 插件其实没有想象中的那么困难。 当然开始的时候还是非常难的,经常碰到问题需要去请教之前做的小伙伴。不过后来基本就驾轻就熟了。然后反向回馈 Sketch 插件社区一些项目中反馈到的问题。成为了插件核心插件 skpm/skpm 的贡献者,想想都还挺激动的呢。 另外我负责了这个项目包括前端、服务端、客户端上的全部重构,所以这块能总结的东西其实也非常多。我是特别喜欢将自己的知识总结成文字分享给大家的,所以那段时间也一连产出了好些文章,都是从这个项目中反馈出来的经验。 Sketch 插件导出切片 如何制作 Sketch 插件 使用 SVG 制作加载动画 如何使用 ThinkJS 优雅的编写 RESTful API 🙏 司徒正美 4月1日,惊闻正美老师过逝的消息,一度还以为是假消息,后来经过正美老师的室友确认。正美老师是在前端圈非常有技术声望的人,他的离世震惊了圈内人士。 我和正美老师的交集在于我在业务上使用了正美老师之前开发的类 React 框架 anu.js。选择这个库的原因有以下几点: 满足了我们的 React IE 兼容性要求 它比较小巧,代码清晰易懂我们自己维护也不费劲 正美老师个人在前端框架这块的技术声望 后续该框架也在我们的项目中扮演越来越重要的角色,我也不遗余力的在推广其他业务的小伙伴有类似兼容性需求的时候使用该框架。而正美老师的突然离世则让该框架成了没爹的孩子。 为了保证业务的可维护性,以及不让正美老师的遗作就这么销声匿迹,我慢慢开始了 anujs 项目的权限申请,主要是 npm 模块的权限申请以及 Github 仓库的权限申请。这些由于账号主人的离世,都只能去邮件和网站管理沟通了。 在经历了一段时间的等待之后,5月1日我成功获得了 anujs 的 npm 模块的发布权限。而 Github 的权限则非常可惜的没有申请下来,最后我们采用了 fork 的方式继续维护。 目前我们项目组有一位对 React 框架有经验的同学在负责维护该框架,主要是一些日常的 Bug 修复。感谢正美老师为我们带来这么好的作品,也愿他在天堂安息。 🌋 垃圾评论 2020年的年末,我不是很开心,因为……我被网暴了。起因是我发布了一篇 《基于 Serverless 的 Valine 可能并没有那么香》 的文章。文章里描述了一款第三方评论工具 Valine 存在的一些安全问题,然后在文末介绍了我为了解决该问题开发的高度兼容 Valine 的评论系统 Waline。 也不知是哪位无聊之人使用我的昵称和邮箱在全网使用 Valine 的博客中套用我的身份给它们发送了大量该文章和 Waline 系统的垃圾评论广告。导致大量的博主到我的博客上投诉甚至谩骂我。 这本是有人利用了 Valine 系统本身的漏洞问题制作的一场恶作剧,本来解释一下大家应该也都能理解就这么过去了。不过其中有一位用户说什么也不相信这不是我本人干的。在我的博客上疯狂的辱骂我。本来他发些垃圾评论我觉得也没什么,大不了定时清理下数据就好了。但是他开始回复我的博客的其它评论。而评论是有回复通知的,这无疑对其它博主造成了困扰。 为了阻止他,我紧急增加了发送频率限制、关键词过滤、IP黑名单等常见的反垃圾评论操规则。当他发现我有 IP 黑名单之后,还会尝试换一些 IP 来操作。好在经过几次的 IP 黑名单完善之后,慢慢的发的也就少很多了。不过后续更过分的事情又来了,他又顺着我的友链列表,去到我的友链博客下面使用我的信息伪造我的发言。真是可笑的事情啊,屠龙者终成恶龙! 💻 后记 其实今年发生的事情非常多,组织也发生了很大的变化,但有些事情真的无法用言语表达出来,就让它默默的存在我心里吧。新的一年 Flag 就不立了,希望在新的一年里能够在技术上有更好的突破,折腾一些更有意思的东西吧。
  •  

RSS 订阅地大新闻 - CUG News

✇遐说
作者Dorad

经常关注不到学校的一些通知之类的,而学校网站又没有 RSS 订阅。

使用 Feed43 创建订阅源,需要的同学可以订阅。

  •  

Word 公式字体替代方案

✇遐说
作者Dorad

word 自带的公式编辑器挺好用,相对 MathType 需要破解、行间距经常出莫名其妙的 bug 而言, word 自带编辑器好用很多。但是,其自带字体为 Cambria Math,无法调为 Time New Roman 字体。在网上检索发现,可以使用 XITS Math 等方案进行替代。

  •  

静态博客如何高性能插入评论

🌏 前言 我们知道,静态博客由于不带有动态功能,所以针对评论这种动态需求比较大众的做法就是使用第三方评论系统。第三方评论的本质其实就是使用 JS 去调取第三方服务接口获取评论后动态渲染到页面中。虽然它很好的解决了这个问题,但是由于需要请求接口,在体验上远比动态博客的直出效果要差很多。所以当我把博客从动态博客 Typecho 迁移到静态博客 Hugo 上来时,就一直在思考这个问题。直到我看到了 Hugo 的 getJSON 方法,发现原来静态博客也是能够像动态博客一样直出评论的。 大部分的静态博客的原理是解析存储内容的文件夹,使用一些模板语言遍历数据生成一堆 HTML 文件。而 Hugo 除了解析 Markdown 内容之外,还支持额外的数据获取方法 getJSON。由于有了 getJSON 方法的出现,我们可以实现在博客编译构建过程中动态的去获取评论接口数据,将其渲染到页面中,实现评论数据的直出效果。关于 getJSON 的更多介绍,可以查看 Hugo 文档数据模板一节。 🎃 方案 高性能方案基本思路是在需要评论数据的地方通过 getJSON 方法调用接口获取评论数据并进行模板渲染。当评论更新的时候,我们需要触发重新构建。实现这个方案依赖三个关键要素: 构建过程支持调取接口获取数据 评论服务提供 HTTP 接口返回数据 博客部署服务支持钩子触发重新构建 我的博客使用的是 Hugo 静态博客系统,如上文所说通过 getJSON 即可解决第一个问题。而我的评论服务使用的是自研的 Waline 评论系统,它提供了评论数、评论列表、最近评论等基础接口满足我们的数据获取需求。并且 Waline 提供了丰富的钩子功能,支持在评论发布的时候触发自第一方法。我的博客部署在 Vercel 上,它提供了 Deploy Hooks 功能,通过 URL 即可触发重新构建。也就是说我只要在 Waline 评论发布的钩子中调用 Vercel 的钩子 URL 触发重新构建即可解决第三个问题。 🥪 实现 我的博客上有三处地方和评论有关,分别是首页侧边栏的最近评论,文章标题下方的评论数,以及文章详情页底部的评论列表展示。 🍞 最近评论 Waline 最近评论接口:文档 {{ $walineURL := .Site.Params.comment.waline.serverURL }} <h2 class="widget-title ">最近回复</h2> <ul class="widget-list recentcomments"> {{ $resp := getJSON $walineURL "/comment?type=recent" }} {{ range $resp }} <li class="recentcomments"> <a href="{{.Site.BaseURL}}{{ .url }}">{{ .nick }}</a>:{{ .comment | safeHTML | truncate 22 }} </li> {{ end }} </ul> 🧀 文章评论数 Waline 获取文章对应的评论数接口:文档 {{ $walineURL := .Site.Params.comment.waline.serverURL }} {{ $count := getJSON $walineURL "/comment?type=count&url=https://imnerd.org/" .Slug ".html" }} <a href="{{ .Permalink }}#comments" title="{{ .Title }}"> <i class="fas fa-comment mr-1"></i> <span>{{- if gt $resp 0}}{{$resp}} 条评论{{else}}暂无评论{{end -}}</span> </a> 🍯 评论列表 评论列表由于有分页的存在,不像最近评论和评论数一样简单的调用接口即可。先获取评论数,发现有评论时先获取第一页的评论,主要是用来获取总共有多少页评论。之后再从第二页开始循环获取评论数据。最终将获取到的数据全部存到 {{$scratch.Get "comments"}} 数组中,使用模板语法渲染该数组数据即可。 {{$baseUrl := .Site.Params.comment.waline.serverURL}} {{$slug := .Slug}} {{$count := getJSON $baseUrl "/comment?type=count&url=https://imnerd.org/" $slug ".html" }} {{$scratch := newScratch}} {{$scratch.Add "comments" slice}} {{if gt $count 0}} {{$comments := getJSON $baseUrl "/comment?path=/" $slug ".html&page=1&pageSize=100"}} {{range $cmt := $comments.data}} {{$scratch.Add "comments" $cmt}} {{end}} {{$totalPages := $comments.totalPages}} {{if gt $totalPages 1}} {{range $page := seq 2 $totalPages}} {{$comments := getJSON $baseUrl "/comment?path=/" $slug ".html&pageSize=100&page=" $page}} {{range $cmt := $comments.data}} {{$scratch.Add "comments" $cmt}} {{end}} {{end}} {{end}} {{end}} <div class="vcards"> {{range $cmt := $scratch.Get "comments"}} <div class="vcard" id={{$cmt.objectId}}> <img class="vimg" src="https://gravatar.loli.net/avatar/{{$cmt.mail}}?d=mp"> <div class="vh"> <div class="vhead"> <a class="vnick" rel="nofollow" href="{{$cmt.link}}" target="_blank">{{$cmt.nick}}</a> <span class="vsys">{{$cmt.browser}}</span> <span class="vsys">{{$cmt.os}}</span> </div> <div class="vmeta"> <span class="vtime">{{dateFormat $cmt.insertedAt "2006-01-02 03:04:05"}}</span> <span class="vat">回复</span> </div> <div class="vcontent" data-expand="查看更多..."> {{$cmt.comment | safeHTML}} </div> <div class="vreply-wrapper"></div> <div class="vquote"> {{range $cmt := $cmt.children}} <div class="vh" id="{{$cmt.objectId}}"> <div class="vhead"> <a class="vnick" rel="nofollow" href="{{$cmt.link}}" target="_blank">{{$cmt.nick}}</a> <span class="vsys">{{$cmt.browser}}</span> <span class="vsys">{{$cmt.os}}</span> </div> <div class="vmeta"> <span class="vtime">{{dateFormat $cmt.insertedAt "2006-01-02 03:04:05"}}</span> <span class="vat">回复</span> </div> <div class="vcontent" data-expand="查看更多..."> {{$cmt.comment | safeHTML}} </div> <div class="vreply-wrapper"></div> </div> {{end}} </div> </div> </div> {{end}} </div> 🍳 构建触发 Waline 在评论发布、更新和删除阶段都支持自定义钩子,在钩子中触发 Vercel 的构建钩子即可完成发布评论重新构建的流程。 按照如下内容修改服务端部署的 index.js 文件,查看文档了解全部的 Waline 钩子。 const Waline = require('@waline/vercel'); const https = require('https'); const buildTrigger = _ => https.get('https://api.vercel.com/v1/integrations/deploy/xxxxx'); module.exports = Waline({ async postSave(comment) { if(comment.status !== 'approved') { return; } buildTrigger(); }, async postUpdate() { buildTrigger(); }, async postDelete() { buildTrigger(); } }); 🍾 后记 通过以上操作,就能在不损失用户体验的情况下实现评论数据的动态支持了。有些人可能会担心是否会在构建阶段造成超多的接口请求。这里大可不用担心,Hugo 自己会在构建的时候做接口的缓存,同 URL 的接口调用会走缓存数据而不会重新调用。 除了用户体验之外,由于只会在构建的时候触发数据的获取,针对有调用次数配额的第三方评论服务也能节省额度。当然,理论上构建次数是远小于访问次数的,所以额度节省的结论是能成立的。如果说你的构建次数要比访问次数还要大的话,那这种方法就无法节省额度了。 当然这种方式也会有带来些问题,主要是评论的更新没那么快。好在 Hugo 的构建速度非常快,一两分钟的时间也能接受。而针对用户评论的发布,则可以通过评论发布后先假插入缓解该问题。
  •  

当一个小镇青年

我从来没有像今年这样待在县城的家里这么久。出生于藏区小镇,长于县城,在四川第二大的城市读高中,在首都念大学。我一直在迁徙,所处的环境越发广阔,所见所闻在变多。然而滑稽的是在此特别的、临近毕业的紧要关头,似乎有意安排似的,我被困在这个小镇。

关于居家隔离、远程办公已经有很多论述。李如一在《一天世界》播客第七十九期里说:

……有的人甚至为此(居家隔离办公)寻求心理医生的帮助。我当时一听觉得,哇,那如果这样的话,像我们这样的人不就是病入膏肓了吗?

李如一口中的「我们这样的人」指的是基本不用到固定办公场所坐班的人。某些职业天生具有远程特性,比如记者、作家、独立开发者等等,这些人已经很适应透过屏幕办公了。但是也正如李如一所说,可能不适应 social distancing 才是正常的,而他那样的人才应该看医生。

若不是这次疫情,我可能没意识到自己几乎丧失了和家人在同一屋檐下朝夕相处的能力。春节期间我的预测是至多需要在家里住到三月,然后返校。三月 —— 大概就是我和家人互相看不惯的临界点。现在快八月了,我敲下这行字时还坐在家里的餐桌前,听着窗外淅淅沥沥的雨声。

我和家人几乎已经过了互相讨厌的阶段,进入井水不犯河水的生活模式。我一度纳闷世界上怎么会有到中年还跟父母住在一起的人?我是绝对受不了的。现在明白,也许我才应该看医生。

身处小镇与身处都市最大的不同是什么呢?也许就是人与人的关系:小镇就是中国,而一群人就是小镇。


这半年来体重跌了不少。母亲总在我耳边说,你看吧,这样的生活适合你,瘦下来健康。但我最清楚体重跌下来的原因。

体重下跌,来自于不健康的精神状态和健康的生活状态。当我在家办公,我没有必要与任何人打交道,结果便是我不与任何人打交道。而在小镇,拒绝与人打交道是行不通的,只面对自己时,负面情绪就开始蔓延。

一个重要的人对我说:我觉得你没有很爱自己。后来我明白了,如果我在乎自己觉得重要的人,那我要爱自己,只有这样才有可能维护健康的关系,否则一切都是只存在于脑海里的烟花。

六月份搞砸了一些事,自己也受了一点不大不小的打击。好在有朋友不离不弃,先是陪我长谈,大骂了我一顿;接着教给我上面这个道理。我逐渐缓过来之后,带着愧疚决定成为更好的人。

每天六点半准时起床,出门晨跑;晚上在瑜伽垫上做一些力量训练;咖啡、糖都减少摄取;晚上尽量在午夜前睡觉。效果很明显。这些天县城雨季,早上经常下雨,跑步也无从进行。有时蒙头一睡再醒就是九点,我会有点惶恐,感觉自己浪费了一个上午。晨跑确实改变了一些东西。


关于自我认识,我曾经与好几个朋友聊过,但似乎并不是每个人都有类似困扰。仅当理想、见识、能力三者不匹配时,这种苦恼才尤为严重。我在 Twitter 上说:

...不过我最近的感受是,相比起整天把正义、理想、社会秩序挂在嘴边的人,喜欢吃、关注自身、关注身边事物的人宜人度显著更高。

但我从不避讳「理想」这个词。大家羞于谈起自己的远大理想和细腻情绪,只顾着说一些不上不下的废话。这是一种逃避,因为你对自己的期望就是你的理想。不去思考理想,那就活得不明不白。只是不可否认:认识自己往往伴随着失望和痛苦。

去年双十一趁打折买了各色的 DNA 测试,收到报告上赫然写着:「创造力一般」,我大失所望。这样从天而降的定论我可以不服,但某些蛛丝马迹却不得不服。

在小镇生活的这大半年,我逐渐心安:挂在嘴上的、心心念念的远方,它终究只是远方;眼前可感知的,是楼下面馆,是菜市场喧嚣,是步行街的熙攘。我终究是一个俗不可耐的小镇青年。

  •  

html table 批量转 Excel(xlsx)

✇遐说
作者Dorad

由于实验设备导出的数据为 html 格式,单个 html 文件达到 10-200M。采用 python 脚本,批量将 html 中的 table 批量转为 Excel,并导出到文件。

  •  

科研资料备份同步方案(FreeFileSync)

✇遐说
作者Dorad

Introduction

作为一名科研民工,科研数据无异于身家性命。无论是数据丢失,还是出差旅行,数据的便携性和安全性都是我们日常的痛点。
本篇介绍一种低成本的科研数据备份同步方案,能够完成科研数据的多端同步。得益于我离校前进行过文件同步,目前在家科研资料齐全。故将该方法与大家分享。
主要工具有:移动硬盘一块(1T, 容量根据实际需要购买)+FreeFileSync软件(开源免费)

  •  

海量文献自定义格式整理方法(EndNote)

✇遐说
作者Dorad

Introduction

面对海量参考文献,需要调整文献格式时,由于各期刊或材料要求不一,可能无法找到合适的格式,而一条条手动调整文献格式又十分繁琐。
EndNote 作为一款成熟的文献管理软件,允许用户自定义文献输出格式。
本篇主要介绍 EndNote 自定义文献导出方法。

  •  

记挖竹笋

✇遐说
作者Dorad

Inrtoduction

清明节被老爹带回老家扫墓,折腾得我~脑壳疼。
然后就被带去挖竹笋,原因在于我妈远程遥控指挥,冒得办法鸭。心里苦。
不过收获还是蛮多滴搞了两大袋!竹笋也很大!

  •  

日常实用小软件

✇遐说
作者Dorad

Introduction

有些比较冷门的小软件,很少用到,但确实很好用~有时候想用的时候又找不到!
So,此条就记录日常用到的一些好用的小软件8,方便日后查阅!

  •  

EndNote导入文献期刊名-J无法识别解决方案

✇遐说
作者Dorad

Problem

EndNote X9 中国科学技术大学授权版目前是网络上较为流通的版本,能够解决大家的激活问题。但 EndNote X9 在更新的时候,不知为何将 %J 更新为 %B,导致很多数据库(如中国知网、谷歌学术和百度学术等)的期刊名无法正常导入。

  •  

Redis学习记录

✇遐说
作者Dorad

Redis 学习记录

​ 由于业务需要,最近需要学习 Redis,处理相关业务。以此文写下学习过程。

  •  

文件名批量提取工具介绍及使用教程

✇遐说
作者Dorad

前言

之前在网络中心远教上班,经常会对下载的文件进行统计分析与核对,时常需要对文件名进行提取,原先用的都是bat批处理进行提取,后发现无法满足某些需求。故做此开发。

  •  

win7x64 下 redis 的安装

✇遐说
作者Dorad

近期项目需要使用Redis进行缓存,远程服务器未开外网。本地搭建Redis方便调试。

前言

近期项目需要使用 redis, 故在本地安装以便测试。

  •  

博客十年了

博客十年了,本来什么也不想写的,想一想还是通过web.archive.org网站找找网站历史,然后截图留念下,比较遗憾的是,博客刚起步的那一两年页面没有被完整缓存,然后我又去论坛翻了翻找了一些早期的网站截图。

截图如下按时间顺序排列(可能顺序也不太准,老登记不住了)

1ws.webp
360截图20160131144549351.webp
Snipaste_2025-06-06_14-19-39.webp
Snipaste_2025-06-06_13-53-05.webp
Snipaste_2025-06-06_13-54-06.webp
Snipaste_2025-06-06_13-56-23.webp
Snipaste_2025-06-06_14-15-14.webp
  •  

《跳出幼教看幼教》的自序(摘)


    ……跳出教育看教育,书的这个命题本就是试图达成这种难以达成的境界的一个途径和方法。言下之意,如若没有全局观念,没有系统思维,没有长远视角,只是站在一己狭隘的专业理论和知识,跳不出教育去看教育,就不可能超越世俗的智慧和宁静,那是达不成“看山只是山”的境界的。
    我是一个学习理科出身的人,经由过科学思维的严格训练,对于什么是科学多少曾有过一些肤浅的认识和体验。在我从学习科学转换为研究教育学科的最初那些年代里,我也曾有过类似于《跳出教育看教育》作者的想法,即教育不是科学,至多不过只是准科学。在数十年的教学和研究过程中,我逐渐远离了“看山”的第一境界,艰难地摆脱了“看山”的第二境界,努力地试图向“看山”的第三境界迈进。
    针对社会上谁都可以批判教育,似乎骂得越凶,就越有水平;谁都可以规划教育蓝图,似乎讲得越离奇,就越高屋建瓴的现象,我也曾经怀疑过教育是否有真谛,是否真的很神圣。后来,我有了一些新的领悟,并很随意地写了一篇短文《一潭很深的水》:“我在一处旅游,看到了一个大水潭,水墨黑墨黑的,有人告诉我,这个水潭的水很深,深不可测。设想有一潭水,水很浅,一眼看得见底,围在水潭子边上的一群人也许不会轻易说出这潭子水到底有多深,因为它可以被用工具测量出来,容易被人验证这些说水是多少深的人究竟是对还是错。相反,如果有一潭水,水很深,看不见底,深不可测,那么围在水潭子边上的一群人也许可以‘爱怎么说就怎么说’,甚至可以‘胡说八道’,有时,谁说得离奇,还会被人认为是‘高手’,因为难以认定他的说法究竟是对还是错。……其实,教育就是一潭深不可测的水,变化无穷的水,既有规律又无规律,既是艺术非又艺术。教育不是谁都能想得明白的,谁都能懂得该怎么做的,真正能够把握这潭水深浅的人少而又少。”
    我是一个从事教育学科教学和研究的人,专攻幼儿教育。面对当今我国幼儿教育领域出现的林林总总的现象,特别是教育者被深度内卷的现象,我原本就有写一本《跳出幼教看幼教》书籍的冲动,一个偶然的机会让我知道有一本十几年前出版的类似题目的书籍,这多少给我带来了一些思考和启示。
    对《跳出幼教看幼教》的释题,我有以下一些基本的想法:
    1.书题的关键词是“看”,“幼教”是我“看”的聚焦点;
    2.站在不同立场,会有不同的视角,决定了我看到怎样的幼教;
    3.在书中,我是从多种立场、视角去看幼教的,有宏观的、中观的和微观的,而且还有时序的;
    4.“跳出”意味着我的立场和视角不局限于狭隘的幼教专业本身;
    5.在书中,我较多涉及的是学术理论及其在幼教实践中运用的问题。
  •  

东野圭吾《幻夜》读后感

读《幻夜》的感觉和《白夜行》很像,这两部小说的写作风格和故事结构差不多,写的都是一对男女合伙作案。女的聪明漂亮有头脑,负责总体规划,男的沉默寡言有技术,负责按部实施。他们一共做了五起案子,还杀了人,警察却毫无办法。

搬起石头砸别人的头

地震后男主的舅舅被压在房子底下,为了撕毁借条,男主临时起意,搬起石头砸了他的头,没想到这一幕被女主看到。女主帮助他隐瞒真相,破坏证据,男主从此听命于她。

假扮妓女刺伤手艺人的手

女主为了给男主找工作,假扮妓女勾引一个金属制造车间的工人,将他迷晕后刺伤他赖以工作的右手。伤者被辞退,男主顺利顶替。男主在这个金属加工厂偷偷给女主做了好多作案工具。

制造毒气事件嫁祸于人

女主在珠宝柜台工作,为了偷窃楼层负责人的钻戒设计图纸,成为他的情人。得手后制造毒气事件和变态狂尾随事件,导致楼层负责人被警方怀疑,然后被辞退,女主得以放开手脚制作钻戒,以此牟利。

制造深夜袭击女性事件嫁祸于人

女主美发店的合伙人想单干,女主设计了一个深夜袭击女性的事件,把合伙人的贴身项链故意遗留现场,导致合伙人被怀疑。当合伙人被警方搞的烦躁不安时,女主又主动拯救,合伙人感激涕零,从此再也不提单干的事儿了。

匿名恐吓和借刀杀人

女主父亲的同事想把她一家三口的照片送还给女主,女主怕的要死,因为她是假冒的,为了防止被拆穿和永绝后患,她决定用借刀杀人的手法除掉他。她制作了一封匿名的恐吓信给男主,信上说有男主杀死舅舅的证据,然后骗男主说这个父亲的同事就是写恐吓信的人,男主信以为真,在酒店杀人后分尸。

我觉得这个同事也太实诚了,为了一张照片,拼命地调查女主的住址,反而搭上了性命。

对待爱情和身体的辣眼睛的观点

女主向男主灌输扭曲的爱情观,她说她很爱男主,她的所作所为都是为了两人以后的幸福。翻译过来就是「我利用身体和别人上床,这只是一个手段,真正爱的人是你,你不要犯傻吃醋,不要放在心上,要努力克服这种不安,为了我们以后的幸福。」

无能的警察和如鲠在喉的结尾

他们做了这么多起案子,却能全身而退,按理说这么多事件总会留下蛛丝马迹,不可能做到天衣无缝,可警察就像不存在一样,都没有怀疑到他们身上。只有一个小警察利用工作之余,像私家侦探一样自行调查,而且从不把调查获取的情报告诉别人。看到这里我就担心,这个警察后面会不会死掉,他死了的话,女主的犯罪行为就没有人知道了。

男主发现自己被骗了,自己做了一把手枪,决定杀死女主,就在他要出手的时候,小警察出来制止,然后两人同归于尽。男主的转变太不自然了,之前对女主唯命是从,也毫无怨言,即使后来知道了女主是假冒的,仍然为了保护她和不喜欢的老女人上床,怎么突然就对女主起了杀心?警察制止男主杀女主这段写的和《彷徨之刃》很像,作为读者很想看到坏人受到惩罚,可作者偏不这样写,非要写在关键时刻杀手被制止,让坏人扬长而去,看着很不爽。

呼应《白夜行》的地方

《幻夜》是《白夜行》的续集,看过《白夜行》的人就会知道这两本书的女主是同一个人。书中总是提到服装店,也提到White Night的店名,还有几处呼应《白夜行》的地方。不过这本书和《白夜行》几乎没什么关系,一直到最后也没有指出女主的真实身份和名字,没有看过《白夜行》直接看这本书一点影响也没有。

  •  

⚠️🚗维修遥控玩具车🚗⚠️

✇Hary
作者Hary

 应该是去年差不多五六月份,给浠浠买的一个玩具车,去年玩了几个月,然后过年的时候带回家玩了,有一次玩着玩着跑到沙发下面了,结果浠浠还在那一直按着遥控,可能就给马达憋坏了,就导致右边两个轮子不转了,当时过年在家事情多也没心思修,然后过完年就又带回来,就一直躺在放玩具的箱子里了,她也没想起来玩,就一直在那落灰。
 最近几次出去玩看见人家小哥哥小姐姐玩遥控车就可开心了,就追着人家的遥控车跑,跑到她跟前,又很害怕,又怕又想玩,这才想起来家里还有一个落灰的遥控车,前几天三天端午假期结果下了三天雨,也没出去,净在家翻箱倒柜玩玩具了,就找到了这个,想着拆开看一下是哪里坏了,是哪个线松了或者马达的原因。
1
 拆开试了下果然是右边的马达不转圈了,打开PDD扫一扫,一模一样的玩具车马达,一块钱买了俩,哈哈,果然网上啥都有,怪不得线下各种实体店生意都不好做,焦急的等了三四天,今天终于到货了,赶紧用把原来的马达下掉,然后用烙铁焊上新的,哦了,满血复活。
2
3
 这里就不得不夸赞一下我的电烙铁了,去年十几块钱买的,已经给浠浠修了三四个玩具了,都是里面的线头掉了或者开关坏了,烙铁一焊,再战三年。
4

  •