刚开始拍摄“湘农曾哥”的时候,一年的脐橙销售季也快要接近尾声,这让我还有机会零距离的接触曾云鹤的脐橙销售全流程,概括起来,可以总结为收货、销售、发货、售后这么几个关键环节,这与我的基本认知是一致的,但是不一致的是,串联起整个流程的各个细节。

一早,曾在家里打印出当天需要发货的订单快递单,再从南庄坪村曾家人骑着他的小电驴至丫髻山下的仓库发货。那是他与另一个长沙的合伙人一起的水果存储仓库,从上一级渠道收购上来的统货全部都会贮存在此处,按客户购买的品类选果打包,一般分为礼品装与普通装以及小果装,不同的包装里面的脐橙果品是分组选择的,分级的标准一般粗略分为大、中、小果。

脐橙的分级有两个专业的工具,一个是选果机,水果自选果机上面滚过之后,会自动的根据果径分离,但这是需要有一定投入的,有一个更便宜但同样实用的工具,叫分级板,我们可以直接拿着水果在分级板的不同圆孔里面检测脐橙的果径,但曾还算是一个选果打包能手了,他什么工具都不使用,纯靠人感也能做到很精准的分级了,但包装最难的并不是分级,至少重量就是一个很难完美解决的问题。

包装一般分为 5斤装与10斤装,以前,我一直以为脐橙的包装规格完全是销售商自己自由制定的,但是后来才发现,绝大多数销售商并不会去自已制作包装规格,5斤或者10斤其实是快递公司制定出来的标准装,这更有利于快递订单处理、运输等,而全部使用统一标准包装箱尺寸的另一个好处是,不同的经销商之间还可以互相调运包装材料,如果哪一个经验商的包装箱不够,重新订包装材料又来不及,那么就可以临时性的借用一下其它销售商的冗余的包装材料。关于包装材料,又还有很多常人不知的秘密,我们以后再细细讨论。

我们在讨论规格的时候,5斤与10斤其实只是包装箱的规格,它并不表示里面装的就是5斤或者10斤果子,而是整个包装不能超过5斤或者10斤,快递在对电商做报价的时候,一般都是按固定的重量以下有一个基础价格,然后每增加多重就增加多少运费的模式,那么,对于10斤装的包装箱,只要不超过10斤,就都是同一个价格,超出10斤的话,价格就需要格外收取,这是一笔不小的快递支出,所以销售商在打包的时候,一般都会尽可能的打包之后的重量不超过10斤,若往一个果箱里面都放大一些的果子,那么果与果之间的间隙就会增大,这会导致水果的净重会远远低于10斤,若全是小果,那么有可能一箱装不满就超重,所以一般都会大小果混装,以尽可能的让最终的包装接近10斤,而又不至于超出其容量,但是这样就会带来另一个问题,客户在收到水果之后,会发现里面有大有小,若是在平台上面销售,那么就肯定发评论说是大果里面掺小果了。

那么能不能就按照一种等级的脐橙定制包装材料呢?这个理论是完全可行且确实有很我销售商是这么做的,可这样一来,就涉及到辰溪脐橙销售的另一个话题了——收购价格,统货是指果径大于某一个尺寸的所有果子,以同样的价格收购,若我们只容易销售的中大果,那么上一级就需要自己去解决小果的销售问题,而最简单的解决方式就是增加中大果收购的价格,这无疑又增加了销售商的成本,作为销售商的曾,需要一直根据当年的实际情况在不同的收购方式中做取舍,我们可以称之为这是生产商与销售商之间的一种永不停息的博弈。

说到这,我们也该讨论一下生产商了,也就是果农,辰溪的脐橙主要产区有原城郊乡、潭湾镇、火马冲镇等,且辰溪以大农场种植为主,

刚开始拍摄“湘农曾哥”的时候,一年的脐橙销售季也快要接近尾声,这让我还有机会零距离的接触曾云鹤的脐橙销售全流程,概括起来,可以总结为收果、销售、发货、售后这么几个关键环节,这与我的基本认知是一致的,但是不一致的是,串联起整个流程的各个细节。在认识曾云鹤时,他的水果销售已经仅限于微信等私域流量分销,掌握着两个卖他的果子的分销微信群,然后自己则努力将供应服务做得更好,从选果、打包、发货、售后都尽可能做到更加精益求精。

清晨,在南庄坪的家里,将当天要发货的订单的快递单打印出来,骑着自己的小电驴至丫髻山下的仓库打包、发货,参考赣南脐橙的一些方式,他认为辰溪的脐橙也一定是要分级销售的,按照一个标准将果子分为三六九等,同时,所以,打包的第一部就是确定客户所购买的是哪种品质、大小的果子。

选果机选果很高效,但是投入很大,分级卡很便宜,但是效率并不高,有这么多年的打包发货经验,曾一般凭感觉即能分个大概,有时自己感觉没办法确定的,就放在精确的电子称上面称一下,不合格的就直接往下一级的框里装着。曾现在的订单几乎全部是来自微信里面的分销商,这些分销商都是这些年积累下来的最信任他的朋友,所以他必须保证每一个收到货的客户都100%满意,所以对于打包装箱的这个活,曾一般是不信任别人来做的,这也导致他现在的这种商业模式本身就不可能做大,一个人全火力打包,也支撑不了多大的业务量,所以他的水果生意只能算是养家糊口吧。曾不愿意做淘宝、抖音等平台销售,虽然他是从平台开始做起来的,他说他的性格不太适合跟客户直接打交道,现在能留下来做他分销的朋友也基本上能很认可他的性格,至少不会因为讨论问题的时候拍拍桌子就分道扬镳。

从仓库打完包,货需要安排一个车子送到武总的快递中转站,这个在当时来讲,可也是全湖南县域内体量场地最大的快递中转站,即使现在也还是排行老二,只是辰溪确实没有足够体量的电商销售能支撑这样的中转站,所以,大家也都很需要努力。

回到家,下午去了趟一户同村老乡家里,前段时间答应了人家说送他们一百株柑橘树苗,这次去详细的给老乡介绍了几个不同品种的异同,一株树苗的价格也得几十元,不过对于曾来说,这是一个很值得的投入,一来村里若是能大更多人种柑橘,整个果园看上去就会更有体量,再者也确实能增加同村人的收入,虽然不是很多。

作为一个有着多年供应链经验的水果贩子,为了更好的研究各品种柑橘的异同,也为了自己将来有一个一生的事业基础,曾云鹤花了几年时间打造了一个名为“辰河云柑果园”的水果基地,湘农曾哥的拍摄基本上都是在这里完成了,果园里面栽种有由良蜜桔、红美人、绿美人、明日见、甘平、金秋沙糖桔、阳光一号等各高端品种的柑橘近30亩,由于回乡的前几年亏损严重,资金压力大,曾采用了一种会员制的方式,每一个会员可以收取会员费用500元,可以认领两棵果园的柑橘树,用于这两棵树的生产投入,待果园有产出之后,会员费用直接采用时价价值的柑橘作为回报。

这会员模式看上去似乎是一个一本万利的买卖,但这只是表像上所看到的,曾云鹤其实是在拿自己在他人心中的信用作为抵押,然后要付出百倍的努力经过多年的时间将大家的资助、信任转化为一棵棵健康的果树,农业是一个三分靠打拼,七分靠天意的产业,任何一次小小的细节上的失误,就有可能导致多年的付出付诸东流。这不是一腔热血就一定能成的事情,也不是靠情怀就能造就的伟业,曾云鹤曾经对我说过一句最触动我的话是:当我知道我的事业需要用一辈子时间去完成的时候,那么果树丰产需要两年还是五年就已经无所谓了,我能很平淡地看待它们缓慢的成长。这不就是一个匠人所拥有的品质吗?这就是我希望拥有的品质。

为了感谢大家的支持,曾云鹤计划在果园的空地上种点其它农产品给会员朋友们采摘,2月中旬,想着去柿溪溪口村张贤社那里买二十株葡萄,21日,在米昱杰的引荐下,我们去了溪口咨询葡萄相关的疑问,正好遇到葡萄园在大面积的为葡萄园搭建架子,在跟张贤社的聊天中,双学习到了一些以前完全不知道的知识。

现代任何一个品种的葡萄,都只能在精心照料下才会有产出,产量受外界影响相当大,有可能因为一次突发的暴雨就导致全年颗粒无收,而今年我县连续多次的降雪,对于普通人来说,那是多年未见之美景,但是对于他来说,则是损失惨重的天灾,2月初的大雪天,他们整个果园的工作人员彻夜未眠,要一直清理葡萄架上的积雪,因为雪量过大,最容易导致葡萄架整体倒下,虽然已经很努力的抗雪灾了,但最终还是有大量的葡萄架倒塌,直接经济损失十多万元。

风险在农业生产中,几乎是无所不在,而这些下定决心扎根农业的人,都是勇士。作为年轻一代新农人,我们不需要有人推我们一把,也不需要有人拉我们一把,我们需要有人为我们引路,而又有幸,在辰溪这片热土之上,能得到很多高人指路,让我们在崎岖的创业之路上披荆斩棘。

2022年3月,我们开设了一个名为“湘农曾哥”的抖音号,就当是正式合作了。

第一次拍摄是一次暴雪过后,果园里面有大量幼苗被积雪压断,他需要去修剪断枝并重新将树苗扶正,我们没有任何脚本,那个时候我还不知道他的工作的具体内容,所以我几乎是全程录制了,以保证能拍摄到任何一个重要的画面的步骤。这在后来被证明是一个极其愚蠢但又没有办法的拍摄方式。前期拍摄的方式,就基本上决定了后期剪辑时的方式,也直接决定了最终的成品视频,无非就是一个流水帐。

我们能商量出这样的合作和拍摄方式,主要原因是:

第一,我们都有各自的职业的,任何额外的时间、精力的付出都有一个前置条件,那就是不能影响到现有的工作,至少是不能出现负影响的;

其次,我们都对真实有一种变态的追求,我们希望最终呈现出来的内容是没有任何演绎的,试图将最真实的一面展现给观众;

再者,主要的原因就在于我有自己的执念。我是一名在IT行业从业十数年的程序员,对效率的追求是极其变态的,总是希望将所有可能重复性的工作都总结成一套标准的流程,然后之后的工作就完全按照流程走,在效率和极致的结果之间,要选择一个平衡点,这对于一个创作型的工作来讲是很难得到高质量的内容的,因为标准化的内容总是没有创意,没有创意,也就很难得到观众的认可。

在第一个作品剪辑完成之后,其实我们就认识到这个问题了,但是当时的决定是,先按那个思路拍摄、运营着,再找时间一起优化整个过程,所以,从那开始,我跟曾就差不多成了一对形影不离的基友一样,每天早出晚归,晨炊星饭。

在与曾云鹤确定合作之后,2月底,在米昱杰的组织下,与回乡的刘老根有了第一次接触,在我认识和了解的辰溪年轻自媒体从业者中,他是唯一一个让我感觉到有希望的人,相比于其他人,他的作品无论是从拍摄、剪辑还是内容上来讲是,都是辰溪自媒体从业者的一个标杆,当然那次见面也只是大家互相认识一下,并没有实质性的讨论到什么重要的话题。

后来也是米昱杰的邀请,约着一起去他家里坐坐,我因为要陪老婆孩子去湘西朋友家,就没有参与,米昱杰原本的计划是希望他与曾云鹤也能有合作,因为曾是有着多年供应链经验的人,现在刘老根有流量,两个人的结合应该是很完美的一件事情,这也是我跟曾云鹤共同认为的最合适的组合方式,包括我与曾共同想去完成的事情,也无非是运营一个有流量的抖音号,然后将产品通过这个抖音号卖出去,关于这样的合作,刘老根是什么样的想法,我不得而知,但是从后来多次与曾的交流中了解到,他是肯定拒绝这样的合作的。

曾认为,刘老根的抖音号不适合过早的带去、卖货,从长远考虑,他应该持续的保持现有的帐号运营,同时应该带动村里或者乡里发展一些特色产业,然后将自己的帐号内容扩大到覆盖这些产业,最后卖这些产业的产品,若是考虑到没有产业运营的经验,那么找一个现有的当地产业进行合作,也是可以的,比如纱帽坪村的刘薯记。只有这样,才能真正的带动一方经济,增加产品附加值,同时还能实现商业切入的软着陆,更容易被现有粉丝接受和支持,说简单点,就是可以做一个小号的李子柒。而曾自认为自己现在只是一个水果贩子,跟其他贩子的唯一区别可能就是他有更多的电商经验,与他合作,会让刘老根的帐号失去很多它原本拥有的更高的价值。

讨论这么多这几次与刘老根的接触,最主要就是想表达,我与曾的合作也无非就是能运营出一个类似于刘老根这样的帐号,然后卖货,唯一的区别就在于我们从一开始就是依托于自己已有的一个产业,假设我们能运营出这样的一个帐号,那么我们去卖任何我们产业内的产品,本身一定会有更高的附加值,即使到现在回忆起当时的想法来,也还是这样认为,只是我们当时没有考虑到整个商业设计最重要也是最关键的一环,正是我们两个人最薄弱的环节,这在后面,让我慢慢道来。

谈起我与曾云鹤的相识,离不开一个人,在一起玩航拍的朋友熊俊介绍下认识的米昱杰,一个乡村振兴办的职员,他是我这么多年以来,我认识到的唯一一个发自内心的希望辰溪的年轻人能一起在辰溪干一翻事业的人,与我也是校友,同时也是曾云鹤的同学,我们都毕业于辰溪县第二中学。

2021年5月,米昱杰约着我与熊俊在东山茶膳喝喝茶,聊到一半的时候,聊到了曾云鹤这个人,他就立马把曾云鹤约了过来,这是我与第一次与曾云鹤见面,当时就加了个微信,不过我们两也没说过几句话,主要还是在围绕着米昱杰提起的话题再讨论,我们聊了哲学、神学、家庭、学习、工作、创业等各种乱七八糟、天马行空的内容,但主要还是聊了下各自的工作、创业经历等。

在这次聊天以前,我与米昱杰只是有过几次通话,我一直错以为他与我联系是政府行为,而我本身也不太希望与政府有什么来往,所以前期内心都是拒绝的,但是之后才明白,这纯粹是他的个人行为,在后来这更长的时间里也验证了我的想法,他希望在他的努力下,辰溪能有一个汇聚了各种能人义士的团队能经常性的组织交流、学习,不管是一直生活在辰溪的,还是从外地返乡的人,通过大家的交流,又能产生一些有共同目标的人组成的团队,在辰溪这个八线县城干一些能改变这个地方的事情。

现实是,这样的团体在辰溪是很难真正存在的,绝大多数离开自己打拼的地方回到家乡辰溪,原因无非这么几种,要么衣锦还乡,回辰溪享受余生,他们没有在辰溪再干一翻事业的原动力;要么在外混不下去,他们即使回到辰溪,也同样需要每天为一日三餐而奔波,无非是换了个城市而已,根本就没有时间去思考与他人交流这件事情,最多也只是找朋友抱怨一下生活;还有一类便是临时性回来处理一些短暂的事情的,他们根本就没有考虑过在辰溪长待,事情干完立马就会离开。正因为这件事情是很难实现的,所以,我是发自内心的很佩服米昱杰,因为这是他的一个执念,并且一直在不求回报的为之努力。

虽然与他的交流总是会激动到要拍桌子,但这不正是气盛的年轻人原本就该有的特质嘛?只有在认真对待交流的前提下,才可能有价值的交流,对对方的发言无动于衷的人,才会敷衍地附和对方,至少从这一点上来看,我们都是在真诚的与对方交流。这有了我们能长久以来能经常交流的前提。后来认识的很多对我在辰溪的工作产生影响的人,也都是通过他认识的,这是我人生中遇到的一个贵人。

我做航拍辰溪,助力乡村发展本身就是重要的目标之一,这也是我这两年以来一直没有太多的参与商业行为的原因,农村才是我的主战场,而这个目标跟曾云鹤的目标是完全吻合的,这是我们两个人能走到一起的基础。

第一次促膝长谈是在我的车上,烧着一壶热茶,都是粗老汉,所以直接用的是大杯子喝,一口干的那种,虽然我的主要职业是一名跟互联网打了十多年交道的程序员,但从他身上我了解、学习到了很多另一个视角下的互联网、电商运营的知识。

他谈到单纯的帮助农民将产品卖出去是很容易的,但是如何保证农户的收入也是另外一回事儿,现在经常能在抖音上刷到很多9.9包邮的优质产品,但深入调查就不难发现,绝大多数让大主播带过货的农户、农场,都是以亏损告终,真正能得到正面影响的很少,就他的分析,最主要的原因是因为,低价走量会导致原头生产商的收益以及抗风险能力都变得极低,反过去会导致农户没有动力和资源去进行生产改进,也无法提升产品品质,甚至会导致产品品质的下降,进而进一步降低了自己产品的价格;另一个原因是,绝大多数生产商都只有一个单品,这是他们唯一能赚钱的产品,若唯一能赚钱的产品被用于营销获取流量了,最后即使得到了大量的流量,也没有其它的产品去赚回投入的成本,这也是他在第一年创业失败的最重要的原因之一,将自己当时唯一用于赚钱的橙子用于淘宝引流,虽然平台不一样,但商业逻辑是互通的。

他第一次卖黄桃,是按着卖橙子的逻辑卖的,先将黄桃收购上来入冷库,然后发快递,最后基本上都在路上就烂了,卖蜂蜜,了解到完全按照检测标准勾兑的蜜蜂比蜜蜂产的蜂蜜还要真,只有想赶早上市的橙子才需要打甜蜜素,若是正常销售的话,必须是原味果才能长时间存放……他经过几年的摸索,现在的生意基本上都是围绕着微信私域流量进行了,一直也没有但希望能去重新拓展公域流量市场。

那一次交谈,最终我们初步确定了一起运营一个抖音号作为销售、推广渠道,产品来自精选的辰溪本地农副产品。我负责拍摄、剪辑、运营,内容是完全无演绎的拍摄、记录曾云鹤在自己辰河云柑果园的农作、生产以及应季水果的销售日常。

曾云鹤,以前有聊过,一个与我同样毕业于辰溪二中的学霸,大学毕业之后在广州做女装电商,2016年响应了一些在县里工作的朋友们的号召,回县城帮助当年的果农通过电商销售滞销的橙子,这一回,就再也出不去了。在那时,他和怀化几乎所有的电商人一样,只是怀揣着一颗小小的梦想回到家乡,就是帮乡亲们多卖点橙子出去,让辰溪的橙子不再滞销。

我们成立了“辰溪县湘思归农电子商务有限公司”,并且注册了“湘思归农”品牌商标,意为“湘思不减·归去为农”,从此成为了新农人,正式踏上了返乡创业这条不归路。

由于一直在从事淘宝行业,对曾云鹤来说,回县之后,最熟悉的电商仍旧是淘宝,因为,2016年第一次经营农产品的时候,继续选择了淘宝,主要的精力还是集中在钻石淘宝规则、研究淘宝销售“技术”之上,在当年的淘宝上,拼流量拼排序拼价格向来是主流的“价值观”,低价走量自然成了首要选择。

那一年,在曾云鹤的努力下,确实卖了小几十万斤橙子,但是隔行如隔山,农产品与工业制品差别太大,同样一个橙子今年和去年的收购价可以差出一倍,而同样一件衣服的造价却可以几年不变。2015橙子遭遇历史最严重的大滞销,价格跌到历史谷底,而2016年橙子却遭遇历史最严重的减产,价格飙升到了历史最高点。显然用2015年的行情去预估2016的行价做预售,结果亏得一塌糊涂。

同年也尝试卖过蜂蜜,但是那里面的水又深又混,感觉蜂蜜行业并不适合他,后来也就彻底告别了那个领域。再此之后又尝试徽商分销,以低价走量的模式差点把自己的朋友圈做死。

在返乡的创业时间里,虽然一直以失败为结果,但结识了一群有着共同志向的朋友,张露、李晓峰、萧春龙、熊俊……关于曾云鹤作为一个年轻人返乡创业的代表,后面还有很多故事,这里就不再过多的阐述,时间跳到2021年底,我们相识。

2020年7月,印象辰溪的粉丝数快要两万人,因为公司有急事儿,就去了几个月天津,期间某日,乡村振兴办的朋友米昱杰打电话给我,两个人聊到了晚上两三点,主体内容主要是关于七八线小县城的小商家、农场主、水果贩子等如何能利用抖音扩大自己的经营,说简单点,就是怎么利用抖音赚更多的钱,当时我就表达了我的愚见:

  • 第一,对于本地商家,少量的本地抖音号探店是没有大的作用的,若想要起作用,除非能连续不断的进行,但是这投入很大,同时,探店的关键在于门店的营销方案也需要有相应的调整;
  • 第二,对于计划运营自己抖音号的小商家来说,投入是极高的,无论是人力还是财力,很有可能最后的结果就是刚开始有三分激情,之后就成了负担,不继续感觉投入浪费了,继续的话又感觉力不从心;
  • 第三,若是小商家们真的想自己投入做抖音的运营,可以考虑多个商家联合运营一个,将一个作为大家共同的营销入口。

忙完公司的事情回到辰溪,米就一直想推动我去做点事情,但是我也是自感本领有限,难以挑起大任,但最终阴差阳错的跟曾云鹤有了交集。2022年春节前,在米昱杰的邀请下,我们三人有了一个短暂而又重大的决定,两个原本没有任何交集的呆子组合到了一起,想着能在抖音电商之路上走一走。后来又经过了多次的讨论,最终决定了一个看上去很成熟也最容易成功的模式,但最终却发现走得很艰难。

6.3 洪灾

2022 年 6 月 3 日,自凌晨起,辰溪县便暴雨如注,清晨五点,谭家场一位养蛙的朋友去查看自己蛙场是否被洪水冲毁,在现场给我发了一条视频,从此便失联了,虽然从画面里能看到思螺溪的水位已没过下谌家的公路,但因为每年都会有这么一两天是这样的情况,所以并没有太在意,接着便全程跟踪直播了辰溪县城及周边几个标志性地方的洪水情况,看上去虽然很多低洼地带被淹,但并没见到什么严重的损失,整个直播过程都还在与朋友们嘻嘻哈哈地扯着闲篇儿。

了解到去往谭家场的公路有塌方,便转头去了一趟柿溪桃田坳村,顿感此次洪灾不同往日,十分严重,通过航拍镜头可见柿溪大溪方向所有的铁索桥都断了,大面积稻田被冲毁,主要道路低洼处淤泥有近三十厘米的深度,多处路段塌方,但即使是这样,我也从来没有想过,印象辰溪的整个六月会跟洪灾杠上。

次日,尝试从溆浦县舒溶溪镇过进入谭家场乡三屯区域,给爷爷奶奶送去了太阳能应急电源,在村里给大家手机充了几个小时的电,然后驾车前往禾桃坳,尽可能地航拍了整个谭家场思螺溪流域的灾后影像,惨不忍睹。回到县城后,听说谭家场乡的道路已经疏通了,立马于当晚十一点多赶到了谭家场乡集镇。

当汽车驶过集市,淤泥中的腐臭扑鼻而来,轮胎碾压着被洪水冲到路上的各种器具发出破碎撕裂的声音,目光所及,皆是忧郁的眼神,没有眼泪,却充满了悲伤。没有亲身经历过的人,永远也无法从任何人的口中听到、从任何影像的画面中看到、像任何一位经历者一样感受到,此次山洪过境时的恐惧与无助。

次日凌晨四点,又开始雷声轰鸣、暴雨倾注,我停车的位置虽然位居高地,但山上很快就有几股大水流倾泻而下,车轮前后很快就被水流冲下的石子围住,睡在车里也一直不安生,辗转反侧到了 6 时许,天微亮,雨渐小,我穿上一双防汛靴、披上一件雨衣就想着去街道上走走。

因为集市填高了几米,山洪水位没有没过它,从上游冲下来的各种汽车、家具等都在这个位置囤积,行走在这陌生而以熟悉的街道上,眼见一片狼藉,各种生活生产物资散落满地,几乎整个谭家场乡集市所有一楼均被冲得一无所有。乡干部敲的那清脆锣声,听得让人心碎,眼泪忍不住直流,可以看到乡村干部和村民,在这百年一遇的洪灾面前的无助与恐惧,一场稍大的雨就能让他们再次神经紧绷。但即使很苦,大家也都还充满希望,不管是普通村民还是商贩,都在积极进行生产自救,大家将屋里的淤泥铲到马路上,再由挖机清理出去。

通讯也已部分恢复,我把车开到农村商业银行门口,正好这天是赶集,大家都把手机拿过来充电,联系自己在外的亲人,报个平安,也让在外务工的家人安心。大家给我分享了很多洪灾当天他们拍的视频,思螺溪的水位几乎要到谭家场桥桥面的高度了,即便是沉重的消防车,也被洪水冲走。午后便回县城,与新阶联商量如何能快速的帮助灾民,在路上遇到已经有物资车进入谭家场,路过瓦子场时,灾情也同样严重。

当晚,父亲给我发来谭家场乡干群全力清理集市的视频,跟我说计划是通宵清理好所有公共道路,此时我与新阶联执行会长、辰溪生活圈蒋中华一起商量着接下来的救灾方案。

6月6日一早,第一车物资在谢记粮油开始装车,按我的意见,主要运送了饮用水、方便面这两样物资,主要是因为灾民很多连做饭的工具、灶都没有了,方便面是最方便的应急食品,之后又同样的向柿溪乡、修溪镇、田湾镇、孝坪镇等运送多车物资,这些就不必过多的叙述了,直到6月11日,一件微乎其微的事情深深的打动了我,高坪村书记想让我帮忙拍摄下整个村子的受灾情况,在去的路途中见到了让很多人第一反应都会想不明白的事情,辰溪话的“栽禾”,真的就成了栽了,用锄头插秧,田里所有的沃土都被洪水冲走,田里只剩硬底子,一句“能有多少有多少”是最让人心疼的。电力、通信也一直工作在一线,努力恢复电力、通讯,直到6月18日,爸爸终于能够休息,回到县城,跟我说已经基本恢复得差不多了,但是谁也不会想到接下来会发生什么。

6.19 洪灾

2022年6月19日,暴雨,陪父亲前往市里保养车子,刚上高速,荆竹溪周家一个朋友就发来视频,说是今天的雨似乎又不正常,父亲只来得及叮嘱一句转移村民到安全地带,就与对方失去的联系,估计才修通的通讯线路又断了,柘木屯姑姑也发了几个大水的视频,老家的店子也快被洪水冲了。回辰溪时,路过山塘驿,山塘驿渡槽的水都已经溢出形成一挂瀑布。父亲与在县城的同事联系上之后计划当天赶紧回乡里,但因沿途洪水过大而没有成行。

次日,父亲随几位同事驾车出发,车只能停在了葛藤溪麝香坳的半山腰,便开始步行至谭家场乡里,进入灾区后,也与我们断了通讯联系,当时并不清楚里面到底有多严重,九时许,零星有些灾民从里面徒步出来发给我的一些视频和照片让人胆战心惊,与姑姑还有另外几个计划回家的人一起驾车往伍家湾方向行驶。

