GithubHelp home page GithubHelp logo

sishenhei7 / react-blog Goto Github PK

View Code? Open in Web Editor NEW
10.0 3.0 3.0 23.34 MB

:rocket:a blog written with reactjs and antd https://sishenhei7.github.io/react-blog

JavaScript 54.82% HTML 6.68% CSS 38.50%
react react-native antd blog

react-blog's Introduction

说明

这是一个使用react + antd + react-router + issue + es6的单页面静态博客,点击「馒头加梨子」查看博客。

此博客基于github issuegithub api搭建。

Fork 使用

将配置文件src/config/config.js配置修改成自己的账号。

命令使用

安装

npm install

运行

npm start

打包

npm run build

技术栈

想做的

  • 增加博客加载进度条

  • 增加标签,点击自动分类搜索

  • github登录 + 评论功能

  • component按需加载

  • 使用redux重构

  • 使用react native重构

  • 加入后台服务端

react-blog's People

Contributors

sishenhei7 avatar

Stargazers

Joey avatar  avatar InVinCiblezz avatar  avatar Garen avatar GopiSuresh Akula avatar yang chengkai avatar carlleton avatar KaiXuan avatar  avatar

Watchers

James Cloos avatar  avatar  avatar

react-blog's Issues

古龙语录

概述

一个人脸上若是脏了,是不是要去照照镜子才知道怎样去擦掉。古龙先生的作品就像是一面镜子,让我们从这面镜子中看清自己,然后帮助我们擦掉生命中的污垢,看清脚下的路

我最喜欢的一句话:阿吉决心要吃完这碗面,他就一定要吃完,不管这碗面里有灰也好,有血也好,有泪也好。

下面是我按照我自己的阅读顺序收录的古龙语录。

《多情剑客无情剑》

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.不错,有种人宁可流血,也不愿流泪。

评:有种人宁可流血,也不愿流泪。

56.一个人无论如何也得为自己活些时候,哪怕是一年也好,一天也好我时常都觉得我这一生根本就没有真正活过。

评:一个人无论如何也得为自己活些时候。

57.老伯的命令既已发出,就必须彻底执行,至于这件事是难是易,他是否能独立完成,那已全不在他考虑之中。老伯就算叫他独立去将泰山移走,他也只有立刻去拿锄头。

评:老伯就算叫他独立去将泰山移走,他也只有立刻去拿锄头。

58.老伯道:“你不想知道林秀到哪里去了?”律香川又沉默了很久,才断然道:“我不想知道,无论她到哪里去,一定都有很好的理由。”

评:有些人做事肯定有很好的理由。

59.小蝶道:“真话有时是很伤人的。”孟星魂道:“谎话也许会不伤人,但却伤人的心。”

评:谎话也许会不伤人,但却伤人的心。

60.酒不好并没有关系,有些人要喝的并不是酒,而是这种喝酒的情趣。

评:这种喝酒的情趣。

70.孟星魂也没有说话。他什么都没有想,他只是静静的享受着这份沉默的乐趣,机智的言语虽能令人欢愉,但一个人若不懂得享受沉默,他就不能算是个真正会说话的人。因为“真正令人欢愉的言语,只有那些能领悟沉默意义的人才能说出来”。

评:真正令人欢愉的言语,只有那些能领悟沉默意义的人才能说出来

71.因为没有痛苦也不会有真正的快乐,我只有跟你在一起的时候,才真正快乐。

评:没有痛苦也不会有真正的快乐。

72.也许有人说:“爱是奉献,不是占有,既然是奉献,就不该嫉妒。”说这句话的人若非圣贤,就是伪君子。

评:爱是奉献,不是占有。

73.爱情几乎可以做任何事,只除了一样,爱情改变的只是你自己,你不能改变别人。

评:爱情不能改变别人。

74.因为她知道男人做的事,女人最好不要干涉,一个女人若是时常要干涉男人的事,迟早定会后悔的。等到这男人受不了她的时候,她想不后悔也不行。

评:等到这男人受不了她的时候,她想不后悔也不行。

75.她并没有阻止他们,也没有掩盖自己,反而冲得更仔细,尽量将自己完美无瑕的胴体裸露到月光下。因为她忽然发觉自己喜欢被男人偷看。每当有人偷看她时,她自己也同样能感觉到一种秘密的欢愉。

评:有些女人就是喜欢有人偷看她。

76.聪明人宁可信任自己的仇敌,也不信任朋友。

评:聪明人宁可信任自己的仇敌,也不信任朋友。

77.朋友并不可怕,真正可怕的是你分不出谁是你的仇人,谁是你的朋友。

评:真正可怕的是你分不出谁是你的朋友。

78.香川道“你有没有听人说过,不喝酒的人不但可怕,而且很难交朋友?”

评:不喝酒的人不但可怕,而且很难交朋友。

79.世上根本很少有值得牺牲的女人。
评;世上根本很少有值得牺牲的女人。

80.你想赚得多,就得花得多,只有会花钱的人才能赚得到更多的钱。

评:你想赚得多,就得花得多,只有会花钱的人才能赚得到更多的钱。

81.屠大鹏抚掌笑道:“我现在才发现你最大的长处,就是无论做什么都从不只替自己着想,你若有肉吃,我一定也有。”

评:你若有肉吃,我一定也有。

82.风风道“一个人若懂得利用别人‘恶的’那一面,懂得利用别人的贪婪,虚荣,嫉驴,仇恨,他已经算是个很了不起的人。”老伯道,“的确如此。”风风道“但你却比那些人更高一着,你还懂得利用别人‘善’的一面,还模得利用别人的感激,同情和义气。” 

评:懂得利用别人‘善恶的’那一面。

83.老伯笑道:“但是我知道有些男人虽然不喜欢他的老婆,还是买了很多漂亮衣服给老婆穿。”凤凤道:“那只因他根本不是为了他的老婆而买的!”老伯道:“是为了谁呢?”凤凤道:“是为了他自己,为了他自己的面子,其实他心里恨不得他老婆只穿树叶子!”

评:是为了他自己,为了他自己的面子。

84.她拒绝,并不是因为她真的要拒绝,只不过因为她关心他。对男人来说,没有什么能够比这种话更具诱惑的了。

评:这才是欲拒还迎。

85.凤风流着泪道"我的脚已经快被你捏碎了,你究竟想什么?难道想。--想-。。"她没有说出那两个字。她要这男人自己去想那两个字,自己去想那件事。"求求你,不要那么样做,我怕……我还是个女孩子。"这不是哀求而是提醒提醒他可以在她身上找到什么样的乐趣。她不怕那件事。那本是她最后的一样武器,无疑也是最有效的种。"你看看我的脚,求求你,我真的已受不了。"这已不是提醒,而是邀请。

评:欲拒还迎的最高境界。

86.年轻人和老人之间,本就有着一段很大的距离,无论对什么事的看法,都很少会完全相同的!所以老人总觉得年轻人幼稚愚蠢,正如年轻人对老人的看法一样。年轻人虽然应该尊敬老人的**和智慧。但尊敬并不是赞成!服从也不是!

评:尊敬并不是赞成!服从也不是!

87.一个人只要有弱点,就容易对付。

评:一个人只要有弱点,就容易对付。

88.老伯打断了他的话道:“等你到了没人信任时,才会知道那种感觉有多可怕。”

评:等你到了没人信任时,才会知道那种感觉有多可怕。

89.老伯叹息了一声,苦笑道:"因为老人剩下的时候已不多,花在睡觉上岂非太可惜了。"

评:老人剩下的时候已不多,花在睡觉上岂非太可惜了。

90.老伯点点头道“而且我还要将她一心想要的那张地契送给她――以后你无论看到谁在想往上爬,都应该去扶他一把,千万不要从背后去推他。” 孟星魂垂下头心里充满了感激,也充满了崇敬。

评:以后你无论看到谁在想往上爬,都应该去扶他一把,千万不要从背后去推他。

91.老伯慢慢地接着道:“一个人种下的种子若是苦的,自己就得去尝那苦果。我既已错了,就得要付出错误的代价,除了我之外,谁也不能替我去承受。”

评:我既已错了,就得要付出错误的代价,除了我之外,谁也不能替我去承受。

《碧血洗银枪》

92.马如龙道:“钱财本是无主之物,交给谁都无妨。”

评:钱财本是无主之物,交给谁都无妨。

93.一个人如果见死不救,他还有什么值得自己骄傲的?马如龙是个骄傲的人,非常骄傲。

评:一个人如果见死不救,他还有什么值得自己骄傲的?

94.男人最大的悲哀是“愚蠢”,女人最大的悲哀是“丑陋”。

评:男人最大的悲哀是“愚蠢”,女人最大的悲哀是“丑陋”。

95.这女人冷笑道:“一个大男人,怎么会混成这种样子,穷得连一文都没有,一定是因为你好吃懒做,不务业。”

评:一定是因为你好吃懒做,不务业。

96.她一双老鼠眼般的眼睛立刻又瞪了起来,大声道:“你是谁?”彭天霸道:“我是条猪。”这女人道:“你虽然长得胖了些,比猪好像还瘦一点。”彭天霸叹了口气,道:“只可惜我比猪还笨一点,所以,才会接下他这件银狐裘。”

评:搞笑。

97.这女人道:“他为什么要把这么好的东西给你?”彭天霸道:“因为他要用这件皮裘拿住我的手。”

评:搞笑。

98.这种错误绝不容人再犯第二次,一次已足以致命!但是他还可以拼,用他的血肉和性命去拼!一个肯拼命、敢拼命的人,不但危险,而且可怕,一个人只有在迫不得已时,才肯拼命。

评:要学会拼命。

99.大婉眨着眼,又问:“你不后悔?见到了她之后,无论发生什么事,你都不后悔?”马如龙的回答很绝。“我已经做了这么多应该后悔的事,再多做一件有什么关系?”

评:一个很好的解释。

100.大婉在笑。“你就算在他们面前翻斤斗,他们也看不见的。”“为什么?”“因为他们都是明白人,都明白应该在什么时候装袭作哑。”

评:明白人都明白应该在什么时候装袭作哑。

101.他无论做什么事,都是全心全意的在做。所以他才会做得比别人好。

评:无论做什么事,都全心全意去做。

102.他忽然想起了很多事,想起了他以前做过的那些自己觉得自己很了不起的事。那些事是不是真的全部都是应该做的?是不是真的有那么了不起?

评:以前做的那些事是不是真的有那么了不起?

103.一个已决心准备流血的人,通常都不会再流泪。

评:一个已决心准备流血的人,通常都不会再流泪。

104.一个人为什么要活下去?是不是因为他还想做一些自己认为应该做的事?如果一个人自己认为绝对应该做的事却不能做,他活着还有什么意思?

评:我还想做一些自己认为应该做的事。

105.“来买红糖。”马如龙道:“她总认为红糖就像是人参一样,不但滋补,而且能治百病。”买不起人参的人,只好买红糖,人参和红糖同样都是心理上的寄托,就好像有人信神,有的人信佛一样。

评:她总认为红糖就像是人参一样,不但滋补,而且能治百病。

106.他说你最大的好处,就是你从来不会忘记别人的好处。

评:从来不要忘记别人的好处。

《七种武器――长生剑》

107.有时候受罪就是享福,享福也就是受罪。究竟是享福还是受罪,恐怕也只有你自己才知道。

评:有时候受罪就是享福,享福也就是受罪。

108.他很了解它所代表的是什么东西――好的酒、华丽的衣服、干净舒服的床、温柔美丽的女人,和男人们的羡慕尊敬。这些都是一个像他这样的男人不可缺少的,但现在,他舍弃了它们,心里却丝毫没有后悔惋惜之意。因为他知道他已得到更好的;因为世上所有的财富,也不能填满他心里的寂寞空虚。而现在他却已不再寂寞空虚。

评:每个人都有寂寞空虚的时候,也有应对方法。

109.这故事给我们的教训是――无论多锋利的剑,也比不上那动人的一笑。所以我说的第一种武器,并不是剑,而是笑。只有笑才能真的征服人心。所以当你懂得这道理,就应该收起你的剑来多笑一笑!

评:只有笑才能真的征服人心。

《白玉老虎》

110.柳三更道:“你最好看清楚些,因为这就是我做错事的代价。”他惨白的脸上忽然露出悲痛之色,慢慢的接着道:“二十年前,我看错了一个人,虽然被他挖出一双眼珠子,我也毫无怨言,因为每个人做错事都要付出代价,无论谁都一样。”

评:因为每个人做错事都要付出代价,无论谁都一样。

《萧十一郎》

111.风四娘是一向不愿迎着急风施展轻功,因为她怕风吹在脸上,会吹皱了她脸上的皮肤。

评:好句。

112.那人还是不停的谢谢,但一双眼睛已盯在风四娘高耸的胸膛上。风四娘倒也并不太生气,因为她知道男人大多数都是这种轻骨头。

评:男人不要做轻骨头。

113.床上的人叹了口气,喃喃道:“我上个月才洗澡,这女人居然说我脏……”风四娘忍不住“噗哧”笑出声来。

评:搞笑。

114.萧十一郎笑道:“拍你马屁的人太多了,能有个人气气你,岂非也很新鲜有趣。”

评:拍你马屁的人太多了,能有个人气气你,岂非也很新鲜有趣。

115.萧十一郎早已又滑到墙上,再一溜,已上了屋顶,就像个大壁虎似贴在屋顶上,摇着手道:“千万莫要动手,我只不过是说着玩的,其实你一点也不老,看起来最多也不过只有四十多岁。”

评:其实你一点也不老,看起来最多也不过只有四十多岁。

116.萧十一郎道:“其实我也并非真的想看,但我若不看,只怕你又要生气了。”

评:就算不愿意,也要做别人开心的事。

117.她倒是真懂得男人,她知道地位越高、越有办法的男人,就越喜欢不听话的女人,因为他们平时见到的听话的人太多了。

评:她知道地位越高、越有办法的男人,就越喜欢不听话的女人。

118.思娘咬着牙,冷笑道:“亏你还敢说自己是男子汉,原来只会欺负女人,欺负女人的男人非但最不要脸,也最没出息。我倒想不到你会是这种人。”

评:欺负女人的男人非但最不要脸,也最没出息。

119.萧十一郎道:“这你就不懂了,一个女人最好看的时候,就是她虽然想板着脸,却又忍不住要笑的时候,这机会我怎能错过?”

评:一个女人最好看的时候,就是她虽然想板着脸,却又忍不住要笑的时候。

120.萧十一郎似乎觉得有些意外,动容道:“莫非尊驾就是‘源记’票号的少东主,江湖人称‘铁君子’的杨大侠么?”杨开泰笑道:“不敢,不敢……”萧十一郎也笑道:“幸会,幸会……”

评:谦虚,谦虚。

121.杨开泰忽然看到满桌子的菜,脸色就立刻发白,喃喃道:“菜太多了,太丰富了,怎么吃得下。”风四娘板着脸道:“这话本该由做客人的来说的,做主人的应该说:菜不好,菜太少……你连这点规矩都不懂吗?”杨开泰擦了擦汗,道:“抱……抱歉,我……我一向很少做主人。”

评:菜不好,菜太少……

122.沈太君无论年龄、身份、地位,都已到了可以随便说话的程度,能够挨她骂的人,心里非但不会觉得难受,反而会觉得很光荣,她若对一个人客客气气的,那人反而会觉得全身不舒服。

评:对于可以随便说话的老板,能够挨他骂的人,心里非但不会觉得难受,反而会觉得很光荣。

123.萧十一郎不禁在暗中叹了口气,因为他很明白一个男人是绝不能太听女人话的,男人若是太听一个女人的话,那女人反会觉得他没出息。

评:男人若是太听一个女人的话,那女人反会觉得他没出息。

124.小公子笑道:“一点也不错,我若是女人,情愿嫁给萧十一郎,也不愿嫁给连城璧。”屠啸天道:“哦?”小公子道:“像萧十一郎这种人,若是爱上一个女人,往往会不顾一切,而连城璧的顾忌却太多了,做这种人的妻子并不容易。”

评:像萧十一郎这种人,若是爱上一个女人,往往会不顾一切。

125.好清好甜的竹叶青,一碗下肚有精神,两碗下肚精神足,三碗下了肚,神仙也不如。

评:好句。

126.她这一生所受到的教育,几乎都是在教她控制自己,因为要做一个真正的淑女,就得将愤怒、悲哀、欢喜,所有激动的情绪全都隐藏在心里,就算忍不住要流泪时,也得先将自己一个人关在屋里。她静静的站在那里,听着那位阔少爷说话。她这一生中从未打断过任何人的谈话;因为这也是件很无礼的事,她早已学会了尽量少说,尽量多听。

评:悲惨的好女人。

127.她不由自主要想:“我若嫁给一个平凡的男人,只要他是全心全意的对待我,将我放在其他任何事之上,那种日子是否会比现在过得快乐?”

评:是吗?

128.大多数男人都有种“病”――懒病。能治好男人这种病的,也只有女人――他爱的女人。

评:能治好男人这种病的,也只有女人――他爱的女人。

129.这首歌的意思是说,世人只知道可怜羊,同情羊,绝少会有人知道狼的痛苦、狼的寂寞,世人只看到狼在吃羊时的残忍,却看不到它忍受着孤独和饥饿,在冰天雪地中流浪的情况,羊饿了该吃草,狼饿了呢?难道就该饿死吗?”

评:狼的孤独。

130.道是不相思,相思令人老,几番细思量,还是相思好……

评:说得好。

131.豆腐自然立刻被摔得稀烂。萧十一郎居然一本正经的板着脸,道:“这门功夫叫‘摔豆腐手’,和‘大摔碑手’是同路的功夫,只不过是师娘教出来的。”

评:摔豆腐手,哈哈。

132.萧十一郎道:“我却只能这么说,在男人面前,他也许是个君子,但遇着单身的美丽女子,他身上恐怕就只剩下头发还像个君子了。”

评:很睿智。

133.萧十一郎道:“越是假正经的女人,越容易上钩,这道理男人都很明白。”

评:很明白。

《七种武器――碧玉刀》

134.连月亮都在窗外偷窥,何况人?段玉悄悄地将眼睛睁开一线,忍不住从心里发出了赞赏之意。

评:连月亮都在窗外偷窥,何况人?

135.王飞也笑了道:“一个男人可以不随便花钱,但却决不能不懂得花钱。”顾道人笑道:“不懂得花钱的男人,一定是个没用的男人。”王飞道:“因为你一定要先懂得花,才会懂得怎么去赚。”

评:一定要先懂得花,才会懂得怎么去赚。

136.王飞一仰脖子就喝了下去,冷笑道:“原来这酒也没什么了不起,简直就像是糖水,喝一杯就已足够了!”

评:说得好。

137.一个女人若总是喜欢找你的麻烦,吃你的醋,跟你斗嘴,这种女人当然不会太笨。所以等到你有了麻烦之时,来救你的往往就是她。

评:一个女人若总是喜欢找你的麻烦,吃你的醋,跟你斗嘴,这种女人当然不会太笨。

138.有人说,女人最厌恶的动物是蛇。也有人说,女人最厌恶的是老鼠。其实女人真正最厌恶的是什么?――女人。女人真正最厌恶的动物,也许就是女人。一个可能成为她情敌的女人,尤其是一个比她更美的女人。

评:女人真正最厌恶的动物,也许就是女人。

139.所以我说的这第三种武器,并不是碧玉七星刀,而是诚实。只有诚实的人,才会有这么好的运气。段玉的运气好,就因为他没有骗过一个人,也没有骗过一次人――尤其是在赌钱的时候。所以他能击败青龙会,并不是因为他的碧玉七星刀,而是因为他的诚实。

评:段玉的运气好,就因为他没有骗过一个人,因为他的诚实。

《七种武器――霸王枪》

140.丁喜道:“它会不会替你捶背,会不会替你端茶倒酒?” 红杏花虽然还想板着脸,却还是忍不住笑了。 

评:奶奶最喜欢的事情。

141.一个男人若能在漂亮的女人面前,侮辱了另一个男人,总会觉得自己很了不起,总会认为那女人也会觉得他很了不起,甚至会看上他。也许就因为这原因,所以女人们才会觉得大多数男人都很愚蠢可笑。

评:所以不要在漂亮的女人面前,侮辱了另一个男人。

142.丁喜微笑道:“第一,假如我要去做一件事,我从来也不想别人报答;第二,我虽然是个强盗,却也有很多事不肯做的,就算砍下我脑袋来,我也绝不去做。” 

评:有所为,有所不为。

143.胖的人都喜欢睡硬床,年轻人都喜欢睡硬床,红杏花既不胖,也不再年轻。她的床很软,又软又大。 

评:胖的人都喜欢睡硬床,年轻人都喜欢睡硬床。

144.邓定侯观人于微,知道只有内心充满矛盾不安的人,才会咬指甲。

评:只有内心充满矛盾不安的人,才会咬指甲。

145.女人就应该像个女人。聪明的女人都知道,若想征服男人,绝不能用枪的。只有温柔的微笑,才是女人们最好的武器。

评:男人也一样,只有温柔的微笑,才是女人们最好的武器。

146.苏小波道:“一个象王大小姐这样的美人,又何必去跟男人舞刀弄剑,只要大小姐一笑,十个男人中已至少有九个要拜倒在裙下了。”

评:只要大小姐一笑,十个男人中已至少有九个要拜倒在裙下了。

147.一个人若是已决心要去做傻瓜,你只有让他去做;一匹马若是已决心不肯往前走了,你也只有让它停下来。

评:决心做的事,就让他去做。

148.丁喜刚才临走的时候,已将这匹马系在树上,他看来虽然是个粗枝大叶的人,其实做事一向很仔细,因为他从小就得自己照顾自己。

评:做事一向很仔细,因为他从小就得自己照顾自己。

149.邓定侯道:“也许他只不过因为吃的苦太多,所以做事就比别人小心些。” 王大小姐冷笑道:“一个真正的男子汉,不管吃了多少苦,都不象他这样怕死。” 

评:一个真正的男子汉,不管吃了多少苦,都不象他这样怕死。

150.一个从来没有家的人,对朋友总是特别够义气。

评:一个从来没有家的人,对朋友总是特别够义气。

151.邓定侯忽然叹了口气,道:“做强盗的确也不容易,不拼命,就成不了名,拼了命又是什么下场呢?那一身的内伤,一脸的刀疤,换来的又是什么?”  王大小姐道:“做保镖的岂非也一样?”  邓定侯勉强笑了笑,道:“只要是在江湖中混的人,差不多都一样,除了几个运气特别好的,到老来不是替别人买烧鸡,就是自己卖烧鸡。” 

评:只要是在江湖中混的人,差不多都一样,除了几个运气特别好的,到老来不是替别人买烧鸡,就是自己卖烧鸡。

152.王大小姐凝视着他,美丽的眼睛里也充满了赞许的仰慕。够义气的男子汉,女人总是会欣赏的。

评:够义气的男子汉,女人总是会欣赏的。

153.老山东抬起头,瞪着眼睛,看了她很久,忽然道:“你决心要去?” 王大小姐道:“我是非去不可。” 老山东道:“就算明知道去了回不来,你也是非去不可吗?” 王大小姐又笑了笑,道:“能不能回来并不重要,重要的是,我们能不能去,该不该去?” 老山东长长叹了口气,道:“说得好,好极了。” 

评:成不成功并不重要,重要的是我们能不能做这件事,该不该做这件事。

154.可是他们现在已知道,一个人只要有勇气去冒险,天下就绝没有不能解决的事。班超、张骞,他们敢孤身涉险,就正是因为他们有勇气。古往今来的英雄豪杰,能够立大功成大事,也都是因为这“勇气”两个字。但勇气并不是凭空而来,是因为爱,父子间的亲情,朋友间的友情,男女间的感情,对人类的同情,对生命的珍惜,对国家的忠心,这些都是爱。若没有爱,谁知道这个世界会变成个什么样的世界,谁知道这故事会变成个什么样的结局?

评:一个人只要有勇气去冒险,天下就绝没有不能解决的事。

《七种武器――七杀手》

155.阴涛的瞳孔收缩,突然冷笑,道:“头颅就在此,你为何不来拿?”杜七道:“你为何不送过来?”

评:哈哈。

156.花生摊子上写明了:“五香花生,两文钱一把。”他抛下了三十文钱,抓了十五把花生,一箩筐花生就几乎全被他抓得干干净净。卖花生的小姑娘几乎已经快哭出来。

评:哈哈。

157.龙五轻轻地叹息了一声,道:“我已有五六年没有等过人了。”蓝天猛道:“是。”

评:我已有五六年没有等过人了。

158.龙五道:“有时我也喜欢说谎话,但我却不喜欢听谎话。”柳长街道:“谁在说谎?”龙五道:“你!”柳长街笑了笑,道:“有时我也喜欢听谎话,却从来不说谎。”

评:有时我也喜欢说谎话,但我却不喜欢听谎话。有时我也喜欢听谎话,却从来不说谎。

159.龙五道:“你若是喜欢,也不妨将她们拿走。”柳长街忽然又笑了笑:“这世上的女人是不是已死光了?”龙五道:“还没有。”柳长街微笑道:“既然还没有死光,我为什么还要她们六个?”柳长街已走了出去。

评:这世上的女人是不是已死光了?

160.胡月儿柔声道:“我实在很怕你不要我。我一定会变得很乖的,就像条母老虎那么乖。”她忽然又一脚把柳长街踢下床去。柳长街怔住,终于怔住,终于笑不出了。胡月儿从被里伸出一只手,拧住了他的耳朵,但声音却更温柔:“从今天起,应该听话的是你,不是我,因为你反正已非娶我不可。但是你若敢不听话,我还是要你睡在地上,不让你上床。”

评:哈哈。

161.来的当然是个女人,而且还是个很美的女人,不但美,而且媚,尤其是一双眼睛,简直已媚到人的骨子里去。随便你上看下看,左看右看,她从头到脚都是个女人,每分每寸都是女人。柳长街看着她,忽然笑道:“我是要女人来陪我喝酒的。”这女人媚笑道:“你看不出我是个女人?”柳长街道:“这样我看不出。”这女人道:“要怎么样你才看得出?”柳长街道:“要脱光了我才看得出。”

评:哈哈。有的女人确实有些部位不像是女人。

162.柳长街叹了口气,道:“我冒着千辛万苦,九死一生,就是为了要来见你一面,现在总算已来了,我怎么肯闭上眼睛?”相思夫人道:“可是我正在洗澡。”柳长街笑了笑:“就因为听见你在洗澡,所以我更不肯闭上眼睛了。”相思夫人也叹了口气,道:“看来你非但不听话,而且也不是个老实人。”柳长街道:“我说的都是老实话。”

评:我虽然不是老实人,但我说的都是老实话。

163.胡月儿咬着嘴唇:“我跟你才分手几天,你就去找过别的女人。”“我没有。”柳长街本来也懒得说话的,但这种事却不能不否认。胡月儿不信:“若是没有,别人为什么要打你的屁股?”柳长街叹息着:“若是有了,她怎么会舍得打我屁股?”

评:哈哈。

164.龙五道:“要骗过秋横波和孔兰君都不是容易事,你却做到了。”柳长街终于笑了笑,道:“但我却是为你做的。”龙五看着他,忽然大笑:“看来你不但聪明,而且很谨慎。”

评:别人夸奖你的时候反夸奖别人的方法。

165.胡力慢慢地点了点头,道:“现在我才明白,老人贪财,只因为老人已看透了一切,已知道这世上决没有任何东西比钱财更实在。”

评:所以老人才是贪官?

166.一个人开始变得会自言自语的时候,就表示他已渐渐老了。

评:一个人开始变得会自言自语的时候,就表示他已渐渐老了。

《三少爷的剑》

167.人活着,就应该懂得怎么去享受生命,怎么去追寻快乐。一个人脸上若是脏了,是不是要去照照镜子才知道怎样去擦掉?我只希望这面镜子也能做到这一点,能够帮助人擦掉生命中的污垢。

评:古龙的小说就像一面镜子,帮助我们擦掉生命中的污垢。

168.燕十三道:“你既个酒色之徒,今天我就让你一次。”乌鸦道:“让什么?”燕十三道:“让你付账。”乌鸦道:“不必让,不客气。”燕十三道:“这次一定要让,一定要客气。”乌鸦道:“不必不必。”燕十三道:“要的要的。”别人吃饭通常都是抢着付账,他们却是抢着不要付账。

评:哈哈。

169.燕十三又笑了,忽然道:“你这位文武双全的公子爷是不是哑巴?”薛可人道:“当然不是!”燕十三道:“那么这些话他为什么不自己来说?”乌鸦冷冷道:“就算他是个哑巴,屁眼总有的,这些屁他为什么不自己来放?”

评:哈哈,很讽刺。

170.谁知道人类有多少不如意,不幸福,不快乐的事,是隐藏在如意、幸福、快乐中的?

评:确实。

171.他告诉我这些事,是不是因为他已将我当做个死人?只有死人才是永远不会泄漏任何秘密的。

评:哈哈。

172.燕十三笑了笑,道:“因为我忽然发觉,一个人的一生中,多多少少总应该做几件愚蠢的事,何况……”他的笑容中带着深意:“有些事做得究竟是愚蠢?还是明智?常常是谁都没法子判断的。”

评:有些事做得究竟是愚蠢?还是明智?常常是谁都没法子判断的。

173.她又在用力拍阿吉的肩:“告诉这些母狗,你叫什么?”阿吉道:“我叫阿吉。”韩大奶奶道:“你没有姓?”阿吉道:“我叫阿吉。”韩大奶奶用力敲了敲他的头,大笑道:“这小子虽然没有姓,却有样好处。”她笑得很愉快:“他不多嘴。”

评:不要多嘴。

174.阿吉道:“我不想死,也不想被饿死,你们若是不付账就走了,就等于敲破了我的饭碗。”这句话刚刚说完,两把刀就刺入了他身子,他连动都没有动,连眉头都没有皱,就这么样站在那里,挨了七八刀。小伙子们吃惊的看着他,忽然乖乖的拿钱出来付了账。

评:为了饭碗。。。

175.近来他才知道,一个人要活着并不是件容易事。谋生的艰苦,更不是他以前所能想像得到的,一个人要出卖自己诚实和劳力,也得要有路子。而他没有路子。

评:确实要有路子。

176.老婆婆道:“你是汉人,汉人总认为我们苗子臭得要命。”阿吉道:“我是汉人,我比他还臭。”老婆婆大笑,也用木杓敲了敲他的头,就好像敲她儿子的头一样。

评:会说话啊。

177.这才是一个女人的本分应该做的,她懂得男人做事,从来不喜欢女人多问。就算这女人是他的母亲也一样。

评:说得对。

178.大老板目光闪动,道:“所以你就从最不可能的地方去找?”竹叶青目光露出尊敬佩服之色,道:“我能想得到的,当然早已在大老板计算之中。”

评:会拍马屁。

179.大老板微笑,道:“这一手阿吉的确做得很聪明,只可惜他想不到我这里还有一个比他更聪明的人!”竹叶青态度更恭谨,垂首道:“那也只不过因为我从来不敢忘记大老板平日的教训!”

评:会拍马屁。

180.阿吉终于吃完了他的面。他决心要吃完这碗面,他就一定要吃完,不管这碗面里有灰也好,有血也好,有泪也好。

评:他决心要吃完这碗面,他就一定要吃完,不管这碗面里有灰也好,有血也好,有泪也好。

181.他忽然发现她以前跟着他吃苦,只不过她从未有这种像这样的机会而已,否则很可能早已背弃了他。这想法就像是一根针,直刺入他的心。

评:他忽然发现她以前跟着他吃苦,只不过她从未有这种像这样的机会而已。

182.偷风不偷月,偷雨不偷雪,偷好人不偷坏人。

评:哈哈。

183.铁开诚道:“陌生人并不可怕。”因为陌生人既不了解你的感情,也不知道你的弱点。只有你最亲密的朋友,才知道这些,等他们出卖你时,才能一击致命。

评:说得对。

184.谢晓峰看着他,目中充满同情:“我看得出你是个老实人。”施经墨垂下头:“我只不过是个没有用的人。”老实人的意思,本来就通常都是没有用的人。

评:老实人的意思,本来就通常都是没有用的人。

185.谢晓峰道:“只要是人,不管是什么样的人,要学坏都比学好容易,尤其像吃喝嫖赌这种事根本连学都不必学的。”

评:只要是人,不管是什么样的人,要学坏都比学好容易。

186.厉真真道:“所以只要你一拔剑,对方就必将死在你的剑下,至今还没有人能挡得住你三招。”谢晓峰道:“那也许只因为我在三招之间,就已尽了全力。”

评:谦虚不一定要反对别人。

187.厉真真嫣然道:“想不到世上居然还有你不懂的事。”她脸上的表情就像是黄梅月的天气般阴晴莫测,笑容刚露,又板起了脸:“你既然不懂,我为什么要告诉你?”

评:你既然不懂,我为什么要告诉你。

188.老人道:“因为有种人天生就不能有朋友。”谢晓峰道:“你是这种人?”老人道:“不管我是不是这种人都一样,因为你是这种人。”

评:哈哈。

189.这世上永远有两种人,一种人生命的目的,并不是为了存在,而是为了燃烧。燃烧才有光亮。哪怕只有一瞬间的光亮也好。另外一种人却永远只有看着别人燃烧,让别人的光芒来照亮自己。哪种人才是聪明人?

评:哪种人?

190.铁开诚的脸色变了。谢晓峰却在微笑,道:“以前我绝不会这么做的,宁死也不会做。”他笑得并不勉强:“可是我现在想通了,一个人只要能求得心里的平静,无论牺牲什么,都是值得的。”铁开诚沉默了很久,仿佛还在咀嚼他这几句话里的滋味。

评:一个人只要能求得心里的平静,无论牺牲什么,都是值得的。

191.谢晓峰忽然叹了口气,道:“一个既没有显赫的家世,也没有父母可依靠的年轻人,要成名的确很不容易。”铁开诚道:“但是年轻人却应该有这样的志气,如果他是在往上爬,没有人能说他走错了路。”

评:如果他是在往上爬,没有人能说他走错了路。

192.他们都是年轻人,热情如火,鲁莽冲动,做事完全不顾后果。可是江湖中永远都不能缺少这种年轻人,就好像大海里永远不能没有鱼一样。就是这群年轻人,才能使江湖中永远都保持着新鲜的刺激,生动的色彩。

评:原谅也是一种美德。

《陆小凤1―陆小凤传奇》

193.陆小凤道:“屋子里如真的有人进来,我刚才为什么没有听见敲门的声音?”勾魂手道:“因为我们没有敲门。”陆小凤又张开眼看了看他们,只看了一眼。忽然问道:“你们真的是人?”铁面判官怒道:“不是人难道是活鬼?”陆小凤道:“我不信。”勾魂手道:“什么事你不信?”陆小凤淡淡道:“只要是个人,到我房里来的时候都会先敲门的,只有野狗才会不管三七二十一就从窗口跳进来。”

评:哈哈。

194.铁面判官道:“世风日下,人心不古,江湖上的冒牌货也一天比一天多了。陆朋友想必不会怪我们失礼的。”

评:世风日下,人心不古,江湖上的冒牌货也一天比一天多了。

195.你明明知道你的朋友在饿着肚子时,却偏偏要恭维他是个可以不食人间烟火的神仙,是条宁可饿死也不求人的硬汉。你明明知道你的朋友要你寄钱给他时,却只肯寄给他一封充满了安慰和鼓励的信,还告诉他自力更生是件多么高贵的事。假如你真的是这种人,那么我可以保证,你惟一的朋友就是你自己。

评:哈哈。

196.除了有一张美丽的脸之外,她居然还有一颗能了解别人、体谅别人的心――这两样东西本来是很难在同一个女孩子身上找到的。只有最聪明的女人才知道,体谅和了解,永远比最动人的容貌还能令男人动心。

评:体谅和了解,永远比最动人的容貌还能令男人动心。

