分类 文章 下的文章

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

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

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

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

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

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

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

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

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

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

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

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

  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,那又该如何实现?

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

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

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
}, [])

先来回答一下下面这个问题:对于 setTimeout(function() { console.log('timeout') }, 1000) 这一行代码,你从哪里可以找到 setTimeout 的源代码(同样的问题还会是你从哪里可以看到 setInterval 的源代码)?

很多时候,可以我们脑子里面闪过的第一个答案肯定是 V8 引擎或者其它 VM们,但是要知道的一点是,所有我们所见过的 Javascript 计时函数,都没有出现在 ECMAScript 标准中,也没有被任何 Javascript 引擎实现,计时函数,其实都是由浏览器(或者其它运行时,比如 Node.js)实现的,并且,在不同的运行时下,其表现形式有可能都不一致

在浏览器中,主计时器函数是 Window 接口的一部分,这保证了包括如 setTimeoutsetInterval 等计时器函数以及其它函数和对象能被全局访问,这才是你可以随时随地使用 setTimeout 的原因。同样的,在 Node.js 中,setTimeoutglobal 对象的一部分,这拿得你也可以像在浏览器里面一样,随时随地的使用它。

到现在可能会有一些人感觉这个问题其实并没有实际的价值,但是作为一个 Javascript 开发者,如果不知道本质,那么就有可能不能完全的理解 V8 (或者其它VM)是到底是如何与浏览器或者 Node.js 相互作用的。

暂缓一个函数的执行

计时器函数都是更高阶的函数,它们可以用于暂缓一个函数的执行,或者让一个函数重复执行(由他们的第一个参数执行需要执行的函数)。

下面这是一个暂缓执行的示例:

setTimeout(() => {
  console.log('距离函数的调用,已经过去 4 秒了')
}, 4 * 1000)

在上面的示例中, setTimeoutconsole.log 的执行暂缓了 4 * 1000 毫秒,也就是 4 秒钟, setTimeout 的第一个函数,就是需要暂缓执行的函数,它是一个函数的引用,下面这个示例是我们更加常见到的写法:

const fn = () => {
  console.log('距离函数的调用,已经过去 4 秒了')
}

setTimeout(fn, 4 * 1000)

传递参数

如果被 setTimeout 暂缓的函数需要接收参数,我们可以从第三个参数开始添加需要传递给被暂缓函数的参数:

const fn = (name, gender) => {
  console.log(`I'm ${name}, I'm a ${gender}`)
}

setTimeout(fn, 4 * 1000, 'Tao Pan', 'male')

上面的 setTimeout 调用,其结果与下面这样调用类似:

setTimeout(() => {
  fn('Tao Pan', 'male')
}, 4 * 1000)

但是记住,只是结果类似,本质上是不一样的,我们可以用伪代码来表示 setTimeout 的函数实现:

const setTimeout = (fn, delay, ...args) => {
  wait(delay) // 这里表示等待 delay 指定的毫秒数
  fn(...args)
}

挑战一下

编写一个函数:

  • delay 为 4 秒的时候,打印出:距离函数的调用,已经过去 4 秒了
  • delay 为 8 秒的时候,打印出:距离函数的调用,已经过去 8 秒了
  • delay 为 N 秒的时候,打印出:距离函数的调用,已经过去 N 秒了

下面这个是我的一个实现:

const delayLog = delay => {
  setTimeout(console.log, delay * 1000, `距离函数的调用,已经过去 ${delay} 秒了`)
}

delayLog(4) // 输出:距离函数的调用,已经过去 4 秒了
delayLog(8) // 输出:距离函数的调用,已经过去 8 秒了

我们来理一下 delayLog(4) 的整个执行过程:

  1. delay = 4
  2. setTimeout 执行
  3. 4 * 1000 毫秒后, setTimeout 调用 console.log 方法
  4. setTimeout 计算其第三个参数 距离函数的调用,已经过去 ${delay} 秒了 得到 距离函数的调用,已经过去 4 秒了
  5. setTimeout 将计算得到的字符串当作 console.log 的第一个参数
  6. console.log('距离函数的调用,已经过去 4 秒了') 执行,输出结果

规律性重复一个函数的执行以及停止重复调用

如果我们现在要每 4 秒第印一次呢?这里面就有很多种实现方式了,假如我们还是使用 setTimeout 来实现,我们可以这样做:

const loopMessage = delay => {
  setTimeout(() => {
    console.log('这里是由 loopMessage 打印出来的消息')
    loopMessage(delay)
  }, delay * 1000)
}

loopMessage(1) // 此时,每过 1 秒钟,就会打印出一段消息:*这里是由 loopMessage 打印出来的消息*

但是这样有一个问题,就是开始之后,我们就没有办法停止,怎么办?可以稍稍改改实现:

let loopMessageTimer

const loopMessage = delay => {
  loopMessageTimer = setTimeout(() => {
    console.log('这里是由 loopMessage 打印出来的消息')
    loopMessage(delay)
  }, delay * 1000)
}

loopMessage(1)

clearTimeout(loopMessageTimer) // 我们随时都可以使用 `clearTimeout` 清除这个循环

但是这样还是有问题的,如果 loopMessage 被调用多次,那么他们将共用一个 loopMessageTimer,清除一个,将清除所有,这是肯定不行的,所以,还得再改造一下:

const loopMessage = delay => {
  let timer
  
  const log = () => {
    timer = setTimeout(() => {
      console.log(`每 ${delay} 秒打印一次`)
      log()
    }, delay * 1000)
  }

  log()

  return () => clearTimeout(timer)
}

const clearLoopMessage = loopMessage(1)
const clearLoopMessage2 = loopMessage(1.5)

clearLoopMessage() // 我们在任何时候都可以取消任何一个重复调用,而不影响其它的

这…… 实现是实现了,但是其它有更好的解决办法:

const timer = setInterval(console.log, 1000, '每 1 秒钟打印一次')

clearInterval(timer) // 随时可以 `clearInterval` 清除

更加深入了认识取消计时器(Cancel Timers)

上面的示例只是简单的给我们展现了 setTimeout 以及 setInterval,也看到了,我们可以通过 clearTimeout 或者 clearInterval 取消计时器,但是关于计时器,远远不止这点知识,请看下面的代码(请):

const cancelImmediate = () => {
  const timerId = setTimeout(console.log, 0, '暂缓了 0 秒执行')
  clearTimeout(timerId)
}

cancelImmediate() // 这里并不会有任何输出

或者看下面这样的代码:

const cancelImmediate2 = () => setTimeout(console.log, 0, '暂缓了 0 秒执行')

const timerId = cancelImmediate2()

clearTimeout(timerId)

请将上面的的任一代码片段同时复制到浏览器的控制台中(有多行复制多行)执行,你会发现,两个代码片段都没有任何输出,这是为什么?

这是因为,Javascript 的运行机制导致,任何时刻都只能存在一个任务在进行,虽然我们调用的是暂缓 0 秒,但是,由于当前的任务还没有执行完成,所以,setTimeout 中被暂缓的函数即使时间到了也不会被执行,必须等到当前的任务完全执行完成,那么,再试着,上面的代码分行复制到控制台,看看结果是不是会打印出 暂缓了 0 秒执行 了?答案是肯定的。

当你一行一行复制执行的时候, cancelImmediate2 执行完成之后,当前任务就已经全部执行完成了,所以开始执行下一个任务(console.log 开始执行)。

从上面的示例中,我们可以看出,setTimeout 其实是将一个任务安排进一个 Javascript 的任务队列里面去,当前面的所有任务都执行完成之后,如果这个任务时间到了,那么就立即执行,否则,继续等待计时结束。

此时,你应该发现,只要是 setTimeout 所暂缓的函数没有被执行(任务还没有完成),那么,我们就可以随时使用 clearTimeout 清除掉这个暂缓(将这条任务从队列里面移除)

计时器是没有任何保证的

通过前面的例子,我们知道了 setTimeoutdelay0 时,并不表示立马就会执行了,它必须等到所有的当前任务(对于一个 JS 文件来讲,就是需要执行完当前脚本中的所有调用)执行完成之后都会执行,而这里面就包括我们调用的 clearTimeout

下面用一个示例来更清楚了说明这个问题:

setTimeout(console.log, 1000, '1 秒后执行的')

// 开始时间
const startTime = new Date()
// 距离开始时间已经过去几秒
let secondsPassed = 0
while (true) {
  // 距离开始时间的毫秒数
  const duration = new Date() - startTime
  // 如果距离开始时间超过 5000 毫秒了, 则终止循环
  if (duration > 5000) {
    break
  } else {
    // 如果距离开始时间增长一秒,更新 secondsPassed
    if (Math.floor(duration / 1000) > secondsPassed) {
      secondsPassed = Math.floor(duration / 1000)
      console.log(`已经过去 ${secondsPassed} 秒了。`)
    }
  }
}

你们猜上面这段代码会有什么样的输出?是下面这样的吗?

1 秒后执行的
已经过去 1 秒了。
已经过去 2 秒了。
已经过去 3 秒了。
已经过去 4 秒了。
已经过去 5 秒了。

并不是这样的,而是下面这样的:

已经过去 1 秒了。
已经过去 2 秒了。
已经过去 3 秒了。
已经过去 4 秒了。
已经过去 5 秒了。
1 秒后执行的

怎么会这样?这是因为 while(true) 这个循环必须要执行超过 5 秒钟的时间之后,才算当前所有任务完成,在它 break 之前,其它所有的操作都是没有用的,当然,我们不会在开发的过程中去写这样的代码,但是并不表示就不存在这样的情况,想象以下下面这样的场景:

setTimeout(somethingMustDoAfter1Seconds, 1000)

openFileSync('file more then 1gb')

这里面的 openFileSync 只是一个伪代码,它表示我们需要同步进行一个特别费时的操作,这个操作很有可能会超过 1 秒,甚至更长的时间,但是上面那个 somethingMustDoAfter1Seconds 将一直处于挂起状态,只要这个操作完成,它才有可能执行,为什么叫有可能?那是因为,有可能还有别的任务又会占用资源。所以,我们可以将 setTimeout 理解为:计时结束是执行任务的必要条件,但是不是任务是否执行的决定性因素

setTimeout(somethingMustDoAfter1Seconds, 1000) 的意思是,必须超过 1000 毫秒后,somethingMustDoAfter1Seconds 才允许执行。

再来一个小挑战

那如果我需要每一秒钟都打印一句话怎么办?从上面的示例中,已经很明显的看到了,setTimeout 是肯定解决不了这个问题了,不信我们可以试试下面这个代码片段:

const log = (delay) => {
  timer = setTimeout(() => {
    console.log(`每 ${delay} 秒打印一次`)
    log(delay)
  }, delay * 1000)
}

log(1)

上面的代码是没有任何问题的,在浏览器的控制台观察,你会发现确实每一秒钟都打印了一行,但是再试试下面这样的代码:

const log = (delay) => {
  timer = setTimeout(() => {
    console.log(`每 ${delay} 秒打印一次`)
    log(delay)
  }, delay * 1000)
}