行至罗家湾,弱鹰坳的山体滑坡十分严重,大量车辆堵在这里,不过听一直在电力抢修一线的朋友说,数台挖机已经连续抢修十数小时,虽然沿途多处公路塌方、滑坡,但总算安全顺利地翻越了弱鹰坳,到达下葛藤溪。遇到很多从里面逃难出来的人,有几位朋友是早上四点起从高坪村出发步行了近8个小时才到达此处的。我放飞无人机,航拍了伍家湾村、柿溪分庄垴两个村落的灾情,从画面中能看到几乎是粮田与道路尽毁,很多房屋倒塌或被掩埋。回城之后,便约了几位朋友,相约次日背上物资进入灾区。

2022年6月21日清晨,我们从谢记粮油取了约一千元物资,一行共十二个人驾车行至伍家湾村,可以明显的感受到这次的洪灾比第一次要严重得多,伍家湾村委会路段全线被冲垮,只能沿着原来的村道绕道而行,身上背着60多斤物资,穿着夹板拖,踩在泥泞的路上太滑,拖了鞋好走点。

2022年6月3日凌晨0点13分,我发了第一个关于暴雨的视频,感觉雨量有点异常;六点左右,因为连夜的暴雨,朋友担心蛙场,一早就过去看看,8点15给我发了第一个洪水的视频,仅三个小时的降雨,溪水已经漫过谭家场村下谌家的公路;8点43分收到龚家湾洪水消息,已经快要漫过公路,8点47分收到朋友给我发了最后一个视频,此时洪水已经开始在谭家场集市中快速流过,水已至膝盖,我与朋友失联,9点21收到来坪村罗家湾洪水视频,此时罗汉果田地已经全部被淹。之后一直持续直播更近辰溪县城周边洪水信息。

上午10点收到谭家场集市商铺、车辆被洪水冲走的视频,此时真正感觉事态严重,之后我一边直播县城周边洪水,同时一直收到从伍家湾、谭家场、田湾、板桥等地发送过来的关于洪水的视频,可以明显看到此次洪水完全超出了所有人的预期,随后联系信任的人一一确认视频是否属实后,将所有被确认的视频发布至印象辰溪抖音号。

下午四点从县城出发,进入柿溪大溪方向,航拍了整个柿溪大溪方向各个村落的受灾情况视频,并将所有视频按地名发布,在进入柿溪时看到的情况是,水位在一天之内上涨和下降高度有四五米,可见洪峰是快速形成快速回落的。

6月4日一早尝试从溆浦县舒溶溪乡上山进入谭家场乡,回到柘木屯给爷爷奶奶安装了应急太阳能电源之后,给村里老乡的手机充了几个小时的电,然后尝试从禾桃坳下山进入谭家场村,但因为禾桃坳金处塌方,只能从禾桃坳放飞无人机拍摄下整个谭家场乡洪水航拍画面,第一次看到此次洪水形成的破坏,惨不忍睹。当晚7点回到辰溪,打听到过伍家湾至谭家场的道路已经疏通,且信号已经部分恢复,遂与妈妈、姑姑三人连夜前往谭家场,晚上11时到达谭家场后亲身看到洪水过后集市的惨状,视频可能无法表达清楚,沿路都是腐败发臭的味道。

6月5日正好阴历逢七,谭家场赶集,五点左右又开始暴雨,赶集起床随乡政府工作人员一起巡视,敲锣预警,白天将车停在信用社门口给大家手机充电,同时走访了一些受灾老乡,汽车、商铺物资等均被洪水冲得遍地都是。下午路过思蒙时,航拍并发布了思蒙、伍家湾灾情视频,当晚回到辰溪,包括辰溪生活圈中华在内作为自媒体身份参与了辰溪县新阶联、民办教育协会抗洪救灾捐赠会议,开始快速的筹备物资。

6月6日清晨8点,在谢记粮油批发部门店集合,物资陆续到达装车,同时见几位在辰溪创业的谭家场老乡也开着自己的面包车采购物资,准备回乡赈灾。十一点左右我们筹集的第一批物资前往谭家场,直至下午4点所有物资入库,驾车前往修溪镇坳门帮乡村振兴办一朋友拍摄、确认坳门村一户罗汉果种植户的受损情况,同时拍摄并发布了坳门村受灾航拍影像,此时见辰溪义工协会正在发放由壹基金提供的抗灾物资。

6月7日,同样一早联络物资并送至修溪镇。

6月8日,进入至修溪镇椒坪溪方向,航拍了椒坪溪受灾视频。

6月9日,联络物资并送至孝坪镇、田湾镇。此次还有数位工会退休老党员干部带着他们筹集的物资同行。

时间线

1940 之前

1804 年,约瑟夫·玛丽·雅卡尔发明了雅卡尔织布机(Jacquard machine),运用打孔卡上的坑洞来代表缝纫织布机的手臂动作,以便自动化产生装饰的图案。

A_Jacquard_loom_showing_information_punchcards.jpeg

工作人员使用雅卡尔织布机前首先设计需要编织的图案,并根据设计以及相应规则在打孔卡上打孔,随后工作人员将打孔卡放入雅卡尔织布机,雅卡尔织布机根据打孔卡孔的有无来控制经线与纬线的上下关系,以达到编织纺织品不同花样的目的。这种使用可更换的打孔卡来编织纺织品花样的原理被认为是计算机硬件历史上的重要一步。

爱达·勒芙蕾丝在1842年至1843年间花费了九个月,将意大利数学家费德里科·路易吉关于查尔斯·巴贝奇新发表机器分析机的回忆录翻译完成。她于那篇文章后面附加了一个用分析机计算伯努利数方法的细节,被部分历史学家认为是世界上第一个电脑程序。

赫尔曼·何乐礼在观察列车长对乘客票根在特定位置打洞的方式后,意识到他可以把信息编码记载到打孔卡上,随后根据这项发现使用打孔卡来编码并纪录1890年的人口统计资料。

霍列瑞斯式的打孔机(pantograph),用于1890年的人口普查:

CTR_census_machine.jpeg

1940 年代

最早被确认的现代化、电力启动(electrically powered)的计算机约在1940年代被创造出来。程序员在有限的速度及存储器容量限制之下,撰写人工调整(hand tuned)过的汇编语言程序。而且很快就发现到使用汇编语言的这种撰写方式需要花费大量的脑力(intellectual effort)而且很容易出错(error-prone)。

康拉德·楚泽于1948年发表了他所设计的Plankalkül编程语言的论文。但是在他有生之年却未能将该语言实现,而他原本的贡献也被其他的发展所孤立。

1950 - 1967 年

有三个现代编程语言于1950年代被设计出来,这三者所派生的语言直到今日仍旧广泛地被采用:

  • FORTRAN (1955),名称取自"FORmula TRANslator"(公式翻译器),由约翰·巴科斯等人所发明;
  • LISP,名称取自"LISt Processor"(枚举处理器),由约翰·麦卡锡等人所发明;
  • COBOL,名称取自"COmmon Business Oriented Language"(通用商业导向语言),由被葛丽丝·霍普深刻影响的Short Range Committee所发明。

另一个1950年代晚期的里程碑是由美国与欧洲计算机学者针对"算法的新语言"所组成的委员会出版的《ALGOL 60报告》(名称取自"ALGOrithmic Language"(算法语言))。这份报告强化了当时许多关于计算的想法,并提出了两个语言上的创新功能:

  • 嵌套区块结构:可以将有意义的代码片段组群成一个块,而非转成分散且特定命名的程序。
  • 词法作用域:区块可以有区块外部无法透过名称访问,属于区块本身的变量、程序以及函数。

另一个创新则是关于语言的描述方式:

  • 一种名为巴科斯-诺尔范型 (BNF)的数学化精确符号被用于描述语言的语法。之后的编程语言几乎全部都采用类似BNF的方式来描述程序语法中上下文无关的部分。

Algol 60对之后语言的设计上带来了特殊的影响,部分的语言很快的就被广泛采用。后续为了开发Algol的扩展子集合,设计了一个名为Burroughs大型系统。

延续Algol的关键构想所产生的成果就是ALGOL 68:

  • 语法跟语义变的更加正交(orthogonal),采用匿名的例程(routines),采用高端(higher-order)功能的递归式输入(typing)系统等等。
  • 整个语言及语义的部分都透过为了描述语言而特别设计的Van Wijngaarden grammar来进行正式的定义,而不仅止于上下文无关的部分。
    Algol 68一些较少被使用到的语言功能(如同步与并行区块)、语法快捷方式的复杂系统,以及类型自动强制转换(coercions),使得实现者兴趣缺缺,也让Algol 68获得了“难用”的名声。尼克劳斯·维尔特就干脆离开该设计委员会,另外再开发出更简单的Pascal语言。

这段时间的主要语言有:

  • 1951 - Regional Assembly Language
  • 1952 - Autocode
  • 1954 - FORTRAN
  • 1954 - IPL (LISP的先驱)
  • 1955 - FLOW-MATIC (COBOL的先驱)
  • 1957 - COMTRAN (COBOL的先驱)
  • 1958 - LISP
  • 1958 - ALGOL 58
  • 1959 - FACT (COBOL的先驱)
  • 1959 - COBOL
  • 1962 - APL
  • 1962 - Simula
  • 1962 - SNOBOL
  • 1963 - CPL (C的先驱)
  • 1964 - BASIC
  • 1964 - PL/I
  • 1967 - BCPL (C的先驱)

