2023 年就这样在恍恍惚惚间过去了,在这一年中发生了许多事情,就让我挑一些大家可能感兴趣的事情来讲讲吧。
如我在《我的 OI 生涯 —— 一名退役竞赛生的回忆录》中所述,我在竞赛失利后,已经选择了回归文化课道路。在回班以后,不时有好友、读者向我私信或者邮件询问我的近况。由于寄宿制学校放假时间极短,未能一一详尽回答,所以我将在此介绍一下我的近况,以回应各位热心读者的关切。
首先先给大家看点好笑的:
截止到现在,我在回班以后主要分为了以下几个阶段:
随着时间的推移,我从最开始几个月的 “听天书” 到现在已经逐渐适应了班内的学习节奏。虽然由于高一高二的长时间停课导致现在的成绩不太理想,但我相信通过一轮复习,我的知识水平会得到很大的提升。虽然今后还有一段很艰苦的道路要走,但我坚信只要努力就能克服路途上的艰难险阻,到达成功的彼岸。
暂时还没想好。考哪算哪,不强求。
暂时不考虑。原因有三:
高考之日未到,现在谈复读与否其实有点早。我个人以及我家长的意见都倾向于不复读。
复读,意味着又要承受一年高三的巨大压力,这对于一个人的身体和心理都是一个巨大的挑战,而我的身体较为羸弱,恐怕很难再扛得住一年这样的压力。除此之外,复读还使我在一条我不喜欢的且充满不确定性的道路上多耗费了整整一年的光阴,这样做真的值得吗?我不太好回答这个问题。
在强基计划公布以后,除非取得国家级的奖项,否则竞赛对高考已经没有了什么实质性的帮助,省一等奖最多也就给三四十分的优惠,所以最后还得看文化课的水平到底如何。
我学竞赛并无太多功利因素,更多的是怀揣着一份对计算机的热爱,这也是支撑着我度过这四年有余的竞赛生涯的最关键因素。此外,我也没见过几个一心为了功利还能取得好成绩的竞赛生。毕竟竞赛的学习过程并不轻松,且其对文化课的影响常常是显著的,所以从功利的角度来看,学习竞赛显然是不划算的。
不过,如果再给我一次机会,我还会选择学习竞赛。正如我在《我的 OI 生涯 —— 一名退役竞赛生的回忆录》中所述,竞赛带给我的并不仅仅是那几张薄薄的证书,更多的是思维方式的蜕变,这将在我今后的人生中产生深远影响。
高一的时候一定要打牢文化课基础,不然等到省选前有你紧张到哭的时候。我就是一个很好的反面教材,高一停课停早了导致文化课约等于没学,结果最后几场比赛前就非常害怕退役回去学文化课,于是就整夜整夜的失眠。
动漫于我的意义并不只是看个 “动画”,一段精彩的作画、一段感人的故事、一段轻松的日常,都能以其积极向上的乐观主义精神,将我从低谷中拉出来,使我能够更加乐观地面对今后的人生道路。
▲ 我宿舍内悬挂的《莉可丽丝》海报
▲ 故地重游(参见:USTC Hackergame 2021 旅行照片)
▲ 燕山大学
▲ 二南随拍
由于学业因素,在过去的一年里我用来写代码的时间大大减少。不出意外的话,在高考结束以前我都会保持这种低频活动状态。
对整体布局进行了一些重新设计。此外我还计划将其迁移至 Next.js 13 App Router,但尚未完工。
在新的一年里,我会继续冲刺高考,争取考一所好大学。同时也在此感谢读者们对我的关心,不过由于我长期住校,故评论、邮件等可能不会及时回复,敬请谅解。
最后,祝大家新的一年里身体健康,万事如意!
]]>在某些情况下,我们会遇到需要将网页打印出来的需求。但是,直接打印网页的效果往往不尽如人意,因为网页的排版和打印的排版是不同的。本文将介绍如何创建一个在打印时具有出色的质量和可读性的网页。
@media print
媒体查询经常编写 CSS 的读者应该对 @media
媒体查询是比较熟悉的了。这个语句在创建响应式网页时是非常有用的,经常被大家用来调整不同屏幕宽度的设备间的样式。而 @media print
媒体查询则是专门用来调整打印时的样式的。
@media print
媒体查询的语法如下:
@media print {
/* 在这里定义打印时应用的样式 */
body {
font-size: 12pt;
}
.header,
.footer {
display: none;
}
/* 更多样式规则... */
}
这些样式只会在打印时应用,而不会在屏幕上显示。了解了 @media print
媒体查询的基本语法后,我们就可以开始创建打印友好型的网页了。
在打印时,页面上的一些与正文无关的元素需要被隐藏掉。
比如在《二分图学习笔记》页面中(本文在后续部分中将会一直以本页面作为示例),顶部的导航栏以及右侧的侧边栏与正文信息并没有什么关联,因此可以在打印输出中隐去。
▲ IT 之家某篇文章的打印版截图。
从这张截图中可以看出这个页面似乎并没有对打印机进行适配,并且侧边栏还遮挡到了正文中的文字。不过由于笔者并没有找到更好的遮挡示例,因此只能给出这么一个有点勉强的例子 —— 侧边栏按照上一节中的建议是应该要被隐藏掉的。
对于这种情况,需要在设计、编写页面布局的时候下功夫,以避免遮挡到正文。
除了文字被遮挡的问题,截图下部的超链接在纸质媒介上显然是不能被点击的。
以此处的超链接为例,可以通过特殊处理来在纸上显示出链接的实际指向 URL:
@media print {
a:not([href^='#'])::after {
content: ' (' attr(href) ')';
font-size: 80%;
color: var(--color-fg-muted);
}
}
效果如图:
除此之外,如果需要,还要对字体及其大小进行一些调整。
笔者认为在相当一部分情况下,使用衬线字体在打印后的观感要比使用非衬线字体时好很多。(PS:在此对通篇使用微软雅黑出试卷的老师表示强烈谴责)
有时网页上会包含一些音频、视频等多媒体内容,这些内容在纸质媒介上与超链接类似,无法与读者交互。
此时可以考虑提供一些替代文本来对其内容进行描述,并提供指向相关资源的链接、二维码等辅助工具来帮助读者获取多媒体资源中的信息。
在网络世界中,我们的常用单位诸如像素(px)、百分比(%)、相对大小(em、rem) 等。然而在现实世界中,我们常用的单位则为物理单位,如厘米(cm)、点(pt)等。这导致了在打印输出时需要额外注意单位制相关的问题。虽然现代浏览器对这些问题的处理已经比较优秀了,但在部分情况下仍然会导致页面排版布局出现错乱。
经常写 CSS 的读者应该对 CSS 中的样式优先级不陌生了。笔者建议在编写 CSS 时将打印相关样式置于靠下的位置以免产生冲突,同时也可以适当地使用 !important
来强制覆盖一些样式。
可以使用 DevTools 来模拟打印环境进行调试(按 Ctrl + Shift + P 组合键唤出菜单;对于中文版浏览器,请搜索「打印」关键字)。
感兴趣的读者可以访问 oi.baoshuo.ren/bi-graph 并尝试打印该页面。
▲ 原网页
▲ 打印效果(预览)
]]>在经历了四年半的不算短也不算长的时光后,我的 OI 生涯画上了一个并不算圆满的句号。
是的,我退役了。
写回忆录的本质是自己给自己整理遗容。
—— 郑渊洁《舒克和贝塔历险记》
谨以此文纪念我与 OI 一同逝去的青春。
我第一次接触信息学竞赛时在初一上学期(2018 年)。当时学校与旁边的高中部合作开设了「信息贯通」课程,使得我在信息老师的帮助下了解到了信息学竞赛这个东西。这便是一切的开端了。
在学 OI 之前,我已经具有了一定的 Python 基础,并且还掌握了一些网页开发相关技能。不过这些东西和 OI 并没有什么关系,如果硬要说有的话,那么这些东西对我的帮助就是使得我的 C++ 语法入门过程并没有那么痛苦,促使了我留下来继续深入学习 OI 知识。
我对计算机有着与众不同的兴趣 —— 别的同龄人用电脑基本上都是打游戏,而我用电脑则是折腾软硬件、写写代码等等。在接触 OI 之后,我找到了有着相同兴趣的一群小伙伴,我们可以在一起交流很多计算机相关的东西 —— 大多是算法相关的内容 —— 我们都为代码可以实现的无限可能性着迷。这让我对 OI 的喜爱更甚 —— 又能学知识,还能结交好友。
▲ 初中开设的「信息贯通」课程正在授课。来源于学校微信公众号。本人跟随高中部学习,因此不在照片中。
不过与此同时,我在班级里并不是很合群,因为我不打游戏。当时流行的游戏叫做《王者荣耀》,同学们周末都会废寝忘食的去玩它,然后在返校后的课余时间交流上周末打游戏的心得,以及规划下次放假的游戏时间。而我因为对游戏没有兴趣,所以很难插上话。这使得我与班级的主体渐行渐远,转而更加亲近我们这个小圈子,在这个圈子里我能获得更多的认同感和归属感。
我初中的 OI 生涯到初三下学期(2021 年)告一段落。初三下学期是一段比较痛苦的日子 —— 我需要补习文化课,来应对即将到来的中考。我和我在学习 OI 时认识的邻班的好伙伴赵泽峰同学一起互帮互助(其实还是我向他取经比较多),共同学习。那段时间几乎每天我们两个都是最后回宿舍睡觉的人。最后的结果很令人振奋,我们都考上了我们理想的高中 —— 石家庄二中实验学校,也就是前文中提到的高中部,这所重点高中有着专业的教练团队和竞赛培养体系,是学习竞赛的好去处。
▲ 二南日落。本人在 2022 年 6 月摄于石家庄二中实验学校存真楼上。
进入高中后,我有更多的时间学习 OI,但相应地,学习文化课的时间减少了。我最初被分入了竞赛班,但我的成绩排在很靠后的位置,这是因为我不仅文化课考不了高分,而且不能兼顾竞赛和文化课的学习。这招来了文化课老师的不满 —— 学竞赛不拿金牌最后还得学文化课,而且文化课成绩太差会拉低班级平均分,这显然是他们所不想看到的。好在我高一下学期被编入了另外一个省理科实验班,这个班的班主任是上一届带竞赛班的班主任(我先前在竞赛班时班主任从没有接触过竞赛生),所以相比之下高一下学期时来自文化课班的压力要减轻许多。
高一下学期的期中考试结束后,我停课了。这给了我充足的时间去研究一些较为困难的知识点,这对我来说是一大收获。
▲ 我在存真楼上旧信息中心 NOI 教室 3 中的机位。由本人在 2022 年 6 月拍摄。
然后我就进入了高二,每天都被模拟赛压得喘不过气来。当时基本上每天的规划都是上午模拟赛,下午改题,晚上隔三岔五的还会有南校自己办的「基础模拟赛」—— 专练第一、二题难度,防止挂分(虽然该挂的还得挂)。
直到快要退役的时候,才能真正体会到往届学长们的痛楚。我送走了好几届学长,这次终于要成为了被送走的那一批。CSP-S 2022 拿了个一等,全省二十多名,这应该就是我能够达到的最好的成绩了吧。NOIP 2022 被取消了,没有考成。春季赛和省选又给我强行续了几个月的命,但于事无补。
▲ 我在 CSP-S 2022 中获得的获奖证书。
我的 OI 之旅到这里就结束了。退役之后特别喜欢学长们常说的一句话:菜是原罪。如果我的实力能够再强一些的话,我肯定不用担心退役这件事情。但即使最终的结局必然是退役,我也无悔竞赛。
在学习竞赛的过程中,我收获了许多宝贵的经验和知识。其中最重要的收获之一就是我的思维方式进行了深刻的转变。
竞赛知识点的数量很大,并且通常都比较深入、复杂、抽象。这要求我们必须具备良好的理科思维和创新思维,能够将问题进行深入研究,并将其与实际问题相结合,产生新的想法和解决方案,从而在比赛中熟练运用它们。
OI 中所涉及的知识非常广泛,仅在《NOI 大纲》中列出的知识点就已经能够涉及到好几摞半人高的书堆了。此外,在日常训练的过程中还需要接触到各类国内外的在线资料,这同时需要良好的外语水平。等等。
对我而言,在学习 OI 之余,我还略微了解了一些软件工程相关的知识,写了一些小玩具出来。
我结识了许多友好的同学,他们都非常优秀。在竞赛学习的过程中,我们经常会相互帮助,互相学习。这种友好的关系使得我们的竞赛旅途更加愉快。
俗话说得好,「人外有人,天外有天」。在学习竞赛的过程中,我时常有机会接触到并认识来自全国乃至全世界的优秀选手。
比自己更强的选手不一定只是对手,更可以成为我们的老师和榜样。从他们身上可以学习到很多独特的思维方式和优秀的解题方法,而这些在自己日常独自训练时是很难接触到的。所以要学会欣赏和学习优秀选手的思路和方法,并从中受益、成长。只有这样,我们才能不断提高自己的水平,成为更好的自己。
对于大部分人,竞赛和文化课是不可兼顾的。既然要抽出时间来学习竞赛,那么就必须压缩一些干其他的事情的时间,比如学习文化课。这会导致文化课的学习效果受到影响,然后成绩就不可避免地下滑了。
考试成绩下降之后,班主任和任课老师们自然会有意见。竞赛不是一条捷径,我们学校每年只有那么几个人能够进入省队并在国赛中取得奖牌,其他人则会慢慢地被淘汰下来,这是不可避免的。老师们自然希望我们的文化课成绩要好一些,所以会鼓动甚至要求我们放弃学习竞赛,毕竟相比之下,竞赛的容错率和回报率太低了。
那么如何在竞赛和文化课之间取得一个较好的平衡就成了一个棘手的问题,这个问题各路人马争论至今也没有一个定论,我觉得以后也不会有一个定论,毕竟人和人是不一样的。
在春季赛后,我休息了半天便准备考虑回归文化课学习的事宜。
我先回班找到了各科的任课老师们,向她们说明了我的实际情况。她们表示理解,希望我能够尽快找回状态,回归文化课的学习,因为我已经落下了很多课程的学习进度。
一些能听懂的科目自然也是要回班听一听的,网课讲得显然不如老师好。不能听懂的科目就只能自己看书听网课,一轮复习再回班跟了。
刚退役的时候还是很失落的,也不能专注到文化课的学习上,不过经过后来的慢慢调整,现在情况有转好的迹象。再慢慢观察吧。
不论结果如何,我能坚持学习竞赛到今天,都少不了来自家长、教练和同学们的鼓励与支持。
我想感谢我的父母,没有他们的支持和鼓励,我不可能坚持到今天。
我想感谢我的教练任亮老师和聂文彬老师,没有他们的指导和帮助,我不可能取得今天的成绩(虽然并不是很出类拔萃)。
我想感谢我的同学们,没有他们的陪伴和帮助,我不可能从竞赛学习中收获如此多的东西。
虽然退役了,但是我应该还会经常回来 OI 圈子看一看,没准还会参加一些比赛呢。
一切皆有可能,接下来的日子里,我会继续努力,不断提高自己的水平,成为更好的自己。
竞赛不是火,却能点亮一生。
这是石家庄二中实验学校旧信息中心旁的信息学竞赛教室墙外贴的一句话。
这句话的意思是,学习竞赛虽然不会像火焰燃烧那样为当下带来光明与温暖,但是它能够在一个人的一生中产生持久的影响。竞赛可以激发人的竞争精神,并培养毅力和耐力等品质。这些优点不仅在竞赛过程中得到锻炼,而且会伴随一个人的一生,对其产生长远、积极的影响。
上初中时第一次看到这句话时,我便对其留下了深刻的印象。随着时间的推移以及心境的不同,每次看到这句话,我都会对其有不同的理解。直到我的 OI 之旅走到尽头之时,我才明白了这句话之中的深意。
在退役之前的一个晚上,我走出实验楼的机房,向旁边的教学楼望了过去。灯火通明的教学楼与人烟稀少的实验楼形成了鲜明的对比 —— 这使得我莫名地产生了一种怅然若失的感觉 —— 我的竞赛之旅即将结束,我将要离开这个我已经熟悉的环境,去面对一个陌生的未来。
我想起了小时候读过的一首诗歌中的内容:
也许多少年后在某个地方,
我将轻声叹息把往事回顾,
一片树林里分出两条路,
而我选择了人迹更少的一条,
因此走出了这迥异的旅途。
– The Road Not Taken, Robert Frost.
我选择了竞赛,一个小众的发展方向,而这个选择决定了我今后的人生道路。竞赛决不是捷径,它只是另一种艰辛的生活方式。我不知道未来的路会怎么走,但我知道,我会一直一步一步脚踏实地地走下去。即使不再参加与竞赛相关的活动,竞赛带给我的思维方式也将伴我一生。
【心态乐观】
有人说,“生命中,我们都接到不同的剧本。平淡或浓烈,欢笑或眼泪,我们总要演好,直至落幕。”
心态好,一切都好。积极乐观的心态,是幸福生活的钥匙。
不管发生什么事,记得告诉自己,一切都会过去,好事自会发生。
—— 摘抄:人民日报夜读《善待自己,过张弛有度的生活》,2023 年 02 月 25 日。
大家都说,高考是千军万马过独木桥,不容易。
可是又有几个「大家」知道,竞赛是一个人摸黑走路,盲人骑瞎马,半夜临深池?
在无数个孤独清冷的深夜,无数次羡慕已经安然入梦的同学们。
我们都是行走在镜面边缘的人。
低下头看到的,是半个迷茫的自己,和半个不见底的深渊。
到哪里,会不会跌倒,是到终点还是滑进深渊,都不知道。
唯一确定的是,自己只有一个人。
—— 《行走在镜面的边缘》
得到与失去,只有时间会去评判;成功与失败,只有历史能去仲裁。
我不会永远成功,正如我不会永远失败一样。
—— 洪骥《……》
本文为原创文章,未经许可禁止任何形式的复制、摘抄与转载。
]]>#define int long long
是一种未定义行为,尽量不要在代码中使用它。
在算法竞赛社区中,经常能看见有人在代码中使用 #define int long long 来偷懒。我是一直极力反对这种做法的,因为这种做法会导致代码的可读性大大降低,并带来一些难以预料的问题。
在 ISO/IEC 14882:2014(E) 的 17.6.4.3.1 Macro names 一节中,有这样一段描述:
翻译并整理一下,就是:
翻译单元不可
#define
或#undef
词法上等同于下列部分的名称:
C++ 中的关键字(表 4、表 5,在 2.12 节 Keywords
[lex.key]
中给出);有特殊含义的标识符(表 3,在 2.11 节 Identifiers
[lex.name]
中给出);任何标准属性记号(attribute-token,在 7.6 节 Attributes
[dcl.attr]
中给出)。
也就是说,标准中 并不允许 #define int
这种操作。
在 GCC 的 C Preprocessor 文档中 给出了下面的说明:
You may define any valid identifier as a macro, even if it is a C keyword.
也就是说,GCC 并没有严格按照标准来实现预处理器,而是稍微放宽了一些限制以允许通过这种方式来使得代码更加灵活,便于增强代码的向下兼容性。
相关文档中并未提及是否允许 define 关键字,但源代码中未见相关限制。
#define
指令 相关文档中并未提及。
使用适当的数据类型来存储数据,有利于代码的可读性和稳定性,便于编写和调试。同时,正确设置变量类型也能提高程序的运行速度和效率。因此,我们应该做好正确的数据类型定义,而不是在编写代码时滥用 #define int long long
。
又一年过去了。由于学业繁忙,这一年中发生的能写出来公之于众的事情并没有多少,但做一些微小的记录总是值得的,所以就有了这篇年度总结。
先向大家报告一个好消息,我在 CSP-S 2022 中取得了省一等奖(省排第 30),这么多年的竞赛算是没有白学。
不过,按照正常的进度,到 NOIP 2022 结束之时也就是我的退役之日了,毕竟河北省只有 15 个省队。
由于疫情影响,河北省取消了 NOIP 2022,并将在 2023 年 3 月举办春季赛,以此作为省选成绩的参考,这也就意味着我可以继续冲刺省选了(虽然进省队的希望不大,但仍然可以一试)。
但是,现在也是时候考虑如何补习文化课的事情了。
在得知 NOIP 2022 取消后,我们便返回家中,开始了上网课的日子。
跟班上网课貌似不太现实,所以只好跟着竞赛一起上网课。竞赛这边没有早读,早上上课时间比较晚,可以多睡会。
在网课期间既要补习文化课,又要兼顾竞赛的进度,实属一个难题。
此部分内容没有文字描述。关于我感染之后的情况记录,可以查看《我的新冠阳性日记》
感谢炮总相助,今年 3 月我终于有了一段属于自己的 IPv4 地址段 —— 174.136.239.0/24
。不过因为线路问题,这段地址并没有开展大规模应用。
关于 AS141776 的更多信息,请访问 baoshuo.ren/network。
基于 Vite + React + Primer Design 的新个人主页上线了!
新的个人主页主要分为了个人简介、项目介绍、友情链接、项目单页等几个板块,并可以方便地在后期增删页面及其内容。
请访问 baoshuo.ren 了解更多信息。
又经过了半年多的开发,OIerDb NG 正式上线了。
详细介绍可以查看文章《OIerDb NG —— 新一代的 OIerDb》,在此不作过多叙述。
附上今年最后一个季度的访问量数据,平均日访客数也能保持在 900 人左右。
欢迎体验:oier.baoshuo.dev。
今年上半年折腾了折腾自己的 OI 博客,在折腾的过程中顺手给 Hexo 发了一些 PR,并在 Sukka 大佬的引荐下加入了 Hexo Core Team。
今年下半年正式接手了学校的 在线测评系统,并进行了 一些大改(当心过大的 diff 导致浏览器卡死)。
▲ 旧版界面
▲ 新版界面
除了界面更新之外,还增加了许多新功能,并修复了一些问题。在开发的过程中,也向上游 UOJ 官网版、UOJ 社区版发送了一些 Pull Request,算是为后人栽树了。
又是碌碌无为的一年呢!
2022 年就这样在疫情阴霾退散的过程中结束了。希望在走出这一困难之后,2023 年能够是一个更加美好的年份,让我们一起期待着明天的希望,共同迎接更加美好的未来。
]]>至前几日,笔者已经基本康复,于是决定写下这篇文章,记录下这一个多星期的别样的体验。
上午正常上课。
午饭后开始感觉略有不适,开始发烧(38.5℃ 左右),遂请假休息。下午烧到 39℃ 后服用一包布洛芬,体温略有下降,但并未退烧。抗原测试呈弱阳性。
半夜继续高烧(39.4℃),服用一包布洛芬,体温略有下降,但仍未退烧。
全天请假休息。
上午高烧(39.5℃),服用一包布洛芬后于午饭前退烧。
下午重新开始低烧(37.5℃),一直烧到晚上睡觉。
继续全天请假休息。
早上感觉身体已经适应了高烧的状态,没有前几日那么蔫,但体温仍在 39.5℃ 附近徘徊。
下午排便后转为低烧,并未服用退烧药。
正常上课。
不再发烧,有轻微咳嗽和流鼻涕的症状。
正常上课。
咳嗽和流鼻涕的症状加重,但没有太大影响。
正常上课。
咳嗽和流鼻涕导致头昏脑胀。晚上因咳嗽久久无法入眠。
正常上课。症状与前一天相似。
正常上课。症状开始转轻。
正常上课。抗原测试基本转阴。仍有些许咳嗽。
症状基本消失,食欲恢复。
人民日报发布的「新冠发病 7 日典型症状过程」还是比较准确的,值得参考。
由于我去年在家中准备了一盒布洛芬颗粒,因此并没有陷入到「一药难求」的境地。从发病到痊愈,只消耗了半盒布洛芬颗粒(即 5 包)和两盒连花清瘟胶囊(48 颗),因此无需囤积药品,够用就好。
最后,希望各位读者在疫情期间保护好自己,也祝大家身体健康,百毒不侵!
]]>日常改参数。
中国科学技术大学 NEBULA 战队(USTC NEBULA)是于何时成立的喵?
在这个页面中有这样一段介绍:
星云战队(Nebula)
中国科学技术大学“星云战队(Nebula)”成立于 2017 年 3 月,“星云”一词来自中国科学技术大学 BBS“瀚海星云”,代表同学们对科学技术的无限向往和追求。战队现领队为网络空间安全学院吴文涛老师,现任队长为网络空间安全学院李蔚林、童蒙和武汉。战队核心成员包括了来自网络空间安全学院、少年班学院、物理学院、计算机学院等各个院系的同学,充分体现了我校多学院共建网络空间安全一级学科的特点。战队以赛代练,以赛促学,在诸多赛事中获得佳绩。
可知答案为 2017-03
。
22 年坚持,小 C 仍然使用着一台他从小用到大的 Windows 2000 计算机。那么,在不变更系统配置和程序代码的前提下,Firefox 浏览器能在 Windows 2000 下运行的最后一个大版本号是多少?
Google 搜索 windows 2000 firefox
可以搜索到一个帖子:Last version of fireFox to work on Windows 2000?,可知答案为 12。
你知道 PwnKit(CVE-2021-4034)喵?据可靠谣传,出题组的某位同学本来想出这样一道类似的题,但是发现 Linux 内核更新之后居然不再允许 argc 为 0 了喵!那么,请找出在 Linux 内核 master 分支(torvalds/linux.git)下,首个变动此行为的 commit 的 hash 吧喵!
关于这部分的限制在 fs/exec.c
文件下,那么 git blame
可知这部分是在 dcd46d8
中被修改的。
全局搜索 flag{
可得:
在 rclone.conf
里可以找到一个密码,通过在 Google 上搜索可以找到 一份现成的解密代码:
package main
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"log"
)
// crypt internals
var (
cryptKey = []byte{
0x9c, 0x93, 0x5b, 0x48, 0x73, 0x0a, 0x55, 0x4d,
0x6b, 0xfd, 0x7c, 0x63, 0xc8, 0x86, 0xa9, 0x2b,
0xd3, 0x90, 0x19, 0x8e, 0xb8, 0x12, 0x8a, 0xfb,
0xf4, 0xde, 0x16, 0x2b, 0x8b, 0x95, 0xf6, 0x38,
}
cryptBlock cipher.Block
cryptRand = rand.Reader
)
// crypt transforms in to out using iv under AES-CTR.
//
// in and out may be the same buffer.
//
// Note encryption and decryption are the same operation
func crypt(out, in, iv []byte) error {
if cryptBlock == nil {
var err error
cryptBlock, err = aes.NewCipher(cryptKey)
if err != nil {
return err
}
}
stream := cipher.NewCTR(cryptBlock, iv)
stream.XORKeyStream(out, in)
return nil
}
// Reveal an obscured value
func Reveal(x string) (string, error) {
ciphertext, err := base64.RawURLEncoding.DecodeString(x)
if err != nil {
return "", fmt.Errorf("base64 decode failed when revealing password - is it obscured? %w", err)
}
if len(ciphertext) < aes.BlockSize {
return "", errors.New("input too short when revealing password - is it obscured?")
}
buf := ciphertext[aes.BlockSize:]
iv := ciphertext[:aes.BlockSize]
if err := crypt(buf, buf, iv); err != nil {
return "", fmt.Errorf("decrypt failed when revealing password - is it obscured? %w", err)
}
return string(buf), nil
}
// MustReveal reveals an obscured value, exiting with a fatal error if it failed
func MustReveal(x string) string {
out, err := Reveal(x)
if err != nil {
log.Fatalf("Reveal failed: %v", err)
}
return out
}
func main() {
fmt.Println(MustReveal("tqqTq4tmQRDZ0sT_leJr7-WtCiHVXSMrVN49dWELPH1uce-5DPiuDtjBUN3EI38zvewgN5JaZqAirNnLlsQ"))
}
跑一遍就出来了:
打开 VSCode,将 \| ([\d]+)\] = ([\d]+)
递归替换为 ] = $2\na[$1] = $2
,然后执行脚本即可得到 flag:
在打开验证码页面的一瞬间将这段 JS 脚本塞进控制台里即可。
for (let i = 1; i <= 3; i++) {
let raw_str = document.querySelector(`label[for="captcha${i}"]`).innerHTML;
let match = /([\d]+)\+([\d]+)/.exec(raw_str);
let sum = BigInt(match[1]) + BigInt(match[2]);
document.getElementById('captcha' + i).value = sum.toString();
}
document.getElementById('submit').click();
exiftool 一把梭。
众所周知,LaTeX 有一个 \input
指令:
Flag 到手:flag{becAr3fu11dUd3c5a1b17ffa}
。
最开始我打算在程序里读文件,结果不知道为什么写挂了……
于是就立马想到了用汇编读文件,代码和动态数据差不多。
查看评测机的源码可以发现 n = 5,那么可以用汇编在编译的时候读文件:
#include <stdio.h>
#include <string.h>
asm("staticInput: .incbin \"data/static.in\"");
asm("staticAnswer: .incbin \"data/static.out\"");
asm("dynamicInput0: .incbin \"data/dynamic0.in\"");
asm("dynamicInput1: .incbin \"data/dynamic1.in\"");
asm("dynamicInput2: .incbin \"data/dynamic2.in\"");
asm("dynamicInput3: .incbin \"data/dynamic3.in\"");
asm("dynamicInput4: .incbin \"data/dynamic4.in\"");
asm("dynamicAnswer0: .incbin \"data/dynamic0.out\"");
asm("dynamicAnswer1: .incbin \"data/dynamic1.out\"");
asm("dynamicAnswer2: .incbin \"data/dynamic2.out\"");
asm("dynamicAnswer3: .incbin \"data/dynamic3.out\"");
asm("dynamicAnswer4: .incbin \"data/dynamic4.out\"");
extern char staticInput[];
extern char staticAnswer[];
extern char dynamicInput0[];
extern char dynamicInput1[];
extern char dynamicInput2[];
extern char dynamicInput3[];
extern char dynamicInput4[];
extern char dynamicAnswer0[];
extern char dynamicAnswer1[];
extern char dynamicAnswer2[];
extern char dynamicAnswer3[];
extern char dynamicAnswer4[];
char n[1000000], a[6][1000000], p[1000000], q[1000000];
int main(int argc, char **argv[]) {
scanf("%s\n", &n);
sscanf(staticInput, "%s", a[0]);
sscanf(dynamicInput0, "%s", a[1]);
sscanf(dynamicInput1, "%s", a[2]);
sscanf(dynamicInput2, "%s", a[3]);
sscanf(dynamicInput3, "%s", a[4]);
sscanf(dynamicInput4, "%s", a[5]);
if (!strcmp(n, a[0])) {
sscanf(staticAnswer, "%s\n%s", p, q);
printf("%s\n%s\n", p, q);
} else if (!strcmp(n, a[1])) {
sscanf(dynamicAnswer0, "%s\n%s", p, q);
printf("%s\n%s\n", p, q);
} else if (!strcmp(n, a[2])) {
sscanf(dynamicAnswer1, "%s\n%s", p, q);
printf("%s\n%s\n", p, q);
} else if (!strcmp(n, a[3])) {
sscanf(dynamicAnswer2, "%s\n%s", p, q);
printf("%s\n%s\n", p, q);
} else if (!strcmp(n, a[4])) {
sscanf(dynamicAnswer3, "%s\n%s", p, q);
printf("%s\n%s\n", p, q);
} else if (!strcmp(n, a[5])) {
sscanf(dynamicAnswer4, "%s\n%s", p, q);
printf("%s\n%s\n", p, q);
}
return 0;
}
手动一个一个试就好啦~
今年是我打 Hackergame 的第三年了,由于近期学业繁忙(甚至我刚从 CSP-S 考场出来就回来写 Write Up),所以并没有能抽出足够的时间来享受这场比赛了,只能用两天的零碎时间水一点签到题了事,binary 还是一如既往地稀烂……
]]>笔者有幸能与 OI 巨神虞浩翔等人合作来共同参与 OIerDb NG 的开发,经过数个月的不断改进,目前项目已经初具雏形,特此写下本篇文章对其进行简要介绍。
笔者主要参与了前端用户界面的开发工作,而数据处理部分则主要由虞浩翔完成。
相比于老版 OIerDb,OIerDb NG 摈弃了传统的「客户端发送查询请求 -> 服务器响应查询请求」的模式,而是采用了纯前端的处理方式 —— 将数据库存储在浏览器的 indexedDB 中,这样在用户查询时无需向服务器发送请求,直接在浏览器端即可处理。这样带来的好处是显而易见的 —— 我们拥有了更快的查询响应速度,同时也减轻了对服务器的压力。至于缺点嘛… 在首次访问网页的时候会下载几 MB 的数据,从统计数据来看,这个过程在大部分情况下最多需要消耗 5 秒左右的时间。
在下载好所有页面的代码、数据加载完成后,即使断开网络连接也能正常使用 OIerDb NG 的基础功能 ,可以在断网打模拟赛 AK 之后找点东西看 。
老版的 OIerDb 只能应对两种类型的查询 —— 以选手或者学校为中心的查询。而新版的 OIerDb 在设计之初就希望具备应对更灵活的查询请求的能力,比如「查询『NOIP 2021』中『河北省』的获奖情况」(如上图所示),更进一步的话还可以「查询『NOIP 2021』中位于『河北省』的『石家庄市第二中学』的获奖情况」(不过这种查询目前还没有在用户界面中实现)。
在 2021 年 12 月初的一天中午,笔者在 OIerDb 的页面底部发现了 nocriz 的《呼吁广大选手积极参与开发下一代 OIerDb》文章,恰巧笔者在课余时间学习了一些在现在看来非常浅薄的前端技术,于是跃跃欲试地在 12 月 12 号的那天创建了一个新的 GitHub 仓库,并使用模板来提交了 第一个 commit。
接下来的几天,笔者利用自己的空余时间来编写代码,终于在 12 月 19 日完成了第一版的 OIerDb NG,并部署到了 Netlify 上。
▲ OIerDb NG 的第一版界面。
当时笔者初学 React,许多知识仍有待学习,再加之笔者忙于完成学业,因此开发进度异常缓慢,网站的功能也有很多欠缺。
第一版完成后没多久,笔者找到了精通 React 的好友 Menci 来帮忙 review 代码。在这个过程中,Menci 提出了许多富有建设性的意见,同时对项目整体进行了一番调整,使其更加现代化、工程化。笔者也从中学到了很多知识。
之后笔者边实践边学习,还从 LibreOJ 的前端中抄来了一些代码,比如手机端的导航栏。
慢慢地,OIerDb NG 上线了 nocriz 的文章中提到的大部分功能(点击图片可以前往对应页面):
▲ 基础 / 高级搜索
▲ 搜索页选手信息卡片
▲ 地区信息学奥林匹克竞赛选手 / 学校排名
▲ 学校 / 比赛详情页面
尽管 OIerDb NG 有了一个还算可以的开始,但仍然存在诸多不足之处。
例如,对于网络速度较慢的用户,加载数 MB 的数据可能仍需要十几秒甚至数十秒。并且,即使是一些小更新也需要重新从服务器拉取全量数据,对用户与服务器的流量都是一种浪费。
再比如一些用户可能需要指向性更强的查询条件,目前还没有找到一个比较好的办法来添加到用户界面中。
除了这些之外,还有一些其他的问题存在。这些问题由于团队内的各位开发者都在现实生活中有着自己的工作、学习任务,无法去逐一解决。笔者希望广大对信息学竞赛感兴趣的朋友们能或多或少地参与进 OIerDb NG 的开发,共同为信息学竞赛社区做出贡献。
感谢 nocriz 建立的 OIerDb 网站,为国内的信息学竞赛社区做出了巨大贡献。
也感谢 yhx-12243、Menci 参与 OIerDb NG 的开发,完成了许多工作。
最后的最后,给 OIerDb-ng/OIerDb 求一波 Star~
]]>当下,Atomic CSS 愈发受到人们的关注。相比于传统 CSS 编写方法中每个组件对应一个 CSS 类,使用了 Atomic CSS 以后,每一个 CSS 类都唯一对应了一条独立的 CSS 规则,随着组件数量逐渐增加、能复用的 CSS 规则越来越多,最终的 CSS 产物体积也会下降许多,使得网页的加载速度能够产生一个质的飞跃。
在介绍 Atomic CSS 之前,让我们先来回顾一下 CSS 编写方法的发展历程。
SMACSS(Scalable & Modular Architecture for CSS),是由 Jonathan Snook 提出的 CSS 理论。其主要原则有 3 条:
SMACSS 将规则分为了五类:Base(基础)、Layout(布局)、Module(模块)、State(状态)、Theme(主题)。
基础(Base) 规则里放置默认样式。这些默认样式基本上都是元素选择器,不过也可以包含属性选择器,伪类选择器,孩子选择器,兄弟选择器。本质上来说,一个基础样式定义了元素在页面的任何位置应该是怎么样的。
布局(Layout) 规则将页面拆分成几个部分,每个部分都可能有一到多个模块。顾名思义,这个分类主要用来做页面的整体或其中一块区域的布局。
模块(Modules) 是我们的设计当中可重用,可模块化的部分。插图,侧边栏,文章列表等等都属于模块。
状态(State) 规则定义了我们的模块或者布局在特殊的状态下应该呈现怎样的效果。它可能定义模块、布局在不同显示屏上应该如何显示。也可能定义一个模块在不同页面(例如主页和内页)中可能呈现怎么样的效果。
主题(Theme) 规则和状态规则类似,定义模块或者布局的外观。很多网站的「深色模式」「换肤」等等功能就是这样实现的。
将规则分成五类之后,还需要命名规范。命名规范能够使得我们立刻了解到某个样式属于哪一类,以及它在整个页面中起到的作用。在一个大型项目中,我们可能会将一个样式分割成几个文件,这个时候命名约定能够使得我们更容易知道这个样式属于哪个文件。
推荐使用前缀来区分布局、模块和状态等等规则。比如对布局规则使用 layout-
前缀,对状态规则使用 is-
前缀就是一个不错的选择。
尽量不要依赖文档树的结构来编写样式。这样可以让我们的样式更加灵活,并且容易维护。
BEM( Block Element Modifier)是由 Yandex 团队提出的一种前端 CSS 命名方法论。它是一个简单又非常有用的命名约定。让前端代码更容易阅读和理解,更容易协作,更容易控制,更加健壮和明确,而且更加严密。
BEM 命名约定的模式是:
.block {
}
.block__element {
}
.block--modifier {
}
block
代表了「块」,用于组件本体。element
代表了「块」中的某个「元素」(也可以叫做「子组件」),是块构成的主要成员。modifier
代表了「块」的修饰符,表示不同的状态和版本。使用 --
做区分,适用于「块」和「元素」,分别称之为「块修饰符」和「元素修饰符」。命名的不同部分之间之所以使用 __
和 --
分割,是因为如果某部分中如果出现了多个单词需要使用 -
分隔,这样可以避免造成混淆。
随着时代的发展,一个大型前端工程中的 CSS 类名越来越多,此时难免会出现类名冲突的情况,此时 CSS Modules 应运而生 —— 它通过为 CSS 类名添加 Hash 等方式来产生唯一的名称来防止冲突的产生。
CSS Modules 并不是 CSS 官方的标准,也不是浏览器的特性,而是使用一些构建工具,比如 Webpack,对 CSS 类名和选择器限定作用域的一种方式。
当传统大型项目使用的 CSS 方法论还都大多是上方提到的 OOCSS、SMACSS、BEM 等等主要聚焦在「关注点分离」的「语义化 CSS」方案的时候,Utility-First 的 CSS 概念脱颖而出、逐渐受到社区的关注。而这之中最为被人熟知的、也最典型的就是 Tailwind CSS 了。
Utility-First CSS 不像 Semantic CSS 那样将组件样式放在一个类中,而是为我们提供一个由不同功能类组成的工具箱,我们可以将它们混合在一起应用在页面元素上。这样有几个好处:
但也存在一些不足:
class
属性的内容过长;在前文介绍的 Utility-First CSS 的基础之上更进一步,Atomic CSS 便映入了人们的眼帘。
Atomic CSS 背后的思想与以往的「关注点分离」的思想可以称得上是背道而驰了。使用 Atomic CSS 时实际上将结构层和样式层耦合在了一起,这样的方式在现代 CSS-in-JS 的代码库中基本上得到了广泛认可,下文将会进行进一步的介绍。
Atomic CSS 可以看作是 Utility-First CSS 的极致抽象版本,每一个 CSS 类都对应一条单一的 CSS 规则。可面对如此繁复的 CSS 规则,手写 Atomic CSS 的类名并不是一个好的办法。于是 Atomic CSS-in-JS 应运而生,它可以看作是「自动化的 Atomic CSS」:
Christopher Chedeau 一直致力于推广 React 生态系统中 CSS-in-JS 理念。在很多次演讲中,他都解释了 CSS 的几大问题:
虽然 Utility-First CSS 和 Atomic CSS 也解决了其中的一些问题,但它们无法解决所有问题(特别是样式的非确定性解析)。
举个例子:Tailwind CSS 会在生成时生成出来许多无用代码,导致样式文件体积的增长,看看下面这份代码:
<div class="before:bg-white before:p-4">content</div>
生成出来的样式文件长这个样子:
.before\:bg-white::before {
content: var(--tw-content);
--tw-bg-opacity: 1;
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
}
.before\:p-4::before {
content: var(--tw-content);
padding: 1rem;
}
可以看到这份文件中包括了许多的无用代码,比如重复的 content: var(--tw-content)
。
传统的 CSS 编写方法无法复用组件间重复的 CSS 规则,比如下图中高亮的几条规则各自躺在它们对应的 CSS 类中:
这样会导致 CSS 产物大小与项目的复杂程度和组件数量线性正相关。
但使用 Atomic CSS 之后,这些规则被提取出来进行复用:
随着后期组件数量逐渐增加、能复用的 CSS 规则越来越多、最终 CSS 产物大小与项目复杂程度呈对数关系:
Facebook 分享了他们的数据:在旧网站上,仅登录页就需要加载 413 KiB 的样式文件,而在使用 Atomic CSS-in-JS 重写后,整个站点只有 74 KiB 的样式文件,还包括了深色模式。
虽然在使用 Atomic CSS 之后,HTML 的体积会显著增大,不过由于这些类名的高冗余度,可以利用 gzip 来压缩掉很大一部分体积。
让我们再来回顾一遍这个经典的 CSS 规则插入顺序的问题:
我们都知道,最后生效的样式不是最后一个类名对应的规则,而是样式表中最后插入的规则。
那么,如何在 CSS-in-JS 中处理这个问题呢?通用的做法是在生成阶段就将冲突的规则过滤掉,以避免产生冲突。比如下面这个组件:
const styles = style9.create({
card: {
color: '#000000',
},
profileCard: {
color: '#ffffff',
},
});
const Component = () => (
<div className={style9(styles.card, styles.profileCard)} />
);
过滤后组件的实际样式如下:
color: #ffffff;
而如果将组件样式中的 styles.card
和 styles.profileCard
调换一下顺序,过滤之后的样式就变成了这样:
color: #000000;
但 CSS 中有一些简写规则,如果只按照规则名称来处理显然是不行的。有的库强制开发者们不使用简写规则来避免这个问题,而另外的一些库则将这些简写规则展开成多条规则后再进行过滤,比如 margin: 10px
可以被拆成 margin-top: 10px
、margin-right: 10px
、margin-bottom: 10px
、margin-left: 10px
四条独立的规则。
Atomic CSS-in-JS 实现有运行时(Runtime)和预编译(Pre-Compile)两种。运行时(Runtime)的优点在于可以动态生成样式,相比于下文中采用预编译方法的库来说灵活度高了不止一点半点。其缺点则在于 Vendor Prefix 等操作需要在 Runtime 执行,因此 Bundle 中必须携带相关依赖导致体积增大。预编译(Pre-Compile)的优点则在于无需将相关依赖打包发送给客户端,改善了性能。而缺点则是预编译的过程高度依赖静态代码分析,所以难以实现动态样式生成与组合。
Styletron 是 Uber 公司开发的一个较为典型的运行时 Atomic CSS-in-JS 库,驱动了 Uber 的官网和 H5 页面。
Styletron 还提供了一套 Styled Components 的实现,可以通过下面的方式使用:
import { styled } from 'styletron-react';
const Component = styled('div', {
marginTop: '10px',
marginBottom: '10px',
});
<Component />;
还可以根据 prop 的值来动态生成样式:
const Component = styled('div', (props) => {
return { color: props.$fraction < 0.5 ? 'red' : 'green' };
});
<Component $fraction={Math.random()} />;
与 Styletron 同为运行时 Atomic CSS-in-JS 库的还有沃尔沃汽车前技术主管开发的 Fela,驱动了沃尔沃汽车官网,Cloudflare Dashboard 和 Medium 等众多网站。
Stylex 是 Meta(原 Facebook)的一个尚未开源的预编译 Atomic CSS-in-JS 库。不过由于 Meta 迟迟不开源 stylex,社区中已经涌现出了数个基于其思想的开源实现,其中以 vanilla-extract 最为知名。
基于 stylex 思想的预编译 Atomic CSS-in-JS 库除了 vanilla-extract 之外还有 style9 和 styleQ。
将视线从 stylex 系列中转移开来,Atlassian 还编写了一个名为 compiled 的预编译 Atomic CSS-in-JS 库,但在笔者的实际使用过程中坑点较多,可能会导致样式的重复生成,并且其对 TypeScript 的支持也不尽人意,不过其代码实现中的许多技巧还是有借鉴价值的。
compiled 依靠一个 babel transformer 来对代码进行转换以插入样式。
在 packages/react/src/styled/index.tsx
文件中可以看到,@compiled/react
包含了一个导出了一个名为 styled
的对象,这个对象一旦被访问就会立刻抛出错误,提示 transformer 没有正常工作:
export const styled: StyledComponentInstantiator = new Proxy(
{},
{
get() {
return () => {
// Blow up if the transformer isn't turned on.
// This code won't ever be executed when setup correctly.
throw createSetupError();
};
},
}
) as any;
那么可以看出,styled
会被 transformer 替换掉,对应的入口逻辑在 packages/babel-plugin/src/babel-plugin.tsx
文件中:
ImportDeclaration(path, state) {
// 不是从 @compiled/react 导入的包不处理
if (path.node.source.value !== '@compiled/react') {
return;
}
// 记录导入的模块
state.compiledImports = {};
// 遍历导入数组中的所有元素
path.get('specifiers').forEach((specifier) => {
if (!state.compiledImports || !specifier.isImportSpecifier()) {
return;
}
(['styled', 'ClassNames', 'css', 'keyframes'] as const).forEach((apiName) => {
if (
state.compiledImports &&
t.isIdentifier(specifier.node?.imported) &&
specifier.node?.imported.name === apiName
) {
// 记录下导入后 API 的名称
state.compiledImports[apiName] = specifier.node.local.name;
}
});
});
// 导入 @compiled/react/runtime 中的 API
appendRuntimeImports(path);
path.remove();
},
这段代码记录了 @compiled/react
的引入情况,为下方的处理提供了便利。
TaggedTemplateExpression(path, state) {
if (t.isIdentifier(path.node.tag) && path.node.tag.name === state.compiledImports?.css) {
state.pathsToCleanup.push({ path, action: 'replace' });
return;
}
if (
t.isIdentifier(path.node.tag) &&
path.node.tag.name === state.compiledImports?.keyframes
) {
state.pathsToCleanup.push({ path, action: 'replace' });
return;
}
if (!state.compiledImports?.styled) {
return;
}
// 处理 styled component
visitStyledPath(path, { context: 'root', state, parentPath: path });
},
CallExpression(path, state) {
if (!state.compiledImports) {
return;
}
if (
t.isIdentifier(path.node.callee) &&
(path.node.callee.name === state.compiledImports?.css ||
path.node.callee.name === state.compiledImports?.keyframes)
) {
state.pathsToCleanup.push({ path, action: 'replace' });
return;
}
// 处理 styled component
visitStyledPath(path, { context: 'root', state, parentPath: path });
},
对 TaggedTemplateExpression
和 CallExpression
的处理,正好对应了文档中的两种不同调用方式:
// 模板字符串
styled.a`
color: blue;
`;
// 函数调用
styled.a({
color: 'blue',
});
跟随着 visitStyledPath
函数的定义,可以找到 packages/babel-plugin/src/styled/index.tsx
文件。
export const visitStyledPath = (
path: NodePath<t.TaggedTemplateExpression> | NodePath<t.CallExpression>,
meta: Metadata
): void => {
// 判断是否是支持的操作
if (
t.isTaggedTemplateExpression(path.node) &&
hasInValidExpression(path.node)
) {
throw buildCodeFrameError(
`A logical expression contains an invalid CSS declaration.
Compiled doesn't support CSS properties that are defined with a conditional rule that doesn't specify a default value.
Eg. font-weight: \${(props) => (props.isPrimary && props.isMaybe) && 'bold'}; is invalid.
Use \${(props) => props.isPrimary && props.isMaybe && ({ 'font-weight': 'bold' })}; instead`,
path.node,
meta.parentPath
);
}
// 提取样式信息
const styledData = extractStyledDataFromNode(path.node, meta);
if (!styledData) {
// 没有样式信息
return;
}
// 生成 CSS
const cssOutput = buildCss(styledData.cssNode, meta);
// 构建并替换节点
path.replaceWith(buildStyledComponent(styledData.tag, cssOutput, meta));
const parentVariableDeclaration = path.findParent((x) =>
x.isVariableDeclaration()
);
if (
parentVariableDeclaration &&
t.isVariableDeclaration(parentVariableDeclaration.node)
) {
const variableDeclarator = parentVariableDeclaration.node.declarations[0];
if (t.isIdentifier(variableDeclarator.id)) {
const variableName = variableDeclarator.id.name;
parentVariableDeclaration.insertAfter(buildDisplayName(variableName));
}
}
};
再来看提取样式信息的函数 extractStyledDataFromNode
,这个函数根据不同情况使用不同的方法提取样式信息:
const extractStyledDataFromNode = (
node: t.TaggedTemplateExpression | t.CallExpression,
meta: Metadata
) => {
// 使用模板字符串
if (t.isTaggedTemplateExpression(node)) {
return extractStyledDataFromTemplateLiteral(node, meta);
}
// 使用函数调用
if (t.isCallExpression(node)) {
return extractStyledDataFromObjectLiteral(node, meta);
}
// 提取不到信息
return undefined;
};
构建新节点的函数被定义在 packages/babel-plugin/src/utils/ast-builders.tsx
文件中:
export const buildStyledComponent = (
tag: Tag,
cssOutput: CSSOutput,
meta: Metadata
): t.Node => {
const unconditionalCss: string[] = [];
const logicalCss: CssItem[] = [];
cssOutput.css.forEach((item) => {
if (item.type === 'logical') {
logicalCss.push(item);
} else {
unconditionalCss.push(getItemCss(item));
}
});
// 去重,只保留最后一个
const uniqueUnconditionalCssOutput = transformCss(unconditionalCss.join(''));
const logicalCssOutput = transformItemCss({
css: logicalCss,
variables: cssOutput.variables,
});
const sheets = [
...uniqueUnconditionalCssOutput.sheets,
...logicalCssOutput.sheets,
];
const classNames = [
...[t.stringLiteral(uniqueUnconditionalCssOutput.classNames.join(' '))],
...logicalCssOutput.classNames,
];
// 返回构建好的节点
return styledTemplate(
{
classNames,
tag,
sheets,
variables: cssOutput.variables,
},
meta
);
};
至于构建节点的操作,则是较为简单的字符串拼接:
const styledTemplate = (opts: StyledTemplateOpts, meta: Metadata): t.Node => {
const nonceAttribute = meta.state.opts.nonce
? `nonce={${meta.state.opts.nonce}}`
: '';
const propsToDestructure: string[] = [];
// 提取样式
const styleProp = opts.variables.length
? styledStyleProp(opts.variables, (node) => {
const nestedArrowFunctionExpressionVisitor = {
noScope: true,
MemberExpression(path: NodePath<t.MemberExpression>) {
const propsToDestructureFromMemberExpression =
handleMemberExpressionInStyledInterpolation(path);
propsToDestructure.push(...propsToDestructureFromMemberExpression);
},
Identifier(path: NodePath<t.Identifier>) {
const propsToDestructureFromIdentifier =
handleDestructuringInStyledInterpolation(path);
propsToDestructure.push(...propsToDestructureFromIdentifier);
},
};
if (t.isArrowFunctionExpression(node)) {
return traverseStyledArrowFunctionExpression(
node,
nestedArrowFunctionExpressionVisitor
);
}
if (t.isBinaryExpression(node)) {
return traverseStyledBinaryExpression(
node,
nestedArrowFunctionExpressionVisitor
);
}
return node;
})
: t.identifier('style');
let unconditionalClassNames = '',
logicalClassNames = '';
opts.classNames.forEach((item) => {
if (t.isStringLiteral(item)) {
unconditionalClassNames += `${item.value} `;
} else if (t.isLogicalExpression(item)) {
logicalClassNames += `${generate(item).code}, `;
}
});
// classNames 为生成好的类名
const classNames = `"${unconditionalClassNames.trim()}", ${logicalClassNames}`;
// 此处的 <CC />, <CS /> 是上文中处理 import 时从 @compiled/react/runtime 中导入的组件
return template(
`
forwardRef(({
as: C = ${buildComponentTag(opts.tag)},
style,
${unique(propsToDestructure)
.map((prop) => prop + ',')
.join('')}
...${PROPS_IDENTIFIER_NAME}
}, ref) => (
<CC>
<CS ${nonceAttribute}>{%%cssNode%%}</CS>
<C
{...${PROPS_IDENTIFIER_NAME}}
style={%%styleProp%%}
ref={ref}
className={ax([${classNames} ${PROPS_IDENTIFIER_NAME}.className])}
/>
</CC>
));
`,
{
plugins: ['jsx'],
}
)({
styleProp,
cssNode: t.arrayExpression(
unique(opts.sheets).map((sheet) => hoistSheet(sheet, meta))
),
}) as t.Node;
};
这样兜兜转转一圈下来,就将使用了 styled
方法生成的组件的样式抽离出来,变成了一个 compiled 的 Atomic CSS-in-JS 组件。
css
Propcompiled 首先 增加了 css
prop 的 TypeScript 定义,然后和 styled component 一样在 babel transform 的时候对这个 prop 进行特殊处理:
JSXOpeningElement(path, state) {
if (!state.compiledImports) {
return;
}
// 处理 css prop
visitCssPropPath(path, { context: 'root', state, parentPath: path });
},
相比于 styled component 繁复的处理方式,css
prop 的处理看起来简洁了许多:
export const visitCssPropPath = (
path: NodePath<t.JSXOpeningElement>,
meta: Metadata
): void => {
let cssPropIndex = -1;
const cssProp = path.node.attributes.find(
(attr, index): attr is t.JSXAttribute => {
if (t.isJSXAttribute(attr) && attr.name.name === 'css') {
cssPropIndex = index;
return true;
}
return false;
}
);
// 不存在 css prop 就不进行处理了
if (!cssProp || !cssProp.value) {
return;
}
// 从 css props 中提取样式信息
const cssOutput = buildCss(getJsxAttributeExpression(cssProp), meta);
// 删除 css prop
path.node.attributes.splice(cssPropIndex, 1);
// 没有样式信息
if (!cssOutput.css.length) {
return;
}
// 构建并替换节点
path.parentPath.replaceWith(
buildCompiledComponent(
path.parentPath.node as t.JSXElement,
cssOutput,
meta
)
);
};
构建新节点的 buildCompiledComponent
函数被定义在 packages/babel-plugin/src/utils/ast-builders.tsx
文件中,这个函数主要完成了以下操作:
className
;css
prop 中的样式;这样就将组件的 css
参数拆成了两部分 —— 静态的样式和附加到原组件的 className
参数值。
微软最近开源的 Griffel 既支持运行时模式,又支持预编译模式,同时拥有着更佳的 TypeScript 支持,不失为一个好的选择。这个库目前驱动了微软官方的 Fluent UI。
以上就是本文要介绍关于 Atomic CSS 的全部内容了。
虽然 Atomic CSS-in-JS 是 React 生态系统中新涌起的一股潮流,但在使用前一定要三思 —— 这个方案到底符不符合项目的需求,而不是盲目地「为了使用而使用」,给将来的维护工作埋雷,但如果使用它能带来显而易见的好处,那么何乐而不为呢?
笔者才疏学浅,只是在前人的基础之上做了一些微小的工作而已,文章中如有错误欢迎在评论区指正。感谢 Sukka 大佬在本文编写过程中的指导。感谢 Byran Lee 指出本文中的错误。
对于一个有很多服务器的人来说,在不同服务器上同步 SSL 证书是一件麻烦事。笔者尝试过很多种方式,最后在 Menci 的推荐下选定了使用 GitHub Actions 来自动申请、续期 SSL 证书,并自动推送到各个服务器上。
本博客的证书也是使用这种方式进行签发、部署的,可以点击浏览器地址栏上的按钮查看证书。
首先请在本地(或自己的服务器上)成功使用 acme.sh 的 DNS-01 验证方式成功申请一次证书,如果不会操作的话可以参考 烧饼博客的教程 来进行。这个过程包括:
第一次申请证书后,CA 的 ACME 账户凭据将被存储到 ~/.acme.sh/ca
中,DNS 提供商的凭据将被存储到 ~/.acme.sh/account.conf
中。将它们打包并使用 Base64 编码存储,以备在 GitHub Actions 中使用:
cd ~/.acme.sh
tar cz ca account.conf | base64 -w0
将输出内容添加到 GitHub 仓库的 Secrets 中。注意不要复制输出中的多余信息。
如果没有特殊需求,可以使用 Menci/acme 来简单地申请证书:
# 全局环境变量
env:
# Checkout 到的目录
CERTS_OUTPUT_BASE: certs
# 证书输出目录
CERTS_OUTPUT_DIRECTORY: example.com
# 证书文件名
FILE_FULLCHAIN: fullchain.pem
# 私钥文件名
FILE_KEY: privatekey.key
jobs:
issue-ssl-certificate:
name: Issue SSL certificate
runs-on: ubuntu-latest
steps:
- uses: Menci/acme@v2
with:
# 指定 acme.sh 的版本
version: 3.0.2
# 上方保存的以 Base64 编码存储的凭据
account-tar: ${{ secrets.ACME_SH_ACCOUNT_TAR }}
# 域名列表,以空格分隔
domains: example.com example.net example.org example.edu
# 是否申请通配符
append-wildcard: true
# 传递给 acme.sh 的额外参数
arguments: --dns dns_cf --challenge-alias example.com
# 导出的证书路径
output-fullchain: ${{ env.CERTS_OUTPUT_BASE }}/${{ env.CERTS_OUTPUT_DIRECTORY }}/${{ env.FILE_FULLCHAIN }}
output-key: ${{ env.CERTS_OUTPUT_BASE }}/${{ env.CERTS_OUTPUT_DIRECTORY }}/${{ env.FILE_KEY }}
如果需要高度自定义 acme.sh 的参数,比如为不同的域名设置不同的 DNS 提供商,可以使用下面的方式手动编写命令来执行:
# 全局环境变量
env:
# Checkout 到的目录
CERTS_OUTPUT_BASE: certs
# 证书输出目录
CERTS_OUTPUT_DIRECTORY: example.com
# 证书文件名
FILE_FULLCHAIN: fullchain.pem
# 私钥文件名
FILE_KEY: privatekey.key
jobs:
issue-ssl-certificate:
name: Issue SSL certificate
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
with:
ref: master
- name: Checkout output branch
uses: actions/checkout@v2
with:
ref: certs
path: ${{ env.CERTS_OUTPUT_BASE }}
# 安装 acme.sh
- name: Install acme.sh
shell: bash
run: curl -s https://get.acme.sh | sh
# 解压 acme.sh 配置信息
- name: Extract account files for acme.sh
shell: bash
run: |
echo "$ACME_SH_ACCOUNT_TAR" | base64 -d | tar -C ~/.acme.sh -xz
env:
# Base64 编码的 acme.sh 配置信息
ACME_SH_ACCOUNT_TAR: ${{ secrets.ACME_SH_ACCOUNT_TAR }}
# 申请证书
- name: Issue SSL certificates
shell: bash
run: |
~/.acme.sh/acme.sh --issue \
-d "example.com" --dns dns_cf \
-d "*.example.com" --dns dns_cf \
-d "example.net" --dns dns_dp \
-d "*.example.net" --dns dns_dp \
--server letsencrypt
# 导出证书
- name: Copy certificate to output paths
shell: bash
run: |
ACME_SH_TEMP_DIR="$(mktemp -d)"
ACME_SH_TEMP_FILE_FULLCHAIN="$ACME_SH_TEMP_DIR/fullchain.pem"
ACME_SH_TEMP_FILE_KEY="$ACME_SH_TEMP_DIR/key.pem"
~/.acme.sh/acme.sh --install-cert -d "$ACME_SH_FIRST_DOMAIN" --fullchain-file "$ACME_SH_TEMP_FILE_FULLCHAIN" --key-file "$ACME_SH_TEMP_FILE_KEY"
[[ -z "$ACME_SH_OUTPUT_FULLCHAIN" ]] || (mkdir -p "$(dirname "$ACME_SH_OUTPUT_FULLCHAIN")" && cp "$ACME_SH_TEMP_FILE_FULLCHAIN" "$ACME_SH_OUTPUT_FULLCHAIN")
[[ -z "$ACME_SH_OUTPUT_KEY" ]] || (mkdir -p "$(dirname "$ACME_SH_OUTPUT_KEY")" && cp "$ACME_SH_TEMP_FILE_KEY" "$ACME_SH_OUTPUT_KEY")
rm -rf "$ACME_SH_TEMP_DIR"
env:
# 修改此处的 example.com 为申请时填写的第一个域名
ACME_SH_FIRST_DOMAIN: example.com
ACME_SH_OUTPUT_FULLCHAIN: ${{ env.CERTS_OUTPUT_BASE }}/${{ env.CERTS_OUTPUT_DIRECTORY }}/${{ env.FILE_FULLCHAIN }}
ACME_SH_OUTPUT_KEY: ${{ env.CERTS_OUTPUT_BASE }}/${{ env.CERTS_OUTPUT_DIRECTORY }}/${{ env.FILE_KEY }}
# 上传证书
- name: Push to GitHub
run: |
git config --global user.name "BaoshuoBot"
git config --global user.email "79077260+BaoshuoBot@users.noreply.github.com"
cd "$CERTS_DIRECTORY"
git add "$FILE_FULLCHAIN" "$FILE_KEY"
git commit -m "Upload certificates on $(date '+%Y-%m-%d %H:%M:%S')"
git push
env:
TZ: Asia/Shanghai
CERTS_DIRECTORY: ${{ env.CERTS_OUTPUT_BASE }}/${{ env.CERTS_OUTPUT_DIRECTORY }}
在申请证书的 Job 执行完成后,可以执行一系列其他的 Job 来将证书部署到各个服务器或云服务。
可以使用 easingthemes/ssh-deploy
来使用 rsync 将证书同步到服务器上。同步完成后再使用 appleboy/ssh-action
远程执行命令重载 Nginx / Apache。
# 部署到服务器
deploy-to-server:
name: Deploy Certificate to Server
runs-on: ubuntu-latest
needs: issue-ssl-certificate
strategy:
matrix:
host:
- 174.136.239.1 # Server 1
- 174.136.239.2 # Server 2
# ...
- 174.136.239.254 # Server N
steps:
- name: Checkout
uses: actions/checkout@v2
with:
ref: certs
# 上传证书
- name: Upload certificate to server
uses: easingthemes/ssh-deploy@v2.1.5
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
ARGS: '-avz --delete'
REMOTE_HOST: ${{ matrix.host }}
REMOTE_USER: ${{ secrets.REMOTE_USER }}
SOURCE: ${{ env.CERTS_OUTPUT_DIRECTORY }}/
TARGET: /path/to/ssl/certs/${{ env.CERTS_OUTPUT_DIRECTORY }}/
# 重载 Nginx
- name: Force-reload nginx
uses: appleboy/ssh-action@v0.1.4
with:
host: ${{ matrix.host }}
username: ${{ secrets.REMOTE_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
sudo /opt/hooks/reload-nginx.sh
需要注意的是,重载 Nginx / Apache 的命令需要 root 权限才能执行,可以采用只允许部署用户以 root 权限执行重载脚本的方式来避免出现安全问题。
在 /opt/hooks
目录下新建一个文件 reload-nginx.sh
,内容如下:
#!/bin/bash
sudo systemctl force-reload nginx
然后新建一个名为 actions-cert
的用户,然后在 /etc/sudoers
文件中添加以下内容:
actions-cert ALL=(ALL) NOPASSWD: /opt/hooks/reload-nginx.sh
这个配置可以使 actions-cert
用户免密码以 root 用户的权限执行 /opt/hooks/reload-nginx.sh
。
最后使用 chmod 755 /opt/hooks/reload-nginx.sh
命令将 reload-nginx.sh
文件设置为可执行,同时禁止非所有者对其进行写入操作。
如果服务器位于 NAT 后,或者禁止了 SSH 连接,还有两个方法可以将证书部署到内网服务器上:
阿里云的 SSL 证书服务 支持上传自定义证书,该证书可以用于 阿里云 CDN。阿里云暂未提供将证书部署至 OSS 的 API,建议 OSS 用户使用 CDN 回源 OSS 来代替。
使用 Menci/deploy-certificate-to-aliyun 将证书部署到阿里云:
# 部署到阿里云
deploy-to-aliyun:
name: Deploy Certificate to Aliyun
runs-on: ubuntu-latest
needs: issue-ssl-certificate
steps:
# 拉取证书存储分支
- name: Checkout
uses: actions/checkout@v2
with:
ref: certs
# 上传证书
- name: Deploy certificate to aliyun
uses: Menci/deploy-certificate-to-aliyun@beta-v1
with:
access-key-id: ${{ secrets.ALIYUN_ACCESS_KEY_ID }}
access-key-secret: ${{ secrets.ALIYUN_ACCESS_KEY_SECRET }}
fullchain-file: ${{ env.CERTS_OUTPUT_DIRECTORY }}/${{ env.FILE_FULLCHAIN }}
key-file: ${{ env.CERTS_OUTPUT_DIRECTORY }}/${{ env.FILE_KEY }}
certificate-name: example.com
cdn-domains: |
example.com
example.net
其中 certificate-name
指定上传的证书在证书服务中的名称(将自动替换旧版本),cdn-domains
指定需要将该证书部署到的 CDN 域名列表(用空白字符隔开)。
建议使用子账户 Access Key,为其赋予以下权限(并按需使用资源组隔离):
使用 renbaoshuo/deploy-certificate-to-tencentcloud 将证书部署至腾讯云 CDN:
deploy-to-qcloud-cdn:
name: Deploy certificate to Tencent Cloud CDN
runs-on: ubuntu-latest
needs: issue-ssl-certificate
steps:
- name: Check out
uses: actions/checkout@v2
with:
# If you just commited and pushed your newly issued certificate to this repo in a previous job,
# use `ref` to make sure checking out the newest commit in this job
ref: ${{ github.ref }}
- uses: renbaoshuo/deploy-certificate-to-tencentcloud@v1
with:
# Use Access Key
secret-id: ${{ secrets.QCLOUD_SECRET_ID }}
secret-key: ${{ secrets.QCLOUD_SECRET_KEY }}
# Specify PEM fullchain file
fullchain-file: ${{ env.CERTS_OUTPUT_DIRECTORY }}/${{ env.FILE_FULLCHAIN }}
# Specify PEM private key file
key-file: ${{ env.CERTS_OUTPUT_DIRECTORY }}/${{ env.FILE_KEY }}
# Deploy to CDN
cdn-domains: |
cdn1.example.com
cdn2.example.com
其中 cdn-domains
指定需要将该证书部署到的 CDN 域名列表(用空白字符隔开)。
建议使用子账户 API 密钥,为其赋予以下权限(并按需使用资源组隔离):
使用 renbaoshuo/deploy-certificate-to-goedge
将证书部署至自建的 GoEdge CDN:
deploy-to-goedge-cdn:
name: Deploy certificate to GoEdge CDN
runs-on: ubuntu-latest
steps:
- name: Check out
uses: actions/checkout@v2
with:
# If you just commited and pushed your newly issued certificate to this repo in a previous job,
# use `ref` to make sure checking out the newest commit in this job
ref: ${{ github.ref }}
- uses: renbaoshuo/deploy-certificate-to-goedge@beta-v1
with:
# GoEdge API endpoint
api-endpoint: https://cdn.api.baoshuo.dev
# Use Access Key
access-key-type: user
access-key-id: ${{ secrets.GOEDGE_ACCESS_KEY_ID }}
access-key: ${{ secrets.GOEDGE_ACCESS_KEY }}
# GoEdge certificate ID
cert-id: ${{ secrets.GOEDGE_CERT_ID }}
# Specify PEM fullchain file
fullchain-file: ${{ env.FILE_FULLCHAIN }}
# Specify PEM private key file
key-file: ${{ env.FILE_KEY }}
注:在部署前需要手动上传一次证书以便获取证书 ID。证书 ID 可以在「证书文件下载」处的 URL 参数中找到。
这个 Action 完成了以下操作:
certs
分支。certs
分支中的证书部署到服务器上。# 名称
name: Issue SSL Certificates
# 触发条件
on:
# 手动运行
workflow_dispatch:
# 定时运行
schedule:
# 每两个月运行一次
- cron: '0 0 1 */2 *'
# 全局环境变量
env:
# Checkout 到的目录
CERTS_OUTPUT_BASE: certs
# 证书输出目录
CERTS_OUTPUT_DIRECTORY: example.com
# 证书文件名
FILE_FULLCHAIN: fullchain.pem
# 私钥文件名
FILE_KEY: privatekey.key
jobs:
issue-ssl-certificate:
# 申请证书并 push 到 certs 分支
name: Issue SSL certificate
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
with:
ref: master
- name: Checkout output branch
uses: actions/checkout@v2
with:
ref: certs
path: ${{ env.CERTS_OUTPUT_BASE }}
# 安装 acme.sh
- name: Install acme.sh
shell: bash
run: curl -s https://get.acme.sh | sh
# 解压 acme.sh 配置信息
- name: Extract account files for acme.sh
shell: bash
run: |
echo "$ACME_SH_ACCOUNT_TAR" | base64 -d | tar -C ~/.acme.sh -xz
env:
# Base64 编码的 acme.sh 配置信息
ACME_SH_ACCOUNT_TAR: ${{ secrets.ACME_SH_ACCOUNT_TAR }}
# 申请证书
- name: Issue SSL certificates
shell: bash
run: |
~/.acme.sh/acme.sh --issue \
-d "example.com" -d "*.example.com" \
--dns dns_cf --server letsencrypt
# 导出证书
- name: Copy certificate to output paths
shell: bash
run: |
ACME_SH_TEMP_DIR="$(mktemp -d)"
ACME_SH_TEMP_FILE_FULLCHAIN="$ACME_SH_TEMP_DIR/fullchain.pem"
ACME_SH_TEMP_FILE_KEY="$ACME_SH_TEMP_DIR/key.pem"
# 不要忘记修改这里的 -d 参数值为上方的第一个域名
~/.acme.sh/acme.sh --install-cert -d "example.com" --fullchain-file "$ACME_SH_TEMP_FILE_FULLCHAIN" --key-file "$ACME_SH_TEMP_FILE_KEY"
[[ -z "$ACME_SH_OUTPUT_FULLCHAIN" ]] || (mkdir -p "$(dirname "$ACME_SH_OUTPUT_FULLCHAIN")" && cp "$ACME_SH_TEMP_FILE_FULLCHAIN" "$ACME_SH_OUTPUT_FULLCHAIN")
[[ -z "$ACME_SH_OUTPUT_KEY" ]] || (mkdir -p "$(dirname "$ACME_SH_OUTPUT_KEY")" && cp "$ACME_SH_TEMP_FILE_KEY" "$ACME_SH_OUTPUT_KEY")
rm -rf "$ACME_SH_TEMP_DIR"
env:
ACME_SH_OUTPUT_FULLCHAIN: ${{ env.CERTS_OUTPUT_BASE }}/${{ env.CERTS_OUTPUT_DIRECTORY }}/${{ env.FILE_FULLCHAIN }}
ACME_SH_OUTPUT_KEY: ${{ env.CERTS_OUTPUT_BASE }}/${{ env.CERTS_OUTPUT_DIRECTORY }}/${{ env.FILE_KEY }}
# 上传证书
- name: Push to GitHub
run: |
git config --global user.name "BaoshuoBot"
git config --global user.email "79077260+BaoshuoBot@users.noreply.github.com"
cd "$CERTS_DIRECTORY"
git add "$FILE_FULLCHAIN" "$FILE_KEY"
git commit -m "Upload certificates on $(date '+%Y-%m-%d %H:%M:%S')"
git push
env:
TZ: Asia/Shanghai
CERTS_DIRECTORY: ${{ env.CERTS_OUTPUT_BASE }}/${{ env.CERTS_OUTPUT_DIRECTORY }}
# 部署证书到服务器
deploy-to-server:
name: Deploy Certificate to Server
runs-on: ubuntu-latest
needs: issue-ssl-certificate
strategy:
matrix:
host:
- 174.136.239.1 # Server 1
- 174.136.239.2 # Server 2
# ...
- 174.136.239.254 # Server N
steps:
- name: Checkout
uses: actions/checkout@v2
with:
ref: certs
# 上传证书
- name: Upload certificate to server
uses: easingthemes/ssh-deploy@v2.1.5
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
ARGS: '-avz --delete'
REMOTE_HOST: ${{ matrix.host }}
REMOTE_USER: ${{ secrets.REMOTE_USER }}
SOURCE: ${{ env.CERTS_OUTPUT_DIRECTORY }}/
TARGET: /path/to/ssl/certs/${{ env.CERTS_OUTPUT_DIRECTORY }}/
# 重载 Nginx
- name: Force-reload nginx
uses: appleboy/ssh-action@v0.1.4
with:
host: ${{ matrix.host }}
username: ${{ secrets.REMOTE_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
sudo /opt/hooks/reload-nginx.sh
部分情况下,GitHub Actions 中的 GITHUB_TOKEN
只有 Read repository contents permission,而本文中的 Actions 要求这个 Token 具有 Read and write permissions,那么需要在仓库的 Settings > Actions > General 页面的底部赋予其写入权限,如图所示:
设置好后点击 Save 按钮即可。
文章头图由 Menci 制作,使用已经过授权,在此表示感谢。
]]>▲ 安装时的报错信息
这个迷惑的报错信息误导了我很长时间,再加上我在学校里拿不到手机,于是 3 月初的问题一直拖到了 4 月底才解决。
拿到手机之后我突然想看看手机里有什么不再需要的应用需要卸载,清理清理手机的存储空间。于是我翻到了这么一个应用:
▲ 应用图标右下角的橙色标志代表了这是一个安装在应用分身中的应用
但是我手机里的应用分身只能安装两个应用:微信和 QQ。所以问题应该就是出在了这里。我试着点击了下方的「卸载」按钮,但提示卸载成功并重启后,这个 Chrome 浏览器依然静静地呆在手机里,丝毫没有要离开的意思。
我在网上找到了一篇名为《技术 | One UI 不熔断,让应用分身双开任何应用》的文章,里面讲解了如何在应用分身中安装其他应用的方法,还提到了应用分身其实是以 Android 多用户的形式实现的,分身中的所有应用都安装在了名为 DUAL_APP
的用户下。知道了这些信息,事情就好办得多了,直接用 ADB 把它搞掉就行了(ADB 下载地址)。
在手机上开启开发者模式 后,在「开发者选项」中允许「USB 调试」,然后使用数据线将手机连接到电脑,这样就可以在电脑上通过 ADB 操控手机了。
使用下面的命令来查看手机中的用户列表:
adb shell pm list users
▲ 图中被红色方框框住的就是应用分身对应的用户了
然后使用下面的命令来查看分身中的应用列表:
adb shell pm list packages --user 95
▲ 图中被红色方框框住的就是应用分身中的 Chrome 浏览器的包名了
那么就可以卸载分身中的 Chrome 浏览器了:
adb uninstall --user 95 com.android.chrome
卸载完成后,就可以通过你喜欢的方式安装应用了。我这里同样使用了 ADB 来进行 Chrome 浏览器的安装操作:
adb install "com.android.chrome_101.0.4951.41-495104123.apk"
最后不要忘记关掉「USB 调试」功能,以免产生安全风险。
]]>这是我第一次出省参加外校集训。此行的目的地是江苏省常州高级中学,于 1907 年建校,截止本文写作时该校在「OIerDb 全国信息学竞赛学校排行榜」上位列第九。
走前 2 天,也就是 2 月 18 日,我们才正式确定要动身去常中参加集训,当时火车票余票已经所剩无几了,索性直接买了一等座,省时省力,就是有点费钱。
一等座确实比二等座舒服了很多,还有免费的零食、饮料、矿泉水。不过,要是下次可以自己挑的话,我还是选二等座,经济实惠。
晚饭在火车上买了份「豚骨面」,花了 39 元,除了汤有点发酸(可能是醋倒多了)以外别的没啥毛病。
由于学校没有给我们提供宿舍,所以我们两人成团一起住宾馆,每间 160 元/晚。
环境还行,凑合着住。
▲ 外景
▲ 饭卡
食堂的餐食比假期的二南好多了,但我觉得赶不上开学时候的二南食堂。
常中并没有给我们分配上网账号,所以要么用打开文件资源管理器都能死机的台式机上网,要么用自己的笔记本离线操作。
毛主席说过:「自己动手,丰衣足食。」所以我们把台式机的网线拔下来插到了自己的电脑上,并且手动配好了 IP 地址,成功解决了上网问题。
蹭网期间出一个小插曲:有人拿自己配好的网开热点给大家共享网络,结果被系统自动断网并且封掉设备了…
进入校园。
标志性的「SCZ」标识。
校园一角。
教学楼外景。
小亭子。
学校旁边的 天宁宝塔。
来的时候在火车上吃的是面条,于是返程的时候买了一盒「红烧牛肉饭」,花了 40 元,味道也不错。
回学校的最后一段路是坐公交回去的。
到站了,老师带着我们下了车,下车后才发现 zzq 还在车上,于是我们眼睁睁地看着公交车继续前行,而 zzq 还不知道他已经坐过站了…
在校期间的学习、交流等活动均属保密范畴,在本文中不做叙述。
希望以后还能有这么好的机会参加外出集训。
]]>时光飞逝,2021 年就这么过完了。在这一年,我经历了很多,也学到了很多。现在,让我坐在电脑前,用文字的方式将这一切都记录下来…
本文内容按照发生时间先后排序。
2021 年 1 月 3 日,石家庄疫情 爆发,石家庄市进入战时状态(新华网)。
次日清晨,学校里传开了一条爆炸性新闻 —— 封校了!
中午下课后,大家纷纷奔向学校超市,开始抢购商品。由于初三下课最晚,等我们奔向超市的时候,超市空得连价签都没了。我并没有去抢,因为我在元旦假期返校的时候带了不少东西,足够我吃了。
然而超市一连好几天无货可卖,这使得我们都开始担心 —— 万一哪天没饭了怎么办?而这时学校终于传来了好消息:学校和政府终于为超市协调来了货源,可以继续上架面包、牛奶和生活必需品了。
不过我去食堂的时候倒是发现了一个以前从未出现的奇妙现象,那就是肉给的比菜多。我举一个典型事例:我去食堂买板面吃,食堂阿姨给我盛了半勺子肉,然后从旁边的菜盆里夹了三根很小的菠菜叶放到了碗里。除此之外,免费续米饭的量也变得越来越少。不过有的吃我就知足了,没必要那么挑。
周末学校还给放了电影,周日按照正常留宿作息多睡了会。封校的第一周就这么过去了。
谁也没想到封校会持续那么久,于是大家都在学校呆着,在学习的同时等待外界的好消息。
有一次教育处的尹主任值我们班晚自习,我正好坐第一排,作业写完了没事干,又去不了高中部,便和老师聊了起来。我向老师抱怨初三的学生觉不够睡,而初一的学生休息时间太多以至于睡不着觉,希望能多给初三的学生点休息时间,不然上课得困死。听完以后,老师给年级主任发了条微信。没想到,第二节晚自习下课,年级就广播说以后每周六都让初三的多睡一小时。多亏了尹主任的反馈,我终于能补个好觉了,上课也终于不用再半睡半醒了。
就这样又过了大概二十天,外面终于传来了好消息 —— 解封了!
听到解封的消息时,我的心情很复杂 —— 我的健康码是黄色的。这代表即使学校解封了,我也回不了家。
好在解封的第二天,我的健康码变回了绿色,当天下午我被接回了家,一切都结束了。
6 月 21 日,我迎来了人生中的第一次大考 —— 中考。
说实话,头进考场我还是有一点点发慌的,因为我的知识大多都是退役以后现补的。物理的力学还没补,语文的古诗文刚背完原文没背课注和赏析,政治看都没看…
总之没好好复习就是了。就这样硬着头皮进了考场,考啥算啥吧。
考完以后便是漫长的等待。7 月 3 日 0 时,终于出分了:
中考试卷的难易度还算适中,考得不算特别差,全市三千多名。
由于疫情的原因,石家庄并没有举行体育和实验的考试,所以中考总分只有 610 分。这对于体育不是很好的我来说是一个救命稻草,要不然我得比别人低至少 10 分,就没学可上了。
中考成绩出来以后,我向我的竞赛教练咨询了一下我的成绩,教练告诉我没啥问题,能上二南。
居然混进了一个省重点高中… 失学儿童有学上了!
上了高中以后因为学习竞赛,被分到了省理科竞赛实验班,大概是全校最好的班级了吧。只不过因为班里其他人都是各地状元,所以我的文化课被同学们吊起来暴锤。
总体还行。只是没想到因为这些事就被别人称为了「别人家的孩子」,当不起当不起,毕竟我是真的菜…
今年的竞赛成绩并不理想。
CSP 拿了个省二,再多考 19 分就有省一了。
NOIP 打算冲省一,结果考试的时候电脑蓝屏了… 以后再也不用画图当草稿纸了!
不过和自己的好朋友们在一起努力还是很开心的~
丢人的练习详情可以在 GitHub 上查看:Compare 2020…2021 - renbaoshuo/OI-codes。
今年年初,我开始使用 GPG 来对我的消息和提交签名。
我的 GPG 公钥可以在 GitHub 上找到。
年初的时候给 HUSTOJ 贡献了两个主题:SYZOJ 和 MDUI。
因为 SYZOJ 主题被设置为了默认主题,所以年末的时候还被教练找上门问 HUSTOJ 相关的问题来了。
从寒假开始我开始接触计算机网络相关的知识,并且在 DN42 和公网上进行了相关实践。
这个主题移植自 imhanjie/gridea-theme-pure。
源代码公开在 GitHub 上。
体验良好,于是年末又入了个 CanoKey 作为备用 Key。
这是个从 12 月份刚刚开始搞的项目,目前正处于开发期,由我和 Menci 共同维护。
计划中 OIerDb NG 将会最终替代现在的 OIerDb。
OIerDb NG 的前端缓存式查询方法在提高查询速度的同时还省去了后端服务器的处理。
代码开源在 GitHub 上,欢迎 Star ~
其实里面有好多都是贡献给自己的私有仓库了。
可以直接去 我的 GitHub 主页 上看公开的贡献详情,懒得再多说了。
2021 年一转眼就过完了,有很多新收获,同时也有很多遗憾,就不再多说了。
希望自己 2022 年会过得更好吧,也祝读者们新年快乐。
本文封面图片来自美国驻华使领馆官方推特账号,其他图片均为本人所拍摄或截取。
]]>笔者最近阅读了 ChrAlpha 大佬的 初探无后端静态博客自动化部署方案 这篇博文,发现其中的一些内容已经过时,所以我决定写下这篇博文来简单说一说现在(2021 年)静态博客的自动部署方案。
笔者 9 月中旬将自己的 OI 博客(oi.baoshuo.ren)迁到了 GitHub 上并使用 GitHub Actions 进行持续部署,在感觉良好以后又在 10 月初将自己的主博客(blog.baoshuo.ren)也迁移到了上面,这期间我尝试了多家 CI/CD 服务,并总结了一些配置要领。
本文中使用的 NodeJS 包管理器为 yarn 。
GitHub Actions 是笔者最常用的 CI/CD 服务,没有之一。关于 GitHub Actions 的介绍可以查看 GitHub Actions 官网文档 和阮老师的 GitHub Actions 入门教程 。
配置 GitHub Actions 是非常方便的,无需绑定任何第三方平台,仅需在仓库中新建一个 .github/workflows/*.yml
即可。
Hexo 核心团队成员之一 Sukka 大佬在他的 将 Hexo 部署到 Cloudflare Workers Site 上的趟坑记录 一文中强烈建议不要使用任何 Hexo 的 GitHub Action ,把 Hexo 当成一个普通的依赖 NodeJS 的构建程序,遵照这个思路编写配置文件即可。
name: Build
on:
push:
branches:
- master
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use Node.js 14
uses: actions/setup-node@v2
with:
node-version: 14
cache: yarn
- name: Install Dependencies
run: yarn install
- name: Build Site
run: yarn build
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: public
cname: blog.baoshuo.ren
该工作流的大体步骤如下:
在构建完成后会发现文章的更新时间出现了错误,可以在生成前添加下面这步来将文件的修改日期更正为该文件最后一次 commit 时的时间。
- name: Fix File Modify Date
run: |
git ls-files | while read filepath; do touch -d "$(git log -1 --format='@%ct' $filepath)" "$filepath" && echo "Fixed: $filepath"; done
当然,为了让 git 有日志可寻,还得在检出的时候顺带把所有提交历史一并拉下来,指定 fetch-depth
就能做到:
- uses: actions/checkout@v2
with:
fetch-depth: 0
然后开启 GitHub Pages 功能即可访问到构建好的站点了。
如果觉得 GitHub Actions + GitHub Pages 比较麻烦的话,才推出不久的 Cloudflare Pages 或许是一个不错的选择。
前往 CloudFlare 管理面板 登录账号后即可在右侧找到「网页」或者「Pages」功能的入口。
点击新建按钮,选择要使用的仓库,点击下一步。
Cloudflare Pages 的构建配置模板中并没有给出 Hexo 的模板,所以需要自己填写:
完成之后点击确定就可以开始构建站点了。
构建完成后,访问分配的 pages.dev
子域名即可查看构建好的站点。
可以在「自定义域」选项卡中添加自定义域名。
Netlify 是一家为静态网站提供托管服务的平台,有一定的免费额度,对于小型站点来说使用免费套餐就足够了。
首先登陆账号,点击「New site from Git」按钮新建一个站点。
可以看到 Netlify 自动识别出了博客使用的框架并且填充好了构建命令,接下来只需要点击下一步即可。
新建站点完成以后可以在「Domain Settings」页面中修改默认分配的 netlify.app
子域名,并添加自定义域名。
Vercel 是一个类似于 Netlify 的静态网站托管平台(Vercel 也支持托管 Serverless 函数,不过不在本文的讨论范围内)。
绑定好 GitHub 账号后导入一个新的项目即可。
Vercel 也和 Netlify 自动填充好了构建命令,如果没有特殊需求可以直接点击下一步。
可以在项目的域名设置中绑定自定义域名。
点击 Next
键,发现页面的 URL 后多了个 ?page=1
,结合第一个页面中的 1970-01-01
字样,可以判断出来 page
参数应为比赛期间的 Unix 时间戳。
示例:http://202.38.93.111:10000/?page=1635002186
可以照着 16 进制数据搞一搞,然后 flag 就出来了:
下载音频之后使用 Adobe Audition 进行变速即可。
从图片中可以看出拍摄者在 14 层,并且楼下有一个蓝色的肯德基,那么使用 Google 搜索关键词 海洋 蓝色 KFC
可以得到以下结果:
从照片的描述中可以得到这家肯德基位于秦皇岛新澳海底世界。
在百度地图上可以找到这家肯德基的电话、详细位置。同时按照卫星图可以推断出拍摄者所在的方向,进而推断出拍摄的大致时间。
flag 获取成功。
查看点击助力按钮后的浏览器请求可以发现请求时有一个名为 IP 的参数,尝试修改这个参数发现会报错提示前后端检测 IP 不一致,那么考虑添加 X-Forwarded-For
头伪造经过代理的来源 IP 地址即可。
比赛平台的速率限制为每秒最多请求一次,所以在每次请求后还需要等待 1 秒。
for ((i=0; $i <= 255; i = ($i + 1))); do
curl "http://202.38.93.111:10888/invite/$invite_id" -H "X-Forwarded-For: $i.11.45.14" -d "ip=$i.11.45.14"
sleep 1
done
Wayback Machine 是个好东西啊。(页面存档)
LUG 官网上直接搜就出来了。但实际上的答案应该是 5 ,可能是官网没更新最新信息。
谷歌是你的好朋友。 Hackergame 2020 「猫咪问答++」 flag 。
可以看到正确答案为 Development Team of Library
。
可以在 the record of the proceedings of SIGBOVIK 2021(页面存档)的 212 页找到这篇论文。
搜索关键词:IETF Protocol Police
可以搜到这个「搞笑 RFC」:Establishing the Protocol Police ,在第 6 节中有相关介绍。
正确答案应为 /dev/null
。
最开始拿到题我先想的是能不能用负数凑,结果发现不行,于是考虑溢出。
试了试发现使用 6 斤瓜无法触发溢出,而使用 9 斤瓜的就可以触发溢出了。
写了个脚本跑一跑,试出来了几个负数,挨个试了下发现放 2e18 个 9 斤瓜可以凑到 20 斤。
然后在计算器里算了一下,只需要加 6 斤的瓜和 9 斤的瓜各 29782938247303441 个就能让称的显示变成 -1 。
接下来放 2 个 6 斤瓜和 1 个 9 斤瓜就能拿到 flag 了。
本题与 ANSI Escape Code 有关。
首先需要将文件中的 [
替换成 \033[
,然后再找一个支持显示 ANSI 控制码的终端输出。
然后发现一片空白,啥也没有。
捣鼓到快怀疑人生才发现终端上的某些字符被遮挡了,进而想到这个脚本可能清除了终端上某些地方的字符来显示 flag 。
先编写一个复读函数用来填满终端:
repeat() {
for ((i = 1; $i <= $1; i = ($i + 1))); do
echo -n "▉"
done
}
再配合上方替换好的文件输出即可,效果如图。
使用 putchar()
函数即可解决此问题。
#include <stdio.h>
int main() {
putchar('H');
putchar('e');
putchar('l');
putchar('l');
putchar('o');
putchar(',');
putchar(' ');
putchar('w');
putchar('o');
putchar('r');
putchar('l');
putchar('d');
putchar('!');
putchar('\n');
return 0;
}
可以使用 __schema
字段查询所有存在的类型:
{
__schema {
types {
name
}
}
}
发现一个名为 GUser
的类型,再构造一个语句查询类型结构:
{
__type(name: "GUser") {
name
fields {
name
type {
name
kind
ofType {
name
kind
}
}
}
}
}
顺便获取了下 GNote
类型的结构:
进行查询即可得到 flag :
今年拿的名次比去年的高,感觉在这一年里自己的 web 水平有很大的提升,但 math 还是一如既往地爆了零,和我的数学中考成绩一样的烂。
以后如果有时间的话逆向、汇编什么的也都要学一学,不然的话每次一看见 binary 就有点不知所措、无从下手属实不太好。
]]>