普通视图

发现新文章,点击刷新页面。
昨天以前首页

母亲看病记录(第二部分:本地医院篇)

作者 石樱灯笼
2024年8月1日 00:24

继续这个大长篇系列。

再不动笔把这个系列写完,这就要成为年更系列了。(可想而知我是有多么不愿意写这玩意)

不了解这个系列的朋友请先看 《母亲看病记录(第一部分:沈阳北部战区总医院篇-上篇)》《母亲看病记录(第一部分:沈阳北部战区总医院篇-下篇)》,连续的。

 

以下全部内容的发生年份为2022年。叙事基本以2022年为基准进行书写。内容均为回忆,所以部分内容尤其是细节可能与事实颇有偏差。

 

另外好多化验单和CT胶片核磁胶片都找不到了。我妈这个人喜欢藏东西,而且是到处藏,然后偶然会有一段时间突然翻出来,然后坐床上就看这些化验单,然后再收起来,而且是按一种奇妙的规律收起来。我刚才翻了一下这些年她堆化验单的地方,2022年的和2017年的化验单一部分堆在一起,一部分找不到了,2019年的化验单就找不到了,本篇文章的化验单和CT胶片也全都找不到。

 

当时也是太累和太乱,没有趁她开始藏东西之前先拍照。现在倒好,找不到了。我也不想认真找,一旦认真找且被她发现了,那就得拆家。


一、从沈阳回来后的后续

(这段内容不知为何没在上一篇中写,本来这段内容应该是第一部分用来收尾的 点睛之笔 的。不过也鉴于本身就没人能读到最后面,忘写了就忘写了吧)
 

1. 大活人不如电视剧

黑龙江不像辽宁,12月份还是很冷的。而我家在山上,比黑龙江平均气温还要冷。12月份还是冬天。

电视台不知道在播什么电视剧,内容是东北抗联,主角是一个人的那种抗日剧,快播放到大结局了。我也没记住名字,毕竟抗联的剧就不多,以单人为叙事主线的正经抗日剧更少,就以为之后随便网上搜搜就能找到名字。大意了啊,在网上搜了好大一堆,没一个是我看的那个。能确定的只有主人公是「杨靖宇」/「马尚德」,在网上搜了一下有他的电视剧只搜到《东北抗日联军》(2015)和《杨靖宇将军》(2005),可以确定都不是这两个片子。

有一段剧情是主人公被叛徒出卖,逃入山中,镜头里他衣服也破了,鞋也破了,基本就是光着脚在雪地里走。

本来是段挺有渲染气氛的剧情,结果我妈突然摆出一副 这很有教育意义 的脸对着我说:「你看多艰苦啊,光着脚在雪地里走」。

好家伙顿时我就觉得恶心了想吐,你儿子刚在雪地里傻走了好几个小时,你是一点没心疼。电视剧里那英雄是为了国家、为了抗日、为了战斗、为了理想、为了未来、为了活命,我他妈的在雪地里光脚傻走了几个小时是他妈的为了搀着你大晚上去不开业的批发市场。
 

2. 如果你受害了那么你活该

脚在冰天雪地里泡几个小时肯定会有后遗症,问题是,这活该的,我也没脸治。发起病来我就忍着。

但是发起病来你不是耽误人家用人效率么,毕竟能干活的男人就你一个。

于是每次我不想动弹,我家里人就怂恿我去医院看看到底怎么回事,我说 活该 。家里人就问你怎么染上这病的,我说 我活该!

因为假如我解释了,那么家里人就会说: 你活该
 

3. 我倒是把她晚上心慌的毛病给治好了

从沈阳回到家之后,去超市买了一堆可以晚上当点心的零食扔我妈屋里沙发上了。也是她嘴馋,有时候吃完饭,甚至吃饭期间,都会去摸一块小饼干啥的吃。

(理解不了的请重看前一篇文章)

然后不知是又看了哪个作孽的短视频,突然在家嚷嚷:人,下午过了2点,就不该吃东西!不能吃零食!不能吃水果!要吃都得上午吃!过了下午2点,就不能吃任何东西!只能喝水,水都不能多喝!晚饭都是作孽物!都是坏人为了祸害中国人才搞出来的东西!

(呵,老子理你呦,小时候你耍起来我就饥一顿饱一顿的,现在我发起疯来经常3天不吃饭,这我都活到35了呦,一天只饿我一顿我就当减肥了)

然后表演了几天的什么是真的晚上肚子响得咕咕咕难受得睡不着觉。然后就老实了。
 

4. 吃完药啥效果都没有

医生开的药,咔咔咔吃完了,啥效果都没有!

睡不着还是睡不着!心慌还是心慌!半身麻还是半身麻!手脚僵硬还是手脚僵硬!半夜尖叫还是尖叫!

半夜睡觉是个极其麻烦的事情。关着灯,开着电视,声音还挺大,不看,就在那躺着。在屋外看她,好似睡着了,都开始打呼噜了,然后电视里一声惊叫,又给她吓醒了,然后换个姿势继续难受。我就得盯着,得趁她睡着了,偷摸进她屋,不能吵醒她,还得把电视给她关了。时间还不能太早,前半夜太早了,她要是没一次性睡到后半夜,有时候自己醒,然后又把电视打开了。

半夜尖叫还特有规律,妥妥的后半夜2-3点之后。尤其是她后半夜一般会上一次厕所,上厕所前尖叫和梦话每个完,但是2-3点上了厕所之后,一下就睡安逸了。当然上厕所也不一定只是上厕所,有时候后半夜就突然进厨房,然后拿个菜板子菜刀在那剁剁剁个没完,或者锅碗瓢盆端来端去连嗑带撞,碗筷盆铲必定要从高出掉下来摔个叮当响。最后不得不过去吼她几句,她才肯回屋。回屋了也不一定躺床上,偌大个2米宽的双人床全给她,她不躺,佝在沙发上窝着。

只不过5点钟就醒,然后就坐在床上难受,也不躺着,就在床上坐着,就在那外放短视频。一坐一放就是几个小时,我自己看着都觉得腰疼。

白天我去厨房检查,一个切了片的菜都看不到,也不知道她后半夜到底在厨房咔咔咔切的啥。

 

问她为啥不好好睡觉,她就总说屋里总有个人影悬空在那,可吓人了。

你这不废话么,谁家里房间中间栓根绳子然后在房间中间挂衣服啊,那大半夜谁看都是一个人影飘在那,不吓人才更有鬼了,我都被你这行为吓了多少年了。

然后给我展示她藏在枕头底下的好几把大菜刀和大尖刀,有得甚至都生锈了,我也是生怕她哪天睡觉不老实再把自己划伤了然后再得破伤风。

就算有时候她好不容易在床上躺下了,也不好好躺着。也不知道她在被子里头摆的什么姿势,那被子弓起来能有半米高一米长,看着就像个地堡。经常的她就开始讲话,话特别多,就像是在和别人打电话一样。有时候还带情绪,甚至一只手把上半身支撑起一半,另一只手指着窗户下的暖气片大声开骂。这时候就很麻烦,因为她平时说话的时候就没先叫别人名字的习惯,也不会面向要对话的人,更不会提高音量让你注意到她,就是毫无预警的张口就来,假如我没及时反应过来,她就发火;但同时她跟别人说话,尤其是打电话的时候也这样。所以我这种平时一直得竖着耳朵生怕她突击说话的人遇到这种场景,也不知道是她在跟我说话,还是在打电话,还是在大半夜说梦话。

反正闹了这么多年,我都麻木了,基本上这几年一直都是后半夜再睡。

她倒是非常的不乐意,只要看着我没睡,冷不丁就在她房间里叫一句「睡觉吧」,或者上厕所的时候「咣当」地砸一下我房间的门,或者干脆地把我房门推开,然后 阴着脸就站在那一动不动

有时候甚至是后半夜我都以为她已经睡熟了,我都躺下了也睡着了,然后她起来上厕所,上完厕所哐地踹我房门一脚然后来一句「快睡觉吧」。


二、要在本地医院看病

1. 前情概要

6月份的时候,瘫痪了15年的外婆去世了。走的时候没遭罪,早上的时候还吃了早饭,结果9点多就走了。

只不过那瘫痪,折磨了全家人15年。

这15年,每天白天是我大姨和我妈白天护理,晚上是我大舅护理,一天虽说也不是非得24小时一直有人看着,但是也是一天都离不开人。我妈也是以这个接口,嘴里嘟囔了多少年要去北京看病但是放了我多少年鸽子。

走了也好,不遭罪了。只是那几天家里人互相看谁都不顺眼,都总觉得外婆走了是别人的错,是有人使坏,还各种找各种阴谋论。

算是至少三个家庭从束缚中解脱出来了,不用再全年无休的再去护理外婆了。
 

2. 突降狂风大暴雨的一天

解脱出来了,终于可以串门了。

7月初,去大姨家做客,结果下午的时候突降大暴雨。我妈一直在那损我是不是我房间没关窗户。我确定一定以及肯定我房间窗户关了,反倒我问我妈阳台和她房间的窗户关了吗?她确定一定以及肯定窗户都关了。

(信你个鬼)

趁着天黑前雨小了一点,我自己打伞跑回家,中间过一条河,肉眼可见涨水了。到路上各种被风折断的树。

DSC_6198

几条主路都被被吹断的树堵死了,不少车被砸。全区大规模停电,大风的破坏力太强了。

跑回家,阳台窗户大开着,阳台跟水淹了差不多,而且窗户下面就放着一袋面粉。我妈卧室的窗户也是大开着,被风吹进来的雨把她的床都浇湿了很大一块。床底下都积水了。我房间,窗户关着,啥事没有。

擦地收拾床,把屋里东西都收拾干净,一直干到天黑。全区停电,里外都是黑的。全年即使是平时后半夜路灯都关了也没这么黑过,倒是很爽,既没有短视频也没有老头老太太吹牛逼了,唯一缺点就是外边不知道啥人报警了,警车开着大警灯一直闪,谁路过谁瞎。

拿着两个小手电筒下楼看一圈,小区里有几个出入口本来是在修路,大风大雨把路灯和警示灯全干灭了,一个看着像个小领导的人在那拿物料在那堆路障,生怕眼瞎的人直接走到大坑里。

打电话跟还在大姨家的我妈说今晚不要回来了,要回来明天中午前再回来,等市政把道路上的危险物都清理干净了再出门。

结果第二天大早上天蒙蒙亮,我妈就干回来了。

室外还一堆未清理的树杈和不稳定的树干没清理完呢,随时都有高空坠落砸伤人的风险。

我问她路上的杂物清理干净了吗?我妈好似兴奋的上来:啥都没说明白。

反正我也没指望她能说清楚啥。
 

