主要是摘抄了一些有感触的点,非原创观点
存在主义指的是一种生命能意识到自己的存在,并且以“我”为中心去探索、追求、解决和优化其生命一切的哲学思想。
索伦·奥贝·克尔凯郭尔(又译齐克果、祈克果、吉尔凯高尔等;1813年5月5日—1855年11月11日)是丹麦神学家、哲学家、诗人、社会批评家及宗教作家,一般被视为存在主义的创立者。
“传统哲学”用最通俗的语言来概括,就是三个字:“盖高楼”。从柏拉图开始,亚里士多德、阿奎那、笛卡尔、洛克、康德、黑格尔,都是体系的建造者,他们都是从一些最基本的概念出发,比如实体、理念、经验、上帝,建造一个自己的哲学大厦,而且几乎每个哲学家都要先把之前哲学家的大厦推倒,从地基开始重建。这是一种上帝视角的哲学,好像世界上发生的一切都尽在哲学家的掌握之中。
在克尔凯郭尔之前都是这种从抽象观念出发的哲学,他反对传统哲学从概念到概念的逻辑推演,开始主张要把个人的生存处境当作哲学的核心问题。
升学填报志愿的时候,上了这个学校就不能上那个,此后的人生就会大不相同;恋爱的时候选择伴侣,和这个人结婚就必须放弃那个人,此后的人生也会大不相同。甚至你今天晚上选择赴哪个饭局,遇到了哪个新朋友,得知了哪个新消息,此后的人生也可能会大不相同。每往前走一步,世界都会逼着我们做出大大小小的选择,而所有的选择都会给我们带来或多或少的焦虑和恐惧。
克尔凯郭尔把这种情绪说成是“面对自由的眩晕”,就好像我们站在悬崖旁边往下看的时候的那种感觉。这种面对自由选择的眩晕感,让他用「非此即彼」做了自己最有名的一本书的书名。
克尔凯郭尔心中的真理,不是传统哲学里那些放之四海皆准的“客观真理”,而是对于他自己而言的“主观的真理”,那就是面对人生的每一个境遇,忠于自己的内心,勇敢地做出“非此即彼”的选择。
费奥多尔·米哈伊洛维奇·陀思妥耶夫斯基1821年11月11日—1881年2月9日,俄国作家。
陀思妥耶夫斯基常常描绘那些生活在社会底层却都有着不同常人想法的角色,这使得他得以19世纪暗潮汹涌的俄国社会中小人物的心理。部分学者认为他是存在主义的奠基人,如美国哲学家瓦尔特·阿诺德·考夫曼、就曾认为《地下室手记》是第一本存在主义的书。
世界是复杂的,并不像二二得四那样简单,因此,某些人“仅仅根据科学和理性的原则”拟定的“幸福体系”,只是空想,是实现不了的。人也是复杂的,不是单凭教育就能改造好的,因为人有个性,有自己的独立人格,每个人的行为都受自己的“自由意愿”支配,有时还有逆反心理,明知不好,对自己不利,却故意为之,以此显示自己的独立存在。
弗里德里希·威廉·尼采,1844年10月15日—1900年8月25日),是出身德国的哲学家、诗人、文化批评家、古典语言学家和作曲家。
尼采所提出的“上帝已死”成了存在主义的中心论点:如果没有上帝,那么就没有必然的价值或道德律;如果没有必然的价值或道德律,人类精神处境的真相是一片虚无,那人面对虚无该怎么办呢?
从苏格拉底之后,理论家们发明出各种各样的概念、真理、信仰,说在现实世界之上,还有一些更伟大的意义。于是,这些理论掩盖了人生虚无的真相,让人们陷入了幻觉,在幻觉中获得虚假的安慰。这就是理论虚假。
尼采提到了教士们发明的五种麻醉人们的手段。
第一种是催眠,教士们用教义的灌输,用冥想、苦修之类的训练,降低人们对生命意志的要求,让人们进入一种类似冬眠状态,追求一种无我的精神解脱。
第二种手段是机械性的活动,什么时间做礼拜,什么时间忏悔,什么时间劳动,都是规定好的。这些按照明确的指示进行的活动,可以分散人们的注意力,甚至填满人们有限的意识。
第三种是给人微小的快乐,基督教里有各种慈善和表彰,这些都能给人带来微小的快乐,减轻人们的痛苦。
第四种手段,是群体认同,就是让人结成一些小的团体,形成一种相互依赖的关系,这样人们就能把不满的情绪释放在小团体之中,而不会对整体造成威胁。
第五种,是让人的某些感情得到过度的发展,从而压制其他的情感。教士们特别注重培养信徒们在上帝面前的罪责意识,有了这种意识信徒就会心甘情愿地接受苦修和责罚,主动放弃自己的权力意志
现代的生活也完全可以上边的对应,那如果一切都是虚假的呢?
我们赤裸裸地站到了虚无面前,人生没有意义,理论都是虚假,安慰都是幻觉——到这个地步,人已经一无所有了,那么他还拥有什么呢?尼采的回答是,还有一样东西,就是人的生命力。
尼采认为,面对无意义的世界和无意义的生命,人应该立足于现实,直面无意义的荒谬,以强大的生命本能舞蹈,在生命活动中创造出价值。用尼采的话说,就是“成为你自己”。这样一来,虚无不再会让你沮丧和绝望,反倒会给你最广阔的创造自我意义的空间,虚无让人变成了积极的创造者,这就是积极的虚无主义。
「与怪兽搏斗的人要注意,不要让自己也变成怪兽。当你长久凝望深渊,深渊也会回望你。」
虚无主义就是这样一只怪兽,一道深渊,要活出自己的生命意义,我们就需要与这只怪兽搏斗,就需要凝望虚无的深渊。同时我们也要随时提防自己被虚无吞没,丧失对生命真正意义的追求。
埃德蒙·古斯塔夫·阿尔布雷希特·胡塞尔(1859年4月8日—1938年4月26日)是一名德国哲学家,现象学创立者。
现代哲学中的现象学可以简单地理解为一种从主观体验上理解意识和世界的学派。 举个例子:
关于桌上的一杯鸡尾酒:
柏拉图会说:在这个酒杯,那个酒杯,所有的、每一个酒杯之下,存在一个“绝对的、完美的、平均的”酒杯。而我们所能看见的、摸到的每一个酒杯,都分享了那个“绝对酒杯”的一部分特征和属性。正是因为在我们面前的那个酒杯也分享了“绝对酒杯”的属性,它才会被我们认知为是一个酒杯。
对于纯粹的经验主义者(譬如休谟和洛克)来说,“表象”是显示在脑海里的感官信息。杏子鸡尾酒酒杯这一物体的“现象”就是我们的视觉接收到的倒锥形形状、透明颜色、光滑材质,加之嗅觉感知到的杏子酒气味、加之手指能触摸到的冰凉的玻璃质感……等等感官信息的集合。
而对于纯粹的理性主义者(譬如笛卡尔)来说,一个人看见酒杯后出现在其脑海里的“酒杯”这个念头,是理性思考得出的酒杯的抽象“理念(idea)”。
胡塞尔会说:只有我看到,触摸到,感受到的酒杯是真实的。
他也并不会止步于此。他会详细描述和归类对鸡尾酒杯的所有体验:视觉、听觉、想象、思考、情绪感受、期望、欲望,以及行动(拿酒杯,喝酒等等)。
胡塞尔确实不是存在主义者,但是在胡塞尔的弟子们的看来,现象学就不仅仅是一个解决传统问题的新工具了,而是开辟出了一大片新的哲学问题。「回到事情本身」这句口号宣告了一种新的真实性,它要求我们不带偏见,尊重意识之中出现的所有现象。哪怕是传统哲学里面完全不屑于讨论的现象,比如无聊、焦虑、忧愁、绝望这些情绪,再比如恶心、幻觉、抑郁、濒死这些体验。
这些情绪和体验都是人在实际的生存境遇里会遇到的问题。但是在传统哲学看来,这些问题太主观、太表面、太非理性,完全入不了他们的法眼。而现象学就可以去关注这些意识对象,因为正确地描述现象,就是认识到了事物的本质。于是,胡塞尔的很多弟子,不约而同地用现象学的方法去观察和描述这些生存现象,存在主义就从现象学里面脱胎而出了。
马丁·海德格尔(1889年9月26日—1976年5月26日),德国哲学家,海德格尔对于存在、时间、技术、语言和真理等问题有着独特的见解。代表作有《存在与时间》、《形而上学导论》等。
在海德格尔看来,传统哲学的错误就在于把人类生存里面那些“当下上手”的东西,当做了“现成在手”的东西去理解,这样就脱离了和遗忘了人的实际生活。海德格尔的存在主义,就是要把哲学转向人的实际生存。
当我们拿起锤子钉钉子的时候,不会注意到它的形状、颜色、重量、用途,而是拿起来直接开始钉,这种状态被海德格尔称为“当下上手”(Zuhandenheit)的状态。除非是钉着钉着锤子不好用了,比如说锤头松了,或者太重了我敲不动了,这个时候,我才会停下手上的活儿,仔细端详这个锤子。海德格尔把这种状态叫做“现成在手”(Vorhandenheit)状态。
当然,如果人的全部生活都像用锤子钉钉子一样自然、顺畅,我们也就不需要哲学了。在我们的实际生活中也会遇到很多从“当下上手”到“现成在手”的转变,比如一次工作的变动、一次人际关系的危机、一次亲人的去世,这个时候,正常的生活中断了,原本明显的意义消失了,就好像我们不得不停下敲击,去注视手中的锤子。
我们被抛入世界之后,大多数时候就是过着“常人”的生活,那是一种没有经过思考,“别人”做什么我也做什么的状态。这种缺乏反思、忘记自我的“常人”状态,就是海德格尔说的“沉沦”状态
我们小的时候,看到别的小朋友要什么,自己也想要;大学生选专业的时候,很多时候是因为父母和老师的希望;毕业工作了,看到别人买LV,自己也想买;当了爹妈,看到别人的孩子上奥数,自己也想给孩子报名;出去旅游,别人拍照打卡的地方,也是我们一定要去留下脚步和照片的地方。
我们大多数的时候,都是这样自觉不自觉地受着别人的影响。而且,我们还经常说不出影响我们的这个“别人”到底是谁,肯定不只是自己的父母、自己身边的一两个同事或者朋友,我们周围还有数量巨大的图书、广告、自媒体,等等,这些东西都在或明或暗地影响着我们。
所有的这些人和东西,组成了一张无形的大网,把我们罩在里面。
和“沉沦”相反的生存状态叫做“本真”(Eingentlichkeit),就是活出真正属于自己的生活。
即便是突然有一个时刻,意识到了自己过的只是“常人”的生活,开始扪心自问,想要做出改变。但是下一刻,可能还是会不由自主地回到那种沉沦的状态,或许只是因为那样更容易,或许只是因为被各种因素掣肘不得不那样。偶尔的灵光乍现还是远远不够的。
比这种“灵光乍现”更能给人当头棒喝的东西,那就是死亡,特别是直面自己终有一死的事实。它更能够迫使我们认识到自己的沉沦状态,甚至能够帮助我们超越沉沦状态
当随口说出“人终有一死”,或者“我终有一死”时,死亡是一个外在于我的东西,一个和我无关的“死亡事件”。就像是谈论一个名人的死,或者谈论第二次世界大战中战死的人数。
我们当然也会悲伤,也会感慨一句:“哎,人生无常,要珍惜生命。”这么说的时候,我们当然也“知道”自己终有一死,但是这种“知道”是一种“闲谈”意义上的、人云亦云的“知道”。
说完了“人生无常,要珍惜生命”,我们还是会该干什么干什么,不管是从心态上还是从做的事情上,都和之前别无二致。这些时候,我们其实是把自己排除在死亡之外的,就像萨洛扬说的“总以为自己不会死”,至少认为死亡离自己很远。这种非常外在的对于死亡的意识,并不会帮助我们进入本真状态。
那什么样的死亡意识可以做到呢?就是真切地与自己将有一死面对面,清楚地认识到,我的死亡是一件最本己的事情,是任何人都不能替代的事情。我只能“亲自去死”,而且我还不知道死亡什么时候会来。
“本真的生活”,在一个意义上确实比沉沦的生活要美好,因为我们毕竟过上了属于自己的生活,而不陷入常人的大网不能自拔。
但是从另一方面讲,如果我们把“美好”理解成确定的、容易的、快乐的,那“本真的生活”很可能一点都不美好,甚至是这些词的反面,它充满了不确定性、困难和痛苦。“本真的生活”没有给我带来任何内容上的确定性,因为海德格尔去掉了良知、本真这些词的道德意味,只强调人的个体性,所以他不会也不能告诉你具体应该去做什么
“向死存在是向着一种可能性的存在,也就是向着此在本身别具一格的可能性的存在。”活出“别具一格的可能性”,就是我们本真的生活。
卡尔·特奥多·雅士培(1883年2月23日—1969年2月26日),旧译雅斯培,德国哲学家和精神病学家,基督教存在主义的代表,1967年他成为瑞士公民。
“边界性境遇”指的是必然与我们的存在联系在一起的,界定了我们作为人的生存的境遇,特别是那些威胁到我们日常生活的安全感和稳定感的境遇。
比如说,我们的生活中一定会经历痛苦,一定会和其他人处于矛盾和斗争之中,一定将会死去。雅斯贝尔斯用了一个很有趣的比喻来描述边界性境遇,他说:“我们看不到边界性境遇背后还有什么别的东西,它们就像一堵墙,我们撞在上面,对它们无能为力……”
那面对这些边界性境遇,我们要怎么应对呢?有两种办法,一种是闭目塞听的态度,假装没有看到它们,这就类似海德格尔说的此在的“沉沦状态”。另一种就是瞪大眼睛直视边界性境遇,把它们当作契机去转变自己的日常生活,通过三重超越实现本真的生存。
第一重超越:在边界性境遇中感受到,自己不仅仅是一个存在着的“东西”或者“物品”,过着没有反思、现成给定的生活,而是真切地感受到“我这个独特的个体”的生存。
在这之后,边界性境遇可以带来第二重超越,从认识上把握这些边界,把它们看作人生的各种非此即彼的可能性。当在死亡的战栗中感受到了自己的生存,开始掂量死亡给我保留了哪些可能性,哪些才是对我的生存真正重要的东西,哪些是可有可无的东西。
再下一步,第三重的超越,就是我基于之前的感受和认识,从我的自由出发,做出实际的行动来明确自己的生存。这样我就从可能性的生存超越到了现实性的生存。
“生存就意味着超越,只要我真的是我自己,我就确信,我并非由于我而是我自己。”
汉娜·阿伦特(1906年10月14日—1975年12月4日),是政治哲学家、作家和纳粹大屠杀幸存者。
.jpg)
最有名的观点应该是“恶的平庸性”或者“平庸之恶”了。
《艾希曼在耶路撒冷》,里面详细地记录了以色列审判纳粹高官艾希曼的过程。阿伦特发现,参与了屠杀很多犹太人的艾希曼,其实并不是人们想象中的恶魔,而仅仅是一个不会自己思考,平庸到可笑的官僚。
阿伦特很有洞见地看到了死亡的反面:人的出生。表面看来,我们是赤裸裸、孤零零地来到这个世界上的,就像我们终将孤身一人离开那样。但是如果我们深想一步就会发现,绝非如此。
出生这个事件,恰恰显示了个人与共同体密不可分。我是由父母生出来的,我的父母又来自他们的父母,我生活在某个家庭、某个社区、某个国家之中。如果说死亡是把个人与他人扯开、孤立起来的境遇,那出生就是一种把个体与他人牢牢绑定在一起的境遇。
阿伦特还更进一步,认为死亡其实也不像海德格尔说的那么孤独,也带有很强的共同体色彩。阿伦特不否认,我们自己的选择和行动塑造了每个人生存的意义。但是一个人完整的人生意义,只有在他死后才能盖棺定论。而这个盖棺定论的工作,必然只能交给他所属的共同体去完成。
死亡让一个人把自己完整的生命意义交给他所属的共同体,让共同体对他形成一个前后连贯的“叙事”或者说“故事”。至于这个共同体是一个家庭、一个工作单位,还是一个国家,或者整个世界,取决于一个人生前做了哪些事,但是不管怎样,给人生赋予完整意义的工作都属于一个共同体
有某个共同体,就意味着一群人生活在一个共同的境遇之中;同时也就意味着有复数的、多元的人。共同的境遇和彼此不同的个体,正是我们“政治性”的核心特征。那些塑造了“自我”的、我自由选择的行动,看似是我这个个体进行的选择,其实都是和我所处的共同体,以及我的政治性密不可分的。
让-保罗·夏尔·艾马尔·萨特(1905年6月21日—1980年4月15日),是法国哲学家、剧作家、小说家、编剧、政治活动家、传记作家和文学评论家。他是存在主义]和现象学哲学的关键人物之一,主要哲学著作《存在与虚无》。
存在就是虚无
萨特把物的那种被决定的、不能改变的存在,叫做“自在”的存在。把人的这种“有待形成”的、不固定的存在,叫做“自为”的存在,就是自己“为自己”而存在。你可以记住这一点:自在的存在有一个固定不变的本质;而自为的存在没有固定的本质,它的本质是可以变化的
萨特坐在花神咖啡馆里,他在思考这样一个问题:人的存在和物的存在究竟有什么区别?我们都知道,人是有意识的,而物品没有。但有意识的人和没有意识的物,究竟不同在哪里呢?
萨特看着眼前忙碌的服务员,又看着自己面前的杯子,他问自己:我们说这个服务员是一个服务员,和说这个杯子是一个杯子,这两种说法是同一回事吗?他感到大不相同!
说这个服务员是一个服务员,并不是注定的。如果这个人下班了,甚至辞职了,他就不再是一个服务员了。一个人是什么,这是可以改变的。
但杯子就不同了,杯子不能改变自己,它被判定为一个杯子,别无选择地就是一个杯子,就算你把它打碎了,它仍然是一个碎掉的杯子,而且杯子甚至不能自己选择把自己打碎。
你可能发现了,其中关键的区别,就在于有没有意识和意识支配的行动。为什么人的存在可以改变?因为人并没有什么预定的本质,人的存在原本就是虚无,它的本质是“有待形成”的。
存在先于本质
哲学里,至少从柏拉图开始,主流的观点都是本质先于存在,比如说人的本质就是理性,圆形的本质就是与某个点距离相等的点的集合。这些带有普遍性的本质,在某个具体的人和具体的圆形存在之前就已经确定了,所以说“本质先于存在”。
但是在萨特看来,只有自在的存在,也就是那些没有意识的东西,才是“本质先于存在”的。一棵树、一张桌子,在它们存在之前,本质就已经确定了,一棵柳树苗就会长成柳树,一张桌子就是供人写字、吃饭的家具。
但是对于人这种“自为的存在”来讲,就完全不同了。因为人从根本上讲就是虚无,而虚无就是没有任何本质。一个婴儿在出生的时候得以存在,但是他这个存在是没有本质的,我们不能说他是好是坏,是工程师还是公务员,甚至不能说他是不是理性的。
因为在拥有存在的时候,他没有任何的确定性,他的一生充满了开放性,有无穷多的可能性,只有通过他日后有意识的选择,才能获得某种稳定的性质,拥有某种类似“本质”的东西。
虚无奠定自由
因为存在先于本质,那么就没有什么预先给定的东西把我们固定住、束缚住,就意味着我们永远可以超越“过去的本质”、“现在的本质”去追求“未来”。
换句话说,人永远不会“是”什么,而是永远都正在“成为”什么。在这个意义上,人是自由的,甚至人就是自由本身。还是那个比喻,站到舞台上,你可以扮演任何角色,每一个角色都不是你本人,但正因为如此,你的行动才是自由,因为你没有被任何一个角色所定义。
人是被判定为自由的,自由就是人的命运。人唯一的不自由就是不能摆脱自由。不论你是多么渺小,不论你受到多少外在的限制,在根本上你都是自由的。
自由的负重
自由选择必定会带来后果,那么谁来为这个后果负责?萨特说,没有任何别人可以承担这份责任,你做出了选择,你就要独自承担责任。但“承担责任”究竟是什么意思呢?为什么只能独自承担,难道这份责任就不能跟别人来分担吗?萨特的回答是:不能。
每个人的生活都充满大大小小的选择,比如毕业之后继续深造还是直接工作,选择什么职业,要不要结婚,要不要孩子……所有的选择都会有后果,我们就生活在自己选择的后果之中,这些后果也在塑造我们自己。所以我们会在乎选择的好坏对错,谁都不想过后悔的人生,我们都会希望自己的选择有一个坚实可靠的依据
任何信条、任何主义,或者别人的建议,都不能成为你的借口。萨特认为,这些说辞都只是自欺欺人,是用来逃避自己的责任。开个夸张点的玩笑,假如你和你的伴侣分手了,朋友来安慰你,会说“这不是你的错”。但萨特可能就会说,这就是你的错,是你自己选择的人,是你自己谈的恋爱,这个结果当然是你的责任。
独自承担责任是什么意思?就是自己做自己的立法者,为自己做出的每一个选择承担绝对的责任。你看,从“存在就是虚无”,萨特推出了人的绝对自由,而从绝对的自由,萨特又推出了绝对的责任。这是一份非常沉重的负担
他人即地狱
萨特认为,人总是要维护自己的主体性,所以人与人之间一定会为了争夺主体性而斗争。每个人在和他人相处的时候,都想把他人变成客体,以此来维护自己的主体性和自由。
萨特举了一个例子,说你走在街上,迎面过来一个陌生人,用眼光上下打量你,你会觉得很不舒服。为什么你会不舒服?萨特解释说,别人注视你时,他下意识地就把你变成了他观察的客体。在这个注视中,他是主导者,你只是被他观看的物品;他要实现自己的主体性,代价就是把你的主体性给否定掉,把你物化。所以,你会下意识地回避对方的注视。但你也可以反抗,他看你一眼,你就回看他一眼,用你的注视把他变成客体。
在萨特看来,人和人的交往就是这样,总是在为了争夺主体性而斗争。即使是在爱情当中也不例外。萨特说,我们想象中的浪漫爱情是一个骗局,那种不分彼此、合二为一的爱情体验,只不过是刚刚开始时候的幻觉罢了。爱情同样充满了为争夺主体性而展开的冲突和斗争,到最后要么是受虐,在羞耻中享受快乐,要么是施虐,在内疚中感到愉悦。
萨特的一个个人生选择,都体现出他对自由和本真生活的向往,他用自己的一生在践行存在主义哲学,自由选择和积极行动。
萨特是一位世界闻名的哲学家,但他从来没有在任何高等学府正式任教。他虽然撰写了很多严肃的哲学论文和著作,却也花了很多精力去写小说和戏剧,甚至获得了诺贝尔文学奖。
但更令人印象深刻的是,获奖之后,他公开拒绝领奖,理由是他“不接受任何来自官方的荣誉”。这引起了很大的争议,有人说这其实是萨特爱慕虚荣的表现,觉得获得诺贝尔奖还不够突出,还要成为第一个主动拒绝诺奖的人。
萨特和波伏瓦在上大学时相识,彼此志趣相投,很快就陷入了恋情。但他们都认为人是绝对自由的,不必受到习俗制度的约束,于是签订了一个奇特的爱情契约,作为彼此的伴侣,但永不结婚。他们的爱情是开放的,不排除与其他人发生亲密关系,但彼此坦诚,不会隐瞒。而且这个契约的有效期只有两年,每过两年双方就要确认一次,是否还继续这段伴侣关系。
这个契约足足延续了 51 年,从萨特 24 岁直到 75 岁去世,两人真的做到了相伴一生。
萨特不仅是哲学家和作家,还是一位社会政治活动家,甚至被哲学家福柯称为“法国最后的公共知识分子”。1968 年,法国又发生了史称“五月风暴”的抗议活动。萨特和波伏瓦发表声明支持这场运动,并且走上街头散发传单,直接参与抗议活动,结果被警察逮捕了。
但当时的法国总统戴高乐迅速介入干预,要求警方放人。戴高乐说,“我们能把伏尔泰关进监狱吗?不能,所以赶快把萨特放了吧”。萨特当时在法国的影响力,甚至足以与启蒙时代的伏尔泰相比。
西蒙·露西·埃内斯蒂娜·玛丽·贝特朗·德·波伏娃(1908年1月9日—1986年4月14日),或称西蒙娜·德·波伏娃、西蒙娜·波伏娃,是出身法国的作家、存在主义哲学家、政治活动家、女权主义者、社会主义者和社会理论家。她的思想与学说等,对女权主义式存在主义和女权主义理论都产生了重大影响,以《第二性》闻名。
在波伏瓦看来,人从来不是孤独的存在,不能孤立地行使自由。像萨特那样强调一个人绝对的自由是没有意义的。我们总是和他人联系在一起,所以我们的自由总是有限的,与他人相互制约的。我们不能行使绝对的自由,而只能行使在某个情境中的相对自由。
一个人如果试图对抗他人,把他人当作“地狱”,他就会失去自由,也失去自我。相比萨特,波伏瓦更愿意强调人际关系中积极的方面,她认为我们之所以能够成为现在的自己,是因为出现在我们生命中的其他人,有父母、老师、朋友、爱人,还有陌生人,自我是不断被他人塑造的,始终处于一种“生成”的过程之中。
从人的这种总是与他人互动的生存状态,波伏瓦提出了一个很有意思的概念,叫做“模糊性的道德”,或者也可以翻译成“模棱两可的道德”。在波伏瓦看来,人类在本质上就带有模棱两可性或者模糊性。
我们既是主体也是客体,既是意识也是物质,既是理性也是非理性,既是自由的也是不自由的,既相互分离又相互依赖。但是传统哲学总是想要打压这种模糊性,把人概括成“理性的动物”、“思维的主体”,或者“物质的构成”
阿尔贝·加缪(1913年11月7日—1960年1月4日),生于法属阿尔及利亚蒙多维城,法国小说家、哲学家、戏剧家、评论家,其于 1957 年获得诺贝尔文学奖。加缪位于20世纪最有名的和最重要的的法国作家之列。代表作有《局外人》、《西西弗神话》、《鼠疫》等。
“自杀是唯一真正严肃的哲学问题。判断人生值不值得活,这本身就是在回答哲学的根本问题。”
《西西弗神话》中描写了希腊神话里的一位国王西西弗,因为欺骗诸神,被罚在地狱里推着一块大石头上山。每当他费尽力气把石头推上山,石头又会重新滑落,西西弗只能从头再来。你还能想象比这更悲催的人生吗?这个神话故事最好地展现了加缪讨论自杀问题的背景:如果人生注定是没有意义的、荒谬的,我们是不是应该选择自杀呢?
人一定要追问意义,但是又注定不可能得到期待的答案,这就是荒谬感的根源,荒谬就是人与世界之间必然的联系。
如果世界注定没有意义,如果人生注定荒谬,我们能怎么办呢?面对“我们是否应该自杀?”的问题,加缪又能给出什么样答案呢?他给出的答案是:坦然接受这个世界的荒谬性,用真诚的心过好当下的生活,感受生活中的美好,这就是我们能够赋予生活的全部意义。
自杀绝对不是对抗荒谬的办法,因为自杀意味着承认荒谬的胜利,那不是对抗,而是投降。2唯有直面荒谬,珍惜当下,才能创造出此时此地的意义,哪怕这种意义只是闻到了海风的气息,只是又推着石头前进了一寸。
“登上顶峰的斗争本身足以充实人的心灵。应该设想,西西弗斯是幸福的。”
假如恶魔在某一天或某个夜晚闯入你最难耐的孤寂中,并对你说:‘你现在和过去的生活,就是你今后的生活。它将周而复始,不断重复,绝无新意,你生活中的每种痛苦、欢乐、思想、叹息,以及一切大大小小、无可言说的事情都会在你身上重现,会以同样的顺序降临’。”(尼采:《快乐的科学》341)如果你听到这话瘫软在地,那你过的就不是本真的生活;如果你面对这个恶魔说,我从来没有听过比这更神圣的话。你会把你所有的人生选择重新选一遍,那么你过的就是本真的生活,忠于自己的生活。
得到:刘玮·存在主义哲学20讲
得到:刘擎·西方现代思想
《哲学家们都干了些什么》
《存在主义咖啡馆:自由、存在和杏子鸡尾酒 》
《苏菲的世界》
chatGPT
维基百科
]]>理解小程序原理的突破口就是开发者工具了,开发者工具是基于 NW.js
,一个基于 Chromium
和 node.js
的应用运行时。同时暴漏了 debug
的入口。
点开后就是一个新的 devTools
的窗口,这里我们可以找到预览界面的 dom
。
小程序界面是一个独立的 webview
,也就是常说的视图层,可以在命令行执行 document.getElementsByTagName('webview')
,可以看到很多 webview
。
我这边第 0
个就是 pages/index/index
的视图层,再通过 document.getElementsByTagName('webview')[0].showDevTools(true)
命令单独打开这个 webview
。
熟悉的感觉回来了,其实就是普通的 html/css
,小程序的原理的突破口也就在这里了。
这篇文章简单看一下页面的 dom
是怎么来的,也就是 wxml
做了什么事情。
源代码:
渲染出来的代码:
view
变成了 wx-view
,text
变成了 wx-text
,并且里边加了 <span>
。两个关键信息,wx-xxx
标签以及 exparser
。
html
是支持我们直接写自定义名字的标签的,并且在上边设置 class
也会直接生效。
区别在于自己写的标签没有一些预制的属性,比如 div
的 display: block
。
如果我们给 wx-view
也加个 display: block
,那表现上它和 div
也就一致了。
微信已经帮我们把自定义标签的属性提前内置了。
至于为什么要把我们写的 view
转成 wx-view
,因为自定义元素中规定必须用 -
连接。
“自定义元素的名字必须包含一个破折号(
-
)所以<x-tags>
、<my-element>
和<my-awesome-app>
都是正确的名字,而<tabs>
和<foo_bar>
是不正确的。这样的限制使得 HTML 解析器可以分辨那些是标准元素,哪些是自定义元素。”
有 -
可以保证一定的兼容性,并且也可以和浏览器自带的元素有一定的区分。
简单讲,就是一个仿照 Web Components
的组件系统,它会维护标签的属性、事件,提供 registerElement
方法用于注册自定义组件,提供 createElement
来渲染组件,对于自定义组件会采用 Shadow DOM
的技术。
Exparser
的相关代码在哪里呢?这就是微信传说中的基础库里了,在渲染层引入的是 WAWebview.js
。
可以右键打开这个文件,复制出来格式化一下:
由于文件比较大,用 VSCode
直接格式化可能会很卡,可以写个脚本来格式化。
1 | // chatGPT 生成 |
然后在命令行执行 node format.js ./WAWebview.js
,接下来就看到格式化的代码了:
是 2.32.3
版本,目前微信已经更到 3.x.x
了,新增了渲染引擎 Skyline
,为了简单些这次就先看 2.x
的版本了。
总共有 14
万行
接下来通过搜索、折行,找一下 Exparser
的部分,因为都是压缩过的代码,逐行理解肯定不现实,就找几个关键点看一下:
提供了注册组件的方法 registerElement
。
提前注册了内置的组件:
wx-view
:
wx-text
:
可以看到上边最终转成了 span
标签,和我们开发者工具看到的也是一致的:
提供了 createElement
方法,将注册的组件生成为最终的 dom
。
最终会调用 document
来创建 dom
。
再回到加载的 dom
看一下 wxml
转换成了什么:
右键打开这个文件:
定义了 $gw
这个函数,接收 path
参数。
返回一个函数:
内部有我们 wxml
的变量:
对应于原文件:
看一下调用这个函数的地方:
传入当前页面路径将生成的函数赋值给了 generateFunc
,接着用 document.dispatchEvent
触发事件 generateFuncReady
,并且将 generateFunc
传入。
我们在控制台手动执行一下 generateFunc
,看下返回值:
可以看到 3
个子元素:
但因为前两个的值是在逻辑层 data
中,因为我们没有传递,所以上边前两个子元素 children
都是空字符串
这个 data
需要在调用 generateFunc
的时候传入:
现在就正常返回了标签的结构,接着渲染层内部就会利用它生成虚拟 dom
,再利用 Exparser
生成最终的 dom
元素了。
大概是下边的流程(下边的代码是最早期的基础库,目前的版本已经不是下边的结构了,目前先按下边的流程理解,后边再理清当前基础库的逻辑):
调用 virtualTree
将 generateFunc
返回的结构变为虚拟 dom
,接着调用 render
,render
内部就是调用前边介绍的 Exparser
的 createElement
方法生成真正的 dom
,最后通过 replaceChild
挂载到页面上。
当然 generateFunc
需要的 data
数据需要等待逻辑层传过来,后边的文章再介绍通信机制。
剩下最后一个问题,wxml.js
是哪里来的?
和 wxss 一样,是微信提前编译生成的。编译工具可以在微信开发者工具目录搜索 wcc
,Library
是个隐藏目录。
我们把这个 wcc
文件拷贝到 index.wxml
的所在目录,然后将我们的 index.wxml
手动编译一下:
1 | ./wcc -js ./index.wxml >> wxml.js |
可以看到 $gw
函数就生成了。
大概过程就是上边了,先提前编译出了 $gw
函数,会返回一个函数,可以把 wxml
实例为一个 dom
的标签结构。传入当前页面的路径执行该函数生成 generateFunc
函数,将函数传给视图层。
视图层拿到逻辑层的数据后将 generateFunc
函数返回的 dom
结构生成虚拟 dom
,通过 Exparser
执行 render
生成最终的 dom
挂载到页面。
至于拿到逻辑层的数据的时机,相互通信的逻辑就放到后边的文章了,看着混淆的代码,头大。
历史文章:
]]>理解小程序原理的突破口就是开发者工具了,开发者工具是基于 NW.js
,一个基于 Chromium
和 node.js
的应用运行时。同时暴漏了 debug
的入口。
点开后就是一个新的 devTools
的窗口,这里我们可以找到预览界面的 dom
。
小程序界面是一个独立的 webview
,也就是常说的视图层,可以在命令行执行 document.getElementsByTagName('webview')
,可以看到很多 webview
。
我这边第 0
个就是 pages/index/index
的视图层,再通过 document.getElementsByTagName('webview')[0].showDevTools(true)
命令单独打开这个 webview
。
熟悉的感觉回来了,其实就是普通的 html/css
,小程序的原理的突破口也就在这里了。
这篇文章简单看一下页面的样式是怎么来的,也就是 wxss
做了什么事情。
源码中 data1
的样式:
开发中工具中对应的样式:
rpx
的单位转成了 px
,同时保留网页不认识的属性名,大概就是为了方便的看到当前类本身的属性和一些文件信息。
这个样式是定义在 <style>
中,
让我们展开 <head>
找一下:
data1
确实在 <style>
中,继续搜索,可以看到这里 <style>
中的内容是通过在 <script>
执行 eval
插入进来的。
把这一段代码丢给 chatGPT
整理一下:
来一段一段看一下:
1 | var BASE_DEVICE_WIDTH = 750; |
主要更新了几个变量,deviceWidth
、deviceDPR
,像素相关的知识很久很久以前写过一篇文章 分辨率是什么?。
这里再补充一下,这里的 deviceWidth
是设备独立像素(逻辑像素),是操作系统为了方便开发者而提供的一种抽象。看一下开发者工具预设的设备:
如上图,以 iphone6
为例,宽度是 375
,事实上 iphone6
宽度的物理像素是 750
。
所以就有了 Dpr
的含义, iphone6
的 dpr
是 2
, 1px
相当于渲染在两个物理像素上。
1 | var eps = 1e-4; |
核心就是这一行 number = number / BASE_DEVICE_WIDTH * (newDeviceWidth || deviceWidth);
,其中 BASE_DEVICE_WIDTH
是 750
,也就是微信把屏幕宽度先强行规定为了 750
,先用用户设定的 rpx
值除以 750
算出一个比例,最后乘上设备的逻辑像素。
如果设备是 iphone6
,那么这里设备的逻辑像素就是 350
,所以如果是 2rpx
,2/750*375=1
最后算出来就是 1px
,实际上在 iphone6
渲染的是两个物理像素,也就是常常遇到的 1px
过粗的问题,解决方案可以参考这篇 前端移动端1px问题及解决方案。
接下来一行 number = Math.floor(number + eps);
是为了解决浮点数精度问题,比如除下来等于 3.9999999998
,实际上应该等于 4
,只是浮点数的问题导致没有算出来 4
,加个 eps
,然后向下 floor
去整,就可以正常得到 4
了,关于浮点数可以看 一直迷糊的浮点数。
接着往下看:
1 | if (number === 0) { |
在 transformRPX
函数整个代码里第一行 if (number === 0) return 0;
,number
等于 0
已经提前结束了,所以这里 number
得到 0
就是因为除的时候得到了一个小数。
如果 deviceDPR === 1
,说明逻辑像素和物理像素是一比一的,不可能展示半个像素,直接 return 1
。
如果不是 iOS
也直接返回 1
,这是因为安卓手机厂商众多,即使 deviceDPR
大于 1
,也不一定支持像素传小数,传小数可能导致变 0
或者变 1
,为了最大可能的保证兼容性,就直接返回 1
。
对于苹果手机,据说是从 iOS 8
开始支持 0.5px
的,但没找到当时的官方说明:
因此上边的代码中,对于 deviceDPR
大于 1
,并且是苹果手机的就直接返回 0.5
了。
1 | setCssToHead( |
通过调用 setCssToHead
把上边传的数组拼接为最终的 css
。
核心逻辑就是循环上边的数组,如果数组元素是字符串直接相加就好,如果是数组 [1]
、[0, 50]
这样,需要特殊处理下:
核心逻辑是 makeup
函数:
1 | function makeup(file, opt) { |
如果遇到 content
是 [1]
,也就是 op
等于 1
,添加一个前缀 res = opt.suffix + res;
。
如果遇到 content
是 [0, 50]
,也就是 op
等于 0
,这里的 50
其实就是用户写的 50rpx
的 50
,因此需要调用 transformRPX
将 50
转为 px
再相加 res = transformRPX(content[1], opt.deviceWidth) + 'px' + res;
。
通过 makeup
函数,生成 css
字符串后,剩下的工作就是生成一个 style
标签插入到 head
中了。
1 | ... |
这里贴一下注入的全部代码:
1 | var BASE_DEVICE_WIDTH = 750; |
剩下一个问题,我们写的代码是:
1 | .container { |
但上边分析的 <script>
生成 css
的数组是哪里来的:
1 | [ |
是微信帮我们把 wxss
进行了编译,编译工具可以在微信开发者工具目录搜索 wcsc
,Library
是个隐藏目录。
我们把这个 wcsc
文件拷贝到 index.wxss
的所在目录,然后将我们的 wxss
手动编译一下:
1 | ./wcsc -js ./index.wxss >> wxss.js |
此时会发现生成的 wxss.js
就是我们上边分析的全部代码了:
因此对于代码 wxss
到显示到页面中就是三步了,第一步是编译为 js
,第二步将 js
通过 eval
注入到页面,第三步就是 js
执行过程中把 rpx
转为 px
,并且把 css
注入到 style
标签中。
趁着对周六分享还有些记忆,就简单写一写周六的分享和感触吧,中间会混着我的一些观点,主要是听到分享后想到的,大家也不要误解。
一位谷歌的大佬,分享的非常幽默,主要介绍了 Chrome 的 devTools,调试相关的东西。
更新了一个认识: Source Maps 规格竟然停留在了 2011,但过去十年前端发生了太多的变化,导致 SourceMap 目前很 flexible, 目前 Chrome 也在与 TC39 合作,强化规范并改善调试体验。
介绍了很多调试 Tips,印象比较深的两个:
前段时间知道了 Chrome 可以覆盖相应内容,意味着 Mock 再也不需要第三方插件了。
会上还展示了 Override headers,通过编辑 headers 也可以直接解决跨域问题,这是之前没有想到的。
另外还有个之前遇到的头疼的问题,如果想调试搜索框选中下拉元素的样式。
当切到 Element,搜索框下边的内容就会消失掉
此时找到 MoreTools,Rendering
开启 Emulate a focused page,此时下拉就出来了。
Jecelyn Yeen 也自嘲这么好的功能为什么大家都不知道,未来也会把这个功能放到外层。
最后,所有的功能其实都是有人在默默开发,遇到问题没有人反馈其实不会自己偷偷修复,Jecelyn Yeen 也提倡大家有问题可以及时报告。
分享了无缝迁移 Mocha 到 TS 的全过程,以及期间遇到的常见的问题。
突然发现是一个学 TS 不错的方法,github 找一个还没有 ts 的开源项目,然后去转 TS 来练习。
关于用不用 TS 一直争议不断,前段时间我也有写过一篇 关于 TS 的思考。
周爱民老师也分享了自己的看法:持续迭代的复杂项目,值得去转 TS。
换言之,如果一个简单项目,直接 js 就好了,用 TS 徒增复杂度。但对于复杂项目是不是也要转 TS 这里我觉得还是有争议的,对于框架类、工具类接入 TS 是毫无疑问的,比如 Vue3 就全面拥抱了 TS。
但对于复杂的业务项目,由于参与人数众多,很多结构也很依赖于后端,随着慢慢迭代,更多的 any 或者类型和实际用的数据渐渐不一致,就变成破窗效应,迭代需求时候就没有人再愿意维护旧类型了。
Vue2 将在年底彻底停止维护,分享了之前 Vue2 修复的安全漏洞,继续使用 Vue2 一个最大的问题就是安全问题。
但在国内这个问题不会引起重视的[旺柴],毕竟 XP 还有很多市场,我们团队内的项目甚至还是 Vue 2.5 的版本,对于业务团队,完全没有足够的理由去说升级这个事,风险远远大于收益。
之前升级项目的 ElementUI 的版本也是灰度了很久才完成,Vue 去升级想都不敢想,只能是新开的项目来使用 Vue3 了。
后边具体的分享了 Vue2/3 同时维护的方案,一种是一套源代码编译成 Vue2/3 两套代码,另一种是直接写两套代码,都一定程度上会多增加一些工作量,目前看来还是没有一种完美的方案来让 Vue2/3 共存。
磊哥分享很硬核,直接上一页又一页的代码,人也自带搞笑属性,哈哈。主要介绍了 Omi 的一些思路,Web components + 信号驱动的响应式编程,响应式编程还就是 Vue 那套,感兴趣的同学也可以看我之前总结的 vue.windliang.wang/。
分享了对于 Web Components 遇到的一些问题,还有许久没有操作的 dom,介绍了一个有用的 API TreeWalker,解决 Shadow Dom 样式被隔离的问题,全面拥抱 TailWind css,解决取名问题,同时避免各个 class 相互影响,实现组件内聚到 HTML 标签上。
我突然产生了一种感觉,随着 Web component 的完善,更多的框架出现,未来会不会又回到原生标签的开发中,哈哈。
最重要的就是 ALL IN AI,生成式 AI 势不可挡,下午也有更多的分享嘉宾带来了 AI 方面的思考。
主要分享了 AR / XR 方面的知识,自己之前也没接触过这方面知识,同时分享了未来的畅享。分享了给他儿子写的一个奥特曼 AR 应用,你相信光吗,看着心潮澎湃,哈哈。
另外提到目前网页都是二维或者三维的渲染,期待未来实现二维和三维的混合渲染,同时三维也尽可能的接近目前 html/css 的开发模式,目前已经有个框架在往这方面发展,还可以直接在 VSCode 上调试,但框架名我忘记了。
此外,AR/XR 的未来,完全看苹果的表现了,虽然已经发展了很多年,那依然还是没能像手机一样普及。如果苹果的 Vision Pro 继续发展,电影中的场景也会很快实现,明年也许是真正的 AR 元年。
对于前端来说,未来又多了一批职业。
演讲的时候没有感受到,吃饭的时候发现一丝姐姐好活泼,哈哈。主要介绍了 SVG 的渲染引擎,底层是基于 Rust ,上次进行封装,提供操作 SVG 的方法,浏览器通过 WebAssebmy 进行调用。
也都是自己知识外的知识,但只记得一点就行了,如果要高性能的渲染 SVG,去看 resvg-js 就好了,哈哈。
介绍了创业项目 AirCode,现场演示了目前的一些功能。
生成式 AI 未来可能会引发新的交互形式,maybe 智能组件,比如一个时间选择组件,直接通过自然语言进行交互,「勾选中秋节假期」,而不是自己先去查中秋是啥时候,当然是指一个简单的设想,未来也说不准会怎么样,比如现在国外团队最近发布的 Ai Pin,直接挂到胸前,通过语音沟通,还能投影到手掌。
另一个点是,ai 有一个问题是不确定性。可能无法很好的遵从我们的指令,如果这个问题可以解决了,未来会有更大的想象力,甚至成为一门新的编程语言,而这门编程语言完全是我们的自然语言了。
好吧,涉及硬件的东西,完全没有看懂:
AI PC 以及 AI Mobile 的新兴时代已经到来,越来越多的设备集成了强大的神经处理单元 NPU,以实现高效的人工智能加速,这对需要端侧推理的应用至关重要。除了通过 CPU 和 GPU 进行推理之外,Web Neural Network API (WebNN) 提供了 Web 应用访问此类专有 AI 加速器 NPU 的途径,以获得卓越性能及更低功耗。
本演讲将会给大家分享 WebNN API 的 W3C 标准进度,对 CNN, Transformer 以及更广泛的生成式 AI (Generative AI) 模型的支持情况和计划,以及在 Chrome, Edge 等浏览器的实现进展。作为 JavaScript ML 框架的后端,WebNN 将会在几乎不更改前端代码的前提下,为 Web 开发者及他们的产品带来相较于 Wasm, WebGL 更为优异的性能体验。
大概就是提供专门的硬件 NPU,同时上层提供专门的 API 加速 AI 计算,为上层的应用服务,比如后边分享的 Transformer。
大佬很和蔼,吃饭的时候甚至就在旁边。分享的很实用,因为 Hax 在做 AI 相关的应用,底层是基于 GPT 的,很细节的分享了如何调用 GPT 和踩的一些坑。
这一次的生成式 AI 真的越来越像人,具备了人的特点。
之前听孟岩的播客也提到这个,GPT 是基于概率来逐次生成完整的对话,我们人其实也完全是这样,如果要说一大段话,基于周围的环境,前边说的,一点一点的完成整个对话,和 GPT 真的太像了。
另外,只要热爱编程,就忘掉 35 岁吧,贺老已经 45 了。
在本次演讲中,你将了解 Hugging Face 如何通过 Transformers.js 将最先进的机器学习带到 Web 中,同时我们将讨论 WebML 技术的优势,以及如何将其用于加速您的 Web 应用程序。最后,为了展示 Transformers.js 的潜力,我们将探索一些使用该库构建的引人注目的应用。
在 chatGPT 出现之前,这一定是非常惊艳的,可以在浏览器本地直接实现语音识别、翻译、图像识别等, Hugging Face 提供了众多训练好的专有模型,前端直接通过一行代码引入就可以直接使用,底层还是之前很火的深度学习模型。之前我也通过 Tensorflow.js 来直接在浏览器实现图像识别.
但当大模型兴起后,之前的深度学习就略显尴尬了,毕竟在各个方面大模型都可以吊打各个深度学习的专有模型了。但在浏览器直接跑模型好处也是有的,那就是隐私,由于是完全本地调用 AI 模型,无需通过网络,隐私完全不会暴露,同时速度也很快。
未来发展方向就是将大模型也能内置到浏览器,但还需要继续发展。
随着抖音电商业务高速增长,商家端在有高复用要求的背景下,选用了 Electron + Web 构建跨平台、快交付的高效率方案,但也面对着不小的性能及稳定性压力。我们通过 Rust 在抖音商家端落地,为高性能挑战的业务(如IM)提供了底层能力支持,抬高原有架构性能天花板,并保持跨平台应对未来变化。
本次分享我们将揭示抖音商家端Rust 实战的精华内容,你将了解到:
- Electron架构面临什么样的性能和稳定性挑战? 选型上与其他方案有什么样的考虑?
- 为什么选择Rust语言? 如何设计 Rust SDK 架构实现跨平台
- Rust 怎么在 Electron 内跑起来?与web如何交互?
- 提升IM的稳定性,抬高性能天花板,我们拿到了什么样的收益?
Rust 一直如火如荼,但一直没有近距离的接触过。七桑用代码演示了使用的过程,同时介绍了在抖音电商的落地。未来相关基建一定会用 Rust 来重新了,但不知道自己啥时候会遇到这样的需求。
会议开头直接抛出了问题,后端是什么,前端又是什么?
确实很难定义清楚,狭义的讲,跑在服务器的就是后端,html/css/js 就是前端。
但随着前端的发展,Node.js、WebAssembly、Rust、React、Vue、RN、小程序的出现,前端的领域范围越来越大,一切都是前端,还有前端不能做的吗[旺柴]
Hux 也形象的用 O(n) -> O(1) 来比喻边际效应,从 Flash 到 H5 到移动端,当技术让边际成本足够低,相应的职业也就会消失。有老的技术的消失,但随之又会有新技术的出现,技术肯定是为人所服务的,只要有技术,我们就会在。
这也是我之前的想法,但 Hux 讲的更深入,更高大上一些,之前我写的详见 工作三年后的胡思乱想。
我们确实站在了 AI 变革的时间点,但不用担心被 AI 取代,历史的经验已经充分告诉我们,一个的消失一定会有新的出现,保持学习即可。
可惜这次还是没有见到尤大,到尤大时间已经比较紧了,视频也开始 1.25、1.5 倍速,因为是在电影院,后面一场电影马上要来人了。
尤大的分享确实很干,主要是关于 Vue2 到 Vue3 中犯的一些错误,业务迁移困难、相关基建迁移困难,但好在 Vue3 现在也蓬勃发展了,并且吸取了教训用在后续 Vite 迭代中。
讲了目前 Vite 开发采用 esbuild 很快,但线上打包采用更加灵活的 rollup,两者可能会带来差异,后续会开发 rolldown,吸取 esbuild 和 rollup 两者的优点,但估计是个漫长的过程了。
一天分享的内容很多很多,不停的超时,哈哈。上边就是自己印象深刻的一些点,后续 FEDay 的组织者也许会在 「前端圈」 把 ppt 都分享出来,大家感兴趣也可以去找找。
一天下来,最重要的大概就是 AI 了,之前一直说风口,Web1.0 时代、移动互联网时代,新的 ai 时代开启了,但我们可以做些什么呢?好吧,果然还是普通人,只能静静的等待 ai 时代的明星公司一个接着一个诞生了。
]]>按时间线来回忆下:
14 年进入大学接触编程,第一门编程语言 C++。
开始就是学基础语法,学循环、学递归,求绝对值、求阶乘、一元二次方程求解,到最后的学生管理系统。
txt 文件是类似下边的数据:
写出的程序就是命令行之间的交互,程序把 txt 读入,然后进行增删改查的操作。
最开始底层是用一个大数组保存的,当时正好在学链表,自己就想着干脆再用链表重写一下吧,然后花了几天时间将整个代码进行了重写,未来遇到链表的问题都变得轻轻松松了。
大一寒假回家闲来无事就在网上找了些课程,学了郝斌的数据构课程,但当时应该是懵懵懂懂,只了解了大概。
大一下学期接触了 GUI,也就是有界面的应用,学校的课程是 MFC。
课程末,把大一上写的黑框的学生管理系统改成了有界面的。
接着暑假开始的时候没有直接回家,学校当时有 ACM 集训第一个月留校了,但当时基础太差,如听天书,最终也没走 ACM 的道路。
但期间因为有了 MFC 的知识,自己又尝试做了一个双人版的贪吃蛇。完全从零自己开始写, 画蛇身,动起来,操控,一步一步最后完成的。
并且实现了局域网对战,对网络有了初步的了解。
用c语言可以实现多人在线游戏吗?100 赞同 · 8 评论回答
大一期间还接触过其他事情:
期间接触到 @萧井陌的 Badger4us:编程入门指南 v2.0 ,陆陆续续开始看里边的 python 课(未来写过很多 python)、哈佛大学的 cs50 课(了解到很多概念,对 scratch 也印象深刻,未来也专门又用了一次)、SICP(神书,也学到了 lisp 语言)。这篇文章对自己帮助很大,每当迷茫的时候就会去读读。
期间联系了一位老师,从大一寒假开始陆陆续续看论文、学 MATLAB、学算法,详细的故事可以看 有一些超级难的算法比如遗传算法,蚁群算法,看了数学建模国赛感觉好难写,那些人怎么写出来的?
学校课程多了数据结构,对链表、树、图有了更多的了解,期间有个小作业继续利用 MFC 进行可视化。
当时学校查成绩只能到教务网站自己去查,没有自己的官方 app,于是就萌发了自己去写一个 app 的想法,开始一步步践行。
寒假的时候开始学习 java 语言:
学习 Java 之后才对面向过程编程和面向对象有了更多的理解。
继续学习 java,开始写简单的安卓应用,之前学数据结构的时候做过无界面的计算器,这里结合安卓就做了一个有界面的计算器。
掌握基本安卓开发后,如果做查成绩的 app 肯定还需要数据,于是又捡起之前的 python,学习爬虫,了解网络知识、html 解析,最终成功查出来。
这个之后,被学校的一个互联网社团看到了,于是有了联系,他们也有做学校 app 的想法,于是从独自作战变成了团队合作。
当时的学校网络需要连好之后进行手动登录,于是又写了一个 app
暑假第一个月继续留校学习,借了几本安卓的书开始学习
在团队里有了更多的事情,由于当时 python 比较熟,又写了一个接口用来在线充值饭卡,之前学校充值饭卡只能线下充。
学校课程里也陆陆续续接触一些底层的计算机知识,操作系统、计算机组成原理、汇编语言、数字逻辑这些。
比如电脑上模拟 8086 CPU 做的一个东西:
17 年生日的时候搭建了自己的第一个博客,https://windliang.wang ,了解了域名、服务器、git 各种概念,后边陆陆续续就开始总结文章了。
大三下一开学,app 正式上线,支持查课表、查成绩、充值饭卡:
经常需要上自习,但每天的空教室是在楼下黑板手写公布的,但其实到教务网站是可以查出来的。于是注册了公众号 windliang,实现了一个查询空教室的功能。
再接着学习了前端 html、css、js,写了一个棋类对战游戏(从这里开始和前端结缘),也作为了软件工程的结课作业。
由于毕业设计和深度学习有关,也开始总结深度学习的知识。零基础入门深度学习
开始陆陆续续刷题,并且总结题解,https://leetcode.wang
除了上边列的,陆陆续续还做过很多小东西,就是那种突然有个想法就去实现,不会就去一点点学。
之后毕业又读了研,但通过大学四年基本上拥有了基础的编程能力,无论学什么新语言,用什么新框架上手都会很快。
接着就是毕业后的故事,前段时间刚满三周年:windliang:工作三年后的胡思乱想
总结下来,入门编程最核心的就是去多写代码了,最好先定个目标,想要实现什么,然后基于此去学习相关知识,不断攻破。
但对于初学者,每当接触一个新知识点的时候还是很痛苦的,只能不停的拆解目标,一步一个脚印来攻克。
从不会到会,这个过程不断循环,每次有成果出来都会非常开心,随着这个过程不断扩充自己边界,到后来再学新知识就不会那么抗拒了,迁移学习的能力越来越强。
]]>前端存在知识点杂多、技术迭代快的特点,对于初学者或者非前端开发者往往会一脸懵逼。
这个系列会逐个介绍前端各个知识点,最后再详细介绍「课程减减」这个网站如何从零开始开发,包含前后端的开发,以及最后部署上线,预览地址 https://coursesub.top/
前端整体结构可以理解为上边的图,底层的硬件、操作系统部分我们不关心,谷歌开源了 V8 引擎,它可以运行 js 语言,基于此又有了 Chrome 浏览器和 Node.js。
课程会详细介绍各个知识点的来龙去脉,目录如下:
前端初学者:简单学习了 html、css、js,对其他概念还不太了解,也没有独立开发项目的经验。
其他开发人员:不管是后端、算法、测开等,只要有过编程经验,都可以轻松地跟上课程。
会了解前端的整体架构,各个部分的作用,跟着教程可以搭建出「课程减减」这个网站。
包含前后端整个开发过程的详细介绍,以及最后通过 nginx 将网站部署上线。
整个课程下来,会对前端有一个大体的认知,未来想写其他的网站也不再迷茫,该干什么, 需要做什么都做到心中有数。
安卓用户进这里 安卓用户购买 点击文章下方「合集详情」购买即可,苹果用户看这里 ios 用户购买 。
购买后添加下边微信可以领取返现红包,具体返现红包以 https://coursesub.top/ 这里展示的为准,目前返现 20 元
]]>加深了两点收获:
上次详细看 ts 还是写斐波那契的时候,用 TypeScript 实现斐波那契数列
混了 Anthony Fu ts 类型体操项目的一道题:
https://github.com/type-challenges/type-challenges
正因为 ts 是一门独立的语言,所以可以用 ts 实现菲波那切数列、实现中国象棋,甚至实现编译器,也就有了上边的类型体操项目。
我 ts 平常用的比较少,猜测有下边的原因:
大家对 ts 的了解深浅不一,ts 说简单也简单,说复杂也复杂,平常快速的业务迭代中很少有时间说专门去刷一遍 ts。
第一次开发的时候会多花时间。 定一个接口需要写类型,函数参数需要写类型,如果参数是对象套对象再套对象,那简直要疯掉。
未来迭代也花时间。后端接口有变动,除了改逻辑代码,还要再把相应的类型也都改了。
写逻辑的时候会被限制,经常遇到动态修改变量的情况,如果有 ts 还需要多考虑下。
收益不明显,如果说为了类型提示,编辑器通过插件一定程度上也可以。如果为了减少 bug,但用 js 写也很少因为类型问题出现 bug,基本上都是逻辑问题。
一般都会通过问号操作符或者 || 操作进行兜底,因为影响范围太广了,一不小心直接影响几千万用户,所以写的时候都会很谨慎,会考虑各种极端情况。比如之前总结的 提升前端开发质量的十点经验沉淀。
当然以上仅我自己的看法,ts 这么火也肯定是有原因的,等未来 ts 写的多了再来补充它的好处。
现在项目达成的共识是新建文件的时候建成 ts,至于里边代码多少用 ts 就不做太多限制了,anyScript 由此诞生 \狗头。
]]>确实是一个很有争议的问题,团队里也经常讨论这个问题,下边分享下我的想法,也不一定是最佳实践。
首先,不要修改 prop 的值肯定是一条比较好的实践,保证数据的流向明确。
官方文档中也有明确指出:
All props form a one-way-down binding between the child property and the parent one: when the parent property updates, it will flow down to the child, but not the other way around. This prevents child components from accidentally mutating the parent’s state, which can make your app’s data flow harder to understand.
In addition, every time the parent component is updated, all props in the child component will be refreshed with the latest value. This means you should not attempt to mutate a prop inside a child component. If you do, Vue will warn you in the console:
1 | export default { |
There are usually two cases where it’s tempting to mutate a prop:
1.The prop is used to pass in an initial value; the child component wants to use it as a local data property afterwards. In this case, it’s best to define a local data property that uses the prop as its initial value:
1 | // Vue3 |
2. The prop is passed in as a raw value that needs to be transformed. In this case, it’s best to define a computed property using the prop’s value:
1 | // Vue3 |
为了避免修改 prop 的值,可以在 data 中初始化为 prop 的值然后再去使用或者定义 computed 属性拿到 prop 值再去使用。
当然,上边的写法也仅仅对原始值生效,如果 props 定义成一个 Array 或者 Object,如果把 Object 的值直接赋值给 data:
1 | props: ['initialCounterObj'], |
当去修改 counterObj
中的值,虽然看起来没有修改 props 的值,但因为 Object 传递进来的是引用,修改 counterObj
的值的时候外部的相应的对象也跟着修改了。
针对这种情况,可以将 Object 摊开,变为一个个原始值。
父组件
1 | <child field1.sync="obj.field1" field2.sync="obj.field2"></child> |
子组件
1 | export default { |
父组件
1 | <child v-model="obj"></child> |
子组件
1 | export default { |
另外一种更暴力的写法就是题主讲到的方案三 「不可变对象型」,每次修改前都把整个对象(或数组)克隆一遍,修改新的对象,再通过 emit 事件把新的对象(这里最好也再克隆一下)传出去。
上边的方案都可以保证不去修改 props 的值。
看下官方对于 props 是 Object/Array 的态度:
When objects and arrays are passed as props, while the child component cannot mutate the prop binding, it will be able to mutate the object or array’s nested properties. This is because in JavaScript objects and arrays are passed by reference, and it is unreasonably expensive for Vue to prevent such mutations.
The main drawback of such mutations is that it allows the child component to affect parent state in a way that isn’t obvious to the parent component, potentially making it more difficult to reason about the data flow in the future. As a best practice, you should avoid such mutations unless the parent and child are tightly coupled by design. In most cases, the child should emit an event to let the parent perform the mutation.
关键句:you should avoid such mutations unless the parent and child are tightly coupled by design.
因此对于表单场景,我认为符合 parent and child are tightly coupled by design ,很多时候由于表单越来越大,一个 Vue 文件会变得巨大,此时想要拆部分表单出来成为一个组件,这种情况下采用题主所说的方案一「直接修改型」我认为是更佳的,不然的话不管采用什么方式保证不修改 props 都会增加很多代码,反而增加了很多理解成本。
更进一步,对于 Object/Array,是否修改 props 取决于当前组件的通用性,如果这个组件专门为了某个父组件使用或者专门服务于某个页面,并且为了不修改 props 会增加很多工作量,这种情况下直接修改 props 我认为是合适的。
但如果这个组件可能用给其他人,此时修改 props ,如果使用方不清楚的话就可能引发问题。
]]>经常说这是最好的时代,也是最坏的时代,互联网便是如此。通过互联网将人与人之间的各种链接都成为了可能,在互联网诞生之前,人与人之间的交流就是现实生活中的圈子,而现在本来这一辈子都不会在现实中产生交集的人在互联网却会相遇。
各种写书的大佬、开源的大佬,以往可能只是从文字、代码中了解他们,但现在通过社交媒体、微信竟然就产生了互动。当然不好一面就是也会遇到和自己不相投的人,也许会影响自己的心情。
通过互联网极大的扩宽了我们的视野,看到了别人在怎么生活,也放大了自己的焦虑和欲望。我们需要认清自己的边界,知道自己想要什么,自己能做什么,不需要对本来不可能发生在自己身上的事情而焦虑。
当迷茫焦虑时,看看宇宙的纪录片,从宇宙的视角去看自己,无论从空间大小还是时间维度,其实自己什么都不是,想那么多干啥。
再想想其他动物,吃饭睡觉喵喵叫,也挺好的。
互联网已经结束了快速扩张的时期,这是个客观事实,因此招聘的人数相对于之前减少了很多,但远没到一个已死的状态,相对于其他行业,选择互联网依旧是一个不错的选择。
前端会不会死不知道,互联网肯定会一直存在下去,现在整个社会都是基于互联网,已经变成了像电、水一样的基础设施,没有人可以离开它。因此互联网的相关的岗位一定会一直一直存在。
至于互联网中具体的职业划分,前端、后端、算法、数据库等,它们各自使用的语言、技术一定会发生变化的,当选择互联网技术行业的时候,就应该抱有持续学习的态度。
塞班操作系统被安卓、iOS 取代、.Net 岗位的减少、客户端大量岗位转前端,这些也就发生在近十几二十年。当某一个岗位减少的时候,一定又会出现新的岗位,保持开放的心态去学就可以,变化再多肯定也有不变的东西。当掌握一门技术再学习另一门技术的时候,肯定会比小白学习一门新技术快很多很多,很多经验也会迁移过去。
去年 12 月出来的 chatGPT 为代表的大模型,到现在也就半年多的时间,很多以前完全不敢想的事情就这样发生了。可以预见的是一部分岗位数量肯定也会减少,目前影响最大的应该是 UI 岗,其次一定程度上可以提高程序员的开发以及学习效率,但还没有到取代的程度,但未来会再怎么发展就不得而知了。
相对于其他行业,虽然互联网相关技术迭代确实很快,但如果是因为热爱而选择这个行业,我觉得去做一辈子是没问题的。
底层技术服务于上层技术,上层技术服务于应用,真正赚钱的是应用,它可能提升了用户的效率、也可能提升了用户的生活体验,这样用户才愿意付费。上层技术的人收到了钱,进一步也愿意为底层技术的人付费。
但对于一个应用,技术并不是最重要的,更多需要的是产品和运营,一个应用在 chatGPT 和各种框架、云服务的加持下做出来变得太简单了,更多的是我们需要思考如何设计产品和如何推广运营产品,和用户产生更亲密的连接,用户才愿意付费。
极端一点,即使现在所有的应用都停止更新了,其实也并不会产生多大的影响。
在公司中亦是如此,对于技术开发,没有谁是不可取代的,公司更期望的是那些可以发现问题、分析问题、定义问题的人,至于怎么解决,问题定义清楚以后,解决方案自然可以出来,谁去解决并不重要了。
但也不用太过悲观,虽然技术不是最重要的,但一定是不可或缺的,在解决问题的过程中也会区分出能力强和能力差的:方案的设定、代码编写的好坏、线上的 bug 数、代码的扩展性等。
赚钱很大程度又是需要运气的,比如同一个人十年前进入互联网和现在进入互联网差别就会很大,再比如开发一个应用突然爆火,例如「羊了个羊」,这些我们是很难控制的,我们只能「尽人事,听天命」。
最近几年,除了在公司工作,对于有技术的同学赚钱有下边的方式:
付费课程、出书
最近几年越来越多的人在极客时间、掘金小册写课程或者直接出书。
对于写课的人赚到了钱,对于买课的人只要跟着看完了,多多少少都会有很多收获。付费课程会比较系统, 如果没有这些课程,去学东西肯定也是可以学的,但需要花很多时间去网上搜一些零碎的资料,由于没有经验甚至可能走很多弯路。
付费社群
市面上也会有一些付费训练的社群或者知识星球
对于组织付费社群的人会花费很大的精力,需要持续运营并且照顾到每一个人,不然就等着挨骂吧。因此这类收益也会很高,一些人会辞去工作专职来搞。
开源
大部分开源基本上是用爱发电,更多是收获一些朋友、流量、提升技术。
比如 core-js 作者的经历,一个 22.6k star 的项目,几乎各个网站都在用的一个项目,作者却因为钱的问题被很多人谩骂。因此如果是个人专职开源一个项目靠 GitHub Sponsor 会很难很难。
当然,开源也是能赚到钱的,比如 Vue 开源就赚到了很多钱,但毕竟是很少很少数了。
依赖纯开源项目赚到钱,还是需要背靠公司。比如阿里云谦的 Umi、通过开源加入 NuxtLab 的 Anthony Fu、在 AFFiNE 的雪碧等等。
应用
身为一个程序员,尤其是前端程序员,当然可以自己维护一个应用来赚钱。
做得很成功的比如 Livid 的 V2ex 社区,Abner Lee 的 Typora(后来知道作者竟然是国内开发者)。
也有一些没有那么出名的,比如大鹏的 mdnice,秋风的 木及简历。
当然如果要做一个很大的项目,背靠公司也是一个很好的选择,比如之前阿里玉伯的语雀、之前极客邦池建强的极客时间。
还有一些小的创业公司会做的,冯大辉的「抽奖助手」、吴鲁加的「知识星球」等。
做出这些应用不需要很多时间,需要我们善于发现生活中的痛点以及强大的执行力,当然想成功的话需要再加一点运气,在成功前需要不断尝试不同的东西。
流量变现
有流量就会赚钱,不管是接广告、还是带货。互联网上也会有部分人专注于怎么搞流量,知乎怎么获得更多曝光、视频号怎么获得更多流量、怎么批量注册号,各个平台规则可能是什么,怎么对抗规则,这类有技术加持也会更加顺利,很多人也在专职做。
赚钱的方式有很多,对于我来说,我会尽量选择复利的事情,这样才能产生更大的价值。比如一对一咨询,一份时间换一份收入。但如果把东西写成课程,只需要花一份的时间就能获得 N 份的收入。
另外就是需要保持分享,分享除了能帮助其他人,对自己也会有很大的帮助,写文章的过程中也会不断的有新的认知得到。虽然当下可能没有金钱方面的收入,但时间放宽到几十年,相信一定会有很大的回报。
人的欲望是无穷的,也不能陷入赚钱的极端,目标应该是关注此刻,体验生活,享受生活,而不是不停的赚钱。之前听播客,有一个恰当的比喻,钱就好比汽油,不停的赚钱相当于不停的加油,但如果汽车停着一直不动,再多的汽油也是无意义的。
最近几年总是爆出程序员突然离世的新闻,前段时间耗子叔突然离世的消息听到之后真的很震惊。twitter 经常刷到耗子叔的动态,然后突然一天竟然就戛然而止了,毫无征兆。
意外是无法避免的,只能尽可能的从饮食、作息、锻炼三方面降低生病的风险。
我是工作第一年体检的时候检查出了中度脂肪肝、尿酸高,当时因为是刚毕业,体重是我的巅峰,140 多斤,脂肪都堆在了肚子上。那段时间就开始跑步加吃沙拉,少吃米饭、面条。降的也快,几个月就回到了 130 斤以下,甚至到 120 多点。
第二年体检的时候,脂肪肝基本没有了,尿酸也降了许多。
后来就保持少吃米饭,多吃蛋白质、蔬菜的饮食了。
有一次得了带状疱疹,那种非常痛的类似于痘痘的东西,后来了解了一下是因为免疫力低导致病毒入侵的。猜测因为晚上坐在电脑前,气温降低了没注意,从而导致了生病。
病好之后就决心养成早睡早起的习惯。
之前作息基本上是 1 点到 2 点睡觉,9 点前后起床。现在基本上保持在 11 点前后睡觉,6 点到 7 点间起床了。
早起的好处就是早上会有大把的时间,而且这段时间是专属于自己的,并且因为大脑刚苏醒,效率也会很高。但如果是工作一天,晚上回家再做自己的事情,此时大脑已经很疲惫了,效率会比较低。
最开始是跑步,但确实很难坚持下去,跑步需要换衣服、出门,还依赖于外边的天气,成本很高。后来陆续尝试过 keep、一些付费课程,都做了但没有完全养成习惯。
后来知道了 switch 的健身环大冒险,然后就一路坚持到了现在,前段时间已经通关了。
目前也一直在坚持,基本上一周会运动三到四次,一次大概花费 50 分钟左右。
大学的时候开始接触到理财,知道了基金的概念,看了银行螺丝钉的「指数基金定投指南」,也看了「穷爸爸富爸爸」、「小狗钱钱」这类理财入门的书。当时赚到的一些钱,就跟着银行螺丝钉投了,主要是一些宽基和中概、医疗。
一直到工作的第一年,基金收入确实不错,甚至赚了百分之四五十。当时想着原来股市这么简单,这咋还能亏钱了。
接着疫情不断发展,还有外部经济的变化,中概、医疗都大跌,当时发了年终奖还不停的补仓中概,到现在亏损也有百分之三四十了。
但我心态是可以的,一切都是浮亏和浮盈,只要不卖一切都是浮云。
经历了大起大落后吸取了一些教训,那就是一定要严格执行计划,现金流多不一定要立刻全部投入,而是按计划定投,因为没人知道会跌多久,只有有充足的现金流,才能够把亏损逐步拉平。
现在国家规定互联网基金这些必须走「投顾」,也就是主理人帮我们买入、卖出,我们只需要交一定的投顾费即可。目前我都是在雪球上投,跟投的有孟岩的「长钱账户」、alex 的「全球精选」、螺丝钉的指数增强和主动优选。
能设置自动跟投的就自动跟投了,我相信专业的事交给专业的人肯定是没问题的。
投资肯定是财富自由不了的,但一定比把钱放余额宝强一些,只要耐心持有,尤其是目前这样的熊市投入,相信到下一个牛市会有不错的回报。
(以上仅个人看法,股市有风险,入市需谨慎)
如果开始接触理财,除了投资,一个绕不过去的点就是保险。
对于保险是什么的比喻,之前听薛兆丰的课时候印象深刻。
我现在还年轻力壮,将来年纪大了可能会生病,为了防止以后生病要花一大笔医药费,今天就开始存钱,每个月拿出 10% 的收入存起来,未雨绸缪。这是一种做法。
另外一种做法,是我每个月也拿出 10% 的收入去买保险。
这两种做法有什么区别呢?
区别在于,如果我是用储蓄来未雨绸缪,那么未来可能就会发生两种不同的情形。
如果我将来年纪大了也没生病,我存的钱就还是我的钱,我不需要花出去,这时候我还是很幸运的,能够保有我原来的收入,这份储蓄没有被花掉,我赚了。
但是如果我运气不好,生病了,这份储蓄就会被用掉,甚至需要借很多钱去治病,生活会发生巨大的变化。
所以通过储蓄来未雨绸缪,它的特点是未来的结局是可变的,是变动的、是带有风险的。要么高、要么低,要么能够保有原来的这份储蓄,要么这份储蓄就被用掉了甚至借更多的钱。
而对于保险来说,如果你没病,那你的生活该怎么样还是怎么样。如果你病了,那会有保险公司给你支付一大笔钱,你也不用和别人借钱,病好后继续该干啥干啥。
因此存钱去防止生病就有赌的成分了,如果没病就白赚了很多钱,如果病了生活质量可能会发生很大的变化。
而保险就可以降低风险,未来即使生病了,由于看病不需要花钱了,病好后生活质量也尽可能的维持在原来轨道 。
我期望未来肯定是尽量稳定的,所以在不影响当前生活质量的条件下我愿意拿出一部分钱来买保险。原计划我可能会 30 岁以后开始买重疾险,之前女朋友的朋友有推荐保险的,然后就跟女朋友一起配置了重疾险。
选保险一定要慎重,一些看起来很划算的保险, 到理赔的时候可能会推三阻四,甚至理赔前公司破产了,尽量要选择大公司。
当然生活没有标准答案,每个人看到世界也都是不同的,我也一直在成长,一直在认识新的东西,上边的所想的也不能保证说未来不会再变。
未来能做的就是多看看书,不限制自己,看看经济学的、哲学的、心理学的、人文的,多出去走走看看,尽可能多的增加人生体验,去认识世界,认识自己,做自己想做的事,爱自己所爱的人,走下去就好了。
]]>正常情况下,页面元素是从左到右和从上到下渲染(x、y 维度),但因为 margin 可以写负值,还有一些定位相关的 css 属性(absolute、relative、fixed、stick),这就会导致元素之间可能重叠,重叠后就需要判断元素堆叠顺序,这就涉及到层叠上下文(Stacking context)了,相当于增加了 z 轴的维度。
我们先抛开层叠上下文的概念,看一下没有 z-index 或者其他特殊 css 属性正常情况下元素的堆叠规则。
按照元素出现的顺序依次堆叠下边的元素:
一句话总结就是同类型的后出现的覆盖先出现的,定位元素覆盖非定位元素。
1 |
|
static 的背景看成 block 元素,文字看成 inline 元素。先堆叠 block 元素,再堆叠 float 元素,再堆叠 inline 元素,最后堆叠定位元素。
static2 的背景遮盖了 static1 的背景,但没有遮盖住 static1 的文字。
float 元素遮盖了 static2 的背景。
static2 的文字遮挡了 static1 的文字,因为 float 元素在 inline 元素之前进行了堆叠,所以 static2 的文字也遮盖了 float 的文字。
relative 元素最后堆叠,直接遮盖了 static1 的背景和文字。
考虑一下有新增的层叠上下文的情况。
层叠上下文可以理解成一张画布,可以在上边独立地一层一层的刷染料。不同的层叠上下文就是不同的画布,他们之间互相独立。而且层叠上下文中也可以在再形成新的层叠上下文。
<html>
).position
value absolute
or relative
and z-index
value other than auto
.position
value fixed
or sticky
(sticky for all mobile browsers, but not older desktop browsers).container-type
value size
or inline-size
set, intended for container queries.z-index
value other than auto
.grid
container, with z-index
value other than auto
.opacity
value less than 1
(See the specification for opacity).mix-blend-mode
value other than normal
.none
:isolation
value isolate
.will-change
value specifying any property that would create a stacking context on non-initial value (see this post).contain
value of layout
, or paint
, or a composite value that includes either of them (i.e. contain: strict
, contain: content
).总结下常用的:
html 元素。
position 为 absolute 或者 relative,并且 z-index 不是 auto。
position 为 fixed,无需设置 z-index 的值。
flex 的子项,并且 z-index 不是 auto。
opacity 设置为小于 1。
上边的这些情况都会生成一个层叠上下文,在自己的层叠上下文内进行一层一层的渲染。
同一个层叠上下文内元素的堆叠就是之前讨论的无新增层叠上下文的情况(之前的情况其实就是只有一个层叠上下文,即 html 元素自己生成了一个层叠上下文)。
同一层叠上下文中,层叠上下文之间堆叠顺序如下:
一个层叠上下文中可以一直嵌套的生成新的层叠上下文,如果要比较不同的层叠上下文下元素的层级关系,首先需要找到当前元素所在的层叠上下文(它所在的层叠上下文又在另一个层叠上下文之中,一直向上找,直到找到从它们共同层叠上下(比如 html 元素)中生成的那个层叠上下文),接着按照堆叠规则比较它们所在的层叠上下文关系即可。
看一个经典的例子:
1 |
|
首先观察除了 html 元素有没有新的层叠上下文。
有一个新生成的层叠上下文:Red 因为设置了 z-index = 1,并且是 absolute 定位,所以生成了层叠上下文,Red 会高于其他元素。
green 和 blue 都是非定位元素,按照出现顺序,blue 覆盖 green。
所以从底层到上边的顺序就是绿色、蓝色、红色。
下边思考一下如果修改代码,并且在下边的限制条件下,让红色到最底层:
如果直接知道答案了,那层叠关系应该是学透了。
答案就是给 div 加一个透明度:
1 | div { |
我们重新分析一下:
1 |
|
现在相当于有五个层叠上下文:
html(初始的一个层叠上下文)
比较 Red、Green、Blue 的层叠顺序,就是比较三者所在的层叠上下文,即各自所在的 div,三个 div 都是通过 opacity 生成的层叠上下文,所以它们层叠顺序就是出现的顺序,从底部到顶层就是 Red、Green、Blue。
即使 Green 和 Blue 本身没有生成层叠上下文,但因为它们所在的父元素的层叠上下文比较高,所以就把 Red 覆盖了。
再举个例子,因为比较的是所在的层叠上下文的顺序,因此平常开发中会遇到设置 z-index = 999(同时是定位元素了),也无法到最上层。原因就是它所在的层叠上下文比较低,类似于下边的情况。
还有一个神奇的现象:
1 |
|
回忆下之前说的堆叠顺序:
因为父元素和子元素都在同一个层叠上下文下,所以会先堆叠 z-index 为负值的元素,所以就形成了子元素穿越到父元素下边的情况。
如果我们让父元素也生成一个层叠上下文,上边的情况就不会发生了:
1 | .my-element { |
当父元素加了层叠上下文之后,父元素和子元素就不在同一层叠上下文中了。
父元素在根元素上。
子元素在父元素上。
总结一下:
判断元素之间的堆叠顺序,首先判断是否在同一层叠上下文中。
如果在同一堆叠上下文,就按照下边的顺序:
如果不在同一堆叠上下文,就找到元素所在的层叠上下文,并且要一直往上找层叠上下文,直到找到从它们共同层叠上下生成的那个层叠上下文:
按照下边的规则判断层叠上下文的顺序,层叠上下文的顺序就是要比较元素的堆叠顺序了:
能不设置 z-index 就不要去设置,设置请三思。
设置了 relative 或者 absolute 的元素会高于其他元素,因此这种情况下完全可以不设置 z-index,如果设置了 z-index 就会生成新的层叠上下文,可能会造成堆叠的混乱。
另外因为设置了 fixed 即使不设置 z-index 也会生成一个层叠上下文,因此 fixed 元素会高于其他所有的普通元素(定位元素和非定位元素)。但如果页面中有定位元素设置了正的 z-index,就不得不给 fixed 元素加一个更大 z-index 了。
当设置了一个 z-index 产生了层叠上下文后,需要考虑当前元素会不会成为别的元素的父元素,如果在多人合作中经常互相改代码或者引用组件,如果某个地方产生了层叠上下文,那子元素的层级就会受到该父元素的影响从而导致达不到想要的层级。
比如将一个弹窗组件放到了一个父元素中,父元素有层叠上下文,这样就会导致弹窗组件达不到自己想要的高度。
团队中一个项目过大之后,层级问题真的是防不胜防,也许可以做下边的事情来降低问题的发生:
因为层级和 z-index 的问题可能没详细去了解过,边开发边调试最后达到效果就好。所以最好可以先宣导一下,把层级的问题团队内完全对齐,降低问题的发生。
设计一套体系来管理 z-index。
常规的做法就是将所有的 z-index 定义为变量统一管理,并且规定范围,普通元素 1 - 100,弹窗 101 - 999 类似这样。
当有页面需要 z-index 时就去注册,命名的时候可以按页面、按组件范围进行区分,这样未来想知道某个页面有哪些 z-index 可以一目了然。
规则有了,但不遵守没啥用。
需要在 commit 以及打包流水线中进行强制卡控,如果发现 z-index 使用了数字就禁止提交 commit,如果强制用 -n 提交了,就在流水线中禁止打包。
对于老项目去推动上边的流程真的太难了,把所有的 z-index 去重新定义变量,对于大项目来说修改、回归工作量会很大很大,因此基本无望。
可以做点工具来尽量避免出现层级的问题:
比如页面的层叠上下文进行静态扫描,可以把层叠上下文的关系展示出来,这样如果需要新加层叠上下文,可以直观的知道会不会影响到别人。
再进一步,如果有全套的 Mock 数据,可以模拟出来所有层叠上下文都渲染时候,真实页面长什么样子,会更加直观。
以上思考都是理想情况下可以做的事情,现实状况可能会遇到小团队没必要推,大团队推不动的情况,哈哈。
]]>C++
写的各种代码的结果,当了解了 Node.js
代码或者 V8
代码再看这些问题真的就是降维打击(当然我只是有了这个感觉,还没细看过[旺柴])。但如果平常用不到,我们也没必要真的去看底层的代码,即使不了解底层代码,我们也可以根据具体的表现来自己定一些规则进行理解,只要根据这个规则来判断执行顺序是正确的,能指导平常开发也就足够了。
这篇文章只讲基本的概念,不进行深入,能够判断
setTimemout
、Promise
的执行顺序即可。
众所周知,JavaScript
单线程执行的,所以对于一些耗时的任务,我们可以将其丢入任务队列当中,这样一来,也就不会阻碍其他同步代码的执行。等到异步任务完成之后,再去进行相关逻辑的操作。
js
在主线程中执行的顺序:宏任务 -> 宏任务 -> 宏任务 …
在每一个宏任务中又可以产生微任务,当微任务全部执行结束后执行下一个宏任务。
【宏任务 [微任务]】 -> 【宏任务 [微任务]】-> 【宏任务 [微任务]】…
生成方法:
Ajax
、Fetch
、WebSocket
等方式)时,会产生宏任务来处理请求和响应。JavaScript
宿主环境提供的定时器函数(例如 setTimeout
、setInterval
)可以设置一定的时间后产生宏任务执行对应的回调函数。DOM
变化:当 DOM
元素发生变化时(例如节点的添加、删除、属性的修改等),会产生宏任务来更新页面。postMessage
实现)会产生宏任务来处理通信消息。JavaScript
脚本执行事件;比如页面引入的 script
就是一个宏任务。重点来看下 setTimeout
。
1 | setTimeout(() => { |
以上代码会输出什么?
什么都不会输出
上边代码相当于两个宏任务:
第一个宏任务就是上边的整个脚本
第二个宏任务是 setTimeout 传入的这个函数
1 | () => { |
第一个宏任务执行到 while true
的时候死循环了,所以自己的 console.log('end here')
不会执行。
第二个宏任务也没有机会执行到。
因此什么都不会输出。
再来看一个:
1 | const t1 = new Date() |
t1
记录开始的时间,设置一个 100
毫秒执行的定时器,定时器中输出执行当前任务的时间。
那么 console.log('t3 - t1 =', t3 - t1)
输出的是多少呢?
输出答案是 200
。
同样的,上边是两个宏任务。
整个脚本是第一个宏任务。
计时器生成了第二个宏任务。
只有第一个宏任务执行结束后才会执行第二个宏任务。
所以即使定时器时间到了也不会立刻执行,只有当第一个宏任务执行结束后才会去执行定时器的任务,此时已经过去了 200
毫秒。
生成方法:
Promise
:Promise
是一种异步编程的解决方案,它可以将异步操作封装成一个 Promise
对象,通过 then
方法注册回调函数,当 promise
变为 resolve
或者 reject
会将回调函数加入微任务队列中。MutationObserver
:MutationObserver
是一种可以观察 DOM
变化的 API
,通过监听 DOM
变化事件并注册回调函数,将回调函数加入微任务队列中。process.nextTick
:process.nextTick
是 Node.js
中的一个 API
,它可以将一个回调函数加入微任务队列中。重点看 Promise
的使用,关于 Promise
怎么用这里不细说了,重点放到输出顺序上。
1 | const r = new Promise(function(resolve, reject){ |
上边的输出什么:
比较基础的使用。输出 1 3 2
。
new Promise
接受一个函数,返回一个 Promise
对象。值得注意的一点是传给 Promise
的那个函数会直接执行。所以会先输出 1
。
Promise
对象拥有一个 then
方法来注册回调函数,当 promise reslove 或者 reject
后会将注册函数加到微任务队列。
上边的代码因为是直接 resolve
了,所以会将 () => console.log("2")
注册到微任务队列中。
宏任务执行完毕后开始执行微任务,所以最后输出 2
。
再看下 async
和 await
:
1 | async function method() { |
上边的会输出什么呢?
先输出 2
,再输出 1
。
这里需要明确一点,async
修饰的函数,相当于给当前函数包了一层 Promise。
所以
1 | function main() { |
相当于
1 | function main() { |
结合前边说的传给 Promise 的那个函数会直接执行。
所以先执行 resolve(method())
,进入method
内部:
接下来是 await
的作用:遇到 await
会先执行 await
右边的逻辑,执行完之后会暂停到这里。跳出当前函数去执行之前的代码。
所以 method()
方法中,
1 | async function method() { |
先执行了 method2
,当 method2
返回了 Promise
后就会暂定执行,跳回 main
函数。
1 | function main() { |
main
函数执行完毕后才会再回到 method
方法中。
所以先输出 2
,后输出 1
。
如果想要先输出 1 再输出 2 需要怎么改呢?
1 | async function method() { |
再看一个:
1 | async function method() { |
上边的会输出什么呢?
当 main
函数执行结束后,按照之前说的应该是回到 await
那里,所以应该输出3 2 1
吗?
其实是不对的,await
还有一个特性,它会把后边执行的代码整个注册为回调函数,相当于放到了 .then 里边,如果 Promise
直接 resolve
,相当于将后边的代码放到了微任务队列中。
所以
1 | async function method() { |
等价于:
1 | async function method() { |
在 await
之前已经有一个 Promise
把任务加到了微任务队列中。所以正确的输出顺序是 3 1 2
。
所以回到 await 继续执行其实是表象,本质上是从微任务队列中把之前要执行的代码取了出来继续执行。
如果想输出 3 2 1
,该怎么改代码呢?
可以将 new Promise((resolve) => resolve()).then(() => console.log(1));
这句中的 reslove()
函数延迟调用,通过 setTimeout
放到下一个宏任务中执行。
1 | async function method() { |
如果理解了上边的,下边的内容就简单了,首先明确几个点:
当宏任务和当前宏任务产生的微任务全部执行完毕后,才会执行下一个宏任务。每遇到生成的微任务就放到微任务队列中,当前宏任 务代码全部执行后开始执行微任务队列中的任务
new Promise
的函数会直接执行async
包装的函数相当于包了一层 Promise
,因此返回的一定是一个 Promise
await
,先执行 await
右边的东西,执行完后后会暂停在 await
这里,并且把后边的内容丢到 then
中(再结合第 5
点)。跳到外边接着执行。外边都执行完之后开始执行微任务队列promise
变为 resolve
或者reject
的时候才会将 then
中注册的回调函数加入微任务队列中 setTimeout
产生宏任务可以多读几遍下边开始正式练习,看代码的时候函数定义直接跳过,从执行函数开始看
来一道魔鬼题:
1 | async function method() { |
上边的代码输出什么?
分析的时候我们需要明确什么时候产生了宏任务,什么时候产生了微任务,什么时候是直接执行的,结合上边总结 6
句话和注释可以看一下:
1 | async function method() { |
当然上边的规则也不是黄金原则,归根到底还依赖于我们运行的环境是什么,现在 js
的运行时有 V8
、Node.js
等,它们也有各自的版本。
对于下边的代码:
1 | const p = Promise.resolve(); |
按照之前规则,先执行 await p
,因为 p
已经 resolve
了,所以会把后边的代码 console.log("after:await");
加入到微任务队列中。
接着又依次把 () => console.log("tick:a")
、() => console.log("tick:b")
加到微任务队列中。
所以输出是 after:await,tick:a, tick:b
。
在浏览器中运行符合我们的想法:
在 Node.js V16
中运行符合我们的想法:
但在 Node.js V10
中运行就些许不一样了:
至于为什么就是文章开头说的了,不管输出什么,其实就是其底层代码所决定的了。再具体的原因就需要去看 Node.js
相应的源码了。
当底层的逻辑影响到我们的业务逻辑的时候,可能就真的得去看这些源码和解决方案了。
]]>HTTPS
相关的东西吧。HTTPS
说白了就是加密传输信息,防止信息泄露,需要提前了解几个概念:
先说说最简单的加密,替换法,每个字符都对应到一个新的字符:
比如明文是 windliang
,通过上边的映射关系密文就是 pbgwebtgz
。
古代就使用过这种加密算法,但通过词频的分析,暴力枚举很容易被破解,因此现代已经不会用这种算法了。
不管什么加密算法,都可以分为明文,密文,和密钥、算法三部分。
这里的密钥就可以理解为上边的映射表,算法就是直接映射。
现代的加密算法,密钥一般就是一个字符串,算法就比较复杂了,会进行各种计算,或操作、与操作,分组等,然后再应用各种数学知识,质数、模相等… ,大学的时候有学过,这里也忘光了,下边只介绍简单概念了。
和古代的加密算法流程是一样的
只是其中的算法相对于简单的替换会更加复杂。常用的有 DES 算法、AES 算法、3DES 算法、TDEA 算法、Blowfish 算法、RC5 算法、IDEA 算法等。其特点是,加密和解密使用同一密钥。
与之前最大的不同之处是包含了两个密钥,一个称之为公钥,一个称之为私钥。并且算法相对于对称加密会更加复杂。
公钥和私钥都能进行加密,用公钥加密后只能用私钥解密,用私钥加密后只能用公钥解密。
常见非对称加密算法包括 DSA 算法、RSA 算法、Elgamal 算法、背包算法、Rabin 算法、D-H 算法、ECC 算法等。由于算法非常复杂,因此非对称加解密会非常耗时。
可以看做一种特殊的加密。
它是单向的,加密后无法再还原。可以将任意长度的明文串映射为较短的(通常是固定长度的)二进制串(Hash
值),并且不同的明文很难映射为相同的 Hash
值。
目前常见的 Hash
算法包括国际上的 Message Digest(MD)系列和 Secure Hash Algorithm(SHA)系列算法,以及国内的 SM3
算法。
利用这个特性,我们就可以快速比对文本是否被篡改,将明文和 hash
值一起传输给对方,收到后将明文重新生成 Hash
值,再和收到的 Hash
值比对,如果 Hash
值不同就说明被篡改过了。
假设教室中第一排的小明想给最后一排的小红传纸条。
第一种最简单的方法就是想传啥直接写到纸上,然后叠起来,让教室中间的人帮助传递过去即可。
但存在一个最大的问题,不安全,中间的某一个同学如果突发好奇,直接拆开纸条,内容就一览无余了。
小明想了想那我和小红约定一个对称加密算法吧,我先把密钥写到纸上传给小红,之后我都加密后写到纸上传给小红,这样就安全了吧。
中间传纸条的小刚突发好奇,拆开了纸条但这次好奇心没有得到满足,发现纸上写的由于加密过了已经完全看不懂了。
但小华拆开纸条却突然笑出了声,因为他读懂了纸条内容,在小明第一次传写有密钥的纸条的时候小华就已经拆开并且偷偷记下来了。所以后续的传递,只要小华想看,拆开以后通过密钥解密一下就可以了。
小明和小红想这可不行啊,于是两个人说干脆我们用非对称加密吧,我们的纸条内容都用对方的公钥加密,拿到纸条后用自己的私钥进行解密。这样纸条被别人看到也无所谓了,因为私钥只有我们自己有。
于是第一次传纸条的时候,小明把自己的公钥写好传给了小红,小红以后拿着这个公钥加密后再写到纸条上。小红也把自己的公钥写好传给了小明,小明以后拿着这个公钥加密后再写到纸条上。
小明收到小红写的纸条内容后,因为纸条是用小明的公钥加密过的,小明只需要用自己的私钥解密一下即可正常阅读了。
小红也是同样的道理。
小华在小红和小明第一次传纸条的时候同样又把两个公钥记了下来,但后续小红和小明的聊天小华却没办法解密了,因为纸条内容都是经过公钥加密的,如果想要解密必须通过私钥,但私钥在小红和小明各自的手里,其他人都无能为力了。
但小刚此时却偷偷笑出了声,因为第一次用纸条传公钥的时候,小刚偷偷动了手脚。
小明将公钥传给小红的时候,小刚偷偷将纸条换成了写有自己公钥的纸条,因此小红拿到的是小刚的公钥。
小红传公钥给小明的时候,小刚同样的将小红的公钥换成了自己的,因此小明拿到的是小刚的公钥。
当小明用收到的公钥加密后传递到小刚这里的时候,小刚就用自己的私钥进行了解密,然后用小红的公钥进行了加密发给了小红。小红和小明以为在安全的通信,其实被小刚一览无余了。
此时小红和小明遇到的问题就是无法确认收到的公钥是否是对方的。
小亮此时站出说,我来把证书内容用我的私钥签名,公钥我直接写到黑板上,具体过程如下:
证书上写好你们自己的名字和你们的公钥,我会把证书上的内容做一次 Hash
,然后把这个 Hash
用我的私钥加密,将加密后的内容也放到证书上。
现在证书上有你们的名字和你们的公钥,外加一个加密后的 Hash
值。
你们拿到别人的证书后,先看下名字是不是你们要的人,然后计算一下证书上的内容得到 Hash
值,再用黑板上的公钥把证书上加密的 Hash
值进行解密,看一下这两个值是否相同,如果相同的话就证明证书没有被篡改过,证书上的公钥可以放心使用。
自从有了证书,小刚拿到小明传给小红的证书后就无能为力了。
第一不能修改证书上的任何内容,一修改就会导致最终的 Hash
值不一样,就会被别人发现造假。
第二他也不能把证书替换成自己的,因为证书写了自己的名字,证书传过去以后小红一看这是小刚的证书那直接暴露。
班上的同学发现这也太棒了 ,再也不会有人读到纸条内容了,但小亮就变的太忙了,越来越多的人跟他要证书。小亮想要不我给小杨发个证书,以后让小杨给别人发证书。
于是后边的人就找小杨发证书,这样传递纸条的时候,除了自己的证书,也要把小杨的证书写上。收到的人用小杨的证书上的公钥来验证收到的证书是不是真的,而小杨的证书用黑板上的写的小亮的公钥来验证是否是真的。
未来小杨也觉得太忙了,她可能也授权某个人也能给别人发证书,这样第一次传递纸条的时候就需要把整个链条上的证书都写上,依次确认真假,但最后一次证书一定使用黑板上的公钥来确认,因为这个是大家都能看见的,一定不会是假的。
课堂上大家传纸条一段时间后发现过程中用非对称算法加密解密实在是太费时间了,本来原文写了 10
个字,加密加密可能得用一小时,虽然安全但太麻烦了。
于是小明对小红说:我们是不是能结合下对称加密算法。当我收到你的证书后,并且验证证书是可信的,我就生成一个对称加密的密钥,用证书上你的公钥加密后写到纸条上传给你。你收到后用自己的私钥解密,拿到对称加密算法的密钥后,以后写纸条都通过对称加密进行加密传给我。这样就既保证了传输的安全性,也节省了加解密的时间。
即使中间有人拿到了加密后的密钥,因为没有你的私钥,他也无能为力。
小红:赞!就这样搞。
实际证书会包含更多的东西,域名信息,有效期,以及之前说的签名等等。
以及上边的图里的证书链,会通过证书上公钥依次验证证书的有效性。而根证书就相当于黑板上写的公钥,这个会提前内置到系统中,如下图所示,是系统中所有的根证书。
浏览器确认当前域名和证书上的域名一致,并且证书是有效的,就会有一个通过的锁,否则会有一个危险提示。
实际过程中对称加密算法的密钥会通过多次传输最终拼接出一个密钥,过程可以参考 SSL / TLS 工作原理和详细握手过程
对称加密的密钥由 client random
,server random
和 premaster secret
三部分结合后生成。
以阿里云为例,记录下整个过程:
打开阿里云的 证书网站,点击创建证书:
每年免费 20
个额度:
点击立即购买,并且完成后续的支付流程。
购买结束后,回到控制台,点击「创建证书」,会在页面中多了一条待申请的证书,接着点击证书申请:
填写自己的域名,然后点击「提交审核」。
如果是阿里云的域名,DNS
记录里会自动加一条下边的 TXT
记录。
可以到自己的 DNS
解析控制台看一眼:
接着只需要等邮箱通知就可以:
刷新一下列表,证书就变为了已签发:
点击右边的下载,会弹出页面选择证书类型,这里我下载 nginx
的:
下来好的两个文件,.pem
就是我们的证书,.key
是我们的私钥。
接下来登录自己的服务器,前提是你已经按照 搭建网站 这个教程配置好了网站。
我们可以先把上边的两个文件重命名,并且通过任意的方式上传到自己的服务器,我是用 FTP
传上去。
接着通过 ssh
登录自己的服务器,ssh -p22 root@你的服务器 ip
,将刚传上来的服务器证书和密钥移动到 nginx
的相应位置:
1 | mv -f /var/ftp/pub/windliang.pem /etc/nginx/cert/ |
/var/ftp/pub/windliang.pem
和 /var/ftp/pub/windliang.key
需要改成你自己的文件地址。
还需要在网站的 nginx
的配置文件中加入 https
的配置,监听 443
端口:
1 | server { |
接下来重启 nginx
。
1 | nginx -s reload |
此时重新打开网站证书就设置成功了:
]]>– 你属什么?
– 属猪
– 那你是 95
年生的咯
– 我是 96
年生的
– 96
年不是鼠年吗
– 96
年 1
月还没过年,所以是 96
年的🐷
因为属相是按农历算的,但腊月一般都是第二年的 1
月了,导致每次都得解释下,哈哈。
今年凑巧和元旦过在了同一天,印象中小时候也有一次赶在了元旦,索性写个小程序来看一下吧。
上一次生日在元旦已经是快 20
年前的 04
年了,还看到了一个神奇的年份,下一次农历和公历重合的生日是 2033
年。
因为农历不像公历一样有确切的数学规律,只能由天文台测定后提供,所以一般都是采用「查表法」获取农历数据,我这里就偷懒直接使用别人的库了 https://www.npmjs.com/package/solarlunar 哈哈,这也导致目前只能查 1900 - 2100
年的数据。
感兴趣的同学也可以试试这个小程序 「农历生日转换」。
说到生日,我的博客 其实也是 17
年生日那天搭建的,现在回过头再看感觉还是很奇妙的,就像和当前的自己产生了连接一样。
mock
。下边介绍几种常用的方式,大家可以结合自己的项目来选取。大致分为三类,重写 xhr/fetch
、node.js
服务中转、系统层面拦截。
为了后边方便的安装 node
包,可以用 webpack
进行打包,具体配置可以参考 2021年从零开发前端项目指南,看到 React
配置的前一步就够了,只需要配置一个 html
和一个接口请求。 需要注意下 webpack
的版本,不同版本后续的配置会不同,这里我用的是 5.75.0
。
最终目标是通过 mock
让下边还没有开发好的接口正常返回数据:
1 | fetch("/api/data", { |
现在肯定是 404
。
better-mock fork
自 Mock.js,使用方法和 Mock.js
一致,用于 javascript
mock
数据生成,它可以拦截 XHR
和 fetch
请求,并返回自定义的数据类型。
只需要在调用接口前,引入 better-mock
。
1 | import Mock from "better-mock"; |
控制台此时就会输出数据了。
better-mock
一个好处就是可以通过它既有的语法来生成一些随机的数据,每次请求都会返回不同的数据。
坏处是会在请求发送前就拦截,导致在 Chrome
控制台就看不见请求了。
just mock 是一个浏览器插件,在代码中什么都不需要更改,只需要添加相应的接口和数据即可实现拦截。
插件安装好后添加相应的域名就可以拦截到相应的请求。
接着进行相应的编辑添加对应的 mock
数据就好。
这样接口就会被拦截,控制台输出预设的数据:
浏览器插件原理和 Better-mock
是一样的,但会更加轻便,无需融入到代码中。两者的原理是一样的,都是在网络请求前重写了全局的 xhr
和 fetch
,具体可以参考 油猴脚本重写fetch和xhr请求。
本地通过 koa
开启一个接口服务。
1 | // serve.js |
本地开启运行:node server.js
,接口提供的地址是 localhost:3000
,但是请求的地址是 loacalhost:8080
,当然可以直接修改代码里的地址为 localhost:3000
,但还可以通过 webpack
的配置,将请求转发到 localhost:3000
。
1 | const path = require("path"); |
这样就可以看到控制台输出了:
此外,Chrome
的 Network
也可以正常看到这个请求:
这种方法也可以用来解决跨域问题,举个例子:
如果本地想访问一个具体域名的接口,比如请求知乎的热榜接口:
1 | fetch( |
由于本地域名是 http://localhost:8080/
,此时浏览器就会报跨域的错了。
此时后端可以通过 CORS
策略解决跨域的问题,但因为是测试环境,后端可能会说你自己解决吧,此时就可以通过 Koa
进行中转。
改写一下 Koa
的代码,先请求后端的接口,接着将收到的数据拿到后返回。
1 | import Koa from "koa"; |
此时还是请求 /api/data
。
1 | fetch("/api/data", { |
依旧让 Webpack
将数据转发到 Koa
。
1 | devServer: { |
现在控制台输出的就是知乎返回的数据了,跨域问题也消失了:
当然上边解决跨域只是一个思路,具体的封装还需要结合项目来进行。
上边可以通过 webpack
进行转发数据,是因为 webpack
也启动了一个 HTTP
服务器,只不过用的不是 Koa
,是更早的一个框架 Express
,而且它们是同一个团队开发的。
既然已经有了一个 HTTP
服务器,所以也没必要再开启另一个 Koa
的了,通过给 webpack
传递一个函数,重写 Koa
返回的数据即可。
只需要通过 setupMiddlewares
重写数据即可。
1 | const path = require("path"); |
此时控制台也可以看到输出的内容:
同时 Network
也是可以看到网络请求的。
终极必杀 mock
方法,因为它除了可以拦截浏览器中的请求,也可以拦截任意 App
的数据,甚至还可以拦截手机中的 HTTPS
请求,前段时间很火的羊了个羊就可以通过 Charles
抓取请求然后迅速通关。
需要注意的是 Charles
抓不到 localhost
的请求,访问的时候需要将 localhost
改为 localhost.charlesproxy.com
。
webpack
需要加一个 allowedHosts
的配置,不然会返回 Invalid Host header
。
1 | devServer: { |
全部配置好后就可以看到 Charles
抓到的请求了。
此时只需要提前写好一个 json
文件,然后将右键选择 Map Local
对应的文件即可。
1 | { |
接下来就可以在控制台看到 mock
成功了。
几种 mock
方式各有优缺点,上边只是提供一个思路,具体的 mock
方案就需要结合项目进行一定的封装了。
cocos
,照猫画虎的写了一个「挑战1024」小游戏。学习一门新语言或者新框架其实就是一个堆时间的过程了,整个过程就是结合已有经验进行不同的猜测,然后验证,搞不定就去官网或者搜索引擎找答案,99.9%
的问题应该都能找到。
cocos
网上很多是视频教程,虽然对新手友好,但是信息密度太低了,这里我总结一下 cocos
专有的或者不太符合直觉的一些点,前端的同学看完以后能更快的进入 cocos
的开发中。
建议先跟着官方的 快速上手 先一步一步实现一个小游戏,再读下边的文章效果会更佳。
cocos
提供了游戏引擎,一些常用的操作,碰撞检测、重力模拟、变换位置、旋转、缩放、粒子系统等都可以通过配置一键实现,游戏引擎最终会帮我们把界面渲染到 canvas
节点上。
因为是渲染至 canvas
,当然很自然的可以支持跨端,一套代码可以编译至 h5
、微信小游戏等平台。
同一个功能不同平台之间有不同的 api
,比如 localstorage
的使用会有所不同,cocos
会帮我们在上层抹平,只需要按照 cocos
的语法编写,编译的时候选择相应的平台就会转成对应平台的 api
。
cocos
开发和平常的前端开发不太一样,它是代码结合 UI
拖拽来实现的,通过拖拽我们可以快速的布局、添加组件、设置属性等。
基于此,项目和编辑器就有了强绑定的关系,如果下载别人的项目,还需要下载相应的编辑器。
打开项目的时候需要选择相应的编辑器。
当然,如果编辑器差的版本比较小,Cocos
也可以帮我们自动升级项目的编辑器版本。如果是 2.x
升到 3.x
就会有 break changes
,需要手动进行一些代码的兼容。
ps
:MAC
M1
版本不支持 2.4.5
以下的版本。
游戏的 ui
、逻辑都挂载在某个场景(Scene
)下,可以在资源管理器右键创建场景,然后双击打开。
接下来我们就可以在当前 Canvas
添加各种节点和代码逻辑了。
游戏如果有多个页面,可以新建多个场景各自维护。
ps:如果从导入网上下载的 cocos
项目,场景不会自动加载,需要双击一下场景然后再预览。
我们可以通过右键创建节点,除了空结点,还帮我们预设了其他的很多节点,比如 Label
、Button
等。
节点是树状关系,每个节点可以得到它的父节点,也可以得到它的子节点。
比如我们可以通过 getChildByName
得到它的子节点。
1 | this.node.getChildByName("message"); // 得到相应的 Node 节点 |
通过 this.node.parent
拿到它的父节点。
一个空结点只有一些位置、大小属性。
我们可以在 Node
节点上挂载一些组件让 Node
拥有样式和功能。
如果我们创建一个 Label
节点,会自动挂一个 Label
组件。
通过 Label
组件我们可以设置文案 、字体大小等,展示到场景中的就是一个普通的 Label
。
我们可以通过将「资源管理器」中的图片拖动到「层级管理器」中生成一个带背景的 Node
节点。
拖过去之后会生成一个带有 Sprite
组件的节点,并将该图片设置为 Sprite Frame
属性的值,这样这张图片就会展示到场景中了。
如果想要更改图片,只要把 Sprite Frame
属性清空,重新拖一个图片上去即可。
这个是最重要的,我们可以编写游戏逻辑,设置一些点击监听、节点之间联动等逻辑,然后挂到 Node
节点上。
先新建一个 js
文件,会自动帮我们生成带有生命周期的一些代码。
双击打开新建的 js
文件,我们可以把文件和 VSCode
关联,用 VSCode
进行代码的编辑。
1 | // Learn cc.Class: |
properties
是脚本组件的属性,写在这里的属性可以在 Cocos
的界面上看到。
比较重要是 OnLoad
和 update
两个生命周期,OnLoad
会在组件渲染前进行执行,这里我们可以进行一些初始化的操作,update
生命周期会在每一帧渲染前执行,这里我们就可以更新节点的位置让一些节点动起来。
文件编写好以后,我们可以以组件的形式逻辑挂载到相应的 Node
节点上。
这个比较简单,它可以设置和边界的相对距离。
两个 Node
节点相撞,我们可以根据它们的坐标手动进行判断,也可以在 Node
节点上挂载碰撞组件,设置它们的分组,然后在脚本组件中增加 onCollisionEnter
回调函数即可。
添加 BoxCollider
组件。
设置 Node
中的 Group
属性。
Group
我们可以手动进行管理,并且设置哪些 Group
产生碰撞。
接下来还需要在游戏最开始的时候开始碰撞检测,可以给层级节点中的 Canvas
节点添加一个用户脚本组件 game.js
,然后修改脚本组件的 OnLoad
中调用下边的方法。
1 | const collisionManager = cc.director.getCollisionManager(); |
最后在相应的 Node
节点的用户脚本中添加 onCollisionEnter
回调函数进行碰撞后的逻辑即可。
1 | onCollisionEnter(other) { |
通过回调参数 other
可以拿到碰撞的节点。
这里通过刚体组件我们可以实现物体受到重力的效果。
首先给节点添加一个 RightBody
组件,并且将类型设置为 Dynamic
。
和碰撞组件一样,我们在 Canvas
对应的用户脚本组件的 OnLoad
中调用下边的方法开启重力模拟即可。
1 | const instance = cc.director.getPhysicsManager(); |
这样相应的节点就会受到重力的作用了。
在层级管理器选中相应的节点,点击「动画编辑器」,然后添加一个 Animation
组件
接着添加一个 Clip
,并进行编辑,设置动画的关键帧等,有点像 photoShop
里的动画编辑器。
保存后将新建的 Clip
拖到到对应的属性上即可。
button
被弹窗盖住,此时 button
依旧会响应到点击时间,此时可以通过给弹窗增加 BlockInputEvents
防止点击穿透。
需要点击的时候激活,关闭的时候取消激活。
1 | this.node.getComponent(cc.BlockInputEvents).enabled = true; // 点击的时候激活 |
设置的 positon
是在父节点坐标系下的位置。
如果它有子节点,它的子节点设置的 positon
就是基于上边的红线和绿线为坐标轴进行排布。
我们可以脚本组件中添加一些属性。
1 | properties: { |
这样在 cocos
编辑器中我们可以通过拖动进行属性的初始化。
值的注意的是,如果我们在 properties
外边写属性,比如下边的 num
。
1 | cc.Class({ |
此时我们在 onLoad
打印该值只会是 undefind
,如果想在当前实例上挂载属性,我们可以选择在 onLoad
中进行值的初始化。
1 | onLoad () { |
这里需要注意的是如果改变脚本代码,保存后我们需要重新切到 Cocos
的编辑页面 才会重新进行编译。
节点可以在编辑器生成,当然也可以通过代码动态生成。对于需要重复生成的节点,我们可以将它保存成 Prefab
(预制)资源,作为我们动态生成节点时使用的模板。
做法就是在「层级管理器」随便新建一个 Node
节点,并且添加所需要的组件和自定义的脚本组件,最后将该 node
节点拖动到「资源管理器」即可。
之后我们就可以层级管理器中刚新建的节点删除。当然,为了后续方便编辑,该 Node
节点也可以保留,但需要将其放到画面外,并且将脚本组件取消勾选一下,不执行逻辑。
有了预制资源后,我们可以通过下边的代码来动态生成 Node
。
1 | let newStar = cc.instantiate(this.starPrefab); // 根据预置资源生成 node 节点 |
对于画面中移动的 node
,当移出画面后我们可以进行重复利用,这里可以引入 NodePool
,出画面后加入节点池,需要的时候再从里边拿。
1 | this.pool = new cc.NodePool(); |
通过节点池我们可以节省内存的开销。
Node
和组件的关系最开始的时候有点懵逼,慢慢的调试后大致了解了,下边讲一下我的理解。
在定义 properties
的时候我们需要定义对象的属性,它可以是 type: cc.Node,
,也可以是自带的组件类型 type: cc.Label
,也可以是我们定义的脚本组件类型,可以先将编写的脚本代码引入 const Bird = require("Bird");
,然后将其作为一种类型 type: Bird
。
一个节点属于复合类型,它既是本身的 cc.Node
类型,如果添加了相应的组件,它也是相应的组件类型。
以下图为例,它既是 cc.Node
类型,也是 cc.Label
类型,还是 test
类型。
下边以动态修改 Label
的值,讲一下 Node
和组件之间的关系。
首先新建一个 canvas
的脚本组件 game.js
,将该组件挂载到 canvas
节点中。
game.js
中添加一个 label
属性,类型为 cc.Node
。
1 | cc.Class({ |
选中 Canvas
节点,将 FirstLabel
节点拖动添加的属性中。
虽然 firstLabel
属于三种类型,但因为我们定义的类型是 cc.Node
,因此拿到的是一个 Node
对象,我们打印看一下。
1 | onLoad() { |
如果想要在运行的时候改变当前节点的位置,调用 setPositon
即可。
1 | cc.Class({ |
如果我们想改变组件的文案,我们需要先通过 getComponent
拿到 Label
组件的实例对象,然后更新 string
属性即可。
1 | onLoad() { |
初值设置的是 设置文案
。
运行起来会发现是我们在 onLoad
中设置的值。
当然,我们也可以在开始的时候将组件类型设置为 cc.Label
,这样我们开始拿到的就是 Label
实例对象,就不需要再通过 getComponent
方法了。
1 | cc.Class({ |
改为代码后我们重新拖动,更新下属性的值。
那么如果我们想改变 node
位置该怎么办呢?
获得的组件实例中有一个 node
属性,我们可以直接拿到当前的 node
对象实例,然后继续调用 setPosition
就可以了。
1 | cc.Class({ |
为了更深刻的理解,我们再绕一下,实现通过当前节点的 Node
,调用自定义脚本组件的方法,来动态修改 Label
的值。
首先编写自定义组件的代码,提供一个方法
1 | // test.js |
当前脚本添加到相应的属性中。
接着我们只需要在 canvas
的脚本组件中调用 getComponent("test")
拿到上边的脚本对象实例,调用 setLabelValue
方法即可。
1 | cc.Class({ |
小结一下,使用对象的时候,我们需要明确当前是 cc.Node
类型,还是某种组件类型,每一个种类型都有自己的方法。
如果想从 cc.Node
对象中拿到相应的组件,调用 getComponent
方法即可。
如果想从组件中拿到 cc.Node
类型,不管是自带的组件,还是自定义的脚本组件,可以直接通过 this.node
拿到当前的 node
实例对象。
最直接就是设置 node
对象的 active
属性即可。
1 | cc.Class({ |
上边的方式类似于 vue
的 v-if
,会直接把节点销毁掉。
如果想保留节点,实现 vue
的 v-show
,我们可以设置 opacity
透明度属性弯道实现,只需要将值设置为 0
实现隐藏。
1 | cc.Class({ |
这里需要注意的是,虽然通过透明度可以隐藏组件,但是此时的点击事件还是存在的,需要处理一下。
编译的时候我们选择微信小游戏,填写 appId
,编译完成后通过微信开发者工具导入 build
出来的文件就可以了。
菜单 ->项目 -> 构建发布:
我们可以设置初始场景、设备方向等。
需要注意的是,微信主包有 2M
大小的限制,如果预览的微信小游戏遇到超包的情况,我们可以将没用到的组件在编译设置中去除。
菜单 -> 项目 -> 项目设置 -> 模块设置:
微信为了防止好友的关系链泄露,提出了一个子域的概念,在子域中可以调用 wx.getFriendCloudStorage
方法拿到好友数据。
为了实现排行榜,我们需要再创建一个空项目,实现排行榜的显示逻辑,和正常项目开发是一致的。
添加 message
回调函数,供主项目调用。
1 | private onMessage(msg: any) { |
编译的时候需要选择 微信小游戏开发数据域
,名称自己定义,我写的是wxSubContext
,路径选择之前项目编译的文件夹。
然后在主项目中我们需要添加一个空节点,并且添加一个 SubContextView
组件,将这个节点作为排行榜项目的容器节点。
如果想要调用排行榜的方法通过 postMessage
。
1 | wx.postMessage({ |
编译的时候指定一下排行榜项目之前设置的名称 wxSubContext
。
上边就是实现排行榜的整个逻辑了,详细的可以参考 这篇文章,相应的 代码仓库,clone
下来可以直接用。这个项目的 cocos
编辑器是 2.3.3
,如果升级到 2.4.5
会出现无法滚动的情况,谨慎升级。
需要注意的一点是,当子项目的容器节点显示的时候,子项目才开始初始化,这就会导致主项目 postMessage
先调用,排行榜项目的onMessage
后调用,导致错失了消息。
解决这个问题的话,我们的显藏可以通过设置透明度的方式实现,让子项目提前加载。
个人开发者提交的资料基本不用啥,有个自审资料网上找个模版在 word
填完截图就可以。
但提交审核的道路比较坎坷,除了慢以外,甚至被拒了两次。
第一次周日提交,周三还没有结果在微信社区平台催了一下审核,下午收到结果审核失败。
小游戏涉嫌侵权,请参考示例截图标记点全面自查游戏内容,请于下个版本有效整改或举证,在微信公众平台-版本管理-提交审核-授权书/版本更新说明提交,包括但不限于游戏内容说明及对应截图、原创证明或有效授权书 主体信用分扣除-3分
原因是一开始是准备仿 Flappy bird
的,直接用了相应的素材,就被驳回了,客服截图如下:
第二次周三早上提交,周五晚上收到结果,又被拒了,这次就无法理解了:
开发者你好,经平台审核,你的小游戏《挑战1024》未通过审核,具体原因如下:
1、小游戏需具有完整的游戏玩法,不能为简单的素材堆砌
网上搜了搜,可能是因为我的游戏只有一个界面,点击就开始了。据说加个菜单就会好,于是又改了改,不同场景也换了换背景。
第三次周六晚上提交,周二晚上收到结果,同上次,审核被拒,原因为「小游戏需具有完整的游戏玩法,不能为简单的素材堆砌」。
已经不知道该怎么改了,周三早上点了审核失败那里的提交反馈,写了一段感人肺腑的话(* 的内容这里就省略了)。
本游戏为益智类游戏,需要分数吃到 1024 才能获得胜利。
游戏场景分为菜单、第一关、最终关、好友排行,不同关卡也会通过背景色来区分。
菜单提供了分享好友、查看排行的功能。
第一关主要是为了体验游戏流程,星星的分数都是×2,因此只需要不停的吃分即可取得胜利。
最终关需要通过自己的策略,除了躲避陨石,还需要吃到星星上不同的分数,才能获得胜利。
游戏过程中,星星的速度、分数的出现会实时通过当前的状态进行变化,主要涉及到一些算法,也是本游戏的核心。
虽然素材都是星星,但结合算法上边的分数会一直变化,同时星星和陨石的比例也在不断变化。
除此之外玩家还需要躲避陨石,同时设定了策略,如果******。
在用户挑战失败的时候,增设了复活功能、重开功能。
游戏名为「挑战1024」,属于*******,来最终取得胜利。
希望审核大大可以再看一下,设计整个流程和算法确实花了很多心思。
周四早上显示反馈成功。
开发者你好,感谢你向小游戏审核团队反馈异议,经平台评估:我们已更正你的历史审核记录,如有需求,可重新提交审核
周五早上进行了重新提审,周二下午终于通过了。
小游戏相比于小程序审核严格好多,前前后后花了有半个多月了,简单游戏竟然不让上线,这是我想不通的。
整体就是这样了,整个 cocos
项目可以理解为一棵树,整个树就是一个场景,根节点是一个包含 Canvas
组件的 node
,接下来可以创建自己的 node
,每个 node
又可以挂载想要的自带组件和用户脚本组件。
希望对大家有帮助,如果错误也欢迎指出,也可以体验一下我这次开发的小游戏,哈哈:
]]>中后台项目中会存在一些配置页面需求的开发,这些需求高度相似,迭代频率低,基本结构为「搜索区域」、「表格区域」、「包含表单的弹窗」三部分组成。
其中「搜索区域」和「表格区域」的操作区交互固化,比如查询、添加、查看、删除、上线、下线。
当前开发时大都采用复制类似需求页面继而修改的方式,如下图所示。每个人都形成了自己的代码组织结构,导致虽属同一团队,但代码风格、交互实现逻辑变为了多条平行线。
这种迭代方式存在两点坏处:
a. 重复劳动较多,同时存在漏改的风险。易变的地方分布在页面中各个部分,修改起来不够方便,改动后存在影响到正常逻辑的风险。
b. 团队内各自抽离的不同交互方式,接手他人页面的时候需要耗费一定的理解成本,同时 code review
时无法快速的理清逻辑。
中后台项目提效一个直接的想法就是低代码的思路:
a. 初态:抽离各个组件,定义 json
的格式,通过 json
渲染出页面。
b. 终态:开发搭建平台,通过拖拽生成 json
并且实时预览页面,开发者也可以通过预定的协议接入自己的组件。
上述两种方案除去搭建成本大之外,最大的问题就是业务开发灵活性将大大降低。
开发者将在新的规范下进行开发,不管是通过 json
配置还是配置平台生成页面,上手难度大大增加,不亚于去学习一个新的前端框架。如果新需求的交互框架没有考虑到,将花费大量的时间进行适配, 甚至超过了从零开发需求的时间。极端情况下,如果无法满足需求的交互,还存在推倒重来的风险。
基于以上考虑,我们采取一个更轻便的方案,以模版代码为基础进行后续开发,并通过脚手架进行模版的配置、拉取。
如上图,大家的开发流程从之前的平行线变为了网状,未来的页面的目录格式和交互方式都会统一。
由于是生成的模版代码页面而且不强依赖于模版,未来需求有大的变化也可以正常迭代。
考虑到今后有可能增加其他场景模版,设计模版要考虑到未来可以进行无缝扩展,有两种方案:
a. 按分支来保存不同场景下的模版:
优点:不同场景下通过分支来拉取不同模版,模版之间完全隔离。
缺点:缺少了 master
分支,各模版都需要自己的 master
分支进行迭代。分支之间差异较大,完全违背了 git
的迭代初心。
b. 按文件夹来保存不同场景下的模版:
优点:所有模版都存在于 master
分支,和普通项目的方式一样从 master
切出分支进行迭代。
缺点:脚手架需要一次性拉取所有模版,然后复制自己需要的模版。
考虑到拉取文件速度较快,最终选取了方案 b
。
各个配置页面之间虽然相似,但也会因实际情况存在细微差异,所以模版不能完全写死,需要支持动态编译,这里采用 EJS
进行编译。
EJS
是一套简单的模板语言,它没有再造一套迭代和控制流语法,只需正常的 JavaScript
语法即可实现一些条件编译、变量替换等,因此可以快速上手。
关于模版内容,核心思想是将变化与不变的内容进行抽离。
我们可以将后端的接口、权限的配置、搜索框的配置、常量抽离出配置文件,将表格、搜索框、表单之间的联动方式预先写好,目录格式如下。
开发者只需进行后端接口的配置、搜索框的配置、表单的开发即可快速完成整个需求。
考虑到一方面脚手架整体架构和功能实现后迭代频率会逐渐降低,另一方面更新脚手架需要走 npm
包的发布流程,如果将模版内容耦合到脚手架中,每次更新都重新进行发包较为繁琐。
因此将模版单独放一个仓库,从脚手架中解耦出来,实现脚手架仓库和模版仓库分离,独立迭代,降低更新成本。
使用者通过输入命令和参数即可生成模版页面代码,脚手架内部实现拉取模版和编译,生成最终页面,架构如下:
提供 ce-cli
命令,结合用户的参数进行进行模版的编译生成,同时提供交互式的形式选择参数,降低使用者的上手难度,交互形式如下:
考虑到模版和脚手架存在强绑定关系,即如果模版更新了,但脚手架没有更新会造成一些模版逻辑未被编译的情况。因此执行命令时需要检查脚手架是否为最新版本,如果版本较低必须强制升级,中断程序的执行。(对于团队内部工具来说,始终保持最新版本才可以及时用到最新功能,这也是强制升级的原因之一)
为提高命令的执行速度,执行命令时将拉取的模版缓存到本地,并且将最新的 commit
名保存起来。第二次执行命令的时候将目前最新的 commit
和此前保存的 commit
进行比对,如果不相等则覆盖原来的模版,否则使用原来的模版即可,减少一次网络请求耗时。
经过一段时间的迭代,后续有近 20
个页面用到脚手架,每需求可降低 0.5pd
的时间,更重要的是团队内相关需求的代码结构、交互实现均已统一,提升了代码质量 和 code review
的效率,团队间交替开发需求时的代码认知难度将大大降低。
在 code review
过程中,团队内提供相关建议,沉淀最佳实践,例如默认对象通过函数返回、公共方法的使用、项目框架一些特有操作都内置到模版中,不断提升代码质量,磨平大家之间的认知差异。
未来有新同学加入,可以在模版的基础上更快的进入开发,极大的降低对系统框架一些特有操作的认知时间,同时保证代码质量。
脚手架中的模版对主要对表格和搜索区域固化了代码逻辑,对于表单的使用我们还是通过原始的 element
表单进行开发,一些常用的规则校验、表单的逻辑每次都需要重复进行开发,经过调研目前公司内已经有多种封装好的表单,未来可以进行详细了解,最终引进到模版代码中,进一步提升开发效率。
在日常需求迭代中,代码的规范与质量是编码的重要一环。Eslint
作为规则扫描器,能够对前端代码进行有效管控,避免出现低级错误,对于前端项目或多或少肯定都会看到 eslint
的相关配置。
但目前存在一些老项目, eslint
的配置仅仅停留在了多年前加的一些 eslint
规则上,没有任何其他动作,导致平常开发中有如下痛点:
diff
。pr
的时候分号、空格、换行各个地方不对齐,逼死强迫症系列。基于此,前段时间对老项目的 eslint
进行了一次完善,分享一下整个配置和思考的过程。
eslint
规则可以单独一条条配置,也有一些规则的集合比如官方推荐的 eslint:recommended
,框架相关的 plugin:vue/recommended,还有公司开源出来的整套规则比如 Airbnb
的 eslint-config-airbnb,腾讯的 eslint-config-alloy。
选取什么规则不是非常重要,大部分规则集也是类似的,此外本地也可以定义相同的规则名对规则集进行覆盖。
以 alloy
的规则为例,按照 eslint-config-alloy 中的文档安装完相应的 node
包以后,在本地根目录中新建 .eslintrc.js
文件引入相应的规则。
1 | module.exports = { |
如上所示,我们可以在 rules
中定义或者覆盖一些规则。
Prettier
是一个代码格式化工具,相比于 eslint
中的代码格式规则,它提供了更少的选项,却更加专业。
相比于 eslint
, Prettier
主要格式样式相关的,比如有没有分号、空格数、一行最大字符数等等,而 eslint
通过解析出代码的 AST
,可以自动格式化或者检测出一些潜在的问题,比如是否允许使用 console
、变量声明但未使用、switch
缺少 defaut
等。
当然 eslint
也可以配置样式相关的规则,但存在一些情况 eslint
无法胜任,因此格式化相关的我们都交给更专业的 Prettier
,安装 Prettier
的 node
包,并且根目录增加配置文件 .prettierrc.js
。
1 | // .prettierrc.js |
这一步我认为是推动 eslint
最重要的一步,大家抗拒项目添加 eslint
一个很大的原因就是本地没有开启实时检查和自动修复,当提交 commit
的时候遇到 eslint
规则卡控就很难受了。
团队内都使用的 VSCode
进行开发,可以安装 Eslint
和 Prettier
插件。
在本地新增 .vscode/settings.json
文件进行插件的配置,并且该文件不忽略 git
,所有人共享。
1 | { |
这个文件是 VSCode
针对当前工程的配置,配置后保存文件的时候插件会自动帮助我们格式化,同时有实时的错误提示。
这里需要注意的一点是,保存的时候会同时进行 prettier
和 eslint
的修复,如果 eslint
也配置了样式相关的规则,此时可能发生冲突,导致自动格式化后会有 eslint
的报错,此时可以将相应的 eslint
规则手动关闭,也可以引入 eslint-config-prettier 这个规则集批量关闭。
为了保证 eslint
规则的有效,需要在提交 commit
的时候进行检查,如果存在没有修复的 eslint
问题直接终止提交。
直接使用 "husky": "^1.3.1"
和 "lint-staged": "^8.1.5"
两个 node
包,需要注意下版本号,最新的配置有些不同了,下边是该版本下的配置。
1 | "husky": { |
husky
提供了 pre-commit
的钩子,然后 lint-staged
对暂存区代码自动进行格式化,如果出错的话会直接退出。
这样当我们提交 commit
的时候就会运行 eslint
和 prettier
进行代码的格式化。
虽然上一步对 commit
进行了卡控,但如果 git commit
的时候添加了 -n
参数,卡控检查也就直接跳过了。
如果想彻底的卡控,我们可以在打包流水线上增加一个 lint
的插件进行检查。
这里实现卡控有两种思路:
发布分支和 master
做 diff
,仅仅对 diff
出的 commit
进行 eslint
的检查。
但这里可能存在两个问题需要注意:
如果本地合并 master
的时候产生了冲突,然后解决冲突会新提交一个 commit
。 此时 diff
出来的 commit
可能会包含其他人的代码,如果之前的代码没有 lint
,此时就需要自己 lint
了。
如果上线流程是先合并 master
,那么上线的时候 master
已经有了自己的代码,此时上线分支和 master
就没有任何 diff
了,所以也就起不到卡控的作用了。
卡控分支前 n
天的 commit
。
理想情况下,前 n
天只包含自己的 commit
和已经 lint
过的 commit
, merge master
的 commit
可以自动过滤掉,因此可以很好的对新加的代码进行卡控。
当然还是无法完全避免遇到别人没有 lint
过的代码,此时还是需要自己进行修复了。
具体逻辑可以参考这个 node 包。
不管是哪种方法,因为是在老项目引入的 lint
,前期如果在流水线加 lint
卡控的话一定会遇到明明不是自己代码,却被 lint
卡控拦截的情况。
我个人看法是流水线 lint
其实不加也可以,如果编辑器自动修复添加了、commit
卡控也添加了,这已经足够了,如果真有人通过 -n
绕过卡控,那肯定是有理由的,也没必要走流水线再卡控。
因为老项目中会有大量的不符合 eslint
规则的代码,因此上线有两种方案。
本地进行全量文件的 eslint --fix
后上线:
优点:未来开发时原有文件的 lint
问题不用关心,开发者只需关注原有 error
和自己当前的 lint
问题即可。
缺点:由于改动文件数较多,eslint
不可完全信任,贸然上线可能会造成线上问题。
仅仅上线 eslint
的卡控和保存时自动 lint
的配置:
优点:未改动代码逻辑,不会存在引发线上问题的隐患。
缺点:当开发者修改、保存老文件后,会自动触发 lint
修复,从而污染混淆本身的修改,增加后续 code review
工作负担。
我是偏向于第 2
个方案的,虽然 eslint
自动修复一般不会引起问题,但程序肯定是不能 100%
相信的,如果造成了线上问题反而得不偿失。
如果采用第 2
个方案,后续开发老页面保存的时候一定会出现大面积的自动 lint
,我们可以在添加新代码前先保存一下触发 lint
并且提交一个 msg
为 lint auto fix
的 commit
。这样做有两个好处:
lint fix
就知道了这行代码不是你写的,他需要再往前找一个 commit
的提交人。pr
的时候我们可以按 commit
看,第一个 lint
的 commit
如果没什么问题可以直接跳过,减轻 cr
的负担。在业务迭代繁忙的时候,想在老项目中引入 eslint
其实还挺难的,毕竟业务价值很难讲清楚,一个反向逻辑就是现在项目没有 eslint
也运行的好好的,但加入 eslint
有什么收益呢?
另一方面,当有人推动项目 eslint
的规则的时候仅仅添加规则和卡控,其他的步骤不去推动,当越来越多人遇到需要手动修复 eslint
或者因为 eslint
的问题被卡控提交,内心就会不断地增加对 eslint
的抗拒。
在安装相关插件、node
包的时候需要注意下版本号,找到匹配自己包的版本号的配置,不然可能会遇到配置了但不生效的问题。
当有新项目开发的时候,一定要把 eslint
的自动修复、相关配置都搞好,这样开发的时候也舒服,未来也不用再进行 eslint
的治理了。
未来也可以结合平时开发的经验和发生的线上问题,逐步完善 eslint
中的 rules
规则,使得项目代码质量越来越高。
特别是网络请求或者其他异步操作中,await
记得包裹 try catch
,可以给用户一个友好提示,同时可以考虑 catch
中需要做什么兜底处理,必要时进行上传日志。
1 | try { |
可以结合 finally
,处理 loading
等。
前端经常使用 !v
,来判断 v
是不是有值。
1 | if(!v){ |
但如果 0
是 v
的有效值 ,此时本该处理,但会提前结束,最终引发错误。此时需要显示的判断是否是 null
或者 undefined
。
1 | if(v === null || v=== undefined){ |
由于 js
中的对象是引用,因此赋默认值的时候最好通过函数,每次都返回一个新对象。
bad:
1 | const defaultCondition = { |
good:
1 | const getDefaultCondition = () => ({ |
将接口的定义放到统一文件中,未来变动改动起来会比较方便,如果各个 url
都写死在页面中以后就很麻烦了。
1 | // service.js |
此外,网络请求一般都会在 npm
包的基础上自己再包一层,一方面可以注入共用参数,另一方面可以对返回数据进行统一的错误处理。
如果定义一个函数需要 3 个以上的参数
1 | function(a,b,c,d){ |
此时可以考虑采用对象解构,改为
1 | function({a=1,b,c,d}={}){ |
好处是未来需要扩展参数的时候,不需要太担心其他地方调用时候传参是否会引起问题。
当然,如果参数过多也需要思考一下当前函数是否承载了太多的功能,进行一下功能上的拆分。
当我们已经定义了一个函数,比如去初始一些变量。
1 | function initOptions(){ |
此时我们需要做另一件无关的事 【A】,虽然它和 initOptions
调用的时机一致,但最好不要直接放到 initOptions
中,而是新建一个函数单独调用。
不然未来如果其他地方也要调 initOptions
,但此时可能并不需要做【A】这件事情就会引起 bug
。
由于 js
语言的灵活性,函数传入的参数很可能不符合预期,必要时我们需要进行判断并且进行兜底处理,不可完全信任调用方。
团队合作中,该函数在未来极大可能会被其他人调用。
1 | function doSomeThing(params1, params2) { |
如果后边的流程强依赖于 params
,我们可以直接 return
,必要时也可以上报日志或者 throw Error
。
js
中没有整数类型,即 java
中的 int
、long
这些,所有数字都遵循 IEEE 754
标准,即 java
中的 double
类型,详细的可参考 浮点数详解。
可以精确表示的最大整数是 9007199254740991
,共 16
位,超过这个数精度可能会丢失,对于新接口,可以问一下后端相应数字字段的最大值会是多少。
对于浮点数的处理,除了众所周知的 0.1 + 0.2 === 0.3
的值为false
外,当我们对数字进行运算的时候也需要注意。
常见的将 9.04
元转为 904
分:
我们需要对结果进行取整处理。
可选链操作符,参考 MDN ,用的比较多。
和后端定的数组或者对象,后端有时候返回来的很可能是 null
甚至没有该字段,因此前端可以用可选链操作符用于数组、对象、函数,防止出现错误直接阻断后续流程。
1 | let nestedProp = obj.first?.second; // 等效于 obj.first && obj.fisrt.second |
但不要过度使用可选链,如果某些地方理论上不会出问题,比如 let test = obj.first?.second
,如果 second
一定能取到,我们直接 let test = obj.first.second
即可。
不然未来如果这里由于某种原因出了问题导致 obj.first
是 null
,但我们使用了可选链,所以 obj.first?.second
也不会报错,我们就永远不会知道这里出现问题了。
当然也需要权衡下,不加可选链造成js Error
会不会影响业务逻辑。
修改或者使用对象、数组时,时刻切记它们为引用,一处修改会造成处处修改。
以上的点应该算已经融入血液中了,平常开发和帮同事过 pr
的时候会格外注意,和业务逻辑没有关系,但可以提升代码质量。还有 Vue
一些常见的点也总结了一下,在语雀建了一个文档,未来有其他想法也会再更新一下,感兴趣的同学可以收藏一下,前端实践沉淀。
fetch
和 xhr
两种类型的请求。先简单写个 html
页面,搭一个 koa
服务进行测试。
html
页面提供一个 id=json
的 dom
用来加数据,后边我们补充 test.js
文件来请求接口。
1 |
|
将 html
通过 VSCode
的 live-server
插件运行在 http://127.0.0.1:5500/
上。
安装 koa
和 koa-route
的 node
包,提供一个接口。
1 | const koa = require("koa"); |
提供了 /api/query
接口,返回 data: [1,2,3],
。运行在本地的 3002
端口上,并且设置跨域,允许从 http://127.0.0.1:5500
访问。
先简单写一个插入 我是油猴脚本的文本
的脚本,后边再进行修改。
1 | // ==UserScript== |
此时页面已经被成功拦截:
这里提一句,油猴脚本如果使用 @grant
申请了权限,此时脚本会运行在一个沙箱环境中,如果想访问原始的 window
对象,可以通过 window.unsafeWindow
。
并且我们加了 @run-at
,让脚本尽快执行。
在 html
请求的 test.js
中添加 fetch
的代码。
1 | fetch("http://localhost:3002/api/query") |
看下页面,此时就会把 data
显示出来。
如果想更改返回的数据,我们只需要在油猴脚本中重写 fetch
方法,将原数据拿到以后再返回即可。
1 | // ==UserScript== |
对 response
的处理有点绕,当时也是试了好多次才试出了这种方案。
做的事情就是把原来返回的 respones
复制,通过 json
方法拿到数据,进行修改数据,最后新生成一个 Response
进行返回。
看下效果:
成功修改了返回的数据。
我们将 fetch
改为用 xhr
发送请求,因为页面简单所以请求可能在油猴脚本重写之前就发送了,正常网站不会这么快,所以这里加一个 setTimeout
进行延时。
1 | setTimeout(() => { |
和 fetch
的思路一样,我们可以在返回前更改 responseText
。
重写 XMLHttpRequest
原型对象的 open
或者 send
方法,在函数内拿到用户当前的 xhr
实例,监听 readystatechange
事件,然后重写 responseText
。
1 | const originOpen = XMLHttpRequest.prototype.open; |
运行一下:
拦截失败了,网上搜寻下答案,原因是 responseText
不是可写的,我们将原型对象上的 responseText
属性描述符打印一下。
可以看到 set
属性是 undefined
,因此我们重写 responseText
失败了。
我们无法修改原型对象上的 responseText
,我们可以在当前 xhr
对象,也就是 this
上边定义一个同名的 responseText
属性,赋值的话有两种思路。
我们定义一个 writable: true,
的属性,然后直接赋值为我们修改后的数据。
1 | const originOpen = XMLHttpRequest.prototype.open; |
看下页面会发现成功拦截了:
1 | const originOpen = XMLHttpRequest.prototype.open; |
我们拿到原型对象的 get
,然后在当前对象上定义 responseText
的 get
属性,修改数据后返回即可。
相比于第一种方案,这种方案无需等待 readystatechange
,在开始的时候重写即可。
需要注意的是,上边方案都只是重写了 responseText
字段,不排除有的网站读取的是 response
字段,但修改的话和上边是一样的,这里就不写了。
通过对 fetch
和 xhr
的重写,我们基本上可以对网页「为所欲为」了,发挥想象力通过油猴脚本应该可以做很多有意思的事情。
element-ui
版本是 2.15.9
,vue
版本是 2.7.8
。
在 el-dialog
中使用 el-tabs
,并且 el-dialog
添加 destroy-on-close
属性,当关闭弹窗的时候页面就直接无响应了。
1 | <template> |
效果如下:
再等一会儿 Chrome
就直接抛错了:
操作过程中控制台也没有任何报错,去 github
的 issues
看一眼发现已经有 3
个人遇到过这个问题了:
[bug report] El dialog [destroy on close] El tabs page crashes #21114
[Bug Report] el-tabs in el-dialog with destroy-on-close=‘true’ ,dialog can’t be closed
看表现应该是哪里陷入了死循环,猜测是 el-tabs
的 render
函数在无限执行。
为了证实这个猜测,我们直接在 node_modules
中 el-tabs
的 render
函数添加 console
。
打开控制台观察一下是否有输出:
直接原因找到了,下边需要排查一下 render
进入死循环的原因。
可能出现问题的点,el-dialog
、el-tabs
、el-tab-pane
,当然如果上述都没问题的话,也不排除 Vue
的问题,虽然可能性很低。
如果我们把 destroy-on-close
属性去掉,然后一切就恢复正常了。所以我们先看一下 destroy-on-close
做了什么。
1 | <template> |
最关键的的是 <el-dialog__body>
的外层 div
中设置了一个 key
。
1 | watch: { |
当我们把 dialog
的 visible
置为 false
的时候,会判断 this.destroyOnClose
的值,然后修改 key
的值。
当 key
值修改以后,div
中的元素就会整个重新渲染了,这就是官网中所说明 this.destroyOnClose
的作用。
为了排除 el-dialog
的问题,我们写一个自定义组件来替代 el-dialog
。
1 | <template> |
接着我们将 el-dialog
换为上边的组件。
1 | <template> |
运行之后发现问题依旧存在,因此我们可以排除是 el-dialog
的问题了。
接下来就是一个二选一问题了,问题代码是在 el-tabs
还是 el-tab-pane
中。
我们把 el-tab-pane
从 el-tabs
去掉再来看一下还有没有问题。
1 | <template> |
运行一下发现一切正常了:
至此,可以基本确认是 el-tab-pane
问题了。
我们来定位是哪行代码出现了问题,看一下 el-tab-pane
的整个代码。
1 | <template> |
定位 bug
所在行数一般无脑采取二分注释法很快就出来了,经过两次尝试,我们只需要把 updated
中的代码注释掉就一切正常了。
1 | updated() { |
子组件发送了 tab-nav-update
事件,看一下父组件 el-tabs
接收 tab-nav-update
事件的代码。
1 | created() { |
这里会执行 calcPaneInstances
方法:
1 | calcPaneInstances(isForceUpdate = false) { |
主要是比较前后的 panes
是否一致,如果不一致就直接用新的覆盖旧的 this.panes
。
由于 render
函数中使用了 panes
,当修改 panes
的值的时候就会触发 el-tabs
的 render
。
1 | render(h) { |
打印一下关闭弹窗的时候发生了什么:
当关闭弹窗的时候,触发了 el-tabs
的 render
,但此时除了触发了 el-tabs
的 updated
,同时也触发到了 el-tabs-pane
的 updated
。
在 el-tab-pane
的 updated
中我们发送 tab-nav-update
事件
1 | updated() { |
tab-nav-update
事件的回调是 calcPaneInstances
,除了改变 this
指向,同时传了一个默认参数 true
。
1 | this.$on('tab-nav-update', this.calcPaneInstances.bind(null, true)); |
对于 calcPaneInstances
第一个参数的含义是 isForceUpdate
。
1 | calcPaneInstances(isForceUpdate = false) { |
如果 isForceUpdate
为 true
就会更新 panes
的值,接着又触发 el-tabs
的 render
函数,又一次引发 el-tab-pane
的 updated
,最终造成了 render
的死循环,使得浏览器卡死。
一句话总结:某些场景下如果父组件重新 render
,即使子组件没有变化,但子组件传递了 slot
,此时就会触发子组件的 updated
函数。
上边的逻辑确实不符合直觉,我们将代码完全从 Element
中抽离,举一个简单的例子来复现这个问题:
App.vue
代码,依旧用 wrap
包裹。
1 | <template> |
Tabs.vue
,提供一个 slot
,并且提供一个方法更新自己包含的 data
属性 i
。
1 | <template> |
Pane.vue
,提供一个 slot
。
1 | <template> |
操作路径:
打开弹窗 -> 关闭弹窗 -> 再打开弹窗(此时 pane
就会触发 updated
) -> 更新 Tabs
的值,会发现 pane
一直触发 updated
。
如果我们在 Pane
的 updated
中引发 Tabs
的 render
,就会造成死循环了。
关于这个问题网上前几年已经讨论过了:
https://segmentfault.com/q/1010000040171066
https://github.com/vuejs/vue/issues/8342
但是上边网站的例子试了下已经不能复现了,看起来这个问题被修过一次了,但没有完全解决,可能是当做 feature
了。
如果你的版本是 Vue 2.6
以上,当时尤大提过了一个解决方案:
指明 slot
的名字,这里就是 default
。
代码中我们在 Pane
中包裹一层 template
指明 default
。
1 | <template> |
再运行一下会发现 pane
的 updated
就不会触发了。
仔细想一下,我们第一次渲染的时候并不会出现问题,因此我们干脆在关闭弹窗的时候把 Pane
销毁掉(Pane
添加 v-if
),再打开弹窗的时候现场就和第一次保持一致,就不会引起 Element
的死循环了。
1 | <template> |
同样的,Pane
的 updated
也不会被触发了。
讲道理,这个问题其实也不能算作是 Element
的,但在 updated
生命周期触发渲染其实 Vue
官方已经给出过警告了。
Element
兼容的话,需要分析一下当时为什么在 updated
更新父组件状态,然后换一种方式了。
应该不会再修复了,毕竟有方案可以绕过这个问题,强制更新子组件应该是某些场景确实需要更新。
但 slot
为什么会引发这个问题,源代码到时候我会再研究下,最近也一直在看源代码相关的,目前 Vue2
响应式系统和虚拟 dom
两大块原理解析已经完成了,模版编译已经开始写了,关于 slot
应该也快写到了,感兴趣的同学也可以到 vue.windliang.wang 一起学习,文章会将 Vue
的每个点都拆出来并且配有相应的源代码进行调试。
在业务开发中,如果业务方能解决的问题,一般就自己解决了,一方面底层包团队更新速度确实慢,另一方面,因为业务代码依赖的包可能和最新版本差很多了,即使底层库修复了,我们也不会去更新库版本,罗老师镇楼。
]]>