1967 年

出任美国高级研究计划署(ARPA,Advanced Research Project Agency)信息处理处(IPTO,Information Processing Techniques Office)处长着手筹建“分布式网络”,不到一年,就提出阿帕网的构想。随着计划的不断改进和完善,罗伯茨在描图纸上陆续绘制了数以百计的网络连接设计图,使之结构日益成熟。

Arpanet_logical_map,_march_1977.png

1968 年

  • 罗伯茨提交研究报告《资源共享的计算机网络》,其中着力阐发的就是让“阿帕”的电脑达到互相连接,从而使大家分享彼此的研究成果。根据这份报告组建的国防部“高级研究计划网”,就是著名的“阿帕网”,拉里·罗伯茨也就成为“阿帕网之父”。
  • Logo 语言诞生

1969 年底

阿帕网正式投入运行,但ARPA网无法做到和个别计算机网络交流,这引发了研究者的思考。根据诺顿的看法,他的设计需要太多的控制和太多的网络中机器设备的标准化。

Allmystery1988.jpeg

最初的 4 个节点分别为:

  1. 斯坦福大学
  2. 加州大学洛杉矶分校
  3. 加州大学圣巴巴拉分校
  4. 犹他州大学

1970 年

  • Pascal 语言
  • Forth 语言

1972 年

  • C 语言
  • Smalltalk 语言
  • Prolog 语言

1973 年

  • ML 语言
  • 春,文顿·瑟夫和鲍勃·康(Bob Kahn)开始思考如何将ARPA网和另外两个已有的网络相连接,尤其是连接卫星网络(SAT NET)和基于夏威夷的分组无线业务的ALOHA网(ALOHA NET)瑟夫设想了新的计算机交流协议

1974 年

罗伯特·卡恩和文顿·瑟夫提出TCP/IP,定义在电脑网络之间传送报文的方法(他们在2004年也因此获得图灵奖),实现了传输层通信。