const readLargeFileSync = () => {
  // 开始时间
  const startTime = new Date()
  // 距离开始时间已经过去几秒
  let secondsPassed = 0
  while (true) {
    // 距离开始时间的毫秒数
    const duration = new Date() - startTime
    // 如果距离开始时间超过 5000 毫秒了, 则终止循环
    if (duration > 5000) {
      break
    } else {
      // 如果距离开始时间增长一秒,更新 secondsPassed
      if (Math.floor(duration / 1000) > secondsPassed) {
        secondsPassed = Math.floor(duration / 1000)
        console.log(`已经过去 ${secondsPassed} 秒了。`)
      }
    }
  }
}

log(1)

setTimeout(readLargeFileSync, 1300)

输出结果是:

每 1 秒打印一次
已经过去 1 秒了。
已经过去 2 秒了。
已经过去 3 秒了。
已经过去 4 秒了。
已经过去 5 秒了。
每 1 秒打印一次
  1. 第一秒的时候, log 执行
  2. 第 1300 毫秒时,开始执行 readLargeFileSync 这会需要整整 5 秒钟的时间
  3. 第 2 秒的时候,log 执行时间到了,但是当前任务并没有完成,所以,它不会打印
  4. 第 5 秒的时候, readLargeFileSync 执行完成了,所以 log 继续执行
关于这个具体怎么实现,就不在本文讨论了

最终,到底是谁在调用那个被暂缓的函数?

当我们在一个 function 中调用 this 时,this 关键字会指向当前函数的 caller

function whoCallsMe() {
  console.log('My caller is: ', this)
}

当我们在浏览器的控制台中调用 whoCallsMe 时,会打印出 Window,当在 Node.js 的 REPL 中执行时,会执行出 global,如果我们将 whoCallsMe 设置为一个对象的属性:

function whoCallsMe() {
  console.log('My caller is: ', this)
}

const person = {
  name: 'Tao Pan',
  whoCallsMe
}

person.whoCallsMe()

这会打印出:My caller is: Object { name: "Tao Pan", whoCallsMe: whoCallsMe() }

那么?

function whoCallsMe() {
  console.log('My caller is: ', this)
}

const person = {
  name: 'Tao Pan',
  whoCallsMe
}

setTimeout(person.whoCallsMe, 0)

这会打印出什么?这个很容易被忽视的问题,其实真的值得我们去思考。

请直接将上面这个代码片段复制进浏览器的控制台,看执行的结果:

My caller is:  Window https://pub.ofcrab.com/admin/write-post.php?cid=2952

再打开系统终端,进入 Node.js REPL 中,执行同样的代码,看执行结果:

My caller is:  Timeout {
  _idleTimeout: 1,
  _idlePrev: null,
  _idleNext: null,
  _idleStart: 7052,
  _onTimeout: [Function: whoCallsMe],
  _timerArgs: undefined,
  _repeat: null,
  _destroyed: false,
  [Symbol(refed)]: true,
  [Symbol(asyncId)]: 221,
  [Symbol(triggerId)]: 5
}

回到这句话:当我们在一个 function 中调用 this 时,this 关键字会指向当前函数的 caller,当我们使用 setTimeout 时,这个 caller 是跟当前的运行时有关系的,如果我想 this 总是指向 person 对象呢?

function whoCallsMe() {
  console.log('My caller is: ', this)
}

const person = {
  name: 'Tao Pan'
}
person.whoCallsMe = whoCallsMe.bind(person)

setTimeout(person.whoCallsMe, 0)

结语

标题是写上了 你需要知道的一切都在这里,但是如果有什么没有考虑到了,欢迎大家指出。

本文列举了一些日常会使用到的 Javascript技巧,可以明显提升代码的表现力。

解构赋值

首先,我们来看一下下面这段代码:

const animal = {
  type: {
    mammal: {
      bear: {
        age: 12
      },
      deer: {
        age: 4
      }
    }
  }
}

console.log(animal.type.mammal.bear) // 输出:{ age: 12 }
console.log(animal.type.mammal.deer) // 输出:{ age: 4 }

对象解构赋值

其实我们可以利用解构赋值做得更好:

const animal = {
  type: {
    mammal: {
      bear: {
        age: 12
      },
      deer: {
        age: 4
      }
    }
  }
}

const { bear, deer } = animal.type.mammal
console.log(bear) // 输出:{ age: 12 }
console.log(deer) // 输出:{ age: 4 }

不管上面哪种实现方式,我们都使用的 const,这表示这些被定义的变量不允许再被赋值,我们推荐 在编写 Javascript 代码时,尽可能的使用 const,除非这个变量确实需要被多次赋值,比如,年龄是可以增长的:

const animal = {
  type: {
    mammal: {
      bear: {
        age: 12
      },
      deer: {
        age: 4
      }
    }
  }
}

const { age } = animal.type.mammal.bear

age += 1 // 这里会报错,因为 age 是一个 const 变量

在这种情况下,我们可以将 const 改为 let

const animal = {
  type: {
    mammal: {
      bear: {
        age: 12
      },
      deer: {
        age: 4
      }
    }
  }
}

let { age } = animal.type.mammal.bear

age += 1
console.log(age) // 输出:13

接下来,我们给每一个 animal 增加一个姓名字段 name,然后,同时使用 letconst,任何动物年龄是会增长的,但是姓名不允许修改:

const animal = {
  type: {
    mammal: {
      bear: {
        name: 'Bug',
        age: 12
      },
      deer: {
        name: 'Debug',
        age: 4
      }
    }
  }
}

const { name } = animal.type.mammal.bear
let { age } = animal.type.mammal.bear

age += 1
console.log(age) // 输出:13
console.log(name) // 输出:Bug

数组解构赋值

我们现在有三只动物,有一个数组保存了它们的名字:

const animalNames = ['Bug', 'Debug', 'Bugfix']
const [bug, debug, bugfix] = animalNames
console.log(debug) // 输出:Debug

解构赋值时重命名

我们还有可能有这样的需求,我想同时拿到上面示例中 animal 那个对象中,两只动物的姓名,这个时候我们可以完全按照对象的结构去解构它:

const animal = {
  type: {
    mammal: {
      bear: {
        name: 'Bug',
        age: 12
      },
      deer: {
        name: 'Debug',
        age: 4
      }
    }
  }
}

const { bear: { name }, deer: { name }} = animal.type.mammal

上面的意思是:从 animal.type.mammal 对象中,访问 bear,并拿到它的 name 的值,并赋值给一个 const 变量,变量名为 name,同时再从 deer 中拿到 name 的值,赋值给一个名为 nameconst 变量。

看出问题来了吧? name 被声明了两次,这是不允许的,此时,我们可以在声明时,为两个 name 指定不同的名称:

const animal = {
  type: {
    mammal: {
      bear: {
        name: 'Bug',
        age: 12
      },
      deer: {
        name: 'Debug',
        age: 4
      }
    }
  }
}

const { bear: { name: bearName }, deer: { name: deerName }} = animal.type.mammal
console.log(bearName) // 输出:Bug
console.log(deerName) // 输出:Debug
数组的解构中同样支持重命名。

箭头函数

箭头函数可以大大减少编码工作量,但是它们并非普通函数的完全替代者,先来看看下面的代码:

const animals = ['Bug', 'Debug', 'Bugfix']

animals.forEach(function (animal) {
  console.log(animal)
})

我们使用箭头函数改写上面的代码:

const animals = ['Bug', 'Debug', 'Bugfix']

animals.forEach(animal => {
  console.log(animal)
})

这是一个简单的示例,只是为了证明箭头函数能让我们的代码更清晰可读,编码量也能大大减少,有一个不成文的经验是,一个项目的代码量越少,维护的成本一般情况下,都会越低,那为了证明箭头函数确实有用,我们再来看一个更复杂点的例子:

function multiplyAndAdd(multiply) {
  const pow = multiply ** multiply
  return function (number) {
    return pow + number
  }
}

const result = multipleAndAdd(3)(5) // 等于:3 ** 3 + 5 = 27 + 5 = 32

console.log(result) // 输出:32

用箭头函数再来改写一次:

const multiplyAndAdd = multiply => {
  const pow = multiply ** multiply
  return number => pow + number
}

如果熟练的话,我一般会这么写:

const multiplyAndAdd = multiply => number => multiply ** multiply + number

这里面可以这么阅读:

  • 声明一个 const 值:multiplyAndAdd,它的值为 multiply => number => multiply ** multiply + number,这个都很好理解
  • 这个值是一个箭头函数,该函数接受一个名为 multiply 的参数,返回 number => multiply ** multiply + number
  • 它的返回值还是一个箭头函数,这个箭头函数接受一个 number 参数,返回 multiply ** multiply + number
这么写可能会提升阅读难度,但是确实能节省代码量,但是个人还是不太推荐在多人协作的项目里面大量使用这样的写法。

原文:https://pub.ofcrab.com/press/instant-post-message-between-wechat-mini-program-webview-h5.html

在做 React Native 应用时,如果需要在 App 里面内嵌 H5 页面,那么 H5 与 App 之间可以通过 Webview 的 PostMessage 功能实现实时的通讯,但是在小程序里面,虽然也提供了一个 webview 组件,但是,在进行 postMessage 通讯时,官方文档里面给出了一条很变态的说明:

网页向小程序 postMessage 时,会在特定时机(小程序后退、组件销毁、分享)触发并收到消息。e.detail = { data }data 是多次 postMessage 的参数组成的数组

这里面已经说的很明白了,不管我们从 H5 页面里面 postMessage 多少次,小程序都是收不到的,除非:

  1. 用户做了回退到上一页的操作
  2. 组件销毁
  3. 用户点击了分享

这里面其实我没有完全说对,官方其实说的是 小程序后退,并没有说是用户做回退操作,经过我的实测,确实人家表达得很清楚了,我们通过微信官方的SDK调起的回退也是完全可行的:

wx.miniProgram.navigateBack()

大体思路

从上面的分析和实测中我们可以知道,要实现无需要用户操作即可完成的通讯,第三种情况我们是完全不需要考虑了的,那么来仔细考虑第 1 和第 2 种场景。

第 1 种方式:回退

当我们想通过网页向小程序发送数据,同时还可以回退到上一个页面时,我们可以在 wx.miniProgram.postMessage 之后,立马调用一次 wx.miniProgram.navigateBack(),此时小程序的操作是:

  1. 处理 postMessage 信息
  2. 回退到上一页

我们在处理 postMessage 的时候做一些特殊操作,可以将这些数据保存下来

第 2 种方式:组件销毁

这是我感觉最合适的一种方式,可以让小程序拿到数据,同时还保留在当前页面,只需要销毁一次 webview 即可,大概的流程就是:

  1. 小程序 postMessage
  2. 小程序 navigateTo 将小程序页面导向一个特殊的页面
  3. 小程序的那个特殊页面立马回退到 webview 所在的页面
  4. webview 所在的页面的 onShow 里面,做一次处理,将 webview 销毁,然后再次打开
  5. 触发 onMessage 拿到数据
  6. H5 页面再次被打开