3. 突然要去医院看病

7月13号的时候,突然说要去本地医院看病。

我以为她是说着玩的。本地这破医院别说什么大病了,连个咽炎都看不了,上来无非是给你开一堆没啥用但是超级贵的药让你吃着玩,吃完了不好使换个方子接着吃。

而且2022年是疫情管控最滥糟的一年,我TM真不想往医院里面进。

我妈却是越说越来气,一会问我一下,一会问我一下。她那眼珠子基本天天插在手机上看短视频,内容全都是什么婆婆媳妇天天掐架考验人性的破烂玩意,脑子都泡烂了。现在搞这么一出,我是确定不了她到底是嘴上说着玩,还是要准备考验我这个35岁未婚男性的婆婆儿子关系了。

晚上没到7点呢,关灯摔卧室门,睡觉了。平时不都是看短视频一直看到11点吗?7点多那对面楼灯亮得照到她房间里,我还以为开着电视没开声音。

反复无常的女人,中老年人版本的更年期。

14号早晨,大早上天蒙蒙亮,也不知道是要逛早市还是逛公园,开门出去了。

结果过一会,电话打过来了,“我在医院呢,你几点过来啊?”

卧草你玩真格的啊。


三、本地医院就医

1. 上午门诊

赶紧起床,简单洗漱一下就饿着肚子往医院狂奔。

到了医院,还得过医院的核酸检查,入口用隔离带围出来2条U型走道。其实倒是不麻烦,就是一个龙江健康码,一个国务院行程码,但是架不住人多,更架不住这都第三年还有相当多的人根本不会扫码展示健康码,这第三针疫苗都已经打完半年了。

过完核酸检查,出口给了张写着日期的粉色的纸条,意思是拿着这张纸条今天全天可以直接进医院,不用再扫两码了。其实扫码根本没多麻烦,走U型过道那个长度完全够搞完两码的。

进医院大楼,上楼,到神经内科科室门口,那人山人海,嘈杂得像菜市场,一帮人在那大声聊天,另一帮人在那大声外放短视频,极为恐怖。

找到我妈,我大姨也在。也没在排队,也不知道在那研究啥,反正说了半天,决定挂号。

好家伙,都来了一个多小时了,还没挂号呢。

俩人倒是兴奋:拿没拿那个核酸的粉票啊,拿了那个票今天再进医院就不用扫码了,你可别把那粉票整丢了。

又爬下一楼,挂号。以前医院还要专门的诊疗卡,现在又不要了,要身份证和社保卡。我妈那社保卡,是什么个被国家抛弃的医保,挂号化验买药统统用不上,只有住院才给报销。

反正是我掏钱。

挂了一个内分泌科的号,又挂了一个神经内科的号。

IMG_1791

破医院也没有电子系统。有的是直接在门诊房间外排队,有的则是把挂号票给门口护士,然后护士按顺序叫号。我还得算计这两个号得分开多久才能前后脚排上。俩大人倒是自在,你都给人家就行了。

心是我操的,心是3年前死的。(合着我是得被判个奸杀罪呗?)

先排了个内分泌科。

问诊过程我都不用描述了,去做化验吧。

然后出内分泌科刚好排到神经内科。

问诊过程我都不用描述了,去做化验吧。

IMG_1792

反正我就是个无情的付款机器。

 

2. 上午抽血拍CT

能化验的只有抽血和CT。

到抽血的地方,观察了一下排队的队伍,人数都一样多队伍都一样长,选了个看起来脑子都正常的没有小孩没有老人的队伍排队,我妈和我大姨倒是随便选了两个队伍在那排。很快我这个队伍就排到了,俩大人的队伍基本没动地方。

然后去拍CT。把俩大人扔后面快速找拍CT的科室,然后赶紧跑回来,不然这俩大人自作聪明随便找个化验科室就进去了。

拍CT得先报道。

CT排队的人也很多,速度比抽血排队慢多了。

排队的房间里熙熙攘攘的,有几个人是从我这XX地级市下面归属的YY县级市来的,在那闲扯「这XX的医院可比YY的医院强多了!」我内心:(这破医院看病就是看着玩的,心理安慰罢了)。

排队有一会了之后,房间里一个人突然在那大叫,「我这都排了这么久了怎么还没到我!」「比我后来的人都进去拍完出来了怎么还没到我!」然后就冲到报道处开始闹,报道处先是问他「刚才有签到吗?」「有。」「那刚才叫号时没听见吗?」「刚才就没叫我!」最后给他加了个塞。但即使加塞,也是要等已经在叫号已经排在门口下一个就要进去的人的后面,这人仍然不乐意。

这强多了的医院也不比大城市,整栋楼除了门卫,可是一个安保人员都没有的。

等排到我们时已经是接近中午了。

DSC_6242
 

3. 中午

把化验和拍CT都搞完,下午才能出结果。

三个人都累懵逼了,走路回家。路上我建议直接在路边找个饭馆直接解决了算了,俩大人不同意,非要回家自己做。

回到家,根本没体力做饭,最后就是吃面条。

赶紧躺下睡一会,俩大人往床上一躺,开始看短视频,然后开始闲聊,然后咔嘣一下就睡着了。
 

4. 下午取报告

差不多医院要上班了,出门去医院。

走到大路上,黑龙江在最右边这个时区,下午的时候太阳就已经在西边了。7月份太阳算是最毒的时候,往北走那肯定是要走右边,行道树多多少少影子是在人行道上的,凉快一点是一点。我大姨不同意,非要过马路,然后顶着大太阳走,我妈马上也跟过去了。我还得跟过去一起挨暴晒,不然这7月份大太阳,晒个早上没吃中午糊弄水都不好好喝的俩老太太,真中暑了倒在地上,都不会有人能注意到。有树荫不走,非要晒太阳。

到了医院,核酸扫码的U型通道一个人没有,下午新来的人比上午少多了。我亮出粉票,工作人员点头示意我直接进。我回头,俩老太太的粉票都找不到了,在那开始翻。工作人员都看得不耐烦了:找不到就再扫个码就行了,我再给你一张新的票。俩老太太不同意:一定能找到,没丢。翻了大半天,终于翻出来两张皱巴巴的粉票,进来了。

进到医院,仅在一楼就能听到整栋楼如同菜市场一样的喧闹,并不比上午好到哪里去。

我说让俩人先上门诊楼上楼排队,下午复查的人多,先去排队。我去另一栋放射科楼取化验单和CT胶片。俩人点头了,但嘴里不知道在闲聊什么玩意。

快步走到化验楼,机打化验报告和胶片,前面排队的怵在那霸机,自己不会用,也不让别人用。总算排到我了,血液化验单打印得倒是很快,CT胶片就得等。我一回头,俩老太太竟然悠悠哒哒走了一百多米从门诊楼走到放射科楼了。 什么玩意,看着就像游戏里的跟屁虫NPC一样,但延迟有10分钟,还不会瞬移

再走回门诊楼,这时间就浪费了几十分钟了,门诊门口的人多得,整个一层楼的好几个候诊室和走廊的椅子全坐满,菜市场都看不到这么多人。

来得早,不排队,现在排队连个座都没有,俩老太太站着吧,一站就是一个多小时。一个全身僵硬,站着难受,另一个膝盖损伤,站着难受。

我在俩科室研究中间时差有多久,研究完了我说把俩科室都排上。俩大人说不用,一个一个排。随便,反正是你俩腿疼又不是我。
 

5. 下午内分泌科1

先排的内分泌科,排到了,进去,化验单给大夫,大夫啥都没看出来。我妈本身有甲减的问题,全身发肿且僵硬,化验单也只能看出来有甲减。优甲乐也是按时吃的。大夫说指标都挺正常的,要不你 有时间 再做一个甲状腺彩超看看。俩大人说什么时候能做彩超?大夫说 有时间 就可以做。俩大人说现在就有时间,今天做现在做可以吗,下午做来得及吗?今天能出结果吗?大夫:「当时做当时出结果,下午能做而且下午做彩超的人少。」然后大夫就沉默了一下,明显就看得出来大夫不想伺候这俩人了,明显的病人身上有病但是自己看不出来,今年赶紧糊弄走以后大夫轮岗不一定又轮到我。

IMG_1793

开了个彩超的单子,先不去做化验,转头到神经内科继续排队。
 

6. 下午神经内科

神经内科排队的人更多。两个入口,三个护士在那堵门,谁想进先过她们这一关。要先在她们这拿个号,然后等着叫号。走廊和候诊室比菜市场吵多了,说话全都得靠吼。

从门诊出来的人基本都是一脸怨气,因为基本是啥都确诊不了,和上午那个「这XX的医院可比YY的医院强多了」真的是鲜明对比。

 

终于排到我们了,俩大人进去了,护士不让我进,说进去的人太多了不行。于是我就只能扒在门诊室出口,在超菜市场级的背噪下听一个大夫如何在俩描述能力稀烂的人下问诊。

大夫先是在那看了一会CT片子,然后就放下了。这CT片子我也看了,非常的干净清晰,没有看到任何异常,下面放射科医师的检查建议也是除老年化现象之外,全正常。

只见那大夫自己在那手舞足蹈了一阵,然后拉起我妈的胳膊,让我妈做和大夫刚才做的一样的姿势,我妈做不到。然后大夫又拉起我妈的胳膊,这捏两下,那拉两下,然后得出结论:「咱们这,看不了这病」

(好家伙,本地医生说上大实话了)

然后开始说3年前去过沈阳也看过。大夫一笑:沈阳大医院都看不出来,咱们这小地市的医院当然更看不出来。

然后开始说我三姨的怪病,嘴上就带了一个词「帕金森」,大夫立刻就认真起来了:「你们要是怀疑是帕金森的话,去北京的大医院去看,去宣武医院看,一抽血 就能化验出来是不是帕金森」。(我差点因为这荒谬的发言笑出声)

家里人当然是想争取下在本地住院,然后转院到北京,这样就能走医保了。大夫就乐了,在本地住院那基本就是调理,无休止的调理,不会说本地医院没有这个调理的医疗水平而给你转院到北京的。
 

7. 内分泌科2

出了神经内科,直奔彩超室,做甲状腺彩超。当场出结果。

回内分泌科,大夫说,这是桥本氏病,你在吃优甲乐,所以有些指标还是正常的。这优甲乐,你吃也可以,不吃也可以。

俩大人这就抬屁股要回家。我急忙问大夫,能不能给开个优甲乐的处方,这样我就可以在网上买优甲乐这个处方药了,平时能省很多钱。大夫愣了下,也不明白个所以然,问我这处方怎么开,我就说「给开个甲减的诊断然后处方里有优甲乐就行」

DSC_6369

折腾一天,至少好歹得有点收获。
 

8. 下午回家