197.陆小凤走过来,迎着从东面吹过来的春风,长长的吸了一口气,微笑着道:“你若要摆脱一个女人,最好的法子就是让她自己说肚子疼,一个出来玩玩的男人,至少应该懂得三种法子能让女人肚子疼。”

评:一个很有办法的男人,至少应该懂得法子能让女人肚子疼。

198.花满楼笑道:“一个人若能知道他自己是个混蛋,总算还有点希望。”

评:一个人若能知道他自己是个混蛋,总算还有点希望。

199.孙秀青笑了,道:“你脸上就算长了花,刚才也已被人家摘走了。”她的眼睛很大,嘴唇薄薄的,无论谁都看得出这女孩子说话一定是绝不肯饶人的。

评:眼睛很大,嘴唇薄薄的,无论谁都看得出这女孩子说话一定是绝不肯饶人的。

200.马秀真叹道:“看来这丫头什么都知道,就是不知道害臊。”

评:好句。

201.他对付生气的人有个秘诀――你既然生气了,就索性再气气你,看你究竟能气成什么样子,看你究竟气不气得死。

评:你既然生气了,就索性再气气你,看你究竟能气成什么样子,看你究竟气不气得死.

202.青枫忽然挥袖拂乱了这局残棋,悠悠道:“人生岂非也正如一局棋,输赢又何必太认真呢?”陆小凤道:“若不认真,又何必来下这一局棋?”

评:说得好。

《陆小凤2―绣花大盗》

203.陆小凤叹了口气,道:“我知道你对人有了恩惠,从不愿别人报答,所以你才不肯将这件事告诉我。”

评:对的。

204.饮茶本是广东人最大的嗜好,饭可以不吃,茶却不可不饮。

评:学到了。

205.现在她已坐下来,向阿土嫣然一笑,道:“又是你来得最早。”阿土叹了口气,道:“男人总是吃亏些,总是要等女人的。”

评:哈哈。

206.陆小凤道:“我既然已要走了,你又何必再问?”公孙大娘凝视着他,悠悠的道:“我既然已问了,你又何必不说?”

评:哈哈。

《陆小凤4―银钩赌坊》

207.孤松并没有否认,反问道:“你能喝多少不醉?”陆小凤道:“我只喝一杯就已有点醉了,再喝千杯也还是这样子。”孤松眼睛里第三次露出笑意,道:“所以你也从未真的醉过?”

评:哈哈。

208.可爱的人,岂非通常都是可怕的?这句话你也许不懂,可是等你真的爱上一个人时,你就会明白我的意思了。

评:真的。

209.他也是个大男人,一个美丽的女人在男人面前,无论说什么话,男人通常都会觉得很有趣的。

评:一个美丽的女人在男人面前,无论说什么话,男人通常都会觉得很有趣的。

210.陆小凤闭上了嘴,他终于发现不吃饭的女人在这世上也许还有几个,但不吃醋的女人却连一个也没有。

评:不吃醋的女人却连一个也没有。

211.假如你自己也觉得自己是个有用的人,就绝不会想死的,因为你的生命已有了价值,你就会觉得它可贵可爱。假如你真正全心全意的去帮助过别人,就一定会明白这道理,因为只要你肯去帮助别人,就一定是个有用的人。

评:只要你肯去帮助别人,就一定是个有用的人。

212.陆小凤笑了,道:“我要你替我做事,我没有谢你,你反而谢我?”赵君武道:“就因为你没有谢我,所以我才要谢你!”陆小凤道:“为什么?”赵君武眼睛里发着光,道:“因为我知道你一定已把我当作朋友!”

评:朋友!

213.在临死前的一瞬间,她忽然领悟到一种既复杂、又简单,既微妙、又单纯的哲理,忽然明白人生本就是这样子的。然后她的人生就已结束。一个人为什么总是要等到最后的一瞬间,才能了解到一些他本来早已了解的事?

评:恩。

《陆小凤5―幽灵山庄》

214.他拍拍花满楼的肩,道:“我们走,假如这世上还有一个人能找到陆小凤的,那个人一定就是你。”花满楼道:“不是我。”木道人道:“不是你是谁?”花满楼道:“是他自己。”一个人若已迷失了自己,那么除了他自己外,还有谁能找得到他呢?

评:说的好。

215.独孤美道:“现在你既然已知道出路,为什么还不抛下我一个人走?”陆小凤也沉默了很久才回答:“也许只因为你还会笑。”独孤美不懂。陆小凤慢慢的接着道:“我总觉得,一个人只要还会笑,就不能算是六亲不认的人。”

评:一个人只要还会笑,就不能算是六亲不认的人。

216.他们只忘了一点,青春虽然美妙,老年也有老年的乐趣。有位西方的智者曾经说过一段话,一段老年人都应该听听的话。年华老去,并不是一个逐渐衰退的过程,而是从一个平原落到另外一个平原,这虽然使人哀伤,可是当我们站起来时,发现骨头并未折断,眼前又是一片繁花如锦的新天地,还不知有多少乐趣有待我们去探查,这岂非也是美妙的事?

评:老年也有老年的乐趣。

217.知道这世上还有个人能了解自己的悲痛和苦恼,无论对谁说来,都是件很不错的事。

评:恩。

218.你若想去刺探别人的秘密,就得先准备随时牺牲自己。

评:你若想去刺探别人的秘密,就得先准备随时牺牲自己。

《陆小凤6―凤舞九天》

219.“孤独”有时本就是种享受,却又偏偏要让人想起些不该想的事。太多伤感的回忆,不但令人老,往往也会令人改变。

评:是的。

220.陆小凤道:“你既然知道,为什么不说?”老实和尚摇着头,喃喃道:“天机不可泄漏,佛云:不可说,不可说。”

评:佛云:不可说,不可说。

221.一个人只要能想得开,这世上本就没有什么值得苦恼埋怨的事。

评:一个人只要能想得开,这世上本就没有什么值得苦恼埋怨的事。

222.这位智者说:友情是累积的,爱情却是突然的,友情必定要经得起时间的考验,爱情却往往在一瞬间发生。这一瞬间是多么辉煌,多么荣耀,多么美丽。这一瞬间已是永恒。

评:这一瞬间已是永恒。

223.小玉道:“女人迟早都要嫁人的,嫁了人就有丈夫,寡妇却没有,一个人自由自在的,也没有人管,还可以去偷别人的丈夫,岂非好玩得很?”

评:有趣。

224.无论在什么时候,你若想要一个女人的命并不是件困难的事,可是你如果想要一个女人不吃醋,那简直是做梦。

评:对的。

225.鹰跟老七忽然说道:“喂,你过来。”老狐狸道:“我本来就要过来。”

评:哈哈。

《陆小凤7―剑神一笑》

226.要对付老太婆,最好的法子就是把她当成一个小女孩,就正如你在一个小女孩面前,千万不能说她还没有长大。

评:哦。

227.“其实我并不怪你,你虽然一直在跟我大吼大叫,乱发脾气,我也可以原谅你。”陆小凤的声音里真的好像充满了谅解与同情:“因为我知道一个女人到了你这样的年纪还嫁不出去,火气总是难免特别大的。”

评:哈哈。

228.这个世界上有两种女人,一种女人走路的时候就好像一块棺材板在移动一样,另外一种女人走起路来腰肢扭动得就像是一朵在风中摇曳生姿的鲜花。

评:哈哈。

229.宫萍说:“天大的理由,也比不上高兴两个字。一个女人要是真的不高兴去做一件事,谁也拿她没法子。”“你错了。”陆小凤说:“世上既然有这种不讲理的女人,就有专门对付这种女人的男人。”

评:哈哈。

230.他很愉快的指着自己的鼻子微笑:“譬如说,我就是这种男人。”宫萍冷笑。“你?你能把我怎么样?”“我当然也不能把你怎么样,最多也只不过能把你的裤子脱下来而已。”这个法子已经是老套了,而且有点俗气,可是用这种法子来对付女人,却是万试万灵的,不管是什么样的女人都怕这一招。

评:哈哈。

231.能够把一件很不文雅的事说得很文雅,也是种很大的学问。

评:确实。

232.如果你认为用这五个字描述西门吹雪还不够,一定要用十三个字才够,那么这十三个字就是除了孤独、寂寞、冷这五个之外,再加上八个字。骄傲、骄傲、无情、无情。

评:哈哈。

《七种武器4―多情环》

233.可是他很快就恢复正常,淡淡道:“就算最清醒的人,有时也会做出糊涂事的,何况我本就是个四不像的半吊子。”王锐叹了口气,苦笑道:“不管怎么样,你这半吊子想的好像比我们两个加起来还多。”

评:学一下谦虚。

234.他好像还生怕别人听不懂,又解释着,说道:“形容一个人烂醉如泥,这一个泥字,说的并不是烂泥。”老板娘居然笑了笑,笑的时候更加迷人:“不是烂泥是什么呢?”萧少英道:“是一种小虫,没有骨头的小虫,这种小虫就叫做泥。”

评:学到了。

235.葛停香道:“一个人若有很深的心机,很大的阴谋,就绝不会做错事。”

评:一个人若有很深的心机,很大的阴谋,就绝不会做错事。

236.他冷笑着,又道:“因为他一向是个聪明人,聪明人总是不肯吃苦,总是要走近路,要练好内功和掌力,却没有近路可走。”

评:聪明人也是糊涂人。

237.“我问他,还想不想再活下去?他的回答是――一个人到了该死的时候,若还想活下去,这个人不但愚蠢,而且很可笑!”

评:一个人到了该死的时候,若还想活下去,这个人不但愚蠢,而且很可笑!

238.仇恨的本身,就是种武器,而且是最可怕的一种。所以我说的第四种武器也不是多情环,而是仇恨。

评:仇恨的本身,就是种武器。

《七种武器2―孔雀翎》

239.小武道:“你若觉得应该去做一件事,就一定要去做,根本不必问别人曾经为你做过什么。”

评:你若觉得应该去做一件事,就一定要去做,根本不必问别人曾经为你做过什么。

240.高立道:“我对你好奇,也许只因为我们现在已是朋友。”小武道:“有朋友的人死得早。”高立道:“没有朋友的人,活着岂非也和死了差不多。”

评:没有朋友的人,活着岂非也和死了差不多。

241.一个女人只要能使她的男人幸福欢愉,其他纵然有些缺陷,又能算得了什么?

评:对。

242.小武叹了口气,道:“你总是在我面前说,你的小公主是世上第一的美人,现在我才知道你是个骗人精。”高立脸色立刻变了,拼命挤眼睛,道:“我哪点骗了你?”小武道:“世上哪有像她这样的美人?她简直是天上的仙子。”

评:哈哈。

243.高手相争,死的那一个人通常总是不想死的那一个。

评:高手相争,死的那一个人通常总是不想死的那一个。

《七种武器6―离别钩》

244.“离别钩也是种武器,也是钩。”“既然是钩,为什要叫作离别?”“因为这柄钩,无论钩住什么都会造成离别。如果它钩住你的手,你的手就要和腕离别;如果它钩住你的脚,你的脚就要和腿离别。”“如果它钩住我的咽喉,我就要和这个世界离别了?”“是的。”

评:哈哈。

245.可是杨铮说:“只要我问心无愧,什么地方我都可以去。”他就是这样一条硬汉。只要他认为应该做的事,做了后问心无愧,你就算拿把刀架在他脖子上,也拦不住他的。

评:硬汉。

246.“岁月匆匆,物移人故。人各有命,谁也勉强不得。”

评:岁月匆匆,物移人故。人各有命,谁也勉强不得。

《绝代双骄》

247.对付女人的法子,我总算知道了,你只要能打动她的心,她就会像马一样乖乖地被你骑着,你要她往东,她就往东,要她往西,她就往西。

评:对付女人的法子就是要能打动她的心。

248.小鱼儿动也不动,瞧着她,淡淡道:“幸好我的心已被狗吃了,我真该谢谢那条狗,否则男人的心若被女人捏在手里,倒真不如被狗吃了算了。”铁心兰已痛哭着自马背上摔倒在地,放声痛哭道:“你不是人……

评:幸好我的心已被狗吃了,我真该谢谢那条狗,否则男人的心若被女人捏在手里,倒真不如被狗吃了算了。

249.小鱼儿大笑道:“我也许是个坏蛋,但却绝不是孬种。别人想要我于什么都容易,但谁也休想叫我求饶。”

评:我也许是个坏蛋,但却绝不是孬种。

250.小鱼儿呆了几乎有半盏茶功夫,这才转过身子,故意东张西望,道:“九姑娘在哪里?我怎地瞧不见呀!”这“小鬼”就是这么会体贴女孩子的心意,这句话出来,慕容九妹明知是假的,也可自我安慰一下了。

评:体贴。

251.神锡道长正色道:“这位小施主年纪虽轻,但来日必将为武林放一异彩,成就必定无人能及,又怎会将区区一面铜牌放在心上?”小鱼儿忍不住大笑道:“我为道长跑跑腿没有关系,道长不必如此捧我。”

评:哈哈。

252.小鱼儿道:“后来我瞧见你,居然住在这种地方,居然自己搬桌子端菜,身旁只用了又聋又哑的老头子,我又想,这人若不是圣贤,就必定是我从未见过的大奸大恶之徒,因为世上只有这两种人能做出这样的事。”

评:学到了。

253.小鱼儿拉起她的手,柔声道:“这些事你以后总会知道的,但现在却请你莫要问我。”世上若有什么事能令女子闭起嘴,那就是她心爱的男人温柔的话了。三姑娘果然闭起了嘴,不再问下去。

评:世上若有什么事能令女子闭起嘴,那就是她心爱的男人温柔的话了。

254.铁萍姑道:“只因男人都不喜欢有头脑的女孩子,他们都生怕女孩子比自己强,所以越是聪明的女孩子,就越是要装得愚笨软弱。男人既然天生就觉得自己比女人强,喜欢保护女人,女人为何不让他们多伤些脑筋,多吃些苦。”

评:哈哈。

255.铁萍姑终于忍不住道:“我们歇歇再走吧!”小鱼儿沉声道:“绝不能歇下来,一歇,就再也休想走得动了。”铁萍姑道:“但我……我现在已……”小鱼儿笑道:“你想,我们在这千古以来都少有人来过的神秘洞穴里拉着手散步,这是多么美,多么风流浪漫的事,别人一辈子都不会有这种机会,我们为何不多享受享受?”

评:体贴。

256.只听白夫人又笑道:“公子远来,贱妾竟不能出来一尽地主之谊,盼公子恕罪。”花无缺道:“能与夫人隔帘而谈.在下已觉不胜荣幸。”

评:礼节。

257.若是换了江玉郎,此刻不扑上去抱住她才怪,若是换了小鱼儿,此刻却只怕要一个耳光掴过去,再问她是什么意思了。但花无缺,天下的女人简直都是他的克星。他既不会对任何女人无礼,更不会对她们发脾气。

评:哈哈。

258.苏樱淡淡笑道:“孔明先生的木牛流马,用于战阵之上倒是好的,若用于奉茶待客,就未免显得太霸气了。”言下之意,竟是连诸葛武侯也未放在她眼里。

评:言下之意。

259.哈哈儿大笑道:“姑娘千万别客气,咱们这些人是天生的贱骨头,有人对咱们一客气,咱们就以为他要来动坏主意了。”

评:恩恩。

260.江玉郎哈哈笑道:“还好还好,只不过方才被条疯狗咬了几口。”小鱼儿大笑道:“疯狗素来只咬疯狗的,江兄既没有疯,也未必是狗,怎会有疯狗咬你?”

评:说得好。

261.屠娇娇道:“但你有把握让他娶你么?”苏樱笑道:“越没有把握的事,做起来就越有趣,是么?”

评:越没有把握的事,做起来就越有趣.

262.那少女道:“怎么会没有呢?你可知道,世上每一个城市里,都有一些可怜的女孩子,被一些她素不相识,甚至是她们厌恶的人蹂躏,但她们还不能像你这样尽情一哭,她们还得装出笑脸,去讨好那些蹂躏她们的人。”她的确很会安慰别人,只因她很了解人们的心。

评:了解别人的心。

263.小鱼儿叹道:“你以为她真会死么?她这只不过是吓吓人的。你难道不知道,女人最大的本事,就是一哭二闹三上吊。”

评:女人最大的本事,就是一哭二闹三上吊。

264.小鱼儿冷冷道:“你以为你很聪明么?真正聪明的女人都知道,她无论和哪个男人说话时,懂得的事都该比那男人少一些,你的毛病就是懂的实在太多了,这么样的女人,大多数男人都不敢领教。”

评:真正聪明的女人都知道,她无论和哪个男人说话时,懂得的事都该比那男人少一些。

265.小鱼儿注视着她的脸,良久良久,才叹息着道:“只可惜你太聪明了些,否则说不定我真的会喜欢你了。”苏樱红着脸,咬着嘴唇道:“我听说女人生了孩子后,就会变得笨些的。”
评:我听说女人生了孩子后,就会变得笨些的。

266.小鱼儿眨着眼笑道:“江小鱼的妙计,你自然是永远弄不懂的,你若也和我一样聪明,我就不会娶你做老婆了。”苏樱忍不住咬了他一口,嫣然笑道:“小鱼儿呀小鱼儿,你真是个坏东西。”

评:哈哈。

《天涯明月刀》

267.蔷薇有刺,明月呢?明月有心,所以明月照人。她的名字就叫作明月心。

评:美句。

268.他用力抓起把砂土,和着血塞进自己的嘴。他生怕自己会像野兽般呻吟呼号。他宁可流血,也不愿让人看见他的痛苦和羞辱。

评:硬汉。

269.明月心却叹了口气,道:“何苦,这是何苦?”傅红雪不懂:“何苦?”明月心道:“你明知必胜,又何必去?他明知必死,又何苦来?”这个费人深思的问题,傅红雪却能解释:“因为他是杜雷,我是傅红雪!”

评:每个人都有必须做的事。

270.无论在哪种行业里,能成功的人,一定都是有原则的人。

评:记住。

271.这世上只要有那些“很要脸”的男人存在,就一定会有她们这些“不要脸”的女人。这才是根本的问题,这问题才是永远无法解决的。

评:很有道理。

272.她说这种话,也并不是令人惊讶的事;一个像她这样的女人,若是知道自己不能再用行动去伤害别人时,总是会说些刻毒的话去伤人的。她伤害别人,也许只不过因为要保护自己。

评:说得对。

《圆月弯刀》

273.贫穷并不可耻,可耻的是懒,是脏。

评:对。

274.他总是拼命克制自己,什么法子他都用过,把冰雪塞进自己的裤裆,把头浸在溪水里,用针刺自己的腿,跑步,爬山,翻跟头……在没有成名之前,他绝不让这些事使自己分心,绝不让任何事损耗自己的体力。

评:男子汉不惜如此。

275.凭他的本事,要去偷,去抢,都一定很容易得手。但是他绝不能做这种事,他绝不能让自己留下一个永远洗不掉的污点。他一定要从正途中出人头地。

评:很赞赏。

276.她说得很谨慎:“如果你要别人真心尊敬你,就一定要替别人留下一条路走!”丁鹏道:“我懂!”

评:一定要铭记。

277.每个男人都有野心,都应该有野心,换一种说法,“野心”就是雄心,没有雄心壮志的男人,根本不能算是个男人。

评:记住。

278.但丁鹏却不是铁石人,他是个心肠比铁石更硬的人,因此他反而现出了厌恶的神情道:“谢小姐,如果你要卖弄风情,年纪太轻了,但是要嚎哭撒娇,年纪又太大了,一个女人最令人讨厌,就是做不合自己年龄的事。”

评:一个女人最令人讨厌,就是做不合自己年龄的事。

279.不错,你年轻,喜欢直截了当地说话,只有年纪大的人,才会拐弯抹角,一句最简单的话,也要绕上个大圈子。”

评:只有年纪大的人,才会拐弯抹角.

280.甲子道:“这是主人给我们的算法,如果我们在此只留一年,剑术未精,心气又浮,必须要那么多的银子,才能够安安稳稳地过日子,否则不是沦为盗贼,就是走人歧途,才能满足自己的欲望。”

评:要知道自己,懂得自己。

281.一种无敌的剑法,绝不是在于杀人的威力。惟仁者而能无敌。

评:对。但是为什么刘备没有无敌?

282.一个真正迷人的女人不是在她的暴露,而是在于她懂得掩饰。一个脱光了的女人对男人固然有诱惑的力量,但是这种诱惑力量竟是有限的。

评:一个真正迷人的女人不是在她的暴露,而是在于她懂得掩饰。

283.杜家的长拳是家传的,很有点火候,她们姊妹俩的拳头下也打倒过不少英雄好汉。可是,那青衣女郎只轻轻地一伸手,就握住了她的拳头笑着道:“别开玩笑,我怕痒,可受不了你胳肢……”

评:哈哈。

284.丁鹏笑道:“婊子不是无情,无情又怎能夜夜春宵,颠倒众生,她们是太多情了。”“多情又如何?”“情到浓时情转薄,多情就显得更无情。”“那么她们就没有一点真情了吗?”“不,她们虽然多情又薄情,却不是没有真情,而是她们对男人的花言巧语听多了,用虚情假意也应付多了,把真情深藏心底,不容易发挥出来而已。”

评:情到浓时情转薄,多情就显得更无情。

285.“无欲则刚,无虑则坚。”这两句话谁都会说,读过几天书的人,也都会解释得明明白白,可是能做到这一点却很难。谁都有心中的欲望,所以人的意志才会软弱。谁都有关心挂虑的人,所以人的意志才会动摇。

评:无欲则刚,无虑则坚。

286.备身就是把身上不干净的东西除掉的意思,怕一个男人对女人不干净,除去的自然是那属于男人的部分。这些人的身体干净了,心里面却未必能干净,而对着一大群如花似玉,却偏又有心无物,因此都形成了各种变异的狂态。这些狂态的共同之点就是痛恨女人,尤其是痛恨漂亮的女人,最看不得的就是脱光了衣服的女人。

评:原来如此。

287.高傲的人,也是勇于负责的人。

评:一个人不高傲了,也就变得不敢负责了。

288.“真情是没有条件的,你放不开那些条件,一辈子也得不到真情。还有,真情是要以真情去换取的,你自己没有付出真情,又怎么能企望别人以真情待你?”

评:对。

289.长于心智者必拙于体力,所以一般读书的人,体质上多半是文弱一点。只有练武的人不然,他们的武功进境是配合着心智并进的,武学登入一个新的境界,体能有了超异的成就,智慧也一定跟着圆熟。

评:学到了。

290.小香惟一好处就是永远不跟人抬杠,她相信对方说的每一句话,丁鹏说惟有用刀解决的问题,她就相信的确没有别的方法了。

评:学到了。

291.老妇人一笑道:“只有最笨的女人,才在丈夫面前攻击另一个女人。多少年来,主公对我一直非常尊敬,就因为我知道如何尽一个女人的本分。”

评:哈哈。

292.柳若松悠然地道:“能成非常事业的人,一定要具有非常的耐性。”

评:对的。

《大人物》

293.她本来一直认为自己已经可以算是武林中一等一的高手。现在她才知道了,别人说她高,只不过因为她是田二爷的女儿。这种感觉就好像忽然从高楼上摔下来,这一跤实在比刚才摔得还重。她第一次发现自己并没有想像中那么聪明,那么本事大。她几乎忍不住要自己给自己几个大耳光。

评:别人说她高,只不过因为她是田二爷的女儿。

294.她的手出血,粗糙的石块,锋利如刀。血从她的手指流出,疼痛钻入她的心。她又跌下,跌得更重。但她已不再流泪。这实在是件很奇妙的事――一个人流血的时候,往往就不再流泪。

评:一个人流血的时候,往往就不再流泪。

295.“难道这就是人生?难道这不是人生?”“难道一个人非得这么样活着不可?”她怀疑,她不懂。她不懂生命中本就有许许多多不公平的事,不公平的苦难。你能接受,才能真正算是个人。人活着,就得忍受。忍受的另一种意思就是奋斗!继续不断的忍受,也就是继续不断的奋斗,否则你活得就全无意思。因为生命本就是在苦难中成长的!

评:忍受的另一种意思就是奋斗!

296.天下有哪个女孩子不喜欢占人的便宜。

评:天下有哪个女孩子不喜欢占人的便宜。

297.女人若看到女人折磨男人时,总会觉得很有趣的,但若看到别的女人被男人折磨时,她自己也会气得要命。男人就不同了。男人看到男人被女人折磨,非但不会同情他,替他生气,心里反而会有种秘密的满足,甚至会觉得很开心。

评:说的好。

298.她脱下鞋子,又脱下袜子,看看自己的脚,又忘了要站起来走走。她好像已看得有点迷了。女人看着自己的脚时,常常都会胡思乱想的,尤其是那些脚很好看的女人。脚好像总跟某种神秘的事有某种神秘的联系。

评:G点?。。。

299.“烧香的人走了很远的路之后,一定会很饥,很饥的时候吃东西,总觉得滋味特别好些。”“所以人们总觉得庙里的青菜特别好吃。”“你总算明白了,素斋往往也正是吸引人们到庙里去的最大原因之一。”

评:很对啊。

300.秦歌道:“每个人站着的地方,本来都是平等的,只看你肯不肯往上爬,你若站在那里乘凉,看着别人爬得满头大汗,等别人爬上去之后,再说这世界不平等,不公平,那才是真正的不公平。”他慢慢地接着道:“假如每个人都能明白这道理,世上就不会有那么多仇恨和痛苦存在。”

评:很有道理。

301.你若是男人,最好懂得一件事。若有别的男人在你面前称赞你,不是已将你佩服得五体投地,就是将你看成一文不值的呆子。而且通常都另有目的。但他若在你背后称赞你,就是真的称赞了。女人却不同。你若是女人,也最好明白一件事。若有别的女人不管是在你面前称赞你也好,在你背后称赞你也好,通常却只有一种意思――那意思就是她根本看不起你。她若在你背后骂你,你反而应该觉得高兴才是。

评:恍然大悟。

302.当一个男人和一个女人单独相处时,问话的通常都是女人。这种情况男人并不喜欢,却应该觉得高兴。因为女人若不停地问一个男人各种奇奇怪怪的问题时,无论她问得多愚蠢,都表示她至少不讨厌你。她问的问题越愚蠢,就表示她越喜欢你。但她若连一句话都不问你,你反而在不停地问她。那就糟了。因为那只表示你很喜欢她,她对你却没有太大的兴趣。也许连一点兴趣都没有――一个女人若连问话的兴趣都没有了,她对你还会有什么别的兴趣?这情况几乎从没有例外的。

评:恍然大悟。

303.秦歌道:“你若经历过很多事,忽然发觉所有的事都已成了过去,你若得到过很多东西,忽然发觉那也全是一场空,到了夜深人静时,只剩下你一个人……”他语声更轻,更慢,缓缓地接着道:“到了那时,你才会懂得什么叫寂寞。”

评:对的。

304.秦歌道:“一个女人若在你面前装模作样,就表示她已经很喜欢你。”

评:一个女人若在你面前装模作样,就表示她已经很喜欢你。

305.田思思忍住笑道:“龙交龙,风交风,老鼠交的朋友会打洞,这句话我们也听说过的,但你居然连一个像样的朋友都没有,我真没想到。”

评:龙交龙,风交风,老鼠交的朋友会打洞.

306.柳风骨皱眉道:“以前他能挨得别人五六百刀,现在怎么会连你的手指头都挨不住了?”杨凡道:“以前他还是个穷小子,穷人的骨头总是特别硬些。”柳风骨道:“现在呢?”杨凡道:“人一成了名,就不同了,无论谁只要过一年像他那种花天酒地的日子,就算是个铁人,身子也会被淘空的。”

评:说得对。

307.田思思道:“你过来,这句话不能让别人听见。”田心垂着头,慢慢地走了过来。田思思道:“再过来一点,好……”她忽然用尽平生力气,一个耳光打在田心的脸上。然后她自己也倒在地上,放声痛哭起来。

评:心痛。

《白玉老虎》

308.她忽然觉得很寂寞。一个十七岁的女孩子的寂寞,通常只有一种法子可以解释――一个可以了解她,而且是她喜欢的男人。她找不到这样的男人。

评:哈哈。

309.穿红衣裳的小孩笑了:“你知不知道我为什么喜欢你?”他相信无忌就算知道,也不会说出来的。所以他自己说了出来:“你这个人的骨头真硬,硬得要命!”

评:做一个硬汉。

310.萧东楼道:“要做天下第一高手,除了剑法胜人外,还得要有博大的胸襟和一种百折不回的勇气与决心,那一定要从无数惨痛经验中才能得来。”他苦笑着道:“太聪明的人总是禁不住这种折磨的,一定会想法子去避免,而且总是能够避得过去。”司空晓风道:“没有真正经过折磨的,永远不能成大器。”萧东楼道:“绝对不能。”司空晓风:“可是受过折磨的人,也未必能成大器。”

评:太聪明的人总是禁不住这种折磨的,一定会想法子去避免,而且总是能够避得过去。没有真正经过折磨的,永远不能成大器。

311.无忌道:“你不肯出手,只因为你根本没有把我看在眼里,人生在世,被人如此轻贱,活着又有什么意思?”僵尸道:“你不怕死?”无忌道:“大丈夫生而有何欢?死有何惧?”僵尸盯着他,眼睛里寒光如电。无忌也瞅着他,绝没有一点退缩的意思。

评:人生在世,被人如此轻贱,活着又有什么意思?

312.无忌道:“那么你就应该知道,你若想赢别人的钱,自己也要冒点险。”他笑了笑,又道:“人生中有很多事都是这样子的,有很多很多事……”

评:你若想赢别人的钱,自己也要冒点险。

313.萧东楼道:“他说无论一个人是天生机敏,还是天生勇敢,都不如天生幸运得好。”

评:他说无论一个人是天生机敏,还是天生勇敢,都不如天生幸运得好。

314.焦七太爷吩咐道:“你拿给赵公子去看看!”中年人道:“是。”他用双手捧过去,无忌却用一只手推开了,微笑道:“我用不着看,我信得过这位老爷子。”焦七太爷又盯着他看了半天,才慢慢的点了点头,道:“好,有气派!”

评:好。

315.一个人开始赌的时候,赢得越多越糟,因为他总是会觉得自己手气很好,很有赌运,就会愈来愈想赌,赌得愈大愈好,就算输了一点,他也不在乎,因为他觉得自己一定会赢回来。输钱的就是这种人,因为这种人常常会一下子就输光,连本钱都输光。

评:输钱的就是这种人,因为这种人常常会一下子就输光.

316.瞎子道:“你是个很有教养的女人,很温柔,很懂事,从来不会说让人讨厌的话,更不会做让人讨厌的事,为了别人你宁可委屈自己。”他居然也叹了口气,又道:“像你这样的女人,现在已经不太多了。”

评:你是个很有教养的女人,很温柔,很懂事,从来不会说让人讨厌的话,更不会做让人讨厌的事,为了别人你宁可委屈自己。

317.对某种人来说,“赐予”远比“夺取”更幸福快乐。凤娘无疑就是这种人。

评:对某种人来说,“赐予”远比“夺取”更幸福快乐。

318.漂亮小伙子又道:“那时候正是春天,好像每个人都不愿死在春天里,所以那一阵子棺材店的生意很不好,伙计和木匠都在店里玩纸牌,有个小木匠输光了,正站在门口生闷气,正好看见赵无忌从门口走过去。”

评:那时候正是春天,好像每个人都不愿死在春天里。

319.可是他喜欢赵无忌。每个人都常常会为一些自己喜欢的人,去做一些自己并不喜欢做的事。

评:每个人都常常会为一些自己喜欢的人,去做一些自己并不喜欢做的事。

320.李玉堂道:“如果你们一定要谢我,只有一个法子。”无忌道:“你说。”李玉堂道:“把我当做个朋友。”

评:很好的交朋友的方式。

321.他从来不用言语来表现他对别人的友谊,“少说多做”,才是他做人的原则。直到现在他才开口:“一个人有困难的时候找朋友,绝不是件丢人的事。”

评:一个人有困难的时候找朋友,绝不是件丢人的事。

322.连一莲道:“那么你一定也学会他的十字慧剑。”无忌道:“我父亲叫我去学的,并不是他的剑法,而是他做人的态度,做事的法子。”

评:做人的态度,做事的法子!

323.他认为“运气好”的意思,只不过是“能够把握机会”而已。一个能够把握机会的人,就一定是个运气很好的人。

评:一个能够把握机会的人,就一定是个运气很好的人。

324.只要一个人对自己的生活觉得不满意,你就有机会收买他的。

评:只要一个人对自己的生活觉得不满意,你就有机会收买他的。

325.无忌道:“不是视钱如命的人,怎么能做财神!”

评:不是视钱如命的人,怎么能做财神!

326.这女人又叹了口气,道:“现在我才知道,你真是个聪明人。”无忌道:“为什么?”这女人道:“因为只有聪明的男人才懂得多用眼睛看,少开口说话。”

评:因为只有聪明的男人才懂得多用眼睛看,少开口说话。

327.唐缺微笑道:“无论谁能够活到七八十岁,做事都不会不谨慎的。”

评:无论谁能够活到七八十岁,做事都不会不谨慎的。

328.人生中最悲惨的境界不是生离,不是死别,不是失望,不是挫败。绝不是。人生中最悲惨的境界,就是到了这种无可奈何,别无选择的时候。

评:就是到了这种无可奈何,别无选择的时候。

《大地飞鹰》

329.卜鹰道:“你的确是个很要命的人,脾气怪得要命。骨头硬得要命,有时阔得要命,有时穷得要命,有时要别人的命,有时别人也想要你的命。”

评:哈哈。

330.一个人的慷慨施予,对另一个人来说,有时反而是侮辱。

评:一个人的慷慨施予,对另一个人来说,有时反而是侮辱。

《边城浪子》

331.紫衫少年道:“你知不知道我是谁?”叶开道:“不清楚,我连你究竟是不是个人,都不太清楚。”

评:哈哈。

332.叶开大笑,道:“但自古以来,黯然销魂者,惟别而已,阁下既然取了个如此引人忧思的名字,就当浮一大白。”

评:哈哈。

333.她并不是那种令人一见销魂的美女,但一举一动间都充满了一种成熟妇人的神韵。无论什么样的男人,只要看见她立刻就会知道,你不但可以在她身上得到安慰和满足,也可以得到了解和同情。

评:很好的恭维的话。

334.若是完全没有接触过女人,也许反倒好些――完全没有接触过女人的男人,就像是个严密的堤防,是很难崩溃的。已有过很多女人的男人,也不危险――假如已根本没有堤防,又怎会崩溃。最危险的是,刚接触到女人的男人,那就像是堤防上刚有了一点缺口,谁也不知道它会在什么时候让洪水冲进来。

评:说得好。

335.傅红雪的身子忽然冰凉僵硬,冷汗已湿透被褥。他本不是来享乐的。她将她自己奉献给他,为的也只不过是复仇!

评:你是来干什么的?

336.叶开跷起脚,指着靴底的洞,道,“你看见这两个洞没有?它会咬人的,谁若对我不客气,它就会咬他一口。”翠浓笑了,站起来走过去,笑道:“我倒要看它敢不敢咬我。”叶开一把拉住了她,道:“它不敢咬你,我敢。”翠浓“嘤咛”一声,已倒在他怀里。

评:哈哈。

337.花满天忽然抬起头,盯着他,厉声道:“我辛苦奋斗十余年,到现在还是一无所有,还得像奴才般听命于你,你若是我,你会不会也像我这么做?”马空群想也不想,立刻接口说道:“我会的,只不过……”他目中露出刀一般的光,接着道:“我若做得不机密,被人发现,我也死而无怨。”

评:硬汉子。

338.这人站在那里,看着他,过了很久,才长长叹息了一声,苦笑道:“我现在当然也不必问你究竟是什么人了。”叶开道:“的确已不必。”这人道:“但是,我却想问问你,你究竟是不是个人呐?”叶开大笑。