1975 年

  • Scheme 语言
  • ARPA网被转交到美国国防部通信处(Defense Department Communicationg Agence)。此后ARPA网不再是实验性和独一无二的了。大量新的网络在1970年代开始出现,包括计算机科学研究网络(CSNET,Computer Science Research Network),加拿大网络(CDnet,Canadian Network),因时网(BITNET,Because It's Time Network)和美国国家自然科学基金网络(NSFnet,National Science Foundation Network)。

1978 年

  • SQL 诞生,起先只是一种查询语言,扩展之后也具备了程序结构

1980 年

美国政府标准化一种名为 Ada 的系统编程语言并提供给国防承包商使用。

440px-Ada_Mascot_with_slogan.svg.png

1981 年

第一个基于 TCP/IP 协议的完整规范建立,CSNET 建立

1982 年中期

ARPA网被停用,原先的交流协议NCP被禁用,只允许使用CERN的TCP/IP语言的网站交流,欧洲落地首个 WAN 广域网。

1983 年 1 月 1 日

NCP成为历史,TCP/IP开始成为通用协议。

1983 年

基于 TCP/IP, ARPANET、PRNET、SATNET三个原始的网络完整的通讯操作。ARPANET被分成两部分,用于军事和国防部门的军事网(MILNET)和用于民间的ARPANET版本。

1984 年

DNS 技术首次实现,电子邮件业务在德国开通。

1985 年

世界上首个域名 symbolics.com 诞生。

TCP/IP成为UNIX操作系统的组成部分。最终将它放进了Sun公司的微系统工作站。

当免费的在线服务和商业的在线服务兴起后,例如Prodigy、FidoNet、Usenet、Gopher等,当NSFNET成为互联网中枢后,ARPANET 的重要性被大大减弱了。

1986 年

Craig Partridge 开发完成 MERS(现代邮件路由系统)。

美国国家科学基金会创建了美国超级电脑中心与学术机构之间互联基于TCP/IP技术的骨干网络NSFNET,速度由最初的56kbit/s,接着为T1(1.5Mbit/s),最后发展至T3(45Mbit/s)。

1987 年

世界上最大的网络服务商之一 UNNET 成立。

1988 年

网络服务商 CERFNET 开始运营业务。

1989 年

  • PSINET 成立,它是第一个商用网络服务商。
  • NSFNET 扩展到欧洲、澳大利亞、新西兰和日本的学术和研究组织。
  • Tim Berners-Lee 开始了 WWW 与 HTTP 开发。
  • Brewster Kahle 发明了首个互联网发布系统 WAIS(WIDE AREA INFORMATION SERVER)。
  • ARPA被关闭。
  • 商业互联网服务提供商(ISP)在美国和澳大利亞成立。
  • MCI Mail 和 CompuServe 与互联网创建连接,并且向50万大众提供电子邮件服务。

1990 年 3 月

  • ARPA网正式退役。
  • 康奈尔大学和欧洲核子研究中心(CERN)之间架设NSFNET和欧洲之间的第一条高速T1(1.5Mbit/s)连接。

1990 年 9 月

9 月 10 日,首个网络搜索引擎 Archie 出现,它是一种用于索引 FTP 档案的工具,使用户可以更轻松地识别特定文件。 它被认为是第一个互联网搜索引擎。最初的实现是由 Alan Emtage 于 1990 年编写的,当时他是加拿大蒙特利尔麦吉尔大学的研究生。

欧洲核能研究组织(CERN)科学家 Tim Berners-Lee,在全世界最大的电脑网络——互联网的基础上,发明了万维网(World Wide Web),从此我们可以网络上面浏览信息

9407011_31-A4-at-144-dpi.jpg

该网页现在还可以在 http://info.cern.ch/hypertext/WWW/TheProject.html 访问到

同样由 Tim Berners-Lee 发明了第一个网页浏览器 WorldWideWeb(后改名为Nexus)。

screensnap2_24c.gif

但此时的不同的操作系统只有终端,伯纳斯-李雇用了妮可拉·佩洛为各种系统编写命令行工具可用的 Line Mode Browser(简称 LMB) 浏览器,能在终端上显示网页,于1991年发行。 其操作只能使用命令行,内容也仅限于字符文本:

file.png

1990 年圣诞节

蒂姆·伯纳斯-李创建运行万维网所需的所有工具:超文本传输协议(HTTP)、超文本标记语言(HTML)、第一个网页浏览器、第一个网页服务器和第一个网站。

1991 年

  • WWW 正式向公众开放
  • Gopher 协议开发完毕

1992 年底

David Thompson 向 NCSA 的软件设计小组展示了 ViolaWWW 浏览器。

ViolaWWW.png

ViolaWWW是万维网(WWW)第一个流行的浏览器,目前已停止开发。其首次在1991/1992年的UNIX操作系统上发布,并成为受万维网发源组织CERN所推荐的浏览器

这启发了马克·安德森和埃里克·比纳在 UNIX 的 X Window 编写了 NCSA Mosaic,名为 xmosaic 这是人类历史上第一个被普及的浏览器,从此网页可以在图形界面的窗口浏览:

XMosaic.gif

mosaic-sm.gif

关于 Mosaic 的发布消息,可以在 https://web.archive.org/web/20070616004435/http://www.seanm.ca/mosaic/ 上面查看到

1993 年

Marc Andreessen 在他的 Mosaic 浏览器的 HTML 实现中,加入了 <img /> 标记,进一步推动了浏览器的创新,从此可以在 Web 页面上浏览图片

Mosaic 1.0 运行于 System 7.1,并打开了 Mosaic通信公司的(后来的Netscape)官方网站

Mosaic_1.0_for_Mac.png

NCSA Mosaic 3.0 运行于 Windows

Mosaic-v3-screenshot.png

安德森是NCSA中Mosaic团队的领导者,他不久后辞职并成立了自己的公司——Netscape,发布了受Mosaic影响的Netscape Navigator。Netscape Navigator很快便成为世界上最流行的浏览器,市占率一度达到90%。

1993 年 6 月

由 IETF 工作小组发布了 HTML 草案

1994 年

  • 10 月,W3C 成立,网络应用发展标准规范六由 W3C 协会制定以及推广
  • Netscape 和 Yahoo! 的最初版本建立

1995 年

  • Amazon.com、Craigslist 以及 eBay 成立
  • AltaVista 建立
  • 11 月,HTML 2.0 发布(最终于 2000 年 6 月被宣布过时)

1996 年

  • 1 月,HTML 3.2 由 W3C 推荐为标准规范
  • 6月4日,对于阿丽亚娜5型运载火箭的初次航行来说,这样一个错误产生了灾难性的后果。发射后仅仅37秒,火箭偏离它的飞行路径,解体并爆炸了。

    V88 debris 01.jpeg

    火箭上载有价值5亿美元的通信卫星。6亿美元付之一炬。后来的调查显示,控制惯性导航系统的计算机向控制引擎喷嘴的计算机发送了一个无效数据。失事调查报告指出,火箭爆炸是因为:

    During execution of a data conversion from 64-bit floating point to 16-bit signed integer value, the floating point number which was converted had a value greater than what could be represented by a 16-bit signed integer. This resulted in an Operand Error.

    它没有发送飞行控制信息,而是提交了一个诊断位模式,表明在将一个64位浮点数转换成16位有符号整数时,产生了溢出。溢出值测量的是火箭的水平速率,这比早先的阿丽亚娜4型运载火箭所能达到的高出了5倍。在设计阿丽亚娜4型运载火箭的软件时,他们小心地分析了数字值,并且确定水平速率绝不会超出一个16位的数。不幸的是,他们在阿丽亚娜5型运载火箭的系统中简单地重新使用了这一部分,而没有检查它所基于的假设。Ada代码如下:

    begin
    sensor_get(vertical_veloc_sensor);
    sensor_get(horizontal_veloc_sensor); 
    vertical_veloc_bias := integer(vertical_veloc_sensor);
    horizontal_veloc_bias := integer(horizontal_veloc_sensor); 
    ... 
    exception
    when numeric_error => calculate_vertical_veloc();
    when others => use_irs1(); 
    end;
    信息: http://www.capcomespace.net/dossiers/espace_europeen/ariane/ariane5/AR501/V88_AR501.htm

1997 年 11 月

HTML 4.0 发布

1998 年

Google 正式向公众开放

1999 年 12 月

HTML 4.01 以 XML 语法重新构建,较为严格,被 W3C 推荐为标准规范

2000 年

  • 1 月,XHTML 1.0 由 W3C 推荐为标准规范
  • DDOS 攻击导致互联网泡沫破裂

2001 年

  • 4 月,Drupal 内容管理系统发布,它成为全球最受欢迎的网站内容管理系统之一,至2012年9月,全球约有 2.2% 的网站由 Drupal 制作,占所有内容管理系统的 7%。至2019年4月,全球约有 1.9% 的网站由 Drupal 制作,占所有内容管理系统的 3.4%。包括美国白宫 (Whitehouse.gov)、The Onion、Ain't It Cool News、Spread Firefox、Ourmedia、KernelTrap、NewsBusters 等等。它特别常见于社群主导的网站。
  • 5 月,XHTML 1.1 由 W3C 推荐为标准规范

2003 年

  • 博客发布系统 WordPress 建立

2004 年

  • WHATWG 小组成立,由各大浏览器开发商组成,重拾 HTML 4 规格,开发 HTML 5 规格
  • Facebook 成立,也因此,该年常被称为社交网络元年

2005 年

  • YouTube.com 和 Reddit 上线

2006 年

  • W3C 认输,承认 XHTML 2.0 不会成功

2007 年

  • W3C 重新成立 HTML 工作小组,参考 WHATWG 的规格开发最新的 HTML 规范

2009 年

  • XHTML 2.0 被放弃,全面投入 HTML 5 规范的开发

2011 年 6 月

  • Google 宣布全面采用 HTML 5 技术

2012 年

  • HTML 5 被选为候选标准

2014 年 10 月 28 日

  • HTML 5.0 由 W3C 正式发布为推荐标准
  • 以 Google 为首的浏览器聪明强化对 HTTPS 的支持

2016 年

  • Google 分析上线
  • 人工智能技术开始大量涌现

2018 年

  • 人工智能技术迅速发展

21
22

一生中有多少这样的时刻,心中有丘壑,眼底存山河。灵魂,在 2200 年历史长河中起舞,与 1990 平方公里的土地对歌。光影、树木、河流、瀑布、古寺、老宅、田野,层层叠叠。

什么是理想中的家园?是群山环绕的小小村落?还是一望无际的田野悠悠?是举目即见的生机绿意?还是抚慰人心的人间烟火? 历史轮转的时光痕迹,世代耕耘的田园风光,透过缥缈的云雾,俯瞰无限生机,慢享,浮生流年,走过小时候走过的小桥流水,看见记忆中熟悉的纯真脸庞,盛上一碗装满瑶家人热情的米酒,喝上一段远离喧嚣的理想时光,妈妈说,走得再远,这里,永远是家,接下来的时间,跟随我的镜头,回忆这一年,我所看到的不一样的辰溪,徜徉心中的,理想家园。

下面这几种方法添加事件监听有什么区别?

  1. el.addEventListener('click', handleClick);
  2. el.setAttribute('onclick', 'handleClick()');
  3. <div onclick="handleClick()"></div>
https://wangdoc.com/javascript/events/model.html

请问 document.body.childNodesdocument.querySelectorAll() 得到的 NodeList,有哪些区别?

https://wangdoc.com/javascript/dom/nodelist.html

请问如何让数字可扩展?比如:const a = [...5] 就可以创建一个 [1, 2, 3, 4, 5] 这样的数组?

Number.prototype[Symbol.iterator] = function*() {
 let i = 0;
 let num = this.valueOf();
 while (i < num) {
   yield i++;
 }
} 

给出下面这样的一个类:

class AnObject {
  constructor(number) {
    this.number = number;
  }
}

如何让它的实例相加时,得到的值为 number 相加的值?

class AnObject {
  constructor(number) {
    this.number = number;
  }

  // 改变下面这样的就可以了
   valueOf() {
    return this.number;
  }
}

假设我们现在有一个 HTML 元素:

<div class="field">
    <label>用户名</label>
    <input />
</div>

通过 CSS 样式可以为 label 添加

.field label:after {
  content: ':';
}

但是这样并不能实现我真正的需求,我的需求是,当系统使用英文时,使用 : ,使用中文时,使用 ,那么,请问:我们该如何动态的改变 label:after 中的 content 的值?假设不允许通过 JavaScript 动态更新 CSS 样式,也不允许动态改变元素的类与 ID,那又该如何实现?

在 C++ 之前,有一个叫 C 的家伙

C 语言是由贝尔实验室的 Dennis Ritchie 于 1972 年发明的一种操作系统编程语言(专门用于开始操作系统的语言),Ritchie 的主要目的,是开发出一种易于编译、能高效访问内容、生成高效代码且不依赖于其它程序的简约语言,作为一门高阶语言,它给开发者提供了很大的控制权,同时又保留了硬件与操作系统的独立性,不必为每个平台重写代码。

由于 C 语言是如此的高效和灵活,所以,在 1973 年,Ritchie 与 Ken Thompson 使用 C 重写了大部门 Unix 操作系统的代码,许多以前的操作系统都是采用汇编语言编写的,只能运行于特定的硬件平台之上,而 C 具有出色的可移植性,允许 Unix 在许多不同类型的计算机上轻松的重新编译,此时,C 语言与 Unix 命运就已经联系在了一起。

1978 年,Brian Kernighan 与 Dennis Ritchie 出版了一本名为《C 程序设计语言》的书,这本书通常被称为 K&R,为该语言提供了非正式的规范,并成为实事上的标准,当需要最大的可移植性时,程序员会坚持 K&B 中的建议,因为当时大多数的编译器都是按照 K&R 的标准实现的。

1983 年,美国国家标准协会(ANSI)成立了一个委员会来建立 C 语言的标准,1989 年,他们完成并发布了 C89 标准,通常称为 ANSI C,1990 年国际标准化组织 ISO 采用了 ANSI C ,这个版本称为 C90,编译器最终符合 ANSI C/C90,并且需要最大可移植性的程序被编码为该标准。

1999 年,ANSI 委员会发布了一个新版本的 C,称为 C99,C99 采用了许多已经作为扩展进入编译器的特性,或者已经在 C++ 中实现的特性。

好吧,在我接触计算机的近 28 年后,我终于鼓起勇气要正式开始学习 C++ 了,虽然以前也计划过很多次,但是这次看上去是认真的了,秉着学习记笔记的良好习惯,这个博客估计又会有几个月的频繁更新了。

为什么要学?

这个问题其实很简单,事情是这样的,最近一直在开发公司的一个名为 XXXX 的项目,里面涉及到大量的地图业务,最开始我们使用了开源的 OpenLayer 库作为底层地图支持,现在功能都开发得差不多了,自己感觉二维的平面地图太弱鸡了,我想整成一个建筑的三维地图,然后就开始接触 WebGL,以前都是使用的 2D 渲染,上周花了一个星期系统性的学习了 WebGL 相关的知识,写了 Hello, 3D 应该是没啥大问题了,但是总想再进一步,就更深入的了解 WebGL,啊,原来就是个 OpenGL 的绑定啊,那我就计算更深入地学习下 OpenGL,然后发现,OpenGL 的知识基本上都是拿 C++ 语言当示例,好吧,那等我先把 C++ 学会了再回来学 OpenGL,事儿就是这么个事儿,你看,我是不是有点忘记初心了?

如何让一个类的属性全部可选?

比如我有下面这样一个类型:

type User = {
  username: string;
  gender: 'male' | 'female';
  age: number;
  bio: string;
  password: string;
}

User 类型要求所有属性都必须有值,所以:

const user: User = {
  username: 'awesomeuser',
};

是不可行的,会提示:

类型“{ username: string; }”缺少类型“User”中的以下属性: gender, age, bio

如何让它可行?使用 Partial 即可:

const user: Partial<User> = {
  username: 'awesomeuser'
}

Partial 内置内型的作用就是让一个已定义了的类型的所有属性变得可选,具体实现如下:

/**
 * Make all properties in T optional
 */
type Partial<T> = {
    [P in keyof T]?: T[P];
};

如何让一个类型的属性全部必填?

假设我们有下面这样一个类型定义:

type User = {
  username: string;
  age?: number;
  gender?: 'male' | 'female'
  bio?: string;
  password?: string;
}

然后从服务器后端拿到了一系列用户数据的结果:

const users: User[] = await api.users.list();

此时我们尝试去访问用户的 age 进行比较,想要取出 age > 18 的用户:

const filteredUsers = users.filter(user => user.age > 18);

此时你的 IDE 是不是提示你:对象可能为“未定义”。?为什么?因为我们定义了 age 本身就是可选的属性,未定义该属性时,它的值就是 undefined,而在 typescript 中, undefined 是不允许与 number 类型进行比较的,但是此时其实我们是能很确定 api.users.list 接口返回给我的,要么是抛出错误,要么给到我的就一定是带了 age 值的 User 类型数据,所以,我是完全可以去比较的,但是由于 TypeScript 并不知道你加载的数据是 User 还是所有属性都已经有值的特殊的 User,那怎么办?什么 Required 即可。

const users: Required<User>[] = [];

或者让 api.users.list() 的返回类型就是 Required<User>[] 即可。

如何自己实现 Required

/**
 * Make all properties in T required
 */
type Required<T> = {
    [P in keyof T]-?: T[P];
};

如何让一个类型的所有属性变成只读?

假设我们现在在写一个系统,当用户登录之后,会共享一个 User 对象给所有组件,但是只允许他人读取其值,不允许他人修改,但是我们定义的 User 是整个系统共用的,有编辑功能的页面也在使用这个类型定义,它们是需要能修改用户数据的,那怎么办?

使用 Readonly 即可:

const user = {
  username: "awesomeuser",
  gender: "male",
  age: 19,
  bio: "Aha, insight~",
};

const sharedUser = user as Readonly<User>;

sharedUser.username = "new Name";

此时,IDE 就会告诉你无法分配到 "username" ,因为它是只读属性。

注意:TypeScript 只作静态类型校验,但是并不表示这些值真的不能被修改了,如果真的要创建一个真正从程序上都不能被修改的对象,请问怎么解决?

我想有一个类,只具有另一个类的部分属性定义

还是那个 User,我想定义一个 Login 类型,它专门用于登录时的类型,但是我又不想重新去写一遍 usernamepassword 的类型定义(虽然在本例中,重新写一遍也很简单,但保不齐哪天就会遇到一个十分复杂的类型定义呢?),怎么办?使用 Pick 即可。

type User = {
  username: string;
  gender?: "male" | "female";
  age?: number;
  bio?: string;
  password?: string;
};

type Login = Pick<User, "username" | "password">;

const login: Login = {
  username: "pantao",
};

你们也看到了,Login 类型还是只有 username 是必填的,password 是可选的,这与我们的要求不符,怎么办?加上 Required 啊。

type User = {
  username: string;
  gender?: "male" | "female";
  age?: number;
  bio?: string;
  password?: string;
};

type Login = Required<Pick<User, "username" | "password">>;

const login: Login = {
  username: "pantao",
};

此时就会要求你必须输入 password 了。

如何自己实现 Pick

/**
 * From T, pick a set of properties whose keys are in the union K
 */
type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};