往家的方向走,俩人也不知为何,既不走来时的大路,可能是被来时的太阳晒怕了,但又不走有菜市场的小路,而是选了中间的一条小路走。

走到半路大姨突然说想吃冰棍。

这俩人专门选了个左边有市场有超市,右边有超市有冷饮店,唯独中间这条一个商户都没有的小路走,大姨在那开骂设计不合理。

最后都走到家门口了才在小区入口的小超市买了跟冰棍。

这还好不是北京,不然走个几公里没有一个超市的日子怎么活。


四、总结

这一天花了500块钱基本上啥都没检查出来。得到的结果总结下就是:

  • 这优甲乐,你吃也可以,不吃也可以
  • 咱们这,看不了这病
  • 一抽血就能化验出来是不是帕金森

第一句,这TM不是扯蛋么,这是医生该说的画么。药,处方药,还是激素类药物,是吃着玩的东西吗?

「咱们这,看不了这病」。我一开始就没指望本地医院能看这病,但能从本地医院医生口中里说出来这个事实,有点震惊我了。

第三句这个「一抽血」,这不是乐子么。
 

1. 抽血化验帕金森

也不算很久的之前,我读过一篇关于肝吸虫的文章。

简单来讲就是原文作者吃了一顿鱼生,之后他的朋友也吃了,然后他的朋友中招,他跟着也慌,然后做检查,以及住院治疗的过程。

这篇文章被作为「不要生食」和「寄生虫」科普文章,可是被各大媒体疯狂洗稿,甚至还有不少版本中间有媒体的 原创内容,比如亲情爱情连哭带闹,比如等高考成绩般的等化验结果,最搞笑的是,文章中本来是中招的同事变成了作者本人了,第三视角变第一视角,甚至某些版本还有 不知是啥病反正先乱吃药后做检查的混乱逻辑

原文链接是:《我如何从豆瓣第一个科普寄生虫到去检查寄生虫的》,结尾是一个「========================待续」,然后这位作者设置了日记内容隐私不可见,所以没人知道后续是啥玩意。在网上也搜不到下文,豆瓣这种内在已经死透了现在只是个尸体的破平台,也不支持匿名访问所以 waybackmachine 也是没有记录,能搜到的只有各种垃圾营销媒体平台的各种洗稿。(也确实符合中文互联网已死的理念)

这篇文章的关键是:「检查需要抽血,理论上你自己抽也可以或者在社区医院。只要送检到热带病研究所就好」。事实上国内很多医院都有抽血后 异地化验 的业务,除了血还有包括「痰」等其他可送检的业务。这要是 帕金森 也能抽一管血就能确诊,我还有个球球的去北京的必要。


五、后续

读完全文的话,应该能猜得出来接下来要发生啥了。

从2015年一直闹到现在,

我妈终于决定去北京看病了!!!

DSC_6547

(未完待续)

The post 母亲看病记录(第二部分:本地医院篇) first appeared on 石樱灯笼博客.

回国体检: 中年男人几大问题都有了(脂肪肝/肺部有结节/前列腺肿大)


中年人需要经常体检

人到中年,尤其是有了孩子之后,时间的流逝仿佛加速了。精力不再如年轻时那般充沛,身体也开始出现各种小问题。这种变化在忙碌的生活中往往不易察觉,直到某个时刻,我们才意识到自己已经不再是那个精力充沛的小伙子了。尤其是身处海外,面对英国的NHS医疗体系,除非有明显的健康问题,否则很难获得全面的体检机会。因此,每次回国时,我们都会选择做一次全面的健康检查。

福州医科大学体检体验

今年回国,我选择在福州医科大学附属第一医院进行体检。这次检查项目包括胸部CT、腹部彩超、抽血、验尿和测血压。我妻子还额外进行了妇科检查。胸部CT的速度比预期快得多,只用了几分钟。尽管对辐射有些担心,但查阅资料后发现,年均一次的CT检查在可接受范围内。

健康问题的发现与应对:中重度脂肪肝

腹部彩超显示我的肝脏有问题,进一步检查后被诊断为中重度脂肪肝医生建议注意饮食、不饮酒、多运动。为了改善健康状况,回到英国后我开始坚持跑步。此外,血压检查结果正常,而抽血检测显示各项指标基本正常,只有维生素C稍有超标,医生表示无需担心。

胸部CT结果显示有一些小结节,医生建议定期复查以监控变化。而如果比较严重有肿瘤的风险,医生会建议再做一次比较慢的胸部CT。

前列腺健康问题

在这次体检中,我还发现有前列腺肿大的问题。中年男性似乎普遍存在这个问题,我没有详细咨询医生,因为检查我的医生是女性。后来的查阅和了解中,我得知这玩意可能和性生活有关系。

性生活频繁并不是良性前列腺增生症(BPH)或前列腺肿大的主要原因。实际上,研究和医学证据并未明确表明性生活频繁会直接导致前列腺增大。前列腺增生主要与年龄、激素变化、家族史和其他健康状况有关,而不是性生活的频率。

然而,前列腺健康与性活动之间确实存在一定的关联。适度的性生活对前列腺健康有一些益处,如促进前列腺液的排出,这可能有助于预防前列腺炎症等问题。相反,长时间没有性活动可能导致前列腺液的积累,从而增加前列腺感染的风险。

总体而言,虽然性生活频率与前列腺增生的直接因果关系并不明确,但保持健康的性生活习惯和适度的性活动可能对整体前列腺健康有益。对于前列腺肿大或其他前列腺健康问题的患者,建议咨询医生,获取适合个人情况的专业建议和治疗方案。

奇怪的是,前几年在有的PSA指标过高的那个问题貌似这次并没有出现。

PSA是前列腺特异抗原(Prostate-Specific Antigen)的缩写,它是一种由前列腺组织产生的蛋白质。PSA主要存在于前列腺和精液中,少量进入血液。PSA的主要作用是帮助液化精液,使精子更容易游动。

在医学上,PSA测试是一种血液检测,用于测量血液中PSA的浓度。它是前列腺疾病,包括前列腺癌、良性前列腺增生(BPH)和前列腺炎的常规筛查工具。PSA水平升高可能提示前列腺存在异常,但它不是绝对的癌症指示,因为多种因素都可能导致PSA水平升高。

体检的重要性与选择

体检的重要性不言而喻,尤其是对癌症的早期筛查至关重要。癌症的潜伏期可能长达二十年,因此定期体检不仅是对自己负责,也是对家人负责。这次回国,我选择在三甲医院体检,感觉那里更专业。虽然医院人多,需要叫号排队,但比起体检中心的流程化和简单服务,医院的医生更有经验。尽管如此,体检费用不低,尤其是像CT这种检查,花费在千元人民币左右。

体检报告

男性全腹彩超

检查所见:
肝大小形态正常,包膜光滑,左肝内见一斑状强回声,径约0.3cm,后不伴声影,余肝实质回声增强致密,远场回声略衰减,肝内管系显示欠清晰,血管走向分布正常。门静脉主干及分支走向正常,内经无增究,内透声好,未见明显异常血流信号。胆囊形态大小正常,壁尚光滑,腔内透声好。胆总管未见扩张。胰可见部分形态大小正常,实质回声均匀,未探及明显异常回声及异常血流信号。牌大小形态正常,实质回声均匀,未探及明显异常回声及异常血流信号。双肾上腺区未探及明显异常回声及异常血流信号。双肾形态大小正常,包膜光滑,皮质回声均匀,锥体分布正常,双肾实未见明显分离,内未见明显异常回声。CDFI示双肾内血流分布正常,频谱未见明显异常改变。双侧输尿管起始段及终末段未见明显扩张。膀胱充盈好,壁连续光滑,腔内透声好。
前列腺形态正常,大小约5.1cmx3.4cmx3.4cm,包膜光滑,实质回声均,未探及明显肿块回声及异
常血流信号。双侧精囊腺未见明显异常回声及异常血流信号。
腹膜后区可见部分未见明显异常回声及异常彩色血流信号。
全查提示: 脂肪肝左肝内斑状强回声(钙化灶?) 前列腺增大

还好没有肝硬化,肝硬化是比脂肪肝更严重的问题。

脂肪肝

脂肪肝是指肝脏内脂肪堆积过多的状况,通常是由肥胖、饮酒、糖尿病、高脂饮食等因素引起的。脂肪肝在早期阶段通常没有明显的症状,但如果不加以控制,它可能会发展为更严重的肝脏疾病。脂肪肝分为两种类型:

  • 非酒精性脂肪肝病(NAFLD):与酒精无关的脂肪肝,通常与代谢综合征有关。
  • 酒精性脂肪肝病(AFLD):由过度饮酒引起。
肝硬化

肝硬化是肝脏的长期损伤导致的结果,通常是由于慢性肝病如脂肪肝、慢性乙型或丙型肝炎、长期酗酒等引起的。在肝硬化中,肝脏的健康组织被疤痕组织(纤维化)取代,导致肝脏功能下降。肝硬化是不可逆的,虽然病情的进展可以通过治疗和生活方式的改变减缓,但已经形成的疤痕组织无法恢复。

脂肪肝/肝硬化 主要区别
  • 严重性:脂肪肝是肝硬化的早期阶段,相对较轻,如果及时发现和干预,可能完全可逆;而肝硬化是肝脏的晚期损伤,严重影响肝脏功能,是不可逆的。
  • 症状和并发症:脂肪肝通常没有明显症状,而肝硬化可能导致腹水、黄疸、肝性脑病、食管静脉曲张出血等严重并发症。
  • 预后:脂肪肝在早期可以通过生活方式的改变和适当的治疗得到控制或逆转;肝硬化则需要长期的管理,如果不加以控制,可能发展为肝功能衰竭或肝癌。

因此,尽管脂肪肝是一个需要重视的问题,但肝硬化是更严重的疾病,预防其发展是关键。

china-health-checks-2024-04-06-02.25.27 回国体检: 中年男人几大问题都有了(脂肪肝/肺部有结节/前列腺肿大) 资讯 身体健康

福建医科大学附属第一医院/彩色超声诊断报告单/男性全腹彩超

甲状腺彩超

检查所见:甲状腺形态大小正常,包膜完整,右侧叶腺组织可见多个低回声结节,大者大小约0.4cmx0.3cm,界清,内回声欠均,CDFI示结节内未见明显血流信号,余腺体组织回声均,CDFI示体内I分布正常。 双侧颈部大血管旁未见明显异常回声及异常血流信号:
金查提示:甲状腺右侧叶多发结节(C-TIRADS3类)

china-health-checks-2024-04-06-02.25.52 回国体检: 中年男人几大问题都有了(脂肪肝/肺部有结节/前列腺肿大) 资讯 身体健康

福建医科大学附属第一医院/彩色超声诊断报告单/甲状腺彩超

胸部CT(胸片)

china-health-checks-2024-04-08-04.57.36 回国体检: 中年男人几大问题都有了(脂肪肝/肺部有结节/前列腺肿大) 资讯 身体健康

胸部CT检查结果(胸片)群里有以前学过一点医学的人说问题不大

结语

通过这次体检,我更加意识到健康的重要性。面对现代快节奏的生活和工作压力,我们更应该重视自己的身体状况。定期体检,不仅能早期发现潜在的健康问题,也是一种对自己和家人的负责。希望在未来的日子里,能通过合理的饮食和运动,保持良好的健康状态。简单来说就是少吃多动-管住嘴/迈开腿。

2024年回国见闻

2024年3月28号回国,4月12号回英国,在国内玩了两周,北京-福州-厦门。

英国NHS免费医疗

本文一共 2607 个汉字, 你数一下对不对.
回国体检: 中年男人几大问题都有了(脂肪肝/肺部有结节/前列腺肿大). (AMP 移动加速版本)

扫描二维码,分享本文到微信朋友圈
75a5a60b9cac61e5c8c71a96e17f2d9c 回国体检: 中年男人几大问题都有了(脂肪肝/肺部有结节/前列腺肿大) 资讯 身体健康
The post 回国体检: 中年男人几大问题都有了(脂肪肝/肺部有结节/前列腺肿大) first appeared on 小赖子的英国生活和资讯.

相关文章:

  1. 按揭贷款(房贷,车贷) 每月还贷计算器 去年给银行借了17万英镑 买了20万7500英镑的房子, 25年还清. 前2年是定率 Fix Rate 的合同 (年利率2.49%). 每个月大概是还 700多英镑. 有很多种还贷的计算方式, 定率/每月固定 是比较常用的. 简单来说就是 每个月交的钱是...
  2. 智能手机 HTC One M9 使用测评 虽然我对手机要求不高, 远远没有像追求VPS服务器一样, 但是怎么算来两年内换了四个手机, 先是三星 S4 用了一年多, 然后 Nokia Lumia 635 Windows Phone, 后来又是 BLU, 半年多前换了...
  3. 同一台服务器上多个WORDPRESS站点的一些设置可以移出去 我自从把所有网站都挪到一处VPS服务器上 就发现很多事情省事很多 可以同时管理多个网站 包括 WORDPRESS博客. 比如我有四个WORDPRESS博客 然后我就把通用的一些资料给移出去 移到 HTTP或者HTTPS都不能直接访问的文件夹里这样就更安全许多. 文件 wp-conn.php 存储了 相同的数据库资料. 1 2...
  4. 公司请的专业摄影师 公司来了新的CEO管理之后,很多事情都不一样了, 特别是一些公司对外形象的事情就特别的在意, 比如公司网站用上SSL.现在公司还有空闲的位置,请速来(钱多人不傻). 一月份出差回LUTON,刚好公司请来摄影师给高层管理照像放网站上的,于是我也凑了凑热闹(但是却还不够资格被放在公司网站上),不过没关系,放这里也差不多. 人到中年, 沧桑感强了些. 更新更新: 同事用他NB的单反给谢菲尔得办公室的人也拍了一组这样的照片.看起来很不错, 很专业,灯光,道具应有尽有.我已经用在了LINKEDIN页面上,立马高大上. 本文一共 230 个汉字, 你数一下对不对. 公司请的专业摄影师. (AMP...
  5. 在英国给孩子换学校的经历: 孩子离开了村里的小学 由于搬了家, 孩子上学得提前半小时出门了, 因为早上堵, 也得开车半小时才能到. 之前在 Fen Drayton 村庄上小学, 早上8:45学校门开, 9点敲钟孩子排队依次进入教室, 我们由于在村里, 只需要提前5分钟出门和孩子一起走路就可以了. 现在一下子早上变得很匆忙, 得叫孩子起床, 做早饭,...
  6. 优化设计 个人主页 并且 PageSpeed Insights 双项 100分 坛子的个人主页 www.tanzhijun.com 不错 很适合个人主页的模板. 而且是手机友好. 于是我照着把 我的主页改了改. https://steakovercooked.com 并且做了几点修改: 0. 使用 google mod_pagespeed 把 JS,...
  7. 在英国开车的简单介绍/英国开车上路需要准备什么? 在英国合法上路需要有: 有效的驾照; MOT 车的年检; 路税 (Road Tax);还有最重要的汽车保险; 四者缺一不可. 千万不要有侥幸心理, 因为警察现在都高科技, 都能扫描车牌就能知道你合不合法. 不合法直接拦下来轻则罚款, 重则扣车上述法庭. 驾照 在英国可以用欧盟的大部分驾照,...
  8. 老婆的配偶签证被拒 郁闷死了, 601镑签证费打水漂,一去不回!费钱费力. 去年12月份我请了律师拿到了永居.老婆是T1G签证的陪工签 (DEPENDENT VISA) 2016年4月份到期. 然后我就想说得趁早把她的签证转成配偶签(SPOUSE)这样她就可以尽快走五年永居的路线. 今天收到拒签信,原因是我没有提供 有工资进帐的那份银行帐单,我提供了我和我老婆的联名帐户, 但是工资并不是直接打到这个帐单上的.所以就这一点被拒了.完全不给解释,不给补材料的机会.601镑就这样再见了. 英国的签证寄出之后是先由另一个部门先收费, 收完费才正式审理,而且不管结果如何是不退钱的.后悔没让律师弄,也不至于到现在浪费这么多时间和金钱,签证还没过.由于原签证还没到期,所以还不能上述.估计只能等搬完家后年底请律师搞定这事. 真是郁闷, 600镑, 我可以再买一个IPHONE6,或者给我的新买的车换四个轮胎....

瓦里奥大陆2:被盗的财宝

作者 大致
2024年6月14日 23:56
Z2FtZV8wMDAwMDAwMDE1MDI2LmpwZw==
原名:ワリオランド2 盗まれた財宝 / Wario Land 2 别名:瓦力欧大陆2 / 瓦力欧乐园2机种:GBC厂商:任天堂类别:ACT发行年月:1998-10耗时:55
Wario_Land_2_131
在我的游戏生涯里,这是款命运多舛的倒霉蛋。这个游戏的通关画面,我已经等了快25年。
1999年从汤球球手里盘来二手GBC的时候,他打包给了我两盘卡:《R-TYPE》、和《勇者斗恶龙怪兽篇》。不久之后我又自己买了两盘,正是本作《瓦里奥大陆2》和《口袋妖怪银》。
大部分时间在玩银了,但本作也没少玩。不幸的是,GBC和这4盘卡不久之后就丢了。丢的时候打了十几关吧。
Wario_Land_2_137

实机丢了,就在模拟器上找回来。2001年的GBC模拟器已经相当成熟,作为GBC首发游戏的本作当然不在话下。
又打了20多关,不幸的事情又发生了,寝室的PC,硬盘挂了,啥都不剩。于是心灰意冷,这一扔下,20多年就过去了。
Wario_Land_2_089

有实体卡的时候,因为资讯的不完全,担心一旦错过关卡中的秘宝和关卡末的拼图之后没有后悔的机会,故而玩得叫一个步步为营,每关拿不到秘宝和拼图就立刻软重启。
实际上我是被没有选关这件事吓到了。这一作有5大结局。只要打到任意一个结局,就可以像一代一样开启选关画面,一次一次地陷入刷金币和秘宝的桎梏之中。
Wario_Land_2_127

本作故事承袭前作,说的是瓦里奥盖好了城堡之后,每天宅在城里呼呼大睡。结果某日遭了贼,前作的BOSS,长得像茉莉公主的那个女人带了三个枪栗,把瓦里奥的钱给偷了。瓦里奥睡醒后勃然大怒,奋起追回财产。
Wario_Land_2_017

本作的秘宝固定成了“神经衰弱”小游戏,而金币的左右也变成了开启隐藏关和过关后换取刷碎片的机会。于是在可以任意使用SL大法的模拟器上,过关变成了简单的找秘宝和隐藏分支,前作中拼老命挣钱为了达成完美结局的动力不复存在。不使用SL大法?臣妾做不到啊!
Wario_Land_2_099

窃以为本作的耐玩性是不如一代的。虽然瓦里奥有多种新变身以及不死之身,但“变身不能出门”这一特点,使得变身的乐趣大大降低。有变身的场景,唯一要判断的就是要利用变身找隐藏门还是个干扰条件。而限于GBC的机能限制,场景不可能很大,所以变身的谜题设计也就只能流于表面表面了。
唯一眼前一亮的“变身”是“金币钓人”,第一次遇见的时候不由得大呼我艹。
Wario_Land_2_033
Wario_Land_2_027
Wario_Land_2_029

BOSS有些敷衍,一小半的BOSS沿用前作,还有一百倍枪栗出现了多次,颇有些缺钱的感觉。
Wario_Land_2_011
Wario_Land_2_015
Wario_Land_2_069
Wario_Land_2_079

隐藏门的设置也比较单调,大部分是通关开关改变地形达成。而秘宝的数量虽然从15个增加到了50个,却鲜有留下深刻印象的谜题。
Wario_Land_2_117

5大分支剧情也差不多。不过是在不同的场景追上金币小偷,把钱抢回来而已。5个BOSS特色也不很鲜明。
Wario_Land_2_045
Wario_Land_2_047
Wario_Land_2_059
Wario_Land_2_065
Wario_Land_2_073
Wario_Land_2_083
Wario_Land_2_095
Wario_Land_2_097

倒是城堡内分支路线的进入方法有点脑洞:游戏开始的时候瓦里奥听到闹钟响之后,只要不起床(不按任何按键),就可以进入隐藏分支了。
Wario_Land_2_057

每种结局的通关画面都差不多。收集全部秘宝和地图碎片后倒是会出现一个非常俗烂的特殊画面。
Wario_Land_2_109
Wario_Land_2_103

5大结局都看完之后,会出现最终的隐藏关。
隐藏的最后一关,是我最讨厌的类型。是容错率很低的动作极限操作。包括变态的加速后下蹲,变态的加速后高挑,变态的跳起后拉右立刻拉左,变态的踩敌人高跳。其中有个连踩敌人9次才能跳过去的大坑,我可是足足跳了三个小时,左手拇指都快脱臼了。
Wario_Land_2_121
Wario_Land_2_129

龙珠大冒险

作者 大致
2024年5月12日 21:42
Z2FtZV8wMDAwMDAwMDE1MDA2LmpwZw==
原名:ドラゴンボール アドバンスアドベンチャー /Dragon Ball: Advance Adventure机种:GBA厂商:BANPRESTO类别:ACT发行年月:2004-11耗时:48
DragonBall_Advanced_Adventure_07

不久前鸟山明先生去世,便决定找个龙珠游戏来缅怀一下他和自己。不少up主也怀了同样的心思,很多怀念视频中便出现了这个游戏。我最喜欢龙珠从与布尔玛的相遇到战胜短笛大魔王之间的剧情。就这么愉快地决定了,这次的缅怀鸟老先生的攻略对象就是它。
DragonBall_Advanced_Adventure_01

此游戏出品于2004年冬,是GBA中后期[2]颇有名气的作品。我对GBA的态度并没有太过积极,只会对真正感兴趣的游戏才会尝试一下。《龙珠大冒险》这种动作+格斗游戏,当时明显不在此列。当年错过了也并不后悔,甚至连遗憾都没有。
DragonBall_Advanced_Adventure_05
DragonBall_Advanced_Adventure_37

龙珠的游戏大多以RPG和格斗游戏为主,动作的玩法实在凤毛麟角。上手之后觉得诚意满满,小悟空的腆肚子跑、发波时被反冲到后方的头发、空中飞踢,棍子变长、旋转棒子挡子弹,格斗版面的连击抵消和空中追打,差不多都可以在原著里找到一样的帧,真的是久违的回忆。
DragonBall_Advanced_Adventure_15
DragonBall_Advanced_Adventure_03

正常游戏中,除了动作版面,还穿插了几个飞行版面和小游戏。并且有二十一、二十二两届武道会的格斗剧情,加上BOSS战的动作面板一对一,玩法可算相当丰富。
DragonBall_Advanced_Adventure_06

龙珠IP当然在万代和眼镜厂手里。眼镜厂的动作游戏我玩的不多,本作难度对我这个动作游戏苦手来说比较适中,在稍微厉害一点的动作玩家眼里可能算比较容易的。空中的鸟类有些讨厌,而地面敌人大多还好。皮拉夫城和马斯尔塔的地形有些烦人。
对于龙珠迷来说,最大的亮点是终于原著。像什么地狱使者、骷髅机器人这样只出场一两话的小角色都有亮相,把犄角旮旯里的记忆都掏了出来。
DragonBall_Advanced_Adventure_10
DragonBall_Advanced_Adventure_23
DragonBall_Advanced_Adventure_36

对我来说,遗憾的是人造人8号没有出场,以及弥次郎兵卫只出现在过场动画里。老八跟小悟空从来没动过手,倒也能理解。但是弥次郎兵卫可是跟小悟空打过两页的,设计个用刀的角色不比重复短胳膊短腿的小悟空、小林、孙悟饭、饺子、那木……这一票可操作角色带感得多吗?
DragonBall_Advanced_Adventure_11

接下来说说缺点。
上篇《卡比弹珠台》的时候,石樱灯笼就留言预言说:“感觉掌机就应该多出一些比较杀时间,操作逻辑简单的游戏,结果到了GBA时代,很多游戏搞得像是准备给掌机做计划报废一样,搓键搓得手指头疼。”
我这个游戏简直是照他这句话找的!
那个年头很多GBA游戏生怕留不住玩家,拼了老命地增加收藏要素。而我又偏偏是个吃这套的人。
DragonBall_Advanced_Adventure_30

为了最终能够操纵短笛大魔王,需要先通关悟空剧情模式,再通关小林剧情模式,再再打附加模式,在各种犄角旮旯找齐7颗龙珠,再再再刷小兵RUSH模式,随机出现大魔王卡,最后回到标题画面输入秘技(日版),才能选用大魔王打附加模式。
短笛大魔王还不是最后一张卡,最后还藏了个机器桃白白。
于是乎刚玩的时候满腔热忱,越玩评价越低。
DragonBall_Advanced_Adventure_35

辛辛苦苦刷出来的大魔王并不怎么好用,主要是连击手感不爽。实际上我刷齐了所有人物卡片挨个试了一遍[3],真正好用的角色屈指可数。就只有蓝将军比较对胃口。
浪费不少时间不说,还把手柄的方向键给摁坏了。
DragonBall_Advanced_Adventure_26

剧情模式最后一个BOSS当然是短笛大魔王,还是颇有些难度的,主要是对招的时候难破防。
DragonBall_Advanced_Adventure_13

剧情模式的通关画面没什么特色。
DragonBall_Advanced_Adventure_14
DragonBall_Advanced_Adventure_16
DragonBall_Advanced_Adventure_17

倒是小游戏通关的时候还恭喜你一下下。
DragonBall_Advanced_Adventure_28


  • (1):地球球花演邦女郎的时候已经是五旬老太了,不在讨论之列
  • (2):本作日版发售后3天,任天堂的下一代掌机NDS在北美上市
  • (3):本作两个流行的汉化版都有大问题。一个D商汉化版不能正常存档,另一个汉化版切换到饺子开始的中间几个人物后会死机。

使用 Docker 和 pnpm 优化打包 Nuxt

作者 折影轻梦
2024年2月25日 08:00

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

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

我的项目构成

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

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

  • Nuxt3
  • Prisma
  • PNPM

开始构建

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

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

第一阶段:构建依赖项

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

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

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

第二阶段:构建应用程序

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

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

第三阶段:生成生产镜像

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

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

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

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

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

分别为:

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

a3c345aaa51a4b8b802c25bc9d3591c0.png

Dockerfile 总览

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

Redux 食用指南

作者 anran758
2021年10月11日 12:08

Redux 是一个强大的状态管理框架,被广泛用于管理应用程序的状态。它的设计理念是让状态的更新可预测和透明。本文将简要探讨 Redux 的核心机制和实际应用。

在 Redux 中,有一个状态对象负责应用程序的整个状态.Redux store 是应用程序状态的唯一真实来源

如果应用程序想要更新状态,只能通过 Redux store 执行,单向数据流可以更轻松地对应用程序中的状态进行监测管理。

Redux store 是一个保存和管理应用程序状态的 state,使用 Redux 对象中的 createStore() 来创建一个 redux store,此方法将 reducer 函数作为必需参数.

1
2
3
const reducer = (state = 5) => state;

const store = Redux.createStore(reducer);

获取数据

Redux store 对象提供了几种允许你与之交互的方法,可以使用 getState() 方法检索 Redux store 对象中保存的当前的 state

1
2
3
4
5
6
const store = Redux.createStore(
(state = 5) => state
);

// 更改此行下方的代码
const currentState = store.getState();

更新状态

由于 Redux 是一个状态管理框架,因此更新状态是其核心任务之一。在 Redux 中,所有状态更新都由 dispatch action 触发,action 只是一个 JavaScript 对象,其中包含有关已发生的 action 事件的信息。

Redux store 接收这些 action 对象,然后更新相应的状态。action 对象中必须要带有 type 属性,reducer 才能根据 type 进行区分处理。
action 除了 type 属性外,还可以附带数据给 reducer 做相应的处理,这个数据是可选的。

我们可以将 Redux action 视为信使,将有关应用程序中发生的事件信息提供给 Redux store,然后 store 根据发生的 action 进行状态的更新。

reducer

reducer 将 state 和 action 作为参数,并且它总是返回一个新的 state。这是 reducer 的唯一的作用,它不应有任何其他的作用。比如它不应调用 API 接口,也不应存在任何潜在的副作用。reducer 只是一个接受状态和动作,然后返回新状态的纯函数

在 reducer 中一般通过 switch 进行判断 action 的类型,做不同的处理。

订阅事件

store.subscribe() 可以订阅 store 的数据变化,它接收一个回调函数作为参数。当 store 数据更新时会调用该回调函数。

模块划分

当应用程序的状态开始变得越来越复杂时,将状态划分为多个部分可能是个更好的选择。我们可以考虑将不同的模块进行划分,Login 作为一个模块,Account 作为另一个模块。

但对 state 进行模块划分也不能破坏 redux 中将数据存入简单 state 的原则。因此可以生成多个 reducer, 再将它们合并到 root reducer 中。

redux 提供了 combineReducers() 函数对 reducer 进行合并。它接收一个对象作为参数,对象中的 key/value 别分对应着 module name 和相对应的 reducer 函数。

1
2
3
4
5
6
7

const rootReducer = Redux.combineReducers({
counter: counterReducer,
auth: authReducer
})

const store = Redux.createStore(rootReducer);

异步

redux 本身是不能直接处理异步操作,因此需要引入中间件来处理这些问题。在 createStore 时,还可以传入第二个可选参数,这个参数就是传递给 redux 的中间件函数。

Redux 提供了 applyMiddleware() 来创建一个中间件,一般处理 redux 异步的中间件有 redux-thunkredux-saga 等。

redux-thunk

redux-thunk 允许 action 创建函数返回一个函数而不是一个 action 对象。这个返回的函数接收 dispatchgetState 作为参数,允许直接进行异步操作和状态的分发。

例如,一个异步获取数据的 thunk 可能如下所示:

1
2
3
4
5
6
7
8
9
function fetchData() {
return (dispatch, getState) => {
// 异步操作
fetch('some-api-url')
.then(response => response.json())
.then(data => dispatch({ type: 'FETCH_DATA_SUCCESS', payload: data }))
.catch(error => dispatch({ type: 'FETCH_DATA_ERROR', error }));
};
}

redux-saga

redux-saga 是一个更高级的中间件,它使用 ES6 的 Generator 函数来让你以同步的方式写异步代码。saga 监听发起的 action,并决定基于这些 action 执行哪些副作用(如异步获取数据、访问浏览器缓存等)。

一个简单的 saga 可能如下所示:

1
2
3
4
5
6
7
8
function* fetchDataSaga(action) {
try {
const data = yield call(fetch, 'some-api-url');
yield put({ type: 'FETCH_DATA_SUCCESS', payload: data });
} catch (error) {
yield put({ type: 'FETCH_DATA_ERROR', error });
}
}

React 与 Redux

在 React 应用中,Redux 被用来跨组件共享状态。使用 react-redux 库可以方便地将 Redux 集成到 React 应用中。

Provider 组件

Providerreact-redux 提供的一个组件,它使 Redux store 对 React 应用中的所有组件可用。通常,我们在应用的最顶层包裹 Provider 并传入 store:

1
2
3
4
5
6
7
8
import { Provider } from 'react-redux';
import { store } from './store';

const App = () => (
<Provider store={store}>
<MyRootComponent />
</Provider>
);

connect 函数

connect 是一个高阶函数,用于将 React 组件连接到 Redux store。它接受两个参数:mapStateToPropsmapDispatchToProps,分别用于从 store 中读取状态和向 store 发起 actions。

1
2
3
4
5
6
7
8
9
10
11
import { connect } from 'react-redux';

const mapStateToProps = state => ({
items: state.items
});

const mapDispatchToProps = dispatch => ({
fetchData: () => dispatch(fetchData())
});

export default connect(mapStateToProps, mapDispatchToProps)(MyComponent);

总结

Redux 提供了一种统一、可预测的方式来管理应用程序的状态。通过使用 actions, reducers 和 store,开发者可以以一种高度解耦的方式来管理状态和 UI。

当结合异步处理和 React 集成时,Redux 成为了一个强大的工具,能够提升大型应用程序的开发和维护效率。

React 知识回顾 (优化篇)

作者 anran758
2020年11月4日 23:48

接下来对 React 性能相关的问题进行知识回顾。

完整目录概览

React 代码复用

  • Render Props
  • 高阶组件 (HOC)
  • 自定义 Hooks
  • Mixins (已被 React 废弃)

Render props

Render props 是一种在 React 组件之间共享代码的简单技术。具体的行为是:

  1. 子组件接收一个用于渲染指定视图的 prop 属性,该属性的类型是函数。
  2. 父组件在组件内部定义该函数后,将函数的引入传给子组件
  3. 子组件将组件内部 state 作为实参传给从外面传来的函数,并将函数的返回结果渲染在指定的视图区域。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 组件使用
<Mouse render={(x, y) => <span>x: {x}, y: {y}</span>} />

// 组件内部大致实现
class Mouse extends React.Component {
state = { x: 0, y: 0 };

render() {
return (
<section>
<header>头部信息</header>
<main>{this.props.render(this.state)}</main>
<footer>底部信息</footer>
</section>
);
}
}

准确来说 Render props 是一个用于告知组件需要渲染什么内容的函数属性。props 的命名可以由自己定义,比如用于在内容区域渲染的 prop 名可以叫 render,同时还可以再接收一个 renderHead 的 prop 用于渲染头部的信息。

高阶函数、高阶组件分别是什么?

高阶函数就是接收其它函数作为参数的函数就称之为高阶函数,像数组的 mapsortfilter 都是高阶函数。

高阶组件(Higher-order component, HOC) 是 React 用于复用组件逻辑的一种高级技巧。它具体的行为是:

函数接收一个组件作为参数,在函数体内定义一个新组件,新组件内编写可复用的逻辑并应用到参数组件中。最后再将新组件作为函数的返回值 return 出去。
redux 中的 connect 函数就是一个高阶组件。

React 性能优化

  1. 对比 props/state 新旧值的变化来决定是否渲染组件,参见:父组件在执行 render 时会不会触发子组件的 render 事件?如果会该怎么避免?
  2. 列表渲染时每项添加唯一的 key。参见:渲染列表为啥要用 key?
  3. 定时器、DOM 事件等在组件销毁时一同销毁,从而避免内存泄露。
  4. 代码分割,使用异步组件。
  5. Hooks 使用 useMemo 缓存上一次计算的结果,避免重复计算值。

父组件在执行 render 时会不会触发子组件的 render 事件?如果会该怎么避免?

如果父组件渲染后,子组件接收的 props 也跟着发生了改变,那么默认情况下会触发子组件的渲染。

若子组件接受的 props 没有发生改变,那就得判断子组件的状况。

如果子组件是继承于 Component 声明的组件,并且没有使用 shouldComponentUpdate 做避免重复渲染的处理,那么子组件会触发 render 事件。

为了避免重复渲染,类组件可以使用 shouldComponentUpdate 来决定是否进行渲染。也可以将继承于 Component 组件改为继承 PureComponment,该组件会浅对比 Props 是否进行改变,从而决定是否渲染组件。

如果是函数组件,可以通过 React.memo 来对函数组件进行缓存。

渲染列表为啥要用 key?

渲染列表时,如果不给列表子项传 key 的话,React 将默认使用 index 作为 key,同时会在控制台发出警告。

key 在兄弟节点之间必须唯一,要避免使用数组下标 index 作为 key。因为使用数组下标作为 `key 时,若数组的顺序发生了改变,将会影响 Diffing 算法的效率。

若列表的节点是组件的话,还可能会影响组件的 state 数据。因为组件实例是基于 key 来决定是否更新与复用。当顺序发生了变化,则 key 也会相应得被修改,从而导致子组件间的数据错乱。

React 使用的 Diffing 算法是通过 tagkey 判断是否是同一个元素(sameNode)。使用唯一的 key 有助于 React 识别哪些元素发生改变,如节点添加或删除。这样有助于减少渲染次数,从而优化性能。

如果数组中的数据没有唯一的 key,可以引入 shortid 预先给数组中每项数据生成唯一的 id

1
2
3
4
5
6
7
8
9
10
const shortid = require('shortid');

function addId(data) {
return {
...data,
id: shortid.generate(),
}
}

const newList = list.map(addId);

若确定没有列表的顺序不会发生变化同时没有其他唯一的 key 来标识列表项时才能使用数组的下标。

虚拟 dom 是如何提升性能的

当组件触发更新时,虚拟 DOM 通过 Diffing 算法比对新旧节点的变化以决定是否渲染 DOM 节点,从而减少渲染提升性能。因为修改真实 DOM 所耗费的性能远比操作 JavaScript 多几倍,因此使用虚拟 DOM 在渲染性能上会高效的多。

简述 React Diffing 算法

Diffing 算法(Diffing Algorithm) 会先比较两个根元素的变化:

  1. 节点类型变化时,将会卸载原有的树而建立新树。如父节点 <div> 标签被修改为 <section> 标签,则它们自身及 children 下的节点都会被重新渲染。
  2. DOM 节点类型相同时,保留相同的 DOM 节点,仅更新发生改变的属性。
  3. 组件类型相同时,组件更新时组件实例保持不变,React 将更新组件实例的 props, 并调用生命周期 componentWillReceiveProps()componentwillupdate(),最后再调用 render。若 render 中还有子组件,将递归触发 Diff。
  4. 列表节点发生变化,列表项没有设置 key 时, 那么 Diffing 算法会逐个对比节点的变化。如果是尾部新增节点,那 Diff 算法会 Diff 到列表末尾,仅新增元素即可,不会有其他的性能损耗。若新增的数据不在数组的尾部而是在中间,那么 Diffing 算法比较到中间时判断出节点发生变化,将会丢弃后面所有节点并重新渲染。
  5. 列表节点发生变化,列表项有设置 key 时, React 可以通过 key 来匹配新旧节点间的对应关系,可以很快完成 Diff 并避免重复渲染的问题。

异步组件怎么使用?

  1. 通过动态 import() 语法对组件代码进行分割。

  2. 使用 React.lazy 函数,结合 import() 语法引入动态组件。在组件首次渲染时,会自动导入包含 MyComponent 的包。

    1
    const MyComponent = React.lazy(() => import('./MyComponent'));
  3. React.Suspense 组件中渲染 lazy 组件,同时可以使用 fallback 做优雅降级(添加 loading 效果):

    1
    2
    3
    <React.Suspense fallback={<div>Loading...</div>}>
    <MyComponent />
    </React.Suspense>
  4. 封装一个错误捕获组件(比如组件命名为 MyErrorBoundary),组件内通过生命周期 getDerivedStateFromError 捕获错误信息。当异步组件加载失败时,将捕获到错误信息处理后给用户做错误提示功能。

    1
    2
    3
    4
    5
    <MyErrorBoundary>
    <React.Suspense fallback={<div>Loading...</div>}>
    <MyComponent />
    </React.Suspense>
    </MyErrorBoundary>

JSX 是如何编译为 js 代码的?

在 React v17 之前,JSX 会被编译为 React.createElement(component, props, ...children) 函数,执行会返回 vnodevnode 通过 patch 之类的方法渲染到页面。

React v17 之后更新了 JSX 转换规则。新的 JSX 转换不会将 JSX 转换为 React.createElement,而是自动从 React 的 package 中引入新的入口函数(react/jsx-runtime)并调用。这意味着我们不用在每个组件文件中显式引入 React

怎么对组件的参数做类型约束呢?

要对组件的参数做类型约束的话,可以引入 prop-types 来配置对应的 propTypes 属性。
FlowTypesScript 则可以对整个应用做类型检查。

React 知识回顾 (使用篇)

作者 anran758
2020年10月31日 20:19

使用 React 进行项目开发也有好几个项目了,趁着最近有空来对 React 的知识做一个简单的复盘。

完整目录概览

React 是单向数据流还是双向数据流?它还有其他特点吗?

React 是单向数据流,数据是从上向下流。它的其他主要特点时:

  • 数据驱动视图
  • 声明式编写 UI
  • 组件化开发

setState

React 通过什么方式来更新数据

React 是通过 setState 来更新数据的。调用多个 setState 不会立即更新数据,而会批量延迟更新后再将数据合并。

除了 setState 外还可以使用 forceUpdate 跳过当前组件的 shouldComponentUpdate diff,强制触发组件渲染(避免使用该方式)。

React 不能直接修改 State 吗?

  1. 直接修改 state 不会触发组件的渲染。
  2. 若直接修改 state 引用的值,在实际使用时会导致错误的值出现
  3. 修改后的 state 可能会被后续调用的 setState 覆盖

setState 是同步还是异步的?

出于性能的考虑,React 可能会把多个 setState 合并成一个调用。

React 内有个 batchUpdate(批量更新) 的机制,在 React 可以控制的区域 (如组件生命周期、React 封装的事件处理器) 设置标识位 isBatchingUpdate 来决定是否触发更新。

比如在 React 中注册的 onClick 事件或是 componentDidMount 中直接使用 setState 都是异步的。若想拿到触发更新后的值,可以给 setState 第二个参数传递一个函数,该函数在数据更新后会触发的回调函数,函数的参数就是更新后最新的值。

不受 React 控制的代码快中使用 setState 是同步的,比如在 setTimeout 或是原生的事件监听器中使用。

setState 小测

输出以下结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
componentDidMount() {
this.setState({ count: this.state.count + 1 });
console.log("1 -->", this.state.count);

this.setState({ count: this.state.count + 1 });
console.log("2 -->", this.state.count);

setTimeout(() => {
this.setState({ count: this.state.count + 1 });
console.log("3 -->", this.state.count);
}, 0);

setTimeout(() => {
this.setState({ count: this.state.count + 1 });
console.log("4 -->", this.state.count);
}, 0);
}

输出结果为:

1
2
3
4
1 --> 0
2 --> 0
3 --> 2
4 --> 3

解答: 调用 setState 后不会立即更新 state,开头两次调用会被异步合并调用,因此只有一次调用。一轮事件循环结束后,调用第 3、4 次 setState。由于在 setTimeout 中调用是同步更新的,因此都能正常的叠加数据。

React 生命周期

React 的生命周期主要是指组件在特定阶段会执行的函数。以下是 class 组件的部分生命周期图谱:

从上图可以看出:React 的生命周期按照类型划分,可分为 挂载时(Mounting)、更新时(Updating)、卸载时(Unmounting) 。图中的生命周期函数效果如下:

constructor (构造函数)

  • 触发条件: 组件初始化时
  • 是否可以使用 setState: X
  • 使用场景: 初始化 state 或者对方法绑定 this。在构造函数中便于自动化测试。

static getDerivedStateFromProps

Tips: 不常用方法

  • 触发条件: 调用 render 函数之前
  • 是否可以使用 setState: X
  • 函数行为: 函数可以返回一个对象用于更新组件内部的 state 数据,若返回 null 则什么都不更新。
  • 使用场景: 用于 state 依赖 props 的情况,也就是状态派生。值得注意的是派生 state 会导致代码冗余,并使组件难以维护。

shouldComponentUpdate

Tips: 不常用方法

  • 触发条件: 当 props/state 发生变化
  • 是否可以使用 setState: X
  • 函数行为: 函数的返回值决定组件是否触发 render,返回值为 true 则触发渲染,反之则阻止渲染。(组件内不写该函数的话,则调用默认函数。默认函数只会返回 true,即只要 props/state 发生变化,就更新组件)
  • 使用场景: 组件的性能优化,仅仅是浅比较 props 和 state 的变化的话,可以使用内置的 PureComponent 来代替 Component 组件。

render

  • 触发条件: 渲染组件时
  • 是否可以使用 setState: X
  • 函数行为: 函数的返回值决定视图的渲染效果
  • 使用场景: class 组件中唯一必须要实现的生命周期函数。

getSnapshotBeforeUpdate

Tips: 不常用方法

  • 触发条件: 在最近一次渲染输出(提交到 DOM 节点)之前调用
  • 是否可以使用 setState: X
  • 函数行为: 函数的返回值将传入给 componentDidUpdate 第三个参数中。若只实现了该函数,但没有使用 componentDidUpdate 的话,React 将会在控制台抛出警告
  • 使用场景: 可以在组件发生更改之前从 DOM 中捕获一些信息(例如,列表的滚动位置)

componentDidMount

  • 触发条件: 组件挂载后(插入 DOM 树中)立即调用,该函数只会被触发一次
  • 是否可以使用 setState: Y (可以直接调用,但会触发额外渲染)
  • 使用场景: 从网络请求中获取数据、订阅事件等

componentDidUpdate

  • 触发条件: 组件更新完毕后(首次渲染不会触发)
  • 是否可以使用 setState: Y (更新语句须放在条件语句中,不然可能会造成死循环)
  • 使用场景: 对比新旧值的变化,进而判断是否需要发送网络请求。比如监听路由的变化

componentWillUnmount

  • 触发条件: 组件卸载及销毁之前直接调用
  • 是否可以使用 setState: X
  • 使用场景: 清除 timer,取消网络请求或清除在 componentDidMount 中创建的订阅等

生命周期阶段

针对 React 生命周期中函数的调用顺序,笔者写了一个简易的 Demo 用于演示: React 生命周期示例

React 组件挂载阶段先后会触发 constuctorstatic getDerivedStateFromPropsrendercomponentDidMount 函数。若 render 函数内还有子组件存在的话,则会进一步递归:

1
2
3
4
5
6
7
8
9
10
[Parent]: constuctor
[Parent]: static getDerivedStateFromProps
[Parent]: render
[Children]: constuctor
[Children]: static getDerivedStateFromProps
[Children]: render
[Children]: componentDidMount
[Children]: 挂载阶段结束!
[Parent]: componentDidMount
[Parent]: 挂载阶段结束!

React 组件更新阶段主要是组件的 props 或 state 发生变化时触发。若组件内还有子组件,则子组件会判断是否也需要触发更新。默认情况下 component 组件是只要父组件发生了变化,子组件也会跟着变化。以下是更新父组件 state 数据时所触发的生命周期函数:

1
2
3
4
5
6
7
8
9
10
11
12
[Parent]: static getDerivedStateFromProps
[Parent]: shouldComponentUpdate
[Parent]: render
[Children]: static getDerivedStateFromProps
[Children]: shouldComponentUpdate
[Children]: render
[Children]: getSnapshotBeforeUpdate
[Parent]: getSnapshotBeforeUpdate
[Children]: componentDidUpdate
[Children]: 更新阶段结束!
[Parent]: componentDidUpdate
[Parent]: 更新阶段结束!

值得注意的是: 在本例 Demo 中没有给子组件传参,但子组件也触发了渲染。但从应用的角度上考虑,既然你子组件没有需要更新的东西,那就没有必要触发渲染吧?

因此 Component 组件上可以使用 shouldComponentUpdate 或者将 Component 组件替换为 PureComponment 组件来做优化。在生命周期图中也可以看到: shouldComponentUpdate 返回 false 时,将不再继续触发下面的函数。

有时你可能在某些情况下想主动触发渲染而又不被 shouldComponentUpdate 阻止渲染该怎么办呢?可以使用 force­Update() 跳过 shouldComponentUpdate 的 diff,进而渲染视图。(需要使用强制渲染的场景较少,一般不推荐这种方式进行开发)

React 组件销毁阶段也没啥好说的了。父组件先触发销毁前的函数,再逐层向下触发:

1
2
3
4
[Parent]: componentWillUnmount
[Parent]: 卸载阶段结束!
[Children]: componentWillUnmount
[Children]: 卸载阶段结束!

其他生命周期

除了上图比较常见的生命周期外,还有一些过时的 API 就没有额外介绍了。因为它们可能在未来的版本会被移除:

上图没有给出错误处理的情况,以下信息作为补充: 当渲染过程,生命周期,或子组件的构造函数中抛出错误时,会调用如下方法:

React 组件通信

  1. 父组件通过 props 给子组件传递数据。子组件通过触发父组件提供的回调函数来给父组件传递消息或数据
  2. React.Context 可以跨层级组件共享数据
  3. 自定义事件
  4. 引入 Redux/Mobx 之类的状态管理器

React.Context 怎么使用

Context 可以共享对于组件树而言是全局的数据,比如全局主题、首选语言等。使用方式如下:

  1. React.createContext 函数用于生成 Context 对象。可以在创建时给 Context 设置默认值:

    1
    const ThemeContext = React.createContext('light');
  2. Context 对象中有一个 Provider(提供者) 组件,Provider 组件接受一个 value 属性用以将数据传递给消费组件。

    1
    2
    3
    <ThemeContext.Provider value="dark">
    <page />
    </ThemeContext.Provider>
  3. 获取 Context 提供的值可以通过 contextType 或者 Consumer(消费者) 组件中获取。contextType 只能用于类组件,并且只能挂载一个 Context

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class MyClass extends React.Component {
    componentDidMount() {
    let value = this.context;
    /* 在组件挂载完成后,使用 MyContext 的值执行一些有副作用的操作 */
    }
    render() {
    let value = this.context;
    /* 基于 MyContext 的值进行渲染 */
    }
    }
    MyClass.contextType = MyContext;

    若想给组件挂载多个 Context, 或者在函数组件内使用 Context 可以使用 Consumer 组件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <ThemeContext.Consumer>
    {theme => (
    <UserContext.Consumer>
    {user => (
    <ProfilePage user={user} theme={theme} />
    )}
    </UserContext.Consumer>
    )}
    </ThemeContext.Consumer>

Context 通常适用于传递较为简单的数据信息,若数据太过复杂,还是需要引入状态管理(Redux/Mbox)。

函数组件是什么?与类组件有什么区别?

函数组件本质上是一个纯函数,它接受 props 属性,最后返回 JSX。

与类组件的差别在于: 它没有实例、不能通过 extends 继承于其他方法、也没有生命周期和 state。以前函数组件常作为无状态组件,React 16.8+ 可以引入 Hooks 为函数组件支持状态和副作用操作。

Hooks

Hook vs class

类组件的不足:

  • 状态逻辑复用难,缺少复用机制。渲染属性和高阶组件导致层级冗余。
  • 组件趋向复杂难以维护。生命周期函数混杂不相干逻辑,相干逻辑分散在不同生命周期中。
  • this 指向令人困扰。内联函数过度创建新句柄,类成员函数不能保证 this。

Hooks 的优点:

  • 自定义 Hook 方便复用状态逻辑
  • 副作用的关注点分离
  • 函数组件没有 this 问题

Hooks 现有的不足:

  • 不能完全取代 class 组件的生命周期,部分不常用的生命周期暂时没有实现。
  • Hooks 的运作方式带来了一定的学习成本,需要转换现有的编程思维,增加了心智负担。

Hooks 的使用

描述 Hooks 有哪些常用的方法和大致用途

  1. useState: 使函数组件支持设置 state 数据,可用于代替类组件的 constructor 函数。

  2. useEffect: 使函数组件支持操作副作用 (effect) 的能力,Hook 第二个参数是 effect 的依赖项。当依赖项是空时,effect 函数仅会在组件挂载后执行一遍。若有一个或多个依赖项时,只要任意一个依赖项发生变化,就会触发 effect 函数的执行。effect 函数里可以做一些如获取页面数据、订阅事件等操作。

    除此之外,useEffect 还可以返回一个函数用于做清除操作,这个清除操作时可选的。常用于清理订阅事件、DOM 事件等。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 绑定 DOM 事件
    useEffect(() => {
    document.addEventListener('click', handleClick);

    // useEffect 回调函数的返回值是函数的话,当组件卸载时会执行该函数
    // 若没有需要清除的东西,则可以忽略这一步骤
    return () => {
    document.removeEventListener('click', handleClick);
    };
    }, [handleClick]);
  3. useLayoutEffect: useEffect 的 effect 执行的时机是在浏览器完成布局和绘制之后会延迟调用。若想要 DOM 变更的同时同步执行 effect 的话可以使用 useLayoutEffect。它们之间只是执行的时机不同,其他都一样。

  4. useContext: 接收一个 Context 对象,并返回 Context 的当前值。相当于类组件的 static contextType = MyContext

  5. useReduceruseState 的代替方案,它的工作方式有点类似于 Redux,通过函数来操作 state。适合 state 逻辑较为复杂且包含多个子值,或是新的 state 依赖于旧的 state 的场景。

  6. useMemo 主要用于性能优化,它可以缓存变量的值,避免每次组件更新后都需要重复计算值。

  7. useCallbck 用于缓存函数,避免函数被重复创建,它是 useMemo 的语法糖。useCallback(fn, deps) 的效果相当于是 useMemo(() => fn, deps)

Hook 之间的一些差异

  1. React.memo 与 React.useMemo

    memo 针对一个组件的渲染是否重复执行,useMemo 定义一段函数逻辑是否重复执行。

  2. React.useMemo 与 React.useCallback

    useMemo(() => fn) 返回的是一个函数,将等同于 useCallback(fn)

  3. React.useStatus 与 React.useRef

    React.useStatus 相当于类的 stateReact.useRef 相当于类的内部属性。前者参与渲染,后者的修改不会触发渲染。

自定义 Hook 的使用

自定义 Hook 的命名规则是以 use 开头的函数,比如 useLocalStorage 就符合自定义 Hook 的命名规范。
使用自定义 Hook 的场景有很多,如表单处理、动画、订阅声明、定时器等等可复用的逻辑都能通过自定义 Hook 来抽象实现。

在自定义 Hook 中,可以使用 Hooks 函数将可复用的逻辑和功能提取出来,并将内部的 state 或操作的方法从自定义 Hook 函数中返回出来。函数组件使用时就可以像调用普通函数一祥调用自定义 Hook 函数, 并将自定义 Hook 返回的 state 和操作方法通过解构保存到变量中。

下面是 useLocalStorage 的实现,它将 state 同步到本地存储,以使其在页面刷新后保持不变。 用法与 useState 相似,不同之处在于我们传入了本地存储键,以便我们可以在页面加载时默认为该值,而不是指定的初始值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import { useState } from 'react';

// Usage
function App() {
// Similar to useState but first arg is key to the value in local storage.
const [name, setName] = useLocalStorage('name', 'Bob');

return (
<div>
<input
type="text"
placeholder="Enter your name"
value={name}
onChange={e => setName(e.target.value)}
/>
</div>
);
}

// Hook
function useLocalStorage(key, initialValue) {
// State to store our value
// Pass initial state function to useState so logic is only executed once
const [storedValue, setStoredValue] = useState(() => {
try {
// Get from local storage by key
const item = window.localStorage.getItem(key);
// Parse stored json or if none return initialValue
return item ? JSON.parse(item) : initialValue;
} catch (error) {
// If error also return initialValue
console.log(error);
return initialValue;
}
});

// Return a wrapped version of useState's setter function that ...
// ... persists the new value to localStorage.
const setValue = value => {
try {
// Allow value to be a function so we have same API as useState
const valueToStore =
value instanceof Function ? value(storedValue) : value;
// Save state
setStoredValue(valueToStore);
// Save to local storage
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
// A more advanced implementation would handle the error case
console.log(error);
}
};

return [storedValue, setValue];
}

注意: 自定义 Hook 函数在定义时,也可以使用另一个自定义 Hook 函数。

Hook 使用约束

  1. 只能在函数组件最顶层调用 Hook,不能在循环、条件判断或子函数中调用。
  2. 只能在函数组件或者是自定义 Hook 函数中调用,普通的 js 函数不能使用。

class 组件与 Hook 之间的映射与转换

函数组件相比 class 组件会缺少很多功能,但大多可以通过 Hook 的方式来实现。

生命周期

  • constructor:class 组件的构造函数一般是用于初始化 state 数据或是给事件绑定 this 指向的。函数组件内没有 this 指向的问题,因此可以忽略。而 state 可以通过 useState/useReducer 来实现。

  • getDerivedStateFromPropsgetDerivedStateFromProps 一般用于在组件 props 发生变化时派生 state。Hooks 实现同等效果如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function ScrollView({row}) {
    const [isScrollingDown, setIsScrollingDown] = useState(false);
    const [prevRow, setPrevRow] = useState(null);

    if (row !== prevRow) {
    // Row 自上次渲染以来发生过改变。更新 isScrollingDown。
    setIsScrollingDown(prevRow !== null && row > prevRow);
    setPrevRow(row);
    }

    return `Scrolling down: ${isScrollingDown}`;
    }
  • shouldComponentUpdate: 使用 React.memo 应用到函数组件中后,当 props 发生变化时,会对 props 的新旧值进行前对比,相当于是 PureComponent 的功能。如果你还想自己定义比较函数的话,可以给 React.memo 的第二个参数传一个函数,若函数返回 true 则跳过更新。

    1
    2
    3
    const Button = React.memo((props) => {
    return <button>{props.text}</button>
    });
  • render: 函数组件本身就是一个 render 函数。

  • componentDidMount / componentDidUpdate / componentWillUnmount:

    useEffect 第二个参数的依赖项为空时,相当于 componentDidMount,组件挂载后只会执行一次。每个 useEffect 返回的函数相当于是 componentWillUnmount 同等效果的操作。若有依赖,则 effect 函数相当于是 componentDidUpdate

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // 没有依赖项,仅执行一次
    useEffect(() => {
    const subscription = props.source.subscribe();

    // 相当于 componentWillUnmount
    return () => {
    subscription.unsubscribe();
    };
    }, []);

    // 若有依赖项,相当于 componentDidUpdate
    // 当 page 发生变化时会触发 effect 函数
    useEffect(() => {
    fetchList({ page });
    }, [page]);

Hooks 没有实现的生命周期钩子

  • getSnapshotBeforeUpdate
  • getDerivedStateFromError
  • componentDidCatch

转换实例变量

使用 useRef 设置可变数据。

强制更新 Hook 组件

设置一个没有实际作用state,然后强制更新 state 的值触发渲染。

1
2
3
4
5
6
7
8
9
10
const Todo = () => {
// 使用 useState,用随机数据更新也行
const [ignored, forceUpdate] = useReducer(x => x + 1, 0);

function handleClick() {
forceUpdate();
}

return <button click={handleClick}>强制更新组件</button>
}

获取旧的 props 和 state

可以通过 useRef 来保存数据,因为渲染时不会覆盖掉可变数据。

1
2
3
4
5
6
7
8
9
10
11
12
function Counter() {
const [count, setCount] = useState(0);

const prevCountRef = useRef();
useEffect(() => {
prevCountRef.current = count;
}, []);

const prevCount = prevCountRef.current;

return <h1>Now: {count}, before: {prevCount}</h1>;
}

受控组件与非受控组件的区别

受控组件主要是指表单的值受到 state 的控制,它需要自行监听 onChange 事件来更新 state

由于受控组件每次都要编写事件处理器才能更新 state 数据、可能会有点麻烦,React 提供另一种代替方案是非受控组件

非受控组件将真实数据储存在 DOM 节点中,它可以为表单项设置默认值,不需要手动更新数据。当需要用到表单数据时再通过 ref 从 DOM 节点中取出数据即可。

注意: 多数情况下React 推荐编写受控组件。

扩展资料: 受控和非受控制使用场景的选择

Portals 是什么?

Portals 就像个传送门,它可以将子节点渲染到存在于父组件以外的 DOM 节点的方案。

比如 Dialog 是一个全局组件,按照传统渲染组件的方式,Dialog 可能会受到其容器 css 的影响。因此可以使用 Portals 让组件在视觉上渲染到 <body> 中,使其样式不受 overflow: hiddenz-index 的影响。

React vs Vue

作者 anran758
2020年7月5日 11:10

在项目架构时选择合适的前端框架是至关重要的。React 和 Vue 都是流行的选择,但它们在灵活性、易用性和性能方面各有特点。本文旨在深入比较这两个框架,让我们在开发前选择技术架构有个参考。

React

React 在处理复杂业务时显示出较高的灵活性。它提供多样的技术方案选择,适用于需要高度自定义的场景。React 的特点包括:

  • 组件名称需要以大写字母开头。
  • 使用 JSX 语法,组件内需要包裹一个元素,可以使用Fragment作为占位符。
  • 响应式设计,主要关注数据。
  • 事件绑定采用驼峰命名方式。
  • 不允许直接修改 state,以保持性能。
  • 构造函数中接受参数。
  • 单向数据流,专注于视图层和数据渲染。
  • 有助于自动化测试。
  • state 或 props 改变时,render 函数会重新执行。
  • 使用虚拟 DOM 来减少真实 DOM 操作,提升性能。
  • 跨端应用实现,例如 React Native。

但它的缺点也很明显:

  • 学习曲线较陡: JSX 和组件生命周期等概念对新手而言可能较难掌握。
  • 只关注视图层: 需要与其他库结合使用以构建完整的解决方案。但 react 的生态非常丰富,甚至会有多种不同的变成风格,社区中没有一个统一认可的解决方案,这会让不熟悉 react 生态的新用户看的眼花缭乱。

Vue

Vue 提供了丰富的 API,使功能实现变得简单。它适合于快速开发和较少复杂度的项目。Vue 的特点包括:

  • 易学性,提供了详尽的文档和指导。尤其作者是国人,也提供了友好的中文文档支持。
  • 更简洁的模板语法糖,如 v-bind 和 v-model。
  • 详细的错误提示和开发工具,使调试更加方便。
  • 数据双向绑定,简化了表单输入和数据展示。
  • 更轻量级,适合小型到中型项目。
  • 提供了过渡效果和动画的集成支持。
  • 可以更方便地集成到现有的项目中。
  • 提供了类似于 React 的虚拟 DOM 和组件系统。
  • 相比 react 生态的复杂, vue 官方提供了整套最基础的 web 开发架构所需的生态。当官方的提供的库无法满足需求后可以允许你去用其他第三方库,相当于起步阶段减少了选择的烦恼。对新手会比较友好。

然而,Vue 也有它的局限性:

  • 规模限制: 对于非常大型和复杂的应用,Vue 可能不如 React 灵活。
  • 过度依赖单文件组件: 可能导致项目结构和组织方式较为单一

总结

React 和 Vue 各有所长,选择哪一个取决于特定项目的需求、开发团队的技能和偏好。React 更适合需要高度灵活和可扩展性的大型应用,而 Vue 在快速开发和简单性方面表现更佳,适合新手和中小型项目。理解每个框架的优缺点有助于做出更合适的选择。

两年以后,与React道别

作者 KotoriK
2022年12月22日 17:56

Photo by Alex Kubsch on Unsplash 雪猫社从来都少不了折腾。 先是雪先生要求加的表情包FacePack,然后又为了精确统计浏览量连了GA桑的API。之后,因为小图片不能放大看,又加了点击放大图片(Sakurairo里面更喜欢叫灯箱)的simple-img-modal,还为了显示EXIF加了EXIF读取功能。而驱动这些的,自然是前端脚本。 最开始雪猫社的这些附加组件都使用了React作为框架,现在回头看,这并不是一个很匹配我们的使用场景的选择,最直接地体现在React框架给我们带来的重达100kb的额外脚本上。这个大小的脚本会严重地拖慢我们的脚本解析速度,带来性能影响。但其实这也是当时无奈的折中之选:与React 16.x同期的Vue 2.6.x 包大小也要90多近100KB,虽然比React可能小20/30k左右,

来源

❌
❌