Jasper Ji

平常心

起因

早前为了了解如净禅师,整理过《如净和尚语录》的简体拼音版,当然有段时间也试图寻找更多关于禅师的资料,不过都不成体系。后来得益于CBETA Online系统提供的线上众多禅师的语录资料,才有以下的研究。

何时入住天童寺

如净禅师是什么时间入住天童寺的呢?单从如净禅师在天童寺上堂的记录来看不好推断,后来发现如净禅师离开净慈寺后石田法薰禅师作为继任,如净禅师住天童寺上堂语录中有提到“浙翁遗书至”,而石田法薰净慈寺上堂语录中也有“浙翁遗书至”。

浙翁遗书

浙翁遗书至。上堂。八月十八钱塘潮。浙翁声价泼天高。尽教四海弄潮手。彻底穷渊辊一遭。重拣择不辞劳。要透龙门继凤毛。忽然收卷还源去。万古曹溪风怒号。

出自《如净和尚语录·明州天童景德寺语录》

浙翁遗书至。上堂。千五百人善知识。不念吾宗正岑寂。五峰趯倒浪翻空。大地山河俱失色。金风体露。叶落归根。只堪惆怅不堪陈。

出自《石田法薰禅师语录·临安府净慈报恩光孝禅寺语录》

因为之前浙翁禅师曾经在天童寺主持过,如净禅师语录中小佛事章节有一篇《为浙翁入祖堂》。

为浙翁入祖堂

昔从太白凌霄去。今自凌霄太白来。不堕去来生死路。

展真云:看堂堂面目笑咍咍。且道,笑向阿谁?

以真指祖云:大家元是主中主,惯入驴胎与马胎。

出自《如净和尚语录·明州天童景德寺语录》

石田法薰在入住净慈寺之前是在建康府太平兴国禅寺,在《建康府太平兴国禅寺语录》中明确的写到“师于嘉定十六年四月二日入院”,发现石田法薰在太平兴国禅寺的上堂语录,最后有明确时间的是嘉定十七年端午,再一转就是已经在净慈寺了,我一开始根据如净禅师关于浙翁遗书至,明确写道是八月份,所以一开始推断石田法薰禅师是嘉定十七年入住的净慈寺,但最后根据石田禅师语录中行状。

庙堂精选择。乃以师补处。宝庆元。有 旨迁南山净慈。端平二。复有 旨。迁北山灵隐。两山居各十年。

出自《石田法薰禅师语录·行状》

确定了石田法薰禅师入住净慈寺的时间后,再根据石田法薰在净慈寺的语录判断收到浙翁遗书的时间是宝庆元年的八月份。再看如净禅师收到浙翁遗书的时间,基本在天童寺语录的后面,根据天童寺上堂语录推断,最后推断如净禅师是在嘉定十七年下半年去的天童寺。

PS:浙翁禅师,全称浙翁如琰(yǎn),确实圆寂于宝庆元年,所以如净禅师是在宝庆元年的前一年,即嘉定十七年下半年入天童基本无误。

派和尚遗书至。上堂。万派朝宗一派收。扬清激浊几经秋。忽然到底都干却。露柱灯笼笑不休。且道。笑箇什么。下座同诣灵凡。羞法供养。

出自《如净和尚语录·再住净慈禅寺语录》

原来没有注意,实际这个派和尚就是无际了派了,正是因为无际了派禅师推荐,如净禅师才入驻天童寺。

何时圆寂

发现时住阿育王寺的无准师范收到了如净禅师的遗书。

前住天童净和尚遗书至。上堂。太白峰前收阵脚。鉴湖归唱村田乐。无端调转入新丰。谁知错处非常错。直得洞水逆流。乳峰倒卓。石女攒眉。木人泪落。此曲如今谁共闻。越山无际天无垠。

出自《无准师范禅师语录·住庆元府阿育王山广利禅寺语录》

这个记录在《无准师范禅师语录·住庆元府阿育王山广利禅寺语录》的前段,是刚入住没有多久就收到的,所以我一直觉得只要知道无准师范是什么时间入住的阿育王寺就可以推出如净禅师圆寂的年份了,后来在《径山无准和尚入内引对升座语录》找到了相关的线索。

绍定六年七月十五日。

皇帝御修政殿引见师领本寺僧众山寿毕。提举都知太尉张延庆引见祝皇帝寿。

师捧香云。根同天地。秀发山川。薰霭宝炉。瑞腾沙界。恭为祝延皇帝陛下圣躳万岁。万岁。万万岁。指七金山为寿山。巍巍不动。以四大海为福海。渺渺无穷。长居北极之尊。永作中天之主。金枝玉叶。育秀腾芳。八表归仁。万邦入贡。

都知太尉云。请长老敷演。

乃奏云(臣)僧(师范)。一介庸衲。生于西蜀。浪游湖海。今四十年。于道无闻。每切自愧。夙生何幸。两蒙
睿旨。扫洒庆元育王。而至双径。自去年八月领职。与四方衲子朝夕禅诵。仰报 圣恩。

出自《径山无准和尚入内引对升座语录》

这个是无准师范在入住径山寺后的第二年受到了皇家的召见,召见时间是绍定六年七月十五,而且提到他是去年八月到的径山寺,也就是绍定五年八月,大部分的语录里面都会记录比如中秋,端午之类的节日。根据无准师范在阿育王山广利禅寺的语录反复推断发现,收到如净禅师遗书是宝庆三年,时间应该是宝庆三年七月十四到九月九日之间,大概可以推断如净禅师应该是这个时段圆寂的。

语录

现存的两本语录《如净和尚语录》、《天童山景德寺如净禅师续语录》。《如净和尚语录》应该是刊行版本,在后世资料都引用的是这个版本里面的内容。《天童山景德寺如净禅师续语录》有人认为是后人伪造,所以暂且不论。这里主要研究下语录发行的几个时间点,按时间顺序排序。

狮子吼无畏说。百兽闻之皆脑裂。天衣举似箇中人。迈古超今离途辙。

绍定戊子中秋 天衣住山比丘文蔚谨跋

绍定戊子即绍定元年,也就是宝庆三年的下一年,因为宝庆只有三年。

净禅师得无师句。用逸格机。娄至德已前。青叶髻之后。突出无面目底。糙暴生狞。通身是眼。要看是录。予保。渠未梦见此老脚跟下汗臭气在。

绍定改元开炉日 灵隐高原祖泉敬跋

绍定改元即绍定元年,根据宋代的《禅苑清规》记载开炉日为农历十月一日,闭炉为农历二月一日,大概就是类似现代的烧暖气吧,所以就是绍定元年十月一日的时候,灵隐寺的高原祖泉题写了跋。

绍定二禩岁在己丑桐柏散吏吕潇 敬书

这个是如净禅师语录序的题款,实际即绍定二年,虽然没有写几月几日,但应该是在同年的六月初伏之前。

岁次己丑六月初伏日。小师广宗募刻板。临安府灵隐景德禅寺住持祖泉挍勘焉。

这个是绍定二年,也是己丑年,六月初伏日刻板的。

灵隐高原

语录作跋的是高原禅师,虽不清楚禅师与如净禅师的关系,但也可见一般。实际上这确实是一位了不起的禅师,在当时也是很有威望的。

高原禅师与无准禅师的关系又非同一般,曾经住梨洲的时候,无准禅师也是一块同行的。后来高原禅师圆寂后无准禅师也收到了讣音,可见关系之一般。

灵隐高原和尚讣音至。上堂。来无所从。南高峰。北高峰。去无所至。东㵎水。西㵎水。幻泡忽灭。证得乌龟成白鳖。清风未已。须信高原元不死。既不死。且道在什么处。拈起拄杖云。见么。见么。卓一下。云。认着依前不相似。

出自《无准师范禅师语录·住庆元府阿育王山广利禅寺语录》

《率庵梵琮禅师语录》中有一个偈语,是送高原从台州去灵隐的。

寄台州瑞岩高原禅师住灵隐

水出在高原。源深到冷泉。饮者秃却舌。嗅者鼻孔穿。口鼻两俱丧。妙用绝正偏。侧耳与招手。听猿同呼猿。藤萝影里石磊磊。双㵎合流波涟涟。南来北来脚下过。欲知冷暖待驴年。

如净禅师在建康府清凉寺后的下一个住锡的就是台州瑞岩,推测应该是接的高原禅师位置,再大胆的猜想一下,或许就是高原禅师的推荐了。

《北磵居简禅师语录·常州顯慶禪寺語錄》写到收到了高原禅师的訃音,可见关系一般,而这位禅师晚年入住了净慈寺。

高原和尚讣至。上堂。二月十一。人从北山来。报高原死也。似者般僧。只患多不患少。哑。更覔一箇。如星中月。砂里金。簸土扬尘无处寻。

《西岩了慧禅师语录·行状》中写道西岩了慧禅师早年跟随过高原禅师。

十九薙发。灯授以般舟念佛三昧。非其志也。辤往成都讲席。习性宗经论。俄叹曰。义学岂究竟法哉。染指足矣。去谒坏庵照于昭觉。一见心许法器。趣其南询。乃束包出三峡。由湖湘而至江浙。见浙翁琰于径山。闻高原泉孤硬径直。往依之。同枯寂甘如饴。泉迁台之瑞岩。令师与俱。泉问。山河大地。是有是无。拟开口。即喝出。以偈呈。即曰没交涉。偶侍次。令书龙门三自省。白杨示众语。泉阅之笑曰。写字与做言何尽得。争奈没交涉何。师愤悱莫伸。泉曰。吾方便娄矣。汝自不顾。盖缘法不在此。其往见雪窦乎。时主雪窦席者。佛鉴无准范也。

“孤硬径直”或许就是对这位禅师最好的表述吧,不知道当时有没有语录还是后来遗失的缘故,只有个别零散的一些语录夹杂在其他资料里面,而且在如净禅师圆寂后绍定二年二月份的时候高原禅师也圆寂了,而绍定元年的十月一日的时候禅师才为《如净和尚语录》作了跋。

生平

解决了入住天童寺的时间以及圆寂的时间,就可以大体推断下禅师的生平了。

师于嘉定三年十月初五日。于华藏褒忠禅寺。受请入寺。

出自《如净和尚语录·住建康府清凉寺语录》

从嘉定三年到宝庆三年,出世主持丛林十八年时间。

师六坐道场未禀承众或是请师云待我涅槃堂里拈出果临终拈香云

如净行脚四十余年。首到乳峰。失脚堕于陷穽。此香今不免。拈出钝置我前住雪窦足庵大和尚。

并书辞世颂云。

六十六年罪犯。弥天打箇𨁝跳。活陷黄泉。咦。从来生死不相干。

出自《如净和尚语录·偈颂》

根据禅师圆寂时六十六岁,即宝庆三年的时候,根据古人过年即增一岁,推断禅师出生于绍兴三十二年即1162年,四十九岁的时候入住清凉寺,开始了主持生涯。但不太确定如净禅师在华藏褒忠禅寺具体是什么职务,应该不是主持了,搜索了下华藏褒忠禅寺基本没有相关的资料,大致推测是现在无锡华藏寺。

如净禅师大概什么时间跟随的雪窦足庵呢?宋人楼钥的《攻愧集》有一篇《雪窦足庵禅师塔铭》,根据塔铭的记载足庵禅师是绍熙二年圆寂的,在雪窦山八年时间,即淳熙十一年至绍熙二年,淳熙十一年的时候如净禅师是二十三岁,到绍熙二年如净禅师是三十岁,所以如净禅师跟随足庵禅师的大致时间段也在这个范围。大致推算从三十岁开始到四十九岁,十八年间应该是在各大丛林参学时间。

