「说到邓丽君的代表作」
「很多粉丝会说〈何日君再来〉」
「〈再见我的爱人〉或〈我只在乎你〉」
「不瞒大家,其实我最喜欢这首」
- 老爸啊,到底想干什么呢?
- 干什么?
「请听邓丽君的〈别离的预感〉」
- 他自己的人生啊
- 我不知道,到最后还是搞不懂他
- 他很多事无法心想事成吧,都怪时代不好
- 他把自己的缺点都怪在时代上
- 你干嘛感慨良多
- 没有
- 你现在把那柱香当成你爸了吧,人走了之后,再思念都是枉然,还是得在人在的时候,好好对待才行。
- 我知道
- 为什么男人们都学不会珍惜当下,总是在追逐失去的东西,做着那些虚妄而无法实现的梦,把自己困住,每天怎么会快乐
- 也许吧
- 其实幸福这种东西,没有牺牲就无法入手
「比海还深,比天还蓝」
- 我到这把年纪了,还没有爱过谁比海还深的
- 别说这种孤僻话
- 你有吗?
- 我?
- 还算有吧
- 一般人不会有的啦,但还是每天过得开开心心。不对,就是没有才过得开心,平凡的生活也能自得其乐
- 真复杂
- 单纯得很,人生很单纯的。我刚讲了很棒的名言吧。借你写到下一本小说里。阿良,笔记一下啊
- 不用啦 - 不然会忘记的
- 不用抄啦,我记住了-- 『比海更深』
「死亡是一种解脱」,是炉石传说中 Lord Godfrey 高弗雷勋爵的登场语音,尽管炉石传说已经退出中国一年了,可是我时常会想起这句话。以及,还时常在微博上检索「离灯_冬眠mode关闭失败」这个关键词。
姥爷是三月份离开的。准确的时间是二〇二三年三月十七日。我是三月十日离开的深圳。
决定离开深圳,对自己而言并没有花太多时间。当时二月底离职,房屋面临到期,继续待下去,无疑要面对一边要找工作,一边要找房子的处境。姥爷从去年开始身体一直都不是很好, 二〇二二年底放开之后,又得了一次新冠,从那开始就一直卧病在床。
二〇一九年三月份来的深圳,在深圳待了有四年,下决定离开不过是一瞬间的事情。在网上购买几个大号的打包箱,胶带,泡沫。花一周的时间,把行李一点一点的寄回家。上一次这样做,还是二〇一六年大学毕业的时候。订了三月十日的机票,确定衣物啊,显示器啊,游戏机啊这些都寄走准备完毕,三月十日那天很早起床,收拾房间,背起背包,和室友在微信上说明原因并告别,然后一个人返乡。
回到家的第二天,我和妈妈回老家农村去照顾姥爷。那个时候,姥爷整日都只能躺在床上,床被放置在了客厅,上面盖了两层棉被,旁边开着电热风扇。三月份,农村依然很冷。那天我回到老家,姥爷咳嗽得厉害,我问姥爷是不是嘴里有痰,想吐出来,姥爷睁大眼睛看着我,然后摇摇头。姥爷以前就是这样,不太喜欢给我们小辈找麻烦,什么事情他都要亲力亲为。那天我看到姥爷的眼神,我很清楚,姥爷什么都明白,什么都知道,只是没有力气,声音沙哑说不上话,也坐不起来。我和妈妈从三月十日待到了三月十五日。每晚我妈妈都是睡在姥爷旁别小床上,我舅舅睡在里屋的一个小硬板床上,两个人轮班照顾。那个小床后来我也睡了一晚,整晚都能听到姥爷咳嗽的声音,半夜两三点的时候,我听到姥姥也起来了,姥姥心疼不下姥爷一直咳嗽难受的样子,半夜起来看望姥爷,问姥爷哪里难受,帮姥爷揉肚子。两位老人都八十多岁了,那一晚我躺在偏里屋的小床上,整晚都没睡着,心里很难过。
有时候,你会特别惧怕明天到来。
姥爷的葬礼持续了三天,农村,红白喜事都特别复杂。葬礼结束那天我开始发烧。发烧持续了四天才好,那几天我一直在睡觉,做各种各样光怪陆离,魑魅魍魉的梦。
2018 年我刚从德国回来的时候,计划着转行,也没有着急出去找工作,那会儿一整年都在家,看电影,打篮球,看书,写代码。
2023 年离开深圳再次回到家乡,熟悉的场景有些相似,又有些不同。只能说我对这种 gap year 的处境驾轻就熟,有着足够的经验去应对。之后的整个四月一直到五月中旬,自己的大部分时间都投入到了自己的个人项目中,偶尔时间出去打打篮球,偶尔放松的时候玩几局 splatoon,zelda: tears of the kingdom。
当你花上一周时间,解决了一些技术难题,把心中的想法实现出来的时候,不工作的焦虑感也会稍微消解,但这种消解总是暂时的,焦虑感却总会随着时间的推移越来越大。每晚都要熬到两三点钟才能睡着,在 live stream 中看几局 splatoon,有次还不小心听到某位 splatoon steamer 抱怨已经很久都没有去工作了。
七月初面试确定,然后决定动身前往上海。
在家里待了接近四个月,经历了「清明」,「五一」,「端午」,线上观看了六月份 Apple WWDC,去了一次北京。
宝可梦系列中,小智每次在前往下一个新的区域的时候,都只是带上皮卡丘,把其他的宝可梦留在家里。
决定动身前往上海,做一次超大规模的「断舍离」,只带上一些必要的生活用品,电脑,平板,不必要的物件都暂时安置在家中。这样的场景,像极了购买了一款游戏续作,主角还是前作的主角,没有改变,只是这一次需要前往崭新的区域去冒险,去面对此前没有见过的,新的敌人。
在上海确定好工作事宜,花上周末两天时间去医院做体检,找房子。网络上下单必备的生活用品,纸巾,洗衣液,衣撑,枕芯等等。一切安置妥当不需要花太久的时间。晚间时分在住的地方附近逛一逛,熟悉一下周边的环境和风景。我在 IM 软件中联系在上海的同学,告诉他们我从深圳换到上海这边工作了,有时间可以出来见一面,叙叙旧。
自己以前总是对未来有着明确的期许,有着可能划分到每一天的明确的计划。但我现在不这样,很多事情并不是事先计划好的,无论是当初去到深圳,离开深圳,还是再次去上海,我都没有明确的规划。只是刚好有什么东西在推着我,好吧,去那里看看吧,去那里也不差。
二〇二三年的下半年,七月到十二月,时间过得很快。自己又回到了之前那个,周一至周五,挤地铁上班,写代码,周末以及下班回家写写个人项目,或是玩一玩游戏的生活状态。和友人开玩笑,如果周末一直宅在家里,甚至都没有自己是生活在上海的感知,很多大体上的事情都和深圳其实差别不大,其他可感知的区别可能在于饮食,文化。上海这边每个月都有好多的展览可以逛。
今年自己经历了蛮多的事情的,姥爷的去世其实自己心里早有预期,但还是内心会有些恍惚。因为这不是在看电影,看电影的时候总会安慰自己,一个角色总会在某个时候展现人物弧光和升华。但现实不是拍电影演电影讲故事,现实就是现实,没有来自导演意志的因果叙事逻辑。心里的难过情绪,在姥爷葬礼被我自己压制着的。但这种压抑,会在以后生活中某个不经意的细节,因为某些不经意的片段,会让我突然想到过去一些回忆,然后就突然止不住的,怎么也停不下来的大哭。
- Wait… what happened?
- [sadly] He’s been forgotten. When there’s no one left in the living world who remembers you, you disappear from this world. We call it the Final Death.
- Where did he go?
- No one knows.
– Coco
我忽然理解,为什么侯孝贤导演安排聂隐娘哭泣的时候,需要用布遮住面部。
以下是按照惯例的盘点内容。
二月末三月初离职的那段时间,自己每天一边打包行李,一边把 The Last of Us™ Part II 通关了。这部作品在网络上有着很大的争议,但我总想,一部游戏,至少需要我亲自玩过,我才有资格去评价它。 The Last of Us Part I 主题是很普世价值的「爱」,讲 Joel 和 Ellie 在末世之中,从相互陌生到互相信任,这个叙事逻辑是安全的,玩家也很容易接受。Part II 的主题却是「恨」。它在叙事上走得太超前了,至少我在玩过以后,我认为它是目前迄今为止在玩过的所有游戏中,叙事最超前最先锋的。我始终认为,不管是听音乐的听众,还是看电影的观众,或是玩游戏的玩家,审美品味都是需要一点一点训练和培养的。几十年前的电影观众可能还只是陶醉在正反对立的二元叙事语境,然后电影叙事在不断进化,开始模糊,反讽,辩证,蒙太奇。观众在这个过程也是在不断地训练和品鉴。我认为作为第九艺术的游戏也是这样。The Last of Us Part II 遭受的巨大争议,专业媒体评论者和普通玩家的巨大分歧,是大多数玩家思考游戏叙事还停留在「勇者斗恶龙」的体系下。
The Last of Us Part II 的双线视角叙事,真的只有亲自玩过才能体会游戏和电影不同媒介叙事的巨大差异。电影始终是以第三者的角度去审视故事,而游戏需要玩家去交互,去操作,去亲自做出选择。强制完全控制带入一个陌生的,让很多人第一眼开始就讨厌的角色,是很困难的。我自己也是,所以我在第一次控制 Abby 的时候,故意让她摔下悬崖好几次。等到玩完整个 Abby 线的时候,我对 Abby 的看法已经改变了好多,这种体会,亲自游玩和在网络上看别人剪辑过的游玩过程会有很大区别,因为当你亲自游玩时,你会在很多地方卡关,你控制 Abby 角色会一遍遍的死亡重新来过,这些内容都会慢慢消解对于 Abby 角色的恨。而如果是在网络上看别人游玩,因为是剪辑过的内容,就没有这种感受了。
Persona 4 Golden 中能看出很多 P5 的影子,有几首音乐特别好听,另外就是「久慈川理世」这个角色设定太有魅力了。
五月份自己心心念念的 『ゼルダの伝説 ティアーズ オブ ザ キングダム』( 薩爾達傳說 王國之淚 )发售了。有两周的时间,自己除了吃饭就是在玩塞尔达。很多人评论这一作神庙的谜题难度降低了,但其实我想一个原因是这作的能力「通天术」和「倒转乾坤」相比上一部都强了不少,另外就是这一作把很多的谜题都设置在了大地图上。我现在还能记得自己不看攻略再一次成功抵达迷雾森林,推理出格鲁德小镇的壁画谜题。半夜两三点,自己躺在被窝中,摸着黑下到地底,朝着远处的一个光亮一点点探索,那种对整片海拉鲁大陆熟悉又陌生,每处地方都能激发出很强的探索欲,这样的感受是很难得的。
八月份的时候大家都开始讨论 Baldur’s Gate 3,都在讲它太惊艳了。我是等到十月份才开始玩这款游戏。CRPG 的游戏是有一定的门槛,有很多的专业术语,眼花缭乱的技能设定,专项,魔法,机制等等。好在游戏的引导设计足够好,了解这些内容并不会很吃力。目前我 Baldur’s Gate 3 玩了 109 个小时,进度还停留在一周目的第三章。我理解这游戏好玩的点在哪,但是因为剧情自由度太高了,总是不经意就错过好多内容。( 一周目因为第一章没有招威尔入队,导致丢失了后面所有的威尔相关剧情 )。
今年听到的好听的音乐依旧大多数来自于游戏原声( 因为今年依旧看很少的电影 )
Replay 2023 - Apple Music for Reyshawn
『天国大魔境』,动漫和漫画都强烈推荐,叙述性诡计,伏笔布局都相当精彩。
六月份的时候由于太过焦虑,花了一周的时间,就重新把整部『柯南』漫画重新看了一遍。我对这部作品有着很强的情感滤镜,因为小时候真的特别特别喜欢。现在随着阅历的加深,我也很清楚这部作品叙事还是人物塑造上存在的问题,但有时候关注一些相关的内容,会有一种逃离现实的安全感。
另外值得一提的是,终于搞定了 Netflix 订阅,之前付款总是失败,然后今年不清楚又试了一次竟然就付款成功了。目前在 Netflix 只是补了 『黑镜』。
又是没怎么系统性看完完整一本书的一年( 想到了 2021 年 )。去年闲暇时开始看『金瓶梅』,只是看了前二十个章节。又重新去读村上的『一九七三年弹子球』也是读到一半,没有读完。
技术类书籍也没怎么去读,不过倒是看了不少 third library 的源码,web 和 iOS 都有包括。
今年很多值得铭记的时刻,是自己花了很大精力解决了一些技术难题,包括但不限于
花了一个晚上的时间,按照论文上的步骤,一步一步在 iPad 手写演算进行推导。其中涉及到了一些点乘,叉乘的化简运算,叉乘运算又极其繁琐,加之论文其实省略了一些推导步骤的。不过好在自己最后算对了,算出结果那一刻真的好开心,又感叹发明 Quaternion 来进行 3d 旋转运算的人,真的好厉害!
我认为 Baldur‘s Gate 3 整个第二章都特别精彩,高潮部分在「拯救暗夜之歌」,演出相当震撼。
R.I.P.
可能是大多数人都不怎么认识和关注的一个人,但是看到消息还是感到很难过。更残酷的是,商业上并不会因为一个人的身故而停止发售她生前已经拍好的作品。
新的城市,新的工作,新的同事。对未来没有太多期许。每周二或周三工作日的晚上去打一场篮球,大汗淋漓之后喝上一杯冰镇的可乐。回到家里再冲上一个热水澡。生活相比以前,有了一些细微的改变。我不清楚,也不去想,不去期许五年十年后的事情,仅以这些文字,纪念即将过去的二〇二三。
🌻
]]>战争,全球范围下的疫情,刺杀,游行,第三任期。所有的恐惧是来自种种的不真实感,像是真人 show,或是被人编排好了剧本,充满戏剧性,夸张,难以言说的巧合,阳谋。顺着这样一条线索最终走向宇宙洪荒,或是世界末日。
我想起上高中的时候,忘记是高一还是高二,也忘记是临近暑假还是寒假,总之是临近假期的期末考试,自己在宿舍里头晕脑胀,量了体温以后发现烧到了 38 摄氏度,身边的室友都劝我下午不要去考试了。我想了想,不行,不能弃考。下午考试还是过去了,那是一场物理考试,那次的物理试题出的又比较偏,题目难度也很大。我不知道是不是因为我发烧的缘故,总之那次考试过程,自己的思路竟然异常清晰。最后的考试结果,我是那次物理考试中唯一一个上了 80 分的人,我自己都有点难以相信。
这是关于发烧,最神奇的一次经历。
The Last of Us™ Part I 是我今年玩到的游戏中,带给我无限感伤的游戏。游戏的总体流程不长,线性关卡,有些许的解谜要素,战斗部分在重制版中,加入了陀螺仪瞄准和 ps5 手柄的适配。游戏通关后,自己又去重新听了三年前 Hard Image 的两期播客:
冗长的感想主要集中在故事的讲法,性格如何塑造,以及对不可知做法的一些想象。
以前就经常和其他人讲,游戏之所以能够在艺术表达上高于电影等艺术媒介,因为游戏比电影多了一层「交互」的维度。然而真正能把「交互」这一维度用好的游戏叙事却很少,但恰恰 The Last of Us™ Part I 算是这么一部作品。其中有很多设计都很精巧,比如主角 Joel 被绳子吊起来,整个屏幕视角上下颠倒,然后要在这样一种状态下保护 Ellie。以及 Joel 和 Ellie 被突如其来的意外事件分开,经历险阻为 Ellie 的第一次开抢埋下伏笔。一些场景至今想起依然难以释怀,第一次陪着 Ellie 穿越酒店,在一间一间房间中经过时,看到浴室里的相对而坐的骷髅,它们临死的前一刻在想些什么呢。在后来的大学场景中,和 Ellie 一起走过学生宿舍,你能看到下午三点半的阳光,透过窗子打在了学生宿舍的书桌上,上下铺的床铺上积了很厚的一层灰尘,墙上还挂着当时流行的游戏海报,书柜中几本书,还有几盒游戏。后来到了一个临时的地下避难所,这里不久之前明明就有幸存者,周边的玩具,黑板也证明了有小孩子在这里生活。然后因为一次意外,里面的幸存的人都不见了踪影。
游戏手法的高明之处,就在于他把上述作者想要表达的内容,传递的情感,透过游戏场景,剧情设计,关卡设计给到玩家,最后由玩家亲自去经历,去体验这样的一个末日故事。
「异度之刃2」很早就买了,21 年元旦的时候沉迷过一段时间,后来因为别的事情不了了之,就搁置了。今年是因为「异度之刃3」要发售,就赶紧把「异度之刃2」拿出来,给通关了。七月份的时候通的关,那一天为了一口气看到最后的剧情,一直熬到了凌晨四点。
「异度之刃3」整体上游玩体验还是很不错的,游戏的整体机制,寻路系统,英雄任务等等,相比前作都是巨大的提升。可能唯一不太舒服的是后半段的节奏问题,在世界观展开以后就突然急转直下,反派也都太过脸谱化。导致最后的 boss 战也没有太多亮眼的地方。
Splatoon3 可以算上十月份以来,游玩时间最长的游戏。此前的自己唯一接触过的射击游戏就是 CS,那还是十几年前的的事情。Splatoon 作为一款 TPS ( 第三人称射击游戏 ) 真的很独特。从玩法,到设计,再到创意,都是独一无二。赞叹任天堂的想象力。
Elden Ring 我到最后也没有玩完,大概是玩到王城下水道那个地方,玩不下去了。此前从没有过任何魂游戏的经验,导致我刚开始玩 Elden Ring 时,甚至新手村都出不去。后来找了一个攻略,就想着按照攻略玩,但那样每一步都按照攻略走,就完全丧失了玩游戏的意义。到后来就索性不去玩了。也许以后会有某个契机,把剩余的内容玩完。
其中的很多首都是出自 The Last of Us™ Part I 游戏中的配乐。
今年并没有很认真地去标记电影了,主要是因为自己在写自己的 app,也就没有很大的动力再去打开 douban。看了很多动漫,有一些没有标记上。
技术类
非技术类
今年有很多神奇的时刻和神奇的故事,比如蛰伏一年,Golden State Warriors 终于又重新回到季后赛,过五关斩六将,杀到总决赛,在 6 月 17 日这天终于捧杯,Steph Curry 也终于拿下了 FMVP。
比如 Messi 的故事也终于圆满,成为了传奇。
后来发现,小智也在这一年拿到了属于他的冠军。
今年的很多时候,我会去看 @xiaolwl 生前最后一条微博下面的评论。我感觉那里的评论是真实的。
14 年炉石刚开始的时候,是主打 iPad 上的卡牌游戏,当时的手边刚好有一台 iPad mini2,就不知不觉上手了。但当时只是玩了一段时间之后就不玩了。一直到 2018 年,当时的自己阴差阳错,机缘巧合又再次打开炉石,一直玩到了现在。炉石可以算做陪伴我走过了很长一段低谷的游戏。
大概在一周以前,平安夜那晚。自己突然感觉身体不适,看了一下 Apple Watch,明明是静坐,心率却一直在 100 上下,摸了摸额头,额头也烫的厉害。心想,大概率是感染了。那一天晚上很痛苦,躺在床上翻来覆去睡不着,身体发烫,心脏也跳的很快,能清楚听到心脏每一次跳动的的声音,还伴随着心绞痛。想着时间快快过去吧,快点跳到早上,好出去买药。可时间却走的无比缓慢,从十二点到一点,从一点到两点,从两点到三点。最后挨到五点钟的时候,起床烧了一壶水,坐在椅子上,半闭着双眼,轻瞥到 Apple Watch 上的心率依然在 100 上下。然后我大概就是那样半躺着靠着椅背,一直挨到七八点钟,出门买药。
从小到大,有过几次发烧,但都没有这次来得这么凶猛猛烈。希望之后能慢慢恢复过来,希望爸爸妈妈,身边的亲人也都身体健康。
🌻
]]>6 月中旬的 E3 发布会,看到了久违的林克从高空纵身一跃的场景,那时的我心中想的都是,这个 2021 年再也待不下去了,好希望快点到 2022,这样就能玩到 Zelda 续作了。
但真的跟随时间来到这里,心里还是会有许多的感慨,不舍,恐惧。这几天,这个月发生了很多事情。晚上回去也是早早躺下。心想,
哎,又到了此刻。
以上列表按照喜好顺序排名。
七夕节的那天收到 PS5,第一时间便入手了 P5R,从 8 月份到 9 月中旬,把自己所有的空闲时间都投入了进去。现代背景,荣格心理学,东京涩谷。日式 rpg 的框架下,种种设定让代入感过于强烈,以至于某些时刻,我仿佛真的回到了学生时代。挤地铁,上课,考试,看书,和一帮小伙伴嬉笑打闹。
因为以前的学生时代,和现在真的有很大不同。经常一帮人一起玩某个游戏,一起打篮球。所以那天 p5r 一周目通关,想了一下,是时候约好友出来见一面聊聊天了。大家平日工作都很忙,虽然在同一个城市,但好久都没见面了。
『女神异闻录』攻略到「回忆」部分结束,剧情的反转设计,和游戏交互上给人带来的沉浸感简直太棒了。几处伏笔并没有意识到,剧情上带来的合理性也让我没有太多思考,只是感到奇怪。使用的手法完全想不出来,太绝妙了。太久没有过这种「啊,竟然是这样,被骗了」这种感觉了。
完全没有想过黑衣人会是「认识的人」。倒叙的故事结构,因为药物而部分记忆丢失,通过只言片语的提示,让玩家和主角一起绞尽脑汁去回忆究竟是哪里出了问题,这样的设计,沉浸感和代入感太棒了。
Metroidvania,一种游戏类型,中文翻译为「类银河战士恶魔城」,分别来自两款游戏,其一便是任天堂旗下的『密特罗德』,另一款是科樂美的『恶魔城』系列。之前玩过同类型的『奥日 1&2』,非常喜欢。对这一部也是期待了很久,也最终没有让我失望。20 多小时一周目通关,几个印象深刻的点是
自己通关会去看了相关的速通视频,才知道原来这系列一直有着「破序玩法」的传统。
因为玩过了 P5R,对于 ATLUS 这套神魔体系有所了解。比如「吉祥天」「爱丽丝」等等这些经典的形象。当本家新一作「真女神转生」发布的时候,很自然就第一时间入手了。初期的时候还是有很多不适应,直到我去买了 dlc,整个的游戏体验就好很多了。和外传不一样,本传的气质挺像『恶魔人Crybaby』的。大地图迷宫,战斗设计都非常精彩。唯一缺憾的可能还是 Switch 的机能。不时的卡顿,掉帧,和流畅,绚烂,不羁的 P5R 比起来,似乎还是差了一个时代。
Replay 2021 - Apple Music for Reyshawn
Joytastic Sarah 算是一个惊喜,在 YouTube 上有她的频道,她的一些翻唱和混音真的非常好听。
豆瓣记录里,今年总共看了 33 部电影。相比于去年的 13 部,提高了不少。有一些是周末的时候去到一个线下小众观影俱乐部看的。年初那会儿,有段时间每周末都过去。
大部分的观影记录集中在了上半年。现在回想起来,看剧的时刻真的非常美妙。花上一下午或一个晚上,binge-watching 的去看整部剧。六七点钟时叫一家附近的披萨和冷饮。
因为『鱿鱼游戏』,之后又去看了『弥留之国的爱丽丝』。
16 年那会儿开始看 EVA,那个时候毕业,放假在家,一边学习德语,一边看剧,看电影。那段时间把 EVA 所有的 TV 剧集,旧剧场版,新剧场版的前三部都看完了。我至今都清楚得记得我当时看新剧场版『Q』的时刻,自己一个人坐在大大客厅沙发上,面对红的一片的屏幕。绫波丽缓缓的走来,明日香开始骂碇真嗣。镜头拉远,三个人就这样慢慢在空无一人红色大地上缓慢走着。
即便是在等待了 5 年之后,看完了『新剧场版:终』。对于其他 EVA 粉丝,这个等待时间更久。在我心里,给我最大震撼和感动的还是来自『新剧场版:破』。在 16 年补完 EVA 的许多作品后,之后的很多时间,我都在循环听里面的音乐。很喜欢 EVA 的音乐。
年度最期待的电影,因为沉迷 p5,拖到了今天才看完。看到一个小时时发现云里雾里,暂停又去回顾了下前三部剧场版。我发现,这么多年,eva 给我留下的是很深刻的情绪,感动,好听的音乐。我发现,在看这部『终』时,我不太能想起tv动画,旧剧场版,新剧场版的种种故事细节。总之,ありがどう、皆さん。年度最期待的电影,因为沉迷 p5,拖到了今天才看完。看到一个小时时发现云里雾里,暂停又去回顾了下前三部剧场版。我发现,这么多年,eva 给我留下的是很深刻的情绪,感动,好听的音乐。我发现,在看这部『终』时,我不太能想起tv动画,旧剧场版,新剧场版的种种故事细节。总之,ありがどう、皆さん。
无论是技术类还是非技术类书籍,今年都没能很完整的去看完一本书。想了一下。非技术类书籍终究是被其他的一些娱乐方式占用或代替了。技术类书籍,则是被大部分看文档,看源码,看 YouTube 视频给代替了。从去年开始陆续看了很多 iOS 相关的技术内容。今年也算小有成效吧,在 iOS 项目的整体架构上有所认识和实践了。
3 月份的时候,关注到了「离灯_冬眠mode关闭失败」,她发了一篇微博,那篇微博的内容开头是
首先,对不起大家,刷到我这么一条让人不开心的微博,还要看我絮絮叨叨。
看了她写的内容,心里很难过。有可能是某些状态我是能和她感同身受来着。
年初大火的 clubhouse,费了好大一番功夫才搞来了邀请码。年初的时候听了一阵子。当再次下载下来听,就是六七月份了,那个时候中文社区已经变得大不一样。我像是以前听播客那样,有段时间里很高频度的收听它。尽管我清楚有 80% 的内容是像「水茧房」一样,但仍然有一些,可能是只有通过 clubhouse 的这种形式才能够被表达出来。现在想想,自己也不过是在某个时间点希望听些东西,这个媒介,无论是播客,还是 clubhouse,似乎都不重要。因为我听 clubhouse,也是大部分时间在听,很少去发言。
有关这些事情,端传媒最近的一篇文章梳理的更加全面。这么多如此密集的,大规模的事情,一桩接着一桩。也让 2021 年保持了一个很高的区分度。
每次在这样的时间点,大家都会说「明年会更好」啊,「新年新气象」啊,这些内容。然而这些年,无论是大的氛围的变化,还是周遭的我的个人感知。都让我认为,过去的那一年,才是更好的一年。哪怕现在站在这里,回想这一年发生的许多事情,似乎是称不上好。但真要我用力迈过去,心中依然相信的是,过去的那一年,才是更好的一年。
这一年告别了挺多人的。从年初到年尾。
按照惯例,一些期许,希望新的 Zelda 续作,希望『弥留之国的爱丽丝』第二季内容,希望『巫师』『Red Dead Redemption』『GTA V』能够推出 PS5 版本。
技术上还有很多未实现的愿景,需要一点一点提上日程了。
🌻
]]>“是的。他一直对将有什么消失这点耿耿于怀,其实何必那样呢?任何东西迟早都要消失。我们每个人都在移动当中生存,我们周围的东西都随着我们的移动而终究归于消失,这是我们所无法左右的。该消失的时候自然消失,不到消失的时候自然不消失。比如你将长大成人,再过两年,这身漂亮的连衣裙就要变得不合尺寸,对Talking Heads你也可能感到陈腐不堪,而且再也不想和我一起兜什么风了。这是没有办法的事情,只能随波逐流,想也无济于事。”
“可我会永远喜欢你的,这和时间没有关系,我想。”
– 「舞!舞!舞!」
在年初疫情肆虐的日子,每个人都出不了门的时候。我每天大量的时间,除了吃饭之外,就是坐在电视机前玩塞尔达。塞尔达这个游戏,在 17 年刚发售时候我就被惊艳到了,也是因为塞尔达认识到了女流的直播,甚至有过整晚不睡觉看塞尔达的录播视频的场景。所以在我真正玩到塞尔达的时候,我对它的剧情已经是比较了解的。但尽管这样,它依然让我异常沉迷。印象最深刻的是,生日当天终于打通「剑之试炼」,前后经历数个小时,打完后关掉电视,躺在床上休憩,内心仍然砰砰直跳心有余悸。
2021 年最期待塞尔达的续作。
Replay 2020 - Apple Music for Reyshawn
(上面的链接,国内用户需要挂代理才可以打开。)
非技术类书籍,今年只看了「舞!舞!舞!」,是在去往九江以及回来的列车上看的。偶然一天发现亚马逊上有了村上书的电子版,于是买了很多,但一直搁置着还没有去看。
技术类书籍看了一些:
闲暇时间看了许多 iOS 开发相关的书籍,也学了 swift, swiftUI, UIKit 这些内容。
看了极少的电影,比之上一年数量锐减。一个原因是空闲时间减少,还有今年空闲时间也花了大量时间去玩游戏。
今年一些值得纪念的时刻:
2020.06.23
凌晨两点半看到 Alan Dye 介绍新的 Big Sur 系统,是 2020 年难得 的特别感动的时刻。
背景音乐是 vox freak 版本的 Ur So F**kInG cOoL,节奏感很好,后来的那段视频被我反复播放。感动在于,这些呈现出来的视觉、听觉和交互,背后是严谨的 coding 和 design,也正是我一生所去追求的东西。
2020.11.12
今年最震撼的事情,New M1 chip has beaten all intel-based MacBooks.
https://browser.geekbench.com/v5/cpu/4652635
https://browser.geekbench.com/v5/cpu/4648107
M1 芯片的发布相当震撼。期待明年的 16寸 MBP。
2020.11.19
看 The Queen‘s Gambit S01E03 的最后一幕,Beth 上车后,握住养母的手,背景音乐 the End of World 响起,那个场景,节奏真的是被击中了!上次有这种感觉是看是枝裕和的「比海更深」。
这种「突然被戳中」感觉,看电影的时候会时有发生,我会很珍惜这样的时刻。
来讲一讲明年的期待吧:
然而事实是,我们只能往前走,往回看。哪怕在往回看的当口,时间也在往前走。2020 年倏地一下过去,我依然会在每周,每月,甚至是当下的每年,告诉自己未来仍旧有一些值得期待的美好事物。这些期待,在我面对无比凶险的明天的时候,让我还保有一些动力。
]]>从 2010 到 2019 的这个十年,自己个人经历了巨大的变化,往后不知道是否还会经历相似的十年,但在这个时间节点,我有必要通过某种形式,把我的回忆记录下来。
2017 - 2019
这句话是此前文章的一个标题。三年的时间,生活经历了巨大的反复。
2017.9.10,自己的文章标题「骑着白马入地狱,叼着纸烟进天堂」,那是我那一年德国生活的回顾,也是最后一次的 DSH 考试前夜。一个月后,我从德国回国,俄罗斯航空,莫斯科转机,到达雾霾的北京,刚好碰上十月国庆的人潮,一个人拖着两个箱子,和过去作别。
到达家乡火车站的时候,看到接我的父母,脸上满是焦虑不安。
在之后的很长的一段时间,我都处于一种很强的抑郁情绪中,当时的自己以为是抑郁,现在想来,应该是还没有达到病理的程度,只是,我当时有把这件事讲给好友听:
这样情绪就是,在高楼看到窗子,会感觉有种奇怪的魔力,有什么东西在窗子上吸引着你,希望你从那里跳下去。
处于一种强烈的抑郁情绪中,并且时刻有可能恶化,对周遭的事物开始麻木,并且和所有的人际关系脱节。所以这里很想提及在那段时间,短期或长期给我带来一些帮助的事情:
2018 年伊始,自己计划每天看一部电影,这样的事情持续了三个月,那一年一共看了 98 部电影。每天晚上 9 点钟,关掉所有的灯,一个人坐在沙发上,电影通过一台旧式的 Windows 笔记本连接电视机播放,观影的时间持续 2 到 3 个小时,结束后耐心写下「电影短评」。
2018 年入夏的时候,开始频繁的打篮球,家附近坐落一座体育馆,有不少篮球场,下午 5 点熙熙攘攘打球的人,自己时常抱着篮球,一个人去那里练习投篮,一个人在一个半场,捡球是一件特别费功夫的事情。
看电影,打篮球,coding,看书,甚至看 porn video,这些能够让我短暂的从现实抽离出来,获得一段时间的沉浸感,恢复学习的效率。但这样的沉浸感一旦消失,当每个夜晚降临,自己躺在床上看着天花板,闭着眼睛睡不着想着所有自己经历的事情的时候,发现自己面前依旧是一堵墙,过不去的墙,自己心中的问题并没有得到解答。以上的那些活动,并没有对我解答这个问题有太多帮助。
2019 年初,饶有兴趣的关注到一款 MC 评分很高的平台跳跃游戏「Celeste」,除夕购买,花了 7 天时间,在正月初七这一天,登上了「山顶」。那一天晚上,妈妈给我买了蛋糕,插了蜡烛,很多年没有吃蛋糕了。
后来的我,因为好友的一句话,去到了另一个城市,期间做了两次心理咨询,但对我的疑问并没有帮助。然后唯一让我突然走出来的,是和胡小姐的重新联系,以及听到的某期播客节目,持续不断的 coding,创造,产出。
世上无难事,只要肯放弃。
我开始逐渐认同这句话,也开始明白几年前自己巨大成本的「放弃」意味着什么。我脑中有许多有趣的想法,项目,2019 年的一段时间,自己完全进入一种 mind flow 状态时,每天会 coding 到很晚,完全进入一种 mind flow 的 development 模式,会忘记时间。到最后发现,自己完全有能力把自己的想法变为现实,也终于明白自己的热爱是什么。
我希望以后的工作,首先不能是纯粹的重复性劳动,最好是需要理科缜密的逻辑思维,还有文科艺术上的审美要求,要有创造性。
这是刚上大学的时候,当被问及自己将来想做什么工作时,给出的答案。几年的兜兜转转,这句话在我心中逐渐变得具象起来。
2012 - 2016
2016 年的端口,每个人都开始怀念过去的四年生活。我在临别的前一晚,和室友吃过晚饭,将近凌晨的时候开始写些文字,写到凌晨两点,第二天上午的火车,从此以后,再也没有回去过。
嗯,大学想要一些新的尝试,想要全方面的锻炼自己,不想再像过去那样只是呆板的读书了。
2012 年,从高中进入大学的我,是这样对自己规划的。于是我尝试许多和自己性格很不相符的事情,又好像在这个时期,自己的 mbti 并非 INTJ 人格。然后发现,无论是哪一方面,无论是感情,学习都经历了严重的挫败,以及自我定位和过去也出现了巨大反差。但此时的我还不至于达到后来的那种抑郁情绪。
2012 到 2013 年,和胡小姐的重新联系,一年时间的异地恋,几次冲动的旅行,都成了那段时间里无法磨灭的注脚。
我怀念的,是夜晚坐在体育馆门口喝啤酒时场景。以及后来几次的冲动旅行,她的突然造访,和我坐在火车过道的场景。那种冲动往后可能再也不会有了,但好幸运的是,我们都拥有彼此这样冲动的经历。
2015 年开始广泛收听 podcast,这里暂且列一下自己最喜爱的 podcast 列表:
从那时起,听播客逐渐占据了生活中很大的比重,开始接受播客作为「wet wares」的存在,透过声音了解到许多有意思的事情,有趣的人,自己内心的观念也发生了巨大的变化。那段时期的我:开始认真的使用中文,中文标点,在一段时间里甚至是使用繁体作为个人的主要输入法。更加认真的选择信息源,开始阅读 economics 和 new yorker 文章,大量的消化这些文字,音频内容,每几周使用 markdown 写一些文字。开始频繁的观影,并认真的进行观影记录。尽管从这一年开始乃至往后,自己大多数时间都开始独处。在无数个独处的白天夜晚,podcast 和电影一直在陪伴我。
2010 - 2012
更准确的这段时期,应该从 2009 年开始,但既然是十年回顾,就先略过高中的第一年吧。两年的时间过的飞快,生活也无比简单。在学习这件事上,可能是源于自己 INTJ 人格,从小到大其实父母催问的很少,现在想来,学习是一个很顺其自然的过程。
每个周末都要坐 BRT 穿越半个城市,回到空无一人的房子里,周末的傍晚,自己一个人边洗衣边看电视,这个片段在我脑海里不断被拾起。最开始学校附近还有一家音像店,回家的时候会过去买几张电影,家里没有网络,这些电影和电视是周末为数不多的娱乐活动。
现在还保存的三张电影光盘:
后来的第三年,周末很少回去,两天的时间就在宿舍度过。中午会去附近的「饺子店」吃盖浇饭,回来的时候在路边的报亭里买一本「看电影」杂志,也是这个时期开始频繁的购买「看电影」杂志,后来积攒了整整一箱,随着高中结束,那箱杂志也被遗忘在那间房子的一个角落。
尽管大部分都是学习生活,但仍旧有几个特别重要的时刻:足球赛上一记超远射门帮助我们进入决赛,决赛上点球大战输掉冠军。
2020 - 未知
生活就是这样,此前也没想到会以这样的形式完成这次跨年。熬夜留下的晕眩感还在,不管怎样,第二天的太阳照常升起。不知道此生能否见到火星移民计划的实施,量子计算机量产的实现,AI 在 fidelity 上进一步趋近人的意识;希望有生之年去见证这个糟糕的世界的变化。
]]>因此要想办法把 webpack 独立出来,让每一个项目都可以访问到 webpack,一个方法是全局安装 webpack。这里提供另一种方法,想到我们通常使用 webpack 的情景是,在 package.json
里预先定义好
"scripts": { "dev": "webpack-dev-server", "build": "webpack"}
然后 npm run dev
。我们需要把这里修改一下,让每个项目都统一调用同一个 webpack。
项目的结构是这样的:
.├── Project1│ ├── dist│ ├── package.json│ ├── src│ └── webpack.config.js├── Project2│ ├── dist│ ├── package-lock.json│ ├── package.json│ ├── src│ └── webpack.config.js├── Project3│ ├── dist│ ├── package-lock.json│ ├── package.json│ ├── src│ └── webpack.config.js├── node_modules // all webpack related node modules├── package-lock.json└── package.json
需要一个parent directory,在 parent directory 里首先 npn init -y
,并安装好 webpack 和所有相关 node modules。为每一个子项目单独创建一个文件夹,把每个项目下 package.json
中的 script
修改成:
"scripts": { "dev": "../node_modules/.bin/webpack-dev-server", "build": "../node_modules/.bin/webpack" }
这样一来,仅仅安装了一遍 webpack,每个子项目都可以 access 到 parent directory 里的 webpack。而且每个子项目都可以独立配置 webpack,配置文件为 webpack.config.js
。
GitHub 开源程序分析库 semantics,这里的一篇文章科普什么是程序分析 Program Analysis,以及它能用来做什么?
程序静态分析,program static analysis,意味着在不运行程序的情况下,我们可以知道:
https://css-tricks.com/the-developers-growth-model/
模仿 Grenier groth model, Dennis Hambeukers 提出他的「设计师成长模型」,分为五个阶段:
Elad Shechter 介绍了他的 CSS 文件结构。
Burke Holland 引入了 Bulma 来重构了它的网站,结果发现编译后 css 从原来 30kb 增加到了接近 300 kb。这里涉及到 Vue 中关于 css style 的 scoped 概念。因为 Bulma 被重复声明了十多次的缘故。解决办法就是 Bulma 首先要能够全局引入,Bulma 的变量要能够在各个 component 被调用。把 Bulma 的所有文件在 main.js
里导入即可,以及在 vue.config.js 增加 css.loadOptions.sass.data 的配置,让 components 能够使用预先定义的变量。
今天在美区 App Store 偶然看到这个。一家荷兰公司,和 New Yorker,The Economists 等报业集团合作,将内容打包统一放在他们这一个平台上,供人们选择阅读。
它的收费模式很有意思。文章按照单篇收费,大概20 - 40 美分不等,不满意可以退款。我下载下来适用了一下,免费的账户会有 0.45$。所有单篇文章价格小于这个数字的都可以打开,但更贵的文章就会提示 no enough credits。在一篇文章停留过长时间,超过 1min?就会判定为阅读,并从你的账户里扣除相应金额。但假如你不满意,可以 refund,被扣除的金额又会立即回来。
一个开源的,e2e 加密,去中心化的 message 项目
The not-so-talked-about but killer feature of Matrix is that you can bridge other services into it. I’m currently able to send and receive messages from Hangouts, iMessage, SMS, and Slack all from within Matrix. If I’m working on my laptop I can put my phone in my bag and not even touch it for 8 hours, because there’s no need. I have Riot running on my laptop with a full keyboard and access to all my communication platforms.
— comments from Hacker News
Which programming language is fastest? | Computer Language Benchmarks Game
Toy-program performance measurements for ~24 language implementations.
JavaScript 版本的 Juypter
CSS layout
jsPerf: JavaScript performance playground
比较不同 js 写法的性能
Flutter - Beautiful native apps in record time
Google 推出的跨平台 UI 框架
]]>// Evaluates with call-by-name strategy1 function callByName (a, b) {2 if (a === 1) {3 return 104 }5 return a + b6 }// Evaluates with call-by-value strategy1 function callByValue (a, b) {2 if (a === 1) {3 return 104 }5 return a + b6 }
两个函数在形式上没有什么区别,只是在运行时采取了不同的策略或态度,前者是 lazy,后者是 eager;
> callByName (1, 2 + 3)> a === 1> return 10> callByValue(1, 2 + 3)> callByValue(1, 5)> a === 1> return 10
使用 lazy evaluation,只用当真正需要读取这个变量或 expression 的时候,才会对其进行运算或 evaluate,也就是字面意义上的 call by need。
实现 lazy evaluation 有很多方法,但其核心概念则是 functional programming。即我们把所有的 variable 写成函数的形式,这样的函数通常被称为 thunk:
// Not lazyvar value = 1 + 1 // immediately evaluates to 2// Lazyvar lazyValue = () => 1 + 1 // Evaluates to 2 when lazyValue is *invoked*// Not lazyvar add = (x, y) => x + yvar result = add(1, 2) // Immediately evaluates to 3// Lazyvar addLazy = (x, y) => () => x + y;var result = addLazy(1, 2) // Returns a thunk which *when evaluated* results in 3.
理解了这一概念,就明白 codewars 上这道题目的用意了。
https://www.codewars.com/kata/foldr/javascript
题目很长,简单概括就是,我们需要实现一个 lazy evaluation 版本的 reduceRight()
函数。再把问题简化就是,如何实现上述所说的 call by need,举例来说,以 indexOf
函数为例:
const indexOf = y => function (x, z) { if (x === y) { return 0 } else { return z + 1 || -1 }};
indexOf
返回的是一个函数,比如 indexOf(1)
函数有两个参数 x 和 z,在 x 值为 1 的时候是 0,其他值时为 z+1。我们需要做的是对参数 z 实现 lazy evaluation,那么按照上述 functional programming 的概念,则应该是:
const indexOf = y => function (x, () => someFunction()) { if (x === y) { return 0 } else { return z() + 1 || -1 }};
这样,当 x 和 y 值相等时,函数直接返回值,z,也就是 someFunction 不会被调用,z 值实现了 lazy evaluation。很完美,不是吗?但问题是,indexOf
函数仅仅是用来测试的一个例子,对于函数内容是不可控也是未知的,我们无法亲自修改,把 return z + 1 || -1
改成 return z() + 1 || -1
。
所以,问题最终就变成了,如何将一个变量,比如 z
,在他需要使用,参与运算,被读取时才会 evaluate 它的值。答案是 Object.prototype.valueOf()
。
JavaScript calls the
valueOf
method to convert an object to a primitive value. You rarely need to invoke thevalueOf
method yourself; JavaScript automatically invokes it when encountering an object where a primitive value is expected.
使用 valueOf()
,可以
> a = {}{}> a.valueOf = () => 3[Function]> a { valueOf: [Function] }> a + 14> a.valueOf = () => true[Function]> !a false> a && false false>
定义了 valueOf
方法后,Object a 可以像普通变量,更确切是 primitive value 那样进行运算,而且就如 lazy evaluation 那样,只有它被使用时,valueOf
函数才会被调用。因此,我们只需要在调用 indexOf
函数时这样调用:
indexOf(1)(1, {valueOf: () => someFunction()})
即可。依照这样的思路,这道 codewars 问题也就迎刃而解。
参考链接:
]]>let i = 0(function test () { console.log('hello')})()
这里会提示报错:
TypeError: 0 is not a function
观察了一会儿,才发现,JavaScript 引擎一定是把第一行和第三行看成一行代码,按道理,第一行末尾应该是要自动加一个分号的。这里如果我们手动加上分号,程序就不会报错了。
let i = 0;(function test () { console.log('hello')})()
关于 JavaScript 的 Automatic Semicolon Insertion,规则是这样的:
}
开头;return
所在行末尾会加分号;break
所在行末尾会加分号;throw
所在行末尾会加分号;continue
所在行末尾会加分号;上面这个例子,就是如果不在第一行加分号,则下一行以 (
开头,则会被当作函数调用。相似的情况还有:
const hey = 'hey'const you = 'hey'const heyYou = hey + ' ' + you['h', 'e', 'y'].forEach((letter) => console.log(letter))// Uncaught TypeError: Cannot read property 'forEach' of undefined
以及关于 return
(() => { return { color: 'white' }})()// Instead, it’s undefined, because JavaScript inserts a semicolon after return.
以上都是由于不写分号,完全依赖 ASI ( Automatic Semicolon Insertion ) 可能造成的错误。
Dr. Axel Rauschmayer 在 2011 年就写了一篇 blog 来阐述这个问题,以及他对于分号的态度:
- Always add semicolons and avoid the headaches of semicolon insertion, at least for your own code. Yes, you will have to type more. But for me, semicolons increase the readability of code, because I’m so used to them.
- Don’t put postfix
++
(or postfix--
) and its operand in separate lines.- If the following statements have an argument, don’t put it in a separate line:
return
,throw
,break
,continue
.- For consistency (with
return
), if an opening brace or bracket is part of a statement, don’t put it in a separate line.
参考文章:
]]>在 UI 设计中如何更好的使用 motifs (小的视觉元素),既不喧宾夺主,又能体现 UI 设计的 consistency 一致性。在 foundationmedicine.com 网站看到它们将 hexagon 作为它们 UI 设计的 motif,这分别体现在:
同样的视觉设计,分析也可用在 The Intercept ,它们选取 command line 的下划线光标作为它们的 motif,在 Logo,字体,分割线等都有体现。同样在 CSS Tricks,则是把「橙黄渐变」作为一种 motif。
关于 JavaScript 中 .sort()
方法的使用。其中提到的几点关键:
.filter
, .map
, and .reduce
will return a new array and leave the original untouched, .sort
will sort the array in place....
;reduce
方法。
- 终身学习英文。不是以通过什么考试、和外国人深入交流、或是融入所在的英文社会为目标,而是以用英文思考和不觉得中国人以英文思考有任何奇怪为目标;
- 关掉一直开着的电视,或,放多一两个屏幕在电视旁边。平板、笔记本电脑都可以。全部一起开着看视频。自动播放要开,让它们都像电视台一样一直播下去。看什么都可以,但每个屏幕要不一样;
- 认真听音乐,在不幹别的的情况下。多人一起更佳;
- …
- 意识到「所有人都是残疾人」这个事实;
- 一定要屌犬儒者的老母。
苹果在随即到来的 Catalina,不再预装 python, ruby, perl 这些脚本语言了。Python 2.7 也不会再支持了。挺好的,一直在用 python 3,python 2 快快淘汰。
走向怎样的未来,很大程度上取决于如何面对自己的过去,人和国家都一样。 — DM.
那是,毕竟吃药的第一步是承认自己有病。 — 妖術
这篇文章举了一个例子来阐释二者的差别。Tesla(INTJ)和 Einstein(INTP),Tesla 研究新的技术,并思考这些技术能否得到应用,是否会有新发明?Einstein 更加注重背后的数学原理。在某豆瓣小组的介绍里,对 INTJ 和 INTP 的解释更加清晰。简单来说,二者都拥有「内向,思考」的性格特征,INTJ 更加灵活,富有创造力,追求效率。INTP 极乐于追求事物因何如此,并试图探寻隐藏一切事物之后的运行逻辑。
看了上面的解释,认为自己可能属于 INTP 多一些。
自从生成 css grid 代码。
一个教你如何打结的网站,每种方法都配有动画。
Compare package download counts over time
用于对比 npm package 的下载量。
Pod Hunters: all of the cool podcasts that we recommend
the verge 专栏 pod hunters,推荐好听的播客节目。
]]>有两种认证方式,一种是基于 cookie 的认证,另一种是基于 token 的认证。后者实现往往是通过 JSON Web Token (以下简称 JWT)实现的。首先要说明一下两种认证方式的区别:
cookie-based authentication 的认证流程是:
token-based authentication:
为什么会说 token-based 更好:
一条完整的 JWT 格式是这样的:header.payload.signature
。
第一步,创建一个 JSON 格式的 header。header 里包含的信息需要有这个 JSON 使用的 hash 算法,例如:
const header = { typ: "JWT", alg: "HS256"}
"typ"
声明这是一个 JWT,"alg"
声明所有是用的 hash 算法;
第二步,创建 payload。这里的 payload 就是你想在 JWT 里存储的任何数据信息,但最好不要把敏感信息,比如密码放在里面,
const payload = { userId: "b08f86af-35da-48f2-8fab-cef3904660bd"}
对于 JWT 的 payload,会有一些标准,比如 iss
代表 issuer,sub
代表 subject,exp
代表 expiration time。
第三步,创建 signature 签名。
const encodedHeader = Buffer.from(JSON.stringify(header)).toString('base64')const encodedPayload = Buffer.from(JSON.stringify(payload)).toString('base64')
将 header 和 payload 都使用 base64 编码。
const crypto = require('crypto')const jwtSecret = 'secretKey'const signature = crypto.createHmac('sha256', jwtSecret).update(encodedHeader + '.' + encodedPayload).digest('base64')
base64 编码后的文本使用 .
连接,再进行 hash,hash 后的文本再进行 base64 编码。
最终 JWT 为:
const jwt = `${encodedHeader}.${encodedPayload}.${signature}`
The very important thing to note here is that this token is signed by the HMACSHA256 algorithm, and the header and payload are Base64URL encoded, it is not encrypted. If I go to jwt.io, paste this token and select the HMACSHA256 algorithm, I could decode the token and read its contents. Therefore, it should go without saying that sensitive data, such as passwords, should never be stored in the payload.
一定要区分认证和加密,JWT 不会加密混淆数据。当用户成功登录,服务器端按照上述过程生成一条 JWT 返回给了客户端。因为 JWT 涉及到了身份认证,还是很敏感的,客户端把这个 JWT 存储在 HttpOnly Cookie
,不同于传统 cookie,标有 HttpOnly
的 cookie 只能由 Server 端获取。
登录成功后,当需要请求某个需要权限的 api 或是进入某个 route 时,client 端在发送 request 请求就会把这个 JWT 稍带着,通常是在 Authorization
里,以 Bearer 作为开头:
Authorization: Bearer <token>
服务器收到请求后,首先需要验证这个 token。验证 JWT 包含以下几个步骤:
在验证 signature 时,具体是先使用 base64 decode 整个 JWT,获得 header 和 payload 的内容。在 header 里能找到 JWT 使用的 hash 算法。使用该 hash 算法和本来就在服务器端存储的 secret key ,重复一遍上面的流程,比较结果和 JWT 中的 signature 是否匹配。
The API needs to check if the algorithm, as specified by the JWT header (property
alg
), matches the one expected by the API. If not, the token is considered invalid and the request must be rejected.To check if the signature matches the API’s expectations, you have to decode the JWT and retrieve the
alg
property of the JWT header.Remember that the signature is created using the header and the payload of the JWT, a secret and the hashing algorithm being used (as specified in the header: HMAC, SHA256 or RSA). The way to verify it, depends on the hashing algorithm:
Node.js API Authentication With JWT
这个视频 step-by-step 讲解了如何在 express 里使用 jsonwebtoken 这个 package,以下是最终完整代码:
const express = require('express');const jwt = require('jsonwebtoken');const app = express();app.get('/api', (req, res) => { res.json({ messgae: 'welcome to the api' })});app.post('/api/posts', verifyToken, (req, res) => { jwt.verify(req.token, 'secretkey', (err, authData) => { if (err) { res.sendStatus(403); } else { res.json({ message: 'post created ...', authData }) } }) })app.post('/api/login', (req, res) => { // Mock user const user = { id: 1, usernmae: 'brad', email: 'brad@gamil.com' } jwt.sign({user}, 'secretkey', (err, token) => { res.json({ token }) });});// Format of Token// Authorization: Bearer <access_token>// Verify Tokenfunction verifyToken(req, res, next) { // Get auth header value; const bearerHeader = req.headers['authorization']; // Check if bearer is undefined if (typeof bearerHeader !== 'undefined') { // Split at the space const bearer = bearerHeader.split(' '); // Get token from array const bearerToken = bearer[1]; // Set the token req.token = bearerToken; // Next middleware next(); } else { // Forbidden res.sendStatus(403); }}app.listen(5000, () => console.log('server started on 5000'))
通过 jwt.sign()
进行签名。
函数 verifyToken
仅仅是作为一个 middleware 去 retrieve header 里的 token,并把它保存在 req.token
里,具体的认证是通过 jwt.verify()
实现的。
前者是对称加密,只有一个 key 值。后者 RS256 是非对称加密,有一个 private key 和一个 public key。上文仅仅提到了 HS256 的认证过程,但如果使用非对称加密(也更推荐这种方式)来生成 JWT,认证时需要用到 JSON Web Key Set。
For
RS256
, the tenant’s JSON Web Key Set (JWKS) is used. Your tenant’s JWKS ishttps://YOUR_DOMAIN/.well-known/jwks.json
.
通过 private key 来生成 JWT,再通过 public key 对 JWT 进行验证。在使用 RS256 非对称加密时,我们可以想象有两个 Server 端,一个是 Authentication Server,进行认证,并使用 private key 产生 JWT。另一个是 Application Server,获得来自 Authentication Server 的 public key,可以对经过Authentication Server 产生的 JWT 进行验证。
参考:
]]>具体的安装教程也可以参考这个 YouTube 视频。
下面按照给我带来惊喜的顺序,来简单谈一下这次的 iPadOS / iOS13。
永远访问桌面网页,这对于使用 iPad 而言是巨大的进步。在电脑端,无论是 Safari 或是 Chrome,都已经成为了无比强大的通用客户端,general client,在浏览器里几乎可以做任何事情。而在 iPhone 上,苹果对于第三方浏览器有很多限制,因此,原生 Safari 的增强就显得无比重要。这让 iPad 变得更像是 laptop 了。不仅如此:
bilibili 的 iPad app 极为难用。而在以前在 iPad 上使用 Safari 浏览 bilibili,总会自动跳到移动端页面,而且可恶的是移动端仅支持 240P 的分辨率。哪怕你在 share sheet 里去 request desktop page 也不行。而在新的 iOS13 里,使用 Safari 进行浏览就和电脑端一模一样,体验非常友好。
理论上,现在的 iPadOS 可以开无数个 Safari 窗口,但由于内存限制,只能同时运行 2-3 个 Safari,多余的就会被后台 kill 掉。目前,只有 1T 容量的 iPad Pro 搭配有 6G RAM 内存,其余 iPad Pro 是只有 4G RAM。
可以预见的是,未来 iPad 产品也将逐渐加入更大的内存,以此实现后台的多任务运行。
Safari 终于有了原生的下载功能。可以在 Settings 设置里选择下载保存的文件夹,可以保存在本地 On My iPad,或者是 iCloud 里。这对于日常使用,通过 http 或 ftp 下载一些文件资源已经足够了。但如果想要使用「磁力链」或 bitTorrent,在目前或可以想象的未来的 iOS 系统中,都不太可能。
手势交互一直是 iPad 有别于 Mac 和 iPhone 的一个最重要特征。
两种截图方式:
第一种是一直以来很传统的截图方式。第二种很有趣,很自然,截图之后直接进入编辑页面。
在以前,要想选中某段文字,需要在想要选中的位置上 touch 两下才会出现光标。在新的 iOS 13 中,苹果把这个操作进一步简化,更加接近在桌面端的交互逻辑。而以前的双击,是选中一个词,三击是选中整个句子。
我们在电脑端编辑文字,使用的很多的快捷键便是 cmd+c/cmd+v。现在在 iPad,复制是三指 pinch (捏合),粘贴是三指 spread。类似的手势在 mac 上很常用。需要指出的是,这里手势操作的复制粘贴并不仅仅限于文字,它可以用于所有可以进行复制粘贴的地方,比如文件。
通常如果使用键盘,cmd+c/cmd+v 是更好的方式。但如果在 files 里整理文件,首先多选,再利用手势进行操作,就非常方便了。
是的,等了很多年了,iPad 终于可以外接闪存了。这里为什么强调是闪存,因为我在发布会上只听到了说 flash drive。而在我自己的实验里,exFAT 格式的 flash drive 可以准确读取,嗯,这点和 mac 是一致的。因为一定会有文件格式的壁垒在。但我使用我的 APFS 格式的外接机械硬盘,读取失败,甚至出现了一个 bug。这里要说明一下,我的这个机械硬盘被分成了两个区,一个用作 time machine,另一个是正常存储。但连接 iPad 后,似乎识别成了 image 什么的,而且出现的外接存储图标再也消除不掉了。
不清楚各位有没有连接 APFS 格式机械硬盘成功的。
![ipad files bug](/images/ipad files bug.png)
苹果在每一代系统里,对 UI 设计都有一些细微的变化。最明显的是顶部选项卡
![iOS13 tab 1](/images/iOS13 tab 1.png)
![iOS13 tab 2](/images/iOS13 tab 2.png)
![iOS13 tab 3](/images/iOS13 tab 3.png)
最后,从 WWDC19 结束后,就很多人在讨论 iPad 上的鼠标。我也看了别人在 iPad 上使用鼠标的演示视频。我能想到比较好的使用方式是进行 FPS 枪战游戏。但目前来看,iPad 上的鼠标还仅仅是模拟手指操作,而不是类似 Apple Pencil 那种更精细的指针,而且不支持滚轮滚动,所以整个下来体验并不好。我个人是很多年都不在用鼠标了,电脑上一直用触控板,体验很好。我也一直认为,iPad 上最好的精确输入方式是 Apple Pencil。而鼠标的作用,想一想,定位可能是和现在已经支持的 playstation controller 或 Xbox controller 那样吧。
]]>在 hacker news 看到这样一则评论:对于 Fermi paradox 费米悖论的一个解释,死去的有机物通过数百万年,在这个星球上形成了丰富的化石燃料。因为这些石油化石资源的丰富,我们才能进入工业时代,才能进入电气时代。但有人反驳,即使没有这些化石,但只要存在了生命,就会有能量源,不管那是什么,就能帮助机器运转。
作者认为专业化, 10000 小时理论,更倾向于在一个 kind or simple 的环境里发生。而事实大多数人所面临的环境都是极其复杂的, wicked 。重申了行动要优先于思考,并引用了 Herminia Ibarra 的话「We learn who we are in practice, not in theory」。最后,他认为让我们避免陷入自我的 cognitive biases 认知偏见的,是「science curiosity」。
T. Greer 用一整条 thread 来阐述,为什么旧的 blog 时代是会比现在的 twitter,reddit,tumbler 更好:
李如一看到这条 thread 后,停止更新了他在 telegram 平台的「一分世界」,认为要知行合一。
Horowitz 1965 年在纽约卡内基音乐厅舞台演出时,在第二乐章结尾处出了些小瑕疵,后来提供给唱片公司的是编辑过的版本,把瑕疵剪掉了。直到 30 年后,才把 unedited 版本放了出来。
]]>Apple Music 上,在这里可以听到音乐会后出版的唱片里的版本(修过的,从七分二秒开始),这里可以听到日后出版的未修版本(即演出现场版本,勋伯格说的严重错误在七分十一秒)。
JavaScript 是一种 single thread 的语言。既然单一线程,那么在某个时间点,只能完成一项任务。于是
$.get('http://twitter.com')$.get('http://youtube.com')$.get('http://google.com')
在单线程下,如果某一行执行所需要的时间太久了,那么余下的的 command 也无法执行,程序就在那一行停滞下来了。我们把这个叫做 blocking。然而事实上,当我们在使用 setTimeout()
函数时,程序似乎不会出现 blocking。
console.log('hi')setTimeout(() => console.log('there'), 0)console.log('Welcome!')// output:// hi// Welcome// there
这里哪怕是设置 delay 为 0s, setTimeout()
里的函数也是在最后才执行,是怎么回事呢?
在 JavaScript 的执行环境中,所有的需要运行的函数是单线程的,按照次序会出现在 call stack 里。而通常,JavaScript 要么在 Browser 里运行,要么在 Node 环境运行,在 Browser 里运行时,会有一整套来自 Browser 提供的 web API,同理在 node 环境里也有相应的 API。 setTimeout()
函数就来自这些提供的 API 中。当我们 declare and call 一个 setTimeout()
函数时,Browser 会生成一个 timer 计时器,计时器的时间达到时, setTimeout()
里定义的 callback 函数会进入到一个叫 task queue
的容器中,此时程序会去检测 call stack 是否为空,当 call stack 为空时,会将 task queue 中最上层的函数移入 call stack 中进行执行。因此本质上来讲,由于有 web API 的加持,最终类似 setTimeout()
函数还是使用了多线程。只不过对于 JavaScript 来讲,一直是执行的是 call stack 里的内容,可以认为一直是单线程操作。
Promise 和 setTimeout()
的区别是,setTimeout()
是 delay 一个确定的时间,比如 3000ms,5000ms,然后执行 callback 函数。Promise 本身执行 callback 的时间是不确定的,只有 resolve 之后才算执行完毕,因为 resolve 后会改变 state,比如从 pending 改变成 fulfilled,通过 .then()
的方法执行下一个任务。
Timeouts and Promises serve different purposes.
setTimeout delays the execution of the code block by a specific time duration. Promises are an interface to allow async execution of code.
A promise allows code to continue executing while you wait for another action to complete. Usually this is a network call. So anything in your
then()
call will be executed once the network call (or whatever the promise is waiting for) is completed. The time difference between the start of the promise and the resolution of the promise entirely depends on what the promise is executing, and can change with every execution.The reason the promise is executing before your timeout is that the promise isn’t actually waiting for anything so it resolved right away.
以下是来自 Implementing,对于 Promise object 的实现:
最好的方式是将下面的代码粘贴进 editor 里,调试几遍。不太明白的地方打上断点,或是 console.log()
。
let PENDING = 0;let FULFILLED = 1;let REJECTED = 2;function Promise(fn) { // store state which can be PENDING, FULFILLED or REJECTED let state = PENDING; // store value or error once FULFILLED or REJECTED let value = null; // store sucess & failure handlers attached by calling .then or .done let handlers = []; function fulfill(result) { state = FULFILLED; value = result; handlers.forEach(handle); handlers = null; } function reject(error) { state = REJECTED; value = error; handlers.forEach(handle); handlers = null; } function resolve(result) { try { let then = getThen(result); if (then) { doResolve(then.bind(result), resolve, reject); return ; } fulfill(result) } catch (e) { reject(e); } } function handle(handler) { if (state === PENDING) { handlers.push(handler); } else { if (state === FULFILLED && typeof handler.onFulfilled === 'function') { handler.onFulfilled(value); } if (state === REJECTED && typeof handler.onRejected === 'function') { handler.onRejected(value); } } } this.done = function (onFulfilled, onRejected) { // ensure we are always asynchronous setTimeout(function () { console.log('instantly implemented ⚠️') handle({ onFulfilled: onFulfilled, onRejected: onRejected }); }, 0); } doResolve(fn, resolve, reject) this.then = function (onFulfilled, onRejected) { var self = this; return new Promise(function (resolve, reject) { return self.done(function (result) { if (typeof onFulfilled === 'function') { try { return resolve(onFulfilled(result)); } catch (ex) { return reject(ex); } } else { return resolve(result); } }, function (error) { if (typeof onRejected === 'function') { try { return resolve(onRejected(error)); } catch (ex) { return reject(ex); } } else { return reject(error); } }); }); }}function getThen(value) { var t = typeof value; if (value && (t === 'object' || t === 'function')) { var then = value.then; if (typeof then === 'function') { return then; } } return null;}function doResolve(fn, onFulfilled, onRejected) { var done = false; try { fn(function (value) { if (done) return done = true onFulfilled(value) }, function (reason) { if (done) return done = true onRejected(reason) }) } catch (ex) { if (done) return done = true onRejected(ex) }}var promise1 = new Promise(function(resolve, reject) { setTimeout(function() { resolve('foo'); }, 3000);});promise1.then(function(value) { console.log(value) // expected output: "foo"});console.log(promise1);
一些帮助理解这段代码的小 tips:
doResolve()
,第一,会直接执行 fn
函数,也就是声明 Promise 时传递的 callback 函数。第二, 保证 resolve
或是 reject
function 仅执行一次。doResolve()
函数;.then()
返回一个新的 Promise object,这个 Promise 会执行 doResolve()
函数,从而会直接执行这个新 Promise 的 callback 函数;line 73 - line 93,执行 callback 函数返回的是 self.done()
函数的执行结果;.done()
方法会 check state 的值,确定是否将 resolve
添加进 handlers
中;resolve
函数执行完毕后,将 state
从 PENDING
改为了 FULFILLED
,同时 handlers.forEach(handle)
依次执行 .then
方法中添加进去的函数。resolve
和 reject
都会将值赋给 value
变量;setTimeout(cb, 0)
使用了上述提到的 event loop,此时 cb
会在几乎 0ms 的间隔时间后,进入 task queue。参考链接:
]]>奇情电影的背后往往蕴含着对时局动荡、社会不稳定的一种本能性的恐惧。要么直接诉苦,要么转移视线。
『还愿』同样是具有「奇情片」内核的恐怖游戏。
以下内容涉及剧透。
『还愿』的故事背景设定在 80 年代的台湾。故事的叙述在 1980 年,1985 年,1986 年三个时间点互相穿梭,交叉叙事。80 年代的台湾究竟发生了什么?1979 年末发生了轰动一时的「美丽岛事件」。
以美麗島雜誌社成員為核心的黨外運動人士,於12月10日組織群眾進行遊行及演講,訴求民主與自由,終結黨禁和戒嚴。
…
此事件對台灣往後的政局發展有著重要影響,台灣民眾於美麗島事件後開始關心台灣政治。之後又陸續發生林宅血案(1980)、陳文成命案(1981)、劉江南命案(1984)撼動國際社會,使國民黨政府不斷遭受國際輿論的壓力以及黨外勢力的挑戰,之後國民黨漸漸不再稱呼黨外人士為野心陰謀份子,並逐漸放棄一黨專政的路線以應時勢,乃至於解除持續38年的戒嚴、開放黨禁、報禁。伴隨著國民黨政府的路線轉向,台灣主體意識日益確立,在教育、文化、社會意識等方面都有重大轉變。
–wikipedia 美麗島事件
「美丽岛军事大审」的同时,1980 年 2 月 28 日,发生了震惊全台湾的「林宅血案」。2017 年上映的电影『血观音』,其中的一些情节就是取材于「林宅血案」,也获得了 54 届金马最佳影片。回到游戏里,1980 年 8 月,在这个时间杜家一家人搬入游戏里的这座「凶宅」。
最开始看到『还愿』预告的时候,就联想到了电影『血观音』。因为都有着很多相似的元素,比如「凶杀」、「观音」、「巫术」等等。但实际游玩(看过 游戏直播)之后,其实差别还是很大的。『血观音』里有较多的政治意味,『还愿』是在各种恐怖,宗教,民俗包裹下,还是只是在讲一个家庭的奇情故事,格局更小一些。
1980 年到 1985 年、1986 年这些年,台湾经历了什么?因为 1979 年台湾和美国的断交,和「美丽岛事件」的影响,台湾在政治上逐渐变得民主和开放,经济上同样也是蓬勃发展。民众有了更多的娱乐活动,看电视,虽然当时全台湾只有三个台,大家能看的东西很少,所以港剧『楚留香』引进一下子就能在台湾创下了 70% 的收视率纪录。『五灯奖』是台视制作的一档综艺选秀节目,播出时间长达 33 年,也是游戏里「七彩星舞台」的原型。除了看电视之外,赌博之风也是尤为盛行,比如所谓的「大家乐」,就是 80 年代台湾很流行的一种非法赌博方式。
台湾的娱乐工业 ,在 20 世纪 80 年代开始有了个爆发式的成长 。可以说 ,大家有了钱 ,就开始爱听靡靡之音 。
电视台三台都有许多的综艺节目出现 ,尤其是星期六及星期天晚上 ,是综艺节目的主战场 。当时的许多主持人或跑龙套的 ,现在都已经是综艺界的大佬 。不过当时的综艺节目 ,在一阵欢乐过后 ,主持人及来宾们一定都会合唱几首 “净化歌曲 ” ——不外就是那些 “观念正确 、意识健康 ” ,鼓吹乐观向上人生观的歌曲 ,就是怕大家听太多靡靡之音导致风气败坏 。于是每当节目结束前 ,我们就会看到所有参与这次节目的艺人排排站 ,在各无线电视台大乐队的伴奏下 ,一起双手打拍子 ,随着伴奏摇摆 ,一起高声齐唱 “净化歌曲 ” 。间奏时 ,主持人还会顺便感谢一下所有来宾 ,以及由 × × ×指挥所领导的 ×视大乐队 。
–『我们台湾这些年』
也是受赌博风气的影响,出现了「求明牌」。怀着能从赌博里中奖和大捞一笔的心态,很多人希望从一些无关的自然现象,或是求神拜佛来得到神明的暗示。「迷信」在当时成为了很普遍的现象,其实即便是现在的中国农村,逢年过节,丧葬嫁娶,其中的许多繁复的形式和说法,在外人看来,也不过就是迷信罢了。但也要承认,「迷信」是民俗里重要的组成部分,所有的「迷信」也都不是突然出现的,它都和当时的社会背景,政治环境,经济环境,民众心态氛围息息相关。游戏里有很多的台湾 80 年代符号,旧电视机,选秀节目,红龙鱼,观音像,麻将。赤烛在一个封闭空间里,透过这些符号,希望还原出一个 80 年代台湾的真实社会状态。
民眾篤信大家樂中獎號碼會出現於各種超自然現象中,稱為「明牌」。一時間,「求明牌」之風吹遍全臺灣。民眾紛紛湧入大小廟宇、道觀、陰廟、墳墓,向神佛、鬼魂求明牌,甚至膜拜各種物體如樹木、石頭等,希望這些物體上的精怪可以有神示。
–wikipedia 大家樂 (賭博)
就是在这样一个伴随着动荡,民主,开放的社会氛围下,小杜美心在这新家里度过了他的 5 岁到 11 岁的时光,也就是从 1980 年到 1987 年。
父亲杜丰于,是一位小有成就的编剧,拿过宝岛文艺奖最佳编剧。母亲巩莉芳本是一位电影明星,在嫁给杜丰于之后,决定息影,在家做全职太太,操持家务。夫妻俩在 1975 年生下杜美心。在此之后,父亲杜丰于的编剧事业却不那么一帆风顺。80 年代的台湾正在逐渐变得开放,因此少有人再去愿意用杜丰于那样传统的,刻板的,了无新意的剧本。剧本多次被拒。事业上的不顺利也让整个家庭的经济情况陷入泥沼。被迫抛售祖宅,也许是因为便宜,搬到了这处据说发生过命案的「凶宅」。乔迁新居,尽管家庭经济状况并不好,喜好面子的父亲仍然邀请众多亲朋好友来家里庆祝。母亲不得不早上四五点起来开始购物置办准备。无论怎样,终于是在新家里安顿下来了。女儿杜美心,父亲母亲最大的希望就是让她像母亲一样成为大明星。从小开始练琴,学唱歌。
故事是如何慢慢走向悲剧的呢?大概从一开始,从父亲的编剧事业不顺,父亲的好面子,大男子主义,封建迷信开始,故事的结局就已经确定了。毕竟「人是很难改变的」。抛开游戏中所有的恐怖元素, jump scare,恐怖谷理论这些内容。单纯去看这个故事,去看这一家人。除了美心以外的其他人物都太平面扁平了。父亲杜丰于,承担了所有的反面角色的作用,他和何老师成为一切罪行的始作俑者。游戏中有一些父亲与女儿的交互,比如最温馨的故事书那段,拍照片那一段。但是更多的桥段都在展现这个父亲的负面形象,冷漠,易怒,装神弄鬼求神拜佛。父亲的存在,行动,都是在努力地推动整个故事的戏剧冲突,但有时候这样在父亲身上的硬设定,缺乏前因后果的行为,也会缺少说服力。尤其是故事的最后,也就是这故事最大的悲剧,更是觉得这父亲愚笨的难以理喻,怎么就信了别人的胡言乱语呢,你明明还那么爱你自己的女儿。这个地方,有点儿是编剧为了制造这么一个悲剧而一定要让父亲选择这么做。情节的展现也都是何老师的电话,单方面讲述,而父亲杜丰于的内心活动是缺失的。
母亲巩莉芳,承担了游戏大部分的惊吓点,也是游戏里唯一的女鬼担当。对于母亲的信息,游戏中透漏的很少。我们只知道她是曾经的电影明星,告别影坛后勤俭持家。在 1986 年决定复出,在之后下落不明。故事里可能唯一让人喜欢的,就是小美心了,心理活动刻画的也很多。所有大人的刻画都是平面的,扁平的,就像游戏里出现的纸人一样。
整体来看,『还愿』仍旧是一步水平上乘的恐怖游戏。尽管借鉴了不少优秀的前作,『寂静岭P.T.』的时空交叉,『艾迪芬奇的记忆』((What Remains of Edith Finch)的电影叙事。但「赤烛」毕竟是一个台湾团队,『还愿』是一个闽南文化背景的中文作品。当你能无障碍的阅读墙上张贴的小广告,电视广告,背景录音,所获的信息和体验都比你去玩一款欧美文化背景游戏多很多,共鸣也会更大。就好像我去看『牯岭街少年杀人事件』,一定总能带给我最大的震撼一样。
参考
]]>![Lara Go 1](/images/Game/Lara Go 1.jpg)
游戏整体美术风格,偏向折纸,背景的黑色剪影提供沉浸感。这样的美术风格在 iOS 平台并不少见。熟悉的有「纪念碑谷」「Alto’s Odyssey」。这样偏折纸的艺术风格,也比较适合这样的游戏模式。Square Enix 在 Go 系列游戏里想做的,是希望把 Go 系列做成一种桌游类型的游戏,所以这样的折纸风格再完美不过了。几年前在玩 Hitman Go 时,每一关的地图,都是按照桌游的包装盒来设计的。
以下内容涉及剧透。
Lara Go 相比于另外两作,整个游戏特色上加入「冒险」的元素。游戏玩法上引入「攀爬」系统。这样在游戏解密的过程中,就不再只是考虑单纯的 XY 平面移动,还会有来自 Z 轴的变换。这一点体现在,比如堆叠石柱,陷坑,升降机关等等。故事的核心就像最传统,最 typical 的 Tomb Raider 的故事那样,主角 Lara 来到一片丛林,在丛林深处发现一处秘境,那里保存着一个类似「魔方」的器物,获得「魔方」需要三颗石头,获得这三颗石头,必须经历三处迷宫:The Maze of Snakes,The Maze of Stones,The Maze of Spirits。要分别经历这三处迷宫,获得三个石头,打开大门,最后才能获得「魔方」,「魔方」被拿下同时,整个秘境也变得不稳定,似乎开始坍塌陷落,于是就要尽快从秘境中逃脱。这是主线内容,除此以外,Lara Go 里还有两个支线,New Adventure,分别是 The Cave of Fire 和 The Mirror of Spirits。
最有趣的章节都是来自番外。本作也是这样,全部玩下来,后面的两个 New Adventure,是我认为这个游戏设计的最棒的两个章节。The Cave of Fire 里引入了「复活系统」,在你消灭怪物后的四个回合,怪物会自动复活。所以,不再像之前那样想着如何解决掉怪物。大多数情况需要通过引诱怪物,触发某些机关,或是利用怪物复活前的四个回合,进行一些别的操作。因此,这就让每一步限制得特别死,在若干条线路中,最优的线路可能只有一条,还要不断计算机关复原的回合数,怪物复活的回合数。这些都需要比之前章节更多的思考。The Mirror of Spirits 这个章节实在是太棒了。这个章节里第一次引入了「光」系统,通过「光」来触发机关。另外最具创意的是加入了「镜像」概念。这样的美术风格和玩法模式,相比类似的「纪念碑谷」来讲,都高出太多了。画面里,游戏的一半会出现一个镜像场景,镜像场景里会有一个镜像人物。镜像场景一开始是和真实世界相同,后来就会慢慢出现了些许不同,比如不同的机关,位置不同的出口,不同的怪物,甚至不同的地形布局。这里,镜像人物的死亡,也会导致真实人物的死亡,所以解谜过程中,不仅要注意真实世界的机关,还要兼顾镜像世界里的种种要素。必要的时候,你还要进入到镜像世界,而镜像人物会来到真实世界。这样的设定,把之前那种简单的 turn-based 解谜游戏,可能只是搬动一下开关,到这里直接上升到一个新的 level,无论是游戏玩法,还是美术风格。
![Lara Go ](/images/Game/Lara Go .jpg)
![Lara Go 3](/images/Game/Lara Go 3.jpg)
说下几处自己游玩时印象深刻的点:在获得「魔方」时候,进入下一篇章,The Escape 逃亡章节。场景变的昏暗,黑色剪影的背景是不断掉落的碎石,背景音乐也突然变得紧张急促。在这个章节的游戏解密里,也尽量设计的简单。比如,拉下机关后,需要迅速通过射箭的区域。恰到好处地营造出逃亡的氛围。第二个印象深刻的点,是在火把的第一次出现,拿到火把之后,之前那些吓人的怪物,终于不再敢靠近半步。以及,在 The Mirror of Spirits,镜像的第一次出现,第一次控制真实人物进入到镜像世界里,第一次通过机关,让镜像人物和真实人物出现「异步」行动。这些一切,心中不免会涌现冒险紧张激动和解谜成功的快感,也让我不断地赞叹设计师,赞叹这个游戏的开发者。
![Lara Go 2](/images/Game/Lara Go 2.jpg)
这部游戏发布于 2015 年,也获得了诸如 Apple Design Award,TGA Best Mobile Game 等多项大奖。但比较可惜的是, Square Enix Montreal 确定不会再制作 Go 系列游戏。
Square Enix Montreal studio head Patrick Naud confirmed that the studio is not working on any new Go games, saying that “one of the challenges we have today is the premium mobile market is diminishing.” He pointed specifically to their prices (the Go games are each $4.99), saying that it’s “such a big barrier for mobile users.”
在移动端平台,大多数游戏的运营模式都是免费加内购的形式。内购的内容也往往是游戏内的金币什么。而对于类似 Lara Go 这样小而美,精致的独立游戏,在浩荡的移动平台市场,并没有什么竞争力。不管是劣币驱除良币,还是说现代生活节奏加快,人们都是更加偏爱快餐类型的消费,游戏也好,电影也好。虽然很明确 Go 不会有下一部续作,但 Go 这种棋盘风格的解谜游戏,这样的游戏方式,因其独有的风格,一定会得到延续。
参考:
]]>需要通过 brew 安装:
使用 npm 全局安装:
文件最后的结构:
.├── LICENSE├── README.md├── knexfile.js├── package-lock.json├── package.json├── src│ └── server│ ├── auth.js│ ├── db│ │ ├── connection.js│ │ ├── migrations│ │ │ ├── 20170817152841_movies.js│ │ │ └── 20190127152820_users.js│ │ ├── queries│ │ │ ├── movies.js│ │ │ └── users.js│ │ └── seeds│ │ ├── movies_seed.js│ │ └── users.js│ ├── index.js│ ├── routes│ │ ├── auth.js│ │ ├── index.js│ │ └── movies.js│ └── views│ ├── login.html│ ├── register.html│ └── status.html└── test ├── routes.auth.test.js ├── routes.index.test.js ├── routes.movies.test.js └── sample.test.js
有一些文件夹,文件属于自动生成的 boilerplate,比如 package.json
,knexfile.js
,db
文件夹里的一些内容。项目的结构清晰明朗。test
为测试文件夹。测试文件的标题统一加上 test
标示,并用 dot 分隔。这也提醒自己,文件标题的命名可以不使用 underscore 或 dash 来分隔,也可以用 dot。在 server
文件夹下,db
几乎都是数据库,knex 相关的。另外有路由 routes
文件夹和 views
视图文件夹。
在 macOS 上直接使用 Brew 安装即可。由于我个人不习惯开机自动启动数据库,那个需要用到类似 service start
的命令。这里是手动开启的办法:
$ pg_ctl -D /usr/local/var/postgres start
和 Mysql 一样,把 start
可以换成 stop
,restart
。
数据库初始化:
$ initdb /usr/local/var/postgres
在这篇教程里,需要我事先创建两个数据库,创建数据库用如下命令:
$ createdb koa_api $ createdb koa_api_test
psql
+ Database,进入 database 的命令行。
\dt
显示所有 tables
Please note the following commands:
\list
or\l
: list all databases\dt
: list all tables in the current databaseYou will never see tables in other databases, these tables aren’t visible. You have to connect to the correct database to see its tables (and other objects).
To switch databases:
\connect database_name` or `\c database_name
See the manual about psql.
除此以外,还可以通过 Knex 来创建数据库。理论上,可以通过 Knex 来进行所有的数据库操作。
Knex.js is a “batteries included” SQL query builder for Postgres, MSSQL, MySQL, MariaDB, SQLite3, Oracle, and Amazon Redshift designed to be flexible, portable, and fun to use.
– Knex.js
Knex,可以看作是各种不同数据库下统一封装的一套 API。通过 Knex 来和数据库进行交互。比如创建表,更新,添加数据等等。上文引用里提到了「battery-included」一词,意思是「开箱即用」,即这个 Library 已经包含了它所需要的全部依赖 Dependency。我们装完拿来直接用即可,不需要在进行其他 Library 的安装。
因为要频繁在 Terminal 里用到 knex
命令,所以最后事先全局安装 Knex。在这篇教程里,我们在使用 Postgresql 创建完数据库以后,会看到需要我们执行这两条命令:
$ knex migrate:latest --env development$ knex seed:run --env development
Tips:
如果不想要全局安装 Knex,依然想在 Terminal 运行。在生成的 node_modules
文件夹下会有个隐藏文件夹 .bin
,里面包含了全部我们可以直接运行的 package。所以直接:
$ node_modules/.bin/knex init
init
之后,本地会自动生成一个 knexfile.js
文件。里面大致是些 boilerplates。在这篇教程给的 source code 里, knexfile.js
已经针对 postgresql 配置完毕。但这里要明白 migration 和 seed 两个操作。
Migrations are a way to make database changes or updates, like creating or dropping tables, as well as updating a table with new columns with constraints via generated scripts. We can build these scripts via the command line using
knex
command line tool.
例如,通过 knex 创建 table:
$ knex migrate:make users
这里会自动生成一个 users.js
文件,文件名前面还会有 time stamp。存储路径在 ./server/db/migration/
。所有的文件夹都会自动生成。在新生成的文件,我们需要定义新建的这个 table 各个 field 属性。例如在这篇文章的例子里,新建了 users table,定义属性如下:
exports.up = (knex, Promise) => { return knex.schema.createTable('users', (table) => { table.increments(); table.string('username').unique().notNullable(); table.string('password').notNullable(); });};exports.down = (knex, Promise) => { return knex.schema.dropTable('users');};
通过下面这条命令来应用我们定义的属性:
knex migrate:latest --env development
To run the migration, use the command:
knex migrate:latest
The default environment is development, which works for us here, but if you need to specify a different environment, such as a test environment, then you can use the env flag in the migration command like:
knex migrate:latest --env test
development
是我们事先在 knexfile.js
里定义好的。可以理解为对于 database 的映射。从上面定义的属性中,可以很轻松的知道这个 table 有两个 field,分别是 username
和 password
。以及每个 field 的属性都通过 chain function 的形式来表达。
Similar to migrations, the
knex
module allows us to create scripts to insert initial data into our tables called seed files! If we have relations on our tables, the seeding must be in a specific order to so that we can rely on data that might already be in the database.
seed 是用来初始化数据的。同 migrate 一样:
$ knex seed:make users$ knex seed:run --env development
line 1 会自动创建一个 user.js
在路径 ./server/db/seeds/
里。line 2 运行这个 seeds,对 table 里数据进行初始化。
一个测试 module 被称为 specification,简称 spec,结构如下图所示:
在这篇教程中,用到 Mocha 和 Chai 两个测试 Library。
使用 Mocha 进行测试,运行 Mocha,它会自动找项目目录里 test 文件夹下的文件运行。
Mocha automatically looks for tests inside the
**test**
directory of your project. Hence, you should go ahead and create this directory in your project root.
以下是写的一个很简单的小例子:
const assert = require('assert');describe("sample", ()=>{ it("it's just a test", ()=>{ let x = 5; let result = x; assert.equal(Math.pow(x, 1), result); });});
在 Terminal 里运行:
$ node_modules/.bin/_mocha sample ✓ it's just a test 1 passing (6ms)
Chai 的作用是提供了更多测试的方法。例如教程里测试是否 render view 成功:
describe('GET /auth/register', () => { it('should render the register view', (done) => { chai.request(server) .get('/auth/register') .end((err, res) => { should.not.exist(err); res.redirects.length.should.eql(0); res.status.should.eql(200); res.type.should.eql('text/html'); res.text.should.contain('<h1>Register</h1>'); res.text.should.contain( '<p><button type="submit">Register</button></p>'); done(); }); });});
其主要内容检测返回的 res 里有没有想的 DOM Node 。当然,前提还是去验证返回代码是否是 200, 返回文本类型这些。
这篇教程的最后介绍了 redis,把用户的 user 信息从 memory 中拿出来存进 redis 里。这样当关闭浏览器,短时间再重新打开时,不需要重新输入用户名密码进行登录。
It’s a good idea to move session data out of memory and into an external session store as you begin scaling your application.
Redis 同样也是数据库 Database,但不同于之前接触的 MySql,Postgresql,Redis 属于 in-memory database。看了 Wikipedia 的解释。in-memory database 主要依赖于内存 memory,而不是通常的外存 storage。
关于 session 和 cookie 的区别。
cookie 是存储在 client 端的,通常是一些偏好设定,比如颜色啊等等,通常不会有敏感信息。session 存储在 server 端,因为 http 通信是无状态的。session 用来保存 client 和 server 之间的通信状态,以及 client 可能会访问多个不同的页面,这些页面都在这一个 server 上,通信的双方并没有变化,通过 session 在不同的页面共享数据。
A session is a unit of maybe variables, state, settings while a certain user is accessing a server/domain in a specific time frame. All the session information is in the traditional model stored on the server (!)
Because many concurrent users can visit a server/domain at the same time the server needs to be able to distinguish many different concurrent sessions and always assign the right session to the right user. (And no user may “steal” another uses’s session)
这里首先用到两个 middleware,koa-session 和 koa-redis。koa-session 是 koa 基础的 session 管理 middleware。通常 session 是存储在 memory 里的,通过 koa-redis 将 session 存储在 redis 里。
把 session 存储在 redis 的优势:
Redis is perfect for storing sessions. All operations are performed in memory, and so reads and writes will be fast.
The second aspect is persistence of session state. Redis gives you a lot of flexibility in how you want to persist session state to your hard-disk. You can go through http://redis.io/topics/persistence to learn more, but at a high level, here are your options …
依旧是通过 brew 安装 redis。
redis 启动:
$ redis-server /usr/local/etc/redis.conf
redis 关闭,直接 ctrl
+ C
或是:
$ redis-cli shutdown
参考:
通过本文,你能了解到。Koa 最基础的 HelloWorld,它 如何渲染一个 template 页面,传递数据。什么是「Routing 路由」,路由在 Koa 中如何实现的。
app.use()
就是添加一个 middleware。我们通过 Koa 进行的许多操作,比如处理 request,处理 data,routing 都是通过 app.use()
来实现的。
ctx
内封装了 request 和 response object。
const Koa = require('koa');const app = new Koa();app.use(async function(ctx) { ctx.body = "hello world ssss";})app.listen(3000, function() { console.log('listen port: 3000...')})
这里以 ejs 为例来进行说明,其他的 template engine,使用方法都是相通的。
使用 npm 安装:
$ npm install koa-views --save$ npm install ejs --save
server.js 内容是
const Koa = require('koa');const app = new Koa();const views = require('koa-views');app.use(views(__dirname + '/views', { map: { html: 'ejs' }}));app.use(async function(ctx) { await ctx.render('layout.ejs');})app.listen(3000, function() { console.log('listen port: 3000...')})
./views/layout.ejs
内容是
<!DOCTYPE html><head></head><body> <h1>Hello Koa, This is from ejs</h1></body>
上面这个例子是不包含传值的,当需要向 template 传递值时,通过 ctx.state
来设置,将上面 render 部分修改成:
app.use(async function(ctx) { ctx.state = { title: 'This is title', body: 'body bla bla' }; await ctx.render('layout.ejs');})
或者写成 render
的参数,二者是等价的:
app.use(async function(ctx) { await ctx.render('layout.ejs', { title: 'This is title', body: 'body bla bla' });})
此时 template 修改成:
<!DOCTYPE html><head></head><body> <h1><%- title %></h1> <p><%- body %></p></body>
通常我们在写一个 template 的时候,会分成好多组件,首先有一个大体的框架,layout.ejs,新建一个 partials 文件夹,里面存储我们所需的各个组件,如 head.ejs,header.ejs,footer.ejs 等等。我们在一个需要渲染的页面里引用这些组件,那么这个过程在 koa 应该如何实现呢?
这里直接在 ejs 里使用 include
进行引用。
header.ejs
<header> <p>This is a header</p></header>
layout.ejs
<!DOCTYPE html><head></head><body> <%- include ./header %> <h1><%- title %></h1> <p><%- body %></p></body>
对于一个 web site,需要处理各种各样不同的请求,针对不同的请求 request,有着不同的反馈 response,以及可能要调用不同的资源 resource。有些需要调用一些 javascript 文件,css 文件,有些需要调用一些图片 images,有些需要访问数据库。这些不同的资源 resource 有着不同的存储路径,为了让 request 得到合适的反馈,就需要一个 router 路由功能,告诉 server,这个 request,需要去哪里找相应的 resource 去反馈。
$ npm install koa-router --save
server.js 中修改为:
const Router = require('koa-router');const router = new Router()router.get('/', async function(ctx) { await ctx.render('layout.ejs', { title: 'This is title', body: 'body bla bla' });})app.use(router.routes());
我们看到,各个页面的渲染完全由 router 进行了接管。
上面是最简单的 “Get” 请求,下面是给出一个”Post” 请求的例子,来自 koa2 进阶学习笔记,我做了一些小改动,原文使用的是原生 koa 中的 ctx 来判断请求。我这里直接使用了 koa-router
实现,通过对比,也可以明白 koa-router 这个 module 是如何工作的,只不过是在原生 Koa 基础上增加了一层判断。
// receive the posting datafunction parsePostData(ctx) { return new Promise((resolve, reject) => { try { let postData = ""; ctx.req.addListener('data', (data) => { postData += data; }); ctx.req.addListener('end', () => { let parseData = parseQueryStr(postData); resolve(parseData); }); } catch(err) { reject(err); } })}// convert the posting data to Objectfunction parseQueryStr(data) { let queryData = {}; let queryStrList = data.split('&'); for (let queryStr of queryStrList) { let itemList = queryStr.split('='); queryData[ itemList[0] ] = decodeURIComponent(itemList[1]); } return queryData;}app.use(views(__dirname + '/views', { map: { html: 'ejs' }}));router.get('/', async function(ctx) { await ctx.render('layout.ejs', { data: 'no data posted' });})router.post('/', async function(ctx) { let postData = await parsePostData(ctx); await ctx.render('layout.ejs', { data: JSON.stringify(postData) })})app.use(router.routes());
layout.ejs 添加一个可以提交的表格,注意表格的 method
是 POST
,action
是根目录页面 "/"
。
<!DOCTYPE html><head></head><body> <%- include ./header %> <h1>koa2 request post demo</h1> <form method="POST" action="/"> <p>userName</p> <input name="userName" /><br/> <p>nickName</p> <input name="nickName" /><br/> <p>email</p> <input name="email" /><br/> <button type="submit">submit</button> </form> <p><%- data %><p></body>
]]>这篇文章以 Node.js Design Patterns 第二章的 Web Spider 例子,探究思考在 Node.js 中是如何通过 callback 来实现异步操作的。
Callbacks are to be used when we don’t know when something will be done. Again, think of something like an API call, fetching data from a database or I/O with the hard drive.
在解决一些算法题目时,经常会用到「递归」。「递归」是函数不断调用自身的过程。callback 和「递归」有些相似,区别是,「递归」是重复的调用自身,而 callback 是去调用另一个不同的函数。本质来讲,都会形成一个 Call stack。那么为什么可以通过 callback 来实现异步?
// This is synchronous.function processData() { let data = fetchData(); data += 1; return data; }// This is asynchronous... function processData(callback) { fetchData(function (err, data) { if (err) { return callback(err); } data += 1; callback(null, data); }); }
在 synchronous 中,line 3 获得数据,存储在 data 中,line 4 对数据进行处理。这是一个线性的,单线程的,需要等待的 synchronous 操作。在 async 中,函数 fetchData()
多了一个 callback 参数,后续的数据处理,data += 1
写在了这个 callback 里。也就意味着,当调用 fetchData()
后,整个程序不会停下来等待,而是接着进行下面的操作。当 fetchData()
中获得了数据,更抽象点,是达成了某个条件,则调用 callback 函数。
Callbacks are functions. You pass them to other functions so they can be executed when the time is right, i.e. when the event needed by the callback has happened.
看似在 async 中出现了第二条线程,实际上,在 Node.js 中依旧是单线程。通过单线程,来模拟多线程下的 concurrency,借助底层库 libuv
来实现。libuv
让 Node.js 有了 non-blocking I/O 特性。
For example, in Unix, regular filesystem files do not support non-blocking operations, so, in order to simulate a non-blocking behavior, it is necessary to use a separate thread outside the Event Loop. All these inconsistencies across and within the different operating systems required a higher-level abstraction to be built for the Event Demultiplexer. This is exactly why the Node.js core team created a C library called libuv, with the objective to make Node.js compatible with all the major platforms and normalize the non-blocking behavior of the different types of resource; libuv today represents the low-level I/O engine of Node.js.
– p17 Node.js Design Patterns
This may sound strange if we consider that Node.js is single threaded, but if we remember what we discussed in Chapter 1, Node.js Design Fundamentals, we realize that even though we have just one thread, we can still achieve concurrency, thanks to the nonblocking nature of Node.js.
–p71 Node.js Design Patterns
每个我们常见的操作系统都为我们封装了类似的高并发异步模型,那libuv其实就是对各个操作系统进行封装,最后暴露出统一的api供开发者调用,开发者不需要关系底层是什么操作系统,什么API了。
这里单线程模拟多线程的原理,和计算机中实现的 concurrency 差不多。因为在计算机中,如果从单个时钟来看,计算机只能完成一条命令。而借助诸如 time shared 分时系统等等,在一段时间内可以认为计算机同时「并发」地在进行多个任务。因此,在 Node.js 由于有了 libuv
,会让有着 callback 的函数会进行「异步」操作。
So why show you this? Because you can’t just call one function after another and hope they execute in the right order. Callbacks are a way to make sure certain code doesn’t execute until other code has already finished execution.
关于 callback 的使用,是有一些 conventions 的。比如 callback 的第一个参数是 error。callback 本身作为函数参数,通常放在最后一个。
Nearly everything in node.js is asynchronous. So, nearly every method that is called, you must specify a callback method to handle the result of the method call. Normally, the callback function takes two parameters: error, result. So it is up to you to check for the error and then handle the result.
在 Node.js Design Patterns 这本书的第二章节,作者通过 web spider 这个例子,介绍了 async 在 node 里的各种实现方案。有最原生的 callback hell,改良后的 callback,也有 async,Promise,generator 等等更加简单的写法。无论使用哪种方式,会用到 fs.stat(path, callback)
和 request(url, callback)
[^1]。前者是 Node.js 自身的关于文件操作的一系列 api,后者是一个第三方 module。因为这两个函数都用到了 callback,所以在 debug 模式下,就去更深一层看看是如何运作的。Web Spider 的函数源码已附在了参考链接里。
function download(url, filename, callback) { console.log(`Downloading ${url}`); request(url, (err, response, body) => { if(err) { return callback(err); } saveFile(filename, body, err => { if(err) { return callback(err); } console.log(`Downloaded and saved: ${url}`); callback(null, body); }); });}function spider(url, callback) { const filename = utilities.urlToFilename(url); fs.stat(filename, err => { if(!err) { return callback(null, filename, false); } download(url, filename, err => { if(err) { return callback(err); } callback(null, filename, true); }) });}
首先进入 fs.stat()
函数:
function stat(path, options, callback) { if (typeof options === 'function') { callback = options; options = {}; } callback = makeStatsCallback(callback); path = toPathIfFileURL(path); validatePath(path); const req = new FSReqCallback(options.bigint); req.oncomplete = callback; binding.stat(pathModule.toNamespacedPath(path), options.bigint, req);}
line 2 到 line 5 是参数判断和转换。line 6 makeStatsCallback(callback)
,调用:
// Special case of `makeCallback()` that is specific to async `*stat()` calls as// an optimization, since the data passed back to the callback needs to be// transformed anyway.function makeStatsCallback(cb) { if (typeof cb !== 'function') { throw new ERR_INVALID_CALLBACK(); } return (err, stats) => { if (err) return cb(err); cb(err, getStatsFromBinding(stats)); };}
按照注释说明,是 makeCallback()
的特殊情况,那我们就去看看 makeCallback()
是什么。
// Ensure that callbacks run in the global context. Only use this function// for callbacks that are passed to the binding layer, callbacks that are// invoked from JS already run in the proper scope.function makeCallback(cb) { if (typeof cb !== 'function') { throw new ERR_INVALID_CALLBACK(); } return (...args) => { return Reflect.apply(cb, undefined, args); };}
这段 code 的关键是 Reflect.apply(cb, undefined, args);
。按照 MDN 的叙述,Reflect
是:
Reflect is a built-in object that provides methods for interceptable JavaScript operations. The methods are the same as those of proxy handlers.
Reflect
is not a function object, so it’s not constructible.Unlike most global objects,
Reflect
is not a constructor. You cannot use it with anew
operator or invoke theReflect
object as a function. All properties and methods ofReflect
are static (just like theMath
object).
Reflect 是一个 global object。那么当调用 Reflect.apply()
,就是在 global context 下进行的。为什么需要 global context 呢?首先想什么时候会去调用 callback,往往是 error handling,出错的时候,或者是进行最后一步工作的时候。两种情况,无论是哪一种,程序都要从不管多深的 call stack 出来,回到地面,回到 global context,去 handle error,或是进行所有前提工作结束后的下一步工作。
makeStatsCallback(callback)
之后关键的三步是:
const req = new FSReqCallback(options.bigint);req.oncomplete = callback;binding.stat(pathModule.toNamespacedPath(path),
FSReqCallback
和 binding
都是对更底层的 C library 调用。
const binding = process.binding('fs');const { FSReqCallback, statValues } = binding;
从这里开始,就逐渐进入 libuv
,C library 的领域了。在这些 C library 中做了什么事情,以我目前的知识结构就很难理解了。只是大体上,应该是设置异步操作,规定在函数结束后去执行 callback 等等,就如这句 req.oncomplete = callback;
字面含义所写的那样。
request 是一个简单的 Http client。这是它们的 Git repo。web spider 的目标就是要下载目标 url 的内容。在上一小节,我们通过 fs.stat()
来检测文件是否存在。当检测到文件不存在的时候,则对目标 url 进行下载。下载这个动作,展开来讲,首要工作就是创建一个 http clinet,来向 server 发送请求,然后接收来自 server 返回的数据。即 body
内容。这些操作,都是通过 request
这个 module 来实现的。创建的 http client 就可类比浏览器,当它发送 request 请求时,需要按照 TCP/IP 协议,加入 head,设置 tunnel,redirect 等等内容。这些是通过 request.Request(params)
来实现的。
function request (uri, options, callback) { if (typeof uri === 'undefined') { throw new Error('undefined is not a valid uri or options object.') } var params = initParams(uri, options, callback) if (params.method === 'HEAD' && paramsHaveRequestBody(params)) { throw new Error('HTTP HEAD requests MUST NOT include a request body.') } return new request.Request(params)}
line 6 对参数进行初始化:
// organize params for patch, post, put, head, delfunction initParams (uri, options, callback) { if (typeof options === 'function') { callback = options } var params = {} if (typeof options === 'object') { extend(params, options, {uri: uri}) } else if (typeof uri === 'string') { extend(params, {uri: uri}) } else { extend(params, uri) } params.callback = callback || params.callback return params}
request()
函数返回的 request.Request(params)
如下:
function Request (options) { // if given the method property in options, set property explicitMethod to true // extend the Request instance with any non-reserved properties // remove any reserved functions from the options object // set Request instance to be readable and writable // call init var self = this // start with HAR, then override with additional options if (options.har) { self._har = new Har(self) options = self._har.options(options) } stream.Stream.call(self) var reserved = Object.keys(Request.prototype) var nonReserved = filterForNonReserved(reserved, options) extend(self, nonReserved) options = filterOutReservedFunctions(reserved, options) self.readable = true self.writable = true if (options.method) { self.explicitMethod = true } self._qs = new Querystring(self) self._auth = new Auth(self) self._oauth = new OAuth(self) self._multipart = new Multipart(self) self._redirect = new Redirect(self) self._tunnel = new Tunnel(self) self.init(options)}
注意一下 line 17,stream.Stream.call(self)
,在进入这个函数内部后,来到:
// legacy.jsconst EE = require('events');const util = require('util');function Stream() { EE.call(this);}
因为 EE 是来自 events 导出的 EventEmitter,EE.call(this)
实际上是对 EventEmitter 的初始化。到这里会发现,request 处理 callback 所使用的方式,是和 EventEmitter 相关的。具体的继承关系是:
EventEmitter
<- stream.Stream
<- Request
这篇文章最初是想弄清楚 fs
和 request
是怎么处理 callback 函数,是如何去调用的,一路 debug 下去,终归绕不开 libuv
,计算机底层关于 thread 的内容以及网络方面的 TCP/IP 协议。这两方面都是我的知识弱项,因此也就在合适的地方浅尝辄止了。当然,写这篇文章也让我对于 callback 有了更深的理解之外,同时,我想必要抽时间再去好好读读 CSAPP 和 TCP/IP 那两本书了。
[^1]: 原文判断文件存在用的是 fs.exists()
,但这个函数,在我查阅 Node.js Documentation 时发现已经 deprecated,所以稍微修改了一下。
参考:
]]>Let be n
an integer prime with 10
e.g. 7
.
1/7 = 0.142857 142857 142857 ...
.
We see that the decimal part has a cycle: 142857
. The length of this cycle is 6
. In the same way:
1/11 = 0.09 09 09 ...
. Cycle length is 2
.
Task
Given an integer n (n > 1), the function cycle(n) returns the length of the cycle if n and 10 are coprimes, otherwise returns -1.
Exemples:
cycle(5) = -1cycle(13) = 6 -> 0.076923 076923 0769cycle(21) = 6 -> 0.047619 047619 0476cycle(27) = 3 -> 0.037 037 037 037 0370cycle(33) = 2 -> 0.03 03 03 03 03 03 03 03cycle(37) = 3 -> 0.027 027 027 027 027 0cycle(94) = -1 cycle(22) = -1 since 1/22 ~ 0.0 45 45 45 45 ...
Note
import mathimport fractionsdef cycle(n) : if n==2 or n==5 : return -1 t = phi(n) p = primes(t) divisor = [] for i in p: x = i while t % i == 0: divisor.append(i) t //= i if isprime(t): divisor.append(t) break divisor.sort() print(divisor) for i in divisor: if 10**i % n == 1: break return idef phi(n): '''compute Euler's totient function values.''' amount = 0 for k in range(1, n + 1): if math.gcd(n, k) == 1: amount += 1 return amountdef isprime(n): """Returns True if n is prime.""" if n == 2: return True if n == 3: return True if n % 2 == 0: return False if n % 3 == 0: return False i = 5 w = 2 while i * i <= n: if n % i == 0: return False i += w w = 6 - w return Truedef primes(limit): D = {} q = 2 while q <= limit: if q not in D: yield q D[q * q] = [q] else: for p in D[q]: D.setdefault(p + q, []).append(p) del D[q] q += 1print(cycle(219199))
这道题是求 $1/n$ 的循环小数位数的。通过观察其实可以得到如下的性质,假设 $1/n$ 的循环小数位数有 $p$ 位,则有:
10**p % n == 1
其实也很好理解,就是 10 扩大 $p$ 倍,小数点向右移动 $p$ 位,整数部分恰好位一个循环数,小数部分则等于 $1/n$ 。
但是如果按照这个思路来进行求解,写一个循环,对一个稍微大一点的数,如上面的 219199
,它的循环位数是 36180
。暴力写循环根本就行不通!
def cycle(n): if not n % 2 or not n % 5: return -1 x, mods = 1, set() while x not in mods: mods.add(x) x = 10 * x % n return len(mods)
lechevalier 所用的这个方法的思路,正是我上面说的,只是并非暴力循环,而是不断迭代。我的疑问是,为何一定能保证,mods
长度刚好是循环的位数?
方法类似这里给出的答案:How to Calculate Recurring Digits?
You can calculate the decimal representation of
a / b
using the long-division algorithm you learned at school, as Mark Ransom said. To calculate each successive digit, divide the current dividend (numerator or remainder) byb
, and find the next dividend as the remainder multiplied by 10 (“bringing down a 0”). When a remainder is the same as some previous remainder, it means that the digits from then on will repeat as well, so you can note this fact and stop.
明白了。其实整个迭代的过程就是去做一次 long division。long division 就是我们小学学过的那种除法计算。
![long division](/images/long division.png)
那么为什么这样一种循环就比之前我想的那种暴力破解快呢?答案就在于迭代。每次只用 remainder 乘 10 进行迭代,一个非常小的数,算起来自然比用 $10^{n}$ 直接去除要快得多。
参考文章:
]]>