评:你究竟是不是个人呐

339.丁灵琳咬着牙,用力用指甲掐着叶开的手。叶开道:“你的手疼不疼?”丁灵琳道:“不疼。”叶开道:“我的手为什么会很疼呢?”

评:哈哈。

340.丁灵琳剥了个柿子,送到叶开面前,柔声道:“柿子是清冷的,用柿子下酒不容易醉!”叶开淡淡道:“你怎知我不想醉?”丁灵琳道:“一个人若真的想醉,无论用什么下酒都一样会醉的。”

评:哈哈。

341.陌生人道:“真正伟大的武功,并不是用聪明和苦功就能练出来的。”为什么不是?大家心里都在问。聪明和武功岂非是一个练武的人所需要的最重要的条件?陌生人道:“你一定先得有一颗伟大的心,才能练得真正伟大的武功。”

评:说得对。

《英雄无泪》

342.“你在这里站了两天一夜,就为了要把这张帖子交给我?”“是。”“你有没有想到过,如果你把它留在柜台,我也一样能看得到。”“小人没有去想,”孙达说:“有很多事小人都从来没有去想过,想得太多并不是件好事。”

评:有很多事小人都从来没有去想过,想得太多并不是件好事。

343.“有些人相遇之后也会变的。”卓东来说,“有些人遇到某一个人之后,就会变得软弱一点。”“就像是掺了水的酒?”“是。”

评:有些人遇到某一个人之后,就会变得软弱一点。

344.“每个人都应该关心别人的,可是为了别人折磨自己就不对了。”钉鞋说,“那样子反而会让他关心的人伤心失望的。”

评:说得好。

345.他的声音充满不屑:“一个人如果有价钱,就不值钱了,连一文都不值。”卓东来又闭上了嘴。

评:说得好。

346.“死并不是件困难的事,他也不会随时为朱猛去死,”卓东来说,“只要朱猛活着,他一定也会想法子活下去,因为他要照顾朱猛,他对朱猛就好像一条老狗对他的主人一样。”卓东来冷冷地接着道:“如果他随时都想为朱猛去拼命,这种人也就不值得看重了。”

评:如果他随时都想为朱猛去拼命,这种人也就不值得看重了。

347.人们总是会在一些不适当的时候想起一些不该想的事,这本来就是人类最大的痛苦之一。现在小高是不是又想起了那个不该想的女人?

评:哈哈。

348.“你想激怒我也没有用的。”卓青说:“我绝不会碰你。”“为什么?”“因为我也是男人,我不想以后每天晚上都要想着你在下面的样子来折磨自己。”

评:说得对。

349.“每个人身上都有条看不见的绳子,他一生中大部分时间也都是被这条绳子紧紧绑住的。”萧泪血说,“有些人的绳子是家庭妻子儿女,有些人的绳子是钱财事业责任。”他也凝视着小高:“你和朱猛这一类的人虽然不会被这一类的绳子绑住,可是你们也有你们自己为自己做出来的绳子。”“感情。”萧泪血说,“你们都太重感情,这就是你们的绳子。”

评:绳子。

350.头可断,血可流,精神却永远不能屈服,也永远不会毁灭。这就是江湖男儿的义气,这就是江湖男儿的血性。

评:头可断,血可流,精神却永远不能屈服。

351.他握紧双拳:“我还是朱猛,你还是司马超群,所以我还是要杀你。”这也是一股气,就像是永生不渝的爱情一样,海可枯,石可烂,这股气却永远存在。就因为有这股气,所以这些什么都没有、连根都没有的江湖男儿才能永远活在有血性的人们心里。

评:这股气。

352.因为他们争的并不是生死荣辱成败胜负。他们将世上人们不能舍弃的生死荣辱都置之度外,他们只不过是在做一件他们自己认为自己必须要做的事。因为这是他们做人的原则。头可断、血可流,富贵荣华可以弃如敝履,这一点原则却绝不可弃。

评:原则。

《武林外史》

353.只见他一拍胸膛,道:“我姓熊,名猫儿,打架从来不会输,喝酒从来不会倒,坏毛病不多,书读得不少,这样的男儿,天下哪里找?”

评:哈哈。

354.你这样聪明的人,本该知道,寡妇不但比少女温柔得多,比少女体贴得多,比少女懂得的多,而且服侍男人,也比少女好得多,所以,聪明的男人都宁愿娶寡妇,你难道不愿意?

评:哈哈。

355.结过婚的男人想必都知道,装睡,有时却是对付女人的无上妙着,再狠的女人遇到这一着,也没戏唱了。

评:学到了。

356.那瘦子冷冷道:“你要作主?”沈浪微笑道:“大哥若作不得主,那么也……”他话未说完,那大汉已大声道:“自然是我作主,出了错也是我的。”他怒冲冲地走过去,又唤了三条大汉,立刻就将沈浪他们移到帐篷后的避风处,前面的灯光,也照不到这里。

评:哈哈。激将法。

357.沈浪笑道:“今日想必忙坏你了。”方心骑躬身笑道:“有事可忙,弟子反觉高兴。”

评:说得好。

358.快活王微露笑容,道:“这两天本王心中不免对今日之婚礼有所牵挂,是以别的事便都疏忽了,你却要分外出力才是。”方心骑恭声道:“王爷抬爱,弟子敢不全力以赴。”快活王颔首道:“好……很好……”

评:说得好。

359.沈浪面上又泛起了他那潇洒、懒散、不可捉摸的笑容,淡淡笑道:“无论任何人,都有失败的时候。只要他们胜利时莫要太得意,纵然失败一次,也就算不了什么了……”

评:无论任何人,都有失败的时候。只要他们胜利时莫要太得意,纵然失败一次,也就算不了什么了。

vue2.x的patch方法和客户端激活

vue2.x的patch方法和客户端混合

之前确实没自己看过 vue2.x 的 _update 这一块,导致今天被面试官问到了,现在回头补一下这方面的知识。

从初始化 watcher 说起

我们知道,在声明了响应式数据之后,我们再实例化一个 watcher并调用响应式数据,才能把 watcher 添加到响应式数据的依赖里面获得更新。而 vue 也是这么做的,它对 vm 来实例化一个 watcher,这个 watcher 在声明的时候会立即对回调函数 updateComponent 求值,从而执行 _update 和 _render,从而把这个 watcher 添加到相应的响应式数据里面。源码如下:

// lifecycle.js
callHook(vm, 'beforeMount');
// ...
updateComponent = () => {
    vm._update(vm._render(), hydrating)
};
// ...
new Watcher(vm, updateComponent, noop, {
    before () {
        if (vm._isMounted && !vm._isDestroyed) {
            callHook(vm, 'beforeUpdate')
        }
    }
}, true /* isRenderWatcher */)

这里有一个注意的点,就是这里的 _render 函数其实是调用的 render 函数,而解析 template 并进行静态节点优化以及生成render函数是在 $mount 方法里面进行的,也就是说 template 转化为 render 方法是在 beforeMount 生命周期里面进行的。(如果使用了 vue-loader 的话,那么 vue-loader 里面会直接把 template 打包成 render 方法。)

_render方法干了些什么呢

_render 方法生成了 vnode,代码如下:

// render.js
Vue.prototype._render = function (): VNode {
    vnode = render.call(vm._renderProxy, vm.$createElement)
    return vnode
}

_update方法干了些什么呢

_update 方法其实就是调用__patch__方法,代码如下:

// lifecycle.js
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    if (!prevVnode) {
        // initial render
        vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
        // updates
        vm.$el = vm.__patch__(prevVnode, vnode)
    }
}

这里如果是初次渲染的话,prevNode 是没有的,所以会拿vm.$el这个 DOM 元素去和新的 vnode 进行 patch;如果不是初次渲染的话,就拿旧的 vnode 和 新的 vnode 进行 patch。

__patch__方法干了些什么呢

__patch__方法的源码和解释如下:

function patch (oldVnode, vnode, hydrating, removeOnly) {
    // 如果没有新的 vnode,但有旧的 vnode,就销毁旧的 vnode
    if (isUndef(vnode)) {
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
      return
    }

    let isInitialPatch = false
    const insertedVnodeQueue = []

    if (isUndef(oldVnode)) {
      // 如果旧的 vnode 没有,就表示是第一次渲染,则直接创建
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue)
    } else {
      const isRealElement = isDef(oldVnode.nodeType)
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // 如果旧的 vnode 不是 DOM node的形式,并且新旧节点一样,则使用 patchVnode 进行更新
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
      } else {
        if (isRealElement) {
          // 如果旧的 vnode 是元素节点,并且有服务端渲染标签的话,就移除标签并且进行服务端渲染
          if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
            oldVnode.removeAttribute(SSR_ATTR)
            hydrating = true
          }
          if (isTrue(hydrating)) {
            // 先判断能不能进行混合,能的话,调用 invokeInsertHook 方法进行混合
            if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
              invokeInsertHook(vnode, insertedVnodeQueue, true)
              return oldVnode
            } else if (process.env.NODE_ENV !== 'production') {
              warn(
                'The client-side rendered virtual DOM tree is not matching ' +
                'server-rendered content. This is likely caused by incorrect ' +
                'HTML markup, for example nesting block-level elements inside ' +
                '<p>, or missing <tbody>. Bailing hydration and performing ' +
                'full client-side render.'
              )
            }
          }
          // 如果混合失败,则使用空的节点
          oldVnode = emptyNodeAt(oldVnode)
        }

        // 如果新旧节点不相同,则使用新的 vnode 进行渲染
        const oldElm = oldVnode.elm
        const parentElm = nodeOps.parentNode(oldElm)

        createElm(
          vnode,
          insertedVnodeQueue,
          oldElm._leaveCb ? null : parentElm,
          nodeOps.nextSibling(oldElm)
        )

        // update parent placeholder node element, recursively
        if (isDef(vnode.parent)) {
          let ancestor = vnode.parent
          const patchable = isPatchable(vnode)
          while (ancestor) {
            for (let i = 0; i < cbs.destroy.length; ++i) {
              cbs.destroy[i](ancestor)
            }
            ancestor.elm = vnode.elm
            if (patchable) {
              for (let i = 0; i < cbs.create.length; ++i) {
                cbs.create[i](emptyNode, ancestor)
              }
              const insert = ancestor.data.hook.insert
              if (insert.merged) {
                for (let i = 1; i < insert.fns.length; i++) {
                  insert.fns[i]()
                }
              }
            } else {
              registerRef(ancestor)
            }
            ancestor = ancestor.parent
          }
        }

        // 移除老的 vnode
        if (isDef(parentElm)) {
          removeVnodes([oldVnode], 0, 0)
        } else if (isDef(oldVnode.tag)) {
          invokeDestroyHook(oldVnode)
        }
      }
    }

    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    return vnode.elm
  }

这里简单来说分为下面几种情况:

  1. 如果没有新的 vnode 只有旧的 vnode,就直接摧毁掉旧的 vnode;
  2. 如果没有旧的 vnode,就直接用新的 vnode 创建 DOM 元素
  3. 对比新旧 vnode,如果是 sameVnode,就开始进行 patchVnode
  4. 如果旧的 vnode 是 DOM,并且有 ssr 标记,则清除 ssr 标记并进行客户端激活。(如果激活失败则创建一个空的节点);
  5. 如果旧的 vnode 是 DOM,并且没有 ssr 标记,就用新的 vnode 创建 DOM 元素,并且挂载到新的 vnode 的父节点上面。

上面过程中主要有三点需要注意:

  1. 怎么用 vnode 创建 DOM 元素的?
  2. patchVnode 是怎么进行的?
  3. 怎么进行客户端激活?

怎么创建 DOM 元素

我们首先来看怎么创建 DOM 元素,下面是 createElm 的源码:

function createElm (
  vnode,
  insertedVnodeQueue,
  parentElm,
  refElm,
  nested,
  ownerArray,
  index
) {
  // 如果这个 vnode 之前用到过,则从缓存里面克隆(注意,这里不是直接用以前的 vnode,因为所有的 vnode 都不能相同)
  if (isDef(vnode.elm) && isDef(ownerArray)) {
    vnode = ownerArray[index] = cloneVNode(vnode)
  }

  vnode.isRootInsert = !nested // for transition enter check
  if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
    // 如果是组件vnode(占位vnode),则使用 createComponent 创建这个组件
    return
  }

  // 如果不是组件 vnode,那证明是 div、span 这种 vnode,那么直接用相应平台的方法进行创建
  const data = vnode.data
  const children = vnode.children
  const tag = vnode.tag
  if (isDef(tag)) {
    if (process.env.NODE_ENV !== 'production') {
      if (data && data.pre) {
        creatingElmInVPre++
      }
      if (isUnknownElement(vnode, creatingElmInVPre)) {
        warn(
          'Unknown custom element: <' + tag + '> - did you ' +
          'register the component correctly? For recursive components, ' +
          'make sure to provide the "name" option.',
          vnode.context
        )
      }
    }

    vnode.elm = vnode.ns
      ? nodeOps.createElementNS(vnode.ns, tag)
      : nodeOps.createElement(tag, vnode)
    setScope(vnode)

    /* istanbul ignore if */
    if (__WEEX__) {
      // in Weex, the default insertion order is parent-first.
      // List items can be optimized to use children-first insertion
      // with append="tree".
      const appendAsTree = isDef(data) && isTrue(data.appendAsTree)
      if (!appendAsTree) {
        if (isDef(data)) {
          invokeCreateHooks(vnode, insertedVnodeQueue)
        }
        insert(parentElm, vnode.elm, refElm)
      }
      createChildren(vnode, children, insertedVnodeQueue)
      if (appendAsTree) {
        if (isDef(data)) {
          invokeCreateHooks(vnode, insertedVnodeQueue)
        }
        insert(parentElm, vnode.elm, refElm)
      }
    } else {
      createChildren(vnode, children, insertedVnodeQueue)
      if (isDef(data)) {
        invokeCreateHooks(vnode, insertedVnodeQueue)
      }
      insert(parentElm, vnode.elm, refElm)
    }

    if (process.env.NODE_ENV !== 'production' && data && data.pre) {
      creatingElmInVPre--
    }
  } else if (isTrue(vnode.isComment)) {
    vnode.elm = nodeOps.createComment(vnode.text)
    insert(parentElm, vnode.elm, refElm)
  } else {
    vnode.elm = nodeOps.createTextNode(vnode.text)
    insert(parentElm, vnode.elm, refElm)
  }
}

这里简单来说分为下面三种情况:

  1. 如果这个 vnode 之前用到过,则从缓存里面克隆(注意,这里不是直接用以前的 vnode,因为所有的 vnode 都不能相同)
  2. 如果是组件 vnode(占位vnode),则使用 createComponent 创建这个组件
  3. 如果不是组件 vnode(div、span 这种 vnode),那么直接用相应平台的方法进行创建

需要说明的是(源码就不贴出来了):

  1. 为了区分不同的平台(web或者weex),统一使用 modules 和 nodeOps 进行封装平台方法提供统一的接口。其中 modules 主要负责操作 dom 上的属性(class、style、events等),nodeOps 主要负责 DOM 的各种操作(创建、插入、寻找节点等)。
  2. insert 方法就是把创建的 DOM 元素插入父节点里面去。

createComponent

我们来看一下对于组件 vnode,createComponent 方法是怎么进行创建的:

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
  let i = vnode.data
  if (isDef(i)) {
    const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
    if (isDef(i = i.hook) && isDef(i = i.init)) {
      // 执行组件 vnode 的 init 钩子,
      // 这个钩子会生成一个组件并赋值给 vnode 的 componentInstance 属性,
      // 然后对这个组件执行 $mount 方法进行挂载
      i(vnode, false /* hydrating */)
    }

    // 由于我们在上面生成了 componentInstance 属性,所以我们在这里把组件生成的 DOM 插入到父节点。
    // initComponent 主要处理了 ref 的情况,并对插入时机做了一些调度。
    if (isDef(vnode.componentInstance)) {
      initComponent(vnode, insertedVnodeQueue)
      insert(parentElm, vnode.elm, refElm)
      // 这里是处理 keep-alive 组件的情况
      if (isTrue(isReactivated)) {
        reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
      }
      return true
    }
  }
}

这里首先执行组件 vnode 的 init 钩子,这个钩子会生成一个组件并赋值给 vnode 的 componentInstance 属性,然后对这个组件执行$mount方法进行挂载。挂载完成后就插入父节点,此时组件 vnode 就创建了 DOM 元素。

需要说明的是:

  1. 由于在这里会执行组件的$mount方法,所以这里会执行组件的 beforeCreate、created、beforeMount、mounted 生命周期。这就是为什么子组件的这些生命周期会出现在父组件 mounted 生命周期之前的原因
  2. 这里组件在执行$mount方法的时候又会对自己进行 patch,从而一直 patch 到叶子节点。

patchVnode 是怎么进行的

我们再来看看相同的 vnode 是怎么 patchVnode 的,下面是 patchVnode 的源码:

function patchVnode (
  oldVnode,
  vnode,
  insertedVnodeQueue,
  ownerArray,
  index,
  removeOnly
) {
  // 新老 vnode 完全一样,则直接返回
  if (oldVnode === vnode) {
    return
  }

  if (isDef(vnode.elm) && isDef(ownerArray)) {
    // 缓存 vnode
    vnode = ownerArray[index] = cloneVNode(vnode)
  }

  const elm = vnode.elm = oldVnode.elm

  if (isTrue(oldVnode.isAsyncPlaceholder)) {
    if (isDef(vnode.asyncFactory.resolved)) {
      hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
    } else {
      vnode.isAsyncPlaceholder = true
    }
    return
  }

  // reuse element for static trees.
  // note we only do this if the vnode is cloned -
  // if the new node is not cloned it means the render functions have been
  // reset by the hot-reload-api and we need to do a proper re-render.
  if (isTrue(vnode.isStatic) &&
    isTrue(oldVnode.isStatic) &&
    vnode.key === oldVnode.key &&
    (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
  ) {
    vnode.componentInstance = oldVnode.componentInstance
    return
  }

  let i
  const data = vnode.data
  // 执行 prepatch 钩子
  if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
    i(oldVnode, vnode)
  }

  const oldCh = oldVnode.children
  const ch = vnode.children
  // 执行 update 钩子
  if (isDef(data) && isPatchable(vnode)) {
    for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
    if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
  }
  // 更新文本
  if (isUndef(vnode.text)) {
    if (isDef(oldCh) && isDef(ch)) {
      // 使用 diff 更新子节点
      if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
    } else if (isDef(ch)) {
      if (process.env.NODE_ENV !== 'production') {
        checkDuplicateKeys(ch)
      }
      if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
      // 旧的 vnode 的子节点不存在,直接增加新的 vnode 的子节点
      addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
    } else if (isDef(oldCh)) {
      // 新的 vnode 的子节点不存在,直接删除旧的 vnode 的子节点
      removeVnodes(oldCh, 0, oldCh.length - 1)
    } else if (isDef(oldVnode.text)) {
      nodeOps.setTextContent(elm, '')
    }
  } else if (oldVnode.text !== vnode.text) {
    nodeOps.setTextContent(elm, vnode.text)
  }
  // 执行 postpatch 钩子
  if (isDef(data)) {
    if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
  }
}

简单来说,patchVnode 首先会执行 prepatch 和 update 钩子(用来更新 props、listeners 等),然后更新两个节点的文本,然后对各自的子节点进行更新,如果新旧节点的子节点都存在,则使用diff 算法进行更新,最后执行 postpatch 钩子。

这里需要注意的是:在对子节点进行 patchVnode 的时候,又会对更深的子节点进行 patchVnode 或创建 DOM 元素,进行循环更新,直到叶子节点。

怎么进行客户端激活的

vue 首先会用 hydrate 判断可不可以进行客户端激活,源码如下:

// Note: this is a browser-only function so we can assume elms are DOM nodes.
function hydrate (elm, vnode, insertedVnodeQueue, inVPre) {
  let i
  const { tag, data, children } = vnode
  inVPre = inVPre || (data && data.pre)
  vnode.elm = elm
  // elm 是服务器发回来的DOM,vnode 是客户端创建的 vnode
  // 如果 vnode 是注释节点或静态节点,则直接返回 true
  if (isTrue(vnode.isComment) && isDef(vnode.asyncFactory)) {
    vnode.isAsyncPlaceholder = true
    return true
  }
  // 比较节点的 tag 是否一样
  if (process.env.NODE_ENV !== 'production') {
    if (!assertNodeMatch(elm, vnode, inVPre)) {
      return false
    }
  }
  // 如果 vnode 是组件节点,则使用 init 钩子生成组件节点
  // 在生成组件节点的时候,组件节点自己会调用 patch 方法和 hydrate 方法判断能否激活
  // 所以,只要组件节点生成成功,那么就不需要往下判断了
  if (isDef(data)) {
    if (isDef(i = data.hook) && isDef(i = i.init)) i(vnode, true /* hydrating */)
    if (isDef(i = vnode.componentInstance)) {
      // child component. it should have hydrated its own tree.
      initComponent(vnode, insertedVnodeQueue)
      return true
    }
  }
  // 如果 vnode 是非组件节点,那么递归比较子节点
  if (isDef(tag)) {
    if (isDef(children)) {
      // elm 没有子节点但是 vnode 有子节点,则直接生成子节点插入到 elm 里面
      if (!elm.hasChildNodes()) {
        createChildren(vnode, children, insertedVnodeQueue)
      } else {
        // 比较 vnode 的 v-html 内容是否和 elm 相同
        if (isDef(i = data) && isDef(i = i.domProps) && isDef(i = i.innerHTML)) {
          if (i !== elm.innerHTML) {
            /* istanbul ignore if */
            if (process.env.NODE_ENV !== 'production' &&
              typeof console !== 'undefined' &&
              !hydrationBailed
            ) {
              hydrationBailed = true
              console.warn('Parent: ', elm)
              console.warn('server innerHTML: ', i)
              console.warn('client innerHTML: ', elm.innerHTML)
            }
            return false
          }
        } else {
          // iterate and compare children lists
          let childrenMatch = true
          let childNode = elm.firstChild
          for (let i = 0; i < children.length; i++) {
            if (!childNode || !hydrate(childNode, children[i], insertedVnodeQueue, inVPre)) {
              childrenMatch = false
              break
            }
            childNode = childNode.nextSibling
          }
          // if childNode is not null, it means the actual childNodes list is
          // longer than the virtual children list.
          if (!childrenMatch || childNode) {
            /* istanbul ignore if */
            if (process.env.NODE_ENV !== 'production' &&
              typeof console !== 'undefined' &&
              !hydrationBailed
            ) {
              hydrationBailed = true
              console.warn('Parent: ', elm)
              console.warn('Mismatching childNodes vs. VNodes: ', elm.childNodes, children)
            }
            return false
          }
        }
      }
    }
    // 比较 elm 和 vnode 的 data
    if (isDef(data)) {
      let fullInvoke = false
      for (const key in data) {
        if (!isRenderedModule(key)) {
          fullInvoke = true
          invokeCreateHooks(vnode, insertedVnodeQueue)
          break
        }
      }
      if (!fullInvoke && data['class']) {
        // ensure collecting deps for deep class bindings for future updates
        traverse(data['class'])
      }
    }
  } else if (elm.data !== vnode.text) {
    // 比较 elm 和 vnode 的文本(如果不一样则直接使用vnode的文本)
    elm.data = vnode.text
  }
  return true
}

这里主要做了下面几件事:

  1. 把 elm 赋值给 vnode.elm 储存起来。
  2. 比较 elm 和 vnode 的 tag 是否一样。(生产环境会跳过)
  3. 如果 vnode 是组件节点,则使用 init 钩子生成组件节点。(在生成组件节点的时候,组件节点自己会调用 patch 方法和 hydrate 方法判断能否激活,所以,只要组件节点生成成功,那么就不需要往下判断了)
  4. 如果 vnode 是非组件节点,那么递归比较子节点。
  5. 比较 elm 和 vnode 的 data
  6. 比较 elm 和 vnode 的文本(如果不一样则直接使用vnode的文本)

需要说明的是:

  1. 生产环境会忽略掉大部分错误,直接使用 vnode 对 elm 进行矫正。
  2. 为什么要进行客户端激活?因为如果直接把 vnode 生成 DOM 并挂载是非常耗时间和内存的,所以这里激活的过程就相当于给 elm 使用 vnode 进行 patch 的过程。
  3. 执行 hydrate 方法之后,其实就已经激活完成了,后面会直接使用invokeInsertHook(vnode, insertedVnodeQueue, true)进行插入了。
  4. invokeInsertHook方法会把createElm里面的insert方法缓存起来,最后在整个 root vnode 更新结束后,再一起执行所有的insert方法。

通俗的认识 vnode

vnode 其实并不神秘,它其实就是一堆数据它的皮(elm)组成,它的皮(elm)是指挂载的 DOM,所以每次 patch 的时候会深度遍历比较它的数据,然后更新它的皮(elm);而在客户端激活的时候,只是 vnode 为了尽可能复用服务器发回来的 html(因为DOM操作很昂贵),而在服务器发回来的 html 上修修补补形成一个新的皮而已!!!

Echarts 里面获取纵坐标间距的算法和思考

概述

今天 PM 说,需要把 echarts 图表的纵坐标调成这样:如果全是 4 位数就用 K 为单位。冷静分析,就是说如果纵坐标刻度的间距是四位数,就用 K 为单位。那要如何获取纵坐标刻度的间距呢?我们都知道,Echarts 纵坐标刻度的间距是它自己生成的,而且会变。于是我去看 Echarts 源码碰碰运气,竟然让我发现了 Echarts 的纵坐标刻度的间距的生成方法!!!记录下来,供以后开发时参考,相信对其他人也有用。

参考资料:

Echarts源码1

Echarts源码2

源码

简单来说,它是用如下方法生成的:

// this.data 是数据
const round = true;
const splitNumber = 4;
const max = this.data.reduce((x,y) => x > y ? x : y);
let val = max / splitNumber;

// echart 内部计算 interval 的方法
// https://github.com/apache/incubator-echarts/blob/fd064123626c97b36cbd6da1b5fc73385c280abd/src/util/number.js
const exponent = Math.floor(Math.log(val) / Math.LN10);
const exp10 = Math.pow(10, exponent);
const f = val / exp10; // 1 <= f < 10
let nf;
if (round) {
  if (f < 1.5) { nf = 1; }
  else if (f < 2.5) { nf = 2; }
  else if (f < 4) { nf = 3; }
  else if (f < 7) { nf = 5; }
  else { nf = 10; }
}
else {
  if (f < 1) { nf = 1; }
  else if (f < 2) { nf = 2; }
  else if (f < 3) { nf = 3; }
  else if (f < 5) { nf = 5; }
  else { nf = 10; }
}
val = nf * exp10;

// Fix 3 * 0.1 === 0.30000000000000004 issue (see IEEE 754).
// 20 is the uppper bound of toFixed.
return exponent >= -20 ? +val.toFixed(exponent < 0 ? -exponent : 0) : val;

可以看到:

  1. 间距只和数据的最大值还有 splitNumber 有关,与其它值都没有关系。
  2. Math.log(val) / Math.LN10 是 lgx(以10为对数);Math.log(val) / Math.LN2 是 lgx(以2为对数)

原理

其实刚开始看Echarts的这段代码我也是一脸懵逼的,但是如果仔细看一下还是觉得挺简单的。

首先,对于纵坐标,我们对它有一个期望的分段值,这个期望的分段值是通过最大数/分段数算出来的,代码如下:

// this.data 是数据
const round = true;
const splitNumber = 4;
const max = this.data.reduce((x,y) => x > y ? x : y);
let val = max / splitNumber;

然后,这个val就是我们的期望分段值,它可能是500,也可能是333等不规则的数。我们希望能够把它矫正为一个整十整百整千这样的数。怎么做呢?分为两步,第一步是确认到底是整十还是整百还是整千,其实就是看它的0有多少个。第二步是确定最高位上的数是多少,打个比方就是确定500、3000等里面的5、3。

对于第一步很简单,我们对期望的分段值取10的对数,然后 floor 一下,最后还原就得到了:

const exponent = Math.floor(Math.log(val) / Math.LN10);
const exp10 = Math.pow(10, exponent);

比如说,期望分段值是66666,因为他是整万的,所以我们期望得到10000,用上面的代码计算出来果然是10000.

对于第二步,那就更简单了,只需要把66666/10000 = 6.6进行约分即可,我们可以约成7,那么70000就是我们的实际分段值。所以Echarts用了它自己的一套约分规则:

// 如果传入了 round 参数,则往小的约分
if (round) {
  if (f < 1.5) { nf = 1; }
  else if (f < 2.5) { nf = 2; }
  else if (f < 4) { nf = 3; }
  else if (f < 7) { nf = 5; }
  else { nf = 10; }
}
else {
  // 如果没有传入 round 参数,则往大的约分
  if (f < 1) { nf = 1; }
  else if (f < 2) { nf = 2; }
  else if (f < 3) { nf = 3; }
  else if (f < 5) { nf = 5; }
  else { nf = 10; }
}

实际上,我们这一步可以向上约分一个最近的整数就行了,比如1.5约分为2,4.6约分为5,7.3约分为8,但是Echarts这里只约到1、2、3、5、10这几个数,把大于5的全部越成了10,可能在实际生产环境中是为了好看吧。

最后把我们第一步和第二步的值相乘就得到了最终的分段值,比如第一步是10000,第二步约成了3,那么最终的分段值为30000。不过最后Echarts处理了一下精度问题:

// Fix 3 * 0.1 === 0.30000000000000004 issue (see IEEE 754).
// 20 is the uppper bound of toFixed.
return exponent >= -20 ? +val.toFixed(exponent < 0 ? -exponent : 0) : val;

总结

1.怎么样?是不是很简单呢?

2.以后在其它地方进行分段的时候,可以参考这里的分段代码了。

利用koa搭建jsonp API和restful API

概述

最近学习利用koa搭建API接口,小有所得,现在记录下来,供以后开发时参考,相信对其他人也有用。

就目前我所知道的而言,API有2种,一种是jsonp这种API,前端通过ajax来进行跨域请求获得数据;另一种是restful API,前端通过fetch或者axios进行cors请求来获得数据。

参考资料:《Koa2进阶学习笔记》KOA docs

建一个服务器

首先用koa来建一个服务器,在这个过程中,我们用到了koa-logger中间件,它会在服务端显示各种请求操作记录,便于我们开发和调试。

//app.js
'use strict'
const Koa = require('koa');
const logger = require('koa-logger');

const app = new Koa();

app.use(logger());

app.listen(3000, () => {
    console.log('koa starts at port 3000!');
})

注意:如果不能运行的话,就换一个端口!

babel

根据官网文档,如果node版本大于7.6,就可以无痛使用async方法,否则要加入babel-register库和transform-async-to-generator库,并且在app.js里面加入如下代码:

require('babel-register');

而且要在.babel文件中,至少有如下代码:

{
  "plugins": ["transform-async-to-generator"]
}

路由

我们需要给jsonp的API新建一个路由,这样就不影响其它路由了。(我们可能会在其它路由搭建restful api,也可能在其它路由搭建静态页面等等。)

'use strict'
const Koa = require('koa');
const logger = require('koa-logger');
const Router = require('koa-router');

const app = new Koa();

app.use(logger());

// 子路由1:主页
let routerHome = new Router();
routerHome.get('/', async (ctx) => {
    ctx.body = '欢迎欢迎!';
})

// 子路由2:jsonp api
let routerJsonp = new Router();
routerJsonp.get('/data1', async (ctx) => {
    ctx.body = '数据';
})

// 装载所有路由
let router = new Router();
router.use('/', routerHome.routes(), routerHome.allowedMethods());
router.use('/jsonp', routerJsonp.routes(), routerJsonp.allowedMethods());
app.use(router.routes()).use(router.allowedMethods());

app.listen(3000, () => {
    console.log('koa starts at port 3000!');
})

现在,先用node运行app.js文件,然后用浏览器访问http://localhost:3000/会看到欢迎欢迎四个字,访问http://localhost:3000/jsonp/data1会看到数据两个字。

jsonp

jsonp的机制是,我们传给服务器一个callback参数,值是我们要调用的函数名字,然后服务器返回一个字符串,这个字符串不仅仅是需要返回的数据,而且这个数据要用这个函数名字包裹。

所以我们需要做如下事情:

  1. 解析请求所带的参数,并且读取callback参数的值。解决方法是,我们用ctx.request.query获得请求所带的所有参数,然后读取出callback参数:ctx.request.query.callback。
  2. 把数据转化为字符串,并用这个函数名包裹。这个很简单,字符串连接即可。

代码如下:

'use strict'
const Koa = require('koa');
const logger = require('koa-logger');
const Router = require('koa-router');

const app = new Koa();

app.use(logger());

// 子路由1:主页
let routerHome = new Router();
routerHome.get('/', async (ctx) => {
    ctx.body = '欢迎欢迎!';
})

// 子路由2:jsonp api
let routerJsonp = new Router();
routerJsonp.get('/data1', async (ctx) => {
    let cb = ctx.request.query.callback;
    ctx.type = 'text';
    ctx.body = cb + '(' + '"数据"' + ')';
})

// 装载所有路由
let router = new Router();
router.use('/', routerHome.routes(), routerHome.allowedMethods());
router.use('/jsonp', routerJsonp.routes(), routerJsonp.allowedMethods());
app.use(router.routes()).use(router.allowedMethods());

app.listen(3000, () => {
    console.log('koa starts at port 3000!');
})

最后用node运行app.js文件就大功告成了!!!我们首先在控制台引入jquery库,然后调用jquery的ajax方法,进行jsonp请求,就能获得“数据”了。

首先我们在浏览器的控制台引入jquery库:

var myScript = document.createElement('script');
myScript.src = 'https://cdn.bootcss.com/jquery/3.3.1/jquery.js';
document.getElementsByTagName('head')[0].appendChild(myScript);

然后用ajax方法进行jsonp跨域请求数据,就可以在控制台看到“数据”二个字了。

$.ajax({
    url : 'http://localhost:3000/jsonp/data1',
    dataType : 'jsonp',
    type : 'get',
    success : function(res){
        console.log(res);
    },
    error: function() {
        alert("网络出现错误,请刷新!");
    }
});

注意:由于原页面是http页面,所以不能在https页面进行jsonp跨域请求。找一个http页面进行jsonp跨域请求测试吧。

restful API

其实搭建restful API很简单,引入cors中间件即可,不需要设置请求头为Access-Control-Allow-Origin,这个中间件会自动帮我们设置。我们先引入中间件,然后重开一个路由存放restful API即可,代码如下:

'use strict'
const Koa = require('koa');
const logger = require('koa-logger');
const Router = require('koa-router');
const cors = require('@koa/cors');

const app = new Koa();

app.use(logger());

app.use(cors());

// 子路由1:主页
let routerHome = new Router();
routerHome.get('/', async (ctx) => {
    ctx.body = '欢迎欢迎!';
})

// 子路由2:jsonp api
let routerJsonp = new Router();
routerJsonp.get('/data1', async (ctx) => {
    let cb = ctx.request.query.callback;
    ctx.type = 'text';
    ctx.body = cb + '(' + '"数据"' + ')';
})

// 子路由3:restful api
let routerRest = new Router();
routerRest.get('/data1', async (ctx) => {
    ctx.body = 'rest数据';
})

// 装载所有路由
let router = new Router();
router.use('/', routerHome.routes(), routerHome.allowedMethods());
router.use('/jsonp', routerJsonp.routes(), routerJsonp.allowedMethods());
router.use('/restful', routerRest.routes(), routerRest.allowedMethods());
app.use(router.routes()).use(router.allowedMethods());

app.listen(3000, () => {
    console.log('koa starts at port 3000!');
})

然后利用下面的请求代码,就会在控制台输出**“rest数据”**。

$.ajax({
    url : 'http://localhost:3000/restful/data1',
    type : 'get',
    success : function(res){
        console.log(res);
    },
    error: function() {
        alert("网络出现错误,请刷新!");
    }
});

改进

不得不说,我们的api是非常简陋的,我们考虑对它做如下改进:

  1. 支持post请求。这个很好办,在路由那里添加post方法即可。
  2. 支持yaml数据导入,然后通过api,把数据作为resonse发送出去。这个需要2步操作,第一步用node导入并解析yaml数据为对象,第二步把对象转化为字符串传递出去。第一步需要用到fs库和yamljs库,第二步需要用到JSON.stringify方法。具体代码如下:

首先支持post请求:

'use strict'
const Koa = require('koa');
const logger = require('koa-logger');
const Router = require('koa-router');
const cors = require('@koa/cors');

const app = new Koa();

app.use(logger());

app.use(cors());

// 子路由1:主页
let routerHome = new Router();
routerHome.get('/', async (ctx) => {
    ctx.body = '欢迎欢迎!';
})

// 子路由2:jsonp api
let routerJsonp = new Router();
routerJsonp.get('/data1', async (ctx) => {
    let cb = ctx.request.query.callback;
    ctx.type = 'text';
    ctx.body = cb + '(' + '"数据"' + ')';
}).post('/data1', async (ctx) => {
    let cb = ctx.request.query.callback;
    ctx.type = 'text';
    ctx.body = cb + '(' + '"数据"' + ')';
})

// 子路由3:restful api
let routerRest = new Router();
routerRest.get('/data1', async (ctx) => {
    ctx.body = 'rest数据';
}).post('/data1', async (ctx) => {
    ctx.body = 'rest数据';
})

// 装载所有路由
let router = new Router();
router.use('/', routerHome.routes(), routerHome.allowedMethods());
router.use('/jsonp', routerJsonp.routes(), routerJsonp.allowedMethods());
router.use('/restful', routerRest.routes(), routerRest.allowedMethods());
app.use(router.routes()).use(router.allowedMethods());

app.listen(3000, () => {
    console.log('koa starts at port 3000!');
})

然后我们新建jsonp.data1.yaml文件作为jsonp API的原始数据,新建restful.data1.yaml作为restful API的原始数据。

//jsonp.data1.yaml
api: "jsonp"
info:
  version: "0.0.1"
  title: test for jsonp

//restful.data1.yaml
api: "restful"
info:
  version: "0.0.1"
  title: test for restful

然后我们添加fs库(node自带,不需要install)和yamljs库进行导入和解析yaml文件,并且用JSON.stringify方法把json对象转化为字符串:

'use strict'
const Koa = require('koa');
const logger = require('koa-logger');
const Router = require('koa-router');
const cors = require('@koa/cors');
const fs = require('fs');
const YAML = require('yamljs');

const app = new Koa();

app.use(logger());

app.use(cors());

// 子路由1:主页
let routerHome = new Router();
routerHome.get('/', async (ctx) => {
    ctx.body = '欢迎欢迎!';
})

// 子路由2:jsonp api
let routerJsonp = new Router();
routerJsonp.get('/data1', async (ctx) => {
    let cb = ctx.request.query.callback;
    ctx.type = 'text';
    ctx.body = cb + '(' + JSON.stringify(YAML.parse(fs.readFileSync('./jsonp.data1.yaml').toString())) + ')';
}).post('/data1', async (ctx) => {
    let cb = ctx.request.query.callback;
    ctx.type = 'text';
    ctx.body = cb + '(' + JSON.stringify(YAML.parse(fs.readFileSync('./jsonp.data1.yaml').toString())) + ')';
})

// 子路由3:restful api
let routerRest = new Router();
routerRest.get('/data1', async (ctx) => {
    ctx.body = YAML.parse(fs.readFileSync('./restful.data1.yaml').toString());
}).post('/data1', async (ctx) => {
    ctx.body = YAML.parse(fs.readFileSync('./restful.data1.yaml').toString());
})

// 装载所有路由
let router = new Router();
router.use('/', routerHome.routes(), routerHome.allowedMethods());
router.use('/jsonp', routerJsonp.routes(), routerJsonp.allowedMethods());
router.use('/restful', routerRest.routes(), routerRest.allowedMethods());
app.use(router.routes()).use(router.allowedMethods());

app.listen(3000, () => {
    console.log('koa starts at port 3000!');
})

用前面类似的方法进行请求,可以看到返回了如下数据,并且支持了post请求。

//jsonp接口
Object {api: "jsonp", info: Object}

//restful接口
Object {api: "restful", info: Object}

其它

到这里就全部完成了,我尽量一点一点地浅显的写出来。实际上还有更多可以优化的地方:

  1. 支持匹配各种模糊的url路径
  2. 支持对传入参数进行处理。
  3. 支持https

而且我们再一次看到,学习koa其实就是各种中间件和api的学习罢了。

最后写一下需要install的库:(虽然可以通过require推测出来)

"@koa/cors": "^2.2.1",
"koa": "^2.5.1",
"koa-bodyparser": "^4.2.0",
"koa-logger": "^3.2.0",
"koa-router": "^7.4.0",
"yamljs": "^0.3.0"

本文代码存放在我的github的blog_server仓库的demo文件夹里面。

函数式编程——惰性链

概述

这是我读《javascript函数式编程》时,对链式编程的总结与思考,供以后开发时参考,相信对其他人也有用。

chain

在jquery和underscore里面,它们的内建对象$和_的方法通常会返回一个this,然后就实现了链式调用,这种调用非常方便。

惰性链

惰性就是延迟执行的意思,在编程的世界里,惰性XX听起来非常高大上,但是实现起来也许非常简单。我下面的代码就实现了一个惰性链:

function lazyChain(fn) {
  var calls = [];
  return {
    invoke: function(funcStr, ...args){
      calls.push(function(target) {
        if(target === undefined) return fn[funcStr](...args);
        return fn[funcStr](target, ...args);
      });
      return this;
    },
    force: function() {
      return calls.reduce((accu, curFunc) => {
        if(accu === undefined) return curFunc();
        return curFunc(accu);
      }, undefined);
    }
  };
}

有以下几点需要说明:

  1. 使用了es6的...args来取剩余参数。
  2. 使用了array函数的reduce方法。
  3. 主要原理是,在使用invoke调用函数时,实际上没有调用,而是把函数储存在calls这个数组里面,等到执行force方法的时候,再一个个地调用函数。

使用方法如下:

函数无参数的情形:

const fn1 = {
  hello: function() {
    console.log('hello');
  },
  mark1: function() {
    console.log(',');
  },
  world: function() {
    console.log('world');
  },
  mark2: function() {
    console.log('!');
  }
};

//此时没有任何输出,因为是惰性的。
const words = lazyChain(fn1).invoke('hello').invoke('mark1').invoke('world').invoke('mark2');

//此时输出:hello , world !
words.force();

函数有参数的情形:

const fn2 = {
  add: function(a,b) {
    return a+b;
  },
  minus: function(a,b) {
    return a-b;
  },
  multiply: function(a,b) {
    return a*b;
  },
  divide: function(a,b) {
    return a/b;
  }
};

lazyChain(fn).invoke('add', 3, 4).invoke('minus', 2).invoke('multiply', 2).invoke('divide', 4).force(); //相当于(3+4-2)*2/4

chain链的改进

无论是juqery还是underscore里面的链式调用,调用的方法必须是jq对象或者underscore对象拥有的方法,那么如果我们想要调用它所没有的方法,应该怎么优化呢?

我们可以类似上面那样,把方法传入一个数组里面,然后最后用force函数执行,代码如下:

function optiChain() {
  var calls = [];
  return {
    invoke: function(func, ...args){
      calls.push(function(target) {
        if(target === undefined) return func(...args);
        return func(target, ...args);
      });
      return this;
    },
    force: function() {
      return calls.reduce((accu, curFunc) => {
        if(accu === undefined) return curFunc();
        return curFunc(accu);
      }, undefined);
    }
  };
}

使用方法如下:

函数无参数的情形:

function hello() {
    console.log('hello');
}

function mark1() {
    console.log(',');
}

function world() {
    console.log('world');
}

function mark2() {
    console.log('!');
}

//此时输出:hello , world !
optiChain().invoke(hello).invoke(mark1).invoke(world).invoke(mark2).force();

//如果不用optiChain则需要这么写
hello();
mark1();
world();
mark2();

函数有参数的情形:

function add(a, b) {
    return a + b;
}

function minus(a, b) {
    return a - b;
}

function multiply(a, b) {
    return a * b;
}

function divide(a, b) {
    return a / b;
}

//相当于(3+4-2)*2/4
optiChain().invoke(add, 3, 4).invoke(minus, 2).invoke(multiply, 2).invoke(divide, 4).force();

//如果不用optiChain则需要这么写
divide(multiply(minus(add(3, 4), 2), 2), 4);

可以看到,使用optiChain避免了难看的多条调用或者难看的嵌套调用,使代码变得非常清晰!!!

过年搭出租车攻略

概述

今天坐出租车被司机坑了,回来就赶紧上网查攻略。

我们难免会到小城市去,小城市管理混乱,出租车司机经常不打表瞎要价或者绕远路,如果碰到这些然后被坑,真的会很闹心。所以我把我查的攻略记录在这里,方便自己以后搭出租车,相信对大家也有用。

小城市可以考虑优先用滴滴打车,计费公正透明,如果实在用滴滴也打不到车才用以下方法。

先说结论

搭车时一定要做以下事情:

**1.记住或者拍下车牌号。**方便你以后投诉,司机看见你这么做也会放乖的。

**2.询问打不打表。**作为司机违规的重要依据,司机说出不打表会非常理亏。而且这也是司机试探你的标志,你不问打不打表,司机会认为你“很年轻”,然后宰你。

**3.询问司机是否听明白了目的地。**作为绕远路或者把你带到其它地方去的证据。

4.事先用百度地图查要走多少公里,在价格有争议或者绕远路的时候作为重要证据。(在价格有争议的时候问清楚起步价和1公里多少钱)

5.有必要的话,可以拍照,录音或者录像。最好在有价格争议的时候录音。也可以在上车时就录音。

过年该不该涨价

根据劳动法规定,节假日工作享受2-3倍工资。但是不包括出租车司机。

所以非法定节日(春节)期间,出租车司机没有任何理由涨价。

就算是法定节日(春节),出租车司机也没有理由涨价,但你可以加1-3元辛苦费。毕竟是过节。

绕远路或者瞎收费

绕远路:用百度地图的法子。

瞎收费:可以用百度地图的法子先理论并且拒付车钱。如果一定要付的话,如果打卡就索要票据,然后投诉;没打卡可以录音,拍照,录像保留证据,然后投诉。(当然我们的目的并不是投诉,而是让司机知道我们有证据,然后老实给出合理价钱)

中途扔下你并收你少量钱

按照相关法律法规,出租车营运人员在未经乘客允许的情况下不得擅自中途甩客或私自更改目的地。

不管收不收钱,都是违规的。即使收钱,你也可以一分钱都不给他。就算给,也要通过录音录像拍照留下证据。

几个法子

如果你肯定出租车司机路说多了,或者路走多了,或者绕远路了,你可以选择叫司机原路返回,如果不是你说的,你就给双倍价钱。

如果你和司机实在商量不下来,你也实在不想付钱,你可以叫司机把你带到交通局去,让交通局仲裁,如果你输,你给司机3倍以上车钱。

最后

还是要努力赚钱买车啊。

flex弹性布局心得

概述

最近做项目用flex重构了一下网页中的布局,顺便学习了一下flex弹性布局,感觉超级强大,有一些心得,记录下来供以后开发时参考,相信对其他人也有用。

参考资料:

Solved by Flexbox

Flex 布局教程:语法篇

flex基础

flex基础语法可以参考上面阮一峰的flex布局教程。简要如下:

display: flex;
justify-content: space-between; //子元素横向排列方式
align-items:center; //子元素纵向排列方式

注意:父元素声明为flex之后,子元素不需要声明为flex。

强大的flex: 1

在布局的时候,我们经常会遇到需要让子元素的宽度随着父元素的宽度改变的情况,即子元素需要自己撑满父元素。比如粘性页脚,让高度未知的页脚粘在高度未知的父元素的底部。这个时候只需要加上下面的css即可:

// 父元素声明为flex,排列方式为上下排列
.Site {
  display: flex;
  min-height: 100vh;
  flex-direction: column;
}

// 要自己撑满父元素的子元素加上这个class(不是页脚哦~)
.Site-content {
  flex: 1;
}

注意:利用flex:1和父div包裹可以实现各种强大的布局。如果不行的话,就给它包一层flexbox的父元素轻松解决啦~

深入flex: 1

flex属性是flex-grow, flex-shrink 和 flex-basis的简写,默认值为0 1 auto。后两个属性可选。

该属性有两个快捷值:auto (1 1 auto) 和 none (0 0 auto)。

flex-grow属性定义了项目的 放大比例,默认为0,就是不放大。

flex-shrink属性则定义了项目的缩小比例,默认为1,就是如果空间不足的话,项目将缩小。

flex-basis定义了项目的本来大小,基本相当于width或者height。

注意:这里有一个坑,就是低版本浏览器在解析flex和width属性的时候会发生冲突,表现出来就像是flex-wrap不生效的样子,当初解决这个问题花了我3.5个小时。所以一般对于flexbox不直接写width: 50%,而是用flex: 0 0 50%来代替;如果width是具体的值width: 200px,则用flex: 0 0 200px代替

写在 2021 年的结尾(上)

写在 2021 年的结尾

时隔一年,我又开始记录起自己的经历了,主要是今天感觉到了自己**观念的转变,不记录一下总感觉会闷在心里会变成一个闷葫芦。

事情还要从半个月前说起,我妈的朋友有个亲戚的女儿也在深圳,然后经过一番牵线搭桥,我和她就联系上了。

但是当时我的背景是工作的并不顺心,想离职;但是我自认为我在工作中挺有同理心的,做事也有自己的风格,在公司也有几个小迷妹,和她聊天应该能展示自己的人格魅力然后走向人生圆满,毕竟我对她的要求不高,不胖就行哈哈。

后来我发现我错了,就像很多人说的一样,要把工作和生活分开。工作和生活确实是不同的,而经常让别人把工作和生活分开的我,竟然也把工作和生活混为一谈了。

继续说我们的事,她当时也想离职,然后我们以离职为契机,开始互相发消息聊天了。俗话说,男女搭配,干活不累,我每天也动力满满,然后我竟然把这大部分动力都花在了编程上,因为我要做出一些东西来,方便找下一份工作。我每天早上8点多就到了公司,开始做自己规划的项目(不是工作),然后10点开始上班才开始工作;中午做完leetcode每日一题;晚上睡前花1-2个小时继续做自己规划的项目。动力满满,乐此不疲。

好景不长,随着我们互相了解,我们的聊天也没有什么新内容了,我的生活也基本上是二点一线,没有什么新奇的事情,虽然在工作中我肆意驰骋,把酒言欢,但是生活的匮乏,让我感到了不安。

于是我开始寻求改变,我想了一些改变的方式,但是迫于自身的局限性,也只想到了约饭、爬山、去公园这些方式。吃苦耐劳的我肯定选择了爬山吖,因为还可以锻炼身体呢。于是自己下了一个决心,明年之前要爬完深圳所有的山,我下的决心,再苦再累,流汗流血都会把它完成。

就算这样,我的生活也没有注入新奇,依旧平平无奇,丝毫没有吸引力。所以该来的还是来了,可能是我的平淡,也可能是我无意的冒犯,她对我逐渐失去了兴趣,我发消息她很长时间就不回了。我很焦急,特别焦急,但是我之前有经验,我知道我不能焦急,不能冲动,如果一冲动,就可能会做出不可挽回的事情,我又不是没有做过。最后她还是回消息了,我当时也有点生气,就草草聊完了。

当时我既有点生气也有点恐慌,生气她没回我,恐慌我们就这样完了。我开始胡思乱想,但是我知道,越是这个时候我越不能胡思乱想,越不能焦急,于是我只能看别人的经验,从中了解到了生活和新奇和吸引力的问题,于是我冷静了下来,仿佛看到了另一个世界——生活的世界,我们太过沉浸在工作中,忙于自我提升,忙于完成任务,忙于工作中的杂事,却忘了怎么生活。工作俨然占据了我们每天所有的时间,我们虽然说不至于行尸走肉,但貌似也没有为我们自己活过,我们一直在为工作而活!

这就是2021年我意识到的,也是想记录的,即使每天忙于工作,也不要忘了生活,要做一些新奇的,有吸引力的事情,为自己而活。

同时这也是我2021年关于感情和生活的年终总结,概括来说就是很失败,希望来年能给自己交一份满意的答卷。

create-react-app脚手架中配置webpack的方法

概述

create-react-app脚手架中的react-scripts能够(1)帮我们自动下载需要的webpack依赖;(2)自己写了一个nodejs服务端脚本代码;(3)使用express的Http服务器;(4)并帮我们隐藏了配置文件

那么假如我们需要额外配置webpack该怎么办呢?比如添加md的loader。

我总结了2种方法,供以后开发时参考,相信对其他人也有用。

方法一

运行如下命令即可把配置文件显示出来:

npm run eject
//然后输入Y

输入后项目目录会多出一个congfig文件夹,里面就有webpack的配置文件。

但是此过程不可逆,所以显现回来后就不能再隐藏回去了。

方法二

(以修改babel插件实现按需加载antd为例,修改其它配置可以仿照这个方法来做。)

(1)使用 react-app-rewired (一个对 create-react-app 进行自定义配置的社区解决方案)和babel-plugin-import(一个babel的管理加载的插件)。

$ yarn add react-app-rewired --dev
$ yarn add babel-plugin-import --dev

//或者使用npm
npm install react-app-rewired --dev
npm install babel-plugin-import --dev

(2)修改package.json 里的启动配置。

/* package.json */
"scripts": {
    "start": "react-app-rewired start",
    "build": "react-app-rewired build",
    "test": "react-app-rewired test --env=jsdom",
}

(3)在项目根目录创建一个 config-overrides.js 用于修改默认配置。

/* config-overrides.js */
const { injectBabelPlugin } = require('react-app-rewired');

module.exports = function override(config, env) {
    config = injectBabelPlugin(['import', { libraryName: 'antd', libraryDirectory: 'es', style: 'css' }], config);
    return config;
  };

Vue2.x是怎么收集依赖的

概述

说到 vue 的响应式原理,我们都能很快答出数据劫持和发布者订阅者模式,通过 Object.defineProperty 来劫持 getter 和 setter,在 getter 的时候订阅依赖,在 setter 的时候发布响应执行依赖,从而达到响应式的目的。

但是如果深入一点,它是怎么收集、发布、管理依赖的呢?或者说,源码里面的 defineReactive、Dep、Watcher 之间有什么样的关系呢?

我通过自己写了一个简易的响应式系统弄懂了这些,记录下来,供以后工作时参考,相信对其他人也有用。

小型的响应系统

为了回答上面的问题,我打算写一个简易的的响应式系统,为了简便,这个系统只考虑没有嵌套的对象,代码如下:

function defineReactive(obj, key, val) {
    const dep = new Dep();

    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get() {
            if (Dep.target) {
                dep.depend();
            }
            return val;
        },
        set(newVal) {
            val = newVal;
            dep.notify();
        }
    });
}

class Dep {
    constructor() {
        this.subs = [];
    }

    addSub(sub) {
        this.subs.push(sub);
    }

    removeSub() {
        const index = this.subs.indexOf(sub);
        if (index > -1) {
            this.subs.splice(index, 1);
        }
    }

    depend() {
        if (Dep.target) {
            Dep.target.addDep(this);
        }
    }

    notify() {
        const subs = this.subs.slice();
        for (let i = 0, l = subs.length; i < l; i++) {
            subs[i].update();
        }
    }
}

Dep.target = null;

class Watcher {
    constructor(cb) {
        this.getter = cb;
        this.deps = [];
        this.value = this.get();
    }

    get() {
        Dep.target = this;
        const value = this.getter();
        console.log('Dep.target', Dep.target);
        return value;
    }

    addDep(dep) {
        dep.addSub(this);
    }

    update() {
        const value = this.get();
    }
}

const obj = {};

defineReactive(obj, 'text', 'Hello World!');

const watcher = new Watcher(() => {
    document.querySelector('body').innerHTML = obj.text;
});

把上面的代码复制到浏览器的控制台运行,就可以看到浏览器里面出现了Hello World,然后我们继续在控制台输入obj.text = 'Define Reactive',可以看到浏览器里面的Hello World就变成了Define Reactive

结合 Vue 的生命周期

我们把上面的例子带入 Vue 的生命周期里面看:

1.首先我们知道,在beforeCreate阶段,Vue会进行各种初始化,比如事件、生命周期等,其实就等效于这段代码:

const obj = {};

2.在created阶段,Vue会初始化状态:data、compute、props、methods等,那其实就等效于这段代码:

defineReactive(obj, 'text', 'Hello World!');

3.在beforeMount阶段,Vue会对 vm 建立一个 watcher,当变动的时候,使用 update 进行更新,这就等效于这段代码:

const watcher = new Watcher(() => {
    document.querySelector('body').innerHTML = obj.text;
});

总结

1.必须先用defineReactive设置为响应式的,然后才能在watcher里面实现依赖收集,并且实例化多个watcher都能被收集到。

2.总的来说,每一个 dep 实例其实就是一个订阅中心,它通过闭包存在,然后在通过 defineReactive 把数据转变为响应式之后,此时的数据没有任何变化,但是一旦实例化了一个 watcher,这个 watcher 如果引用了这个数据(getter),那么数据就会自动把这个 watcher 添加到自己的订阅中心,当改变这个数据的时候(setter),也会自动发布通知,达到更新订阅的 watcher 的目的。

3.vue2.x的源码的响应式系统除了上面的代码,还做了很多别的工作,比如观测嵌套对象、观测数组、处理已经存在的getter和setter、对 watcher 进行调度等等。

用户认证和授权

概述

因为做的项目涉及到用户认证和授权,所以好好总结了一下这块。

认证和授权

一般我们说的认证主要指的是用户登录认证;一般我们说的授权主要是第三方授权

用户登录认证主要有2种方法,一种是基于session-id的认证,另一种是基于token的认证。

第三方授权主要是Oauth2.0标准的授权,它主要包括授权码模式的授权和简单模式的授权。

基于session-id的认证

基于session-id的认证是比较传统的认证方式,它包含以下几个步骤

  1. 用户输入用户名和密码,发送给服务器。
  2. 服务器验证这个用户名和密码,如果成功就生成一个会话id(session-id),然后把这个会话id同时保存在服务器和浏览器的cookie里面。因为保存的地方是浏览器的cookie,所以这种认证方式也叫基于cookie的认证方式。
  3. 用户每次请求资源,都会把这个会话id发送给服务器,服务器端就在本地找这个会话id,如果能够找到,就返回用户请求的数据。
  4. 当用户退出登录时,会话同时在客户端和服务器端被销毁。

对于这种方式,我们前端的处理方式是:

  1. 发送用户名和密码给服务器,并接收服务器返回的会话id。
  2. 服务器会自动把会话id储存在浏览器的cookie里面,不需要我们前端处理。
  3. 由于在发送http请求的时候会自动带上cookie里面的数据,所以在发送其它http请求的时候不需要前端特殊处理。
  4. 用户登出的时候(注意是用户主动注销或者退出登录),前端需要请求一个登出接口,然后服务器会销毁储存在浏览器cookie中的会话id。

需要注意的是,上面所有的一切都由服务器来完成,前端只请求接口就行了。服务器能够自己把会话id写入到浏览器的cookie里面。但是对于restful api,服务器是无状态的,所以在第2步需要前端手动把会话id储存在cookie里面,在第4步需要前端手动把会话id从cookie里面删除。

基于token的认证

基于token的认证主要是指json web token认证,即jwt认证。

它主要包含如下几个步骤

  1. 用户输入用户名和密码,发送给服务器。
  2. 服务器验证用户名和密码,成功则返回一个token(可以认为是一个很长的字符串)给浏览器。
  3. 后续每次请求,浏览器都会把token发送给服务器,服务器验证token是否有效,有效则返回用户请求的数据。
  4. 当用户退出登录时,浏览器端销毁这个token就行了。

需要注意的是,jwt认证有一个很重要的特点:服务器端没有储存token

为什么服务器端不需要储存token呢,我们来看一下token的生成过程。

jwt这个字符串是由三部分组成:header(头部),payload(主体信息),signature(签名)。

头部是给JWT的基本信息,然后通过加密(base64加密)得到的。通过解密可以得到原始信息。比如下面这段基本信息:类型是jwt,签名算法是HS256。

{
  "typ": "JWT",
  "alg": "HS256"
}

加密后的头部如下:

ewogICJ0eXAiOiAiSldUIiwKICAiYWxnIjogIkhTMjU2Igp9

主体信息是描述用户的信息,然后通过加密(base64加密)得到的。通过解密可以得到原始信息。比如下面这段主体信息。

{
    "iss": "Yang JWT",
    "iat": 1441593502,
    "exp": 1441594722,
    "aud": "www.example.com",
    "sub": "[email protected]",
    "from_user": "B",
    "target_user": "A"
}

加密后的主体信息如下:

ew0KICAgICJpc3MiOiAiWWFuZyBKV1QiLA0KICAgICJpYXQiOiAxNDQxNTkzNTAyLA0KICAgICJleHAiOiAxNDQxNTk0NzIyLA0KICAgICJhdWQiOiAid3d3LmV4YW1wbGUuY29tIiwNCiAgICAic3ViIjogInlhbmdAZXhhbXBsZS5jb20iLA0KICAgICJmcm9tX3VzZXIiOiAiQiIsDQogICAgInRhcmdldF91c2VyIjogIkEiDQp9

将上面加密后的2段字符串用点号连接在一起,如下所示:

ewogICJ0eXAiOiAiSldUIiwKICAiYWxnIjogIkhTMjU2Igp9.ew0KICAgICJpc3MiOiAiWWFuZyBKV1QiLA0KICAgICJpYXQiOiAxNDQxNTkzNTAyLA0KICAgICJleHAiOiAxNDQxNTk0NzIyLA0KICAgICJhdWQiOiAid3d3LmV4YW1wbGUuY29tIiwNCiAgICAic3ViIjogInlhbmdAZXhhbXBsZS5jb20iLA0KICAgICJmcm9tX3VzZXIiOiAiQiIsDQogICAgInRhcmdldF91c2VyIjogIkEiDQp9

再对上面的字符串进行HS256算法加密,加密的时候需要我们提供一个密钥(比如我们设置密钥为63161014009),加密后的内容就是签名:

rSWamyAYwuHCo7IFAgd1oRpSP7nzL7BF5t7ItqpKViM

最后再把签名通过点号拼在最后就得到了token:

ewogICJ0eXAiOiAiSldUIiwKICAiYWxnIjogIkhTMjU2Igp9.ew0KICAgICJpc3MiOiAiWWFuZyBKV1QiLA0KICAgICJpYXQiOiAxNDQxNTkzNTAyLA0KICAgICJleHAiOiAxNDQxNTk0NzIyLA0KICAgICJhdWQiOiAid3d3LmV4YW1wbGUuY29tIiwNCiAgICAic3ViIjogInlhbmdAZXhhbXBsZS5jb20iLA0KICAgICJmcm9tX3VzZXIiOiAiQiIsDQogICAgInRhcmdldF91c2VyIjogIkEiDQp9.rSWamyAYwuHCo7IFAgd1oRpSP7nzL7BF5t7ItqpKViM

上面的三个加密都是可逆的,其中头部和主体信息任何人都能用base64解密,签名需要密钥才能解密。黑客没有密钥是不能伪造签名的,所以也不能伪造token。然后,在服务器端只需要重新对token里的头部和主体信息用密钥加密一次,然后和签名对比,就能知道这个token合不合法了。所以根本就不需要把token存在本地。

由于服务器不需要把token存在本地,节省了查找token的时间,在用户登出的时候,服务器也不需要销毁token等额外处理。所以jwt减少了服务器的很多压力。

值得一提的是,上面只是说明了一种jwt的模式,过程中都可以根据实际情况对流程进行改造。比如有的业务把token放到http header里面发送给服务器,有的业务把token放到url参数里面发送给服务器。

虽然jwt相比session-id有很多优势,但是它不能解决jwt失效的问题,除非设置过期时间。也就是说,如果把token放在服务器中储存的话,能够解决这个问题:就是假如我储存token在redis里面,每次认证的时候比较这个token,这样就能做到,用户A用A的信息登录之后,用户B再用A的信息登录,那么用户A的登录就会失效,因为储存了新的token(用户B收到的token)。但是jwt实现不了这种情形。

所以说,jwt只适用于有效期极短的token!!!它并不能取代session-id!!!

混合认证

由于基于session-id的认证有如下缺点:

  1. 全部靠服务端完成,服务端拥有状态,压力会很大。
  2. 对于跨域请求,不会自动发送cookie。

基于jwt的认证又有如下缺点:

  1. 只能通过设置有效期来废除token,如果token的有效期设置太长,那么在这段时间内没有办法废除这个token。

所以对于一般的认证,现在通常用混合方法,具体如下:

  1. 用户输入用户名和密码,发送给服务器。
  2. 服务器验证用户名和密码,成功则返回一个token给浏览器,并且把这个token和用户名一起储存在服务器中。
  3. 后续每次请求,浏览器都会把token发送给服务器,服务器把收到的token和本地的对比,一致则返回用户请求的数据。
  4. 每次用户重新登录,服务器都会重新生成一个token,并清除以前的token,储存起来。
  5. 当用户退出登录时,浏览器端销毁这个token,服务器不作处理。

这种认证的优点是:

  1. 服务端只需要生成和储存token,压力会小很多,考虑到存取速度,可以用redis来储存token,这样读取和查找会很快。
  2. 服务端没有状态,也适合restful api。

授权码模式的授权

授权码模式是Oauth2.0中最精彩的授权模式。它的步骤如下:

比如说,用户需要在豆瓣上注册登录,希望用用户的qq的信息来注册,这个时候就需要取得第三方QQ的授权,并拿取QQ里面这个用户的信息。

  1. 用户访问豆瓣,豆瓣将用户导向QQ的认证服务器页面。
  2. 用户选择同意QQ的授权。
  3. QQ的认证服务器页面将用户导向豆瓣指定的“重定向URI”,并且附上一个授权码(code)。
  4. 豆瓣指定的“重定向URI”利用授权code,再加上储存在“重定向URI”里面的服务器id和密码,向QQ的认证服务器申请令牌(token)。
  5. QQ的认证服务器核对授权code,服务器id和密码,确认无误之后,向“重定向URI”发送令牌(token)。
  6. “重定向URI”利用令牌(token)向QQ的资源服务器申请用户信息,然后在豆瓣注册此用户。
  7. 用户在豆瓣的注册完成。(不需要用户填写任何用户信息)

有下面几点需要注意:

  1. 黑客即使获取了第二步中的授权码也没用,因为黑客没有服务器id和密码,所以黑客还是不能访问QQ的服务器中的用户信息。

  2. 服务器id和密码是什么?服务器id和密码是豆瓣的“重定向URI”向QQ的认证服务器注册的时候获得的id和密码,作为豆瓣的标识。在注册的时候,QQ的认证服务器也会储存这个“重定向URI”,所以不同的用户都会导向同一个“重定向URI”。

  3. 第4步中的请求必须用https请求,否则授权code,服务器id和密码有可能被黑客截获,认证服务器发送回的令牌也可能被截获。

  4. 为什么需要一个“重定向URI”?首先,豆瓣的服务器id和密码不能储存在其它页面,它只能储存在豆瓣的“重定向URI”里面,否则很可能被泄漏。其次,“重定向URI”和认证服务器的通信必须为https,一般的豆瓣页面与认证服务器的通信可能不是https。

  5. 在第6步中,“重定向URI”可能会把令牌(token)发回给豆瓣原页面,也可能制造一个豆瓣的token发回给原页面。

  6. Oauth2.0还有一个refresh_token。一般QQ的认证服务器发回的token的有效期很短,而refresh_token的有效期比较长,所以当token过期的时候,可以通过refresh_token向QQ的认证服务器申请token。

  7. 用户一共要进行3次uri跳转,一次是跳转到QQ的认证服务器。第二次是跳转到“重定向URI”。第三次跳转回以前的页面。其中第一次需要用户操作,点击是否同意QQ授权;第二次不需要用户操作,自动进行第三次跳转。

简单模式的授权

简单模式是不利用“重定向URI”,直接向认证服务器中申请token的模式。还是以豆瓣和QQ为例,它的步骤如下:

  1. 用户访问豆瓣,豆瓣将用户导向QQ的认证服务器页面。
  2. 用户选择同意QQ的授权。
  3. QQ的认证服务器页面将用户导向豆瓣指定的“重定向URI”,并且直接在URI的hash部分附带了令牌(token)。
  4. “重定向URI”利用令牌(token)向QQ的资源服务器申请用户信息,然后在豆瓣注册此用户。
  5. 用户在豆瓣的注册完成。(不需要用户填写任何用户信息)

有下面几点需要注意:

  1. 和授权码模式的授权的区别是,没有授权码,从而没有检测服务器id和密码。

  2. 黑客可以通过这2种方式进行攻击:1.黑客直接截取这个token。2.黑客伪造一个第三方网站,然后引导用户在伪造的第三方网站进行授权,最后就能理所当然的得到token了。授权码模式的授权可以防止第二种攻击,但是同样不能防止第一种攻击(可以利用https加强安全性)。

  3. 授权码模式的授权和简单模式的授权为了防止CSRF攻击,都会在跳转的URI上面加一个state参数来进行校验。

  4. 微信的静默授权和这个差不多,只是没有第二步,直接默认同意授权。

  5. URL跳转才会在safari浏览器下面留下白条,非URL的URI跳转不会。由于微信的非静默授权会跳转到一个用户确认是否进行授权的URL,所以非静默授权会留下白条,静默授权不会。(不是因为有code才留下白条的)

  6. 为什么要在hash部分附带令牌?因为浏览器处理hash参数的时候不会使页面重新加载。(虽然说新的history api能够解决这个问题。)

  7. 如果认证服务器不支持跨域,那么只能用简单模式的授权。因为授权码模式的授权会提交服务器id,密码和code到认证服务器,所以需要认证服务器支持跨域的post请求;但是简单模式的授权全部是URI跳转,所以不用担心跨域。

ios的跨站脚本限制

概述

项目中碰到一个问题,就是在ios机上,用iframe内嵌的网页有时需要登录,有时候又不需要登录。查找了半天,终于发现是ios的跨站脚本限制导致的。这里就来介绍下跨站脚本限制,供以后开发时参考,相信对其他人也有用。

参考资料:Intelligent Tracking Prevention

跨站追踪和第三方cookie

假设,一个用户同时浏览example-products.com和example-recipies.com,而这2个网站都加载了example-tracker.com的内容,并且example-tracker.com能在用户的浏览器里面设置cookie的话,那么example-tracker.com能够知道用户在example-products.com和example-recipies.com所干的事。这就是跨站追踪,而example-recipies.com设置的cookie被称为第三方cookie

ios的应对策略

ios通过机器学习的方法,利用用户最近30天的行为数据,智能的分析哪些第三方cookie需要阻止,哪些不需要阻止。

当然,用户可以自行在ios浏览器里面取消跨站脚本限制。

对前端来说意味着什么

这就表示,在ios浏览器里面,不能用iframe等方式注入第三方cookie;同时也表示,如果iframe里面的网址是跨域的话,就不能获得里面网址的cookie,ios浏览器会阻止这种行为。

所以前端不能通过第三方的形式注入cookie,也不能通过第三方的形式获取cookie,也就不能自动登录了

注意

有以下2点需要注意:

  1. 无论是safari还是ios上的chrome抑或是ios上的微信浏览器,都会有这个跨站脚本限制。

  2. 注意是第三方才会限制,也就是说,如果iframe的一级域名和外面的相同,就不会限制了。

Vue2.x计算属性为什么能依赖于另一个计算属性

