binnng

binnng 查看完整档案

北京编辑  |  填写毕业院校摩拜单车  |  前端工程师 编辑 binnng.github.io 编辑
编辑

前端工程师

个人动态

binnng 赞了文章 · 7月28日

一个成都程序猿眼中的成都和天府软件园,先从蚂蚁金服说起…

我是一个成都天府三街的程序猿,而蚂蚁金服在天府四街。

写这篇文章的时候我测试了一下,从我的工位出发,步行 6 分钟就可以走到蚂蚁金服的楼下,然后拍下这张照片:

途中会路过腾讯、字节跳动、万科、携程.......

所以,每天从天府三街地铁站下站的人特别多,这里面大概率聚集了天府软件园中最有钱的一批员工:

需要说明一下的是这篇文章我会提到成都蚂蚁金服。但是更多的,我想聊的是我眼中的成都,和我所看见的天府软件园,蚂蚁金服只是恰好在天府软件园而已。

另外,需要严正声明的是,全文谨代表个人观点,观察角度有限。杠精勿扰。

还有,对不起,用标题党把你骗了进来。

我眼中的成都

我的老家并不是成都。

我对成都最早的记忆应该得追溯到小学五年级的时候,也就是 2005 年,那时在学校订阅了少年先锋报,每周五的时候都会发一期新报纸给我们。

我就是在五年级的一个周五的最后一节课上领到了报纸,然后看到了报纸上的一个硕大的标题:《成都,一座来了就不想离开的城市》,然后翻页一看还有一个标题我记得特别清楚:《成都有条街,名字叫锦里》。

里面写的什么内容我全忘记了,但是看完之后,就是朦朦胧胧的向往这个地方。就类似于小时候的音乐课,学了“我爱北京天安门,天安门上太阳升”之后,对北京天安门的无限遐想一样。

但是天安门离小学五年级的我,还是太远了,成都还可以幻想一下。

也许,成都,就是读书的时候课本里面,总是提到的大城市吧。

而我第一次来成都是 2010 年的劳动节,那时候的劳动节还是黄金周,放七天。

仔细一算,转眼 10 年过去了。

我记得那一年是我带着我堂妹去找我表姐,坐了整整 6 个小时的班车,才到成都。那时候表姐在成都读大学,她正在读大二。

姐姐第一站就带着我们去了摩肩擦踵的春熙路,那个时候还没有一只熊猫趴在那里,旁边也没有太古里,甚至地铁还得再等接近 2 年半的时间,才会出现一个叫做“春熙路站”的地铁站。

还看了一场电影,看的是甄子丹主演的《叶问2》,因为那是我第一次在电影院的大银幕上看电影,看的非常的激动。以至于之后的每次看到叶问这个 IP 的时候,就会想起我第一次看电影的场景。

然后还去了一趟欢乐谷,我甚至到现在都记得,10 年前我排队玩的最后一个项目是大摆锤,因为住的地方距离欢乐谷太远了,预计会错过末班车,所以我最终放弃排队。

那天晚上到家太晚了,我们就在附近的路边摊吃的晚饭,坐在临时撑起的小桌子上,吃的什么饭我忘了。但是,我记得姐姐怕我没吃饱,多给我点了一个肉夹馍,吃的时候我没拿稳,还掉了一坨肉出去。

第一次来成都好像只待了短短的 3 天时间。

但是这是我第一次触摸到成都的样子。和我从小长大所待的逼仄的小村庄、小县城比起来,她是光怪陆离的,光鲜亮丽太多太多了。

后来,我上大学考到了成都。

军训结束之后,我便骑车去了朋友家,然后和他一起去了锦里。

我在出发之前,我就知道锦里不是一个非常值得去的地方。但是,我执意要去只是因为这个地方对于小学五年级的我而已,意义非凡。

小学五年级的我在课外报纸上看到锦里时充满了憧憬,甚至我把那份报纸放在了床头柜里面,偶尔拿出来翻阅一下。而真正站在锦里的入口处的时候,已经过去整整 7 年的时间了。

当我站在入口处,拍下一张游客照的时候, 7 年前的报纸没了,床头柜没了,甚至床都没了,但是那份憧憬我还能回忆起来。

从报纸中的锦里,到现实中的锦里。从梦开始的地方结束一段梦,然后开启另外一段更加梦幻的旅程。

大学我在成都待了四年。用了整整四年的时间,试图去理解这座城市。

第一个理解到的是一句诗的前半部分,来自诗圣杜甫:

窗含西岭千秋雪,门泊东吴万里船。

这句诗就是杜甫在成都的时候,写下的《绝句》。

成都在天气极好的时候,是真的可以用肉眼看到雪山的。因为我在大学的宿舍里面就看到过好几次。

我觉得这是一件非常浪漫的事情。当然我不是指看雪山很浪漫。

我指的是在公元 764 年(距今 1256 年),一个诗人在杜甫草堂,一个初春的早上推开自己的窗户,听见了虫鸣鸟叫的声音,再看到天上飞着一排白鹭,随着白鹭飞行的路线往远处一看,发现有一座清晰可见的雪山,恰好,他又觉得自己看到的是西岭雪山。

其实据后来人考证杜甫看到的应该是蜀山之后四姑娘山的幺妹峰。

图片来源,四川画报。

图片来源,四川画报。

但是没关系,也许是西岭的名字和诗更搭一点,于是纸墨笔砚,一气呵成,写下一首千古《绝句》:

两个黄鹂鸣翠柳,一行白鹭上青天。 窗含西岭千秋雪,门泊东吴万里船。

诗人没有想到的是,这一首《绝句》,竟然吟唱千年,写入了小学的课本之中。

千百年后的我们,早上起来,发现天气极好,于是爬上楼顶,没有听到虫鸣鸟叫,没有看到一行白鹭,但是往远处一看,也发现了一座清晰可见的雪山,于是拍下一张照片,再配上千百年前就准备好的文案:窗含西岭千秋雪。

1256 年,白云苍狗,换了人间,雪山还在,诗歌还在。

难道,这不是时间赋予成都的一件极度浪漫的事情吗?

前段时间,github 也把我们的代码埋到北极,冰封 1000 年,这两件事有异曲同工之妙。一个是成都的浪漫,一个是程序猿的浪漫。

再看现在,大多数人提到成都可能都会哼两句《成都》:

在那座阴雨的小城里 我从未忘记你 成都 带不走的 只有你 和我在成都的街头走一走 喔哦 直到所有的灯都熄灭了也不停留

《成都》这首歌,也是一首浪漫的歌。

我第一次听到这首歌的时候在北京,雷子在《我是歌手》的舞台上,让更多的人再次认识到神采奕奕的成都。

而大多数人都喜欢这首歌的“玉林路的尽头,小酒馆的门口”。

我不一样,我喜欢“阴雨的小城里”。因为太准确了,成都就是一座阴雨的城市,一座下夜雨的城市。

我一次听到这句歌词的时候我就想到了大学军训的时光。大学刚刚开学军训的时候,特别辛苦,晚上一听见下雨的声音就非常开心,因为第二天可以不用早起军训了。

但是第二天一早起来,地都快干了,让昨夜的幻想随着破灭。

所以成都是一座下夜雨的城市。阴雨的小城,在杜甫的诗歌里面同样有体现,《春夜喜雨》:

好雨知时节,当春乃发生。 随风潜入夜,润物细无声。 野径云俱黑,江船火独明。 晓看红湿处,花重锦官城。

所以其实成都还有一个别名,或者古时候的名称:锦官城。

多美丽的名字。

而且,话说回来,成都没有玉林路,只有玉林西路。玉林西路上有一个小酒馆,但是我建议你直接去九眼桥。

有的人说《成都》把里面的地点一换,就可以是其他的城市,这一点我是不同意的。

因为,“阴雨的小城”就是我记忆中的成都。

另外,和雷子的《成都》相比,我更喜欢张靓颖的《I Love This City》,这首歌是成都市形象主题曲,比《成都》更能代表成都。

雷子不是成都人,他唱的是外来人对成都的爱恋。

张靓颖是成都人,她唱的是本地人对成都的热爱。

2008 年地震后,在四川,在成都,在电视里,到处都在循环放这首歌。这首歌有一种治愈的力量,如果你也经历过汶川大地震,听这首歌的时候,你会真正的明白,什么叫做:

前奏才刚刚响起,就有人哭红了眼睛。

仔细一算,地震也过去 12 年了。

12 年前,成都还没有 IFS,没有太古里,南边还是一片菜地,没有天府新区的概念,更谈不上软件园的建设,地铁就只有一号线,最远也就到世纪城,距离现在的科学城还差着十万八千里,二环上的高架桥也还没有修好,也没有BRT,三环外感觉都是一个很远的地方,我现在所在的双流那更是乡下了。

人们出行大多依赖公交车,坐在摇摇晃晃的车厢里。我大二的时候才第一次做成都地铁,地铁的车厢偶尔也会摇晃一下。

这一摇晃,从公交到地铁,12 年弹指一挥间。

从某种意义上来说《I Love This City》这首歌真正的代表了成都人地震后的态度。

摧毁了,我们就把它就建设的更好。

逝者已逝,生者坚强。这句话不是说说而已,对于一部分成都人来说,也融入到了骨子里。

我之前去映秀的时候,住宿的老板亲口对我说:“已经经历过生死了,现在活着的每一天,都是我赚的。当时不是说了吗,逝者已逝,生者要坚强。”

所以,从某种角度上来说, 2008 年的大地震,让原本就休闲的成都人,伤愈之后,又活出了一份洒脱的劲。

其实我还想再写写成都的休闲、成都的美食和有趣的成都话的,但是我发现怎么写都不对劲,算了吧。只说一句,然后放个朋友圈截图。

非常多的朋友来成都问我玩什么?

我的回答都是,来成都市里面可以玩的地方非常少,都是人文风景,如果要看自然风光,首推川西环线(可惜大多数人没有这么多时间),然后九寨沟什么的,总之成都好玩的地方在成都周边。

但是你要是来吃东西,就不一样了,在市里面你可以吃到非常非常多的好吃的:

火锅 串串 干锅 冒菜 抄手 米粉 豆花 锅盔 凉粉 凉糕 冰粉 三大炮 钵钵鸡 冷锅鱼 冷吃兔 肥肠粉 毛血旺 冷沾沾 担担面 口水鸡 韩包子 钟水饺 懒汤圆 叶儿粑 甜水面 奶汤面 龙抄手 炒龙虾 江油肥肠 牛肉焦饼 龙眼包子 夫妻肺片 珍珠丸子 水煮肉片 李庄白肉 灯影牛肉 泸州黄粑 鸡丝凉面 麻婆豆腐 跷脚牛肉 莲茸层层酥 酥盒回锅肉 红锅黄辣丁

那个不巴适!

之前星球研究所写了一篇专门报道成都的文章,我看到后发了一个朋友圈,不假思索的打了这么多字:

从上面的观点来看,从一个普通的学生的角度来看,成都真的是一个慢节奏的,适合休闲的城市。

现在,我已经脱离学生身份好几年了,看法发生了些许变化。

境随心转,我再带你了解一下我眼中的成都软件园。

我眼中的成都软件园

不知道什么时候出现了一个“新一线城市”的概念。但是从知道这个概念的时候,成都就一直名列前茅。现在更是稳居榜首。

从我个人有限的了解和观察而言,我觉得高新区、天府新区功不可没。而其中的成都软件园更是中流砥柱。

不夸张的说,单单看成都软件园,你来天府一到五街走一走,特别是看一下晚上无人机拍的夜景,这妥妥的一个国际化大都市。

我 2016 年年初的时候就在成都软件园的长虹科技大厦里面实习。也就是四年前。

那个时候我住在中和版块,一个叫做黄金时代的小区,开车不到五公里,但是不论你怎么开,都需要过桥。

过桥之前路很窄,单向双车道。过桥的时候非常堵,上了天府大道后更是堵的心慌。所以一般五公里的路程需要开车 40 多分钟的样子。

后来同事也不开车去公司了,我就和同事在小区门口坐一个三轮车,三轮车师傅就会走一些羊肠小道。半路上还会经过一个比较陡的斜坡,蹬三轮车的师傅就会给我们说:

“兄弟伙,下来一下呢,前面这个坡有点陡,我怕冲不上去。走两步嘛,坡坡上等你。”

拥堵,是我对软件园的第一个映像。很多地方只是表面上看起来过得去。

或者换个成语来表达:金玉其外,败絮其中

但是需要说明一下的是,这是我 4 年前的感受,现在好多了,路修的比较宽了,除了下雨天,也不会那么堵了。

然后,实习的时候在长虹科技大厦的阳台上,可以看见腾讯大厦的一个角,那个时候怎么也不会想到,在那座大厦里面会产生一款风靡全国的游戏《王者荣耀》。

现在早上做地铁的时候偶尔还会遇见一个挂着王者荣耀工牌的小哥哥。我站在你他不远的地方,不经意间瞟一眼过去,感觉他的工牌在发光,他的整个人也在发光。也许这就是传说中的大厂光环吧。

而实习的时候,长虹的对面还在修一座写字楼,每天我都从这座写字楼的旁边走过,突然有一天,楼上挂了一个 logo,上面写的是:Alibaba Group。

那个时候才知道,原来这是阿里在成都的根据地。

同样,我怎么也想不到这栋楼还会挂一个蚂蚁金服的牌子,更想不到的是这栋楼蛰伏在这里,闷声不语,短短 4 年后,然后突然有一天,这栋楼里面会发出财富自由的欢呼。

响彻在整个天府一街到五街,回荡在整个软件园的上空,成为程序员的茶余饭后的谈资。

大家讨论这件事的无外乎围绕着这几个词:程序员,阿里,级别,财富自由......

蚂蚁传出上市消息后的第二天,我去银泰城吃了一碗猪脚饭。一路上看见了很多挂着蚂蚁金服工牌的年轻人,他们三两成群的走在一起,谈笑风生,意气风发,不经意间流露出最近买房换车的打算。也是,这就是有钱人的生活,朴实无华,且枯燥。

而在蚂蚁传出上市消息后的第三天,仅仅一街之隔的软件园 C 区 7 栋,有一个程序员哥们,从 6 楼一跃而下,结束了自己短暂的一生。

大家讨论这件事的无外乎围绕着这几个词:程序员,年轻,外包,没钱......

当我看到这个消息的时候我先是一阵惋惜,而后想起了鲁迅先生的一句话:

人类的悲欢并不相通,我只觉得他们吵闹。

蚂蚁上市的欢乐,我感受不到。一跃而下的悲伤,我也感受不到。

这两件事情之间毫无客观上的联系。但这都是发生在软件园的故事,这都是程序员的故事。

有人财富自由,有人跳下高楼。

这事发生之后有人在某平台提问:如何看待成都程序员跳楼事件。成都不是以慢节奏和休闲著名吗?

我以我有限的视角和亲身的经历来试着回答一下这个问题。

首先,就像我前面一节说的,当我是一个在成都读大学的学生时,常常骑着自行车瞎逛,路过天府广场的时候一定会绕到春熙路上去,去感受一下人群,顺便欣赏美女。或者就在学校附近打几圈麻将,晚上到饭点了,谁赢了就请大家在学校小吃街,吃着便宜且美味的晚饭,然后再约着三两好友围坐在操场上闲聊。

那个时候对美食不感兴趣,但是唯独追求险峻的美景,恰好又都大把大把的时间,于是走过了成都很多你叫的出或者叫不出名字的美景。

甚至在宿舍里面都要摆点茶具,沏几杯泡着碧潭飘雪的茶。喝的不是味道,是感觉,因为只有这样的感觉,才配得上成都这个地方。那确实是休闲,但是这种休闲是刻意出来的,或者说是装出来的。

那个时候,我觉得成都是一个非常适合生活的地方。很休闲,

我是什么时候觉得成都并不是那么休闲的呢?

当我毕业季,面临找工作,在知乎提出这个问题的时候:

当你在学校的时候,住宿费一学期也才 1200 元。一个月 1500 元的生活费,由于食堂和美食街的存在,日子可以过的美滋滋的。

毕业之后就不一样了,要直面入不敷出的生活。所以于我而言,我可能是选择了另外一种逃避。

反正都要吃苦,那我为什么不选最难走的那条呢,于是我逃离了成都,去了北京。

现在我从北京也回来一年多的时间了,我发现我眼中的成都,并不休闲,或者准确的说成都的软件园并不休闲。

成都软件园,放在北京,这就是西二旗。你能说西二旗休闲吗?

这里同样是一个需要拼命的地方。

每天早上,我从五根松坐五站地铁,到天府三街。由于最近的地摊文化,所以出了地铁口就可以解决每天的早饭问题,这些推着小推车的叔叔阿姨每天早上就在这里守候着我们:

我所在的公司的食堂是提供早餐的,但是我来了一年多,去食堂吃早饭的次数加起来不超过 10 次。

因为电梯太堵了。新希望 A/B/C 三座写字楼,你去打听打听,是多少高楼层上班族的噩梦。

每天早上排队等电梯的,有一种在欢乐谷等跳楼机的感觉,我所在的办公楼层低,爬楼梯就上去了,但是食堂是在顶楼啊,听同事说有一次等了差不多 20 分钟:

要是放在十年前,你对一个成都人说,早上上班光排队等电梯就得 20 分钟。

他大概率对你说:啥子?20 分钟?疯了迈(吗)? 20 分钟都够老子搓几把麻将了。

现在应该不会了,他大概率会说:哎呀,好正常嘛。我开车还不是经常随随便便堵个半小时,动都动不到,恼火的很。没得事,习惯就好了。

然后中午吃饭的时候大家也是四处觅食。我偶尔去银泰城吃。

下面这张图就是银泰城,是我一个摄影师朋友拍的:

拍摄者山岚,使用经过授权

拍摄者山岚,使用经过授权

银泰城里面的饭店特别多,你一天吃一家,一年都不带重样的。

我偶尔去这里吃的原因是有几个大学朋友也在软件园上班,偶尔叫在一起,中午吃个便饭,随便瞎聊几句。

在上面这张图片中,你其实是可以看到阿里和腾讯的写字楼的,同样也能看见很多住宅。

所以我们在一起经常讨论的一个话题就是:为什么没有在读大学的时候就在成都买房子?

有一次我就问一个成都的土著朋友:如果真的回到大学的时候,大一就叫你来软件园这边买房子,8000 一个平方,你来买不买?

他的回答是:疯了迈?过来种菜?我会觉得你脑壳被门夹了。

而现在,就这附近的房价,由于有银泰城和软件园加持,差一点就到均价 3w 吧。没有加持的地方,大概 2w 左右。

但是说实在的,这个价格和一线城市比起来,真的是低了很多很多了。每次我给北京的朋友说成都的房价的时候他们都表示震惊,太便宜了。

当然便宜了,你拿着一线城市的工作,来成都买房当然觉得压力不大。

但是实际情况呢?在软件园,有多少是外地来的蓉漂。你去问他们休闲吗。

哦,不。你去问他们:有买房资格了吗?落户落在哪个区的?摇了几次号了?摇中了吗?买房了吗?房子买在哪里的?

能轻松的笑着回答的人真的不多。

特别是刚刚从学校里面走出来的大学生和刚刚从培训班走出来的从业者。

在我的朋友圈里面有非常多的培训机构,他们的宣传语大多都非常的浮夸,我不说你也知道。

但是有一个成都本地的小的线下培训机构,前两周我看到其创始人发了一个朋友圈,里面有这样的一张图片:

看了之后我发至内心的说了一句:培训界的一股清流。

没有动辄 1w+ 的浮夸,没有 offer 选不过来的困扰。这个薪资水平可能才代表了大多数双非本科毕业的、刚刚入行的程序员的标准。

一个培训班至少也有好几十人,在这一群人里面选了九个薪资还算说得过去的做为宣传。这样克制的宣传,加上我对成都的了解,我相信里面不会有夸大的成分。

这样的程序员大多出没在软件园,刚刚入行的前两年左右的时间,他们在小公司里面拿着 10000 左右的工资,税后大概两个月挣一个平方的钱。

更别说其他的服务行业从事者了,基本上一个月 3000 多点。

你不要以为我说的是少部分,这样的人真的很多。软件园这么大,而叫的出名字的就那么几家公司。绝大部分都是小型公司,他们可能真的觉得你是人才,但是他们可能也真的给不起钱。

其实房价这个事情,客观的讲不能怪成都。这个事情,你放到任何一个二线以上的城市,都不会是一个轻松的话题,和休闲更是沾不上边。

只是从我所经历的事情来看,成都这两年的房价由于政策原因真的是突飞猛进。短短 3 年不到的时间,就已经是翻了至少一倍。

而且一件非常神奇的事情是,由于限购政策的出台,导致非常多的人摇不到号,那些真正的刚需转而投向二手房市场,我身边这样的例子太多太多了。

如果你不懂行,你去问他们:“为什么要买二手房,而不买新房呢?”。

他们都会说:“你以为我不想吗?摇不上号啊!摇了无数次了!我等不起了!”

我之前也去摇了一把,这是我的最终结果:

一共 786 套房子,有 40568 人报名摇号。刚需的中签率是 2.43%。

这种盘俗称万人摇。为什么这么多人去摇呢?

因为太便宜了:

清水售价 1.3w ,而周围的二手房价格大概在 1.7w 左右。

4k 一平的剪刀差(二手房和新房的价格差),你就算买到一个最小的 108 平,到手就赚 43w。足够让工薪阶层的人疯狂。

当然这个算法肯定是不严谨的,我举这个楼盘也是少有的神盘,但是我想表达的是:

这种盘真刚需和假刚需都会想要去试一试,普通资格的人也会来凑热闹,买来就是投资。基数大了,导致就算你是真刚需,也很难摇到心仪的楼盘。最终,没有房的真刚需抗不住了,退而求其次转向二手房市场。而投资者们,还在乐此不疲的摇着一个又一个神盘,摇中了,成都又多一套二手房,稳赚几十万。毕竟,名下已经有房了,不慌。

而且,关于房子我必须要说一句,成都现在清水房基本上是灭绝了,全是精装房。

平均 3k 一平的精装房装修,客气一点来说,开发商吃相太难看了,装出来的东西都没眼看。

不客气的说,我花钱买几只哈士奇,让他们进屋一顿输出,最终的效果都比开发商装修的好。因为反正有钱又追求品质的人是选择全部翘掉,重新装修的。

就精装房这个事,成都又叫维权之都。了解一下?(需要说明一下,这里我不是黑成都,精装房这个事全国都有,只是我在成都,了解的更多的是成都方面的消息。)

我身边的真事,之前有个同事请假,请假理由写的是:精装房维权。

老板会心一笑,点了通过审批,然后说了一句:“任重道远,要加油哦。”

好不容易摇到号了,工作日努力工作加班挣钱,周末还要去四处维权,争取自己的利益。

这不是个体,而是普遍的事情。

所以,魔怔的房价,疯狂的房市,让这座原本休闲的成都,变的并不是这么休闲了。

房价起来了,人们也开始慢慢的忙碌起来了。房价,它在催着你往前走。

而最有购买力的人群代表之一,就是软件园的程序猿们,所以每次出地铁口你都会看见售楼小哥哥给你发传单,诱惑你,让你去买公寓,让你去买非常非常远的地方,比如那个神奇的地方:视高。因为他们知道你没有购房资格,就算有你也摇不到号,所以你就是他们的潜在客户。

所以你说那些来蓉漂的人,他们不想在成都买一套自己的房子,真正的扎根下来吗?

大多数人都有这样的想法吧。

为了这一套房子,你说来成都做蓉漂的人会觉得忙碌吗?

当然是会了,但是他们中的大多数最后都会靠自己的努力留在成都。而离开的那一部分人,走的时候也会轻轻的说一句谢谢成都。

因为成都给你机会了,是你自己要走的。

成都欢迎你,但是成都也从来不挽留你,只是在你走的时候会给你说:欢迎再来。

这让我想起了我在北京的一次月会上,领导对我们说:

在场的大多数人都是北漂。无论什么理由,都是自己选择的北漂。这就像一场考试,既然你选择了最难的题,就不要用随便的方式去做。

蓉漂一样的。

这就像一场考试,既然你主动的选择了成都这套题,就不要用随便的方式去做。也不要被各种繁杂的信息所迷惑,看好自己眼前的路,坚定的走下去就行了。现在贩卖焦虑已经是一件非常普遍的事情了,别人卖,你别买就行。包括我这篇文章,也有贩卖焦虑的倾向。

这篇文章我完全可以从另外一个角度去表现我所听到的成都,那个成都是休闲之都,人人都早九晚六,早上自然醒,晚上夜生活,周末四处游玩。我身边有这样的朋友,他们的生活就是这样。

在微信群,经常听见他们的语言:下班没,走切耍~噻。(出去玩,而且这个耍字要拖音长一点)。科华南路又整了一家网红店,走切尝一哈嘛。

另外一个人回复是:哎呀,耍不起了 。昨天晚上被喝翻了,今天就喝点养生茶,搓几圈麻将就行了,你们切吃嘛,回来给我描述一哈味道就行了。(背景音里面还有麻将碰撞的声音。)

而我的回复是:

也许这就是成都两种人的真实生活写照吧。他们代表着老成都,而我代表着新一代的奋斗逼。奋斗逼终将胜利。

不过话说回来,成都也算是一个温柔的城市了。

因为一个更加残酷的事实是,你在成都也许努努力,垫垫脚,家人帮衬帮衬,还能拥有一个属于自己的小天地。但是你在一线城市想留下来,除了努力之外,你还需要一部分运气,而运气这个事情,你说的准吗?

上面杂七杂八的说了这么多,所以你说成都生活节奏慢吗?

三环内的老成都,生活也许慢一点,但是你来软件园看一眼。它能慢一下嘛?

最后说一句(求关注)

最后,送给大家一张,我拍的,麻辣牛肉,味道巴适的很:

欢迎大家来成都做客。

感谢您的阅读,我坚持原创,十分欢迎并感谢您的关注。

我是 why,一个被代码耽误的文学创作者,不是大佬,但是喜欢分享,是一个又暖又有料的四川好男人。

查看原文

赞 3 收藏 0 评论 0

binnng 关注了用户 · 2019-10-30

爱阳光的小菇凉 @aiyangguangdexiaoguliang_5b580a688831f

前端小白,每天都要学习鸭!🦆

关注 7

binnng 发布了文章 · 2019-10-25

小程序技能进阶回忆录 - 怎样让 wx.navigateBack 更好用

即使是战争,也要像西伯利亚风雪中挺拔的白桦,出落得亭亭玉立,楚楚动人。

相信只要开发过小程序,对wx.navigateBack 这个 api都不会陌生。在摩拜单车的小程序中,它也被改造的更方便满足复杂的业务需求,可谓之 增强型的 wx.navigateBack

先来看看官方文档中的用法:

wx.navigateBack({
  delta: 2
})

delta 表示返回的层级数。通过具体的业务示例来说明我们如何改造它:

余额充值的例子

两个页面:

  • A 页面展示用户余额,使用 H5 实现,通过 web-view 嵌套在小程序里
  • B 页面为用户充值,为了方便使用支付api,用小程序原生页面实现

用户在 B 页面充值完成后返回页面 A,更新用户余额。翻译成技术语言就是:从小程序原生页面返回到 H5 页面,需要刷新

简单的业务代码如下:

<!-- pages/balance/index.wxml -->
<web-view data-original="{{url}}"></web-view>
Page({
  data: {
    url: 'https://balance/url'
  },
  onShow() {
    // ...
  },
  onHide() {
    // ...
  }
})

只要再次进入 A 页面更新 URL,就能达到刷新的目的。可以每次动态加参数,也可以离开 A 时清空 URL,再次进入的时候还原回来:

const URL = 'https://balance/url'
Page({
  data: {
    url: URL
  },
  onShow() {
    this.setData({
      url: URL
    })
  },
  onHide() {
    setTimeout(() => {
      this.setData({
        url: ''
      })
    }, 800)
  }
})

为了不让屏幕突然变白,加了 setTimeout 延迟下。

选择性的返回刷新

上面虽然实现了需求,但是有个体验问题:不管用户充值与否,回到 A 页面都会刷新下。理论上,只有用户充值成功后才需要刷新 A 页面

简单看下 B 页面的代码:

Page({
  data: {},
  onTopup() {
    wx.requestPayment({
      // ...
      success(res) {
        wx.navigateBack()
      }
    })
  }
})

可否在返回 A 的时候告诉 A 是否充值成功?这样 A 就能选择性的刷新。

const URL = 'https://balance/url'
Page({
  data: {
    url: URL,
    isPaySuccess: false
  },
  onShow() {
    if (this.data.isPaySuccess) {
      this.setData({
        url: URL + '?refresh=1'
      })
    }
  }
})

A 页面有个 isPaySuccess 标记位控制是否刷新,那么如何在 B 页面支付成功后去修改这个标记位?直接看 B 页面的代码:

Page({
  data: {},
  onTopup() {
    wx.requestPayment({
      // ...
      success(res) {
        let pages = getCurrentPages()
        let pageA = pages[pages.length - 2]
        pageA.setData({
          isPaySuccess: true
        })
        wx.navigateBack()
      }
    })
  }
})

封装成 this.$back

可以封装成通用的 back 方法,返回页面的同时更改其数据:

function back(config) {
  let prevPageData = config.prevPageData
  let delta = config.delta || 1
  if (prevPageData) {
    let pages = getCurrentPages()
    let prevPage = pages[pages.length - (delta + 1)]
    prevPage.setData(config.prevPageData)
  }
  wx.navigateBack(config)
}

通过这样封装,上面的页面 B 的代码可以改成这样:

let back = require('../utils/back')
Page({
  data: {},
  onTopup() {
    wx.requestPayment({
      // ...
      success(res) {
        back({
          prevPageData: {
            isPaySuccess: true
          }
        })
      }
    })
  }
})

如果看过 globalData 的那些事儿,把 back 方法挂载到 this.$back 下,将会更方便使用:

Page({
  data: {},
  onTopup() {
    wx.requestPayment({
      // ...
      success(res) {
        this.$back({
          prevPageData: {
            isPaySuccess: true
          }
        })
      }
    })
  }
})

总结

简单的 api 也可以变得丰富,一切都是基于日益复杂的业务需求。通过增强 wx.navigateBack 不仅仅可以改变前一个页面的标记位,还可以改变其页面显示数据。比如页面 A 的余额值是小程序通过参数传给 H5 的,而页面 B 充值成功后接口返回用户新的余额。这样就可以在充值成功后直接改变页面 A 的余额数据,而不是先返回到页面 A 再刷新重新请求接口。

查看原文

赞 9 收藏 5 评论 0

binnng 发布了文章 · 2019-10-24

小程序技能进阶回忆录 - 如何改变 onLoad 的执行时机

人活着,总得要点英雄主义的,哪怕失败、死亡。

也许在小程序所有生命周期里,我们用的最多的就是 onLoad,一大堆代码都要在初始化的时候执行。

很多时候,初始化的代码是每个页面共用的,比如获取用户信息、获取定位等:

Page({
  onLoad() {
    this.userData = getUserData()
    wx.getLocation({
      type: 'wgs84',
      success (res) {
        // 业务代码
        // ...
      }
    })
  }
  // ...
})

久而久之,每个页面的 js 里都是如上的重复代码。如果可以先执行完通用的初始化代码,再执行每个页面各自的 onLoad 多好,可惜小程序并没有提供类似的钩子函数,那就自己来吧。

代理 onLoad

按照前几篇的方法,可以代理原有的 onLoad 事件:

var originPage = Page

function MyPage(config) {
  this.lifetimeBackup = {
    onLoad: config.onLoad
  }
  config.onLoad = function(options) {
    // 自定义代码
    // 公共的初始化代码
    this.userData = getUserData()
     wx.getLocation({
      type: 'wgs84',
      success (res) {
        // 执行 onLoad
        this.lifetimeBackup.onLoad.call(this, options)
      }
    })
  }
  
  // ...

  originPage(config)
}

当然,实际开发过程中的初始化代码不可能这么少,可以用很多方式把它抽离出去,比如这样:

// utils/initial.js
function initial(callback) {
  this.userData = getUserData()
  wx.getLocation({
    type: 'wgs84',
    success (res) {
      callback()
    }
  })
}
  
// utils/wx.js
var initial = require('./initial')
var originPage = Page

function MyPage(config) {
  this.lifetimeBackup = {
    onLoad: config.onLoad
  }
  config.onLoad = function(options) {
    initial(() => {
      this.lifetimeBackup.onLoad.call(this, options)
    })
  }
  // ...
  originPage(config)
}

也可以使用更多高级的方法抽离出去,比如 event bus 之类的,就不多赘述。

看似很简单,但其实这样忽略了一个问题 —— 生命周期顺序被打乱了!如果初始化方法里有异步代码,那首先执行的可能就是 onShow ,而不是约定的 onLoad

恢复生命周期顺序

为了保证生命周期函数能够按顺序执行,可以先临时清空生命周期函数,然后再依次执行,如下代码所示:

// utils/wx.js
const LIFETIME_EVENTS = ['onLoad', 'onShow', 'onReady']
var initial = require('./initial')
var originPage = Page


function MyPage(config) {
  LIFETIME_EVENTS.forEach((event) => {
    // 备份生命周期函数
    this.lifetimeBackup[event] = config[event]
    // 临时清空
    config[event] = function() {}
  })
  config.onLoad = function(options) {
    initial(() => {
      // 依次执行生命周期函数
      LIFETIME_EVENTS.forEach((event) => {
        this.lifetimeBackup[event].call(this, options)
      })
    })
  }
  // ...
  originPage(config)
}

注意上述代码还是有问题的,当小程序业务跳走再返回或者切后台到前台时,onShow 无法正常触发,因为被设置为空函数了。

为了保证 onShow 等生命周期函数的后续正常运行,需要在依次执行完生命周期函数后,再把它们恢复到 config 下,这是必不可少的。完整代码如下:

// utils/wx.js
const LIFETIME_EVENTS = ['onLoad', 'onShow', 'onReady']
var initial = require('./initial')
var originPage = Page


function MyPage(config) {
  LIFETIME_EVENTS.forEach((event) => {
    // 备份生命周期函数
    this.lifetimeBackup[event] = config[event]
    // 临时清空
    config[event] = function() {}
  })
  config.onLoad = function(options) {
    initial(() => {
      // 依次执行生命周期函数
      LIFETIME_EVENTS.forEach((event) => {
        this.lifetimeBackup[event].call(this, options)
        // 执行完后,恢复过来
        config[event] = this.lifetimeBackup[event]
      })
    })
  }
  // ...
  originPage(config)
}

总结

代理了 onLoad 后,就可以手动控制其执行的时机,可以折腾的事情就多了很多。比如当初始化函数需要执行(请求)的内容比较多,耗时比较长时,可以统一给页面增加一些 loading 提示等。总之,可以自由控制了。

查看原文

赞 8 收藏 5 评论 0

binnng 发布了文章 · 2019-10-23

小程序技能进阶回忆录 - globalData 的那些事儿

我在严冬下掘起结冰的泥土,我在黑暗的战壕里跃动,我用生命守护房屋的石板,我在散发着焦土味的废墟下长眠。

小程序中,除了每个页面有自己的 data,还有个全局数据存储地方:globalData,获取方式如下:

let globalData = getApp().globalData

实际业务代码中总会遇到这种情况:写着写着发现需要用全局数据,但是不能直接就在当前函数里直接写上面的一段代码,因为要和别的函数共用,所以返回 js 文件顶部添加一段代码,再返回刚刚断掉的地方继续写,类似这样:

// 省略12行
let globalData = getApp().globalData
// 省略15行
Page({
  data: { ... }
  //.. 省略863行
  onButtonTap(e) {
    // ...
    let myData = globalData.myData
    // ...
  }
})

经过这么一折腾,可能刚刚的代码思路都没了。那能不能直接在想用的函数里直接方便的获取这个 globalData 呢?比如:

// 省略27行
Page({
  data: { ... }
  //.. 省略863行
  onButtonTap(e) {
    // ...
    let myData = this.globalData.myData
    // ...
  }
})

或者换种方式获取和设置 myData

let myData = this.$global('myData')
// ...
this.$global('myData', 2)

实现 global 方法

function global(name, value) {
  var globalData = getApp().globalData
  var data = {}
  // this.$global()
  if (arguments.length === 0) {
    return globalData
  }
  // this.$global('myData')
  if (arguments.length === 1) {
    if (is.string(name)) {
      return globalData[name]
    }
    // this.$global({
    //   name: 1
    // })
    if (is.object(name)) {
      data = name
    }
  }
  // this.$global('myData', 2)
  if (arguments.length === 2) {
    data[name] = value
  }
  return extend(data, data)
}

其中,is.stringis.object 是类型判断函数。简单实现了多种操作 globalData 的方法,详细见注释。

挂载到 this

函数有了,那么怎么挂在到小程序页面的 this 中呢,看过前几篇可能知道,要对小程序原有的 Page 函数进行改造。

有两种方式,一种是直接添加到 config 里:

var originPage = Page
var global = require('../utils/global')

function MyPage(config) {
  // ...
  config.$global = global
  // ...
  originPage(config)
}

function page (config) {
  return new MyPage(config)
}

或者在代理后的 onLoad 函数里定义:

var originPage = Page
var global = require('../utils/global')

function MyPage(config) {
  this.watch = config.watch
  this.lifetimeBackup = {
    onLoad: config.onLoad
  }
  config.onLoad = function(options) {
    this.$global = global
    // 其他代码
    this.lifetimeBackup.onLoad.call(this, options)
  }
  
  // ...

  originPage(config)
}

总结

利用这种方式,可以定义很多通用的小程序页面的方法,比如在摩拜小程序内部封装了 this.$utilthis.$navigate等适合自身业务的工具方法等。

查看原文

赞 1 收藏 1 评论 1

binnng 发布了文章 · 2019-10-22

小程序技能进阶回忆录 - 如何自主实现拦截器

窗外,是5月明媚的阳光,澄澈蔚蓝的天空,有炮弹欢快地叫着飞过。50多年过去了,我依然如此清晰地记得。

在一些框架中发现会提供一个很实用的功能:拦截器(interceptor)。例如要实现这个需求:小程序每次获取到定位后都存到 globalData 里:

wx.getLocation({
  // ..
  success(res) {
    getApp().globalData.location = res
    // ...
  }
})

如果每一处使用 wx.getLocation 的地方都这么写也没啥大问题,但总显得不够“智能”,一方面是多了重复代码,另一方面如果需求变动,获取到定位后存到别的地方,那要改很多次。

优雅的拦截器

有了拦截器,可以更优雅的实现它:

intercept('getLocation', {
  success(res) {
    getApp().globalData.location = res
  }
})

只要在一处定义如上的拦截器,其他地方直接用 wx.getLocation 即可。那么,如何实现上面的方式呢?

实现 intercept 方法

// utils/intercept.js
// 存储拦截器定义
var interceptors = {}
function intercept(key, config) {
  intercept[key] = config
}
export {
  intercept,
  interceptors
}

很简单,暴露出 intercept 方法,定义一个存储器也一并暴露出去。

代理 wx

要实现使用 wx.getLocation 自动应用拦截器,就必须基于原有方法重新定义它。

import { interceptors } from './intercept'

// 备份原有微信方法
var wxBackup = {}
[
  'getLocation'
  // 还可以有很多其他方法 ...
].forEach((key) => {
  wxBackup[key] = wx[key]
  wx[key] = (config) => {
    if (interceptors[key]) {
      // 备份业务代码传入的回调方法
      var backup = {}
      var interceptor = interceptors[key]
      [
        'success',
        'fail',
        'complete'
      ].forEach((k) => {
        backup[k] = config[k]
        config[k] = (res) => {
          interceptor[k](res)
          backup[k](res)
        }
      })
    }
    wxBackup[key](config)
  }
})

当然,上述代码用数组列出了所有可能被定义拦截器的微信函数,也可以使用 Object.keys(wx) 通用处理。

更多使用场景

上面的场景比较简单,拦截器的应用还有更多场景。比如每次请求传参带上公参经纬度,接口返回的数据都会约定包裹在 object 中,请求回来需要取一遍。数据异常时还要针对错误码做特定处理,就可以很方便的用拦截器处理:

intercept('request', {
  data(data) {
    var location = getApp().globalData.location
    data.location = location.latitude + ',' + location.longitude
    return data
  },
  success(res) {
    if (res.code == 200) {
      return res.object
    } else {
      if (res.code == 'xxx') {
        // 登录失效,重新登录
        // ....
      }
    }
  }
})

注意,拦截器函数里多了返回值,具体实现方法就不多写,基于上述实现完善代码即可。

总结

细心的读者可能发现,我们代理或者改造了很多微信提供的方法,有些开发者可能不喜欢这样,希望保持原有代码的纯洁性。这要看团队喜好吧,基于此考虑,主要是不想定义太多新的方法或 api,尽量以大家最为熟悉的方式书写代码。

查看原文

赞 1 收藏 1 评论 0

binnng 发布了文章 · 2019-10-22

小程序技能进阶回忆录 - 自主实现数据侦听器和计算器

告诉元首我已尽力,告诉父亲我仍然爱他!

熟悉 Vue 的同学对 computedwatch 一定很熟悉,这些特性大大方便了我们对代码中的数据进行处理:

var vm = new Vue({
  el: '#example',
  data: {
    message: 'Hello'
  },
  computed: {
    // 计算属性的 getter
    reversedMessage: function () {
      // `this` 指向 vm 实例
      return this.message.split('').reverse().join('')
    }
  }
})
var vm = new Vue({
  el: '#demo',
  data: {
    firstName: 'Foo',
    lastName: 'Bar',
    fullName: 'Foo Bar'
  },
  watch: {
    firstName: function (val) {
      this.fullName = val + ' ' + this.lastName
    },
    lastName: function (val) {
      this.fullName = this.firstName + ' ' + val
    }
  }
})

这是 Vue 官网中两段代码。

官方实现

如今小程序也有了自己的实现,详见官方文档 observer 。小程序官方 github
中也开源了通过 Behaviors 实现的 Vue 风格的computedwatchhttps://github.com/wechat-miniprogram/computed

那么在微信没有提供这些方法之前,如何自主实现数据的侦听器和计算属性呢?

## 自主实现

先看看定义的使用文档:

Page({
 data: {
   list: [],
   list2: [],
   size: 0
 },
 // 侦听器函数名必须跟需要被侦听的 data 对象中的属性同名,
 // 其参数中的 newValue 为属性改变后的新值,oldValue 为改变前的旧值
 watch: {
   // 如果 `list` 发生改变,这个函数就会运行
   list(newValue, oldValue) {
     console.log(oldValue + '=>' + newValue)
   }
 },
 // 传入的参数list必须是 data 里面的属性
 // 这里可以传入多个 data 属性
 computed({
   list,
   list2
 }) {
   return {
     size: list.length,
     size2: list2.length
   }
 }
})

Page 的传参中多了两个熟悉的属性,用法不用解释太多。需要继续对小程序提供的 Page 方法进行改造,此外,因为所有数据变化都会用到 setData 方法去触发,所以还需要改造这个方法。

改造 Page 和 setData

想要基于原有的 setData 进行封装,那就得先得到这个函数缓存下来(像是缓存原有的 Page 一样)。想到 onLoad 是小程序页面的第一个生命周期函数,可以在这里进行操作:

// 缓存原有的 `Page`
let originPage = Page

// 定义新的 Page
function MyPage(config) {
  let that = this
  this.watch = config.watch
  this.computed = config.computed
  this.lifetimeBackup = {
    onLoad: config.onLoad
  }
  config.onLoad = function(options) {
    // 缓存下原有的 `setData`
    this._setData = this.setData.bind(this)
    this.setData = (data) => {
      // 侦听器
      that.watching(data)
      // 计算器
      let newData = that.getComputedData(data)
      this._setData(extend(data, newData))
    }
    // 备份下页面实例
    that.context = this
    // 执行真正的 `onLoad`
    this.lifetimeBackup.onLoad.call(this, options)
  }
  
  // ...

  originPage(config)
}

MyPage.prototype.watching = funtion(data) {
  // 执行侦听器
  // ...
}

// 计算器
MyPage.prototype.getComputedData = function(data) {
  // 开始生成新的数据
  // ...
}

function page (config) {
  return new MyPage(config)
}

大致代码如上,重新定义了 this.setData,备份了原有的 setDatathis._setData。当然,这里只考虑了 setData 传一个参数的情况,多个参数需要再对代码优化下。

注意:调用 watchingcreateNewData 的对象是 that,因为 this 指向小程序页面实例,没有自定的这个方法。

做完上述改造,后续的 watchcomputed 就简单多了。

侦听器 watch

MyPage.prototype.watching = function(data) {
  var context = this.context
  var oldData = context.data
  // 开始生成新的数据
  var watch = this.watch
  if (watch) {
    Object.keys(watch).forEach(function (k) {
      // 如果新的 data 中属性被侦听,执行侦听函数
      if (k in data) {
        var newValue = data[k]
        var oldValue = oldData[k]
        watch[k].apply(context, [
          newValue,
          oldValue
        ])
      }
    })
  }
}

简易的侦听器就写好了,通过 setData 触发自定的 watch 中的侦听函数。

计算器 computed

MyPage.prototype.getComputedData = function(data) {
  var context = this.context
  var computed = this.computed
  var computedData
  if (computed) {
    computedData = computed.call(context, data)
  }
  return computedData
}

这样就得到了计算后的新生成的数据:computedData

总结

不断的通过备份、代理微信原有的方法,自主实现了简单的侦听器和计算器。当然这些代码只是为了方便分享提取出来了提供思路,实际业务中遇到情况复杂的多,代码量远远也不止这些。

查看原文

赞 1 收藏 1 评论 0

binnng 发布了文章 · 2019-10-22

小程序技能进阶回忆录 - 在缺少组件化的日子里

战争,信念,意志和情感,这些散发着光芒和硝烟的词汇,象一枚枚炮弹轰入我们现在的生活。历史的记忆不会被抹灭。

当我们在各自项目里幸福的拷贝着官方代码 demo,在 componnets 文件夹里使用 Component 方法书写一个个组件时,不要忘记,在 2018 年上半年以前,小程序是没有提供组件化方案的。

当时,主要有两种解决方法,一种是 WePY 拷贝法,另一种则是摩拜 template 法。

WePY 拷贝法

比如有个最简单的按钮组件:

<!-- components/button.wpy -->
<template>
  <view class="button">
    <button @tap="onTap">点这里</button>
  </view>
</template>

<!-- pages/index.wpy -->
<template>
  <view class="container">
    <wpy-button /> // button 组件1
    <wpy-button2 /> // button 组件2
  </view>
</template>

经过编译后结果如下:

<view class="container">
  <view class="button">
    <button bindtap="$wpyButton$onTap">点这里</button>
  </view>
  <view class="button">
    <button bindtap="$wpyButton2$onTap">点这里</button>
  </view>
</view>

为了方便变量隔离,所以引入到页面中的组件得单独命名:

import wepy from 'wepy'
import Button from '@/components/button'
export default class Index extends wepy.page {
  components = {
    'wpy-button': Button,
    'wpy-button2': Button
  }
  ...
}

有一些不便的地方,但也很好的解决了组件化缺失的问题。

摩拜 template 法

有心的同学可能记得当初我们发了这篇文章:微信小程序组件化解决方案wx-component,当时主要讲了如何使用,这次讲讲技术的细节。

主要利用小程序当时提供的 template 模板方法,使用方式如下:

<!-- pages/template/login.wxml -->
<template name="login">
  <view class="login">这是登录组件</view>
</template>
<!-- pages/login/index.wxml -->
<import data-original='../../components/login/index.wxml'/>
<view class="login-box">
  <template is="login" data="{{...}}"></template>
</view>

由于知道这只是临时的解决方法,最终还会迁移到微信官方组件化方案。了解到微信团队正在开发,就死皮赖脸找了微信研发同学要下技术方案,以便后期迁移成本做到最低。最后微信同学不耐烦的扔给我们如下代码,并特别嘱咐不要泄露出去

Component({
  // 组件名
  name: '',
  // 为其他组件指定别名
  using: {},
  // 类似mixins,组件间代码复用
  behaviors: [],
  // 组件私有数据
  data: {
  },
  // 外部传入的组件属性
  propties: {
  },
  // 当组件被加载
  attached () {
  },
  // 当组件被卸载
  detached () {
  },
  // 组件私有方法
  methods: {
  }
})

一目了然,依照此文档实现一个简单的组件化方案也有了思路。

如何引入组件

由于没有办法在小程序全局注入 Component 方法,可以将组件代码以模块方式导出,在页面的 Page 方法里引入:

// components/login/index.wxml
<template name="login">
  <form bindsubmit="onLoginSubmit">
    ...
    <button type="primary" formType="submit">{{btnText}}</button>
  </form>
</template>
// components/login/index.js
module.exports = {
  name: 'login',
  data: {
    btnText: ''
  }
  ....
}
// pages/index/index.js
Page({
  data: {
    ...
  },
  components: {
    login: {
      btnText: '开始',
      onLoginCallback() { ... }
    }
  }
})
<!-- pages/index/index.wxml -->
<import data-original='../../components/login/index.wxml'/>
<view class="login-box">
  <template is="login" data="{{...login}}"></template>
</view>

Page 的传参里多了 components 属性,传入了组件名login,以及组件对应的属性值和方法。为了使这些新增传参生效,那势必需要对 Page 进行改造。

改造 Page

如何用一行代码毁掉你的小程序,在小程序根目录的 app.js 里加入这段代码即可:

Page = funtion() {}

这样核心的 Page 的方法就被覆盖掉了,所以利用这个“特性”,可以改造 Page 方法:

// utils/wx.js
var page = function() {
  // 改造代码
  ...
}
module.exports = {
  page
}
// app.js
Page = require('./utils/wx').page

这就完成了独一无二的自定义的小程序 Page 的方法。

Component 怎么写

精简了核心的代码如下:

function noop() {}

class Component {
  constructor (config) {
    // 兼容 onLoad onUnload 的写法
    config.onLoad = config.onLoad || config.attached || noop
    config.onUnload = config.onUnload || config.detached || noop
    this.data = config.data || {}
    this.config = config
    this.methods = config.methods || {}
    for (let name in this.methods) {
      // 为了使组件事件绑定生效,直接挂在到 this 下
      this[name] = methods[name]
    }
  }
  setData (data, deepExtend) {
    let name = this.name
    let parent = this.parent
    let mergeData = extend(deepExtend !== false, parent.data[name], data)
    let newData = {}
    newData[name] = mergeData
    this.data = mergeData
    // 更新页面的 data
    parent.setData(newData)
  }
  setName (name) {
    this.name = name
  }
  setParent (parent) {
    this.parent = parent
  }
}

主要完成了三件事:

  • 配置了组件的生命周期事件 attacheddetached
  • 绑定了组件的事件,使得 templatebindtap 等代码生效
  • 实现了组件的 setData 功能

有个细节,为了让大家容易理解(不泄露微信的方法名),分享到外部的文章用 onLoadonUnload 代替了 attacheddetached,但内部早就开始用微信命名的这两个属性名,才有了代码中的兼容写法。

自定的 Page 怎么写

整理了大致的核心代码如下:

// 缓存下微信的 Page
const originalPage = Page
// 组件生命周期
const LIFETIME_EVENT = [
  'onLoad',
  'onUnload'
]
class MyPage {
  constructor (origin) {
    this.origin = origin
    this.config = {}
    this.children = {}
    this.childrenEvents = {}

    // 是否需要`components`
    let components = this.components = origin.components

    if (components) {
      this.config.data = {}
      for (let item in components) {
        let props = components[item] || {}
        let component = new Component(require(`../components/${item}/index`))

        this.children[name] = component
        // 合并组件的 data
        extend(component.data, component.props)
        // ...
        // 合并组件的 method
        for (let fnName in component.methods) {
          this.config[fnName] = component.methods[fnName].bind(component)
        }
        // ...
        let childrenEvents = this.childrenEvents[item] = {}
        LIFETIME_EVENT.forEach((prop) => {
          childrenEvents[item][prop] = component.config[prop]
        })
      }
      
      // 合并所有依赖组件的生命周期函数
      LIFETIME_EVENT.forEach((prop) => {
        this.config[prop] = () => {
          for (let item in this.components) {
            this.childrenEvents[item][prop].apply(this.component, arguments)
          }
          this.origin[prop] && this.origin[prop].apply(this, arguments)
        }
      })

      // 把新生成的 config 传给原始的微信的 Page 方法
      originalPage(this.config)
    } else {
      // 没有依赖组件,直接透传给微信的 Page 方法
      originalPage(origin)
    }

  }
}

可能有点乱,其实就是不断 merge datamethod的过程。最终所有组件自定的数据和方法都被挂在到了 Page 的传参里。

最后,导出自定义的 page

// utils/wx.js
const page = function (config) {
  return new MyPage(config)
}

module.exports = {
  page
}

app.js 中覆盖掉原有的 Page 方法:

// app.js
Page = require('./utils/wx').page

不完善的地方

虽然满足业务了,但也是有些问题的,例如上面 MyPage 方法里的这段:

for (let fnName in component.methods) {
  this.config[fnName] = component.methods[fnName].bind(component)
}

可以看出,直接把组件内部定义的方法,挂在到 config 中去了,这就要求页面的方法和组件的方法不能重名,这是为了方便 template 可以直接绑定组件定义的事件,只能通过把组件事件转移到页面的事件方法里。

也有很多不完善的地方,但通过内部约束代码规范也基本可以解决。

结语

这种近乎 Hack 的方式支撑了摩拜单车小程序业务大半年的时间,期间产出了大大小小十多个组件。而由于组件内部基本是按照微信官方组件化 api 书写,等待官方推出组件化方案后,全部迁移过去的成本也大大减小。

查看原文

赞 2 收藏 2 评论 0

binnng 发布了文章 · 2019-10-17

小程序技能进阶回忆录 - 也许你并不需要小程序框架

你也许并不需要小程序框架。

市面上不停的会有大的公司推出自己的小程序的研发库 / 框架,功能十分强大,也为小程序的开发带来了便利。但在一些积极的反馈中,我们也看到不一样的声音:

  • @白小虫:又一个轮子。。
  • @jsweber:小程序不用任何框架,开发体验也不错啊,本身就借鉴了 vue 和 react 的思想。
  • @月月木子:现在中上流公司的前端都很热衷于造自己的轮子或者给别人的轮子换皮然后说是自己的轮子,疯狂垒高自身的技术壁垒,即有了给领导吹牛的资本,让新来的人肯定属于不熟练工,又可以给自身带来安全感,不知道到底是好是坏。
  • @redbuck:轮子越造越多,我寻思下一个小程序要不转回原生算了。
  • @fantasy525:感觉一次编码全端支持没多大必要,支持的越多就可能会出越多的bug,我们开发时会很难受,本来只解决一端的bug,多端我们又要解决其他端,还不如各司其职好。
  • @肉很多:学不动了呀。。。。😠
上列评论从掘金用户评论中挑选。

这几天公司校招,面了一些在校生,其中有两位同学让人印象深刻:

一、同学 A 面试接近尾声突然问起,你们被美团收购后,是不是都要必须使用 mpvue(美团早年推出的小程序框架)?当我回答不是后,同学 A 长舒一口气:那就好。他解释道:更偏向用原生去写小程序,因为微信团队更新节奏比较快,框架经常跟不上微信的节奏,导致新特性无法在项目中使用。

二、同学 B 在简历中写道精通 jQuery,我在想这年头了,jQ 在简历中出现的越来越少了,故意抓着这个问了下,果然比较了解。他说道:经常用 jQ 做一些网页 demo,因为上手比较容易,直接引入一个cdn js就行,都不用装 node 包。

em... 好像说的都挺在理。

在摩拜单车内部,我们封装了基于微信小程序原生语法进行扩展、对原生微信 API 支持友好的小程序基础库 - Mozzy。注意:其定位是基础库,而不是框架。只要在原生语法的小程序项目里引入一个 js 文件就可以使用,即便是开发到一半的小程序也可以快速引入。

记得刚做完这个项目在公司内部分享时,说到未来的愿景时最后一句是:

也许有一天,当使用了 Mozzy 开发的小程序,删除 mozzy.js 后,发现功能竟然一切正常。

相信早些年用 jQ 做项目的时候很多同学都脑洞过,要是浏览器原生支持 jQ 的 api 多好,或者干脆浏览器直接集成 jQ,就不用在每个项目的 html 里都引入一段 jQ 代码了,毕竟 jQ 在当年几乎是网页开发必备基础库。

随着微信官方 api 的更新,Mozzy 的很多实现都有了官方支持。也许当时分享的未来愿景已经来了,最近要开启删(改造) Mozzy 行动,换种方式存在于千万行代码中。

拥抱变化。

接下来的一系列文章里,会记录下 Mozzy 甚至是整个摩拜单车小程序研发细节和心路历程,可称为小程序技能进阶回忆录

主要内容目录如下,大部分已经写完,会不定期进行更新:

  • 小程序技能进阶回忆录 - 在缺少组件化的日子里
  • 小程序技能进阶回忆录 - 自主实现数据侦听器和计算器
  • 小程序技能进阶回忆录 - 自主实现拦截器
  • 小程序技能进阶回忆录 - globalData 的那些事儿
  • 小程序技能进阶回忆录 - 什么时候执行 onLoad
  • 小程序技能进阶回忆录 - 增强型的 wx.navigateBack

当然,不排除标题修改为“标题党”形式糊弄人。正如此篇一样。

查看原文

赞 8 收藏 3 评论 0

binnng 赞了文章 · 2018-03-17

深入理解学习Git工作流(git-workflow-tutorial)

个人在学习git工作流的过程中,从原有的 SVN 模式很难完全理解git的协作模式,直到有一天我看到了下面的文章,好多遗留在心中的困惑迎刃而解,于是我将这部分资料进行整理放到了github上,欢迎star查看最新更新内容, https://github.com/xirong/my-git/blob/master/git-workflow-tutorial.md

  • 我们以使用SVN的工作流来使用git有什么不妥?
  • git 方便的branch在哪里,团队多人如何协作?冲突了怎么办?如何进行发布控制?
  • 经典的master-发布、develop-主开发、hotfix-不过修复如何避免代码不经过验证上线?
  • 如何在github上面与他人一起协作,star-fork-pull request是怎样的流程?

我个人很感激这篇文章,所以进行了整理,希望能帮到更多的人。整篇文章由 xirong 整理自 oldratlee 的github,方便统一的学习回顾,在此感谢下面两位的贡献。

原文链接:Git Workflows and Tutorials
简体中文:由 oldratlee 翻译在 github 上 git-workflows-and-tutorials


一、译序

工作流其实不是一个初级主题,背后的本质问题其实是有效的项目流程管理和高效的开发协同约定,不仅是GitSVNVCSSCM工具的使用。

这篇指南以大家在SVN中已经广为熟悉使用的集中式工作流作为起点,循序渐进地演进到其它高效的分布式工作流,还介绍了如何配合使用便利的Pull Request功能,体系地讲解了各种工作流的应用。

行文中实践原则和操作示例并重,对于Git的资深玩家可以梳理思考提升,而新接触的同学,也可以跟着step-by-step操作来操练学习并在实际工作中上手使用。

关于Git工作流主题,网上体系的中文资料不多,主要是零散的操作说明,希望这篇文章能让你更深入理解并在工作中灵活有效地使用起来。

PS

文中Pull Request的介绍用的是Bitbucket代码托管服务,由于和GitHub基本一样,如果你用的是GitHub(我自己也主要使用GitHub托管代码),不影响理解和操作。

PPS

本指南循序渐进地讲解工作流,如果Git用的不多,可以从前面的讲的工作流开始操练。操作过程去感受指南的讲解:解决什么问题、如何解决问题,这样理解就深了,也方便活用。

Gitflow工作流是经典模型,体现了工作流的经验和精髓。随着项目过程复杂化,会感受到这个工作流中深思熟虑和威力!

Forking工作流是协作的(GitHub风格)可以先看看Github的Help:Fork A RepoUsing pull requests 。照着操作,给一个Github项目贡献你的提交,有操作经验再看指南容易意会。指南中给了自己实现Fork的方法Fork就是服务端的克隆。在指南的操练中使用代码托管服务(如GitHubBitbucket),可以点一下按钮就让开发者完成仓库的fork操作。

:see_no_evil: 自己理解粗浅,翻译中不足和不对之处,欢迎建议(提交Issue)和指正(Fork后提交代码)!

二、Git工作流指南

:point_right: 工作流有各式各样的用法,但也正因此使得在实际工作中如何上手使用变得很头大。这篇指南通过总览公司团队中最常用的几种Git工作流让大家可以上手使用。

在阅读的过程中请记住,本文中的几种工作流是作为方案指导而不是条例规定。在展示了各种工作流可能的用法后,你可以从不同的工作流中挑选或揉合出一个满足你自己需求的工作流。

Git Workflows

2.1 集中式工作流

如果你的开发团队成员已经很熟悉Subversion,集中式工作流让你无需去适应一个全新流程就可以体验Git带来的收益。这个工作流也可以作为向更Git风格工作流迁移的友好过渡。
Git Workflows: SVN-style

转到分布式版本控制系统看起来像个令人生畏的任务,但不改变已用的工作流你也可以用上Git带来的收益。团队可以用和Subversion完全不变的方式来开发项目。

但使用Git加强开发的工作流,Git有相比SVN的几个优势。
首先,每个开发可以有属于自己的整个工程的本地拷贝。隔离的环境让各个开发者的工作和项目的其他部分修改独立开来 ——
即自由地提交到自己的本地仓库,先完全忽略上游的开发,直到方便的时候再把修改反馈上去。

其次,Git提供了强壮的分支和合并模型。不像SVNGit的分支设计成可以做为一种用来在仓库之间集成代码和分享修改的『失败安全』的机制。

2.1.1 工作方式

Subversion一样,集中式工作流以中央仓库作为项目所有修改的单点实体。相比SVN缺省的开发分支trunkGit叫做master,所有修改提交到这个分支上。本工作流只用到master这一个分支。

开发者开始先克隆中央仓库。在自己的项目拷贝中像SVN一样的编辑文件和提交修改;但修改是存在本地的,和中央仓库是完全隔离的。开发者可以把和上游的同步延后到一个方便时间点。

要发布修改到正式项目中,开发者要把本地master分支的修改『推』到中央仓库中。这相当于svn commit操作,但push操作会把所有还不在中央仓库的本地提交都推上去。

git-workflow-svn-push-local

2.1.2 冲突解决

中央仓库代表了正式项目,所以提交历史应该被尊重且是稳定不变的。如果开发者本地的提交历史和中央仓库有分歧,Git会拒绝push提交否则会覆盖已经在中央库的正式提交。

git-workflow-svn-managingconflicts

在开发者提交自己功能修改到中央库前,需要先fetch在中央库的新增提交,rebase自己提交到中央库提交历史之上。
这样做的意思是在说,『我要把自己的修改加到别人已经完成的修改上。』最终的结果是一个完美的线性历史,就像以前的SVN的工作流中一样。

如果本地修改和上游提交有冲突,Git会暂停rebase过程,给你手动解决冲突的机会。Git解决合并冲突,用和生成提交一样的git statusgit add命令,很一致方便。还有一点,如果解决冲突时遇到麻烦,Git可以很简单中止整个rebase操作,重来一次(或者让别人来帮助解决)。

2.1.3 示例

让我们一起逐步分解来看看一个常见的小团队如何用这个工作流来协作的。有两个开发者小明和小红,看他们是如何开发自己的功能并提交到中央仓库上的。

有人先初始化好中央仓库

第一步,有人在服务器上创建好中央仓库。如果是新项目,你可以初始化一个空仓库;否则你要导入已有的GitSVN仓库。

中央仓库应该是个裸仓库(bare repository),即没有工作目录(working directory)的仓库。可以用下面的命令创建:

bashssh user@host
git init --bare /path/to/repo.git

确保写上有效的userSSH的用户名),host(服务器的域名或IP地址),/path/to/repo.git(你想存放仓库的位置)。
注意,为了表示是一个裸仓库,按照约定加上.git扩展名到仓库名上。

所有人克隆中央仓库

下一步,各个开发者创建整个项目的本地拷贝。通过git clone命令完成:

bashgit clone ssh://user@host/path/to/repo.git

基于你后续会持续和克隆的仓库做交互的假设,克隆仓库时Git会自动添加远程别名origin指回『父』仓库。

小明开发功能

在小明的本地仓库中,他使用标准的Git过程开发功能:编辑、暂存(Stage)和提交。
如果你不熟悉暂存区(Staging Area),这里说明一下:暂存区的用来准备一个提交,但可以不用把工作目录中所有的修改内容都包含进来。
这样你可以创建一个高度聚焦的提交,尽管你本地修改很多内容。

bashgit status # 查看本地仓库的修改状态
git add # 暂存文件
git commit # 提交文件

请记住,因为这些命令生成的是本地提交,小明可以按自己需求反复操作多次,而不用担心中央仓库上有了什么操作。
对需要多个更简单更原子分块的大功能,这个做法是很有用的。

小红开发功能

与此同时,小红在自己的本地仓库中用相同的编辑、暂存和提交过程开发功能。和小明一样,她也不关心中央仓库有没有新提交;
当然更不关心小明在他的本地仓库中的操作,因为所有本地仓库都是私有的。

小明发布功能

一旦小明完成了他的功能开发,会发布他的本地提交到中央仓库中,这样其它团队成员可以看到他的修改。他可以用下面的git push命令

bashgit push origin master

注意,origin是在小明克隆仓库时Git创建的远程中央仓库别名。master参数告诉Git推送的分支。
由于中央仓库自从小明克隆以来还没有被更新过,所以push操作不会有冲突,成功完成。

小红试着发布功能

一起来看看在小明发布修改后,小红push修改会怎么样?她使用完全一样的push命令:

bashgit push origin master

但她的本地历史已经和中央仓库有分岐了,Git拒绝操作并给出下面很长的出错消息:

error: failed to push some refs to '/path/to/repo.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Merge the remote changes (e.g. 'git pull')
hint: before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

这避免了小红覆写正式的提交。她要先pull小明的更新到她的本地仓库合并上她的本地修改后,再重试。

小红在小明的提交之上rebase

小红用git pull合并上游的修改到自己的仓库中。
这条命令类似svn update——拉取所有上游提交命令到小红的本地仓库,并尝试和她的本地修改合并:

bashgit pull --rebase origin master

--rebase选项告诉Git把小红的提交移到同步了中央仓库修改后的master分支的顶部,如下图所示:

如果你忘加了这个选项,pull操作仍然可以完成,但每次pull操作要同步中央仓库中别人修改时,提交历史会以一个多余的『合并提交』结尾。
对于集中式工作流,最好是使用rebase而不是生成一个合并提交。

小红解决合并冲突

rebase操作过程是把本地提交一次一个地迁移到更新了的中央仓库master分支之上。
这意味着可能要解决在迁移某个提交时出现的合并冲突,而不是解决包含了所有提交的大型合并时所出现的冲突。
这样的方式让你尽可能保持每个提交的聚焦和项目历史的整洁。反过来,简化了哪里引入Bug的分析,如果有必要,回滚修改也可以做到对项目影响最小。

如果小红和小明的功能是相关的,不大可能在rebase过程中有冲突。如果有,Git在合并有冲突的提交处暂停rebase过程,输出下面的信息并带上相关的指令:

CONFLICT (content): Merge conflict in <some-file>

Git很赞的一点是,任何人可以解决他自己的冲突。在这个例子中,小红可以简单的运行git status命令来查看哪里有问题。
冲突文件列在Unmerged paths(未合并路径)一节中:

# Unmerged paths:
# (use "git reset HEAD <some-file>..." to unstage)
# (use "git add/rm <some-file>..." as appropriate to mark resolution)
#
# both modified: <some-file>

接着小红编辑这些文件。修改完成后,用老套路暂存这些文件,并让git rebase完成剩下的事:

bashgit add <some-file> 
git rebase --continue

要做的就这些了。Git会继续一个一个地合并后面的提交,如其它的提交有冲突就重复这个过程。

如果你碰到了冲突,但发现搞不定,不要惊慌。只要执行下面这条命令,就可以回到你执行git pull --rebase命令前的样子:

bashgit rebase --abort

小红成功发布功能

小红完成和中央仓库的同步后,就能成功发布她的修改了:

bashgit push origin master

如你所见,仅使用几个Git命令我们就可以模拟出传统Subversion开发环境。对于要从SVN迁移过来的团队来说这太好了,但没有发挥出Git分布式本质的优势。

如果你的团队适应了集中式工作流,但想要更流畅的协作效果,绝对值得探索一下 功能分支工作流 的收益。
通过为一个功能分配一个专门的分支,能够做到一个新增功能集成到正式项目之前对新功能进行深入讨论。


2.2 功能分支工作流

功能分支工作流以集中式工作流为基础,不同的是为各个新功能分配一个专门的分支来开发。这样可以在把新功能集成到正式项目前,用Pull Requests的方式讨论变更。

Git Workflows: Feature Branch

一旦你玩转了集中式工作流,在开发过程中可以很简单地加上功能分支,用来鼓励开发者之间协作和简化交流。

功能分支工作流背后的核心思路是所有的功能开发应该在一个专门的分支,而不是在master分支上。
这个隔离可以方便多个开发者在各自的功能上开发而不会弄乱主干代码。
另外,也保证了master分支的代码一定不会是有问题的,极大有利于集成环境。

功能开发隔离也让pull requests工作流成功可能,
pull requests工作流能为每个分支发起一个讨论,在分支合入正式项目之前,给其它开发者有表示赞同的机会。
另外,如果你在功能开发中有问题卡住了,可以开一个pull requests来向同学们征求建议。
这些做法的重点就是,pull requests让团队成员之间互相评论工作变成非常方便!

2.2.1 工作方式

功能分支工作流仍然用中央仓库,并且master分支还是代表了正式项目的历史。
但不是直接提交本地历史到各自的本地master分支,开发者每次在开始新功能前先创建一个新分支。
功能分支应该有个有描述性的名字,比如animated-menu-itemsissue-#1061,这样可以让分支有个清楚且高聚焦的用途。

master分支和功能分支之间,Git是没有技术上的区别,所以开发者可以用和集中式工作流中完全一样的方式编辑、暂存和提交修改到功能分支上。

另外,功能分支也可以(且应该)push到中央仓库中。这样不修改正式代码就可以和其它开发者分享提交的功能。
由于master仅有的一个『特殊』分支,在中央仓库上存多个功能分支不会有任何问题。当然,这样做也可以很方便地备份各自的本地提交。

2.2.2 Pull Requests

功能分支除了可以隔离功能的开发,也使得通过Pull Requests讨论变更成为可能。
一旦某个开发完成一个功能,不是立即合并到master,而是push到中央仓库的功能分支上并发起一个Pull Request请求去合并修改到master
在修改成为主干代码前,这让其它的开发者有机会先去Review变更。

Code ReviewPull Requests的一个重要的收益,但Pull Requests目的是讨论代码一个通用方式。
你可以把Pull Requests作为专门给某个分支的讨论。这意味着可以在更早的开发过程中就可以进行Code Review
比如,一个开发者开发功能需要帮助时,要做的就是发起一个Pull Request,相关的人就会自动收到通知,在相关的提交旁边能看到需要帮助解决的问题。

一旦Pull Request被接受了,发布功能要做的就和集中式工作流就很像了。
首先,确定本地的master分支和上游的master分支是同步的。然后合并功能分支到本地master分支并push已经更新的本地master分支到中央仓库。

仓库管理的产品解决方案像BitbucketStash,可以良好地支持Pull Requests。可以看看StashPull Requests文档

2.2.3 示例

下面的示例演示了如何把Pull Requests作为Code Review的方式,但注意Pull Requests可以用于很多其它的目的。

小红开始开发一个新功能

在开始开发功能前,小红需要一个独立的分支。使用下面的命令新建一个分支

bashgit checkout -b marys-feature master

这个命令检出一个基于master名为marys-feature的分支,Git-b选项表示如果分支还不存在则新建分支。
这个新分支上,小红按老套路编辑、暂存和提交修改,按需要提交以实现功能:

bashgit status
git add <some-file>
git commit

小红要去吃个午饭

早上小红为新功能添加一些提交。
去吃午饭前,push功能分支到中央仓库是很好的做法,这样可以方便地备份,如果和其它开发协作,也让他们可以看到小红的提交。

bashgit push -u origin marys-feature

这条命令pushmarys-feature分支到中央仓库(origin),-u选项设置本地分支去跟踪远程对应的分支。
设置好跟踪的分支后,小红就可以使用git push命令省去指定推送分支的参数。

小红完成功能开发

小红吃完午饭回来,完成整个功能的开发。在合并到master之前
她发起一个Pull Request让团队的其它人知道功能已经完成。但首先,她要确认中央仓库中已经有她最近的提交:

bashgit push

然后,在她的GitGUI客户端中发起Pull Request,请求合并marys-featuremaster,团队成员会自动收到通知。
Pull Request很酷的是可以在相关的提交旁边显示评注,所以你可以很对某个变更集提问。

小黑收到Pull Request

小黑收到了Pull Request后会查看marys-feature的修改。决定在合并到正式项目前是否要做些修改,且通过Pull Request和小红来回地讨论。

小红再做修改

要再做修改,小红用和功能第一个迭代完全一样的过程。编辑、暂存、提交并push更新到中央仓库。小红这些活动都会显示在Pull Request上,小黑可以断续做评注。

如果小黑有需要,也可以把marys-feature分支拉到本地,自己来修改,他加的提交也会一样显示在Pull Request上。

小红发布她的功能

一旦小黑可以的接受Pull Request,就可以合并功能到稳定项目代码中(可以由小黑或是小红来做这个操作):

bashgit checkout master
git pull
git pull origin marys-feature
git push

无论谁来做合并,首先要检出master分支并确认是它是最新的。然后执行git pull origin marys-feature合并marys-feature分支到和已经和远程一致的本地master分支。
你可以使用简单git merge marys-feature命令,但前面的命令可以保证总是最新的新功能分支。
最后更新的master分支要重新push回到origin

这个过程常常会生成一个合并提交。有些开发者喜欢有合并提交,因为它像一个新功能和原来代码基线的连通符。
但如果你偏爱线性的提交历史,可以在执行合并时rebase新功能到master分支的顶部,这样生成一个快进(fast-forward)的合并。

一些GUI客户端可以只要点一下『接受』按钮执行好上面的命令来自动化Pull Request接受过程。
如果你的不能这样,至少在功能合并到master分支后能自动关闭Pull Request

与此同时,小明在做和小红一样的事

当小红和小黑在marys-feature上工作并讨论她的Pull Request的时候,小明在自己的功能分支上做完全一样的事。

通过隔离功能到独立的分支上,每个人都可以自主的工作,当然必要的时候在开发者之间分享变更还是比较繁琐的。

到了这里,但愿你发现了功能分支可以很直接地在 集中式工作流 的仅有的master分支上完成多功能的开发。
另外,功能分支还使用了Pull Request,使得可以在你的版本控制GUI客户端中讨论某个提交。

功能分支工作流是开发项目异常灵活的方式。问题是,有时候太灵活了。对于大型团队,常常需要给不同分支分配一个更具体的角色。
Gitflow工作流是管理功能开发、发布准备和维护的常用模式。


2.3 Gitflow工作流

Gitflow工作流通过为功能开发、发布准备和维护分配独立的分支,让发布迭代过程更流畅。严格的分支模型也为大型项目提供了一些非常必要的结构。

Git Workflows: Gitflow Cycle

这节介绍的Gitflow工作流借鉴自在nvieVincent Driessen

Gitflow工作流定义了一个围绕项目发布的严格分支模型。虽然比功能分支工作流复杂几分,但提供了用于一个健壮的用于管理大型项目的框架。

Gitflow工作流没有用超出功能分支工作流的概念和命令,而是为不同的分支分配一个很明确的角色,并定义分支之间如何和什么时候进行交互。
除了使用功能分支,在做准备、维护和记录发布也使用各自的分支。
当然你可以用上功能分支工作流所有的好处:Pull Requests、隔离实验性开发和更高效的协作。

2.3.1 工作方式

Gitflow工作流仍然用中央仓库作为所有开发者的交互中心。和其它的工作流一样,开发者在本地工作并push分支到要中央仓库中。

2.3.2 历史分支

相对使用仅有的一个master分支,Gitflow工作流使用2个分支来记录项目的历史。master分支存储了正式发布的历史,而develop分支作为功能的集成分支。
这样也方便master分支上的所有提交分配一个版本号。

剩下要说明的问题围绕着这2个分支的区别展开。

2.3.3 功能分支

每个新功能位于一个自己的分支,这样可以push到中央仓库以备份和协作
但功能分支不是从master分支上拉出新分支,而是使用develop分支作为父分支。当新功能完成时,合并回develop分支
新功能提交应该从不直接与master分支交互。

注意,从各种含义和目的上来看,功能分支加上develop分支就是功能分支工作流的用法。但Gitflow工作流没有在这里止步。

2.3.4 发布分支

一旦develop分支上有了做一次发布(或者说快到了既定的发布日)的足够功能,就从develop分支上fork一个发布分支。
新建的分支用于开始发布循环,所以从这个时间点开始之后新的功能不能再加到这个分支上——
这个分支只应该做Bug修复、文档生成和其它面向发布任务。
一旦对外发布的工作都完成了,发布分支合并到master分支并分配一个版本号打好Tag
另外,这些从新建发布分支以来的做的修改要合并回develop分支。

使用一个用于发布准备的专门分支,使得一个团队可以在完善当前的发布版本的同时,另一个团队可以继续开发下个版本的功能。
这也打造定义良好的开发阶段(比如,可以很轻松地说,『这周我们要做准备发布版本4.0』,并且在仓库的目录结构中可以实际看到)。

常用的分支约定:

用于新建发布分支的分支: develop
用于合并的分支: master
分支命名: release-* 或 release/*

2.3.5 维护分支

维护分支或说是热修复(hotfix)分支用于生成快速给产品发布版本(production releases)打补丁,这是唯一可以直接从master分支fork出来的分支。
修复完成,修改应该马上合并回master分支和develop分支(当前的发布分支),master分支应该用新的版本号打好Tag

Bug修复使用专门分支,让团队可以处理掉问题而不用打断其它工作或是等待下一个发布循环。
你可以把维护分支想成是一个直接在master分支上处理的临时发布。

2.3.6 示例

下面的示例演示本工作流如何用于管理单个发布循环。假设你已经创建了一个中央仓库。

创建开发分支

第一步为master分支配套一个develop分支。简单来做可以本地创建一个空的develop分支push到服务器上:

bashgit branch develop
git push -u origin develop

以后这个分支将会包含了项目的全部历史,而master分支将只包含了部分历史。其它开发者这时应该克隆中央仓库,建好develop分支的跟踪分支:

bashgit clone ssh://user@host/path/to/repo.git
git checkout -b develop origin/develop

现在每个开发都有了这些历史分支的本地拷贝。

小红和小明开始开发新功能

这个示例中,小红和小明开始各自的功能开发。他们需要为各自的功能创建相应的分支。新分支不是基于master分支,而是应该基于develop分支

bashgit checkout -b some-feature develop

他们用老套路添加提交到各自功能分支上:编辑、暂存、提交:

bashgit status
git add <some-file>
git commit

小红完成功能开发

添加了提交后,小红觉得她的功能OK了。如果团队使用Pull Requests,这时候可以发起一个用于合并到develop分支。
否则她可以直接合并到她本地的develop分支后push到中央仓库:

bashgit pull origin develop
git checkout develop
git merge some-feature
git push
git branch -d some-feature

第一条命令在合并功能前确保develop分支是最新的。注意,功能决不应该直接合并到master分支。
冲突解决方法和集中式工作流一样。

小红开始准备发布

这个时候小明正在实现他的功能,小红开始准备她的第一个项目正式发布。
像功能开发一样,她用一个新的分支来做发布准备。这一步也确定了发布的版本号:

bashgit checkout -b release-0.1 develop

这个分支是清理发布、执行所有测试、更新文档和其它为下个发布做准备操作的地方,像是一个专门用于改善发布的功能分支。

只要小红创建这个分支并push到中央仓库,这个发布就是功能冻结的。任何不在develop分支中的新功能都推到下个发布循环中。

小红完成发布

一旦准备好了对外发布,小红合并修改到master分支和develop分支上,删除发布分支。合并回develop分支很重要,因为在发布分支中已经提交的更新需要在后面的新功能中也要是可用的。
另外,如果小红的团队要求Code Review,这是一个发起Pull Request的理想时机。

bashgit checkout master
git merge release-0.1
git push
git checkout develop
git merge release-0.1
git push
git branch -d release-0.1

发布分支是作为功能开发(develop分支)和对外发布(master分支)间的缓冲。只要有合并到master分支,就应该打好Tag以方便跟踪。

bashgit tag -a 0.1 -m "Initial public release" master
git push --tags

Git有提供各种勾子(hook),即仓库有事件发生时触发执行的脚本。
可以配置一个勾子,在你push中央仓库的master分支时,自动构建好对外发布。

最终用户发现Bug

对外发布后,小红回去和小明一起做下个发布的新功能开发,直到有最终用户开了一个Ticket抱怨当前版本的一个Bug
为了处理Bug,小红(或小明)从master分支上拉出了一个维护分支,提交修改以解决问题,然后直接合并回master分支:

bashgit checkout -b issue-#001 master
# Fix the bug
git checkout master
git merge issue-#001
git push

就像发布分支,维护分支中新加这些重要修改需要包含到develop分支中,所以小红要执行一个合并操作。然后就可以安全地删除这个分支了:

bashgit checkout develop
git merge issue-#001
git push
git branch -d issue-#001

到了这里,但愿你对集中式工作流功能分支工作流Gitflow工作流已经感觉很舒适了。
你应该也牢固的掌握了本地仓库的潜能,push/pull模式和Git健壮的分支和合并模型。

记住,这里演示的工作流只是可能用法的例子,而不是在实际工作中使用Git不可违逆的条例。
所以不要畏惧按自己需要对工作流的用法做取舍。不变的目标就是让Git为你所用。


2.4 Forking工作流

Forking工作流是分布式工作流,充分利用了Git在分支和克隆上的优势。可以安全可靠地管理大团队的开发者(developer),并能接受不信任贡献者(contributor)的提交。

Forking工作流和前面讨论的几种工作流有根本的不同,这种工作流不是使用单个服务端仓库作为『中央』代码基线,而让各个开发者都有一个服务端仓库。这意味着各个代码贡献者有2个Git仓库而不是1个:一个本地私有的,另一个服务端公开的。

Forking工作流的一个主要优势是,贡献的代码可以被集成,而不需要所有人都能push代码到仅有的中央仓库中。
开发者push到自己的服务端仓库,而只有项目维护者才能push到正式仓库。
这样项目维护者可以接受任何开发者的提交,但无需给他正式代码库的写权限。

效果就是一个分布式的工作流,能为大型、自发性的团队(包括了不受信的第三方)提供灵活的方式来安全的协作。
也让这个工作流成为开源项目的理想工作流。

2.4.1 工作方式

和其它的Git工作流一样,Forking工作流要先有一个公开的正式仓库存储在服务器上。
但一个新的开发者想要在项目上工作时,不是直接从正式仓库克隆,而是fork正式项目在服务器上创建一个拷贝。

这个仓库拷贝作为他个人公开仓库 ——
其它开发者不允许push到这个仓库,但可以pull到修改(后面我们很快就会看这点很重要)。
在创建了自己服务端拷贝之后,和之前的工作流一样,开发者执行git clone命令克隆仓库到本地机器上,作为私有的开发环境。

要提交本地修改时,push提交到自己公开仓库中 —— 而不是正式仓库中。
然后,给正式仓库发起一个pull request,让项目维护者知道有更新已经准备好可以集成了。
对于贡献的代码,pull request也可以很方便地作为一个讨论的地方。

为了集成功能到正式代码库,维护者pull贡献者的变更到自己的本地仓库中,检查变更以确保不会让项目出错,
合并变更到自己本地的master分支
然后pushmaster分支到服务器的正式仓库中。
到此,贡献的提交成为了项目的一部分,其它的开发者应该执行pull操作与正式仓库同步自己本地仓库。

2.4.2 正式仓库

Forking工作流中,『官方』仓库的叫法只是一个约定,理解这点很重要。
从技术上来看,各个开发者仓库和正式仓库在Git看来没有任何区别。
事实上,让正式仓库之所以正式的唯一原因是它是项目维护者的公开仓库。

2.4.3 Forking工作流的分支使用方式

所有的个人公开仓库实际上只是为了方便和其它的开发者共享分支。
各个开发者应该用分支隔离各个功能,就像在功能分支工作流Gitflow工作流一样。
唯一的区别是这些分支被共享了。在Forking工作流中这些分支会被pull到另一个开发者的本地仓库中,而在功能分支工作流和Gitflow工作流中是直接被push到正式仓库中。

2.4.4 示例

项目维护者初始化正式仓库

和任何使用Git项目一样,第一步是创建在服务器上一个正式仓库,让所有团队成员都可以访问到。
通常这个仓库也会作为项目维护者的公开仓库。

公开仓库应该是裸仓库,不管是不是正式代码库。
所以项目维护者会运行像下面的命令来搭建正式仓库:

bashssh user@host
git init --bare /path/to/repo.git

BitbucketStash提供了一个方便的GUI客户端以完成上面命令行做的事。
这个搭建中央仓库的过程和前面提到的工作流完全一样。
如果有现存的代码库,维护者也要push到这个仓库中。

开发者fork正式仓库

其它所有的开发需要fork正式仓库。
可以用git clone命令SSH协议连通到服务器
拷贝仓库到服务器另一个位置 —— 是的,fork操作基本上就只是一个服务端的克隆。
BitbucketStash上可以点一下按钮就让开发者完成仓库的fork操作。

这一步完成后,每个开发都在服务端有一个自己的仓库。和正式仓库一样,这些仓库应该是裸仓库。

开发者克隆自己fork出来的仓库

下一步,各个开发者要克隆自己的公开仓库,用熟悉的git clone命令。

在这个示例中,假定用Bitbucket托管了仓库。记住,如果这样的话各个开发者需要有各自的Bitbucket账号,
使用下面命令克隆服务端自己的仓库:

bashgit clone https://user@bitbucket.org/user/repo.git

相比前面介绍的工作流只用了一个origin远程别名指向中央仓库,Forking工作流需要2个远程别名 ——
一个指向正式仓库,另一个指向开发者自己的服务端仓库。别名的名字可以任意命名,常见的约定是使用origin作为远程克隆的仓库的别名
(这个别名会在运行git clone自动创建),upstream(上游)作为正式仓库的别名。

bashgit remote add upstream https://bitbucket.org/maintainer/repo

需要自己用上面的命令创建upstream别名。这样可以简单地保持本地仓库和正式仓库的同步更新。
注意,如果上游仓库需要认证(比如不是开源的),你需要提供用户:

bashgit remote add upstream https://user@bitbucket.org/maintainer/repo.git

这时在克隆和pull正式仓库时,需要提供用户的密码。

开发者开发自己的功能

在刚克隆的本地仓库中,开发者可以像其它工作流一样的编辑代码、提交修改新建分支

bashgit checkout -b some-feature
# Edit some code
git commit -a -m "Add first draft of some feature"

所有的修改都是私有的直到push到自己公开仓库中。如果正式项目已经往前走了,可以用git pull命令获得新的提交:

bashgit pull upstream master

由于开发者应该都在专门的功能分支上工作,pull操作结果会都是快进合并

开发者发布自己的功能

一旦开发者准备好了分享新功能,需要做二件事。
首先,通过push他的贡献代码到自己的公开仓库中,让其它的开发者都可以访问到。
他的origin远程别名应该已经有了,所以要做的就是:

bashgit push origin feature-branch

这里和之前的工作流的差异是,origin远程别名指向开发者自己的服务端仓库,而不是正式仓库。

第二件事,开发者要通知项目维护者,想要合并他的新功能到正式库中。
BitbucketStash提供了Pull Request按钮,弹出表单让你指定哪个分支要合并到正式仓库。
一般你会想集成你的功能分支到上游远程仓库的master分支中。

项目维护者集成开发者的功能

当项目维护者收到pull request,他要做的是决定是否集成它到正式代码库中。有二种方式来做:

  1. 直接在pull request中查看代码
  2. pull代码到他自己的本地仓库,再手动合并

第一种做法更简单,维护者可以在GUI中查看变更的差异,做评注和执行合并。
但如果出现了合并冲突,需要第二种做法来解决。这种情况下,维护者需要从开发者的服务端仓库中fetch功能分支,
合并到他本地的master分支,解决冲突:

bashgit fetch https://bitbucket.org/user/repo feature-branch
# 查看变更
git checkout master
git merge FETCH_HEAD

变更集成到本地的master分支后,维护者要push变更到服务器上的正式仓库,这样其它的开发者都能访问到:

bashgit push origin master

注意,维护者的origin是指向他自己公开仓库的,即是项目的正式代码库。到此,开发者的贡献完全集成到了项目中。

开发者和正式仓库做同步

由于正式代码库往前走了,其它的开发需要和正式仓库做同步:

bashgit pull upstream master

如果你之前是使用SVNForking工作流可能看起来像是一个激进的范式切换(paradigm shift)。
但不要害怕,这个工作流实际上就是在功能分支工作流之上引入另一个抽象层。
不是直接通过单个中央仓库来分享分支,而是把贡献代码发布到开发者自己的服务端仓库中。

示例中解释了,一个贡献如何从一个开发者流到正式的master分支中,但同样的方法可以把贡献集成到任一个仓库中。
比如,如果团队的几个人协作实现一个功能,可以在开发之间用相同的方法分享变更,完全不涉及正式仓库。

这使得Forking工作流对于松散组织的团队来说是个非常强大的工具。任一开发者可以方便地和另一开发者分享变更,任何分支都能有效地合并到正式代码库中。


2.5 Pull Requests

Pull requestsBitbucket提供的让开发者更方便地进行协作的功能,提供了友好的Web界面可以在提议的修改合并到正式项目之前对修改进行讨论。

开发者向团队成员通知功能开发已经完成,Pull Requests是最简单的用法。
开发者完成功能开发后,通过Bitbucket账号发起一个Pull Request
这样让涉及这个功能的所有人知道要去做Code Review和合并到master分支。

但是,Pull Request远不止一个简单的通知,而是为讨论提交的功能的一个专门论坛。
如果变更有任何问题,团队成员反馈在Pull Request中,甚至push新的提交微调功能。
所有的这些活动都直接跟踪在Pull Request中。

相比其它的协作模型,这种分享提交的形式有助于打造一个更流畅的工作流。
SVNGit都能通过一个简单的脚本收到通知邮件;但是,讨论变更时,开发者通常只能去回复邮件。
这样做会变得杂乱,尤其还要涉及后面的几个提交时。
Pull Requests把所有相关功能整合到一个和Bitbucket仓库界面集成的用户友好Web界面中。

2.5.1 解析Pull Request

当要发起一个Pull Request,你所要做的就是请求(Request)另一个开发者(比如项目的维护者)
pull你仓库中一个分支到他的仓库中。这意味着你要提供4个信息以发起Pull Request
源仓库、源分支、目的仓库、目的分支。

这几值多数Bitbucket都会设置上合适的缺省值。但取决你用的协作工作流,你的团队可能会要指定不同的值。
上图显示了一个Pull Request请求合并一个功能分支到正式的master分支上,但可以有多种不同的Pull Request用法。

2.5.2 工作方式

Pull Request可以和功能分支工作流Gitflow工作流Forking工作流一起使用。
但一个Pull Request要求要么分支不同要么仓库不同,所以不能用于集中式工作流
在不同的工作流中使用Pull Request会有一些不同,但基本的过程是这样的:

  1. 开发者在本地仓库中新建一个专门的分支开发功能。
  2. 开发者push分支修改到公开的Bitbucket仓库中。
  3. 开发者通过Bitbucket发起一个Pull Request
  4. 团队的其它成员reviewcode,讨论并修改。
  5. 项目维护者合并功能到官方仓库中并关闭Pull Request

本文后面内容说明,Pull Request在不同协作工作流中如何应用。

2.5.3 在功能分支工作流中使用Pull Request

功能分支工作流用一个共享的Bitbucket仓库来管理协作,开发者在专门的分支上开发功能。
但不是立即合并到master分支上,而是在合并到主代码库之前开发者应该开一个Pull Request发起功能的讨论。

功能分支工作流只有一个公开的仓库,所以Pull Request的目的仓库和源仓库总是同一个。
通常开发者会指定他的功能分支作为源分支,master分支作为目的分支。

收到Pull Request后,项目维护者要决定如何做。如果功能没问题,就简单地合并到master分支,关闭Pull Request
但如果提交的变更有问题,他可以在Pull Request中反馈。之后新加的提交也会评论之后接着显示出来。

在功能还没有完全开发完的时候,也可能发起一个Pull Request
比如开发者在实现某个需求时碰到了麻烦,他可以发一个包含正在进行中工作的Pull Request
其它的开发者可以在Pull Request提供建议,或者甚至直接添加提交来解决问题。

2.5.4 在Gitflow工作流中使用Pull Request

Gitflow工作流和功能分支工作流类似,但围绕项目发布定义一个严格的分支模型。
Gitflow工作流中使用Pull Request让开发者在发布分支或是维护分支上工作时,
可以有个方便的地方对关于发布分支或是维护分支的问题进行交流。

Gitflow工作流中Pull Request的使用过程和上一节中完全一致:
当一个功能、发布或是热修复分支需要Review时,开发者简单发起一个Pull Request
团队的其它成员会通过Bitbucket收到通知。

新功能一般合并到develop分支,而发布和热修复则要同时合并到develop分支和master分支上。
Pull Request可能用做所有合并的正式管理。

2.5.5 在Forking工作流中使用Pull Request

Forking工作流中,开发者push完成的功能到他自己的仓库中,而不是共享仓库。
然后,他发起一个Pull Request,让项目维护者知道他的功能已经可以Review了。

在这个工作流,Pull Request的通知功能非常有用,
因为项目维护者不可能知道其它开发者在他们自己的仓库添加了提交。

由于各个开发有自己的公开仓库,Pull Request的源仓库和目标仓库不是同一个。
源仓库是开发者的公开仓库,源分支是包含了修改的分支。
如果开发者要合并修改到正式代码库中,那么目标仓库是正式仓库,目标分支是master分支。

Pull Request也可以用于正式项目之外的其它开发者之间的协作。
比如,如果一个开发者和一个团队成员一起开发一个功能,他们可以发起一个Pull Request
用团队成员的Bitbucket仓库作为目标,而不是正式项目的仓库。
然后使用相同的功能分支作为源和目标分支。

2个开发者之间可以在Pull Request中讨论和开发功能。
完成开发后,他们可以发起另一个Pull Request,请求合并功能到正式的master分支。
Forking工作流中,这样的灵活性让Pull Request成为一个强有力的协作工具。

2.5.6 示例

下面的示例演示了Pull Request如何在在Forking工作流中使用。
也同样适用于小团队的开发协作和第三方开发者向开源项目的贡献。

在示例中,小红是个开发,小明是项目维护者。他们各自有一个公开的Bitbucket仓库,而小明的仓库包含了正式工程。

小红fork正式项目

小红先要fork小明的Bitbucket仓库,开始项目的开发。她登陆Bitbucket,浏览到小明的仓库页面,
Fork按钮。

然后为fork出来的仓库填写名字和描述,这样小红就有了服务端的项目拷贝了。

小红克隆她的Bitbucket仓库

下一步,小红克隆自己刚才fork出来的Bitbucket仓库,以在本机上准备出工作拷贝 查看原文

赞 104 收藏 412 评论 12

认证与成就

  • SegmentFault 讲师
  • 获得 340 次点赞
  • 获得 6 枚徽章 获得 0 枚金徽章, 获得 2 枚银徽章, 获得 4 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2012-09-06
个人主页被 3.4k 人浏览