这种方式虽然变态,但是至少可以做到实时拿到数据,同时还保留在当前 H5 页面,唯一需要解决的是,在做这整套操作前,H5 页面需要做好状态的缓存,要不然,再次打开之后,H5 的数据就清空了。

第 1 种方式:通过回退,将数据提交给小程序之后传递给 webview 的上一页面

这种方式实现起来其实是很简单的,我们现在新建两个页面:

sandbox/canvas-by-webapp/index.js

const app = getApp();

Page({
  data: {
    url: '',
    dimension: null,
    mime: '',
  },
  handleSaveTap: function() {
    wx.navigateTo({
      url: '/apps/browser/index',
      events: {
        receiveData: data => {
          console.log('receiveData from web browser: ', data);
          if (typeof data === 'object') {
            const { url, mime, dimension } = data;
            if (url && mime && dimension) {
              this.setData({
                url,
                dimension,
                mime,
              });

              this.save(data);
            }
          }
        }
      }
    })
  },

  save: async function({ url, mime, dimension }) {
    try {
      await app.saveImages([url]);
      app.toast('保存成功!');
    } catch (error) {
      console.log(error);
      app.toast(error.message || error);
    }
  },
});

上面的代码中,核心点,就在于 wx.navigateTo 调用时,里面的 events 参数,这是用来进行与 /apps/browser/index 页面通讯,接收数据用的。

apps/browser/index.js

我省略了绝大多数与本文无关的代码,保存最主要的三个:

Page({
  onLoad() {
    if (this.getOpenerEventChannel) {
      this.eventChannel = this.getOpenerEventChannel();
    }
  },
  handleMessage: function(message) {
    const { action, data } = message;
    if (action === 'postData') {
      if (this.eventChannel) {
        this.eventChannel.emit('receiveData', data);
      }
    }
  },

  handlePostMessage: function(e) {
    const { data } = e.detail;
    if (Array.isArray(data)) {
      const messages = data.map(item => {
        try {
          const object = JSON.parse(item);
          this.handleMessage(object);
          return object;
        } catch (error) {
          return item;
        }
      });

      this.setData({
        messages: [...messages],
      });
    }
  },
})

其实,onLoad 方法中,我们使用了自微信 SDK 2.7.3 版本开始提供的 getOpenerEventChannel 方法,它可以创建一个与上一个页面的事件通讯通道,这个我们会在 handleMessage 中使用。

handlePostMessage 就是被 bindmessagewebview 上面的方法,它用于处理从 H5 页面中 postMessage 过来的消息,由于小程序是将多次 postMessage 的消息放在一起发送过来的,所以,与其它的Webview不同点在于,我们拿到的是一个数组: e.detail.datahandlePostMessage 的作用就是遍历这个数组,取出每一条消息,然后交由 handleMessage 处理。

handleMessage 在拿到 message 对象之后,将 message.actionmessage.data 取出来(*这里需要注意,这是我们在 H5 里面的设计的一种数据结构,你完全可以在自己的项目中设计自己的结构),根据 action 作不同的操作,我在这里面的处理是,当 action === 'postData' 时,就通过 getOpenerEventChannel 得到的消息通道 this.eventChannel 将数据推送给上一级页面,也就是 /sandbox/canvas-by-webapp,但是不需要自己执行 navigateBack ,因为这个需要交由 H5 页面去执行。

H5 页面的实现

我的 H5 主要就是使用 html2canvas 库生成 Canvas 图(没办法,自己在小程序里面画太麻烦了),但是这个不在本文讨论过程中,我们就当是已经生成了 canvas 图片了,将其转为 base64 文本了,然后像下面这样做:

wx.miniProgram.postMessage({
  data: JSON.stringify({
    action: 'postData',
    data: 'BASE 64 IMAGE STRING'
  })
});

wx.miniProgram.navigateBack()

将数据 postMessage 之后,立即 navigateBack() ,来触发一次回退,也就触发了 bindmessage 事件。

使用销毁 webview 实现实时通讯

接下来,咱就开始本文的重点了,比较变态的方式,但是也没想到更好的办法,所以,大家将就着交流吧。

H5 页面的改变

wx.miniProgram.postMessage({
  data: JSON.stringify({
    action: 'postData',
    data: 'BASE 64 IMAGE STRING'
  })
});

wx.miniProgram.navigateTo('/apps/browser/placeholder');

H5 页面只是将 wx.miniProgram.navigateBack() 改成了 wx.miniProgram.navigateTo('/apps/browser/placeholder') ,其它的事情就先都交由小程序处理了。

/apps/browser/placeholder