概述

说到 computed 和 watch 有什么不同,也许大多数人都知道:computed 是用现有数据生成一个新数据,并且能够被缓存;而 watch 是根据数据变化,执行一些回调函数,它有很多配置比如 deep、immediate 等。

大家也都知道,watch 只是源码里面 watcher 的一个实例,computed 属性也用到了 watcher,但是 computed 属性为什么能够相互依赖变化呢?明显 watcher 自己是做不到这一点的,因为 watcher 并不能 update 其它 watcher。我为了弄懂其中的原理根据 vue2.x 的源码写了一个简易的 computed 属性,供以后工作时参考,相信对其他人也有用。

部分代码来源于Vue2.x是怎么收集依赖的

简易的 computed

为了简便,暂不考虑 computed 的 setter 的情况,我实现了一个简易的 computed,代码如下:

function defineReactive(obj, key, val) {
    const dep = new Dep();

    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get() {
            if (Dep.target) {
                dep.depend();
            }
            return val;
        },
        set(newVal) {
            val = newVal;
            dep.notify();
        }
    });
}

class Dep {
    constructor() {
        this.subs = [];
    }

    addSub(sub) {
        this.subs.push(sub);
    }

    removeSub() {
        const index = this.subs.indexOf(sub);
        if (index > -1) {
            this.subs.splice(index, 1);
        }
    }

    depend() {
        if (Dep.target) {
            Dep.target.addDep(this);
        }
    }

    notify() {
        const subs = this.subs.slice();
        for (let i = 0, l = subs.length; i < l; i++) {
            subs[i].update();
        }
    }
}

Dep.target = null;
const targetStack = []

function pushTarget (target) {
  targetStack.push(target)
  Dep.target = target
}

function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

class Watcher {
    constructor(cb, dirty = false) {
        this.getter = cb;
        this.deps = [];
        this.newDeps = [];
        this.value = this.get();
        this.dirty = dirty;
    }

    get() {
        pushTarget(this);
        const value = this.getter();
        popTarget(this);
        this.deps = [...this.newDeps];
        this.newDeps = [];
        return value;
    }

    addDep(dep) {
        this.newDeps.push(dep);
        dep.addSub(this);
    }

    update() {
        this.dirty = true;
        this.value = this.get();
    }

    evaluate() {
        this.value = this.get();
        this.dirty = false;
    }

    depend() {
        let i = this.deps.length;

        while (i--) {
            this.deps[i].depend();
        }
    }
}

const obj = {};
defineReactive(obj, 'text', 'Hello World!');

const vm = {};
const computed = {
    text1() {
        return `${obj.text}-text1`;
    },
    text2() {
        return `${vm.text1}-text2`;
    }
};

function createComputedGetter(key) {
    return function computedGetter() {
        const watcher = vm.computedWatchers[key];

        if (watcher) {
            if (watcher.dirty) {
                watcher.evaluate();
            }

            if (Dep.target) {
                watcher.depend();
            }

            return watcher.value;
        }
    }
}

function defineCompute(target) {
    const watchers = vm.computedWatchers = Object.create(null);

    for (key in target) {
        const cb = target[key];
        watchers[key] = new Watcher(cb, true);

        //defineComputed
        Object.defineProperty(vm, key, {
            get: createComputedGetter(key),
            set(a) {
                return a;
            }
        });
    }
}

defineCompute(computed);

const watcher = new Watcher(() => {
    document.querySelector('body').innerHTML = vm.text2;
});

把上面的代码复制到浏览器的控制台运行,就可以看到浏览器里面出现了Hello World-text1-text2,然后我们继续在控制台输入obj.text = 'Define Reactive',可以看到浏览器里面的Hello World-text1-text2就变成了Define Reactive-text1-text2

显然,由于我们改变了obj.text的值,然后自动的导致了vm.text1vm.text2的值发生了响应式变化。

而其中的原理是,假如计算属性 A 依赖计算属性 B,而计算属性 B 又依赖响应式数据 C,那么最一开始先把计算属性 AB 都转化为 watcher,然后在把计算属性 AB 挂载到 vm 上面的时候,插入了一段 getter,而计算属性 B 的这个 getter 在这个计算属性 B 被读取的时候会把计算属性 A 的 watcher 添加到响应式数据 C 的依赖里面,所以响应式数据 C 在改变的时候会先后导致计算属性 B 和 A 执行 update,从而发生改变。

而其中关键的那段代码就是这段:

function createComputedGetter(key) {
    return function computedGetter() {
        const watcher = vm.computedWatchers[key];

        if (watcher) {
            if (watcher.dirty) {
                watcher.evaluate();
            }

            // 这里非常关键
            if (Dep.target) {
                watcher.depend();
            }

            return watcher.value;
        }
    }
}

为什么在计算属性 B 的 getter 函数里面会添加计算属性 A 的 watcher 呢?这是因为计算属性 B 在求值完成后,会自动把Dep.target出栈,从而暴露出计算属性 A 的 watcher。代码如下:

class Watcher {
    get() {
        // 这里把自己的 watcher 入栈
        pushTarget(this);
        const value = this.getter();
        // 这里把自己的 watcher 出栈
        popTarget(this);
        this.deps = [...this.newDeps];
        this.newDeps = [];
        return value;
    }
}

这就是 pushTarget 和 popTarget 调度 watchers 的美丽之处~~

其它

需要注意以下两点:

1.在给计算属性生成 getter 的时候,不能直接使用 Object.defineProperty,而是使用闭包把 key 值储存了起来。

2.为什么不直接使用 defineReactive 把计算属性变成响应式的。因为当把计算属性用 setter 挂载到 vm 上面的时候,计算属性这里确实变成了一个具体的值,但是如果使用 defineReactive 把计算属性变成响应式的话,计算属性会执行自己的依赖,从而和响应式数据的依赖重复了。其实这也是把非数据变成响应式的一种方法。

提升键盘可访问性和AT可访问性

概述

很多地方比如官网中需要提升 html 的可访问性,我参考 element-ui,总结了一套提升可访问性的方案,记录下来,供以后开发时参考,相信对其他人也有用。

可访问性

可访问性基本上分为 2 类:

  1. 键盘可访问性:要求页面上的所有可交互的地方均可通过键盘操作。
  2. AT可访问性:AT 是 Assistive Technologies 的简写,主要是方便残障用户和网站内容进行交互。

键盘可访问性

键盘可访问性有很多内容,但是我觉得主要实现如下 2 点即可:

1.可以使用 tab 来获取焦点。这其中用到的主要方法是 tabindex

  • tabindex等于 -1:不能使用 tab 键获取焦点,如果元素是隐藏的,那么它的 tabindex 必须设置为 -1;
  • tabindex等于 0:能使用 tab 键获取焦点,适用于无顺序的内容
  • tabindex等于 xx:能使用 tab 键获取焦点,xx 的值越大,越在前面

示例如下:

<!-- 没有tabindex 属性的话, 这些 <span> 元素不会被键盘focus中 -->
<ul>
  <li tabindex="0">
    Include decorative fruit basket
  </li>
  <li tabindex="0">
    Include singing telegram
  </li>
  <li tabindex="0">
    Require payment before delivery
  </li>
</ul>

**需要说明的是:**html 里面的 a、button、form 标签即使不添加 tabindex 也能用 tab 键获取焦点。

2.可以使用上、下、左、右键移动选项。比如一个 popover 组件,弹出的内容是可供选择的,那么需要可以使用上、下键进行选择,方法是监听键盘的上、下键,然后监听 enter 键进行选择。示例如下:

<template>
  <popper
    trigger="clickToOpen"
    :options="{
      placement: 'top',
      modifiers: { offset: { offset: '0,10px' } }
    }">
    <ul
      class="popper"
      @keydown.down.prevent="navigateOptions('next')"
      @keydown.up.prevent="navigateOptions('prev')"
      @keydown.enter.prevent="selectOption"
    >
      <li tabindex="0">
        Include decorative fruit basket
      </li>
      <li tabindex="0">
        Include singing telegram
      </li>
      <li tabindex="0">
        Require payment before delivery
      </li>
    </ul>

    <button slot="reference">
      Reference Element
    </button>
  </popper>
</template>

<script>
import Popper from 'vue-popperjs';
import 'vue-popperjs/dist/vue-popper.css';

export default {
  components: {
    'popper': Popper
  },
  methods: {
    navigateOptions() {
      // ...
    },
    selectOption() {
      // ...
    },
  },
}
</script>

AT 可访问性

AT 可访问性我觉得也主要分为以下 2 点:

1.尽量使用语义化标签;图像使用 alt 属性描述图像内容;视频使用 title 属性。

2.用 role 来标记这一块 html 是用来干什么的,文档在这里:文档。示例如下:

<div id="main" role="main">
  <h1>Avocados</h1>
  <!-- main section content -->
</div>

我觉得如下 role 一定需要标出来:

navigation、main、article、dialog、search、img、banner、button

如果希望更进一步,可以参考 ARIA文档 使用 ARIA。

小程序的一些坑

概述

最近项目使用小程序的web-view把现有项目迁移到小程序里面去。有一些心得,记录下来供以后开发时参考,相信对其他人也有用。

通用

1.企业小程序需要把接口域名填入服务器域名,把web-view的网址域名填入业务域名,把小程序和运营的公众号绑定。

2.授权。

根据授权接口升级公告,无法使用 wx.getUserInfo 接口直接弹出授权框,但是wx.getUserInfo 接口仍旧可以使用。目前微信上的授权和登录一般有2种方式:

  1. 比较通用,具有独立的个人中心页,每次打开个人中心页都会向开发者服务器发送http请求,请求中有code,开发者服务器利用这个code向微信服务器获取用户数据来确定用户是否已经授权,如果没有授权就需要用户点击授权按钮,如果已经授权就顺便传回用户头像等信息。(这个方法适用于需要传回用户头像等信息的场合)
  2. 没有独立的个人中心页,也不需要开发者服务器传回用户头像等信息,直接用 wx.getSetting 接口判断用户是否已经授权,没授权则跳转授权页让用户点击授权按钮,已授权则走正常流程,示例代码如下:
wx.getSetting({
  success: (settingRes) => {
    if (settingRes.authSetting['scope.userInfo']) {
      wx.showLoading({ title: 'Loading!' });
      wx.getUserInfo({
        success: (res) => {
          that.globalData.encryptedData = res.encryptedData;
          that.globalData.iv = res.iv;
          resolve(res);
        },
        fail: err => reject(err),
      })
    } else {
      // 未授权,跳转授权页
      wx.reLaunch({
        url: '../auth/index',
      });
    }
  },
  fail: err => reject(err),
})

3.接口封装

因为小程序使用的微信api都是回调形式的,并且很容易嵌套,引起回调地狱。所以一般需要使用promise进行封装(如果要使用await的话,需要引入regenerator-runtime库)。使用promise进行封装还能利用catch很简便的处理error信息。示例代码如下:

wxLogin: function () {
  const that = this;
  return new Promise((resolve, reject) => {
    wx.login({
      success: (res) => {
        that.globalData.loginCode = res.code;
        resolve(res);
      },
      fail: err => reject(err),
    });
  });
},
wxGetSetting: function () {
  const that = this;
  return new Promise((resolve, reject) => {
    wx.getSetting({
      success: (settingRes) => {
        if (settingRes.authSetting['scope.userInfo']) {
          wx.showLoading({ title: 'Loading!' });
          wx.getUserInfo({
            success: (res) => {
              that.globalData.encryptedData = res.encryptedData;
              that.globalData.iv = res.iv;
              resolve(res);
            },
            fail: err => reject(err),
          })
        } else {
          // 未授权,跳转授权页
          wx.reLaunch({
            url: '../auth/index',
          });
        }
      },
      fail: err => reject(err),
    })
  });
},
onLoad(options) {
  const that = this;
  this.handleUrlFromShare(options);
  app.wxLogin()
    .then(res => app.wxGetSetting())
    .then(res => that.requestCodeApi())
    .catch((err) => {
      wx.hideLoading();
      this.setData({
        isFirstLogin: true,
      });

      // 正常登陆
      if (err.hideToast) return;
      // 登陆失败
      that.showErrorToast(err, 'Login Again Please!');
      console.log('Login failed-------', err);
    });
},

web-view组件的使用

小程序的web-view是承载网页的容器,相当于iframe。它会自动铺满整个小程序页面,个人类型与海外类型的小程序暂不支持使用,并且 navigationStyle: custom 对 组件无效

1.web-view 里面的项目可以通过判断 userAgent 中包含 miniProgram 字样来判断小程序 web-view 环境(微信7.0.0以上)。示例代码如下:

export function isMiniProgram() {
  return !!navigator.userAgent.match(/miniProgram/i);
}

2.不能在web-view里面打开新窗口,所以在项目上需要判断小程序环境,在需要新窗口打开的时候变成本窗口打开。

3.分享

在有 web-view 的小程序页面可以利用 options.webViewUrl 拿到 web-view 里面的网址,然后在分享的时候带上这个网址,在跳转页面判断是否有url参数来接收这个网址。

需要注意的是,

  1. 如果网址中有token等信息,需要先去掉这个信息。
  2. 如果网址中有汉字等符号,就需要使用encodeURIComponent转义一下。
  3. 有些时候,可能需要双重转义才能拿到想要的url。

示例如下:

deleteTkAndRtk: function(url) {
  const host = url.slice(0, url.indexOf('?'));
  let queryArr = url.slice(url.indexOf('?') + 1, url.length).split('&');

  queryArr = queryArr.filter(
    query => query.indexOf('tk=') === -1 && query.indexOf('rtk=') === -1);

  if (queryArr.length !== 0) {
    return host + '?' + queryArr.join('');
  }

  return host;
},
onShareAppMessage: function (options) {
  const url = options.webViewUrl;
  const filteredUrl = this.deleteTkAndRtk(url);
  return {
    path: `pages/index/index?url=${encodeURIComponent(filteredUrl)}`,
    title: 'YiDrone',
  }
},
onLoad: function (options) {
  if (options.url) {
    this.setData({
      // 这里的encodeURI必不可少。。。
      yidroneUrl: encodeURI(decodeURIComponent(options.url)),
    })
  }
},

4.jssdk

web-view中的项目可以给小程序发送信息,还可以控制小程序的页面跳转,方法如下:

// 首先在项目内引入 weixin-js-sdk 库
npm install weixin-js-sdk --save

// 然后在plugin文件夹建立 wx-sdk.js
import Vue from 'vue';
import wx from 'weixin-js-sdk';

Vue.prototype.$wx = wx;

// 然后在main.js里面判断小程序环境按需引入 wx-sdk.js
// weixin-js-sdk
try {
  if (isMiniProgram()) {
    import('./plugins/wx-sdk').then(() => {});
  }
} catch (err) {
  console.error('>>>wx-sdk', err);
}

// 最后在需要的地方进行操作
if (isMiniProgram() && this.$wx) {
  // 不能使用这个格式:this.$wx.miniProgram.postMessage({ data: 'bar' });
  this.$wx.miniProgram.postMessage({ data: { foo: 'bar' } });
  this.$wx.miniProgram.navigateTo({ url: '../auth/index' });
}

// 在小程序的web-view组件加入 handleBindmessage 事件
<web-view src="{{ url }}" bindmessage="handleBindmessage"></web-view>

前端图片压缩调研

概述

最近做项目思考了一下前端图片压缩问题,有一些小的心得,记录下来,供以后开发时参考,相信对其他人也有用。下面按优先级列出了前端图片压缩的解决方案

webpack

现在前端项目都是利用webpack打包,所以我调研了一下在webpack里面压缩图片的解决方案,主要使用基于imagemin插件的imagemin-webpack-plugin插件

首先安装imagemin-webpack-plugin插件:

npm install imagemin-webpack-plugin

然后在webpack配置中添加如下配置,就可以在项目打包的时候自动压缩图片了。

var ImageminPlugin = require('imagemin-webpack-plugin').default

module.exports = {
  plugins: [
    // Make sure that the plugin is after any plugins that add images
    new ImageminPlugin({
      test: /\.(jpe?g|png|gif|svg)$/i,
      disable: process.env.NODE_ENV !== 'production', // Disable during development
      pngquant: {
        quality: '95-100'
      }
    })
  ]
}

如果使用webpack-chain的话,webpack的配置如下:

const ImageminPlugin = require('imagemin-webpack-plugin').default;

module.exports = {
  chainWebpack: config => {
    config
      .plugin('ImageminPlugin')
      .use(ImageminPlugin, [{
        test: /\.(jpe?g|png|gif|svg)$/i,
        disable: process.env.NODE_ENV !== 'production', // Disable during development
        pngquant: {
          quality: '95-100'
        }
      }]);
  },
};

这个插件灵活性挺高的,可以通过调整quality来调整生成的图片的品质。

这里说一下ImageminPlugin的配置:

  1. pngquant设置压缩图片的品质,建议设置为95-100。
  2. minFileSize设置多大以上的图片才压缩,单位是比特,建议设置为5120,即5k以上的图片才压缩。
  3. test设置那里的图片才压缩,这里的路径是打包后的路径,如果打包后图片存放的文件夹是img,那么这里的值是:/img\/.*\.(jpe?g|png|gif|svg)$/i

wordpress

我们官网是用wordpress制作的。我找了一下wordpress上面的图片压缩插件,发现都不能自己调整最终图片的品质。比较主流的有以下几个:

1.Compress JPEG & PNG images。TinyPNG官方发布的wordpress压缩图片插件,支持在后台一键压缩所有wordpress上的图片,但是每个api key每月限制最多只能压缩500张,否则就需要付费购买次数了(但是可以申请多个api key来解决这个问题)。
2.Smush。wordpress上活跃安装数超过1百万次。免费,后台界面非常人性化。
3.ShortPixel Image Optimizer。支持lossy,glossy和lossless三种图片压缩模式。

我个人建议使用Compress JPEG & PNG images,毕竟TinyPNG的名声在那里,压缩的图片也不会太差。

另外还可以修改上传图片的大小限制,在functions.php中添加如下代码,这样上传限制就是64M:

@ini_set( 'upload_max_size' , '64M' );
@ini_set( 'post_max_size', '64M');
@ini_set( 'max_execution_time', '300' );

另一种方法是在根目录新建php.ini文件,里面加入如下代码:

upload_max_filesize = 64M
post_max_size = 64M
max_execution_time = 300

压图工具

如果对wordpress的插件没有自己设置最终图片的品质的功能不满意,那么可以在本地安装客户端来进行图片压缩。这里推荐使用智图。它是 腾讯ISUX 设计团队出品的一款免费压缩工具,有客户端,可以离线使用,支持自定义压缩品质,还可以自动导出为WebP格式。

压图网站

最后可以在一些图片压缩网站上进行在线压缩。比如tinypng图好快智图jpeg ioOptimizilla。这里我推荐tinypng。

不过这种方式强烈不推荐,如今是前端自动化的时代,这种手工压缩的方式已经落伍了。

总结

项目中的图片文件可以分为以下四种:

  1. ps或figma等专业工具导出的jpg图片。导出的时候可以定一个导出图片的品质,然后ui设计师按照这个品质来导出图片。可以不压缩。

  2. ps或figma等专业工具导出的png图片。这种图片在导出的时候不能设置品质,所以需要压缩。

  3. 相机或者手机里面拍照的图片。这种图片需要压缩。

  4. svg。使用svgo进行压缩。已经在项目中的ym-svg-sprite插件中支持。

总的来说,在项目中使用imagemin插件进行图片压缩;在wordpress里面使用插件进行图片压缩,或者安装本地客户端来压缩wordpress里面的图片。

当我们说前端,我们在说什么?

概述

过年的时候和表哥聊天,他说以前也用html,js,css写过网页,那现在的前端和那个时候有什么不同,我当时就向他介绍了一下前端。

但是当时我对前端的理解也不是很透彻,所以我自我感觉也没有介绍清楚。

随着我自己学习的深入,我自己觉得有必要总结一篇前端和写网页不同的博文了。同时也为了记录下我现在的见解和认识,等过一段时间回过头来在看,肯定会对自己的提高有很多感悟

由于知识水平实在有限,如果有什么错误,麻烦在评论区中指出,谢谢!

网页开发时代

那个时候,人们通过html写网页,并且用js,css调整样式。

我们在各种经典书籍《Head First HTML与CSS》《精通CSS:高级Web标准解决方案》《JavaScript高级程序设计》里面学到的,也基本是这方面的东西。慕课网上的大部分视屏,也都仅限于这方面而已。

网页模板时代

随着网页的复杂度加大,程序员为了开发上的便利,于是开发各种网页模板语言

比如说简化写css的less语言,简化写js的coffeejs,还有很多html和js混写的各种模板。

ajax时代

随着js的发展,XHR的发明使得程序员可以在前端处理数据,分担了一些后端的工作。

很好的例子是表单验证啊,浏览器滑块滑到底部才开始加载图片啊什么的。

SPA时代

SPA=Single Page Application单页面应用。

随着ajax的发展,人们在浏览器端就可以做数据处理,这就意味着前端可以写一整个应用程序,即SPA。

在SPA时代,程序员用js来处理整个前端部分,用户在打开网站的时候就下载所有的js,在以后的打开页面等交互中,就直接在浏览器端用js进行处理,不需要向服务端发送http请求。

这个时候有一个很重要的概念就是路由,由于打开页面不发送http请求,所以对于一个网址,需要浏览器不知道怎么办。这个时候需要有一种机制来指导浏览器对于什么网址打开什么内容,这就是路由。

MVC时代

随着SPA时代的发展,人们为了开发的方便,经常把项目分为各种模块。其中最典型的是MVC,即model,view和control。

这个时代涌现出各种框架,比如backbone框架等。

MVVM时代

由于MVC的control模块一般是由服务端处理的,这不属于前端的内容。为了解决这个问题,人们提出了一种解决办法,就是利用view model来代替control,这就是MVVM框架的由来。实现的技术叫做双向绑定

这个时代就出现了现代前端框架:reactjs,angularjs等。这个时候,前端工程师已经可以开发在浏览器上运行的和应用软件一样的软件了,包括在手机浏览器上运行的。

vue-cli的跨域设置

概述

今天打算快速使用vue-cli建立一个小应用用于测试,使用axios发送http请求,但是遇到了跨域问题,总结了一下,供以后开发时参考,相信对其他人也有用。

vue-cli的跨域设置

在vue.config.js里面的devServer的proxy加入如下设置。

// vue.config.js
const tableauApi = 'https://tableau.proxy.web.yimian.com.cn/';

module.exports = {
  devServer: {
    proxy: {
      '/tableau': {
        target: tableauApi,
        changeOrigin: true,
        pathRewrite: {
          '^/tableau': ''
        },
      },
    },
  },
};

上面的设置表示,把/tableau开头的api代理到https://tableau.proxy.web.yimian.com.cn/,并且去掉/tableau。比如/tableau/test1就会被代理到https://tableau.proxy.web.yimian.com.cn/test1

这里底层使用的是http-proxy-middleware插件。

后续

  1. 上面的设置中有devServer,表示只能在开发环境中使用代理,而打包之后就无效了。打包之后需要使用nginx进行反向代理才行。
  2. 上传到静态服务器上面之后有一个路径问题,需要用publicPath给js, css等文件的路径添加前缀,我自己的设置如下:
// vue.config.js
module.exports = {
  publicPath: process.env.NODE_ENV === 'production' ?
    '/test/tableau/dist/':
    '/'
};

从小白到用antd+react+react-router+issue+es6搭建博客

概述

本身是前端小白,学过html,css,js的各种书,各种视屏,就是没有接触web开发的内容。偶然看见一个朋友用react搭建了一个博客,于是本着程序员无所不能的精神,也尝试着用react搭建博客。

下面记录我从小白到搭建博客的过程,没有写方法,因为网上已经很多方法了。

这是我搭建的博客地址:馒头加梨子

结论

先说结论,我学到了什么

  1. 单页面web开发的流程。把要做的分为一个个模块,逐个实现,然后用webpack设置,开发并打包上线。

  2. antd库的使用和一些组件的配置参数。

  3. 相关知识:SPA, react, react-router4, antd, fetch, promise, es6等。

  4. 怎么搜索?在github和相关社区搜索,通常能找到意想不到的惊喜。

  5. 程序员怎么学习?一定要手打教程,并思考为什么要这么做。

我碰到了哪些难点

  • webpack配置。一开始我没有使用脚手架,以前只打过webpack的demo但是忘了,看了很多资料才学会webpck的实际使用。
  • antd库组件配置。以前没用过这种库,完全不知道怎么下手,后来在官网看见每个例子都有代码demo,才慢慢熟悉的。
  • router4中的路由赋值。为了把博客打造成SPA的形式,我思考了很多。
  • fetch内容报错。我看了很多遍关于promise和生命周期的内容,最后通过添加loading解决。
  • 添加目录。给文章加上锚点和通过锚点跳转,为了更优雅的编程,我遇到了很多坑。

搜索参考博客

既然自己是小白,那么当然要去参考其他人的博客,寻找他们的优点并且学下来呀。

那怎么搜呢?我主要通过这些渠道搜索:

经过一番搜索,我最后定下来参考这几个大神的博客,他们都是用react搭建的,并且都能在github上找到源码。

然后样式参考这几个大神的博客,他们不是用react搭建的,但是样式很好看。

学习react

作为一个小白,肯定要先学习react,那么去react中文官网把文档看一遍,并且把教程手打一遍啦。

思考博客架构

我要一个什么样的博客

我的博客要有以下特点:

  1. 一个好看时髦的导航栏。
  2. 一个自动生成的目录(文章界面)。
  3. 一个回到顶端的功能。
  4. 要有代码高亮。
  5. 一个分类的功能。(没做)
  6. 一个加载的时候的进度条。(没做)
  7. 要简洁,扁平化。
  8. 要响应式。
  9. 要速度快。

我还进行了如下思考:

  1. 我不要首页和侧栏。因为显得太复杂了。

  2. 我不要翻页。因为我有回到顶端功能,而且我现在写的文章还少,不需要翻页。而且阮一峰的日志也没有做翻页功能。

单页面软件SPA

我之前没有接触SPA,但是在搜索的过程中偶然碰到了,觉得很有必要学习一下,因为这是当代web开发的分水岭。于是去看了一本书《单页Web应用:JavaScript从前端到后端》

这是我学完SPA之后写的一篇感想博客:当我们说前端,我们在说什么

SPA让我了解到了模块化开发的**,也解决了我的一个需求:速度快。

路由

路由是SPA,也是react中很重要的一个功能。

于是我去学习了react-router4,并且把react-router-tutorial自己手打了一遍。并且查资料补充了redirect等内容。

antd

在学习water博客的时候接触了一个很有意思的ui库:antd。我以前也没有用过这种类型的库,于是本着好奇的精神,也打算用这个库。

我学习了这个库里面的这些组件:Button, Icon, Layout, Affix, Dropdown, Menu, Card, Collapse, List, Tag, BackTop。在学习的时候踩了很多坑,也懂了这些组件的一些配置参数

antd也解决了我的一个需求:响应式。

es6

虽然我在react中文官网的教程里面学习了部分es6语法,但是在学习别人博客的时候碰到了很多es6语法,我自己也有强迫症,能用es6语法的地方尽量用es6语法。比如我就很不喜欢用if-else,能用map+箭头函数的地方我就用map+箭头函数。

于是我学习并实践了如下es6的知识:模板对象,箭头函数,解构赋值,类,promise,let和const。

路由赋值

路由路由,还是路由。

学过SPA之后,我觉得有必要把博客打造成SPA的形式。于是各种思考和查资料。最后打造成了目前这种形式:只在打开博客的时候发送http请求,其余操作不发送http请求,直接由浏览器完成

其实还有另一种实现方法,就是利用redux,真的是与我的想法不谋而合,由于我还没有学,而且redux文档不建议小型网站使用redux,所以我没有用这种方法。

这个时候我总结了一篇博文:react在router中传递数据的2种方法

fetch

怎么获取博客内容呢?

我一开始打算用老办法:写完markdown文件就上传至github,然后一个个解析md文件。但是这个方案有个缺点,就是每次写完都要build上传非常麻烦。强迫症迫使我寻找更好的方法。偶然我发现用issue写博客是真的好,于是最后改用从issue获取博客内容。

那么怎么获取呢?在别人博客中找了三种方案:

  • 用es5原生的fetch方法。
  • 用isomorphic-fetch库的fetch方法。
  • 用axios库的相关方法。

最后我决定用用isomorphic-fetch库的fetch。

代码高亮

一开始我还不知到什么是代码高亮,只是看资料各种说代码高亮。代码写着写着才发现,文章页面的代码区很单调啊,所以这才醒悟原来是代码区的代码高亮。

我最后用的别人现成的方案:marked结合highlight.js设置代码高亮。

fetch报错

由于fetch方法返回的是promise对象,有一定的延迟,所以模块的render函数开始渲染的时候并不能取到数据,然后marked库各种报错。

于是我去看了又看promise的文档和组件生命周期的文档。

最后通过在模块的state属性里面添加一个loading属性成功解决。

目录和锚点

由于antd没有在文章界面自动生成目录的组件,于是我自己动手写了自动生成目录的组件。

目录要不要跳转功能?我的目录必须要与众不同啊,强迫症需要我添加这个功能。

在用js写跳转功能的时候,我才发现react的锚是个巨坑,因为react的路由,es5的#锚点不能正确被解析,于是我又去查资料,最后用scrollIntoView解决了。本来以为解决方法超麻烦的,最后一看真的超简单。

路由污染

对,路由路由,还是路由。

搭建快完成了,搬上github可以看了,但是我发现,我github上的其它githubPage都变成了我的博客了

为了解决这个问题,我只好把博客放在我的github的一个分目录下面。又要改路由。

改好路由之后我又发现,刷新键不能用了。网上查资料,最后看大牛router4的原理解析里面说,需要在服务端解决。但是我是github博客,没有服务端。也不能不支持刷新键啊,而且不解决的话,收藏文章也不支持了,只能收藏首页。

由于强迫症,我只好又把博客放在github小号上面,来让别人体验完整的功能。

收尾阶段

这个时候有2个问题。

一个是导航栏太单调了,需要加入一些其它的模块,经过考虑,我加入了作品和关于模块。这个没有什么难度。

另一个是css样式,我参考我自己在博客园的博客和其他人的博客,一下就设置好了,没什么难度。

vue实现一个简易Popover组件

概述

之前写vue的时候,对于下拉框,我是通过在组件内设置标记来控制是否弹出的,但是这样有一个问题,就是点击组件外部的时候,怎么也控制不了下拉框的关闭,用户体验非常差。

当时想到的解决方法是:给根实例创建一个标记来控制,然后一级一级的把这个标记传进来。但是这样每次配置都要改根组件,非常不灵活

最近看museUI库,发现它的下拉框Select实现的非常灵活,点击组件外也能控制下拉框关闭,于是想探究一番,借此机会也深入学习一下vue。

museUI源码

首先去看Select的源码:

directives: [{
    name: 'click-outside',
    value: (e) => {
        if (this.open && this.$refs.popover.$el.contains(e.target)) return;
        this.blur();
    }
 }],

可以看到,有个click-outsidepopover,然后它是通过用自定义指令directives实现的。然后去museUI搜popover,果然这是一个弹出组件,并且能够在组件外部控制弹窗关闭。于是开始看popover的源码

close (reason) {
    if (!this.open) return;
    this.$emit('update:open', false);
    this.$emit('close', reason);
},
clickOutSide (e) {
    if (this.trigger && this.trigger.contains(e.target)) return;
    this.close('clickOutSide');
},

可以看到,它也是通过click-outside来实现的,click-outside字面意思是点击外面,应该就是这个了。然后看click-outside的源码

name: 'click-outside',
bind (el, binding, vnode) {
  const documentHandler = function (e) {
    if (!vnode.context || el.contains(e.target)) return;
    if (binding.expression) {
      vnode.context[el[clickoutsideContext].methodName](e);
    } else {
      el[clickoutsideContext].bindingFn(e);
    }
  };
  el[clickoutsideContext] = {
    documentHandler,
    methodName: binding.expression,
    bindingFn: binding.value
  };
  setTimeout(() => {
    document.addEventListener('click', documentHandler);
  }, 0);
},

原来它是通过自定义指令,在组件创建的时候,给document绑定一个全局click事件,当点击document的时候,通过判断点击节点来控制弹窗关闭的。这差不多就是事件代理

所以总结一下,要实现组件外部控制组件弹窗的关闭,主要利用directives,bind,document就行了。

自己实现

既然知道原理就有点跃跃欲试了,通过查阅官方文档得知,directives可以用于局部组件,这样就变成了局部指令。于是写代码如下:

<template>
    <div class="pop-over">
        <a @click="toggleOpen" class="pop-button" href="javascript: void(0);">
            {{ 按钮1 }}
        </a>
        <ul v-clickoutside="close" v-show="open" class="pop-list">
            <li>选项1</li>
            <li>选项2</li>
            <li>选项3</li>
            <li>选项4</li>
        </ul>
    </div>
</template>

<script>
export default {
    name: 'PopOver',
    data() {
        return {
            open: false
        }
    },
    methods: {
        toggleOpen: function() {
            this.open = !this.open;
        },
        close: function(e) {
            if(this.$el.contains(e.target)) return;
            this.open = false;
        }
    },
    directives: {
        clickoutside: {
            bind: function (el, binding, vnode) {
                const documentHandler = function (e) {
                    if (!vnode.context || el.contains(e.target)) return;
                    binding.value(e);
                };

                setTimeout(() => {
                    document.addEventListener('click', documentHandler);
                }, 0);
            }
        }
    }
}
</script>

注意,在我们close方法里面,我们通过判断点击节点是否被组件包含,如果包含的话,不执行关闭行为。

但是上面的组件不通用,正好官方文档学习了slot,于是用slot改写如下:

<template>
    <div class="pop-over">
        <a @click="toggleOpen" class="pop-button" href="javascript: void(0);">
            {{ buttonText }}
        </a>
        <ul v-clickoutside="close" v-show="open" class="pop-list">
            <slot></slot>
        </ul>
    </div>
</template>

<script>
export default {
    name: 'PopOver',
    props: ['buttonText'],
    data() {
        return {
            open: false
        }
    },
    methods: {
        toggleOpen: function() {
            this.open = !this.open;
        },
        close: function(e) {
            if(this.$el.contains(e.target)) return;
            this.open = false;
        }
    },
    directives: {
        clickoutside: {
            bind: function (el, binding, vnode) {
                const documentHandler = function (e) {
                    if (!vnode.context || el.contains(e.target)) return;
                    binding.value(e);
                };

                setTimeout(() => {
                    document.addEventListener('click', documentHandler);
                }, 0);
            }
        }
    }
}
</script>

<style scoped>
.pop-over {
    position: relative;
    width: 100%;
    height: 100%;
}
.pop-button {
    position: relative;
    width: 100%;
    height: 100%;
    text-decoration:none;
    color: inherit;
}
.pop-list {
    position: absolute;
    left: 0;
    top: 0;
}
.pop-list li {
    width: 100%;
    height: 100%;
    padding: 8px 3px;
    list-style:none;
}
</style>

利用props自定义按钮文字,slot自定义弹窗文字,这样一个简易的Popover组件就完成了。

我学到了什么

  1. directives自定义指定,事件代理,slot练手一番,感觉很爽。
  2. 在看源码的过程中,也看到了render方法的使用,以及museUI的组件化**
  3. 对于组件外控制组件的行为有了新的思路。

知识资源整理

概述

在学校里,我们有现成的课本学习里面的知识,这时候,考验我们的学习知识的能力

但是,在社会上,我们一般没有现成的知识来学习,这个时候就考验我们寻找知识的能力了。

虽然说互联网使知识变得更容易获得了,但同时也让垃圾知识更容易获得了,优质的知识还是那么的难获得。

所以我打算经常整理一下获得知识的途径,供自己以后参考,相信对别人也有用。

前端资讯

InfoQ新闻
掘金
w3ctech

前端团队

AlloyTeam - 腾讯前端团队
FEX - 百度前端研发部
FED - 淘宝前端团队
奇舞团 | 360前端开发团队
凹凸实验室
腾讯网前端团队
携程UED | 前端开发团队
新浪UED | 前端开发
Web前端 腾讯IMWeb团队

UI

京东JDC
UI**
网易用户体验中心

其它资源

前端资源聚合
Bing搜索
V2EX
NPM搜包
JQuery API
jQuery Mobile
小火柴的前端小册子
前端里
前端网(W3Cfuns)
前端观察
前端工程师手册
xufei/blog
阿里云前端知识体系
前端技能汇总-朴灵
入门webpack
ant design
antv蚂蚁数据可视化
egg企业级node建站框架
ant motion设计动效
蚂蚁金服体验科技

代码规范

nec
nej
Eslint 从入门到放弃
airbnb入门

react学习

React.js 小书
React全家桶

前端库

Ant Design of React

前端工具

rgb颜色转换
Live DOM Viewer
转换为jpg文件
图片压缩
gif压缩裁剪编辑合成
在线哈希工具
在线SVG压缩

vue项目向小程序迁移调研

概述

今天调研了一下vue项目怎么向小程序迁移,有些心得,记录下来,供以后开发时参考,相信对其他人也有用。

基本上vue项目向小程序迁移不外乎2种方法,一种是用小程序的web-view组件,另一种是用mpvue重新开发一个。第二种成本太高,所以我这里调研的基本上是第一种方法。

解决方案

对于一般项目来说,直接在小程序中给web-view组件的src填入vue项目的地址即可。但是web-view组件有如下限制:

1.web-view组件的src不能动态变化。这个限制基本可以无视,因为我们是单页面,不需要经常改变web-view的src。但是有些特殊情况需要考虑这个限制,比如我们使用setData方法通过判断url的参数来加载web-view的src是行不通的。

2.小程序的page中只能存在一个web-view。这个限制也可以无视,因为我们只需要一个web-view就行了。

3.web-view里面的项目,不能用window.open(xx, '_blank'),但是可以用window.open(xx, '_self')。

我们项目中有用到window.open(xx, '_blank'),所以我们需要判断是不是小程序环境,是的话就把window.open(xx, '_blank')改成window.open(xx, '_self')。判断方法有如下2种:

// 方法1(推荐)——要求:微信版本>=7.0.0
// 通过判断userAgent中包含miniProgram字样来判断小程序web-view环境。
export function isMiniProgram() {
  return !!navigator.userAgent.match(/miniProgram/i);
}

// 方法2——无要求
// 引入微信sdk,然后用微信sdk判断
npm install weixin-js-sdk --save

wx.miniProgram.getEnv(function(res) {
  console.log(res.miniprogram); //true
});

基本上按照上面的方法就可以使用web-view迁移vue项目到小程序中了。

其它

在调研过程中我还踩了一个其它的坑,这里也记录下来。

1.使用微信sdk的postMessage传的值必须是字符串,所以对于对象来说需要先用JSON.stringify处理一下。

wx.miniProgram.postMessage({ data: JSON.stringify(navigator.userAgent) });

2.web-view的bindmessage属性可以接受postMessage传递过来的值,但是它只会在特定时机(小程序后退、组件销毁、分享)才触发。所以不能期望,马上传完值,web-view就能马上接收到并做出响应。

3.官方文档对于navigateTo的描述有误。传递的值并不在options.query里面,而是就在options里面

wx.navigateTo({
  url: 'test?id=1'
})

// test.js
Page({
  onLoad(options) {
    console.log(options.id)
  }
})

教你怎么看网站是用react搭建的

概述

SPA和react可以说是web开发的分水岭,我一直在寻找判断网站是普通网站还是SPA抑或是react的方法。今天突然找到一个判断网站是不是react搭建的简便方法。现在记录下来供以后开发时参考,相信对其他人也有用。

方法

方法就是利用控制台console

(1)打开你要判断的网站。如果网站的导航全是新打开一个窗口,那么这个网站一定不是react搭建的,甚至连SPA都不是。

(2)按F12打开控制台,在console里面随便输入一个变量。示例如下:

> let test = 55
< undefined
> test
< 55

(3)去点击网站的导航,这个时候页面或者页面的某一部分会刷新。

(4)按F12打开控制台,在console里面查查看刚才的变量是否存在。示例如下:

//如果是react搭建的,则输出如下
> test
< 55

//如果不是react搭建的,则输出如下
> test
< Uncaught ReferenceError: test is not defined
    at <anonymous>:1:1

机制解析

貌似是因为react不发送请求就渲染模块的原因。具体原因我也不太清楚,等以后明白了就补上。

ssh 多秘钥管理和坑

概述

很久之前就想研究一下 ssh 的多秘钥管理,今天正好有时间就研究了一下,挺简单的,记录下来,供以后开发时参考,相信对其他人也有用。

参考资料:

Git - 生成 SSH公钥 , Linux 下多密钥管理

.ssh/config 文件的解释算法及配置原则

查看 ssh-keygen 命令信息

我们首先查看一下 ssh-keygen 这个命令的信息,先使用 man :

man ssh-keygen

提示没有手册信息:

No manual entry for ssh-kengen

那我们再使用 info :

info ssh-keygen

能显示 ssh-keygen 的各个参数信息,具体可以自己看。

生成 ssh 多秘钥

一般我们生成 ssh 的命令是:

ssh-keygen -t rsa

但是这会覆盖原先的秘钥。那怎么生成一个新的不覆盖旧的秘钥呢?我们先来介绍一下 ssh-keygen 的相关指令

ssh-keygen 参数:

-t 指定密钥类型,默认是 rsa ,可以省略。
-f 指定密钥文件存储文件名。
-C 设置注释文字,比如邮箱。

所以我们只需要使用 -f 参数即可:

// 生成 github 的 ssh
ssh-keygen -t rsa -f ~/.ssh/id_rsa.github

需要注意的是,-C 表示注释,不一定要填邮箱。

ssh 秘钥复制

生成 ssh 秘钥之后我们需要复制它然后填到 github 里面去,复制命令是:

clip < ~/.ssh/id_rsa.github.pub

有些 shell 里面没有 clip 这个命令,这个时候如果是 mac 的话,可以使用 pbcopy 命令

// 方法一:使用 <
pbcopy < ~/.ssh/id_rsa.github.pub

// 方法二:使用管道
cat ~/.ssh/id_rsa.github.pub | pbcopy

ssh 测试是否能够连接成功

然后我们来测试一下能不能免密连接:

提示不能连接,因为需要我们配置一下 config 文件。

ssh 多秘钥管理

一般我们生成了多秘钥之后,默认是不生效的,需要我们配置一下 config 文件。配置模板如下:

Host github
	HostName github.com
	IdentityFile ~/.ssh/id_rsa.github
	User git

每一行的解释如下:

// 设置别名为 github
// 域名为 github.com
// 使用 ssh 文件为 ~/.ssh/id_rsa.github
// 用户为 git

这个设置别名的功能非常方便,在设置了别名之后,我们只需要用别名就可以测试是否能够连接成功

ssh -T github

这个时候就能收到成功提示:

Hi xxxxxxx! You've successfully authenticated, but GitHub does not provide shell access.

一个坑

在用如上方法设置之后,能够正常登陆,但是关机后再开机,又不能正常登陆了,这个时候需要把 rsa 添加到 ssh 高速缓存里面去,命令如下:

ssh-add -K ~/.ssh/id_rsa

那我们肯定不能每次开机都要输入上面的命令吧,我们需要开机自动运行上面的命令:

// 创建 .bash_profile
touch ~/.bash_profile
// 在 .bash_profile 中写入命令
echo 'ssh-add -K ~/.ssh/id_rsa' >> ~/.bash_profile

然后在开机的时候就会自动运行 ~/.bash_profile 里面的命令了。

但是如果我们使用了别的 bash 比如 zsh,上述的方法可能会不管用,这个时候我们需要打开 zsh 的配置文件,用配置文件来启动 ~/.bash_profile 这个文件

// 打开 zsh 的配置文件
open ~/.zshrc
// 写入下面的代码
source ~/.bash_profile

或者直接用下面的命令写入:

echo 'source ~/.bash_profile' >> ~/.zshrc

注意:echo 命令后面如果使用单引号则表示写入代码片段,会自动换行;如果使用双引号则表示写入单一字段,不会自动换行

在vue中使用svg sprite

概述

这几天研究了一下在vue中使用svg sprite,有些心得,记录下来,供以后开发时参考,相信对其它人也有用。

在vue中导入svg

在vue中导入svg的方法有很多种,比如在img标签的src属性中导入,但是这样就不能使用class改变svg的颜色。所以一般利用svg的use标签使用内联svg的方法导入。例如下面:

<svg>
  <use xlink:href="@/assets/sprite.svg#notification"/>
</svg>

使用这种方法需要注意一点,就是如果你想把路径写成变量的形式,下面的写法是行不通的:

<svg>
  <use :xlink:href="'@/assets/sprite.svg#notification'"/>
</svg>

因为vue没有解析里面的字符串,所以不能生成所需要的路径,而且,即使能够解析里面的字符串,也由于没有加上hash值导致解析不了。

解决方法是使用require生成相对路径。示例如下:

<svg>
  <use :xlink:href="`${require('@/assets/sprite.svg')}#notification`"/>
</svg>

需要注意的是,使用require的下面写法是不行的。因为路径中的#解析不了。

<svg>
  <use :xlink:href="require('@/assets/sprite.svg#notification')"/>
</svg>

使用svg雪碧图

如果svg图标有很多的话,会发出很多http请求,资源消耗量很大。这个时候最好把svg做成雪碧图。一般情况下,我们手动把要做成雪碧图的svg文件编上id,全部放到一个svg文件比如sprite.svg文件里面去,然后用如下方式引用就可以了(其中notification是引用的svg的id):

<svg>
  <use xlink:href="@/assets/sprite.svg#notification"/>
</svg>

上面的方法看起来很完美,但是有一个很严重的缺点,它的svg雪碧图是存在内存里面的,所以在切换路由的时候,svg雪碧图就没有了,需要重新下载这个svg雪碧图。很浪费资源啊。

改进方案

参考svg-sprite-loader实现加载svg自定义组件VUE-cli3使用 svg-sprite-loader可以看到,可以利用svg-sprite-loader来做svg雪碧图。但是都需要修改webpack的loader配置。

webpack-chain

vue-cli内部是利用webpack-chain插件修改webpack配置的,这是源码。外部也只能在vue.config.js里面利用webpack-chain来修改webpack配置。

具体的使用方法可以参考:webpack-chain文档webpack-配置-module

但是这里有一个坑,就是怎么按条件修改loader配置,比如在某个文件夹使用一种loader,在其它文件夹使用其它loader。看了半天文档,我最后发现,可以用oneOf来实现,其中oneOf(name)的name是自定义的,随便写语义化的名称就行。webpack-chain里面的oneOf和webpack配置里面的oneOf是对应的。实例如下:

module.exports = {
  chainWebpack: config => {
    const resolve = dir => path.join(__dirname, dir);
    const svgRule = config.module.rule('svg');
    svgRule.uses.clear();

    svgRule
      .test(/\.svg$/)
      .oneOf('normal')
      .exclude
        .add(resolve('src/assets/svg-sprite'))
        .end()
      .use('file-loader')
        .loader('file-loader')
        .end()
      .end()
      .oneOf('sprite')
      .include
        .add(resolve('src/assets/svg-sprite'))
        .end()
      .use('svg-sprite-loader')
        .loader('svg-sprite-loader')
        .options({
          symbolId: 'sprite-[name]'
        });
  }
};

还有一个坑就是end方法的使用,在适当的嵌套中需要加end方法返回上一层,否则后面的语句不会执行。

实现svg雪碧图

具体可以参考我写的yi-svg-sprite插件;

解释一下相关的原理和配置:

原理是利用svg-sprite-loader自动形成一个大的svg雪碧图内嵌到app的html里面,然后只需要在其它地方使用id引用里面的svg片段即可。

vue.config.js里面的操作是删除vue-cli里面对svg的loader处理,然后加上自己对svg的loader处理:在svg-sprite文件夹里面使用svg-sprite-loader,在其它文件夹里面使用file-loader(抄的vue-cli原配置);

main.js里面的操作是导入yi-svg-sprite库,并且把svg-sprite文件夹里面的svg文件组装成一个svg雪碧图,id是各自的文件名。

其它svg-sprite库

后来我才发现,已经有很多svg-sprite库了,下面对它们一一评价:

  • vue-svg-sprite:这个svg sprite库使用directive让人眼前一亮,但是它仍然有在切换路由的时候会重新加载svg雪碧图的缺点。
  • vue-cli-plugin-svg-sprite: 这个svg sprite库看起来很完美,也是包装的svg-sprite-loader,但是配置项太多,担心出现其它问题,而且好像没有维护了。

写在 2021 年的结尾(下)

这是对我在 2021 年事业和技术提升方面的总结。

由于疫情的原因,我所在的公司无论是现状还是前景都不是很好,所以我也没有长时间留在公司往上爬的打算。我所在的部门是一个纯业务部门,来了之后大部分时间也在做重构和维护的工作,对电商的 B 端和 C 端的业务有了一些粗浅的理解,但并不深入。对日常开发的 vue 和 nuxt 也有一些粗浅的理解,但并不深入,对大公司前端乃至整个技术部门做的事情也有了一些粗浅的理解,但并不深入。总的来说,在工作方面我做的事情平平淡淡,所幸我的做事风格得到了老大的认可,并且虽然部门没有太多有技术深度的scope,但是老大也给了一些时间让我自己去研究一些东西,这里我要感谢我的老大。

在自我提升方面我基本满意吧。在做的业务都是重构、公司前景也不好的情况下,我之所以没有离职,就是因为我一直在自学计算机基础。今年2021年我一共看了[8本计算机基础书],还看了一个关于 OS 的 coursera 课程,自认为对计算机基础有了一些粗浅的了解了,以后不至于在学 node 或者其它东西的时候看到一些计算机名词不知所云了。在我自己了解了整个技术领域能下到多深之后,我自己对于前端领域的深度提升了。比如我又读了一遍 vue 源码,发现很多之前理解错误的地方,更何况之前理解不清楚的地方了;再比如学 node 相关 api 的时候也能在 OS 里面找到对应的设计原型;我对后端技术也有了一定的了解,不再恐惧用其它语言写后端了。这也帮助我扩展了前端的视野,对我自己要发展的方向有了一些认识,后面我打算深耕前端领域特别是node领域了。

对于来年2022年,我的希望就是能够写一写后端,学习并使用 rust 写一个库,如果可能的话再学一学 electron 或者 flutter吧,最后也希望自己能去一家心仪的公司,加油!!!

kibana研究

概述

Kibana是一个针对Elasticsearch的开源分析及可视化平台,用来搜索、查看交互存储在Elasticsearch索引中的数据。它操作简单,基于浏览器的用户界面可以快速创建仪表板(dashboard)实时显示Elasticsearch查询动态。

安装部署kibana

kibana需要64位操作系统,并且需要先安装Elasticsearch,运行Elasticsearch又需要先安装java

参考资料:

Elasticsearch guide
Kibana guide

下面以mac上安装部署kibana为例:

1.安装java。到java里面下载对应的最新java版本,并安装。

2.安装并运行elasticsearch。到download elasticsearch里面下载对应的最新elasticsearch版本,解压之后进入elasticsearch文件夹运行命令:./bin/elasticsearch。

3.查看elasticsearch是否运行成功。浏览器打开elasticsearch默认地址:http://localhost:9200/,如果显示如下信息则表示运行成功:

{
  "name" : "Cv0Qzv6",
  "cluster_name" : "elasticsearch",
  "cluster_uuid" : "LFNZ-yjkRRW4dcVyuWlbug",
  "version" : {
    "number" : "6.5.3",
    "build_flavor" : "default",
    "build_type" : "tar",
    "build_hash" : "159a78a",
    "build_date" : "2018-12-06T20:11:28.826501Z",
    "build_snapshot" : false,
    "lucene_version" : "7.5.0",
    "minimum_wire_compatibility_version" : "5.6.0",
    "minimum_index_compatibility_version" : "5.0.0"
  },
  "tagline" : "You Know, for Search"
}

4.安装并运行kibana。到download kibana里面下载对应的最新kibana版本,解压之后进入kibana文件夹运行命令:./bin/kibana。

5.查看kibana是否运行成功。在浏览器打开kibana默认地址:http://localhost:5601,如果能够打开kibana则表示kibana运行成功。

6.添加数据。首次打开kibana会提示导入数据,直接倒入demo data即可。然后就可以愉快的调教kibana了。

kibana的大致结构

kibana是开源的,但是只能用于elastic公司的项目。从这里可以看到kibana的源码:elastic/kibana

kibana的大致结构如下:

  1. 整体框架:React 和 Angular 结合使用(利用ngReact库在Angular里面内嵌React)。

  2. ui框架:EUI。Elastic公司自己开发的一套UI,开源,但是不是MIT协议,只能用于Elastic公司开发的产品。https://github.com/elastic/eui。

  3. 可视化框架:D3和Vega。D3官网vega官网

  4. 使用的拖拽库:react-grid-layout

  5. word cloud用的d3-cloud。优点是能够自动把文本直接转化为word cloud。

  6. 实现angular里面使用react的库:ngreact

kibana的主要功能

  1. 主要研究kibana的visualize和dashboard两个板块。

  2. visualize板块的功能:

    • 可以增加或删除自定义的展示图表。
    • 可以给这些展示图表自定义配色方案,标题等。
    • 可以把这个图表分享出去或者内嵌出去。
  3. dashboard板块的功能:

    • 自定义添加visualize里面的图表,或者添加saved searches数据。
    • 每个图表或者数据是一个panel,支持panel的自定义拖拽排序和改变大小。
    • 拥有fileters,queries,time picker三种功能,通过搭配这三种可以实时改变panel中的展示。
    • 可以在dashboard里面针对具体visualization跳转到visualize板块修改对应的visualization。
    • 通过点击panel中的不同位置,可以快捷的实现dashboard的filters操作。
    • 可以通过右飘窗实时查看panel里面的数据。
    • 可以把这个dashboard分享出去或者内嵌出去。

*说明:*visualization指的是visualize板块里面那样的展示图表,saved searches指的是储存的数据,它在dashboard里面以表格的形式展示出来。

kibana的数据结构

为了实现上面的主要功能,kibana有如下的数据结构:

ES

通过一个index储存这三类数据:saved searches, visualizations and dashboards。

  1. saved searches:是用户自定义的查询数据,可以通过visualize板块转化为图表,也可以在dashboards里面查看纯数据。
  2. visualizations:是用户在visualize板块定义的图表数据,包括使用哪个searches,以及自定义配色等。
  3. dashboards:是用户在dashboards板块定义的dashboards数据,包括dashboard标题,里面有哪些visualizations或者saved searches。

embeddables

kibana把所有能放在dashboard上的数据都叫做embeddable,按我的理解就是visualization 和 saved searches的数据。

每个embeddable数据包含2类数据:

  1. 一个是embeddable metadata,它是不变的,包括所有embeddable的配置部分,供用户配置使用;
  2. 另一个是embeddable state,它是可变的,是用户对于某个visualization或者saved data的配置信息,比如saved object id,visualization的配色方案等。

注意:embeddable state里面有没有具体查询出来的数据?我觉得应该有,还应该有相应的查询条件。

通过embeddable state,kibana实现了2个功能:

  1. 可以通过在url里面添加这些state的参数的形式把visualization分享或者内嵌出去。
  2. 通过把这些state和dashboard的state进行交互:实现改变dashboard能够实时改变并且定制panel,比如说定制panel的标题。并且通过panel也可以改变dashboard的filters。

dashboards

每个dashboard都有2类数据:

  1. 一个是dashboard storage data,这是储存在ES中的data,用来提供给panel进行更新,并不放在redux里面。
  2. 另一个是dashboard redux tree,这是dashboard的redux的data,它包含如下几个方面:
  3. metadata。这里修改dashboard的标题和描述。
  4. embeddables。这里放置embeddable state里面redux感兴趣的部分数据,主要把embeddable里面的数据传给dashboard。
  5. panels。这里用来删除panel,增加panel,更新panel,修改panel标题,重置panel标题。
  6. view。这里用来实现fileters,queries,time picker三种功能,还有开启kibana的编辑模式,panel全屏观看等。

dashboards和panel的数据交互

  1. 在panel方面,通过dashboard的redux提供的redux来修改dashboard的数据。
  2. 在dashboard方面,把dashboard storage data通过lodash的_cloneDeep分发给各个panel,每个panel内部再利用lodash的_isequal来比较新旧data来确定更不更新展示,或者利用redux tree的一部分数据来修改panel的展示。(比如自定义panel的标题等。)
  3. 对于dashboard storage data,每个panel都有一个id,然后更新的时候dashboard会把所有panel的id聚合在一起,再加上查询条件,上传给服务器,服务器就返回新的数据,然后通过2来更新panel。当点击save的时候,这些数据就保存为这个dashboard的dashboard storage data。(实际上,kibana实现了如下的包装:savedObjects,savedDashboard,savedSearch等等)

kibana的其它功能

inspector

dashboard板块有这么一个功能:可以通过右飘窗实时查看panel里面的数据。

而从panel到右飘窗之间,kibana封装了一层inspecter,用来处理panel数据并显示到右飘窗上面。这之间的东西又叫做adapter。

目前kibana有2个adapter,一个用来处理visualization里面的数据,一个用来发送http请求(saved searches需要这种处理)。

courier

由于从dashboard到es之间的数据获取是异步的,并且有等待时间。所以kibana封装了一层courier来处理requests,比如说设置ß时间间距啊,中断requests,分发fetch事件来更新panels等。

其中把requests queued up的必要性没有看懂~

D3和Vega

kibana同时使用D3和Vega图表库,其中:

  1. D3主要用于常规图表配置,用户在界面UI的配置。
  2. Vega主要用于让用户使用json数据的形式配置图表,另外Vega也支持多种数据源,用户可以利用URL的形式导入非ES里面的数据源。

kibana的基本架构

目录说明

kibana的主要目录如下(其他目录有的没有看懂,有的是服务目录,有的是处理特定业务逻辑的目录,就不研究了):

├── src
│   ├── core_plugins            #核心插件
│   │   ├── kibana              #kibana插件
│   │   ├── elasticsearch       #处理elasticsearch的插件
│   │   ├── table_vis           #处理table图表的插件
│   │   └── ...                 #其他插件
│   ├── ui                      #ui
│   │   ├── public              #主要ui模块
│   │   │   ├── chrome          #chrome浏览器模块
│   │   │   ├── draggable       #拖拽模块
│   │   │   ├── embeddable      #embeddable模块
│   │   │   ├── i18n            #国际化模块
│   │   │   ├── inspector       #inspector模块
│   │   │   ├── private         #ag模块
│   │   │   ├── register        #ag注册模块
│   │   │   ├── vis             #可视化模块
│   │   │   └── ...             #其他模块
│   │   ├── ui_render           #ui的render
│   │   ├── ui_setting          #ui的setting
│   │   └── ...                 #其它ui模块
│   └── test                    #测试
│       ├── functional          #功能测试
│       ├── scripts             #封装的js
│       ├── dev_certs           #封装的授权js
│       └── ...                 #其他

从上面可以看到:

  1. kibana主要分为core_plugins,ui和test三个模块。
  2. kibana自身的视图只是作为一个插件被放在core_plugins里面,kibana就相当于一般小型项目的src或者view文件夹。
  3. 这么分的目的是因为,kibana项目太大,对于很多模块都会一层层封装,每一层会作为一个独立的模块,这样模块就太多了,然后就考虑把部分模块开发为插件的形式。

下面对于core_plugins,ui和test三个模块分别进行深入研究。

core_plugins

core_plugins中的每一个插件都至少有public文件夹,index.js和package.json。其中public文件夹进行业务处理,index.js进行导出,package.json标注这个插件的名字和版本等信息。

其中index.js的结构是这样的:

// 以类似npm module的形式挂载模块
Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.default = function (kibana) {
  return new kibana.Plugin({
    id:
    require:
    config() {}
    init(){}
    uiExports: {}
  })
}

ui

ui主要包括渲染相关的模块。其中最重要的是modules.js,它里面导出一个uiModules来进行ag模块加载,提取和删除等。

其它一些模块会根据provider和factory来进行封装,其中利用provider来导出,利用factory以工厂模式的形式来创建模块。部分代码示例如下:

// 提供导出内容
const noneRequestHandlerProvider = function () {
  return {
    name: 'none',
    handler: function () {
      return new Promise((resolve) => {
        resolve();
      });
    }
  };
};

// 注册这个模块
VisRequestHandlersRegistryProvider.register(noneRequestHandlerProvider);

// 导出
export { noneRequestHandlerProvider };
// 获取kibana模块
const module = uiModules.get('kibana');

// 以工厂模式的形式封装PersistedState
module.factory('PersistedState', ($injector) => {
  const Private = $injector.get('Private');
  const Events = Private(EventsProvider);

  // Extend PersistedState to override the EmitterClass class with
  // our Angular friendly version.
  return class AngularPersistedState extends PersistedState {
    constructor(value, path) {
      super(value, path, Events);
    }
  };
});

test

像这种大项目又是怎么进行测试的呢?由于测试的每个模块都与很多模块关联,所以kibana做了如下工作:

  1. 自己用node开发了一套测试方法。
  2. 把测试过程中用到的一些方法都封装在一起,然后测试的时候就只调用这些方法即可。
  3. 所有的测试组件都导出成一个函数,然后先加载测试环境,再加载所有的测试进行测试。(所以这不是单元测试!)
  4. 对于其他一些能进行单元测试的模块就用jest进行单元测试。
import expect from 'expect.js';

// 接受测试环境的getService和getPageObjects作为参数导入
export default function ({ getService, getPageObjects }) {
  const log = getService('log');
  const PageObjects = getPageObjects(['common', 'visualize']);

  describe('chart types', function () {
    before(function () {
      log.debug('navigateToApp visualize');
      return PageObjects.common.navigateToUrl('visualize', 'new');
    });

    it('should show the correct chart types', async function () {
      const expectedChartTypes = [
        'Area',
        'Controls',
        'Coordinate Map',
        'Data Table',
        'Gauge',
        'Goal',
        'Heat Map',
        'Horizontal Bar',
        'Line',
        'Markdown',
        'Metric',
        'Pie',
        'Region Map',
        'Tag Cloud',
        'Timelion',
        'Vega',
        'Vertical Bar',
        'Visual Builder',
      ];

      // find all the chart types and make sure there all there
      const chartTypes = await PageObjects.visualize.getChartTypes();
      log.debug('returned chart types = ' + chartTypes);
      log.debug('expected chart types = ' + expectedChartTypes);
      expect(chartTypes).to.eql(expectedChartTypes);
    });
  });
}

和vue项目对比

一般vue项目的目录结构是这样的:

├── src
│   ├── api              #api
│   ├── assets           #图片等资源
│   ├── components       #组件
│   ├── i18n             #国际化
│   ├── plugins          #插件
│   ├── router           #路由
│   ├── store            #store
│   ├── styles           #样式
│   ├── utils            #工具
│   ├── views            #视图
│   │   ├── admin        #管理界面
│   │   ├── auth         #权限界面
│   │   ├── layouts      #视图布局
│   │   └── ...          #其他

对于小项目来说,上面的目录结构已经足够了,并且比kibana的结构更加清晰(个人认为,kibana的目录结构有点混乱,可能是因为kibana迭代过很多次的原因)。但是随着项目的增大,可以考虑参考kibana的目录结构。

另外,对于vue项目,组件一般都是.vue后缀的单文件组件,所以如果想要在这之上对组件进行封装的话,怎么办?mixin和vue.extend了解一下。

声明

以上纯属个人理解,由于我自己水平和时间有限,一些理解错误在所难免,欢迎提出并一起讨论。

感受:

  1. 有些项目就算把源码给你看,你也不一定看得懂。(挥泪~)
  2. 能架构kibana这种项目的才能叫架构师嘛~

参考资料

Dashboard State Walkthrough

Discover Context App Implementation Notes

Discover Context App Implementation Notes

Inspector

unify the way global context is passed down to visualize

[Meta] Improve support for custom embeddable configurations at the dashboard panel level

Embeddables API

vega-vs-vegalite

Vislib general overview

DOMContentLoaded事件中使用异步

概述

我在之前的博文(Performance面板看js加载)中提到过,如果利用监听DOMContentLoaded事件的方式来加载js是不能优化加载的,不能够替代jquery中的ready方法原因是加载js的时候DOMContentLoaded事件还没有结束,自然不会发生页面渲染。

于是我去看jquery的源码,发现jquery里面用到了异步。我灵机一动,对啊,如果利用异步把监听DOMContentLoaded事件来加载的js放到任务队列进行延迟那不就行了。于是我分别用setTimeout和promise的方式来实现,竟然发生了不同的结果,真是令人惊喜。我把过程记录下来,供以后开发时参考,相信对其他人也有用。

使用setTimeout

代码如下:

//haha.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <link rel="stylesheet" type="text/css" href="haha.css">
</head>
<body>
    <div id="haha"></div>
    <script type="text/javascript" src="haha.js"></script>
</body>
</html>

//haha.css
div {
    width: 800px;
    height: 800px;
    background-color: green;
    font-size: 300px;
}

//haha.js
document.addEventListener("DOMContentLoaded", function(event) {
  setTimeout(function() {
    var a = 1;
    while(a < 1000000000) {
        a++;
    }
  }, 0);
});

通过js查看首屏渲染时间,发现非常完美的得到了优化,而且效果和jquery里面的ready方法达到的效果差不多,nice。

有一点需要提出的是,setTimeout可以只加一个参数,因为第二个参数delay有一个默认值,是0。所以上面的haha.js里面的代码可以改写如下:

document.addEventListener("DOMContentLoaded", function(event) {
  setTimeout(function() {
    var a = 1;
    while(a < 1000000000) {
        a++;
    }
  });
});

如果把执行代码变成一个模块,也能完美的嵌入到上面的代码里面。

使用Promise

自然的,那使用es6里面的promise呢?代码如下:

//haha.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <link rel="stylesheet" type="text/css" href="haha.css">
</head>
<body>
    <div id="haha"></div>
    <script type="text/javascript" src="haha.js"></script>
</body>
</html>

//haha.css
div {
    width: 800px;
    height: 800px;
    background-color: green;
    font-size: 300px;
}

//haha.js
document.addEventListener("DOMContentLoaded", function(event) {
  const haha = new Promise( (resolve, reject)=>{
    resolve();
  } );
  haha.then(function() {
    var a = 1;
    while(a < 1000000000) {
        a++;
    }
  });
});

但是这一次,首屏时间没有得到优化

通过复习一遍事件循环和任务队列的知识,可以解释这种情况:setTimeout是放在macrotask queue,这是这是浏览器实现的任务队列,并不是es6规范的任务队列;而promise是放在microtask queue里面,这才是es6规范的任务队列,每一个macrotask可以有许多小的microtask queue,并且当这次的macrotask执行完了之后再执行它下面的microtask queue,执行完microtask queue之后再执行下一个macrotask。

所以当我们用setTimeout的时候,会在DOMContentLoaded事件,渲染事件之后再加一个延迟事件,它是macrotask,与DOMContentLoaded事件和渲染事件同级,所以等DOMContentLoaded事件,渲染事件执行完之后才执行这个延迟的js。但是如果我们用promise的话,只会在DOMContentLoaded事件下的microtask queue塞入js,执行DOMContentLoaded事件完以后不会执行渲染事件,而是会先执行microtask queue。并且,通过看performance面板也可以看到,执行promise里面的代码的时候,DOMContentLoaded事件并没有结束,也是因为microtask queue是属于DOMContentLoaded事件的缘故。

其它

这个例子使我对macrotask queue和microtask queue的理解加深了许多!!!

搭建微服务器:express+https+api代理

概述

最近打算玩一下service worker,但是service worker只能在https下跑,所以查资料自己用纯express搭建了一个微服务器,把过程记录下来,供以后开发时参考,相信对其他人也有用。

参考资料:express官方文档

http服务器

首先我们用express搭建一个http服务器,很简单,看看官方文档就可以搭建出来了。代码如下:

// server.js
const express = require('express');
const http = require('http');

const app = express();
const PORT = 7088; // 写个合理的值就好
const httpServer = http.createServer(app);

app.get('/', function (req, res) {
  res.send('hello world');
});

httpServer.listen(PORT, function () {
  console.log('HTTPS Server is running on: http://localhost:%s', PORT);
});

加入到项目中

我们的理想状况是,在项目目录下建立一个server文件夹,然后在server文件夹里面启动服务器,加载项目目录下的dist文件夹。

所以我们加入代码解析静态资源:

// server.js
const express = require('express');
const http = require('http');

const app = express();
const PORT = 7088; // 写个合理的值就好
const httpServer = http.createServer(app);

app.use('/', express.static('../dist'));

httpServer.listen(PORT, function () {
  console.log('HTTPS Server is running on: http://localhost:%s', PORT);
});

加入https

我们想把http变成https,首先我们要生成本地证书

brew install mkcert
mkcert localhost 127.0.0.1 ::1

上面的代码意思是说,先安装mkcert,然后用mkcert给localhost,127.0.0.1和::1这三个域名生成证书。

然后我们可以在文件夹下面看到2个文件:

秘钥:example.com+3-key.pem
公钥:example.com+3.pem

我们在钥匙串里面把公钥添加信任。方法可参考:在Vue里用Service Worker来搞个中间层(React同理)

添加完之后我们把秘钥和公钥放在certificate文件夹,然后添加到credentials.js文件中,我们通过这个文件引入秘钥和公钥:

// credentials.js
const path = require('path');
const fs = require('fs');

// 引入秘钥
const privateKey = fs.readFileSync(path.resolve(__dirname, './certificate/example.com+3-key.pem'), 'utf8');
// 引入公钥
const certificate = fs.readFileSync(path.resolve(__dirname, './certificate/example.com+3.pem'), 'utf8');

module.exports = {
  key: privateKey,
  cert: certificate
};

最后我们把http变成https,并且引入秘钥和公钥:

// server.js
const express = require('express');
const https = require('https');
const credentials = require('./credentials');

const app = express();
const SSLPORT = 7081; // 写个合理的值就好
const httpsServer = https.createServer(credentials, app);

app.use('/', express.static('../dist'));

httpsServer.listen(SSLPORT, function () {
  console.log('HTTPS Server is running on: https://localhost:%s', SSLPORT);
});

设置api代理

在项目中,我们经常遇到跨域问题,在开发时我们是通过devServer的proxyTable解决的,而proxyTable在打包后是无效的。所以我们需要在服务器上面代理api请求。代码如下:

// proxy.js
const proxy = require('http-proxy-middleware');

const authApi = 'your-authApi-address';
const commonApi = 'your-commonApi-address';

module.exports = app => {
  app.use('/api/auth', proxy({
    target: authApi,
    changeOrigin: true,
    pathRewrite: {
      '/api/auth': '/auth'
    },
    secure: false,
  }));

  app.use('/api/common', proxy({
    target: commonApi,
    changeOrigin: true,
    pathRewrite: {
      '/api/common': '/api'
    },
    secure: false,
  }));
};

写法和devServer里面是一样的,因为devServer底层也是通过express实现的。

然后我们在server.js里面引入上面写的代理:

// server.js
const express = require('express');
const https = require('https');
const setProxy = require('./proxy');
const credentials = require('./credentials');

const app = express();
const SSLPORT = 7081; // 写个合理的值就好
const httpsServer = https.createServer(credentials, app);

app.use('/', express.static('../dist'));

setProxy(app);

httpsServer.listen(SSLPORT, function () {
  console.log('HTTPS Server is running on: https://localhost:%s', SSLPORT);
});

最后

最后我们把server.js,credentials.js和proxy.js放在一起就好了啦!