通过研究当时禅师的语录,发现围绕着当时南宋首都临安的这些有名的寺院大都是临济宗在主持,而且各个主持之间还有着密切的联系,比如无准师范和石田法薰,都是破庵的门人,无准师范系在后面的日子基本上占据丛林大部。如净禅师在离开足庵禅师后的十八年间,在各大丛林的参学,也让他结识如临济宗的如松原、高原等禅师,这是这段时间的参学,使得禅师的禅风有了兼容五家之风,禅师生前一直不说自己的师承,不太清楚具体的原因,或许也有难言之隐,毕竟在当时的五山中都是临济宗的。另外或许在参学了不同宗派后,禅师本身并不认可宗派的做法,所谓自成一体,八面受敌。

颀然豪爽。丛林号曰淨长。礼真歇塔偈云。歇尽真空透活机。儿孙相继命如丝。而今倒指空肠断。杜宇血啼花上枝。示众云。心念纷飞。如何措手。赵州狗子佛性无。只今无字铁扫帚。扫处纷飞多。纷飞多处扫。转扫转多。扫不得处𢬵命扫。昼夜竪起嵴梁。勇勐切莫放倒。忽然扫破太虗空。千差尽豁通。宗趣可知矣。有问。瑞世嗣谁。曰。如淨。问。道号谓何。曰。淨长。后于太白山感疾。退席。下涅槃堂。始大哭为鑑足庵烧香。入寂时。侍者告以法堂宝盖镜堕于座上。曰。镜枯禅至矣。如其言。

《枯崖漫录·庆元府天童如净禅师》

临终大哭,这在一般禅师中很少见,作为足庵门下,足庵的老师大休宗珏也曾入住过天童寺,而大休宗珏的师父真歇清了宏智正觉禅师同出丹霞子淳门下,所以对于当时的曹洞宗而言天童寺有着不一样的情节,再加上禅师“颀然豪爽”的性格,本可以在天童有所作为,可惜因病而止。

如净禅师圆寂后的天童寺主持

如净禅师之后是的天童寺的主持,但是都不太准确,但都是临济宗的,以下是后面几年的主持:

枯禅镜,这个是根据《枯崖漫录》关于如净禅师的记载得出的。

痴绝道冲,嘉熈三年己亥十月初三日入驻,於淳祐甲辰即淳祐四年七月十四日去灵隐禅寺入驻。四年多时间。

淳祐五年至淳祐十二年,七年多时间主持未知。

西岩了慧,淳佑十二年十一月十五日入驻,居五年。

其他资料中的如净禅师

南宋

目前我能找到的对如净禅师墨迹的题跋,这两位禅师都是南宋时期的也,离如净禅师圆寂后的时间不是很长,那会还是有禅师的墨迹留世,当然现在我们已经无法看到这些墨迹了。

《无文道灿禅师语录·䟦天童净和尚寿无量墨迹》,这位禅师是南宋宝佑之后,可见那会如净禅师禅师的墨迹还是有留世的。

䟦天童净和尚寿无量墨迹

无量拳头能杀而不能活。天童拳头能活而不能杀。闲云亲中二老之毒。山河大地草木丛林至今忍痛未已也。虗空霹未尝不殷然天地间。雅维那于展卷处忽然轰入髑髅。政恐不及掩耳。

实际后来发现无文道灿禅师还为如净禅师起棺

南庵主起棺

见了天童便跺跟。佛来有口不能吞。莓苔绿遍门前路。坐看春风四十年。无禅道可论。无佛法可传。拾薪樵子无可寻之迹。衔花飞鸟无可见之缘。折脚铛中烂煑乾坤清气。长柄杓内舀干沧海根源。了生死去来之如幻。观涅槃寂灭之现前。回首鉴湖青山未老。笑携藜杖白首言蔙。这箇又是某人可见之踪迹。设若放阔步于藕丝孔中。入正定于微尘影里。诸人又向什处与此老相见。阎浮树在海南边。

禅师与如净禅师曾有交集,时间在如净禅师住健康府清凉寺。

璨禅客至上堂。金刚宝剑入红炉,煆出杨岐三脚驴。

出自《如净和尚语录·住建康府清凉寺语录》

《希叟绍昙禅师广录·䟦天童净和尚墨迹。诸老䟦后》,这位禅师是南宋淳佑年间人。

䟦天童净和尚墨迹。诸老䟦后

太白死句中有活句。诸老活句中有死句。死活向上有事在。拟议寻思。吴元济不待夜入蔡州城。已被擒捉了也。具透关眼者。切忌扫雪求迹。年月日。

《虚堂和尚语录·行状》可以得知,禅师当时有去过净慈,当时正是如净禅师主持。

道过金山。掩室和尚。一见甚器重。

这里提到虚堂禅师有见过掩室和尚,而如净禅师在首次入住净慈寺语录中有记录到这位禅师。

谢掩室和尚。上堂。掩室摩竭国。老胡豁开顶门。杜口毗耶城。净名败缺话柄。提上古两端公案。发今朝一段威光。所以宾主历然。江湖有在。还知么。不是诗人不献诗。春风吹作鹧鸪词。

出自《如净和尚语录·临安府净慈禅寺语录》

这位掩室和尚即掩室善开禅师,松源崇岳禅师的法嗣。如净禅师也曾在松源崇岳处参学。掩室和尚当时在金山,所以再联系到虚堂禅师行状中的“净和尚”应该就是当时的如净禅师。

由是回浙到净慈。见净和尚。净问云。尔还知所生父母通身红烂。在荆棘林中么。师云。好事不在匆忙。净随后打一拳。师展两手云。且缓缓。

出自《虚堂和尚语录》

元代

月江正印是元代一位禅师,曾经在元顺帝元统元年入住阿育王寺,而且归在佛祖赞章节中,可见对于禅师的认可。

天童净和尚

两头白牯眉毛竖。三面狸奴鼻孔凹。一只皮靴能剔脱。月明金凤宿龙巢。

出自《月江正印禅师语录·佛祖赞·天童净和尚》

明清以后

如净禅师虽然也曾两次入住净慈寺,但是清代编撰的《净慈寺志》却没有关于如净禅师的记录,同样是清代的《天童寺志》直接把禅师归入到元代。这些都说明元代之后,如净禅师逐渐被人们遗忘,或许当时的资料也是不全。

如净与道元

按道元的记载他是宝庆元年五月一日见的如净禅师,这时如净禅师在天童寺已经半年多了,到宝庆三年如净禅师圆寂,实际上道元禅师跟如净禅师学法也就两年时间。看起来时间不长,但或许在参学的过程中道元禅师其实已有自己的答案,只是需要一个验证而已,刚好在对的时间遇到了如净禅师。

只管打坐

道元创建日本的曹洞宗,提倡直观打坐,不参话头,但如净禅师语录看不出如净禅师有这样的观点,实际上反而可以看出当时也是参公案的。

上堂。心念分飞。如何措手。赵州狗子佛性无。只箇无字铁扫帚。扫处纷飞多。纷飞多处扫。转扫转多。扫不得处拼命扫。昼夜竖起脊梁。勇猛切莫放倒。忽然扫破太虚空。万别千差尽豁通。

出自《如净和尚语录·明州天童景德寺语录》

再看元代智彻禅师《禅宗决疑集·禅林静虑门》一节。

此举丛林纲纪坐禅寂静一节。古来佛法兴隆丛林茂盛。天龙协佑施主归崇。受用现成常住丰厚。处处安禅着众。人人慕道精修。或三五百之多僧。或一二千之众士。东西两序执事营为。内外一如铺心若地。箇箇如因识果。人人见道明心。三德六味总无亏。四事七珍皆具足。所以僧堂中学般若菩萨。十指不点水。百事不干怀。粥饭之余专心在道。上根利器者。不离单位坐究一乘。昼夜惺惺端持正观。后来各人有大发明成大法器收因结果。向丛林中为大宗匠。开大炉鞴煆炼学人。做工夫处。先举上床一种。威仪事在精诚。须要跏趺端坐。眼端鼻鼻端脐。牙关紧咬拳头紧捏。待喘息已定。举箇话头。僧问赵州。狗子还有佛性也无。州云无。不用动口动舌。默默参究以悟为期。此是丛林中坐禅仪式样子。众所共知。

打坐时,参赵州狗子佛性无这一公案,是当时禅门普遍的一种方式,所以如净禅师也并没有出其左右。

但是在北宋的《禅苑清规·坐禅仪》中,并没有参公案的方式,所以道元更多的应该是吸收这个比较多一点。

尽学般若菩萨。先当起大悲心。发弘誓愿。精修三昧。誓度众生。不为一身独求解脱尔。乃放舍诸像。休息万事。身心一如。动静无间。量其饮食不多不少。调其睡眠不节不恣。欲坐禅时。于闲静处厚敷坐物。宽系衣带。令威仪齐整。然后结跏趺坐。先以右足安左䏶上。左足安右䏶上。或半跏趺坐亦可。但以左足压右足而已。次以右手安左手上。左掌安右掌上。以两手大拇指面相拄。徐徐举身前欠。复左右摇振。乃正身端坐。不得左倾右侧前躬后仰。令腰脊头项骨节相拄状如浮屠。又不得耸身太过。令人气急不安。要令耳与肩对。鼻与脐对。舌拄上腭唇齿相着。目须微开免致昏睡。若得禅定。其力最胜。古有习定高僧坐常开目。向法云圆通禅师亦诃人闭目坐禅。以谓黑山鬼窟。盖有深旨。达者知焉。身相既定。气息既调。然后宽放脐腹。一切善恶都莫思量。念起即觉。觉之即失。久久忘缘。自成一片。此坐禅之要术也。

如净禅师圆寂于天童寺,记载塔也在那边,但是时间流逝,很多禅师的塔都已经无考,如净禅师塔也是。日本的《贞和集》,是一本收录国内禅师言语的合集,里面礼塔章节中有楫翁者,曾礼如净禅师塔的记录。

净和尚塔

楫翁

杜鹃啼血绿阴交,三远萝龛恨转淆。紫癜宸台绝车迹,月明金凤宿龙巢。

《贞和集·卷一·礼塔》

楫翁者,并不见于现存的资料中,除此之外没有看到其他人礼塔的记录存世。如今杭州过的净慈寺后有如净禅师塔,但是应该是后来建设的纪念塔,而且我查过嘉庆版的《净慈寺志》并未有如净禅师塔的记录,甚至在主持中都没有把如净禅师列入,虽然禅师两度主持净慈寺,但不清楚为什么没有记载,所以禅师塔不可能在净慈寺,况且现在的净慈寺塔没有几个留下来的,所以寺内的如净禅师塔,不过是出于宣传需要的纪念塔,但是并没有表明,这个多少会误导大家。

因为之前看过《启示录:打造用户喜爱的产品》,看到这本时,我当时想着是否要入手,看了下目录似乎跟《启示录》不一样,但当时并入手,后来看一些豆瓣的评价,与前作相比评分不高,主要是集中在章节多,内容不够深入,所以更不想入手了。最近的一些事让意识到产品的重要性,于是在京东的试读上,看了点开头,提到了坎贝尔这个人,这个其实硅谷很有名的被誉为硅谷教练的人物,之前在《乔布斯传》中有,但我没有想到坎贝尔对于谷歌、脸书这样的公司也是影响,这个是后话,总之我入手《启示录2:打造优秀的产品团队》这本书,不到300页的,大部分在上班路上的地铁上看到,最后这点在家看完了。

内容

先说说这个书名吧,《启示录:打造用户喜爱的产品》,原书名是《Empowered: Ordinary People, Extraordinary Products》,如果直译的应该是《赋能:普通人,非凡产品》,但可能是为了凑前作《启示录》的热度,起了这么一个名字。

书中倡导自主型团队,我感觉的这是一种过于理想的团队,或许这应该是创业团队的理想状态。大家志同道合,相互贡献自己的知识,为一个共同的产品愿景努力。有书中的团队,也让想到过去或者现在所在团队的一些问题。

总结

面向老板型团队

几年前我所在的一个公司就是这样现状,老板很有钱,决定超着互联网的方向玩一把,当时正值移动应用兴起的2014年,大家都在做APP,当时的开发团队其实比较全,客户端有iOS、安卓,服务端、前端、测试。我当时负责iOS,安卓的人当时比我们iOS多些。现在回想起来,那会技术团队人员配置最全,设计团队只是按项目配的人并没有设计负责人,所有人向当时的一位类似项目经理的人汇报。现在回想起来,当时是没有产品经理的,如果真的要找一位,那可能就是并不是常来但总是不断提需求的老板吧。后来的一些项目也设置过产品的岗位,但这名产品是项目经理转过来的,并不专业,完全是他们设计他们的,我们拿到了就得开发,而我们觉得这个功能有问题,那会总感觉我们想把产品做好,但总是做不好的怪圈。

这样的现象,我把称为面向老板型团队,当时没有产品经理是一个最大问题,老板自己想着如何开发产品,然后将这种需求直接给开发团队,并没有转换成合理的产品需求,如果老板比较强势的话,即使大家觉得有问题,在一次次沟通无效后,最终在变成了妥协,而后的团队的一些人就转变成面向老板的倾向,不再关心产品本身,最终这样的团队注定成功不了,事实也是这样。

面向业务型团队

后来我去了一家传统公司,当时招技术,本想着技术可以对这个业务有一定的赋能。但慢慢的发现陷入了书里提到的功能性团队的境地,而我更愿意称为面向业务型团队。传统公司的问题可能更严重,尤其是中小型的,这类企业的大都是通过贷款来做业务,所以每一分钱都需要有产出,所以这类公司很难在产品上有预先的投入,所以我们技术这边往往是平时不太忙,一旦有新的业务来了,我们就得立马开发业务相对应的功能,这几乎注定每次都是草草开始,只能先做一些功能让业务先用。后来我渐渐发现除了技术团队的投入不足外,我们最大问题就是没有产品,没有产品愿景,有的只是不停变化的业务。

无法启动问题

安装的教程主要参考官方文档,主要记录下安装遇到的问题,首先因为域名没有下来,使用的是IP,另外端口也是自定义的。

sudo docker run --detach \
  --publish 47.xxx.xxx.xx:8929:80 \
  --name gitlab \
  --restart always \
  --volume $GITLAB_HOME/config:/etc/gitlab \
  --volume $GITLAB_HOME/logs:/var/log/gitlab \
  --volume $GITLAB_HOME/data:/var/opt/gitlab \
  --shm-size 256m \
  gitlab/gitlab-ce:15.1.2-ce.0

这个命令会提示bind: cannot assign requested address,所以就只改端口。

sudo docker run --detach \
  --publish 8929:80 \
  --name gitlab \
  --restart always \
  --volume $GITLAB_HOME/config:/etc/gitlab \
  --volume $GITLAB_HOME/logs:/var/log/gitlab \
  --volume $GITLAB_HOME/data:/var/opt/gitlab \
  --shm-size 256m \
  gitlab/gitlab-ce:15.1.2-ce.0

IP在gitlab.rb配置中修改

external_url 'http://47.xxx.xxx.xx' # 注意不需要加端口

修改完毕后,我就直接执行Docker命令重启了,访问网页直接显示502的界面,然后CPU暴涨,直接卡死,只能控制台重启机器。我以为是机器配置的问题,毕竟是2核4G的,于是我又换了一个低一点的版本13,试了下,发现依旧卡死。最后参考这篇文章docker 搭建gitlab后,出现502的处理方案之一

docker exec gitlab gitlab-ctl reconfigure

Dcoker启动后先不要访问网页,等一会CPU降下去后再执行这个命令。

初始化密码问题

之前安装的是13,密码在第一次访问时会显示重置密码的网页,但是安装的15,发现没有这个重置密码的,只有一个登录,原来从14开始初始密码放置在/etc/gitlab/initial_root_password,找到后直接登录后再重置。

安装Gitlab也算有好多次了,之前主要是原生的安装并没有用过Docker,另外吐槽下这玩意,动不动就CPU爆满,真的不是很友好。

参考

Linux初装gitlab初始默认密码

缘起

早些时候有使用React Native开发了第一版的App,后来又用Flutter重新开发了。Flutter的开发和用户体验确实比RN要好,我们的App其实一直没有使用,用的是小程序,功能的频繁变更,如果是App的话,可能用户手机上依旧安装的是老的版本,当然也可以通过版本控制来让用强制更新,从而可以避免老版本的问题。另外就是热更新,RN的最大优势就是热更新吧,如果不考虑热更新我肯定还是Flutter优先,RN的热更新主要以微软的CodePush,刚出来的时候免费的,目前已经变成AppCenter,需要付费了。另外React Native 中文网的Pushy,不过没有用过。后来偶然发现code-push-server这个开源的项目,最近才想到应该试下这个方案。

CodePush Server

这个项目的初衷是因为微软的CodePush在国内太慢了,另外官方的CodePush Server是没有开源的,只有React Native CodePush是开源的。理论上是可以通过React Native CodePush 反推出CodePush Server逻辑的,但是不知道CodePush Server的这个项目是怎么来的,不过能用就好。

不过这个项目在3年前就已经停止更新了,另外React Native CodePush则一直在更新,所以实际会有不少的问题。我用的是Docker搭建的CodePush Server服务,这个很简陋没有什么管理的界面,都是用React Native CodePush命令行来管理的。

依赖

package.json中的依赖:"react-native-code-push": "~5.6.0",一定要使用小版本,因为5.7以上的版本就会报错。

屏蔽自动生成