如果快速定义一个类型中,具有相同数据类型的属性?

假设我们现在在开发一个商城系统,每一个 Store 都有一个店长,一个仓库管理员,一个收银员,他们都关联到了 User 上面,此时你可以这样定义 Store 这个类型:

type User = {
  username: string;
  gender?: "male" | "female";
  age?: number;
  bio?: string;
  password?: string;
};

type Store = {
  name: string;
  location: string;
  leader: User;
  warehouseKeeper: User;
  cashier: User;
};

当然,你还可以这样定义:

type Store = Record<'leader' | 'warehouseKeeper' | 'cashier', User> & {
  name: string;
  location: string;
}

两种方式,哪种更好,当然各有优劣,但是假设我们遇到下面这样的一个情况:

type User = {
  username: string;
  gender?: "male" | "female";
  age?: number;
  bio?: string;
  password?: string;
};

const users: User = [
  {
    username: "awesomeuser",
  },
  {
    username: "baduser",
  },
  {
    username: "gooduser",
  },
];

为了访问方便,我想将 users 变量转换成一个属性名称为 users 数组索引,值为 users 数组元素(即 User 类型)的对象,怎么定义这样的一个类型?

你可以这样:

type UserSet = { [key: number]: User };

const userSet: UserSet = users.reduce(
  (set, user, index) => ({ ...set, [index]: user }),
  {} as UserSet
);

你也可以这样:

type UserSet = Record<number, User>;

如何自己实现 Record

/**
 * Construct a type with a set of properties K of type T
 */
type Record<K extends keyof any, T> = {
    [P in K]: T;
};

博客,从时记,到日记,再到周记,然后到月记,现在都成年记了……

你想制造一辆房车,但是不知道从哪里开始,解决这个问题的方法是很多的,思考如何解决问题的方法,最重要的一点是你需要考虑你的预算、你的动手能力以及你希望最终的生活方式。

Install Samba

sudo dnf install samba samba-client
sudo systemctl enable --now {smb,nmb}

Configuring the firewall

sudo firewall-cmd --info-service samba
sudo firewall-cmd --permanent --add-service=samba
sudo firewall-cmd --reload
sudo firewall-cmd --list-services

Update /etc/samba/smb.conf

[global]
    workgroup = SAMBA
    security = user

    passdb backend = tdbsam

    printing = cups
    printcap name = cups
    load printers = yes
    cups options = raw
    map to guest = bad user

[homes]
    comment = Home Directories
    valid users = %S, %D%w%S
    browseable = No
    read only = No
    inherit acls = Yes

[printers]
    comment = All Printers
    path = /var/tmp
    printable = Yes
    create mask = 0600
    browseable = No

[print$]
    comment = Printer Drivers
    path = /var/lib/samba/drivers
    write list = @printadmin root
    force group = @printadmin
    create mask = 0664
    directory mask = 0776

[shared]
    path = /home/pantao/Shared
    guest ok = no
    writable = yes

Setup SELinux for samba

sudo chcon -R -t samba_share_t /mnt/shared
sudo semanage fcontext -a -t samba_share_t "/mnt/shared(/.*)?"
sudo semanage fcontext -l | grep /mnt/shared
sudo setsebool samba_enable_home_dirs=1

Install Google Chrome

  1. Download Google Chrome rpm installer from https://google.com/chrome
  2. cd ~/Downloads
  3. dnf install google-chrome-stable_current_x86_64.rpm
  4. Start google chrome via terminal: google-chrome

Install Postman

  1. Download Postman from https://www.getpostman.com/downloads/
  2. cd ~/Downloads
  3. tar -zxf postman.tar.gz -C /opt
  4. ln -s /opt/Postman/Postman /usr/bin/Postman
  5. Add desktop icon

    [Desktop Entry]
    Encoding=UTF-8
    Name=Postman
    Exec=/opt/Postman/app/Postman %U
    Icon=/opt/Postman/app/resources/app/assets/icon.png
    Terminal=false
    Type=Application
    Categories=Development;

Install latest git

  1. Remove existed git version: sudo dnf remove git
  2. sudo dnf install wget unzip curl
  3. sudo dnf groupinstall "Development Tools"
  4. sudo dnf install curl-devel expat-devel gettext-devel openssl-devel zlib-devel perl-CPAN perl-devel
  5. Download Git source code from https://github.com/git/git/releases
  6. cd ~/Downloads
  7. tar -zxf git-VERSION.tar.gz
  8. sudo make prefix=/usr/local all install
  9. config git

    git config --global user.name "Your Name"
    git config --global user.email "youremail@yourdomain.com"

Install VSCode

  1. Import Microsoft GPG key

    sudo rpm --import https://packages.microsoft.com/keys/microsoft.asc
  2. Add VS Code repository: sudo vi /etc/yum.repos.d/vscode.repo

    [code]
    name=Visual Studio Code
    baseurl=https://packages.microsoft.com/yumrepos/vscode
    enabled=1
    gpgcheck=1
    gpgkey=https://packages.microsoft.com/keys/microsoft.asc
  3. Install Visual Studio Code on CentOS 8

    sudo dnf install code

Install Oracle Java SDK

  1. Download Java SE Development Kit from https://www.oracle.com/
  2. sudo dnf install ~/Downloads/jdk-8u231-linux-x64.rpm

React Hooks 在 2018 年年底就已经公布了,正式发布是在 2019 年 5 月,关于它到底能做什么用,并不在本文的探讨范围之内,本文旨在摸索,如何基于 Hooks 以及 Context,实现多组件的状态共享,完成一个精简版的 Redux。

初始化一个 React 项目

yarn create create-app hooks-context-based-state-management-react-app
cd hooks-context-based-state-management-react-app
yarn start

或者可以直接 clone 本文完成的项目:

git clone https://github.com/pantao/hooks-context-based-state-management-react-app.git

设置我们的 state

绝大多数情况下,我们其实只需要共享会话状态即可,在本文的示例中,我们也就只共享这个,在 src 目录下,创建一个 store/types.js 文件,它定义我们的 action 类型:

// 设置 session
const SET_SESSION = "SET_TOKEN";
// 销毁会话
const DESTROY_SESSION = "DESTROY_SESSION";

export { SET_SESSION, DESTROY_SESSION };

export default { SET_SESSION, DESTROY_SESSION };

接着定义我们的 src/reducers.js

import { SET_SESSION, DESTROY_SESSION } from "./types";

const initialState = {
  // 会话信息
  session: {
    // J.W.T Token
    token: "",
    // 用户信息
    user: null,
    // 过期时间
    expireTime: null
  }
};

const reducer = (state = initialState, action) => {
  console.log({ oldState: state, ...action });

  const { type, payload } = action;
  switch (type) {
    case SET_SESSION:
      return {
        ...state,
        session: {
          ...state.session,
          ...payload
        }
      };
    case DESTROY_SESSION:
      return {
        ...state,
        session: { ...initialState }
      };
    default:
      throw new Error("Unexpected action");
  }
};

export { initialState, reducer };

创建 src/actions.js

import { SET_SESSION, DESTROY_SESSION } from "./types";

export const useActions = (state, dispatch) => {
  return {
    login: async (username, password) => {
      console.log(`login with ${username} & ${password}`);
      const session = await new Promise(resolve => {
        // 模拟接口请求费事 1 秒
        setTimeout(
          () =>
            resolve({
              token: "J.W.T",
              expireTime: new Date("2030-09-09"),
              user: {
                username,
                password
              }
            }),
          1000
        );
      });

      // dispatch SET_TOKEN
      dispatch({
        type: SET_SESSION,
        payload: session
      });

      return session;
    },
    logout: () => {
      dispatch({
        type: DESTROY_SESSION
      });
    }
  };
};

关键时刻,创建 store/StoreContext.js

import React, { createContext, useReducer, useEffect } from "react";
import { reducer, initialState } from "./reducers";
import { useActions } from "./actions";

const StoreContext = createContext(initialState);

function StoreProvider({ children }) {
  // 设置 reducer,得到 `dispatch` 方法以及 `state`
  const [state, dispatch] = useReducer(reducer, initialState);

  // 生成 `actions` 对象
  const actions = useActions(state, dispatch);

  // 打印出新的 `state`
  useEffect(() => {
    console.log({ newState: state });
  }, [state]);

  // 渲染 state, dispatch 以及 actions
  return (
    <StoreContext.Provider value={{ state, dispatch, actions }}>
      {children}
    </StoreContext.Provider>
  );
}

export { StoreContext, StoreProvider };

修改 src/index.js

打开 src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';

ReactDOM.render(<App />, document.getElementById('root'));

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

做如下修改:

import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import * as serviceWorker from "./serviceWorker";
import { StoreProvider } from "./context/StoreContext"; // 导入 StoreProvider 组件

ReactDOM.render(
  <StoreProvider>
    <App />
  </StoreProvider>,
  document.getElementById("root")
);

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

src/App.js

内容如下:

import React, { useContext, useState } from "react";
import logo from "./logo.svg";
import "./App.css";

import { StoreContext } from "./store/StoreContext";
import { DESTROY_SESSION } from "./store/types";

function App() {
  const { state, dispatch, actions } = useContext(StoreContext);

  const [username, setUsername] = useState("");
  const [password, setPassword] = useState("");
  const [loading, setLoading] = useState(false);
  const { user, expireTime } = state.session;

  const login = async () => {
    if (!username) {
      return alert("请输入用户名");
    }
    if (!password) {
      return alert("请输入密码");
    }
    setLoading(true);
    try {
      await actions.login(username, password);
      setLoading(false);
      alert("登录成功");
    } catch (error) {
      setLoading(false);
      alert(`登录失败:${error.message}`);
    }
  };

  const logout = () => {
    dispatch({
      type: DESTROY_SESSION
    });
  };

  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        {loading ? <div className="loading">登录中……</div> : null}
        {user ? (
          <div className="user">
            <div className="field">用户名:{user.username}</div>
            <div className="field">过期时间:{`${expireTime}`}</div>
            <div className="button" onClick={actions.logout}>
              使用 actions.logout 退出登录
            </div>
            <div className="button" onClick={logout}>
              使用 dispatch 退出登录
            </div>
          </div>
        ) : (
          <div className="form">
            <label className="field">
              用户名:
              <input
                value={username}
                onChange={e => setUsername(e.target.value)}
              />
            </label>
            <label className="field">
              密码:
              <input
                value={password}
                onChange={e => setPassword(e.target.value)}
                type="password"
              />
            </label>
            <div className="button" onClick={login}>
              登录
            </div>
          </div>
        )}
      </header>
    </div>
  );
}

export default App;

总结

整个实现我们使用到了 ReactuseContext 共享上下文关系,这个是关系、useEffect 用来实现 reducerloguseReducer 实现 redux 里面的 combineReducer 功能,整体上来讲,实现还是足够绝大多数中小型项目使用的。

关于如何去除一个给定数组中的重复项,应该是 Javascript 面试中最常见的一个问题了,最常见的方式有三种:SetArray.prototype.filter 以及 Array.prototype.reduce,对于只有简单数据的数组来讲,我最喜欢 Set,没别的,就是写起来简单。

const originalArray = [1, 2, '咩', 1, 'Super Ball', '咩', '咩', 'Super Ball', 4]

const bySet = [...new Set(originalArray)]

const byFilter = originalArray.filter((item, index) => originalArray.indexOf(item) === index)

const byReduce = originalArray.reduce((unique, item) => unique.includes(item) ? unique : [...unique, item], [])

使用 Set

先让我们来看看 Set 到底是个啥

Set 对象允许你存储任何类型的唯一值,无论是原始值或者是对象引用。

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Set

  • 首先,Set 中只允许出现唯一值
  • 唯一性是比对原始值或者对象引用

const bySet = [...new Set(originalArray)] 这一段的操作,我们将它拆分来看:

const originalArray = [1, 2, '咩', 1, 'Super Ball', '咩', '咩', 'Super Ball', 4]

const uniqueSet = new Set(originalArray)
// 得到 Set(5) [ 1, 2, "咩", "Super Ball", 4 ]

const bySet = [...uniqueSet]
// 得到 Array(5) [ 1, 2, "咩", "Super Ball", 4 ]

在将 Set 转为 Array 时,也可以使用 Array.from(set)

使用 Array.prototype.filter

要理解 filter 方法为什么可以去重,需要关注一下另一个方法 indexOf

indexOf()方法返回在数组中可以找到一个给定元素的第一个索引,如果不存在,则返回 -1

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf

const beasts = ['ant', 'bison', 'camel', 'duck', 'bison'];

console.log(beasts.indexOf('bison'));
// expected output: 1

// start from index 2
console.log(beasts.indexOf('bison', 2));
// expected output: 4

console.log(beasts.indexOf('giraffe'));
// expected output: -1

filter() 方法创建一个新数组, 其包含通过所提供函数实现的测试的所有元素。

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/filter

filter 方法接受两个参数:

  • 第一个参数:一个回调函数, filter 会将数据中的每一项都传递给该函数,若该函数返回 真值,则数据保存,返回 假值,则数据将不会出现在新生成的数据中
  • 第二个参数:回调函数中 this 的指向

我们将上面的去重方法按下面这样重写一下,就可以看清整个 filter 的执行过程了。

const originalArray = [1, 2, '咩', 1, 'Super Ball', '咩', '咩', 'Super Ball', 4]

const table = []

const byFilter = originalArray.filter((item, index) => {
  // 如果找到的索引与当前索引一致,则保留该值
  const shouldKeep = originalArray.indexOf(item) === index
  table.push({
    序号: index,
    值: item,
    是否应该保留: shouldKeep ? '保留' : '删除'
  })
  return shouldKeep
})

console.log(byFilter)
console.table(table)
序号是否应该保留-
01保留第一次出现
12保留第一次出现
2保存第一次出现
31删除第二次出现
4Super Ball保留第一次出现
5删除第二次出现
6删除第三次出现
7Super Ball删除第二次出现
84保留第一次出现

使用 Array.prototype.reduce

reduce() 方法对数组中的每个元素执行一个由您提供的 reducer 函数(升序执行),将其结果汇总为单个返回值。

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce

Array.prototype.reduce 方法接受两个参数:

  • Callback:回调函数,它可以接收四个参数

    • Accumulator:累计器,这个其实是让很多人忽略的一点,就是,累计器其实可以是任何类型的数据
    • Current Value:当前值
    • Current Index:当前值的索引
    • Source Array:源数组
  • Initial Value:累计器的初始值,就跟累计器一样,这个参数也总是被绝大多数人忽略

就像 filter 章节一样,我们来看看 reduce 的执行过程:

const originalArray = [1, 2, '咩', 1, 'Super Ball', '咩', '咩', 'Super Ball', 4]

const byReduce = originalArray.reduce((unique, item, index, source) => {
  const exist = unique.includes(item)
  const next = unique.includes(item) ? unique : [...unique, item]
  console.group(`遍历第 ${index} 个值`)
  console.log('当前累计器:', unique)
  console.log('当前值:', item)
  console.log('是否已添加进累计器?', exist)
  console.log('新值', next)
  console.groupEnd()
  return next
}, [])