**用法:**只需要把整个文件夹放到项目目录,在里面运行下面的指令就好了:

yarn i
node server.js

详细代码可以参考我的github

开发油猴脚本:给任意网页的选中文字涂色

概述

简单来说:就像在现实课本上用mark笔涂色划重点一样,可以用这个脚本在任意网页上涂色划重点。

开发缘由:每次在网上看资料的时候,都会默默归纳几个重要的地方,但是看完资料写博客的时候又容易忘重点,所以我开发了这款脚本。

脚本缺陷:(1)不能刷新网页,否则标记就没了。(2)只能标记同一种文字,不能超链接,文本,引用,强调一起标记,但是可以分开标记。

演示

webmark图片演示

脚本代码

首先,需要在浏览器上面安装油猴(Tampermonkey)插件。360浏览器可在扩展中心找到。其它浏览器的安装方法请自行百度。

最后,打开油猴->添加新脚本,把代码复制进去即可

// ==UserScript==
// @name         Mark on Web
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  try to take over the world!
// @author       You
// @match        https://developer.mozilla.org/*
// @match        http://*/*
// @match        https://*/*
// @grant        none
// @copyright  2018+, yang-zhou
// ==/UserScript==

(function() {
    'use strict';

    var funcGetSelectText = function(){
        var txt = '';
        if(document.selection){
            txt = document.selection.createRange().text;//ie
        }else{
            txt = document.getSelection();
        }
        return txt.toString();
        };
    var container = container || document;
    container.onmouseup = function(){
        var txt = funcGetSelectText();
        if(txt)
            {
                event.target.innerHTML =event.target.innerHTML.replace(txt, '<span style="background-color:yellow">'+txt+'</span>');
            }
    };
})();

注意:请确保打开网页的时候开启了油猴插件,并且启用了我编写的脚本。

js查重去重性能优化心得

概述

今天产品反映有个5000条数据的页面的保存按钮很慢,查看代码看到是因为点击保存按钮之后,进行了查重操作,而查重操作是用2个for循环完成了,时间复杂度是O(n^2)。没办法,只能想办法优化一下了。

主要参考了这篇文章:JavaScript 高性能数组去重

源码

简单来说,这个页面的要求是查找一个数组中的重复项,并且返回重复项的行号。源码简化后如下:

    checkData(tableData) {
      // console.time('数组检查重复项时间');
      // 检查重复项,检查空值(全局)
      const repeatMidArr = [];
      const repeatArr = [];

      for (let i = 0; i < tableData.length; i += 1) {
        // 检查重复项
        for (let j = i + 1; j < tableData.length; j += 1) {
          const arr1 = tableData[i].condition;
          const arr2 = tableData[j].condition;
          if (arr1.length === arr2.length && JSON.stringify(arr1) === JSON.stringify(arr2)) {
            repeatMidArr.push(i + 1);
            repeatMidArr.push(j + 1);
          }
        }
      }

      // 给repeatMidArr去重
      repeatMidArr = repeatMidArr.sort();
      if (repeatMidArr.length <= 1) {
        repeatArr = repeatMidArr;
      } else {
        repeatArr.push(repeatMidArr[0]);
        for (let i = 1; i < repeatMidArr.length; i += 1) {
          if (repeatMidArr[i] !== repeatMidArr[i - 1]) repeatArr.push(repeatMidArr[i]);
        }
      }
      // console.timeEnd('数组检查重复项时间');

      if (repeatArr.length !== 0) {
        this.sendRepeatMsg(repeatArr);
        return true;
      }

      return false;
    },

注意:

  1. 因为需要对一个数组查重,所以使用了JSON.stringify把数组转化为字符串简单处理。
  2. 给纯数字数组利用sort方法去重。

优化

优化的核心**是算法中的hash表,也就是字典。在js中可以利用对象的键值不重复这个特性来把对象变成一个hash表。简化后的代码如下:

    checkData(tableData) {
      // console.time('数组检查重复项时间');
      // 检查重复项,检查空值(全局)
      const repeatObj = {};
      let repeatMidArr = [];
      let repeatArr = [];

      for (let i = 0; i < tableData.length; i += 1) {
        // 检查重复项(优化方法)
        const itemCondition = JSON.stringify(tableData[i].condition);
        const index = repeatObj[itemCondition];
        if (!index) {
          repeatObj[itemCondition] = i + 1;
        } else {
          repeatMidArr.push(index);
          repeatMidArr.push(i + 1);
        }
      }

      // 给repeatMidArr去重
      repeatMidArr = repeatMidArr.sort();
      if (repeatMidArr.length <= 1) {
        repeatArr = repeatMidArr;
      } else {
        repeatArr.push(repeatMidArr[0]);
        for (let i = 1; i < repeatMidArr.length; i += 1) {
          if (repeatMidArr[i] !== repeatMidArr[i - 1]) repeatArr.push(repeatMidArr[i]);
        }
      }
      // console.timeEnd('数组检查重复项时间');

      if (repeatArr.length !== 0) {
        this.sendRepeatMsg(repeatArr);
        return true;
      }

      return false;
    },

代码很简单,这里就不细说了。这种方法既然都能用到查重并返回重复项中,当然也能够用到去重里面去。

结果

优化之后,在5000条数据下,点击保存按钮的响应时间从35秒缩短到了3秒,性能提升了10倍!!!

实现简易Promise

概述

异步编程离不开promise, async, 事件响应这些东西,为了更好地异步编程,我打算探究一下promise的实现原理,方法是自己实现一个简易的promise。

根据promise mdn上的描述,我们主要实现如下api:

  1. Promise.prototype.resolve
  2. Promise.prototype.reject
  3. Promise.prototype.then
  4. Promise.all
  5. Promise.race

为了更好地性能和使用,我还需要加上惰性求值特性,即:只有调用then的时候才真正调用Promise里面的异步方法。并且我们忽略pending,fulfilled和rejected这几个状态,因为封装后的promise并没有暴露这几个状态,目前看来也没什么用(除非用事件响应实现Promise)。

为了简便,暂时不考虑错误处理

实现resolve,reject和then

其实就是调用resolve和reject的时候调用相应的success和error函数就行,代码如下:

let Promise = function(func) {
    this.func = func;
    this.success = null;
    this.error = null;
}

Promise.prototype.resolve = function(value, that) {
    console.log('resolve');
    if(typeof that.success == 'function') {
        that.success(value);
    }
}

Promise.prototype.reject = function(value, that) {
    console.log('reject');
    if(typeof that.error == 'function') {
        that.error(value);
    }
}

Promise.prototype.then = function(onFulfilled, onRejected) {
    this.success = onFulfilled;
    this.error = onRejected;
    setTimeout(() => {
        this.func(this.resolve, this.reject);
    });
}

let myPromise = new Promise(function(resolve, reject){
    setTimeout(() => {
        resolve("成功!", this);
    }, 1000);
});

myPromise.then((successMessage) => {
    console.log('输出', successMessage);
});

需要注意的是,这里如果不带入this的话,resolve里面的this就会丢失。

但是这么写不优雅,我想了很多办法,比如重新包装一下,比如用事件响应,但还是解决不了,最后我突然想到,用bind,哇,成功解决。代码如下:

let Promise = function(func) {
    this.func = func;
    this.success = null;
    this.error = null;
}

Promise.prototype.resolve = function(value) {
    if(typeof this.success == 'function') {
        this.success(value);
    }
}

Promise.prototype.reject = function(value) {
    if(typeof this.error == 'function') {
        this.error(value);
    }
}

Promise.prototype.then = function(onFulfilled, onRejected) {
    this.success = onFulfilled;
    this.error = onRejected;
    setTimeout(() => {
        this.func(this.resolve.bind(this), this.reject.bind(this));
    });
}

let myPromise = new Promise(function(resolve, reject){
    setTimeout(() => {
        resolve("成功!", this);
    }, 1000);
});

myPromise.then((successMessage) => {
    console.log('输出', successMessage);
});

值得一提的是,为了实现惰性求值,需要先把异步方法缓存起来,等调用then的时候再调用它。

还有,在Promise内部,为了简便,我使用的是setTimeout进行异步,并没有使用setImmediate

实现all和race

all和race在多异步promise里面非常有用,下面我们来实现它们:

Promise.all = function(promiseArr) {
    let results = [];
    let sum = promiseArr.length;
    let count = 0;
    return new Promise(function(resolve, reject) {
        if(promiseArr || sum) {
            for(let i=0; i<sum; i++) {
                promiseArr[i].then((res) => {
                    results[i] = res;
                    count ++;
                    if(count >= sum) {
                        resolve(results);
                    }
                });
            }
        }
    });
};

Promise.race = function(promiseArr) {
    let sum = promiseArr.length;
    let count = 0;
    return new Promise(function(resolve, reject) {
        if(promiseArr || sum) {
            for(let i=0; i<sum; i++) {
                promiseArr[i].then((res) => {
                    if(count == 0) {
                        count ++;
                        resolve(res);
                    }
                });
            }
        }
    });
};

可以看到,方法是使用传说中的哨兵变量,真的很有用。

测试

简易的测试代码如下:

let myPromise1 = new Promise(function(resolve, reject){
    setTimeout(() => {
        resolve("成功1111111!");
    }, 1000);
});

let myPromise2 = new Promise(function(resolve, reject){
    setTimeout(() => {
        resolve("成功222222222222!");
    }, 1500);
});

myPromise1.then((successMessage) => {
    console.log('输出', successMessage);
});

console.time('all计时开始');
Promise.all([myPromise1, myPromise2]).then((results) => {
    results.map(item => {
        console.log('输出', item);
    });
    console.timeEnd('all计时开始');
});

console.time('race计时开始');
Promise.race([myPromise1, myPromise2]).then((res) => {
    console.log('输出', res);
    console.timeEnd('race计时开始');
});

可以看到,all计时刚好1.5秒,race计时刚好1秒。

我学到了什么

  1. 对Promise的理解更加深入。
  2. 对bind的使用更加熟练。
  3. 可以看到,Promise的缺点是对于每个异步方法,都需要用构造函数封装一遍,如果有其它需求,则需要更特别的封装。
  4. 打算找个时间用事件响应重新实现一遍,目前代码有点乱,如果用事件响应的话可能会更加优雅。

这一个月找工作的感悟

这一个月找工作的感悟

离职之后由于某些原因(医院二三事)我差不多到9月22号才开始找工作,到今天也差不多一个月了,结果是还没找到合适的公司,记一下自己的感悟吧。

大公司需要什么

我面试大公司的时候,记得遇到最多的问题是:xxx的原理是什么?有看过 echarts 是怎么实现的吗?ssr 的原理是什么?客户端激活具体是怎么做的?有什么指标或者 kpi 能够衡量你做的项目?

是的,大公司并不会关注你用了哪些 api 或者接口,他们只会关注你是否知道原理,因为只有明白原理,才能更好地去使用和解决问题;除此之外,他们还会关注你做项目的结果,而不是做项目本身。

现状技术断层

目前市面上的前端现状是:高级紧缺,中低级满地走。为什么会出现这种情况?因为目前的前端发展已经有了很多vue-cli、vue-router、axios这种开箱即用的库,小公司只需要使用这些库的 api 就能很快搭建一个业务,并不需要前端人员了解其中的原理,再加上小公司业务比较简单,触及不到深层次的场景,也就不会需要前端人员去了解这些 api 背后的设计原理和方法。于是小公司的前端人员每天调用这些接口并成功完成一个又一个业务,得到了满足,没有了大公司的眼界和思考,也就更不会深入原理,最终技术无法进一步提升,结果是触及不到高级前端的技术需求。这就导致了技术断层

然后,高级前端人才有的会出来自己创业,有的由于某些原因离职,导致出现了人才需求缺口,但是小公司的中低级前端人才由于以上原因无法成长为高级前端人才,填补这些缺口,所以最终才有了现在高级紧缺,中低级满地走的局面。

解决方案

其实小公司的前端还是有补救方法的,方法就是去学那些底层原理。那么怎么去学呢?

我先说一下我的经历,我最一开始是在一家外包公司做前端开发的,记得当时干了大半年跳槽的时候,面试官问我,你一般是怎么解决遇到的难题的?我当时很确信的回答,先去 google,再去 stackoverflow。当时我觉得我的回答挺好的,因为我确实是这么做的,而且解决了同事解决不了的问题。但是现在看来,当时还是太幼稚了,因为不从文档出发,不从源码出发是很难去完美解决一个问题的

后来我在另一家互联网公司工作,每天写业务,碰到棘手的问题就去看源码,然后进行解决。当时我觉得能去通过看源码解决问题是一件沾沾自喜的值得骄傲的事情。但是现在看来,当时也是太幼稚了,因为当你使用工具库的这个 api 的时候,你难道不应该去关心一下这个工具库的 api 是怎么实现的吗?不然你怎么放心使用它?

所以解决方案已经很清晰了,就是在使用的时候,要思考使用的这个 api 是怎么实现的,自己能不能实现,不能的话就去看源码,自己多对自己进行灵魂拷问,在心中搭建这个库的原理体系。这也是为什么有的培训班会去讲原理,但是没有用的原因,因为终归不是你自己摸索的,你无法凭借理解连成体系。

即使这么做了,可能还是无法跟上大厂顶级前端人才的脚步,因为他们遇到的场景更复杂,所以你思考的绝对没有他们多。这个不是很好解决,只能靠自己的涉猎和学习github上面的优质库来弥补了。

写博客

这里我反省一下自己写博客这件事,因为我发现自己写的博客越来越水了,真的没有技术含量,都不敢发到掘金上面去。其实通过自省我也发现,这也是因为我之前觉得稍微深入一下底层就是很了不起的事情的原因,导致自己研究的内容,其他人早已研究透了习以为常了。

于是怎么解决呢?我自己觉得这不是一个短期能够解决的事情。如果按照我上面的学习方案,我会去接触原理,接触设计方法,但是这些原理和设计方法别人都已经写了好几百遍了,只有通过不断积累,把这些原理和设计方法认为是习以为常的东西,然后再在这个基础上继续向更深的方向挖掘,才能写出高质量的博文。但是在此之前,只能通过写一些比较水的博文来整理自己的知识体系了。

最后

不知不觉写了很多了,虽然我意识到了这个问题,但是未来仍然是任重而道远,我自己的很多不足仍然摆在那里,目前的离职状态对我也十分不利。但是既然决定了前行,就要风雨兼程

《你不知道的javascript》读书笔记

概述

放假读完了《你不知道的javascript》上篇,学到了很多东西,记录下来,供以后开发时参考,相信对其他人也有用。

js的工作原理

  • 引擎:从头到尾负责整个js的编译和运行。(很大一部分是查找操作,因此比如二分查找等查找方法才这么重要。)
  • 编译器:负责语法分析和代码生成。
  • 作用域:收集所有声明的变量,并且确认当前代码对这些变量的访问权限。

LHS查询和RHS查询:

  • LHS查询:当变量出现在赋值操作左边时,会发生LHS查询,如果LHS查询不到,那么会新建一个变量。严格模式下,如果这个变量是全局变量,就会报ReferenceError。
  • RHS查询:当变量出现在赋值操作右边时,会发生RHS查询,如果RHS查询不到,那么会报ReferenceError错误。

TypeError和Undefined:

  • TypeError:当RHS查询成功,但是对变量进行不合理的操作时,就会报TypeError错误,意思是作用域判别成功了,但是操作不合法。
  • Undefined:当RHS查询成功,但是变量是在LHS查询中自动新建的,并没有被赋值,就会报Undefined错误,意思是没有初始化。
//下面这段代码使用了3处LHS查询和4处RHS查询
function foo(a) {
    var b = a;
    return a + b;
}
var c = foo( 2 );

欺骗词法

js中使用的作用域是词法作用域,意思是变量和块的作用域是由你把它们写在代码里的位置决定的。还有一种是动态作用域,意思是作用域是程序运行的时候动态决定的,比如Bash脚本,Perl等。下面的代码在词法作用域中会输出2,在动态作用域中会输出3。

function foo() {
    console.log( a );
}
function bar() {
    var a = 3;
    foo();
}
var a = 2;
bar();

有2种方法欺骗词法作用域,一个是eval,另一个是with,这也是它们被设计出来的目的。需要注意的是,欺骗词法作用域会导致性能下降,因为当编译器遇到它们的时候,会放弃提前设定好他们的作用域,而是需要在运行的时候由引擎来动态推测它们的作用域。

eval()接受一个字符串,这个字符串是一段代码,执行的时候,这段代码中的变量定义会修改当前eval()函数所在的作用域。在严格模式下,eval()函数有自己的作用域,里面的代码不能修改eval()函数所在的作用域。

//修改foo函数中的作用域,使b=3
function foo(str, a) {
    eval( str ); // 欺骗!
    console.log( a, b );
}
var b = 2;
foo( "var b = 3;", 1 ); // 1, 3

//严格模式下,eval()函数有自己的作用域
function foo(str) {
    "use strict";
    eval( str );
    console.log( a ); // ReferenceError: a is not defined
}
foo( "var a = 2" );

with可以把一个对象处理为单独的完全隔离的作用域,它的本意是被当做重复引用同一个对象中的多个属性的快捷方式,但是由于LHS查询,如果对象中没有这个属性的时候,会在全局中创建一个这个属性。在严格模式下,with被完全禁止使用。

function foo(obj) {
    with (obj) {
        a = 2;
    }
}
var o1 = {
    a: 3
};
var o2 = {
    b: 3
};
foo( o1 );
console.log( o1.a ); // 2
foo( o2 );
console.log( o2.a ); // undefined
console.log( a ); // 2——不好,a 被泄漏到全局作用域上了!

匿名函数表达式

匿名函数表达式是一个没有名称标识符的函数表达式,比如下面的:

setTimeout( function() {
    console.log("I waited 1 second!");
}, 1000 );

匿名函数表达式有很多缺点:

  1. 匿名表达式不会在栈追踪中显示出有意义的函数名,使得调试很困难。
  2. 由于没有函数名,所以当想要引用自身的时候只能用arguments.callee,而这又会倒置很多问题。
  3. 匿名函数影响了可读性。一个描述性的名称,可以让代码梗易读。

所以最好始终给函数表达式命名。上面的代码可以改成如下所示:

setTimeout( function timeoutHandler() { // <-- 快看,我有名字了!
    console.log( "I waited 1 second!" );
}, 1000 );

IIFE

之前我在博文中说明过IIFE,所以这里只补充一个IIFE的其它用途,就是传入一些特殊的值。

//传入undefined
undefined = true; // 给其他代码挖了一个大坑!绝对不要这样做!
(function IIFE( undefined ) {
    var a;
    if (a === undefined) {
        console.log( "Undefined is safe here!" );
    }
})();

//传入this
(function IIFE( this ) {
    console.log( this.a );
}
})(this);

显式的块作用域

有时候,可以把一段代码显式地用块包起来,这样写能够更易读,也更容易释放内存。如下所示:

function process(data) {
    // 在这里做点有趣的事情
}
// 在这个块中定义的内容可以销毁了!
{
    let someReallyBigData = { .. };
    process( someReallyBigData );
}
var btn = document.getElementById( "my_button" );
btn.addEventListener( "click", function click(evt){
    console.log("button clicked");
}, /*capturingPhase=*/false );

代码缺陷

for (var i=1; i<=5; i++) {
    setTimeout( function timer() {
        console.log( i );
    }, i*1000 );
}

上面的例子是一个很常见的前端面试题。我们来深入研究一下。

首先是写出这段代码我们期望什么?我们期望每一个循环中都有一个当前i的副本被绑定到setTimeout函数里面,所以当setTimeout函数执行的时候,会输出不同的i值。

但是事实并不是这样的,一个原因是setTimeout函数是异步的,另一个原因是所有的setTimeout函数所在的作用域都是全局作用域,这个全局作用域中只有一个i(只有函数作用域的代码缺陷)。

所以解决方法是给每一个setTimeout函数创建一个独自的作用域,可以用闭包创建函数作用域,也可以用let创建块作用域。

现代的模块机制

现代的模块机制有AMD模块机制和CMD模块机制。前者是在模块执行之前加载依赖模块,后者是在模块执行的时候动态加载依赖模块。

下面是AMD模块机制的模块加载器。需要注意的是deps[i] = modules[deps[i]];作用是加载依赖模块,modules[name] = impl.apply( impl, deps );作用是加载模块impl。

//通用的模块加载器
var MyModules = (function Manager() {
    var modules = {};
    function define(name, deps, impl) {
        for (var i=0; i<deps.length; i++) {
            deps[i] = modules[deps[i]];
        }
        modules[name] = impl.apply( impl, deps );
    }
    function get(name) {
        return modules[name];
    }
    return {
        define: define,
        get: get
    };
})();

为什么modules[name] = impl.apply( impl, deps );不写成modules[name] = impl( deps );?为什么要给自己传一个自己的this指针进去?原因是如果不传进去的话,impl( deps )中的this会指向全局作用域!

未来的模块机制

最新的es6的模块机制是这样的:

bar.js
function hello(who) {
    return "Let me introduce: " + who;
}
export hello;
foo.js
// 从 "bar" 模块导入 hello()
import hello from "bar";
var hungry = "hippo";
function awesome() {
    console.log(
        hello( hungry ).toUpperCase()
    );
}
export awesome;

闭包

看完本书之后感觉自己对闭包的理解还是不够深刻。闭包真正的理解是:当函数在当前作用域之外执行的时候,它仍然能够访问自己原本所在的作用域,这个时候就出现了闭包。

在哪些地方用到了闭包?闭包在不污染全局变量,定义模块和立即执行函数方面有很多运用,特别要注意的是,所有异步操作中的回调函数都使用了闭包。比如定时器,事件监听器,Ajax请求,跨窗口通信,WebWorkers等。因为在异步编程中,回调函数一般是在代码执行完毕之后再执行的,这个时候怎么记住回调函数里面的各种参数(即回调函数的作用域)?当然是用闭包啦。

另一点需要注意的是,回调函数会丢失this。比如下面的代码:

function foo() {
    console.log( this.a );
}
var obj = {
    a: 2,
    foo: foo
};
var a = "oops, global"; // a 是全局对象的属性
setTimeout( obj.foo, 100 ); // "oops, global"

虽然foo函数执行的时候,前面有一个obj,但是foo里面的this指向的却是全局对象,原因是setTimeout()函数的伪代码其实是如下所示的,它执行了这个操作fn=obj.foo;fn()。所以实际调用的是fn()函数。(同时也可以很明显的看出,foo函数并没有在它定义的那个作用域执行,而是跑到了setTimeout的作用域,所以出现了闭包。)

function setTimeout(fn,delay) {
// 等待 delay 毫秒
fn(); // <-- 调用位置!
}

this

this设计的初衷是提供一种更优雅的方式来隐式“传递”一个对象引用,因此可以将API设计的更加简洁并且易于复用。

判断this的指向:

  1. 函数是否在 new 中调用( new 绑定)?如果是的话 this 绑定的是新创建的对象。var bar = new foo()
  2. 函数是否通过 call 、 apply (显式绑定)或者硬绑定调用?如果是的话, this 绑定的是
    指定的对象。var bar = foo.call(obj2)
  3. 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话, this 绑定的是那个上
    下文对象。var bar = obj1.foo()
  4. 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到 undefined ,否则绑定到
    全局对象。var bar = foo()

值得说明的是,箭头函数并没有使用上面的规则,而是根据外层的作用域来决定this。所以箭头函数常用于回调函数中(因为回调函数丢失了this,会造成很多错误)。

另外Function.prototype.bind()函数使用了上面的规则,只不过强制把this绑定到定义的作用域上面。它与箭头函数有着本质的不同。

使用apply展开数组

下面的例子是使用apply把数组展开为参数。es6中可以用...展开数组。

function foo(a,b) {
    console.log( "a:" + a + ", b:" + b );
}
// 把数组“展开”成参数
foo.apply( null, [2, 3] ); // a:2, b:3
// 使用 bind(..) 进行柯里化
var bar = foo.bind( null, 2 );
bar( 3 ); // a:2, b:3

更安全的this

一个非常安全的做法是把this绑定到一个不会对程序造成任何影响的空对象上面,而Object.create(null)和{}很像,但是并不会创建Object.
prototype这个委托,所以它比{}“更空”,所以一般把this绑定到Object.create(null)对象上面。不过es6规定的严格模式对这种情况有缓解。

function foo(a,b) {
    console.log( "a:" + a + ", b:" + b );
}
// 我们的 DMZ 空对象
var ø = Object.create( null );
// 把数组展开成参数
foo.apply( ø, [2, 3] ); // a:2, b:3
// 使用 bind(..) 进行柯里化
var bar = foo.bind( ø, 2 );
bar( 3 ); // a:2, b:3

软绑定

这里介绍一种软绑定,只把代码放在下面,代码我还没有看懂。。。。

//软绑定函数softBind
if (!Function.prototype.softBind) {
    Function.prototype.softBind = function(obj) {
        var fn = this;
        // 捕获所有 curried 参数
        var curried = [].slice.call( arguments, 1 );
        var bound = function() {
            return fn.apply(
                (!this || this === (window || global)) ?
                obj : this
                curried.concat.apply( curried, arguments )
            );
        };
    bound.prototype = Object.create( fn.prototype );
    return bound;
    };
}

//软绑定例子
function foo() {
    console.log("name: " + this.name);
}
var obj = { name: "obj" },
obj2 = { name: "obj2" },
obj3 = { name: "obj3" };
var fooOBJ = foo.softBind( obj );
fooOBJ(); // name: obj
obj2.foo = foo.softBind(obj);
obj2.foo(); // name: obj2 <---- 看!!!
fooOBJ.call( obj3 ); // name: obj3 <---- 看!
setTimeout( obj2.foo, 10 );
// name: obj <---- 应用了软绑定

对象的误区

经常可以在js中听到一句话,**万物皆对象,其实在某种意义上来说,这句话是错的。**因为js中还有很多对象的子类型,比如函数,数组,内置对象等,他们除了有对象的性质之外,还具有一些特别的行为,严格说来,他们不等于对象。

在其它语言中,属于对象的函数通常被称为方法。因此我们经常把对象里面的函数称为方法。但是严格说来,对象里面的函数并不属于这个对象,对象中的这个函数在很多情况下都只是个引用而已。

怎么复制对象

复制对象有深复制和浅复制。浅复制非常简单,es6定义了如下方法来复制一个对象。

var newObj = Object.assign({}, myObject);

另外,对于JSON安全的对象来说,有下面这种巧妙的复制方法。JSON安全指的是被序列化为一个JSON字符串后再解析回来的对象和原对象完全一样。

var newObj = JSON.parse(JSON.stringify(someObj));

对象遍历

  1. 可以用for..in来遍历对象的可枚举属性列表。
  2. 可以用map(),some(),every()来遍历数组。
  3. es6中定义了for..of来遍历数组的值。

在js中实现类

我们引入类这种设计模式,就是为了简化代码的书写。类这种设计模式通过下面三种方式简化代码书写:

  1. 混入。把一个类的属性和方法直接扔到另一个类里面去,来复制前一个类的代码。
  2. 继承。使一个子类继承它的父类,让子类复制父类的所有代码。
  3. 实例。通过创建一个实例,让实例复制类的方法。

由于js中最基本的是对象,所以首先来看怎么用对象来*"实现"*一个类,即给对象实现上面的三种代码复制方式。

在对象之间实现混入其实很简单,就是对象的复制。代码如下:

// 非常简单的 mixin(..) 例子 :
function mixin( sourceObj, targetObj ) {
    for(key in sourceObj) {
        if(!(key in targetObj)) {
            targetObj[key] = sourceObj;
        }
    }
}
var Vehicle = {
    engines: 1,
    ignition: function() {
        console.log( "Turning on my engine." );
    },
    drive: function() {
        this.ignition();
        console.log( "Steering and moving forward!" );
    }
};
var Car = mixin( Vehicle, {
    wheels: 4,
    drive: function() {
        Vehicle.drive.call( this );
        console.log(
            "Rolling on all " + this.wheels + " wheels!"
        );
    }
} );

接下来是继承,继承其实还好,和混入差不多,但是第三种实例呢?怎么通过对象来生成一个实例?由于对象本来就是js中最小的单位,那还怎么生成更小的单位?

所以我们考虑复杂一点的对象,比如函数,然后函数实例化后就可以得到一个对象了。

那么对于函数,怎么实现第一种方法混入呢?很简单,用call()函数,代码如下:

function Vehicle() {
    this.engines = 1;
    //实例方法
    this.ignition = function() {
        console.log( "Turning on my engine." );
    }
}
//原型方法
Vehicle.prototype.drive = function() {
    this.ignition();
    console.log( "Steering and moving forward!" );
};

function Car() {
    //混入Vehicle的属性和实例方法
    Vehicle.call(this);
    //自己的属性
    this.engines = 2;
    //混入Vehicle的原型方法
    for(key in Vehicle.prototype) {
        Car.prototype[key]=Vehicle.prototype[key];
    }
    //然后可以定义自己的方法
}

然后是第二种方法继承,继承和混入差不多,可以用上面的来实现,也可以用实例来实现,书上有很多方法,我就不写了。

最后是第三种方法实例。用new关键字可以给函数构造一个对象,我们可以叫它为实例,因为这个对象复制了构造函数的代码。代码如下:

var car = new Car();

这样就完成了!我们用js中的函数对象实现了一个类。看起来很美好,但是用起来不是那么的舒服。原因是js中对于对象的复制并不是复制代码,而是复制引用!

也就是说,在用函数对象实现一个类的过程中,混入和继承实际上并没有复制代码,而是在复制引用!这就是js糟糕的继承方式,也是实现类最大的痛点。

值得注意的是,由于第三种方法实例是在复制实例属性和方法的代码(虽然也是在引用原型方法的代码),所以有些人用第三种方法实例来实现第二种方法继承,这就是各种继承方法的本质由来。而这些带有技巧性并且难读的复杂方法,都或多或少的带有另一些痛点。

最后es6推出了class关键字来定义一个类,并规范化了混入,继承和实例,对很多方法进行了一些官方的封装。不得不说这个改变使得类在js中方便书写了很多。

原型

函数有一条特殊的性质,那就是可以通过new来获得一个对象。为了实现共享的方法,在获得这个对象的同时,会有一个不可枚举的属性[[Prototype]]被附加到了这个对象中,这个属性就是我们所说的原型。这个属性被关联到了构造函数的prototype属性。这样,所有通过new的子对象都可以访问构造函数的prototype中的同一个方法。值得注意的是,这里并不是复制,而是引用。

function Foo() {
    // ...
}
var a = new Foo();
Object.getPrototypeOf( a ) === Foo.prototype; // true

而构造函数的prototype属性是怎么来的呢?答案是,这个属性是所有的函数自带的属性。

值得注意的是,对象的原型的作用不仅仅是为了访问构造函数的共享方法,它也是对象之间互相访问的桥梁。

那么这个[[Prototype]]到底是什么?它其实有一个名字,叫做**proto**。它的实现大致是这样的:

Object.defineProperty( Object.prototype, "__proto__", {
    get: function() {
        return Object.getPrototypeOf( this );
    },
    set: function(o) {
        // ES6 中的 setPrototypeOf(..)
        Object.setPrototypeOf( this, o );
        return o;
    }
} );

一般来说,我们并不直接调用__proto__来访问对象的原型,那我们怎么对对象的原型进行各种关联操作呢?代码如下:

var foo = {
    something: function() {
        console.log( "Tell me something good..." );
    }
};
var bar = Object.create( foo );
bar.something(); // Tell me something good...

Object.create的简化的源码如下。由于Object.create是在es5中声明的,所以下面的代码也可以当做polyfill代码。

if (!Object.create) {
    Object.create = function(o) {
        function F(){}
        F.prototype = o;
        return new F();
    };
}

委托

委托是另一种设计模式,它非常适合js,示例代码如下:

Task = {
    setID: function(ID) { this.id = ID; },
    outputID: function() { console.log( this.id ); }
};

// 让 XYZ 委托 Task
XYZ = Object.create( Task );
XYZ.prepareTask = function(ID,Label) {
    this.setID( ID );
    this.label = Label;
};
XYZ.outputTaskDetails = function() {
    this.outputID();
    console.log( this.label );
};
// ABC = Object.create( Task );
// ABC ... = ...

这段代码的易读性很好,相比较类这种设计方式来说,可以直接看出我们要干什么。

个人觉得对于小型系统,用委托这种设计模式远远优于类设计模式。对于复杂的系统,我还没有体验过,不好妄下判断。

antd在webpack里面的配置

概述

antd是蚂蚁金服打造的一个react组件,真的非常棒,我看了下官方文档,感觉比bootstrap要好。唯一的缺点可能就是打包的时候要打包它的一些样式表,所以资源体积会很大,并且css可能会和自己的相冲突。

我在webpack中配置antd配置了很久,现在把它记录下来,供以后开发时参考,相信对其他人也有用。

配置

配置代码如下:

    module: {
        loaders: [
        {
            test: /\.(js|jsx)$/,
            exclude: /node_modules/,
            loader: 'babel-loader',
            query: {
                presets:['react', 'es2015'],
                plugins:[
                    ['import', {libraryName: 'antd', style: 'css'}] //按需加载
                ]
            },
        },
        {
            test: /\.css$/,
            loader: 'style-loader!css-loader'
        },
        ]
    }

我从中学到了什么

由于自己还是比较小白的,所以在写配置文件的时候踩了许多坑,记录如下:

(1)引号后面的大括号,中括号,小括号的左括号{、{、(不能另起一行。(本来为了好看,我就把他们另起一行写,但是这样是不行的,原因是编译的时候会自动在引号后面加分号,造成错误)

(2)变量名一定要注意,有的变量名要用单引号括起来,有的不需要。(比如上面的libraryName和style就不能用单引号括起来)

深入理解class和装饰器

深入理解class和装饰器

class 的出现大大简化了 javascript 中类的写法,而装饰器又是 class 里面非常实用的功能,但是老实说,它们都是语法糖,并没有引入新的功能,那它们的原理是怎样的呢?本文来一一探究。通过本文,您可以学到:

  1. class 语法糖的原理是什么?
  2. super 的原理是什么?有什么注意事项?
  3. 装饰器的原理是什么?
  4. vue-class-component 是怎么实现 vue 的 class 写法的?
  5. vue-property-decorator 是怎么实现 watch 装饰器的?

class 语法糖

我们首先用 class 的写法写一个 demo:

class A {
  constructor(name) {
    this.name = name;
  }

  say() {
    console.log(this.name);
  }

  static move() {
    console.log('move');
  }
}

class B extends A {
  constructor() {
    this.a = 1;
    super();
    this.b = 2;
  }

  hello() {
    console.log('hello');
  }

  static go() {
    console.log('go');
  }
}

通过分析 babel 打包后的代码,其实可以简化成下面这样:

var A = /*#__PURE__*/function () {
  function A(name) {
    this.name = name
  }

  A.prototype.say = function say() {
    console.log(this.name)
  }

  A.move = function move() {
    console.log('move')
  }

  return A
}()

var B = /*#__PURE__*/function (_A) {
  var _super = function _createSuperInternal() {
    return _A.apply(this, arguments) || this
  }

  function B() {
    var _this;

    // _this.a = 1; // 这里会出现 _this 未定义,导致报错,所以不能在 super 之前绑定实例属性
    _this = _super.call(this)
    console.log(_this)
    _this.b = 2;
    return _this;
  }

  // 这里其实等价于:
  // B.prototype = Object.create(_A.prototype)
  // B.prototype.constructor = B
  B.prototype = Object.create(_A.prototype, {
    constructor: {
      value: B,
      writable: true,
      configurable: true
    }
  })

  B.prototype.hello = function hello() {
    console.log('hello');
  }

  B.go = function go() {
    console.log('go');
  }

  return B;
}(A);

可以看到:

  1. class 语法糖原理其实就是使用 constructor 作为构造函数,然后在构造函数的 prototype 上面绑定实例方法,并且直接在构造函数上面绑定静态方法
  2. class 的继承就是组合继承的形式。

关于 super

我们从上面可以看到,super 其实是内部创建的一个方法,它使用构造函数继承的方法来继承实例属性。但由于 _this 其实是由 super 返回的,所以如果在 super 之前绑定实例属性的话,_this 还未定义,导致报错。所以在 super 之前不能绑定实例属性

那这里的 _this 为什么不直接用自己的 this 呢?

我们来思考这样一种场景,就是构造函数里面有返回值

class C {
  constructor() {
    return {a:2}
  }
}

上面的类会被编译成:

var C = /*#__PURE__*/function () {
  function C(name) {
    return {a:2}
  }

  return C
}()

我们在实例化这个类的时候,其实得到的是构造函数的返回值,即{a:2}这个对象。所以如果父类是这种返回对象的形式的话,子类在继承的时候,就必须在这个返回值上面绑定实例属性,而不是在自己的this上面绑定实例属性。

装饰器

在 vue 中,我们一般使用vue-class-component来把 vue 里面组件的写法转变为类形式的,写法如下:

<template>
  <div>{{ message }}</div>
</template>

<script type='ts'>
import { Vue, Component, Watch } from 'vue-property-decorator';

@Component
export default class HelloWorld extends Vue {
  // Declared as component data
  message = 'Hello World!'

  @Watch('visible')
  onVisibleChanged(newValue: any) {
    this.$emit('input', newValue);
  }
}
</script>

那么它是怎么实现的呢?主要分为 2 步:

  1. 在打包的时候会把装饰器打包成原始代码
  2. component 装饰器会对组件类做一些处理

装饰器是怎么打包的

装饰器是一个函数,它接收的三个参数:

  1. target(对象的 prototype,如果是类装饰器的话,就只有这一个参数)
  2. key(当前的方法名或属性名)
  3. descriptor(就是用于 defineProperty 的 config)

我们经常使用的 class 装饰器有 2 种(存在的一共有4种,我们只讨论常见的这 2 种):

  1. 放在 class 上的类装饰器
  2. 放在方法上的方法装饰器

首先我们来看一段示例代码:

function show(target, key, descriptor) {
  console.log(target);
  console.log(key);
  console.log(descriptor);
}

// 类装饰器
@show
class A {
  constructor(name) {
    this.name = name;
  }

  // 方法装饰器
  @show
  say() {
    console.log(this.name);
  }
}

打包之后的简化代码如下:

function _applyDecoratedDescriptor(target, property, decorators, descriptor) {
  var desc = {};

  Object.keys(descriptor).forEach(function (key) {
    desc[key] = descriptor[key];
  });

  desc = decorators.slice().reverse().reduce(function (desc, decorator) {
    return decorator(target, property, desc) || desc;
  }, desc);

  return desc;
}

function show(target, key, descriptor) {
  console.log(target);
  console.log(key);
  console.log(descriptor);
}

var A = show(_class = (_class2 = /*#__PURE__*/ function () {
  function A(name) {
    this.name = name;
  }

  A.prototype.say = function say() {
    console.log(this.name);
  }

  return A;
}(), (_applyDecoratedDescriptor(
  _class2.prototype,
  "say",
  [show],
  Object.getOwnPropertyDescriptor(_class2.prototype, "say")
  )
), _class2)) || _class;

可以看到:

  1. 对于类装饰器,只接收了 _class 参数,而_class =后面的括号里的三个值其实是一种顺序写法,最终返回的是括号里面的最后那个值也就是 _class2。
  2. 对于方法装饰器,会被放到一个数组里面去,然后调用 _applyDecoratedDescriptor 对被装饰的方法顺序执行各个装饰器(谁在上面谁先执行)。
  3. _applyDecoratedDescriptor 会收集 prototype、method key 和 property descriptor,然后传给装饰器进行执行。
  4. 这里注意一下类装饰器和方法装饰器的执行顺序:先执行方法装饰器再执行类装饰器

到这里就很清晰了,装饰器其实并不是什么黑魔法,只是在编译的时候依次给类或者对象执行的函数罢了。

vue-class-component 是怎么实现 vue 的 class 写法的?

vue-class-component是通过 @component 装饰器来实现 vue 的 class 写法的,源码如下:

// index.ts
function Component (options: ComponentOptions<Vue> | VueClass<Vue>): any {
  // 要装饰的类其实是函数类型,所以会从这里进入
  if (typeof options === 'function') {
    return componentFactory(options)
  }
  // 如果传入一个对象的话,就返回一个装饰器函数
  return function (Component: VueClass<Vue>) {
    return componentFactory(Component, options)
  }
}

它对传入的参数做了一层适配:

  1. 如果传入的是函数类型,则证明是一个类,然后直接返回装饰器。(所以能够支持@component()这种不加参数的写法)
  2. 如果传入的不是函数类型,则证明是一个配置,然后返回装饰器函数。(所以能够支持@component(config)这种加参数的写法)

最终,它是通过调用 componentFactory 来进行装饰的,它的源码如下:

export const $internalHooks = [
  'data',
  'beforeCreate',
  'created',
  'beforeMount',
  'mounted',
  'beforeDestroy',
  'destroyed',
  'beforeUpdate',
  'updated',
  'activated',
  'deactivated',
  'render',
  'errorCaptured', // 2.5
  'serverPrefetch' // 2.6
]

export function componentFactory (
  Component: VueClass<Vue>,
  options: ComponentOptions<Vue> = {}
): VueClass<Vue> {
  options.name = options.name || (Component as any)._componentTag || (Component as any).name
  // prototype props.
  const proto = Component.prototype
  Object.getOwnPropertyNames(proto).forEach(function (key) {
    if (key === 'constructor') {
      return
    }

    // 加上生命周期函数、钩子函数
    if ($internalHooks.indexOf(key) > -1) {
      options[key] = proto[key]
      return
    }
    const descriptor = Object.getOwnPropertyDescriptor(proto, key)!
    if (descriptor.value !== void 0) {
      // 加上 methods
      if (typeof descriptor.value === 'function') {
        (options.methods || (options.methods = {}))[key] = descriptor.value
      } else {
        // 使用 mixins 的形式加上 data
        (options.mixins || (options.mixins = [])).push({
          data (this: Vue) {
            return { [key]: descriptor.value }
          }
        })
      }
    } else if (descriptor.get || descriptor.set) {
      // 加上 computed
      (options.computed || (options.computed = {}))[key] = {
        get: descriptor.get,
        set: descriptor.set
      }
    }
  })

  // 收集 constructor 上的 data
  ;(options.mixins || (options.mixins = [])).push({
    data (this: Vue) {
      return collectDataFromConstructor(this, Component)
    }
  })

  // 处理其它装饰器(方法装饰器、属性装饰器等)
  const decorators = (Component as DecoratedClass).__decorators__
  if (decorators) {
    decorators.forEach(fn => fn(options))
    delete (Component as DecoratedClass).__decorators__
  }

  // 初始化 super 里面的实例属性
  const superProto = Object.getPrototypeOf(Component.prototype)
  const Super = superProto instanceof Vue
    ? superProto.constructor as VueClass<Vue>
    : Vue
  const Extended = Super.extend(options)

  // 处理子类和父类的静态方法
  forwardStaticMembers(Extended, Component, Super)

  // 复制使用 reflect 声明的属性
  if (reflectionIsSupported()) {
    copyReflectionMetadata(Extended, Component)
  }

  return Extended
}

总的来说,这段代码做了如下工作。其实就是筛选出相应的属性和方法按 options 的形式进行组装罢了

  1. 保存一个内部钩子列表,筛选出生命周期钩子、路由钩子等作为相关的方法。(所以我们如果要加入路由钩子的话,首先需要先把它加到列表里面去)
  2. 筛选出 data、methods、computed 加到 options 上面去。(所以这些数据要遵循相应的写法)
  3. 收集 constructor 上的 data
  4. 初始化 super 里面的实例属性
  5. 处理子类和父类的静态方法
  6. 复制使用 reflect 声明的属性

这里需要注意的是,在收集 data 的时候,并不是直接把 data 进行赋值的,因为 data 可以是一个函数,所以这里使用mixins的方法进行混合。

vue-property-decorator 的 watch 装饰器的原理

vue-property-decorator是基于vue-class-component封装的库,它提供了很多方便的装饰器,现在我们来看下它的 watch 装饰器。源码如下:

// vue-class-component 库
export function createDecorator (factory: (options: ComponentOptions<Vue>, key: string, index: number) => void): VueDecorator {
  return (target: Vue | typeof Vue, key?: any, index?: any) => {
    const Ctor = typeof target === 'function'
      ? target as DecoratedClass
      : target.constructor as DecoratedClass
    if (!Ctor.__decorators__) {
      Ctor.__decorators__ = []
    }
    if (typeof index !== 'number') {
      index = undefined
    }
    Ctor.__decorators__.push(options => factory(options, key, index))
  }
}

// vue-property-decorator 库
export function Watch(path: string, options: WatchOptions = {}) {
  const { deep = false, immediate = false } = options

  return createDecorator((componentOptions, handler) => {
    if (typeof componentOptions.watch !== 'object') {
      componentOptions.watch = Object.create(null)
    }

    const watch: any = componentOptions.watch

    if (typeof watch[path] === 'object' && !Array.isArray(watch[path])) {
      watch[path] = [watch[path]]
    } else if (typeof watch[path] === 'undefined') {
      watch[path] = []
    }

    watch[path].push({ handler, deep, immediate })
  })
}

这段代码其实就是在 componentOptions 上面开了一个 watch 属性,用来把各个字符串的 watch 函数推进去。值得注意的是执行过程

  1. 首先执行方法装饰器,把装饰器工厂函数推到__decorators__保存起来,此时装饰器并没有被执行
  2. 然后执行 component 装饰器,在执行过程中,会把__decorators__里面的装饰器取出,然后执行,这个时候方法装饰器才生效了。

有一点非常奇怪,因为在 component 装饰器里面,会先把实例方法(就是 watch 的方法)挂载到 methods 里面去,然后再执行方法装饰器,把方法作为 handler 推到相应的 watch 数组里面去。那么这个实例方法不是没有从 methods 里面删除吗?看了半天源码也没找到删除的地方,期待大佬解答~~

markdown上下左右,跳至行尾行首,重设快捷键

概述

用markdown输入代码的时候觉得下面2件事非常不方便:

(1)光标上下左右。(需要挪动手去按方向键)

(2)光标跳至行尾和行首。(需要动手去按Home和End键)

为了简化,我特地更改了ST3的快捷键,供自己开发时参考,相信对其他人也有用。

代码

在ST3的快捷键设置中加入如下代码即可:

    { "keys": ["alt+a"], "command": "move_to", "args": {"to": "bol", "extend": false} },
    { "keys": ["alt+f"], "command": "move_to", "args": {"to": "eol", "extend": false} },
    { "keys": ["alt+j"], "command": "move", "args": {"by": "characters", "forward": false} },
    { "keys": ["alt+l"], "command": "move", "args": {"by": "characters", "forward": true} },
    { "keys": ["alt+i"], "command": "move", "args": {"by": "lines", "forward": false} },
    { "keys": ["alt+k"], "command": "move", "args": {"by": "lines", "forward": true} },

快捷键设置情况为:

上: alt + I

下: alt + K

左: alt + J

右: alt + L

跳至行首: alt + A

跳至行尾: alt + F

理解js中的函数调用和this

概述

这是我看typescript的时候看引用资源看到的,原文在这里:Understanding JavaScript Function Invocation and "this",我简单地总结一下记下来供以后开发时参考,相信对其他人也有用。

机制

js中的函数调用机制是这样的:

  1. 建立一个表argList,从索引1开始塞入函数的参数。
  2. 表的索引0的值时thisValue。
  3. 把this赋给thisValue,然后调用func.call(argList)。

说这么多,其实就是想说明,函数调用f(x,y)其实就是f.call(this, x, y)的语法糖。内部是通过f.call(this, x, y)来调用的。

可以看到,虽然f(x,y)里面没有this,但是f.call(this, x, y)里面有this,所以非常好理解为什么函数调用中会有this了。

那么this是从哪里来的呢?当f(x,y)没有调用者的时候,this自动被赋值为window;当f(x,y)有调用者的时候,this被赋值为指向调用者。

例子

举几个实例感受一下:

//例子1
function hello(thing) {
  console.log(this + " says hello " + thing);
}
hello.call("Yehuda", "world"); //输出Yehuda says hello world

//例子2
function hello(thing) {
  console.log(this + " says hello " + thing);
}
//相当于hello.call(window, "world");
hello("world"); // 输出[object Window] says hello world

//例子3
function hello(thing) {
  console.log(this + " says hello " + thing);
}
let person = { name: "Brendan Eich" };
person.hello = hello;
//相当于hello.call(person, "world");
person.hello("world"); //输出[object Object] says hello world

**注意:**我们这里不是strict mode。

更优雅的写法

有时候,我们希望函数没有调用者,但是this却不指向window对象。有以下2种优雅的解决方案:

箭头函数

es6里面定义了箭头函数,不只是为了简化函数的写法,而且还有这么一条有用的规定:箭头函数的this不会随调用者的不同而变化,它的this永远是被定义的函数域中的this。

//不用箭头函数
let person = {
    hello: function(thing) {
        return function() {
            console.log(this + " says hello " + thing);
        }
    }
}
let helloTest = person.hello('world');
helloTest(); //输出[object Window] says hello world


//使用箭头函数
let person = {
    hello: function(thing) {
        return () => {
            console.log(this + " says hello " + thing);
        }
    }
}
let helloTest = person.hello('world');
helloTest(); //输出[object Object] says hello world

需要注意的是,需要用一个function()把箭头函数包裹起来,因为如果不这样的话,它被定义的函数域是window。

bind

用箭头函数有点麻烦,我们可以这么写一个bind函数达到效果。

let person = {
    hello: function(thing) {
        console.log(this + " says hello " + thing);
    }
}

let bind = function(func, thisValue) {
    return function() {
        return func.apply(thisValue, arguments)
    }
}

let boundHello = bind(person.hello, person);
boundHello('world'); //输出[object Object] says hello world

es5给所有Function封装了上面的bind方法,所以我们只需要这么写:

let person = {
    hello: function(thing) {
        console.log(this + " says hello " + thing);
    }
}

let boundHello = person.hello.bind(person);
boundHello('world'); //输出[object Object] says hello world

这就是bing()方法的来历0.0

《人人都是产品经理》读书笔记

概述

无意中看到了这本书《人人都是产品经理》,阿里的前产品经理苏杰写的,看了几页挺有意思的。我看这本书主要目的主要有以下几点:

  1. 了解产品经理的想法,从而能更好的和他们交流。
  2. 学习生活中的一些知识与诀窍。
  3. 看看阿里的产品经理和其它产品经理有什么不同。

我把我看这本书的心得记录下来,供以后参考,相信对其他人也有用。

-1到3岁的产品经理

1.产品经理是一类人,他的做事思路和方法可以解决很多实际的生活问题,只要你能够发现问题并描述清楚,转化为一个需求,进而转化为一个任务,争取到支持,发动一批人,将这个任务完成,并持续不断以主人翁的心态去跟踪、维护这个产物,那么你就是产品经理。

2.产品就是同时解决用户的问题和公司的问题;而解决问题其实就是满足人们的需求,这样才能产生价值。

3.对于传统行业,产品已经定型,市场已经成熟,用户也已经成熟,较难改变,所以这类行业的产品经理会偏重营销类创新;而对于互联网、软件行业这类新兴行业,三天一小变,五天一大变,产品需要推陈出新,先入为主,主导用户习惯,这就导致了产品工作的重头戏在前期,偏重研发类创新。

4.传统行业的另一个特点是,他们的产品多是为付钱的客户做的,也许搞定几个大客户,就3年不愁吃穿;但是互联网行业,面对的是互联网上的海量用户,用户多了,自然就能盈利。所以互联网行业的产品经理会更重视用户研究、数据分析等工作,需要把握用户需求,实现用户需求。

5.传统行业的产品,东西是买来的,遇到不爽的地方也能凑合着用。但是在互联网行业,如果用的不爽,能在网上很方便的找到另一个试试。所以互联网行业更重视用户体验。

6.管理的能力,其实就是在资源不足的情况下把事情做成的能力,这里的资源不足是指:

  1. 信息不足以决策。
  2. 事件不足以安排周密的计划。
  3. 人员不足以支持工作强度和难度。
  4. 资金不足以自由调配。

7.产品经理面试最在乎的是:有没有激情,是否够机灵、好学,逻辑思维是否清晰,沟通表达是否顺畅。

一个需求的奋斗史

8.产品经理怎么研究需求:用户访谈、调查问卷、可用性测试、数据分析、单项需求卡片。

9.用户跟福特要一匹更快的马,福特却给了用户一辆车。这就是产品经理存在的价值:一个是用户需求,一个是产品需求,要通过用户需求,找到用户内心真正的渴望,再转化为产品需求(需求分析)。

10.伟大的需求分析师,可以无视用户想要的东西,去探究他内心真正的渴望,再给出更好的解决方案。

11.需求的DNA检测过程:需求转化、确定基本属性、分析商业价值、初评实现难度、计算性价比。

12.需求的分类:新增功能、功能改进、体验提升、bug修复、内部需求;需求的层次:基础、扩展(期望需求)、增值(兴奋需求)。

13.做项目,终极目标是:多快好省。即范围大、时间段、品质高、资源省。

14.阿里巴巴的马云说过:少做就是多做。情愿把一半的功能做到尽可能完美也不要把全部功能都做成半吊子。所以我们应该在动手前找找有没有成本低,收效大的解决方案。

项目坎坷的一生

15.产品经理:靠想,产品经理做正确的事,其所领导的产品是否符合市场的需求,是否能给公司带来利润;项目经理:靠做,项目经理是把事情做正确,把事情做得完美,在时间、成本和资源约束的条件下完成目标。

16.产品经理最重要的是判断力和创造力,他是产生一个想法,然后“我要把它实现”;项目经理最重要的是执行力与控制力,他是接到一个任务,然后“我要把它完成”。

17.一个产品经理可能想要增加非常多的功能和特征以满足获取到的用户需求,但是项目经理却想要尽可能小的控制工作范围,以保证项目在规定时间与预算内完成。可能一个人会同时兼任产品经理和项目经理,这个时候就要在其中找到平衡点。

18.项目誓师大会,也就是项目Kick Off会议,是很有必要的。它通常只需要15分钟左右,需要传达这些信息:

  1. 项目背景。我们在哪儿,为什么做这个项目。
  2. 项目意义、目的和目标。我们去哪里?解决什么问题就算成功了。
  3. 需求、功能点概述。我们怎么去?具体使用什么方法?
  4. 项目组织架构。让项目成员互相认识,明确有什么事应该找谁。
  5. 项目计划。项目的时间点和里程碑,各个时段需要的资源。
  6. 沟通计划。约定好怎么沟通。

19.项目的基本流程。

  1. 需求确认阶段:需求评审、需求确认,设计评审,功能评审,测试评审,需求最终确认。
  2. 开发阶段:设计、设计评审、编码、单元测试。
  3. 发布阶段:发布评审、预发布、发布、线上验证。
  4. 项目小结:遇到了哪些问题,怎么解决;资源评估是否合理,如何提高准确度;根据数据监控的反馈,有什么结果;是否达到目标。

20.自己的文档规范:

  1. 商业需求文档,产品需求文档。
  2. 需求规范类:PD做什么,用户体验规范,通用原则。
  3. 需求管理类:用户调研,产品需求列表,产品信息架构。
  4. 流程管理类:日常发布流程,变更事件流程。
  5. 项目管理类:项目管理制度,项目任务书,kick off的ppt,项目组织结构,项目WBS,项目日报周报,项目发布预告与公告。
  6. 日常工作类:会议记录,个人日报周报。

21.长视者把目的当手段,短视者把手段当目的。文档只是手段,流程也只是手段,这些手段都是为了把项目做好,把产品做好,把项目做好也只是手段,是为了达到公司的商业目标等等。

22.当年的“英雄”把自己的个人经验转变成显性知识表达出来,而对于经常做的事情,就可以用流程这种形式固化、传承,后人在做这些事的时候起码不会太无助。在这点上,规范、模板的作用也类似,这就是团队的核心竞争力。

23.项目中的敏捷方法:

  1. 有计划,更要“拥抱变化”。开始订的项目计划,在一个月后可能已经面目全非了,没必要强行遵守。
  2. 迭代周期内尽量不加任务。如果在某次迭代内的任务无法完成,可以为了时间点的要求,移出一部分任务到下一个迭代。
  3. 每天问自己:昨天做了什么?今天要做什么?碰到什么问题,打算如何解决,需要什么帮助?
  4. 持续细化需求,强调测试。
  5. 不断发布,尽早交付。让需求方不断地、尽早地看到结果,并给予反馈。

24.《圣经》中说:请赐予我力量,去接受我所不能改变的;请赐予我勇气,去改变我所能改变的;并赐予我智慧,去分辨两者的不同。

25.有良知的职业杀手:在不认同项目目标的时候,只要不违背自己的价值观,就尽心尽力地完成任务。否则,要么调整自己的价值观,要么放弃这份工作。

我的产品,我的团队

26.五种用户群体:

  1. 创新者:新鲜感强,消费能力不高,忠诚度不高,需要新鲜东西不断刺激。产品刚上市的时候,主流用户是创新者。
  2. 早期追随者:观念比较新,但是需求目的性强,需要迅速能够解决其问题的产品。忠诚度一般。
  3. 早期主流用户:典型的实用主义者,心中对新产品存在期待。
  4. 晚期主流用户:对新产品心存抵触,直到老产品出现明显的劣势,才会很不情愿地使用新产品。
  5. 落伍者:最后一批用户,他们的附加值已经比较低了。

27.阿里是商业主导的,商业的强势也说明了阿里为什么不招技术很强的毕业生。

28.大家在找工作的时候必须调查清楚自己的职位在公司里是不是最受重视的,是不是强势方,这很重要。举个不是很现实的例子:如果你在特种部队式的组织里,那么可以安心地做一个特种兵;但如果你进了塔利班或基地组织,那就抓紧时间进入管理层。

29.产品设计的5个层次。这5个层次也对应互联网产品设计的五个层次:战略、范围、结构、框架、表现。

  1. 战略层:网站目标,用户需求。明确商业目标和用户需求,找准方向。
  2. 范围层:功能规格说明,内容需求。明确“做多少”。
  3. 结构层:交互设计,信息架构。考虑产品的各个部分互相之间是什么关系。
  4. 框架层:界面设计,导航设计,信息设计。这里才出现用户真正能看到的东西。
  5. 表现层:视觉设计。视觉设计和内容的优化。

30.部门协作出现问题,是因为“找不到共同的利益”。合作的基础就是有共同的利益,或物质或精神,或短期或长期。

31.常见的3种公司组织结构:

  1. 职能型组织:把相同职责的人划分到一个部门里。缺点是目标在分解到各个部门之后,很容易不一致,而且没有人对真正的客户负责。
  2. 项目型组织:把各种职责的人组成一个项目组,缺点是会浪费资源。
  3. 矩阵型组织:上面2种组织的融合。缺点是,对员工来说,一面是部门经理,另一面是产品经理,双头领导很头疼。

32.产品的版本细分:

  1. 一种是做功能区分,打细分市场。
  2. 另一种是为了促进销售,利用消费者心理,纯策略性地做出“炮灰版”。

33.纵向营销是进化,特点是渐变;水平营销是革命,特点是突变。比如说卖包子,纵向营销是把包子做成大号、中号、小号,另外做32种不同的馅。水平营销是“精品包子”、“神秘的东方膳食”、“牛郎织女吃包子”、“中药包子”、“豆皮包子”、“西式包子”等。

34.两种开发工程师:

  1. 一类是技术痴迷者。在项目碰到技术难题的时候,他们是攻坚的主力。有极少数热衷于技术的人,缺乏必要的责任心和使命感。
  2. 另一类是实用主义者。公司考核他们什么,他们就做好什么,尽量少做事,做简单的事。

35.怎么和开发工程师沟通:他们最看重的是“流程”,他们喜欢被规则管理而不是被人管理。

36.人性的弱点决定了在争论的过程中每个人都希望自己得到认同,而这点往往导致思路的变形,不再考虑产品怎么做更好,而是去想如何说服对方,并且,经常有同学会把对人的反感转移到对此人观点的反对上,这很可怕。

37.菜鸟成长的阶段:

  1. 最初,菜鸟啥也不懂,蒙着头做事,眼巴巴地盼着老板光临。好汇报一下工作,而往往是当老板主动找你问事情的时候就是他开始担心的时候,这时期的菜鸟很容易把事情做偏,吃力不讨好。
  2. 渐渐地,菜鸟觉得这样太辛苦了,于是每走一步就问老板“我碰到一个问题,应该怎么做”,这叫老板做问答题,老板每每给出答案,菜鸟再也不会做无用功了,做起事情也踏实多了,但是老板细腻嘀咕起来,太烦了,终于在某次菜鸟又来问问题的时候,冲了他一句:这些问题你怎么不自己先想想,你什么信息都不给我,我怎么告诉你答案 ·······
  3. 菜鸟继续体会,发现让老板做问答题,老板是很累的,需要让老板做选择题,于是,每次有问题的时候,他都会自己先收集很多的背景资料,然后选出几种可行的解决方案,再拿着所有的这些资料给老板做决定。现在好多了,老板开始有点轻松了。并且在这个过程中,菜鸟发现有些问题在自己寻找解决方案的过程中,已经被自己解决了,大喜。
  4. 又是很久过去了,突然有一天,菜鸟发现一件有意思的事情:那就是还可以更进一步,让老板做判断题,于是菜鸟在某次呈现给老板几条解决方案以后,又会加上自己的选择:我觉得A方案是最好的,因为什么什么·······当然,菜鸟毕竟是踩奶哦,因为各种原因,经常与老板的判断不同,但菜鸟在疑惑中又学会和老板讨论,渐渐地学到了一些老板的判断方法。
  5. 白驹过隙,慢慢地老板发现,自己做的判断题答案都是“勾”了,似乎每次菜鸟的汇报自己就是听听,恩恩两声就没什么事情了,但是菜鸟仍然在及时地问,不停地汇报,也越来越学会和老板开条件,要资源,当然目的是为了把事情做得更好,这就是:事情我做,黑锅你背,各司其职。这是职位的思维。老板“嗯”了一声,就意味着这件事菜鸟做起来是经过授权的,除了问题是要老板承担责任的。对于菜鸟来说,稚嫩的肩膀经受不起,所以要找人帮忙,是出于对自己的保护。
  6. 再往后,事情如果向好的方向发展,那就是老板不用再帮你背锅了,你完全可以自己决策了,爽吗?其实,只是新的开始,可以自己背黑锅以后,必然碰到更大的黑锅,还是要让老板背,也许是更大的老板,只不过在这个过程中,在自己的肩膀得到了锻炼。

38.公司最大的财富是人、是团队,只要人在团队在,哪怕现在的产品没有了,也能重新杀出一条血路。所以,不能只是产品好,需要达到“大家好才是真的好”这个目标。

39.管理和领导是不同的。管理者靠的是权力,领导者靠的是自身的个人魅力。

40.管理岗位的优势在于:拥有话语权,能够获取信息,争取资源;劣势在于:有很多行政工作,并且容易脱离群众。

41.让员工更开心的方法:

  1. 大中之小不如小中之大。
  2. 有用的不如无用的。
  3. 需要的不如想要的。
  4. 有选择不如无选择。
  5. 小奖不如没奖。
  6. 晚说不如早说。
  7. 一次送不如两次送。
  8. 公开不如不公开。
  9. 涨工资不如发奖金。

42.要记住,奖励或送礼的目的并不是真正给对方最大的效用,而是要让对方开心,并且感激和记住你。(这也是与人交往的原则)

别让灵魂跟不上脚步

43.不论是个人还是公司,一定都有做不完的事,那么,必然有一些事情要被放弃,另一些事情要优先完成。所以我们碰到的问题就是“应该做什么?”这个问题需要用价值观来回答。

44.产品的灵魂,企业的灵魂是什么?探到最深处,是由价值观决定的,而企业的价值观就是:企业决策者对企业性质、目标、经营方式的取向做出的选择,是员工所接受的共同观念,是长期积淀的产物。可以说,无论对个人还是企业,如果按价值观做事,无论成败,都会很安心。

45.有了价值观作为企业做事的最基本指导原则之后,我们就需要思考公司或产品的使命和愿景。使命是指“我们为什么存在,要做什么事情”。愿景就是“我们希望成为什么”。

46.可行性分析三步曲:我们在哪儿;我们去哪儿;我们怎么去。

47.在战略决定好了,可行性分析好了之后,我们就需要一步步实现我们的愿景了。而具体怎么走,则要依赖大大小小的计划。这个时候就需要做路标,想清楚有什么事情可以提前准备,什么时候做什么来一步步实现目标。

48.我们在从起点通往目的地的路上大踏步前进,除了“低头走路”,还要“抬头看天”,这就是所谓的里程碑、检查点,需要回想一下前一段路上的得失,修正方向,以便下一段路程走得更好。

49.一个简单的实用**集中原则:所有人提供意见,少数人讨论,一个人拍板。即所谓的“高层定向,中层分解,基层执行”。

50.产品设计的好坏是假的,用户体验的好坏也是假的,只有商业利益的多少才是真的。

产品经理的自我修养

51.热爱生活,可以帮助你把“要做的事”变成“想做的事”;学会思考,不断提升自己,可以把“要做的事”变成“能做的事”;寻找理想,就是把“要做的事”、“能做的事”都整合成“想做的事”。我们不断努力扩大“想做、要做、能做”三者的重合部分,这就是你的理想,也是你的核心竞争力,别人学不来的。

52.“教”是为了“不教”,是为了激发其自我反思、自我管理的能力,当开启一个人的心智之后,他就可以自我发展,成为一个独立的人。

53.有些时候,解决问题最重要,而是否“独立”在更多场景下其实并不需要,也不可能,充分调动团队、用最有效的办法解决问题才是本质。

54.多年的教育让我们误以为所有问题都是有标准答案的,可实际上很多问题连参考答案都没有。这是因为,考试题往往都是对现实情况的简化,只有这样才说的清楚,才便于将答案与“分数”这个KPI映射,很多背景条件都不用我们去考虑,而真实的问题往往是背景复杂的。

55.所以,今后想问问题的时候,不妨自己先想想,这个问题有明确答案么?如果有的话,那不妨去百度,不用问人;如果没有的话,那更是不要去问别人的答案,而是用交流的心态和别人讨论方法。

56.世界对每个人来说本来是一片黑暗,你对世界认识的发展,就好比在一片黑暗的空间中,去不同的地方点亮一盏盏知识的小灯,然后看到一些情况并且猜测着还看不清的情况。当亮的灯越来越多的时候,就可以不断修正对这个世界的认识。每个人都会经历着这种“认识中的世界越来越复杂”的过程,期间可说快乐,也可说痛苦。但少数人会突破一个拐点,开始“发现世界越来越简单”,我粗浅的认为,突破拐点的一种表现就是有一些关键的灯被点亮了,渐渐的发现黑暗中的世界原本是一个整体,有着根本的道理,很多事情底层的规则都是相同的,从而我们会觉得做起事来反而越来越轻松。一旦到了这个阶段,我们就会忍不住的拼命点亮更多的小灯,试图看到这个世界的全貌,这其实是很痛苦的,因为你发现了方向和终点,但同时也知道必然走不到那里,也知道任何人都走不到那里,也许,真正强悍的人会把这个过程视为一种快乐。

57.沟通不是为了说服,而是为了更好地认识世界。每次沟通都是一个大家互相帮助,共同提高对世界的认识的好机会。

58.“土老板”破冰必杀技:我们的目的是尽快找出对方感兴趣的、熟悉的、擅长的、自己也懂一点的话题,从而成功破冰,尽快进入需求采集阶段。

59.春晚的策略是:跟风,渐变,需要照顾方方面面,不求有功但求无过。所以对待春晚,不妨少一些抱怨,多一些理解。

60.当有人骂春晚的时候,产品经理的思路应该像对待一个提需求的用户: 骂的人是不是典型用户?他的观点能代表多少人?他的影响力多大? 他是不是只是 “嗓门大"的用户? 他说的是不是解决方案?他的本质需求是什么? 把他的需求加人需求列表应该标什么级别?什么属性?想完了就会发现,事情并不是很糟,于是嫣然一笑发现定位没有问题,继续这么干。另一方面,看了又骂的人,首先不要急,春晚不可能突变,而且,一个本来就不是为你做的产品,你掺和个什么劲啊?

61.解决问题的通用思路:为了什么?做什么事,解决什么人的什么问题?何时做?谁来做?效果如何?

前端工程资源小优化

概述

最近给vue项目优化了一下资源打包,有一些心得,记录下来,供以后开发时参考,相信对其他人也有用。项目使用vue-cli搭建的,所以优化方向是基于vue-cli3的。

css按需加载

项目使用的是element ui库,我们使用了element的自定义主题。但是我看见打包的时候打包了2份element ui库的css:app.css打包了一份,vendor.css也打包了一份。严格说来,element-ui库的样式应该打包进第三方css里面也就是vendor.css里面。

查找原因,发现有如下配置:

// babel.config.js
module.exports = {
  presets: [
    '@vue/app',
  ],
  plugins: [
    [
      'component',
      {
        libraryName: 'element-ui',
        styleLibraryName: 'theme-chalk',
      },
    ],
  ],
};

// main.js
import '../../theme/index.css';

其中babel.config.js里面的配置是说,element-ui使用的style是element-ui库里面theme-chalk文件夹里面的样式;main.js里面的配置是说,导入自定义主题文件夹theme里面的样式。所以前者把element-ui库里面的css打包进了vendor.css,后者把自定义主题的css打包进了app.css。

最后考虑到我们的需要是一定要使用自定义主题,所以做了如下修改:

// babel.config.js
module.exports = {
  presets: [
    '@vue/app',
  ],
  plugins: [
    [
      'component',
      {
        libraryName: 'element-ui',
        styleLibraryName: '~theme',
      },
    ],
  ],
};

意思是,element ui库使用自定义主题theme文件夹里面的样式。所以不会再把element ui库里面的css打包进vendor.css里面了。

虽然还是有一点瑕疵,就是这样会使app.css很大,导致每次只要修改了一点点css,都要把element ui的css重新打包一次。但这也没办法,需要是一定要自定义主题。

element-ui组件按需加载

上面我们在babel.config.js里面已经配置了element ui组件的按需加载,但是在实际引入组件的时候我们引入了多余的组件,特别是time-picker和date-picker组件,占了很大体积,于是我检查了一遍,把多余的组件删除了。

这里有必要提一下的是,如果你使用的是vue-cli3,那么在package.json里面加入如下指令:

"report": "vue-cli-service build --report"

然后运行npm run report,就可以在dist文件夹里面生成一个report.html,里面非常直观的显示出打包的js的组成成分和大小,对减少打包体积很有帮助。

moment.js

通过上面的指令npm run report,我们可以看到,moment.js的打包体积非常大,都快赶上element ui整个库了。于是我想办法减少moment.js的打包体积。

通过You-Dont-Need-Momentjs可以看到好几个moment.js的替代库,但是由于它们都没有UTC的相关功能,而我们项目非常依赖moment.js的UTC功能,所以我们项目并不能采用这些替代库。

最后只能在moment.js上想办法了,只能通过删减它里面的国际化内容来减少它的打包体积了。webpack官方也建议在使用moment.js的时候用ignorePlugin删除它的国际化部分

具体方法如下,只需要在vue.config.js里面加入下面的配置就行了:

const webpack = require('webpack');

module.exports = {
  chainWebpack: config => {
    config
      .plugin('ignorePlugin')
      .use(webpack.IgnorePlugin, [{
        resourceRegExp: /^\.\/locale$/,
        contextRegExp: /moment$/,
      }]);
  },
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.