react-native-code-push插件会自动在/android/app/build/generated/rncli/src/main/java/com/facebook/react/PackageList.java文件中生成CodePush的类型,但是如果使用CodePush Server的话,就需要手动的更改CodePush的Server路径,而不是微软的,需要的项目下生成react-native-config.js,屏蔽自动生成。

module.exports = {
    dependencies: {
        'react-native-code-push': {
            platforms: {
                android: null, // disable Android platform, other platforms will still autolink
            },
        },
    },
};
Gradle 配置

需要变更app/build.gradle

apply from: "../../node_modules/react-native/react.gradle"
apply from: "../../node_modules/react-native-code-push/android/codepush.gradle" // 新增此段
常用命令

热更新是分平台,所以需要创建对应的安卓以及iOS的。

code-push release-react [应用名称]-android android -d Production # 打包生产版本

运行程序,注意热更新也是分为测试和生产版本的。

react-native run-android --variant=release  # 生成release版本,默认是Debug版本

总结

整个算是跑通了,从业务角度来看,热更新确实很棒。但实际情况就是如果不是大厂,那只能自己搭建CodePush Serser的服务了,不过考虑到这个开源的项目以及很久没人在维护了,所以实际上留下一个巨大坑,后续维护的难度还是有的。

同事的之前是用Mac本开发的,后来换成了小米的本,发现无法用真机和模拟器调试,执行adb devices 命令后直接卡死。网上搜索很多,多是说端口被占用的问题,但是试了好多次,发现端口没有被占用。当时也没有细想,后来冷静下来发现还是得从原理上来寻找问题的根源,于是一大早就把谷歌官网关于adb的文档重新看了下,原来只是使用,并没有深入的去理解原理。adb分为服务端和客户端,端口一般是5037,连接不上的问题实际上网络连接的问题。第一个想到的是防火墙,结果发现不是,后来觉得是不是代理软件的问题,于是就把代理软件关闭了,发现也没有解决问题。后来搜索发现了这篇文章如何解决adb卡死,命令不返回的问题,于是从微软官网下载了Process Explorer,发现果然是代理软件的问题,虽然之前虽然有关闭代理软件,因为adb连接的时候会默认加入代理,因为代理无法连接,所有adb的连接就一直不响应。

其实过去几年因为代理而产生的问题也是不少,但每次的还是会中招。另外发现现在的网络确实很厉害,很多问题只要一搜索大都能解决问题,而面对的这个问题的时候,我也陷入了这样的思维,不停的搜索,完全丧失了自己思考的能力。实际上当一个问题搜索很多遍后,依旧没有解,那么多半是一些只有在自己电脑上遇到的特殊问题。

Process Explorer,这个软件真的非常棒,主要还是免费提供的,直接从微软官网下载,我第一次从其他地方下载,没有发现问题而且显示患有问题,应该是一个老版本,后来从官网下载的就没有问题。因为一直使用Mac对于Win的使用经验还是停留在很多年前,发现要深度玩转Win。Sysinternals 套件真是宝藏库。

《苹果上的缺口:我与史蒂夫·乔布斯的生活回忆录》(The Bite in the Apple: A Memoir of My Life with Steve Jobs),很早之前听说乔布斯第一任,也是乔布斯第一个孩子的母亲,女友克里斯安·布伦南(ChrisannBrennan)写了一本《The Bite in the Apple》的书,我英语太菜,后来发现居然被翻译成中文版了,也是第一时间就购买了,与乔布斯有关的书很多,甚至连乔布斯经常光顾的餐厅的厨师都出书了,此书有点被忽视,人们大都了解成功后的乔布斯,虽然《乔布斯传》也曾描写创建苹果之前的乔布斯的一些事,不过还是有点不够细致,而此书刚好是一种补充。

18年的时候为了开发出海的应用,专门购买了一台二手的LG Nexus 5X,图的就是有谷歌服务,最近又有用到开发,发现一些问题,都是众所周知的原因了,好在都一一解决了,提前是要先安装adb,通过adb来设置。

连接WIFI成功后,提示无法访问互联网

实际上是可以访问网络,当然谷歌是访问不了的,网上搜索了下,主要是替换验证的网址了,以下方法亲测可用。

adb shell settings put global captive_portal_detection_enabled 1
adb shell settings put global captive_portal_mode 1
adb shell settings put global captive_portal_use_https 0
adb shell settings put global captive_portal_server connect.rom.miui.com
adb shell settings put global captive_portal_http_url http://connect.rom.miui.com/generate_204
adb shell settings put global captive_portal_https_url https://connect.rom.miui.com/generate_204

无法使用互联网时间

同样的也是无法谷歌提供的时间服务, 我用的阿里的NTP服务。

adb shell settings put global ntp_server ntp.aliyun.com

参考

开发者必备手机nexus 5x 开发环境预备

Android 系统时间不对有遇到的吗?

记事本这个小应用完全是为了学习Rust而产生的应用,今年4月6号创建了这个项目,主要是用业余时间弄下,刚好五一假期,我也就加把劲,算是弄出一个基本功能的东西。本来想写那些遇到的问题,可一旦动笔却不知道如何写起来,问题已经都解决了,新的问题依旧在路上。于是直接把项目上传到Github了,yew-notepad,有兴趣的可以自己拉下来看看,或许有点用。

目前用Rust写前端的资料很少,Yew资料更少,遇到问题基本上是反复的看官方的文档以及例子为主,很多时候总想抛开这些去寻找,但最后发现还是得认真的看文档和例子。另外就是看书吧,程序类的书籍我一般喜欢看纸质书,之前买的《Rust权威指南》,结合项目中遇到的问题期间又翻了翻,蛮有收获的。昨天又去省图借了《Rust程序设计》(Programming Rust),英文书名跟前者很像,出版也早一些,所以《Rust权威指南》为什么不是《Rust 程序设计语言》,估计是害怕读者搞混乱吧。这两本书各有千秋吧,我是拿来互补的。《Programming Rust》貌似已经出了第二版,我估计会考虑买本。

在线书籍

Rust 程序设计语言,其实就是我买的那本《Rust权威指南》,我觉得在线版的译名更合适。

通过例子学 Rust 中文版

尝试用Rust已经有点时间了,之前主要的问题是卡在IndexedDB的使用上,最后的代码是这样。

spawn_local(async move {
        let (tx, rx) = oneshot::channel::<IdbDatabase>();
        let window = web_sys::window().unwrap();
        let idb_factory = window.indexed_db().unwrap().unwrap();

        let open_request = idb_factory
            .open_with_u32(String::from("todo").as_str(), 1)
            .unwrap();

        let on_upgradeneeded = Closure::once(move |event: &Event| {
            let target = event.target().expect("Event should have a target; qed");
            let req = target
                .dyn_ref::<IdbRequest>()
                .expect("Event target is IdbRequest; qed");

            let result = req
                .result()
                .expect("IndexedDB.onsuccess should have a valid result; qed");
            assert!(result.is_instance_of::<IdbDatabase>());
            let db = IdbDatabase::from(result);
            let store:IdbObjectStore = db.create_object_store(&String::from("user")).unwrap();
            let _index = store.create_index_with_str(&String::from("name"), &String::from("name")).expect("create_index_with_str error");

        });
        open_request.set_onupgradeneeded(Some(on_upgradeneeded.as_ref().unchecked_ref()));
        on_upgradeneeded.forget();

        let on_success = Closure::once(move |event: &Event| {
            // Extract database handle from the event
            let target = event.target().expect("Event should have a target; qed");
            let req = target
                .dyn_ref::<IdbRequest>()
                .expect("Event target is IdbRequest; qed");

            let result = req
                .result()
                .expect("IndexedDB.onsuccess should have a valid result; qed");
            assert!(result.is_instance_of::<IdbDatabase>());

            let db = IdbDatabase::from(result);
            let _ = tx.send(db);
        });
        open_request.set_onsuccess(Some(on_success.as_ref().unchecked_ref()));
        on_success.forget();

        let db = rx.await.unwrap();
        let transaction = db.transaction_with_str_and_mode(&String::from("user"), IdbTransactionMode::Readwrite).expect("transaction_with_str error");
        let store = transaction.object_store(&String::from("user")).expect("store error");

        let name = JsValue::from_str(_content_element.value().as_str());
        let add_request = store.add_with_key(&name, &JsValue::from("name")).expect("add error");

        let on_add_error = Closure::once(move |event: &Event| {
            console::log_1(&String::from("写入数据失败").into());
            console::log_1(&event.into());
        });
        add_request.set_onerror(Some(on_add_error.as_ref().unchecked_ref()));
        on_add_error.forget();

        let on_add_success = Closure::once(move |event: &Event| {
            console::log_1(&String::from("写入数据成功").into());
        });
        add_request.set_onsuccess(Some(on_add_success.as_ref().unchecked_ref()));
        on_add_success.forget();

        console::log_1(&String::from("do").into());
 });

因为IndexedDB连接成功是个异步事件,db 只能在成功事件中才能拿到,最初因为对IndexedDB的使用不熟悉,这玩意在很早以前看HTML5的时候就看到过,不过一直没有用过。我在成功事件中调用create_object_store,结果给报错了,实际上创建数据库只能在onupgradeneeded事件中处理。

let myDatabase = MyDatabase::new();
myDatabase.add(String::from("jasper", String::from("name")));

早期的想法是把数据库的操作封装起来,但是连接是异步的,上面的add操作时数据库可能还没有连接成功,db等于为空。可能是之前做iOS的缘故,我一直想着数据库的操作应该是个单例,不过Rust的单例好像不那么简单,一直报错,所以这个想法就先放放了。

早期的时候甚至不知道如何用Rust设置回调,最后发现了这个库kvdb_web,代码也是参考了indexed_db.rs中的方法。这个库并没有演示的例子,所以只是参考了他打开数据库的方法。里面有用到futures::channel,但是实际使用老是报错。后来我忘了Rust写前端,实际上是运行在Wasm虚拟机的环境下,Rust的多线程则是一般的操作系统的环境下了。实际上要在Wasm使用多线程,得用另外一种方式。实际上是用Rust的代码调用JS了,JS是单线程运行的。所以整个思路虽然是用Rust在写,但实际思想的话得跟着JS来走。我想在后面的代码中拿到db,有点类似JS中使用await的方式。最后用了两个库,wasm-bindgen-futures主要使用了spawn_local这个东西,另外还有futures-channel这个库。

spawn_local(async {
	let (tx, rx) = oneshot::channel::<i32>();
	// 省略中间代码
	let on_success = Closure::once(move |event: &Event| {
                // Extract database handle from the event
                let target = event.target().expect("Event should have a target; qed");
                let req = target
                    .dyn_ref::<IdbRequest>()
                    .expect("Event target is IdbRequest; qed");

                let result = req
                    .result()
                    .expect("IndexedDB.onsuccess should have a valid result; qed");
                assert!(result.is_instance_of::<IdbDatabase>());

                let db = IdbDatabase::from(result);
                let _ = tx.send(db);
        });
    open_request.set_onsuccess(Some(on_success.as_ref().unchecked_ref()));
    on_success.forget();
   
    // 等于这块回一直阻塞,直到拿个值。
    let db = rx.await.unwrap();
    // 省略剩余代码
})

futures-channel这个库的用法跟Rust自带那个有点类似,不过只用这个是可以工作的。后面的问题基本上是IndexedDB的用法问题,最后终于可以把数据成功插入了,虽然再次插入数据时会有写入失败的问题,不过这个已经是后面的事了。

当我费了很多周折,成功写入时,也就是写这篇文章时候,突然间发现可以直接用下面的方式写,如果只是要写入数据的话。

let window = web_sys::window().unwrap();
let idb_factory = window.indexed_db().unwrap().unwrap();

let open_request = idb_factory
    .open_with_u32(String::from("todo").as_str(), 1)
    .unwrap();

let on_upgradeneeded = Closure::once(move |event: &Event| {
    let target = event.target().expect("Event should have a target; qed");
    let req = target
        .dyn_ref::<IdbRequest>()
        .expect("Event target is IdbRequest; qed");

    let result = req
        .result()
        .expect("IndexedDB.onsuccess should have a valid result; qed");
    assert!(result.is_instance_of::<IdbDatabase>());
    let db = IdbDatabase::from(result);
    let store: IdbObjectStore = db.create_object_store(&String::from("user")).unwrap();
    let _index = store
        .create_index_with_str(&String::from("name"), &String::from("name"))
        .expect("create_index_with_str error");
});
open_request.set_onupgradeneeded(Some(on_upgradeneeded.as_ref().unchecked_ref()));
on_upgradeneeded.forget();

let on_success = Closure::once(move |event: &Event| {
    // Extract database handle from the event
    let target = event.target().expect("Event should have a target; qed");
    let req = target
        .dyn_ref::<IdbRequest>()
        .expect("Event target is IdbRequest; qed");

    let result = req
        .result()
        .expect("IndexedDB.onsuccess should have a valid result; qed");
    assert!(result.is_instance_of::<IdbDatabase>());

    let db = IdbDatabase::from(result);
    let transaction = db
        .transaction_with_str_and_mode(&String::from("user"), IdbTransactionMode::Readwrite)
        .expect("transaction_with_str error");
    let store = transaction
        .object_store(&String::from("user"))
        .expect("store error");

    let name = JsValue::from_str(_content_element.value().as_str());
    let add_request = store
        .add_with_key(&name, &JsValue::from("name"))
        .expect("add error");

    let on_add_error = Closure::once(move |event: &Event| {
        console::log_1(&String::from("写入数据失败").into());
        console::log_1(&event.into());
    });
    add_request.set_onerror(Some(on_add_error.as_ref().unchecked_ref()));
    on_add_error.forget();

    let on_add_success = Closure::once(move |event: &Event| {
        console::log_1(&String::from("写入数据成功").into());
    });
    add_request.set_onsuccess(Some(on_add_success.as_ref().unchecked_ref()));
    on_add_success.forget();
});
open_request.set_onsuccess(Some(on_success.as_ref().unchecked_ref()));
on_success.forget();

总结

至此算是把IndexedDB的问题解决了,如果想把操作封装起来,那么可以用前面的方式。只是简单的操作的话,后面的代码也是可以的。到目前为止,基本上还没有写网页的东西,Yew的东西,也就只是搭了个架子时有用到,并没有深入的使用。后面得需要好好的深入下,Yew这个框架跟传统的还不太一样,用trunk serve启动后,实际会有一个WebSocket在前后端之间通讯。

再谈谈Rust学习的东西,实际上《Rust权威指南》这本书我只是看了个大概,而目前的实验项目中并没有使用Rust去写很多逻辑的代码,更多的是调用。用Rust写前端这个方式,起步会比一般的难点,主要还是牵涉的东西不只是Rust的问题,还有Wasm以及JS相关的东西,最重要的还是要明白他的运行环境是浏览器中Wasm虚拟机。不过后续如果有大量成熟的库,可以解决这些基础问题的话,只写网页的话,难度会小些。

虽然项目整体进展不大,也走了一些弯路,不过却也让我思考到了更多的东西。现在感觉Rust是未来全栈程序员必备的语言,这门语言是非常考验你是不是一个合格的程序员,因为他既有很底层的东西,比如内存概念,也有很现代的语法,但前提是你需要都懂,对于那些不是科班出身又没有好好学习计算机相关的东西人来说,这个确实有难度。

Python平时主要是处理数据用,在Mac上使用很方便,最近想把一个程序让下面人用,他们是Window环境,Python他们并不熟悉,所以直接把源文件丢过去,他们又得折腾一番。我就想到打包成exe,其实很早就有这样的想法,但一直没有做过。用的是PyInstaller这个库,还想着这么简单,结果一打包,发现并不是Window下的exe,原来是我理解的错误,以为可以直接打包成Window上执行的exe了,实际上PyInstaller只是在不同平台上将python打包成对应平台的应用了。难道我又得找台Windows,环境弄好,再打包一个?真麻烦,想着可以交叉编译吗?查了下PyInstaller以前好像可以,后来就去掉了。之前用过点Go语言,就是可以交叉编译的,一时间我尽有想用Go重写那个功能的想法,但是一对比还是Python方便。最后找了一个通过docker进行打包的方案docker-pyinstaller

直接这个命令就可以打包了,具体可以参考文档。

docker run -v "$(pwd):/src/" cdrx/pyinstaller-windows

但是有个问题,打包出来的应用一执行,提示一个依赖的库,没有找到。不对啊,文档中明明说把依赖写在requirements.txt文件里,我也写了的。

文档中的这段话,看着没有其他的操作,也没有相关的示例代码。

If the src folder has a requirements.txt file, the packages will be installed into the environment before PyInstaller runs.

折腾了好几个小时,最后找到了一篇日文的文章,看完后豁然开朗,原来是写法的问题,我对docker的一些东西不够熟吧。

docker run --rm -v "$(pwd):/src/" cdrx/pyinstaller-windows -c \
  "pip install -r requirements.txt && \
  pyinstaller main.py --onedir --onefile --clean && \
  mv dist/main.exe main.exe && \
  rm -rf __pycache__/ build/ dist/ main.spec"

最后打包后的exe就没有依赖缺失的问题了。不过打包成exe后,我最终还是改了下python的代码,比如os.path.dirname(__file__)是无法使用了,另外打开文件时提示UnicodeDecodeError: 'gbk' codec can't decode byte的错误,需要加encoding='utf-8'

参考

Docker環境のPyInstallerでキレイにExe化する

0%