这个页面的功能其实很简单,当打开它了之后,做一点点小操作,立马回退到上一个页面(就是 webview 所在的页面。

Page({
  data: { loading: true },
  onLoad(options) {

    const pages = getCurrentPages();

    const webviewPage = pages[pages.length - 2];

    webviewPage.setData(
      {
        shouldReattachWebview: true
      },
      () => {
        app.wechat.navigateBack();
      }
    );
  },
});

我们一行一行来看:

const pages = getCurrentPages();

这个可以拿到当前整个小程序的页面栈,由于这个页面我们只允许从小程序的 Webview 页面过来,所以,它的上一个页面一定是 webview 所在的页面:

const webviewPage = pages[pages.length - 2];

拿到 webviewPage 这个页面对象之后,调用它的方法 setData 更新一个值:

    webviewPage.setData(
      {
        shouldReattachWebview: true
      },
      () => {
        app.wechat.navigateBack();
      }
    );

shouldReattachWebview 这个值为 true 的时候,表示需要重新 attach 一次 webview,这个页面的事件现在已经做完了,回到 webview 所在的页面

apps/browser/index.js 页面

我同样只保留最核心的代码,具体的逻辑,我就直接写进代码里面了。

Page({
  data: {
    shouldReattachWebview: false, // 是否需要重新 attach 一次 webview 组件
    webviewReattached: false,     // 是否已经 attach 过一次 webview 了
    hideWebview: false            // 是否隐藏 webview 组件
  },
  onShow() {
    // 如果 webview 需要重新 attach 
    if (this.data.shouldReattachWebview) {
      this.setData(
        {
          // 隐藏 webview
          hideWebview: true,
        },
        () => {
          this.setData(
            {
              // 隐藏之后立马显示它,此时完成一次 webview 的销毁,拿到了 postMessage 中的数据
              hideWebview: false,
              webviewReattached: true,
            },
            () => {
              // 拿到数据之后,处理 canvasData
              this.handleCanvasData();
            }
          );
        }
      );
    }
  },
  // 当 webview 被销毁时,该方法被触发
  handlePostMessage: function(e) {
    const { data } = e.detail;
    if (Array.isArray(data)) {
      const messages = data.map(item => {
        try {
          const object = JSON.parse(item);
          this.handleMessage(object);
          return object;
        } catch (error) {
          return item;
        }
      });

      this.setData({
        messages: [...messages],
      });
    }
  },
  // 处理每一条消息
  handleMessage: function(message) {
    const {action, data} = message
    // 如果 saveCanvas action
    if (action === 'saveCanvas') {
      // 将数据先缓存进 Snap 中
      const { canvasData } = this.data;
      // app.checksum 是我自己封装的方法,计算任何数据的 checksum,我拿它来当作 key
      // 这可以保证同一条数据只会被处理一次
      const snapKey = app.checksum(data);
      // 只要未处理过的数据,才需要再次数据
      if (canvasData[snapKey] !== true) {
        if (canvasData[snapKey] === undefined) {
          // 将数据从缓存进 `snap` 中
          // 这也是我自己封装的一个方法,可以将数据缓存起来,并且只能被读取一次
          app.snap(snapKey, data);
          // 设置 canvasData 中 snapKey 字段为 `false`
          canvasData[snapKey] = false;
          this.setData({
            canvasData,
          });
        }
      }
    }
  },
  // 当 webview 被重新 attach 之后,canvas 数据已经被保存进 snap 中了,
  handleCanvasData: async function handleCanvasData() {
    const { canvasData } = this.data;
    // 从 canvasData 中拿到所有的 key,并过滤到已经处理过的数据
    const keys = Object.keys(canvasData).filter(key => canvasData[key] === false);

    if (keys.length === 0) {
      return;
    }

    for (let i = 0; i < keys.length; i += 1) {
      try {
        const key = keys[i];
        const { url } = app.snap(key);
        // 通过自己封装的方法,将 url(也就是Base64字符)保存至相册
        const saved = await app.saveImages(url);
        // 更新 canvasData 对象
        canvasData[key] = true
        this.setData({
          canvasData
        })
        console.log('saved: ', saved);
      } catch (error) {
        app.toast(error.message);
        return;
      }
    }
  },
})

对应的 index.wxml 文件内容如下:

<web-view src="{{src}}" wx:if="{{src}}" bindmessage="handlePostMessage" wx:if="{{!hideWebview}}" />

流程回顾与总结

  1. 打开 webview 页面,打开 h5
  2. h5 页面生成 canvas 图,并转为 base64 字符
  3. 通过 wx.miniProgram.postMessagebase64 发送给小程序
  4. 调用 wx.miniProgram.navigateTo 将页面导向一个特殊页面
  5. 在特殊页面中,将 webview 所在页面的 shouldReattachWebview 设置为 true
  6. 在特殊页面中回退至 webview 所在页面
  7. webview 所在页面的 onShow 事件被触发
  8. onShow 事件检测 shouldReattachWebview 是否为 true,若为 true
  9. hideWebview 设置为 true,引起 web-view 组件的销毁
  10. handlePostMessage 被触发,解析所有的 message 之后交给 handleMessage 逐条处理
  11. handleMessage 发现 action === 'saveCanvas' 的事件,拿到 data
  12. 根据 data 计算 checksum ,以 checksumkey 缓存下来数据,并将这个 checksum 保存到 canvasData 对象中
  13. 此时 hideWebviewonShow 里面 setData 的回调中的 setData 重新置为 falseweb-view 重新加 attach,H5页面重新加载
  14. webview 重新 attach 之后, this.handleCanvasData 被触发,
  15. handleCanvasData 检测是否有需要保存的 canvas 数据,如果有,保存,修改 canvasData 状态

整个流程看旧去很繁琐,但是写起来其实还好,这里面最主要的是需要注意,数据去重,微信的 postMessage 里面拿到的永远 都是 H5 页面从被打开到关闭的所有数据。

原文:https://pub.ofcrab.com/press/react-native-deep-linking-for-ios-android.html
代码:https://github.com/pantao/react-native-deep-linking-example

我们生活在一个万物兼可分享的年代,而分享的过程,几乎最终都会分享某一个链接,那么,作为开发者,最常遇到的问题中应该包括如何通过一个URL地址快速的打开App,并导航至特定的页面。

什么是深度链接(Deep Link)

深度链接是一项可以让一个App通过一个URL地址打开,之后导航至特定页面或者资源,或者展示特定UI的技术,Deep 的意思是指被打开的页面或者资源并不是App的首页,最常使用到的地方包括但远远不限于 Push Notification、邮件、网页链接等。

其实这个技术在很久很久以前就已经存在了,鼠标点击一下 mailto:pantao@parcmg.com 这样的链接,系统会打开默认的邮件软件,然后将 pantao@parcmg.com 这个邮箱填写至收件人输入栏里,这就是深度链接。

本文将从零开始创建一个应用,让它支持通过一个如 deep-linking://articles/{ID} 这样的 URL 打开 文章详情 页面,同时加载 {ID} 指定的文章,比如:deep-linking://articles/4 将打开 ID4 的文章详情页面。

深度链接解决了什么问题?

网页链接是无法打开原生应用的,如果一个用户访问你的网页中的某一个资源,他的手机上面也已经安装了你的应用,那么,我们要如何让系统自动的打开应用,然后在应用中展示用户所访问的那一个页面中的资源?这就是深度链接需要解决的问题。

实现深度链接的不同方式

有两种方式可以实现深度链接:

  • URL scheme
  • Universal links

前端是最常见的方式,后者是 iOS 新提供的方式,可以一个普通的网页地址链接至App的特定资源。

本文将创建一个名为 DeepLinkingExample 的应用,使得用户可以通过打开 deep-linking://home 以及 deep-linking://articles/4 分别打开 App 的首页以及 App 中 ID 为 4 的文章详情页面。

react-native init DeepLinkingExample
cd DeepLinkingExample

安装必要的库

紧跟 TypeScript 大潮流,我们的 App 写将使用 TypeScript 开发。

yarn add react-navigation react-native-gesture-handler
react-native link react-native-gesture-handler

我们将使用 react-navigation 模块作为 App 的导航库。

添加 TypeScript 相关的开发依赖:

yarn add typescript tslint tslint-react tslint-config-airbnb tslint-config-prettier ts-jest react-native-typescript-transformer -D
yarn add @types/jest @types/node @types/react @types/react-native @types/react-navigation @types/react-test-renderer

添加 tsconfig.json

{
  "compilerOptions": {
    "target": "es2017",                       /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */
    "module": "es2015",                       /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
    "lib": [                                  /* Specify library files to be included in the compilation:  */
      "es2017",
      "dom"
    ],
    "resolveJsonModule": true,
    "allowJs": false,                         /* Allow javascript files to be compiled. */
    "skipLibCheck": true,                     /* Skip type checking of all declaration files. */
    "jsx": "react-native",                    /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
    "declaration": true,                      /* Generates corresponding '.d.ts' file. */
    "sourceMap": true,                        /* Generates corresponding '.map' file. */
    "outDir": "./lib",                        /* Redirect output structure to the directory. */
    "removeComments": true,                   /* Do not emit comments to output. */
    "noEmit": true,                           /* Do not emit outputs. */

    /* Strict Type-Checking Options */
    "strict": true,                           /* Enable all strict type-checking options. */
    "noImplicitAny": true,                    /* Raise error on expressions and declarations with an implied 'any' type. */
    "strictNullChecks": true,                 /* Enable strict null checks. */
    "strictFunctionTypes": true,              /* Enable strict checking of function types. */
    "noImplicitThis": true,                   /* Raise error on 'this' expressions with an implied 'any' type. */
    "alwaysStrict": true,                     /* Parse in strict mode and emit "use strict" for each source file. */

    /* Additional Checks */
    "noUnusedLocals": true,                   /* Report errors on unused locals. */
    "noUnusedParameters": true,               /* Report errors on unused parameters. */
    "noImplicitReturns": true,                /* Report error when not all code paths in function return a value. */
    "noFallthroughCasesInSwitch": true,       /* Report errors for fallthrough cases in switch statement. */

    /* Module Resolution Options */
    "moduleResolution": "node",               /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
    "baseUrl": "./",                          /* Base directory to resolve non-absolute module names. */
    "paths": {                                /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
      "*": [
        "*.android",
        "*.ios",
        "*.native",
        "*.web",
        "*"
      ]
    },
    "typeRoots": [                            /* List of folders to include type definitions from. */
      "@types",
      "../../@types"
    ],
    // "types": [],                           /* Type declaration files to be included in compilation. */
    "allowSyntheticDefaultImports": true,     /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
    // "preserveSymlinks": true,              /* Do not resolve the real path of symlinks. */

    /* Experimental Options */
    "experimentalDecorators": true,           /* Enables experimental support for ES7 decorators. */
    "emitDecoratorMetadata": true             /* Enables experimental support for emitting type metadata for decorators. */
  },
  "exclude": [
    "node_modules",
    "web"
  ]
}

添加 tslint.json 文件

{
  "defaultSeverity": "warning",
  "extends": [
    "tslint:recommended", 
    "tslint-react",
    "tslint-config-airbnb",
    "tslint-config-prettier"
  ],
  "jsRules": {},
  "rules": {
    "curly": false,
    "function-name": false,
    "import-name": false,
    "interface-name": false,
    "jsx-boolean-value": false,
    "jsx-no-multiline-js": false,
    "member-access": false,
    "no-console": [true, "debug", "dir", "log", "trace", "warn"],
    "no-empty-interface": false,
    "object-literal-sort-keys": false,
    "object-shorthand-properties-first": false,
    "semicolon": false,
    "strict-boolean-expressions": false,
    "ter-arrow-parens": false,
    "ter-indent": false,
    "variable-name": [
      true,
      "allow-leading-underscore",
      "allow-pascal-case",
      "ban-keywords",
      "check-format"
    ],
    "quotemark": false
  },
  "rulesDirectory": []
}

添加 .prettierrc 文件:

{
  "parser": "typescript",
  "printWidth": 100,
  "semi": false,
  "singleQuote": true,
  "trailingComma": "all"
}

编写我们的应用

在项目根目录下创建一个 src 目录,这个将是项目原代码的目录。

添加 src/App.tsx 文件

import React from 'react'

import { createAppContainer, createStackNavigator } from 'react-navigation'

import About from './screens/About'
import Article from './screens/Article'
import Home from './screens/Home'

const AppNavigator = createStackNavigator(
  {
    Home: { screen: Home },
    About: { screen: About, path: 'about' },
    Article: { screen: Article, path: 'article/:id' },
  },
  {
    initialRouteName: 'Home',
  },
)

const prefix = 'deep-linking://'

const App = createAppContainer(AppNavigator)

const MainApp = () => <App uriPrefix={prefix} />

export default MainApp

添加 src/screens/Home.tsx 文件

import React from 'react';

添加 src/screens/About.tsx

import React from 'react'

import { StyleSheet, Text, View } from 'react-native'

import { NavigationScreenComponent } from 'react-navigation'

interface IProps {}

interface IState {}

const AboutScreen: NavigationScreenComponent<IProps, IState> = props => {
  return (
    <View style={styles.container}>
      <Text style={styles.title}>About Page</Text>
    </View>
  )
}

AboutScreen.navigationOptions = {
  title: 'About',
}

export default AboutScreen

const styles = StyleSheet.create({
  container: {},
  title: {},
})

添加 src/screens/Article.tsx

import React from 'react'

import { StyleSheet, Text, View } from 'react-native'

import { NavigationScreenComponent } from 'react-navigation'

interface NavigationParams {
  id: string
}

const ArticleScreen: NavigationScreenComponent<NavigationParams> = ({ navigation }) => {
  const { params } = navigation.state

  return (
    <View style={styles.container}>
      <Text style={styles.title}>Article {params ? params.id : 'No ID'}</Text>
    </View>
  )
}

ArticleScreen.navigationOptions = {
  title: 'Article',
}

export default ArticleScreen

const styles = StyleSheet.create({
  container: {},
  title: {},
})

配置 iOS

打开 ios/DeepLinkingExample.xcodeproj

open ios/DeepLinkingExample.xcodeproj

点击 Info Tab 页,找到 URL Types 配置,添加一项:

  • identifier:deep-linking
  • URL Schemes:deep-linking
  • 其它两项留空

打开项目跟目录下的 AppDelegate.m 文件,添加一个新的 import

#import "React/RCTLinkingManager.h"

然后在 @end 前面,添加以下代码:

- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation {
  return [RCTLinkingManager application:application openURL:url sourceApplication:sourceApplication annotation:annotation];
}

至此,我们已经完成了 iOS 的配置,运行并测试是否成功。

react-native run-ios

打开 simulator 之后,打开 Safari 浏览器,在地址栏中输入:deep-linking://article/4 ,你的应用将会自动打开,并同时进入到 Article 页面。

同样的,你还可以在命令行工具中执行以下命令:

xcrun simctl openurl booted deep-linking://article/4

配置 Android

要为Android应用也创建 External Linking,需要创建一个新的 intent,打开 android/app/src/main/AndroidManifest.xml,然后在 MainActivity 节点添加一个新的 intent-filter

<application ...>
  <activity android:name=".MainActivity" ...>
    ...
    <intent-filter>
      <action android:name="android.intent.action.VIEW" />
      <category android:name="android.intent.category.DEFAULT" />
      <category android:name="android.intent.category.BROWSABLE" />
      <data android:scheme="deep-linking" />
    </intent-filter>
    ...
  </activity>
</application>

Android 只需要完成上面的配置即可。

执行:

react-native run-android

打开系统浏览器,输入:

deep-linking://article/4

系统会自动打开你的应用,并进入 Article 页面

也可以在命令行工具中使用以下命令打开:

adb shell am start -W -a android.intent.action.VIEW -d "deep-linking://article/3" com.deeplinkingexample;

附录

点击以下链接即可:

Javascript 总是以超自然的方式执行我们的代码,这是一件很神奇的事情,如果不信的话,思考一下 ['1', '7', '11'].map(parseInt) 的结果是什么?你以为会是 [1, 7, 11] 吗?我都这么问了,那肯定不是:

['1', '7', '11'].map(parseInt)
// 输出:(3) [1, NaN, 3]

要理解为什么为会这里,首先需要了解一些 Javascript 概念,如果你是一个不太喜欢阅读长文的人,那直接跳到最后看结论吧

真值与假值

请看下面示例:

if (true) {
  // 这里将启动都会被执行
} else {
  // 这里永远都不会被执行
}

在上面的示例中, if-else 声明为 true,所以 if-block 会一直都执行,而 else-block 永远都会被忽略掉,这是个很容易理解的示例,下面我们来看看另一个例子:

if ("hello world") {
  // 这里会被执行吗?
  console.log("条件判断为真");
} else {
  // 或者会执行这里吗?
  console.log("条件判断为假");
}

打开你的开发者工具 console 面板,执行上面的代码,你会得到看到会打印出 条件判断为真,也就是说 "hello world" 这一个字符串被认为是真值

在 JavaScript 中,truthy(真值)指的是在布尔值上下文中,转换后的值为真的值。所有值都是真值,除非它们被定义为 假值(即除 false、0、""、null、undefined 和 NaN 以外皆为真值)。

引用自:Mozilla Developer: Truthy(真值)

这里一定要划重点:在布尔上下文中,除 false0""(空字符串)、nullundefined 以及 NaN 外,其它所有值都为真值

基数

0 1 2 3 4 5 6 7 8 9 10

当我们从 0 数到 9,每一个数字的表示符号都是不一样的(0-9),但是当我们数到 10 的时候,我们就需要两个不同的符号 10 来表示这个值,这是因为,我们的数学计数系统是一个十进制的。

基数,是一个进制系统下,能使用仅仅超过一个符号表示的数字的最小值,不同的计数进制有不同的基数,所以,同一个数字在不同的计数体系下,表示的真实数据可能并不一样,我们来看一下下面这张在十进制、二进制以及十六进制不同值在具体表示方法:

十进制(基数:10)二进制(基数:2)十六进制(基数:16)
000
111
2102
3113
41004
51015
61106
71117
810008
910019
101010A
111011B
121100C
131101D
141110E
151111F
161000010
171000111
181000212

你应该已经注意到了, 11 在上表中,总共出现了三次。

函数的参数

在 Javascript 中,一个函数可以传递任何多个数量的参数,即使调用时传递的数量与定义时的数量不一致。缺失的参数会以 undefined 作为实际值传递给函数体,然后多余的参数会直接被忽略掉(但是你还是可以在函数体内通过一个类数组对象 arguments 访问到)。

function sum(a, b) {
  console.log(a);
  console.log(b);
}

sum(1, 2);    // 输出:1, 2
sum(1);       // 输出:1, undefined
sum(1, 2, 3); // 输出:1, 2

map()

快要有结果了。

map() 是一个存在于数组原型链上的方法,它将其数组实例中的每一个元素,传递给它的第一个参数(在本文最开始的例子中就是 parseInt),然后将每一个返回值都保存到同一个新的数组中,遍历完所有元素之后,将新的包含了所有结果的数组返回。

function multiplyBy3 (number) {
  return number * 3;
}

const result = [1, 2, 3, 4, 5].map(multiplyBy3);

console.log(result); // 输出:[3, 6, 9, 12, 15]

现在,假想一下,我们想使用 map() 将一个数组中的每一个元素都打印到控制台,我可以直接将 console.log 函数传递给 map() 方法:

[1, 2, 3, 4, 5].map(console.log);

输出:

1 0 (5) [1, 2, 3, 4, 5]
2 1 (5) [1, 2, 3, 4, 5]
3 2 (5) [1, 2, 3, 4, 5]
4 3 (5) [1, 2, 3, 4, 5]
5 4 (5) [1, 2, 3, 4, 5]

是不是感觉有点神奇了?为什么会这样?来分析一下上面输出的内容都有些什么?

  • 第一列:这个看上去跟我们想要的输出是一致的
  • 第二列:这个是一个从 0 开始递增的数字
  • 第三列:总是 (5) [1, 2, 3, 4, 5]

再来看看下面这段代码:

[1, 2, 3, 4, 5].map((value, index, array) => console.log(value, index, array));

在控制台上试试执行上面这行代码,你会发现,它与 [1, 2, 3, 4, 5].map(console.log); 输出的结果是完全一致的,**总是会被我们忽略的一点是,map() 方法会将三个参数传递传递给它的函数,分别是 currentValue(当前值)、currentIndex(当前索引)以及 array 本身,这就是,上面为什么结果有三列的原因,如果我们只是想将每一个值打印,那么需要像下面这样写:

[1, 2, 3, 4, 5].map((value) => console.log(value));

回到最开始的问题

现在让我们来回顾一下本文最开始的问题:

为什么 ['1', '7', '11'].map(parseInt) 的结果是 [1, NaN, 3]

来看看 parseInt() 是一个什么样的函数:

parseInt(string, radix)将一个字符串 string 转换为 radix 进制的整数, radix 为介于 2-36 之间的数。

详情:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/parseInt

这里还有一条需要注意的是,虽然我们一般都是使用 parseInt('11') 这样的调用方式,认为是将 11 按十进制转换成数字,但是,请在使用的时候,永远都添加 radix 参数,因为, radix10 并不保证永远有效(这并不是规范的一部分)。

那么看看下面这些的输出:

parseInt('11');             // 输出:11
parseInt('11', 2);          // 输出:3
parseInt('11', 16);         // 输出:17

parseInt('11', undefined);  // 输出:11 (radix 是假值)
parseInt('11', 0);          // 输出:11 (radix 是假值)

现在,让我们一步一步来看 ['1', '7', '11'].map(parseInt) 的整个执行过程,首先,我们可以将这一行代码,转换为完整的版本:

['1', '7', '11'].map((value, index, array) => parseInt(value, index, array))

根据前面的知识,我们应该知道, parseInt(value, index, array) 中的 array 会被忽略(并放入 arguments 对象中。

那么,完整的代码就是下面这样的:

['1', '7', '11'].map((value, index, array) => parseInt(value, index))
  • 遍历到第一个元素时:

    parseInt('1', 0);

    radix0,是一个假值,在我们的控制台中,一般将使用 10 进制,所有,得到结果 1

  • 遍历到第二个元素时:

    parseInt('7', 1)

    在一个 1 进制系统中,7 是不存在的,所以得到结果 NaN(不是一个数字)

  • 遍历到第三个元素时:

    parseInt('11', 2)

    在一个 2 进制系统中,11 就是进十制的 3

总结

['1', '7', '11'].map(parseInt) 的结果是 [1, NaN, 3] 的原因是因为,map() 方法是向传递给他的函数中传递三个参数,分别为当前值,当前索引以及整个数组,而 parseInt 函数接收两个参数:需要转换的字符串,以及进制基数,所以,整个语句可以写作:['1', '7', '11'].map((value, index, array) => parseInt(value, index, array))arrayparseInt 舍弃之后,得到的结果分别是:parseInt('1', 0)parseInt('7', 1) 以及 parseInt('11', 2),也就是上面看到的 [1, NaN, 3]

正确的写法应该是:

['1', '7', '11'].map(numStr => parseInt(numStr, 10));

自己的一句话理解:JSON Web Token 并不是一种认证方式,他只是认证信息的载体。

基于自己的理解,谈谈 JSON Web Token 的一些事儿。

Session 认证

  1. 用户向服务器A发送用户名和密码
  2. 服务器验证通过后,在当前会话里保存相关数据,比如登录时间、过期时间,用户角色等,并生成一个 session_id
  3. 服务器响应用户登录时,将 session_id 写入 cookies
  4. 用户随后发送的每一次请求都会将 session_id 传回服务器
  5. 服务器在处理请求之前,先拿到 sesion_id ,然后找到前面创建的会话信息

这种模式最简单,但是它的扩展性(Scaling)不好,如果是多台服务器,还需要考虑到所有服务器都能读取会话,那么会话就需要在多台服务器之间共享了,然后,如果有跨域的话,还需要保证 Cookie 在多个域下都是可访问的,这种方式看起来,确实就很麻烦了。

客户端保存会话数据

这种方式比 Session 实现起来就简单得多了:

  1. 用户还是发起登录请求
  2. 服务器验证通过之后生成会话数据
  3. 服务器直接把会话数据发送给请求方
  4. 请求方之后去请求任何服务的时候,自己带上就可以了
  5. 服务器接收到请求之后,自己解析会话数据,然后接下步处理即可

那么,这种方法,需要关注的问题并不是会话数据如何共享的问题了,要关注的点是:

  1. 我如何解析会话数据?
  2. 我如何认定客户端发送的会话数据是合法的?

JSON Web Token(JWT)的原理

JWT 就是为解决上面这个问题的,它首先能保存下很多会话数据,其次,它还提供了数据校验机制,下面我们来看看它的原理。

用户在登录时,服务器校验成功之后,生成一个 JSON 对象,比如:

{
  "id": 1,
  "name": "pantao",
  "role": "manager",
  "loginTime": "2019-08-01"
}

上面的这个 JSON 对象保存下了当前登录用户的ID、姓名、角色以及登录时间,以后的客户端在请求任何服务时,都应该要带上这些信息。

当然,真正的 JWT 肯定不止是保存这些数据就足够了的,上面这样的信息,只是载体(Payload),也就是认证的数据本身,但是我们还需要提供一些别的信息,来帮助服务器确定这条数据是合法的(你不能随便造一条这样的信息我就认为你是真实的吧?),在完整的 JWT 中,还会带有另外两种数据:

  • Header:头部信息,也是一个 JSON 对象,用于描述当前这个 JWT 数据是什么的元数据,比如通常是下面这样的:

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

    在上面的代码中, algalgorithm 的缩写,表示了当前这个JWT使用了什么签名算法,默认就是 HMAC SHA256,缩写就是 HS256typ 属性表示了,这个是一个 JWT 类型的 token

  • Signature:这部分是对 Header 跟 Payload 两部分的签名字符串,在生成这个字符串的时候,服务器端会有一个 Secret 密钥,这使得,除了服务器自己,别人是没有办法生成正确的签名的,所以,即使前面两个内容可以随意的造,别人也没有黑涩会生成正确的签名,那么数据是否合法,最主要就是通过这个内容了。

整个认证流程就是:

  1. 客户端拿到 JWT 数据之后,在以后的请求里面带上 JWT 信息
  2. 服务器先校验这个JWT是不是自己生成的(根据 Header, Payload 以及自己保存的 Secret 再计算一次 Signature 看是不是跟用户传进来的一致就成了)
  3. 如果是自己生成的,则解析 Payload 部分,拿到上面所设计的载体JSON对象,这里面就保存了我们的会话信息
  4. 拿到会话信息后进行进一步

Base64URL

上面说了 Header.Payload.Signature 这样三段式的格式,客户端收到这样的一个 TOKEN 之后,可能是通过 COOKIE 发送服务器,也可能是通过 Header,还可能是通过 POST 请求的Payload中的一个字段,也可能是 URL 里面的查询参数,为了保证在各种不同的一方传输的过程中都通用,所以,我们肯定不能直接把两个JSON字符串以及一个签名字符串用两个 . 连起来就用,这里面就用到了 Base64URL 的算法。

在 Base64 算法里面,有三个字符 += 以及 / 是有特殊含义的,Base64URL 算法就是,将字符串按 Base64 处理之后,再将 = 省略,将 + 换成 - ,将 / 替换成 _,看个示例:

const jwt = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ7XCJ1c2VySWRcIjpcIjExNTE4NjA4NjM5NTA0NTg4ODFcIixcInBob25lXCI6XCIxODM3NDUwMDk5OVwiLFwiYXBwSURcIjpcInd4YTJhY2IwZGVhODgyYmNmN1wiLFwib3BlbklkXCI6XCJvckpxcDVRMlo3U3V4Y3Jxa3dmelJNZ2RpVGRJXCIsXCJ1bmlvbklkXCI6XCJvZ0FtLTFBbm9mMU1rVzdtZEY3bGVsalZDcURvXCIsXCJjaXR5XCI6XCJcIixcImNvdW50cnlcIjpcIkNoaW5hXCIsXCJnZW5kZXJcIjpcIlwiLFwibmlja05hbWVcIjpcIuWkp-iDoeWtkOWGnOawkeW3pea9mOWNiuS7mVwiLFwicHJvdmluY2VcIjpcIlwiLFwiYXZhdGFyVXJsXCI6XCJodHRwczovL3d4LnFsb2dvLmNuL21tb3Blbi92aV8zMi9EWUFJT2dxODNlckhLVTNPdEk3WUliazB1NmliQlA2eTdZeDZpY2dwbXpUdWRPbEVQeHUydldpYmhudlhwWmlhSndpYjhjcEpOVjRaUFRtbERjb09vMnR5Q2ljQS8xMzJcIn0iLCJpc3MiOiJkZXZlbG9wIiwiZXhwIjoxNTY4MDc2ODcxLCJpYXQiOjE1NjU0ODQ4NzF9.kta-7LP7dIEbWYILDfw93aiKg1FRC4IOAajsUzSXeUY';

上面这个就是一个完整的 JWT 字符串,如何能解析出里面的数据呢?很简单:

  1. . 号分割字符串
  2. 将第0与第1项里面的 - 号换成 + 号,_ 号换成 / 号,
  3. 再使用 atob 将字符串从 base64 转成正常的字符
const [header, payload] = jwt.split('.').slice(0,2).map(s => s.replace(/-/gi, '+').replace(/_/gi, '/')).map(s => atob(s));

// header = {typ: "JWT", alg: "HS256"}
// payload = {"sub":"{\"userId\":\"1151860863950458881\",\"phone\":\"18374500999\",\"appID\":\"wxa2acb0dea882bcf7\",\"openId\":\"orJqp5Q2Z7SuxcrqkwfzRMgdiTdI\",\"unionId\":\"ogAm-1Anof1MkW7mdF7leljVCqDo\",\"city\":\"\",\"country\":\"China\",\"gender\":\"\",\"nickName\":\"大胡子农民工潘半仙\",\"province\":\"\",\"avatarUrl\":\"https://wx.qlogo.cn/mmopen/vi_32/DYAIOgq83erHKU3OtI7YIbk0u6ibBP6y7Yx6icgpmzTudOlEPxu2vWibhnvXpZiaJwib8cpJNV4ZPTmlDcoOo2tyCicA/132\"}","iss":"develop","exp":1568076871,"iat":1565484871}

要转成对象的话,再 JSON.parse() 一下就可以了:

const [header, payload] = jwt.split('.').slice(0,2).map(s => s.replace(/-/gi, '+').replace(/_/gi, '/')).map(s => atob(s)).map(s => JSON.parse(s));

可以得到下面这样结构的对象:

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

{
  "sub": "{\"userId\":\"1151860863950458881\",\"phone\":\"18374500999\",\"appID\":\"wxa2acb0dea882bcf7\",\"openId\":\"orJqp5Q2Z7SuxcrqkwfzRMgdiTdI\",\"unionId\":\"ogAm-1Anof1MkW7mdF7leljVCqDo\",\"city\":\"\",\"country\":\"China\",\"gender\":\"\",\"nickName\":\"大胡子农民工潘半仙\",\"province\":\"\",\"avatarUrl\":\"https://wx.qlogo.cn/mmopen/vi_32/DYAIOgq83erHKU3OtI7YIbk0u6ibBP6y7Yx6icgpmzTudOlEPxu2vWibhnvXpZiaJwib8cpJNV4ZPTmlDcoOo2tyCicA/132\"}",
  "iss": "develop",
  "exp": 1568076871,
  "iat": 1565484871
}

可以看到, Payload 段中的 sub ,应该也是一个 JSON 字符串,再解析一下即可:

{
  "userId": "1151860863950458881",
  "phone": "18374500999",
  "appID": "wxa2acb0dea882bcf7",
  "openId": "orJqp5Q2Z7SuxcrqkwfzRMgdiTdI",
  "unionId": "ogAm-1Anof1MkW7mdF7leljVCqDo",
  "city": "",
  "country": "China",
  "gender": "",
  "nickName": "大胡子农民工潘半仙",
  "province": "",
  "avatarUrl": "https://wx.qlogo.cn/mmopen/vi_32/DYAIOgq83erHKU3OtI7YIbk0u6ibBP6y7Yx6icgpmzTudOlEPxu2vWibhnvXpZiaJwib8cpJNV4ZPTmlDcoOo2tyCicA/132"
}

虽然我们可以解析出 Header 跟 Payload,但是只要我们对其修改之后再发送回服务器,内容一改,签名就会改,所以,服务器直接就认定这是假的 TOKEN了

安全性

  • JWT 默认是不加密的,但是,我们也可以对生成之后的 Header 与 Payload 字符串进行一次加密,这样客户端就无法直接解析出明文了,只是在接收时,在进行 JWT校验之前,先对 Header 与 Payload 密文进行一次解密即可
  • JWT 不加密的情况下,不能将私密数据写入 JWT
  • JWT 除了认证外,还可以用于数据交换,可以大大减少查询数据库的次数
  • 如果服务器端不做特殊的其它逻辑,那么一个JWT签发之后,除非它过期,否则将永远有效,如果权限记录在 JWT 中,那么,就算修改了某个用户的权限,在签发给他的TOKEN过期前,他的权限还将是上一次签发的,由于这条特性,对于重要的操作,比如转帐等,应该进行二次确认。
  • 应该使用 HTTPS 协议传输数据

定义 Packages

应该在文件的最顶部定义包名

package my.demo

import java.util.*

//...
文件名与包名并不一定必须一致。

定义函数

定义一个接口两个数字,返回它们和的函数

fun sum(a: Int, b: Int): Int {
  return a + b
}

只有一个表达式作为主体的函数,Kotlin 会自动推断出它的返回类型

fun sum(a: Int, b: Int) = a + b

函数默认会返回无具体意义的值

fun printSum(a: Int, b: Int): Unit {
  println("Sum of $a and $b is ${a + b}")
}

Unit 类型是可以被省略的

fun printSum(a: Int, b: Int) {
  println("Sum of $a and $b is ${a + b}")
}

定义变量

使用 val 定义一个本地只读的只能被赋值一次的变量

val a: Int = 1 // 立即赋值
val b = 2 // `Int` 类型是被推断出来的
val c: Int // 先定义一个 `Int` 类型的变量
c = 3 // 然后再赋值

也可以使用 var 定义一个可以被多次赋值的变量

var x = 5
x += 1

上层变量

val PI = 3.14
var x = 0

fun incrementX() {
  x += 1
}

注释

就如同 Java、JavaScript,Kotlin 同时支持文件行末注释以及块注释:

// 这是一个行末注释

/* 这是一个
  块级注释 */
与 Java 不同点在于,Kotlin 的块注释支持嵌套

使用字符模板

var a = 1
// 模板中和简单名称
val s1 = "a 的值为 $a"

a += 1

// 在模板中的任意格式
val s2 = "${s1.replace("为", "以前为")},但是现在为 $a"

使用条件表达式

fun maxOf(a: Int, b: Int): Int {
  if (a > b) {
    return a
  } else {
    return b
  }
}

使用 if 表达示

fun maxOf(a: Int, b: Int) = if (a > b) a else b

使用可为 null 的值以及对 null 的检查

对于一个可为 null 的值,必须显示的标记它可能为 nullnullable

fun parseInt(str: String): Int? {
  // ...
}

在函数中返回可为 null 的值

fun printProduct(arg1: String, arg2: String) {
  val x = parseInt(arg1)
  var y = parseInt(arg2)

  // 使用 `x * y` 的话,会报错误,因为它们有可能为 `null`
  if (x != null & y != null) {
    // 在 `null` 检查完成之后,`x` 与 `y` 会自动的被转为不会为 `null`(`non-nullable`) 的值
    println(x * y)
  }
  else {
    println("'$arg1' 与 '$arg2' 不是都为数字")
  }
}

或者

if (x == null) {
  println("$arg1 的类型错误")
  return
}

if (y == null) {
  println("$arg2 的类型错误")
  return
}
println(x * y)

使用类型检查以及自动类型转换

is 关键字会检测一个值是否是某个类型的,只要完成类型检查,那么被检查的值就可以直接当作该类型去使用。

fun getStringLength(obj: Any): Int? {
  if (obj is String) {
    // `obj` 已经被自动转换为 `String` 类型了
    return obj.length
  }
  return null
}

或者

fun getStringLength(obj: Any): Int? {
  if (obj !is String) return null
 
  return obj.length
}

也可以

fun getStringLength(obj: Any): Int? {
  if (obj is String && obj.length > 0) {
    return obj.length
  }
  return null
}

使用 for 循环

val items = listOf("apple", "banana", "kiwifrut")
for (item in items) {
  println(item)
}

或者

val items = listOf("apple", "banana", "kiwifruit")
for (index in items.indices) {
  println("item at $index is ${items[index]}")
}

使用 while 循环

val items = listOf("apple", "banaba", "kiwifruit")
var index = 0
while (index < items.size) {
  println("item at $index is ${items[index]}")
  index += 1
}

使用 when 表达式

fun describe(obj: Any): String = 
  when (obj) {
    1 -> "One"
    "Hello" -> "Greeting"
    is Long -> "Long"
    !is String -> "Not a string"
    else -> "Unknown"
  }

使用 ranges

检查一个数字是否在一个范围内

val x = 10
val y = 9
if (x in 1..y+1) {
  println("fits in range")
}

检查一个值是否不在一个范围内

val list = listOf("a", "b", "c")
if (-1 !in 0..list.lastIndex) {
  println("-1 不在范围内")
}
if (list.size in !in list.indices) {
  println("list.size 同样的也不在 list.indices 范围内")
}

遍历一个范围

for (x in 1..5) {
  print(x)
}

或者可以指定一个步进值

for (x in 1..10 step 2) {
    print(x)
}
println()
for (x in 9 downTo 0 step 3) {
    print(x)
}

使用集合

遍历一个集合

for (item in items) {
  println(item)
}

使用 in 确定一个集合是否包含某个对象

when {
  "orange" in items -> println("juicy")
  "apple" in items -> println("apple is fine too")
}

在集合上使用 filtermap

val fruits = listOf("banana", "avocado", "apple", "kiwifruit")
fruits
  .filter { it.startsWith("a") }
  .sortedBy { it }
  .map { it.toUpperCase() }
  .forEach { println(it) }

创建基础类实例

val rectangle = Rectangle(5.0, 2.0) // 不需要 `new` 关键字
val triangle = Triangle(3.0, 4.0, 5.0)

开发计算机软件是一个复杂的过程,在过去很长的一段时间时间里,计算机软件的开发被开发者们规整为以下这些常见的不同活动(Activity):

  1. 定义问题(Problem Definition)
  2. 需求分析(Requirements Development)
  3. 规划构建(Construction Planning)
  4. 软件架构(Software Architecture)或者高层设计(High-level Design)
  5. 详细设计(Detailed Design)
  6. 编码调试(Coding and Debugging)
  7. 单元测试(Unit Testing)
  8. 集成测试(Integration Testing)
  9. 集成(Integration)
  10. 系统测试(System Testing)
  11. 保障维护(Corrective Maintenance)

构建有时候被简单的理解为编码(Coding)或者编程(Programming),但是编码并不算是最贴切的,因为它有一种“把已经存在的设计机械化地翻译成计算机语言”的意味,而构建并不都是这么机械化的,需要可观的创造力与判断力。下面列出了一些软件构建活动中的具体任务(Task):

  • 验证有关的基础工作都已经完成,因此构建活动可以顺利的进行下去
  • 确定如何测试所写的代码
  • 设计并编写类(Class)和子程序(Routine)
  • 创建并命名变量(Variable)和具名常量(Named Constant)
  • 选择控制结构(Control Structure),组织语句块
  • 对你的代码进行单元测试和集成测试,并排除其中的错误
  • 评审开发团队其他成员的底层设计和代码,并让他们评审你的工作
  • 润饰代码,仔细进行代码的格式化和注释
  • 将单独开发的多个软件组件集成为一体
  • 调整代码(Tuning Code),让它更快,更省资源

为什么构建活动很重要?

  • 构建活动是软件开的主要组成部分
  • 构建活动是软件开发中的核心活动
  • 把主要精力集中于构建活动,可以大大提高程序员的生产率
  • 构建活动的产中物 —— 源代码 —— 往旆是对软件的唯一精确描述
  • 构建活动是唯一一项确保会完成的工作

用隐喻更充分的理解软件开发

你走进一间安全严密、温度精确控制在 20 摄氏度的房间,并在里面发现了病毒(Virus)、特洛伊木马(Trojan horse)、蠕虫(Worm)、臭虫(Bug)、逻辑炸弹(Logic bomb)、崩溃(Crash)、论坛口水战(Flame)、双绞线转换头(Twisted sex changer),还有致使错误(Fatal Error)……

重要的研发成果常常产自类比(Analogy),通过把不太理解的东西和一些你较为理解、且十分类似的东西做比较,可以对不理解的东西产生更深刻的理解,这种使用隐喻的方法,叫作“建模(Modeling)”。建模有其不好的一面,但是总的来说,模型是有好处的,它的威力在于其生动性,让你能够把握整个概念,能隐隐地暗示各种属性(Properties)、关系(Relationships)以及需要补充查证的部分(Additional areas of inquiry)。

隐喻的价值绝不应实低估,隐喻的优点在于其可预期的效果,能被所有人理解。不必要的沟通和误解也因此大为减少,学习与教授更为快速。实际上,隐喻是对概念进行内在化(Internalizing)和抽象(Abstracting)的一种途径,它让人们在更高的层面上思考问题,从而避免低层次的错误

-- Fernando J. Corbato

人地心说到日心说,从以计算机为中心到以数据为中心

托勒密的地心说模型持续了1400年,直到1543年哥白尼提出了以太阳为中心的理论,最终帮助人们找到了更多的行星,并将月亮重新界定为地球的卫星而不是一个独立的行星,它使人们对人类在宇宙中的地位有了一个完全不同的理解。

20世纪70年代早期计算机编程方面的变化就像从日心说到地心说一样,以前的编程观点是以计算机为中心(Computer-centered),数据处理是把所有的数据看作是流经计算机(Flowing through a computer)的连续卡片流(stream of crads),而后转变为以数据为中心(Database-centered),把焦点放到的数据池(Pool of data),而计算机偶尔涉足其中。

与其说是隐喻是一张路线图,不如说它是探路明灯,它不会告诉你该去哪里寻找答案,而是告诉你该如何去寻找答案,隐喻的作用更像是启示(Heuristic),而不是算法(Algorithm)。算法是一套定义明确的指令,使你能完成某个特定的任务,算法是可预测的(Predictable)、确定性的(Deterministic)不易变化的(Not subject to chance)。

启发式方式是一种帮你寻求答案的技术,但是它给出的答案是具有偶然性的(Subject to chance)

一直用着 Microsoft 的 AppCenter.ms 服务都不错,功能强大,但是最近总是抽风,没办法,只能自己部署私有 Code Push Server了,如果直接搜索 Code Push Server,一般得到的结果都是 https://github.com/lisong/code-push-server 这个,我安装过,不过并没有实现去测试,因为发现它并没有完美的实现 Code Push 的逻辑,在各种坛里面找了好几天之后,终于发现了 http://Electrode.io,Walmart Labs 的东西总是这么难发现, Hapijs 也是。

什么是 Electrode ,大家可以直接上官方去了解,我们只使用 Electrode OTA Server 功能,我本身就是一个长期的 HapiJS 用户,所以一看到这货,还是很亲切的。

安装运行环境

安装 Node

安装 nvm

nvm 是一个很不错的 Node 版本管理工具,使用下面任何一个命令安装即可,如果在安装过程中有任何疑问,请直接自行解决 https://github.com/nvm-sh/nvm

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.34.0/install.sh | bash

或者

wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.34.0/install.sh | bash

安装最新版本 Node

nvm install node

安装 Docker

这个不是必须的,但是如果只是在本地测试的话,建议安装,Electrode OTA Server 默认使用的是 Apache Cassandra 数据库,有了 Docker 之后,数据库的问题更好解决,否则需要在本机安装个 Cassandra 也是很烦人的一件事情,当然,如果不使用 Cassandra 的话,也可以直接使用 MariaDB 数据,这个下面都会说,因为我的机器配置不高,所以,最终还是选择了 MariaDB 数据库。

如果你已经安装了 Docker 了,那么直接跳过这一步,如果感觉没有安装过,那么继续,使用下面的命令删除所有过往的 docker 版本。

sudo yum remove docker \
                  docker-client \
                  docker-client-latest \
                  docker-common \
                  docker-latest \
                  docker-latest-logrotate \
                  docker-logrotate \
                  docker-engine

安装安装 yum-utils 以提供 yum-config-manager 工具,同时,device-mapper-persistent-data 以及 lvm2devicemapper 所必须的库:

sudo yum install -y yum-utils \
  device-mapper-persistent-data \
  lvm2

使用下面的命令设置 stable 版本 docker

sudo yum-config-manager \
    --add-repo \
    https://download.docker.com/linux/centos/docker-ce.repo

使用下面命令安装 docker

sudo yum install docker-ce docker-ce-cli containerd.io

安装数据库

基于 docker 之后,我们就直接安装 MariaDB 以及 Cassandra 数据库了。

安装 Cassandra

docker pull cassandra

或者有一个增强版本的选择:

docker pull datastax/dse-server

两个任选一个即可

docker run --name parcmg-cassandra -p 9042:9042 -d cassandra
docker container start parcmg-cassandra

安装 MariaDB

docker pull mariadb
docker run --name parcmg-mariadb -p 3306:3306 -d mariadb
docker container start parcmg-mariadb

现在我们已经有了可用的数据库服务了,接下来,部署 Electrode OTA Server

创建 Github App

这一步就不多说了,直接在 Github 后台创建一个 App,拿到 ClientId 以及 ClientSecret 两个值,在下面会用。

部署 Electrode OTA Server

创建 parcmg-ota-server 项目

mkdir parcmg-ota-server
yarn init
yarn add electrode-ota-server electrode-ota-server-dao-mariadb

添加 config/default.json 配置文件

在项目目录下面创建一个名为 config 的目录,在目录中,添加一个 default.json 的配置文件(这个是我最喜欢 HapiJS 的一点,所有东西都是配置优先。

{
  "connection": {
    "host": "localhost",
    "port": 9001,
    "compression": false
  },
  "server": {
    "app": {
      "electrode": true
    }
  },
  "plugins": {
    "electrode-ota-server-dao-plugin": {
      "module": "electrode-ota-server-dao-mariadb",
      "priority": 4,
      "options": {
        "keyspace": "parcmg_ota",
        "contactPoints": ["localhost"],
        "clusterConfig": {
          "canRetry": true,
          "defaultSelector": "ORDER",
          "removeNodeErrorCount": 5,
          "restoreNodeTimeout": 0
        },
        "poolConfigs": [
          {
            "host": "<%Database Host%>",
            "port": 3306,
            "dialect": "mysql",
            "database": "<%Database Name%>",
            "user": "<%Database Username%>",
            "password": "<%Database Password%>"
          }
        ]
      }
    },
    "electrode-ota-server-fileservice-upload": {
      "options": {
        "downloadUrl": "https://<%ota.domain.com%>/storagev2/"
      }
    },
    "electrode-ota-server-auth": {
      "options": {
        "strategy": {
          "github-oauth": {
            "options": {
              "password": "<%RandomKey%>",
              "isSecure": true,
              "location": "https://<%ota.domain.com%>",
              "clientId": "<%GithubClientId%>",
              "clientSecret": "<%GithubClientSecret%>"
            }
          },
          "session": {
            "options": {
              "password": "LYG2AqpUK3L4rKQERbuyJWxCqMYh5nlF",
              "isSecure": true
            }
          }
        }
      }
    }
  }
}

然后给 package.json 添加下面两个 script

{
  "scripts": {
    "start": "NODE_ENV=production node node_modules/electrode-ota-server",
    "development": "NODE_ENV=development node node_modules/electrode-ota-server"
  }
}

此时,可以直接使用 yarn development 或者 yarn start 运行了。

这里需要注意一点,如果使用 MariaDB,需要自己先建立好数据库以及数据表, schema 保存在 https://github.com/electrode-io/electrode-ota-server/tree/master/electrode-ota-mariadb-schema/electrode-ota-db/tables 这里面,一个一个创建即可。

安装 pm2

安装 pm2 工具

npm install -g pm2

添加 ecosystem.config.js 文件

内容如下:

module.exports = {
  apps: [
    {
      name: "parcmg-ota",
      script: "node_modules/electrode-ota-server/index.js",
      env: {
        NODE_ENV: "production"
      }
    }
  ]
};

package.json 中添加 serve 命令:

{
  "scripts": {
    "serve": "yarn install && pm2 startOrRestart ecosystem.config.js --env production",
    "start": "NODE_ENV=production node node_modules/electrode-ota-server",
    "development": "NODE_ENV=development node node_modules/electrode-ota-server"
  }
}

启动服务

yarn serve

或者

pm2 start ecosystem.config.js --env production

配置域名

安装 nginx

vi /etc/yum.repos.d/nginx.repo

内容如下:

[nginx]
name=nginx repo
baseurl=http://nginx.org/packages/mainline/centos/7/$basearch/
gpgcheck=0
enabled=1

然后执行下面命令安装:

yum install -y

添加虚拟主机

/etc/nginx/conf.d 目录下新建一个虚拟主机配置文件:

vi /etc/nginx/conf.d/ota.domain.com.conf

内容如下:

upstream parcmg_ota {
    server 127.0.0.1:9001; 
    keepalive 64;
}

server {
  listen               80;
  listen               [::]:80;
  server_name          ota.parcmg.com;
  return 301 https://$host$request_uri;
}

server {

  listen 443 ssl;
  
  ssl_certificate   cert.d/YOUR_PEM.com.pem;
  ssl_certificate_key  cert.d/YOUR_KEY.com.key;
  ssl_session_timeout 5m;
  ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
  ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
  ssl_prefer_server_ciphers on;

  server_name ota.parcmg.com;

  charset utf-8;

  # Global restrictions configuration file.
  # Designed to be included in any server {} block.
  location = /favicon.ico {
    log_not_found off;
    access_log off;
  }

  location = /robots.txt {
    allow all;
    log_not_found off;
    access_log off;
  }

  # Deny all attempts to access hidden files such as .htaccess, .htpasswd, .DS_Store (Mac).
  # Keep logging the requests to parse later (or to pass to firewall utilities such as fail2ban)
  location ~ /\. {
    deny all;
  }

  location / {
    if ($request_method = 'OPTIONS') {
      add_header 'Access-Control-Allow-Origin' '*';
      add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
      #
      # Custom headers and headers various browsers *should* be OK with but aren't
      #
      add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
      #
      # Tell client that this pre-flight info is valid for 20 days
      #
      add_header 'Access-Control-Max-Age' 1728000;
      add_header 'Content-Type' 'text/plain; charset=utf-8';
      add_header 'Content-Length' 0;
      return 204;
    }
    if ($request_method = 'POST') {
      add_header 'Access-Control-Allow-Origin' '*';
      add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
      add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
      add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
    }
    if ($request_method = 'GET') {
      add_header 'Access-Control-Allow-Origin' '*';
      add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
      add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
      add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
    }

      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header Host $http_host;
      proxy_set_header X-Nginx-Proxy true;
      proxy_set_header Connection "";
      proxy_pass http://parcmg_ota; 
  }
}

启动 nginx

systemctl start nginx
systemctl enable nginx
如果没有 SSL 证书,可以上 Aliyun 或者 QCloud 上面去申请免费的,当然,也可以直接使用 http 协议。

Electrode OTA Desktop

牛逼的Walmart Labs 还提供了一个可视化的管理工具,咱现在就先用上,直接去 https://github.com/electrode-io/electrode-ota-desktop/releases 下载最新版本即可,打开之后,会看到登录界面。暂时离开一会儿,回到 Terminal 中去。

Electrode OTA Desktop Login Screen@2x.png

登录 OTA Server

退出已有 code-push 会话

如果你以前已经使用了 Appcenter.ms 的服务,那么现在可以退出登录了。

code-push logout

注册

重新在私有 OTA 服务中注册帐号:

code-push register https://ota.parcmg.com

此时会跳转到 https://ota.parcmg.com 的授权页面,在页面最下方点击 Github 完成 OAuth 授权之后,会得到一个 Access Token,复制该 Token,在 Terminal 中粘贴,按回车,即可登录成功,同时,将该 Token 粘贴至 Electrode OTA Desktop 应用的登录框的 Token中,在服务地址中填写你的 OTA 服务地址即可完成会话登录。

添加 App

Electrode OTA Desktop 里面,创建两个新的应用,就跟使用 appcenter.ms 一样,比如:

MyApp-Android
MyApp-IOS

创建成功之后,会分别生成对应的 Production 以及 Staging Key,在接下面我们会用到。

code push 服务迁移到自己的私有服务器

IOS

打开 info.plist 文件,我们需要修改以前的 Code Push 配置,找到:

<key>CodePushDeploymentKey</key>
<string>SecrtKey-----------Here</string>

在此处,将 MyApp-IOSProduction Key粘贴至此处,同时还需要添加一个配置项目:

<key>CodePushServerURL</key>
<string>https://ota.parcmg.com</string>

完整配置如下:

<key>CodePushDeploymentKey</key>
<string><%YourKeyHere%></string>
<key>CodePushServerURL</key>
<string>https://ota.parcmg.com</string>

如果你使用的不是 https 协议 ,那么还需要增加:

<dict>
  <key>NSAllowsArbitraryLoads</key>
  <true/>
  <key>NSExceptionDomains</key>
  <dict>
    <key>ota.parcmg.com</key>
    <dict>
      <key>NSExceptionAllowsInsecureHTTPLoads</key>
      <true/>
    </dict>
  </dict>
</dict>

Android

MainApplication.java 文件中,找到下面这一行:

new CodePush(getResources().getString(R.string.reactNativeCodePush_androidDeploymentKey), getApplicationContext(), BuildConfig.DEBUG)

添加一个参数如下,表示我需要使用这个作为 code push 的服务。

new CodePush(getResources().getString(R.string.reactNativeCodePush_androidDeploymentKey), getApplicationContext(), BuildConfig.DEBUG, "https://ota.parcmg.com")

大功告成了,需要测试的可以直接使用我的 ota 服务: https://ota.parcmg.com,但请不要在生产中使用,鬼知道我什么时候就会把这个停用了。

最近研究 React Native、Redux Saga 以及 TypeScript 相关的内容,整理成了一个 React Native Template,可以直接使用下面的命令创建一个新的应用:

react-native init MyApp --template=parcmg

初始化完成之后,按下面的方式执行命令:

cd MyApp
node setup.js
npm install
react-native link react-native-gesture-handler

完成之后,即可像往常一样开发了:

react-native run-ios

初始化 NPM 项目

mkdir project-name
cd project-name
npm init

添加开发基础包

添加 TypeScript

yarn add typescript -D

添加 Jest 测试工具

yarn add jest ts-jest @types/jest -D

添加 @types/node

yarn add @types/node -D

初始化 TypeScript 配置

./node_modules/.bin/tsc --init

这会在你的项目根目录新建一个 tsconfig.json 文件

现在的目录结构如下:

.
├── node_modules
├── package.json
├── tsconfig.json
└── yarn.lock

文件解析

tsconfig.json

这是 TypeScript 的配置文件,默认仅启用了几项,我一般的配置如下:

{
  "compilerOptions": {
    /* Basic Options */
    "target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */,
    "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
    "lib": [
      "es6"
    ] /* Specify library files to be included in the compilation. */,
    // "allowJs": true,                       /* Allow javascript files to be compiled. */
    // "checkJs": true,                       /* Report errors in .js files. */
    // "jsx": "preserve",                     /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
    "declaration": true /* Generates corresponding '.d.ts' file. */,
    "declarationMap": true /* Generates a sourcemap for each corresponding '.d.ts' file. */,
    // "sourceMap": true,                     /* Generates corresponding '.map' file. */
    // "outFile": "./",                       /* Concatenate and emit output to single file. */
    "outDir": "./dist" /* Redirect output structure to the directory. */,
    // "rootDir": "./",                       /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
    // "composite": true,                     /* Enable project compilation */
    // "incremental": true,                   /* Enable incremental compilation */
    // "tsBuildInfoFile": "./",               /* Specify file to store incremental compilation information */
    // "removeComments": true,                /* Do not emit comments to output. */
    // "noEmit": true,                        /* Do not emit outputs. */
    // "importHelpers": true,                 /* Import emit helpers from 'tslib'. */
    // "downlevelIteration": true,            /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
    // "isolatedModules": true,               /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */

    /* Strict Type-Checking Options */
    "strict": true /* Enable all strict type-checking options. */,
    "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */,
    "strictNullChecks": true /* Enable strict null checks. */,
    "strictFunctionTypes": true /* Enable strict checking of function types. */,
    "strictBindCallApply": true /* Enable strict 'bind', 'call', and 'apply' methods on functions. */,
    "strictPropertyInitialization": true /* Enable strict checking of property initialization in classes. */,
    "noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */,
    "alwaysStrict": true /* Parse in strict mode and emit "use strict" for each source file. */,

    /* Additional Checks */
    "noUnusedLocals": true /* Report errors on unused locals. */,
    "noUnusedParameters": true /* Report errors on unused parameters. */,
    "noImplicitReturns": true /* Report error when not all code paths in function return a value. */,
    "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */,

    /* Module Resolution Options */
    // "moduleResolution": "node",            /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
    // "baseUrl": "./",                       /* Base directory to resolve non-absolute module names. */
    // "paths": {},                           /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
    // "rootDirs": [],                        /* List of root folders whose combined content represents the structure of the project at runtime. */
    // "typeRoots": [],                       /* List of folders to include type definitions from. */
    // "types": [],                           /* Type declaration files to be included in compilation. */
    // "allowSyntheticDefaultImports": true,  /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
    "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
    // "preserveSymlinks": true,              /* Do not resolve the real path of symlinks. */

    /* Source Map Options */
    // "sourceRoot": "",                      /* Specify the location where debugger should locate TypeScript files instead of source locations. */
    // "mapRoot": "",                         /* Specify the location where debugger should locate map files instead of generated locations. */
    // "inlineSourceMap": true,               /* Emit a single file with source maps instead of having a separate file. */
    // "inlineSources": true,                 /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */

    /* Experimental Options */
    // "experimentalDecorators": true,        /* Enables experimental support for ES7 decorators. */
    // "emitDecoratorMetadata": true,         /* Enables experimental support for emitting type metadata for decorators. */
  },
  "include": ["./src/**/*"],
  "exclude": ["node_modules", "src/__tests__"]
}

package.json

添加了几条 scripts

{
  "name": "project-name",
  "version": "0.0.0-development",
  "description": "",
  "main": "index.js",
  "scripts": {
    "build": "rm -rf ./dist && tsc",
    "watch": "yarn test --watch",
    "setup": "yarn install && npx npm-install-peers",
    "test": "jest"
  },
  "author": "",
  "license": "SEE LICENSE IN LICENSE FILE",
  "devDependencies": {
    "@types/jest": "^24.0.13",
    "@types/node": "^12.0.3",
    "jest": "^24.8.0",
    "ts-jest": "^24.0.2",
    "typescript": "^3.4.5"
  }
}

添加 jest.config.js 文件

内容如下:

module.exports = {
  // We always recommend having all TypeScript files in a src folder in your project. We assume this is true and specify this using the roots option.
  roots: ["<rootDir>/src"],
  transform: {
    "^.+\\.tsx?$": "ts-jest" //The transform config just tells jest to use ts-jest for ts / tsx files.
  }
};

添加 .gitignore 文件

该文件告诉 git 哪些文件不需要跟踪

# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# nyc test coverage
.nyc_output

# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# TypeScript v1 declaration files
typings/

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variables file
.env

# next.js build output
.next

# map
*.map

# gitbook
_book

初始化 git 项目

git init
jest watch 需要有 git 环境才能运行

添加 src/index.ts 文件

内容如下:

export const sum = (a: number, b: number): number => a + b;

添加 src/__tests__

按我们的 jest 测试用例:src/__tests__/index.test.ts,内容如下:

import { sum } from "..";

test("sum", () => {
  expect(sum(1, 2)).toBe(3);
});

开始开发

yarn jest

此时终端显示如下信息:

 PASS  src/__tests__/index.test.ts
  ✓ sum (15ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        2.133s
Ran all test suites related to changed files.

Watch Usage
 › Press a to run all tests.
 › Press f to run only failed tests.
 › Press p to filter by a filename regex pattern.
 › Press t to filter by a test name regex pattern.
 › Press q to quit watch mode.
 › Press Enter to trigger a test run.

现在修改一下 index.ts,文件内容改为下面这样:

export const sum = (a: number, b: number): number => a + b;

export const multiply = (a: number, b: number): number => a * b;

再修改 __tests__/index.test.ts,内容如下:

import { sum, multiply } from "..";

test("sum", () => {
  expect(sum(1, 2)).toBe(3);
});

test("multiply", () => {
  expect(multiply(2, 3)).toBe(6);
});

会发现终端会自动测试你的代码:

 PASS  src/__tests__/index.test.ts
  ✓ sum (4ms)
  ✓ multiply

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        4.164s
Ran all test suites related to changed files.

完整的代码可以在下面看到: