尾巴

这是故事的结局,我把它叫做尾巴。

是尾巴但也有开头

尾巴起源于我一个不敢想但是蓄谋已久的想法。22岁,我大学毕业,来到了梦想中的城市,找了一份自认为还不错的工作,然后开始奋斗。这几乎是每个大学毕业生的剧本,我也不例外。22岁总会带来点什么,或是我们应该下意识改变些什么。而22岁的我,决定付诸行动,虽然已经蓄谋很久了。

见老朋友啊。

这就是尾巴,但尾巴也有开头。

旅程

毫不夸张的讲,这是一趟漫长的旅程。即使我生活的地点一直向南在变化,就算我们可以选择的交通工具既方便又快捷,虽说没有千里之隔,但就像是回趟家一样。漫长,但充满期待。

路途开始那天就下起了雨,虽说都快七月了,但是上海就像梅雨天一样,几乎每天都会下会儿小雨,或者是太阳雨。傍晚时分,刚坐上火车,雨又开始沥沥淅淅下起来了,我把脸贴在玻璃上,甚至可以感受到火车车轮与钢轨摩擦发出的吱吱声,随着火车越开越快,这种感觉就越发强烈,直到到了忍耐的极限。在火车上的六个小时不长,但却够拿来回忆很多年的故事呢!好在六个小时不长,不然陷入回忆这种事就要发生在我身上了。可又为何会这样,也许是因为除了回忆,什么都不剩下了。

晚上10点,顺利到达目的地,一走出火车站,潮湿的热空气像我袭来,就像在冬季里吸入冷空气一样,吸入这样的空气也会让人顿时提起神来,它仿佛在告诉,你活着。是啊,活着真好!

第二天,很早就起,赶上清晨的前几班公交,离目的地还有很远,换乘,再步行,差不多两小时才到。穿过菜市场,湿漉漉的街道和发出的鱼腥味让我想起了和妈妈为数不多买菜的情景。眼前是一个有点年头的小区,旁边有高架桥,不远处还在绿化施工,这虽然是一片住宅,的的确确和D描述的一样,吵死了!

我环顾四周,茫然写在脸上。我开始打电话,又等了几分钟,一个阿姨向我走来,我认出来了,她是来接我的,旁边还跟着一个小女孩。我赶忙上去打招呼,阿姨看见我也很开心,好久没见到这样的笑容挂在一个中年妇女脸上了,上一个还是半年前的寒假,那个人是我妈。

就这样,我上了楼,一口气爬了六层,到了她家。房子从外面看真不怎么样,但走进真觉得精致啊,客厅厨房都挺宽敞的,这点和我合租的房子真是天壤之别。阿姨问我吃早餐了没,我碍于情面不好说自己饥肠辘辘,上一顿还是在昨天下午吃的,就说自己吃了,阿姨也没多问。

我坐在客厅的沙发上,环顾四周,却没有发现记忆里的东西,心里不禁失落,四年的时间里,也许什么都在发生改变。我和阿姨坐下来聊天,她以为我是D的高中同学,我说不是呢,我是D最好的朋友也是D小学同学,在远在千里的家乡,小学三年级的时候,我们还做过半年多同桌呢!阿姨的震惊完全写在脸上,又有谁能想到,眼前的这个人,竟然是D的小学同学!说道这里,毕竟是同乡人,我们用家乡的方言开始交流。我给阿姨讲我和D的故事,记忆深刻的。这样的桥段在我的脑海里不知过了几遍,语言斟酌了多少次,却没想到,直到真的要说出去的时候,是如此的困难。

好在时间冲淡了这一切,我们都没有太动情。阿姨带我去看了D的房间,这间房子我还是很熟悉的,虽然房间的主人不在了,陈设也变了,但是透过那窗台,望过去就是外面齐平的高架桥。D曾告诉过我,当她无聊的时候,还可以数过往车辆,数到自己不无聊为止。

我问阿姨有没有老照片。

她拿出了一本相册,整整满满的一个相册都贴满了,有多年的老照片,有全家福,还有很多有关D的照片。阿姨在和我一起翻看这些照片,去给我讲有关D的故事。

至始至终我脸上都没有表现出悲伤,直到离开。我谢绝了留下来吃中午饭的好意,阿姨也陪着我下楼,在最后的时间里,我给阿姨说,如果D没离开,可能我们早就见面了。

阿姨叫我以后多来家做客,可是就像是四年前那样,我又有什么理由再来这座城市呢!经过那条菜市场街,我拐了好几道弯,终于走出了小区。一路上,我在想,真的走出来了吧,不然小萝莉都快3岁了!我很欣慰,前所未有的释然。

是尾巴但终究还是结局了

回到新的大本营,迫不及待的开始写这个结局。今天下了好久的雨,肯定积了好多水坑,我打开窗呼吸着还算清爽的空气,听到蛙鸣,一切都是那么安静。我想,是时候让故事告一段落了。

我的这22年来,对我影响最大的三个女性是妈妈,老姐和D。妈妈在我童年的那段时间教会了我太多,善恶美丑,做人准则;有一个姐姐真的很棒,总会收到各种各样的礼物,在最无法沟通的时候,同龄人的姐姐却可以懂我;D,我们在小学就已经相识,到了高中,如果不是有D,恐怕我已经荒废那段最宝贵的时光,然后浑浑噩噩过着日子。

我走不出D离开给我带来的阴影,看似只有在电视剧上才会发生的剧情,发生在了我头上,并且没有一个好的结局。

五年可以忘掉什么?那些看似很有用的高中知识点,我忘了;不常用的邮箱密码,也搞忘了;可是,D的离开,快五年了,我以为我忘记了,可是一旦记起,唯独耿耿于怀。

这一次,我依然没法忘记这一切,毕竟真实存在过,亲生经历过;正是因为忘不掉,才证明我们真正的活着啊,并且要好好活着。我没法忘掉这一切,但我看开了,这就是结局。

分享到 评论

Hello Goodbye & Hello

5月1日,阴天,雾霾挺严重的。下午,戴着耳机,出门;傍晚,下起了雨,躲进快餐店;点了宫保鸡丁和土豆豆角,开始狼吞虎咽,然后,等着雨伞送到;离学校最后的一公里路程,需要十分钟。直到现在,12时,雨还在下,伴随着风。

小长假的最后一天,也是五月的第一天。对啊,五月已经来了。

打开网易云音乐,点开日推,第一首歌就是《Hello Goodbye & Hello》,很早以前看过的番剧《追逐繁星的孩子》ED。此刻我正苦于构思一个标题,所以就用了这个歌名“你好,再见,再也不见”。

在为数众多的看过的动画中,《追逐繁星的孩子》故事讲得不算好,但那句原来是我太孤独直戳内心深处。我想,我何尝不是这样呢,虽然性格不内向,容易和人打成一片,但我还是习惯一个人呆,去健身,躲在网吧打游戏。可我也挺怕一个人的,会因为一个人吃饭吃得特别快,走路也走得特别快,久而久之,给别人留下了不可接近的印象。

五月带给我的印象里就是忙吧,无论是高中还是上了大学,所有的事情都会在五月变得忙碌起来。而这个五月,也就是现在,我却过得非常的悠闲,甚至有点百无聊赖。早早的做完毕业设计,我有很多时间去做很多做感兴趣的事。这样的日子,还会持续两个月。不知道是好是坏。

我的生日一般都在六月,这个时候我呆都在学校,所以这么多年来,我都没怎么过过生日。高中的时候,有时候学习特别忙,都忘记了哪天是五月初三,每次都是妈妈打电话提醒我,让我吃顿好的;上了大学,我更不想让同学们知道我过生日,所以那天我很早就出门,找个自习室好好的学习一天。如果说要有什么不同,那一定是17岁那年,端午节。

至于为什么不同,因为我收到了特别的礼物。为数不多的收到过的贺卡,学生时代追了个遍的小说,画得还算不错的画。

去年寒假回去叔叔家把遗留在那里的高中记忆都翻了个遍,很可惜的是,贺卡不见了;而那本单行本也码在书堆中早已泛黄,我拿走了那本书,把它带在身边,就像那幅画一样。

对于只会画条条框框的我,无法描述那幅画的内容,但站在一个观赏者的角度,还算可以把,仅此而已了。

说起那幅画,我曾经给很多人看过,他们问我画的什么有什么含义,我总是说,我怎么知道啊,就是别人送的礼物而已。其实背后的故事我是知道的,那是我最珍贵的宝物,那是我的记忆啊。

其实记忆才是是每个人最珍贵的宝物。

分享到 评论

前端路由拦截和http响应拦截

问题由来

最近在制作毕业设计的时候,遇到一个问题,那就是用户的访问控制。简单点来说,就是未登录用户只能访问某些特定的页面、API。最初我的想法是用户登录后返回一个凭证,用户以后的每次http请求都带上该凭证,进行验证,只有验证成功才能继续请求。然后在每个页面进行判断,如果用户是未登录或者凭证失效,则进行相应的提示和路由跳转。刚开始的时候,这个方法是完全可行的,但是在开发过程中,随着业务逻辑变得复杂、页面增多,重复代码太多,这样的方式也许并不合适。

如何解决

首先是后端,我使用了jsonwebtoken,用户登录成功都会生成一个具有一定时效的 token,这个token会发回到客户端,并且接下来每次发起http请求,都在http头的authorization字段带上这个token。我这里使用了axios这个http请求库,只需要在拿到token后:

1
axios.defaults.headers.common.authorization = `Bearer ${token}`;

就可以了。

由于在开发过程中涉及到跨域,这里我使用CORS来解决:通过设置一系列Access-Control-Allow-*响应头进行访问控制,上面提到了在请求头的authorization字段中设置token,因此发出的请求都不是简单请求,所以注意在每次发起http请求时,就会自动发起一个OPTIONS请求。

我服务器端用的是Express框架,我们需要写一个中间件来处理每一个请求。处理逻辑为:针对每个OPTIONS请求,直接放行;对于某些请求,如果在白名单中(例如登录、注册等不需要验证的路由),放行;对于其他请求,我们拿到其携带的token,并且进行验证,如果验证通过,放行,否则结束请求,返回未授权。具体的代码如下,这里我使用jsonwebtoken这个package,用于生成token和进行token验证。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
app.use((req, res, next) => {
console.log('methods' ,req.method);
if( req.method === 'OPTIONS' ) {
console.log('option请求直接通过');
next();
}else {
// 除去某些特定的API,其余的都做token的验证
let { path } = req;
if(path === '/api/users/auth'
|| path === '/api/users/auth_vc'
|| path === '/api/users/check_id_validation'
|| path === '/api/users/regist'
|| path === '/api/users/send_reset_email'
|| path === '/api/users/reset_password'
)
{
console.log('本次请求不需要验证权限');
next();
}else {
const token = req.headers.authorization ? req.headers.authorization.split(' ')[1] : '';
req.token = token;
jwt.verify(token, KEY, (err, decoded) => {
if(err) {
res.status(401).json({ status: 3, error: '用户认证失败', data: '' })
}else {
console.log('验证权限通过');
req.decoded = decoded;
next();
}
})
}
}
})

然后是前端,我想如果能像后端拦截每个请求一样,写一个逻辑拦截所有的相应,并进行处理,信号,axios自带拦截器,我们只需要写我们的逻辑就可以了。我的想法是,拦截每一个相应,如果其状态码是401,那么久提示token失效,并且进行路由跳转。
vue-cli构建的应用为例,在main.js中,下面是实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
axios.interceptors.response.use(data => data, (error) => {
if(error.response) {
switch(error.response.status) {
case 401: {
localStorage.removeItem('token');
router.replace({
path: '/auth',
query: {redirect: router.currentRoute.fullPath}
})
}
}
}
return Promise.reject(error);
})

值得说明的是,如果我们在某个访问的过程中,token失效,我们需要跳转到登录页面,但是想登录过后再跳转回来,所以这里在进行路由跳转的时候,我设置了一个参数, redirect,表示传入当前的路径,当我们登录成功后,在跳转回来即可。

最后是路由拦截,这里我使用了vue-router,其实vue-router的路由对象提供一个钩子函数beforeEach,其会在每一次路由跳转之前,执行这个函数,我们就在这里进行路由拦截。原理很简单,使用一个标志位标明每个路由是否需要用户权限,如果需要的话,我们检查保存在本地的凭证,一般存在localStorage中,如果不含凭证就直接跳转到登录页面。

好了,找到根路由文件,添加:

router.beforeEach((to, from , next) => {
  if(to.matched.some(res => res.meta.requireAuth)) {
    if(localStorage.getItem('token')) {
      next();
    }else {
      next({
        path: '/auth',
        query: { redirect: to.fullPath }
      })
    }
  }else {
    next();
  }
})

这里要注意的是,res.meta.requireAuth是你自己在声明路由的时候自定义的。

总结

差不多,这算是一个比较好的解决方案了。但是有这样一个情况:如果用户凭证有效期是1小时,那么如果我浏览网页超过一个小时了,凭证还是保存在本地的,当我们进行路由跳转的时候,并没有验证凭证是否失效,所以还是会进行路由跳转。这里不用担心,因为进入进入了一个路由后,一旦发起http请求,token失效,http相应拦截就会生效,进而跳转到登录页面。

加油!

分享到 评论

最近这段时间

不知不觉,大学里最长也是最后的一个寒假就快要过完了,而学生生涯也要画上一个句号了,真的好快啊!

还记得寒假带回来几本书,现在才开始翻看;本着要掌握的目标去学习Vue,然后现在才刚刚入门;信誓旦旦说要在家好好健身,虽说不规律,可总算是去做了,收效甚微;寒假总共“吃鸡”40小时,可总算成功吃鸡了。总之,最近(这个寒假)做了很多事,也下定了决心将要去做一些事。

那些做过的事

  • 学习Vue及其技术栈,为毕业设计做准备
  • 继续完善自己的Project
  • 钓鱼!钓鱼!!钓鱼!!!说三遍~
  • 有些事情,向前迈出一步
  • 不懈努力,成功“吃鸡”

“吃鸡”与“吃到鸡”

作为一名“吃鸡”萌新,标配夕阳红枪法,又是老年人视力,那肯定是行走的盒子精无疑啦。可是萌新也有大梦想,那就是吃一把鸡,为了这个目标,一直苦练枪法,终于吃鸡。既然如此,下一步目标是,打上2000分!就看时间允许不允许了😂

学习Vue

毕竟一年多React经验的自信,所以毕设直接选了使用Vue来实现的一个App,那么理所当然的就要在寒假“补课”啦,官方教程半小时过一遍,再花1小时细细过一遍,OK不OK?ojbk!看完教程连个脚手架都搭建不出来!既然如此,还是视频大法好啊,看了差不过2个小时时间的基础教程,再配合上github上的开源项目,差不多get了吧。现在才感叹,当初学React的时候,真是费了好大的劲啊,光是Redux都够你喝一壶的了,最后悟出一个道理:珍爱生命,远离React😂

钓鱼,第一等大事

我为什么那么喜欢钓鱼到现在还是一个未解之谜。我几岁的时候就开始钓鱼了,各种水域,各种鱼类都钓过,毫不夸张的说,我钓过的与鱼少说也是成千上万了吧。寒假一回来没有回老家就在农家乐钓过几次,太好钓了反而无聊。回老家的一个星期也钓了几次,总算过瘾了。以后夏天基本上没机会回家乡想想都可怕,只寄希望于春节少德可怜的假期能够撑起一个钓鱼爱好者的心啦~

我的项目

其实一直有在写一些简单的项目,脑子里想到的就要去试着实现,在学校一直在断断续续做一个关于想法的App,寒假无聊的时候也会一个点一个点去完善。但是倒霉的是,某个电脑管家竟然在清理垃圾的时候把这个proj删了,我还没提交到远程仓库呢,真是气死一个人,果断卸载!所以呢,找个时间得重新起航,也许到时候又会有新的点子呢~

有些事情,得迈出一步

其实这个故事是这样的:你一直暗恋一个女孩很久,在后来的某个时候,你向她表白,她接受了,可是后来由于各种原因虽不说天各一方但也没能走到一起。很多年过去了,你总会在某些特定的时候想起她,你还是忘不掉她。你决心不留遗憾,在某次很愉快的聊天的时候,话题突然一转,你问她:你有男朋友吗?她说:你猜~ 你回答:也许有,也许没有,一半一半吧。她沉默了好一会儿,才憋出几个字:有的哦,都出来上班那么久啦。外面的花花世界,不比得你学校。听到这个意料之中的答案,男孩心中说不出的感觉,对她说了最后一句:打扰了。其实男孩心中从最开始就知道,如果可以把人分类的话,他们从来都不是一类人,从最开始他们就走向了不同的十字路口。至于为什么时常回想起,他也不知道。他迈出了一步,解开了自己的心结。

那些将要做的事

被逼无奈,我要学车


今年回家过年,七大姑八大姨除了问你:找到工作了吗、有女朋友吗、成绩怎么样呢这样的问题以外,学没学车竟然也加入了进来,在他们看来,不会开车的人好像就不能生存以外,所以为了不被念叨,我决心要学车啦,就在接下来的三个月时间里搞定!

对,我要增重

在外人眼里,我172的身高配上快140的体重是再好不过了的,不过我还是要增重,直到长到145,当然,这不是简单的增重而是增肌。所以,是时候努力了。

补课,然后大补考

补一门大二未上的实验课,然后大补考挂掉的《形势政策》,然后毕业!

技术上,继续

除了继续在web开发领域继续摸索深耕以外,还有扩展自己的技术栈,接下来要把大数据、数据挖掘纳入我的技术栈。

其它

比如一定要吃早饭、好好睡觉这种是肯定要做的,无论什么时候。

本来挺喜欢玩《炉石传说》的,可是这个版本快攻横行,打不过术士又没尘加入他们,所以暂时要放弃啦。最近在看两部番剧《紫罗兰永恒花园》和《比宇宙更远的地方》。最近在读一本书叫做《24个 比利》。就这样。

分享到 评论

SQL参数化查询

SQL注入想必是每个人都听过,其原理和XSS攻击很相似,都是把用户的输入当做程序去执行。防御办法也很类似,就是对用户的输入进行转义,但是同样转义十分麻烦,因为SQL注入攻击的方式和变种实在太多,转义需要考虑到的情况也复杂多变;而另外一种方式就是使用参数化查询–Prepared Statements。

SQL注入

在先介绍参数化查询的时候我们先复习一下SQL注入,上面提到其原理是把用户的输入当做了SQL语句程序的一部分去执行,因为我们经常使用字符串拼接来构建SQL语句。

在这里开始演示一下(使用MySQL):

在我的数据库中我数据库中我建立了一个名为urls的表,其结构和数据如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
mysql> desc urls;
+-------------+------------------+------+-----+-------------------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------------+------------------+------+-----+-------------------+----------------+
| id | int(11) unsigned | NO | PRI | NULL | auto_increment |
| url | varchar(255) | NO | | | |
| insert_time | timestamp | NO | | CURRENT_TIMESTAMP | |
| tid | bigint(20) | YES | | NULL | |
+-------------+------------------+------+-----+-------------------+----------------+
mysql> select * from urls;
+----+-----------------------+---------------------+------+
| id | url | insert_time | tid |
+----+-----------------------+---------------------+------+
| 1 | http://www.limoer.cc | 0000-00-00 00:00:00 | NULL |
| 2 | http://baidu.com/news | 0000-00-00 00:00:00 | NULL |
| 3 | http://do.io | 0000-00-00 00:00:00 | NULL |
| 5 | http://github.iod | 0000-00-00 00:00:00 | NULL |
+----+-----------------------+---------------------+------+

该表有4字段并且有4条记录,现在我们如果想要查询id=1的那条记录,应该这样写:select * from urls where id=1。执行该条语句,正确返回结果,现在我们修改一下这条语句,改成:select * from urls where id=1 and 1=1,执行这条语句,同样没问题,返回结果正常;我们接下来再把and 改成 or再执行,结果出乎我们的意料,我们把所有的记录都查询了出来,id=1的限定条件失效了。至于如何导致其失效,是因为or后面的条件1=1是恒等的,所以前面的限定条件已经不重要了,and也是如此,我们想要获取正确的结果,那么and后面的限定条件必须要正确才可以。

说到这里,其实我们就已经进行了一次SQL注入的攻击,并且窃取了数据库的所有记录(更严重的删库、窃取管理员密码也很easy)!

其实不光是上面演示到的使用and or来进行SQL注入,还有很多神奇的SQL语法让SQL注入有了可乘之机,例如我们常用的union等等。

解决办法

如果我们把上面情景放在实际开发过程中,我们可能现在有一个输入框,用户可以输入任意一个数据来查看某条记录,
服务端的SQL语句也许是这样的:select * from urls where id=${userInput}。如果某个淘气的用户不遵守约定输入了非数字,例如10 or 1=1,SQL语句拼接过后就成了这样:select * from urls where id=10 or 1=1,表中的信息一次被完全暴露!

针对上面的情况,我最想想到的不是转义输入也不是使用参数化查询,而是针对本问题,我们直接对其进行输入验证即可,既然其必须限定用户输入数字,那么在进行SQL拼接之前,对用户输入进行验证即可!

例如,在Node.js环境下,我们可以使用parseInt(userInput)就可以完成对用户输入进行强制性的验证。

第二种也就是最常用的解决办法就是转义,和防御XSS攻击一样,我们需要构建用于转义的函数,对用户的输入进行转义,还是上面的那个例子:

1
select * from urls where `id`= ${id};

如果用户输入1 or 1=1,那么毫无疑问将会导致一次非常严重的SQL注入攻击,现在假设我们已经写好了我们的转义函数escape,我们只需要在进行字符串拼接之前,做一次转义即可。 例如对于用户的输入1 or 1=1经过转义后变成了'1 or 1=1',经过SQL拼接过后则变成了:

1
select * from urls where `id`='1 or 1=1';

不出意外,我们得到了正确的结果。

关于转义函数escape如何实现,这里就不不再多说,很多数据库的驱动工具都带有相应的工具函数,我们在实际开发过程中一定要注意对用户的输入进行转义,来避免SQL注入攻击;当然,如果你使用参数化查询的话,就完全没有必要了。

参数化查询

最开始提到参数化查询的时候,我提到了Prepared Statements也就是预处理语句,其实我们可以把参数化查询理解为预处理,我们把完整的一次SQL查询分成两部分,第一步是预先查询,第二步使用参数得到结果。具体该怎么理解呢,还是接着上面的那个例子,现在我们使用参数化查询执行select * from urls where id=1。其分为两步,第一步执行select * from urls where id=?,注意这里的?,其实代表了未来将要传入的参数;第二步,传入用户的输入作为具体的id值,并且输出结果。这里要注意,因为执行完第一步的时候期待第二步传入的是一个用户的id(这里必须是数字),这时候用户传入的非法输入就不会生效,这也就从根本上杜绝了了SQL注入攻击。

好了,参数化查询(预处理)可以完全避免SQL注入,其还有其他的优点例如更加可读(相比于字符串拼接),多次查询性能会有提升(因为会对预处理语句进行缓存再利用)等。

说了这么多,那么如何使用参数化查询呢?很简单,使用一个支持该特性的数据库连接工具就可以了,比如我们下面要演示的Node环境下MySQL的参数化查询。

Demo

我们在Node环境下进行演示,首先通过npm install mysql2命令安装数据库连接工具,这里是mysql2,能够支持参数化查询。

如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const mysql2 = require('mysql2');
const conn = mysql2.createConnection({
host: 'localhost',
user: 'admin',
password: '123',
database: 'news'
});
// 不使用任何防护手段(将导致SQL注入攻击)
const userInput = '1 or 1=1';
conn.query(
`select * from urls where id=${userInput}`,
(err, result) => {
console.log(result);
}
);
// 使用转义(这里默认进行了转义)
conn.query(
'select * from urls where `id`=?',
['1 or 1=1'],
(err, result) => {
console.log(result);
}
);
// 使用参数化查询
conn.execute(
'select * from urls where `id`=?',
['1 or 1=1'],
(err, result, fields) => {
console.log(result);
}
);

尾巴

关于SQL注入和参数化查询就介绍到这里,如果你觉得参数化查询两步走我说得并不明确,你可以使用抓包工具来加深理解;还有最后的Demo,其实query和execute的区别就是一个支持了参数化查询而另外一个不支持;如果你运行Demo,仔细看,区别就藏在里面(Tips:B & T);最后,请总是使用参数化查询!

分享到 评论

我的应聘经历(不断更新中...)

只有在写简历的时候才会明白,真后悔自己平凡的大学四年,没有拔尖的学习成绩、没有丰富的实践经历、也没有牛X的技术,对于大牛而言,自己就是简历收割机,而对于我而言…好吧,如果谁能看得上我,管吃管住就行!可是,时间没有倒带,不会从头再来/感慨。

概括

从8月开始到如今一个多月过去了,简历虽不是海投但还是投了有数十家,除去没有参加笔试的,剩下通过笔试拿到面试的就更少了,接下来我好好回顾一下,算是一个小小的总结吧,虽然到目前还没有找到工作…

某测(通信测试行业)

经历:

这是校招来学校宣讲的第一家企业,很小型的宣讲会,参会人数不多;在简单的介绍企业过后,就直接现场笔试。吐槽下笔试题,融合了C++/前端/智力测试的一张综合试卷,难度不大,我花15分钟做完了试卷上前端相关的试题就离开了,一个小时后就收到通知当天下午面试得通知。

面试(时间50分钟):

面试也安排的特别简单,二面一,连自我介绍都没有(害得我准备好久…)。问题主要有以下几个:

  1. 你了解通信测试行业吗?
  2. 你觉得本公司适合你吗? 这里的时候我直接问了他几个问题然后做出判断,我们都觉得不适合
  3. 你期待的工作方式? 构思以下结合公司实际谈了谈数据可视化
  4. 数据挖掘相关知识。 基本的分类、聚类算法,分词等。
  5. 给我的建议。 推荐我要有自己的想法,最好选择互联网公司,并且赞了我字写得比较好(谢谢!)。

某某网络(初创互联网公司)

经历:

来自于知乎大牛的推荐,一家自认为小而美的公司!投递简历差不多一个月才进行电话面试。面试时间也真是会挑,早上8点过就打来电话(幸好每天都早起!),大概谈了超过一个小时,到目前为止仍然在等结果。

面试:

面试官大概是在上班路上给我打的电话,电话那头有点吵。面试流程也比较常规,自我介绍,介绍项目,目前在做的事,有关于React及其技术栈。当然还包括面试官对公司的介绍,要求我谈对公司发展的看法;后面又聊了前端开发的潮流;有关于SSR、SPA等。最后告诉了我他们公司前端开发团队的现状(规模很小!),问我如何看待,特别是如何快速的融入和承担起相应的责任。

总结:

当了解了他们正在做的事情的时候,我觉得这就是我想要加入的公司。我喜欢小而美的公司!面试过程十分愉快,虽说是等通知但是都快一周了还是没有音讯,大概是挂了吧~

某家网(卖房的?)

经历:

就在中午临时决定去试一试,幸好赶上校车去了宣讲现场。宣讲过程中真的能够感受到这是一家非常了不起和大牛聚集的地方。宣讲完直接笔试,笔试后就等面试通知咯。晚上接到面试通知,第二天在某某咖啡馆参加了面试,最终是倒在终面前。

笔试:

难度四星,10题全算法,对于数据结构和算法没掌握好的同学就是煎熬,我花了一个半小时做完了自己会做的题目,然后还是错过了一家不错企业面试的机会/哭。

面试:

一面一上来就是自我介绍,随便问问。然后十题算法挨个回顾,并且面试官对每道题进行打分(自己得了好多零!),然后一面结束,等通知。

二面已经是下午了,前端方向就我一个人,所以聊了比较久,一个半小时。面试过程中主要聊了:

  1. Java和JavaScript的特性(被我引入了我喜欢JS而不喜欢Java的怪圈…)
  2. 做过最酷的项目(介绍了一下正在开发的视频编辑器)
  3. 从架构角度谈某个数据平台(简历上提到)的建立和开发过程
  4. 如何做情感分析(数据挖掘方面)
  5. 提问环节

吐槽:

在面完二面后又是等通知,十分钟后我去问通过了吗,然后HR小姐姐告诉了过了,等待三面,我当时的那颗心啊,真的高兴,以为自己就要进入这家感觉十分良好的企业了!结果过了几分钟,HR姐姐给我说我没过,然后这就尴尬了😓。总的来说,本次面试的体验还是不错的(主要是中午管饭了!)。

某有(b2b)

经历:

很早很早就投了简历,并且做完测评等着来软件园宣讲。真的到了那一天去现场一看,人真的多!短暂的企业介绍后,就开始了笔试(分前端、Java、基础三套试卷)。结果由于弄错试卷做了前端、基础两套。试卷难度不大,但都是十分容易出错的地方。晚上收到通知,第二天面试。面试过程非常愉快,上午面完下午HR就打来电话说通过了,然后我说考虑一下,最终开始放弃了到手的offer(说好的就去第一家能看上你的公司呢!),到现在好几天了,我有点后悔自己做的决定。

面试:

面试官应该是管理层,年龄40+,基本不问技术方面,谈谈项目,谈谈个人经历,再结合职业测评问问就完了。二面HR问了问家庭状况,是否考研,以及如何看待将要在工作中遇到的一些问题,后面又问道期待薪资(最大的败笔),然后就结束了。

总结:后悔 X N!!!

还有很多…但是…

不知不觉半个月又过去了,期间不间断的参加过很多公司的招聘,但始终没能更进一步。有些时候晚上睡不着觉,就在想,到底是哪里出了问题,没抓住机遇?实力不够?表现不好?… 我不知道。

可是,就在今天,我暂时不用去为自己的将来而担忧,能够有时间去做自己想要去做的事,能够静下心来提升自己。所以,我愉快的做了决定,好比平时做决定那样–洒脱?。

我打电话通知了家人,以往不想让我离家太远的母亲也很高兴,至少无论他们口头上怎么反对我去外地工作,可真当找到工作后,无论在哪里,只有一如既往的支持。

我认为我就是一个平庸(凡?)的人,我每天会花时间在打游戏娱乐上,我也会时不时睡睡懒觉,天气冷了我可能更多的会宅在宿舍。

尾巴里面提到我不会错过第二家看上我的公司,但是我也食言了,可是我真的不会再错过这一个机会了。我准备好来年在人生的下一站,开启我的程序猿人生了。

分享到 评论

使用react-transition-group实现路由切换动画

我们在使用React开发SPA的时候,使用react-router可以完成路由切换,但是这样路由切换是非常生硬的。有什么解决办法呢?我们可以使用react-transition-group来实现自定义的路由切换效果。

需要注意的是react-transition-group目前有两个版本,v1和v2版本的差距十分巨大,本教程使用的是最新的V2版本,你可以使用npm install --save react-transition-group来安装,如果想安装v1版本,则只需使用npm install --save react-transition-group@1.x命令即可。

react-transition-group主要提供三个组件TransitionTransitionGroupCSSTransition。从名字当中我们知道TransitionGroup作为一个容器组件,而其它两个组件才是实现动画的关键。这里我只介绍CSSTransition如何使用以及其注意的点。如需了解更多react-transition-group,请查看官方文档

CSSTransition

这个组件主要是使用css来控制组件的转场。它使用了在缓动中appearenterexit的三个状态,并且提供钩子类让我们自定义效果。

我们常用到的类有:

.className-enter
.className-enter.className-enter-active
.className-exit
.className-exit.className-exit-active

这里className是你自定义动画的名称,和V1版本大体相同的钩子类,只不过把leave改成了更加语义化的exit,这里需要注意。

CSSTransition有多个十分重要的属性:

  1. classNames属性接收一个字符串类名,注意这里是classNames而不是className
  2. timeout用于规定动画执行的时间,如果enterexit的持续时间相同的话可以使用timeout={number}即可,如果持续时间不一样,则timeout接收一个字典,两个键分别是enter和exit。
  3. 其他参数例如onEnteronExit你可以自定义逻辑在动画进行到某个阶段后触发。
  4. 动画进行的阶段:enter->entering->entered->exit->exiting->exited

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

let App = () => (
<BrowserRouter>
<Route render={({location}) => {
return <div>
<Route exact path="/" render={() => (
<Redirect to="/home"/>
)}/>
<TransitionGroup>
<CSSTransition
key={location.pathname.split('/')[1]}
classNames="transitionWrapper" // 这里一定要注意的是:classNames 而不是className
timeout={400}
mountOnEnter={true}
unmountOnExit={true}
>
<div className="wrapper">
<Switch location={location}>
<Route exact path="/home" component={Home}/>
<Route path="/inspiration" render={() =><NavLink style={{marginRight: '20px', marginLeft: '20px'}} to="/home">HOME</NavLink>
}/>
<Route path="/mood" render={() => <h1>this is page3 mood!</h1>}/>
</Switch>
</div>
</CSSTransition>
</TransitionGroup>
</div>
}}/>
</BrowserRouter>
);
分享到 评论

使用antd和css-modules冲突的解决办法

在暑假做项目实训的时候前端就使用到React构建并且使用了Ant Design作为组件库,当时就使用了extract-text-webpack-plugin把css单独抽离出来成为一个单独的css文件并引入,当时就遇到一个问题,当我使用css-loader来处理css时,并不能处理自定义的css,但是我把CSS直接写进组件中是可行的,由于当时项目比较小并且时间比较赶,就直接使用了这种方式,在开发过程中也有苦说不清,但总算是完成了。

最近想写一点东西,又用到antd了,当然是相同的问题,只不过时过境迁,我有足够多的时间来处理这个遗留下来的问题。可是即使有那么多的时间,可是还是踩坑无数,最终还是完成了。相信遇到这个问题的并不止我一个人,这里就先记录下来,希望能对你有所帮助。

解决办法

经过查询和思考,解决这样的问题最好是单独处理antdCSS和自定义的CSS。好了问题解决办法已经很明显了,我们需要些两个不同的规则来出来css,就像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
...
{
test: /\.css$/,
use: ExtractTextPlugin.extract({
use: 'css-loader'
})
},
{
test: /\.css$/,
exclude: /node_modules/,
use: 'css-loader'
},
...
plugins: [
new ExtractTextPlugin('style.css')
]

上面的代码我建立了两规则分别处理自定义css和antd 预定义css,我们可以正常的使用import './style.css'的形式引入css,但是我们查看页面,并没有加载我们自定义的css。

好吧,既然这样再试试css-modules的方式算了,我们把第二个规则改成下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
...
{
test: /\.css$/,
exclude: /node_modules/,
use: [
loader:'css-loader',
options: {
modules: true,
localIndentName: '[local]--[hash:base64:5]'
}
]
}

现在我们可以通过import style from './style.css'的形式引入自定义css,并且通过style.className的形式给元素设置类。这次倒好,直接build不成功了,我一气之下索性不搞了;为了继续捣鼓下去,我直接又把CSS写在组件中了,直到我要使用react-transition-group来做路由切换动画,不得不倒回来解决。这次比以往更加冷静,我仔细阅读了extract-text-webpack-plugin的readme过后,恍然大悟,原来我们可以在一个项目中使用多个ExtractTextPlugin实例来生成多个css文件!好了,这次还是通过两个规则处理css,并且构建两个css文件,一个是自定义的css,一个是antd css,问题迎刃而解,又可以开心的捣鼓了!

好了,show you the code!:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const extractANTDCSS = new ExtractTextPlugin('[name]-antd.css');
const extractNormalCSS = new ExtractTextPlugin('[name]-normal.css');

module.exports = {
...
module:{
rules: [
{
test: /\.css$/,
include: /node_modules/,
loader: extractANTDCSS.extract({
fallback: 'style-loader',
use: [{
loader: 'css-loader',
options: {modules: false}
}]
})
},
{
test: /\.css$/,
exclude: /node_modules/,
use: extractNormalCSS.extract({
fallback: 'style-loader',
use: [{
loader: 'css-loader',
options: {
modules: true,
localIndentName: '[local]--[hash:base64:5]'
}
}]
})
}
]
},
plugins: [
extractANTDCSS,
extractNormalCSS,
...
]
}

以上的两个loader会生成两个css文件,分别是vendor-antd.cssmain-normal.css,我们只需要在正确的位置引入这两个css文件就好了!

尾巴

我在前面提到我把CSS直接写在元素/组件的style标签中,其实这种方式实不可取的,它会让你在编码和代码review中苦不堪言,因为一旦项目变得很大,当你想修改某个样式的时候,花在定位CSS的时间是非常多的;并且,可读性和可复用性也会大打折扣;而且我们经常在写样式的过程中使用的各种选择器、伪类、伪元素都无法发挥其灵活的作用。所以,无论你是以何种方式写前端,请尽量不以这种方式写CSS。

当然,我们也要从性能上去考虑。因为css是在页面解析正式前就加载好了的(写在header)里面,在我们再解析页面的时候,加载速度就会变得更快;再有,如果我们使用把CSS写在组件中后,无可避免的会产生更多的重绘和回流,这会严重影响渲染性能。比如我们使用JS修改我们在style属性中标明的样式,那么必然会触发一次repaint。

好了,到此打住!如果你想学习reflow和repaint,点击这里,也许会帮助你!

分享到 评论

Three.js

最近貌似Node又有了新的fork ayo.js(怎么读!哎呦?),加之前端一不留神就出框架的节奏,在2016年就开始用Next(wtf!你能看出来其是一个前端框架?)来命名,以后恐怕就得future.js、plus.js的节奏…贵圈真乱啊!

当然当然,这和我们今天的主角three.js并没有太大的关系,比起这些看了名字不知所云的xxx.js,Three.js这个就和明显了,其是一个3d JavaScript库,更准确的说是用JavaScript编写的WebGL三方库,那么什么是WebGL呢?这个我不解释,有兴趣的小伙伴可以去探索。

作为我最想学却一直学不会的技术之一,WebGL的确对于大部分的前端猿们来说有些复杂和繁琐了,早些时候我花了大量的时间去啃API,学习如何使用,可到目前脑子还是一团乱麻。既然这样的话,我们得另辟蹊径,不能因为有困难就放弃学习不是!所以我了解到了Three.js,其化繁为简,做同样的事,其只需要少于1/5的代码量就可以完成,并且API也十分通俗易懂,学习难度降低了不少,可以让我们关注使用WebGL创造而不是痛苦的学习和编码。

如果你还不理解WebGL是什么,这是官方文档上的原话:

WebGL (Web Graphics Library) is a JavaScript API for rendering interactive 3D and 2D graphics within any compatible web browser without the use of plug-ins. WebGL does so by introducing an API that closely conforms to OpenGL ES 2.0 that can be used in HTML5 canvas elements.

如果你对Three.js比较有兴趣的话可以直接进去官网,其中首页展示了很多featured projects,个人比较喜欢这个Paper Planes

你也可以去gayhub把Three.js代码download下来,里面有很多很多(大约几百个例子)可供学习,当然如果你想学习Three.js,来百度云下载,这是目前少有的全方面介绍Three.js的书籍。

好了,差不多介绍完该跑了。但是我好想发现了我竟然连副标题都没取,好吧,还是再多讲一会儿,为了彰显Three.js的简单易用的特性,我讲决定再写一个全面但是简单的例子,并且配上必要的讲解。

例子?不存在的!

这是一个很小的例子,它将会展示Three.js使用流程,并且是经过测试没有错误(也许有!),请放心食用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
<!DOCTYPE html>
<html>
<head>
<meta charset="utf8">
<title>使用Three.js</title>
<script src='./three.js'></script>
</head>
<body>
<div id="three-container"></div>
<script type='text/javascript'>
// three.js中有几个非常重要的知识点,为了构成一个3D程序,我们至少需要以下几部分。
// 1. Scene 场景,用于承载一些必要元素
let scene = new THREE.Scene();
// 2. Camera 相机(此相机非你想的那个相机哦!)
// Three.js中提供了两种相机,透视相机和正交相机,这里使用的是透视相机(类似于人眼看到的)
let camera = new THREE.PerspectiveCamera(60, window.innerWidth/window.innerWidth, 1, 1000);
camera.position.set(-20, 40, 20);
// 3. renderer 渲染器, 也可以在canvas中渲染,但是复杂场景可能有性能问题
let renderer = new THREE.WebGLRenderer();
renderer.setClearColor(0x708090);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMapEnabled = true;
// 4. 物体
let cubegeo = new THREE.CubeGeometry(10, 10, 10);
// 5. 材质 用于物体表面,不同材质包含不同特性,可设置颜色等。
let material = new THREE.MeshLambertMaterial({
color: 0xffffff
});
// 组合物体与材质成为一个网格
let cube = new THREE.Mesh(cubegeo, material);
// 设置物体能够产生光源阴影
cube.castShadow = true;
scene.add(cube);
// 6. 光源 Three.js中存在多种光源
let light = new THREE.SpotLight(0xfffff);
light.position.set(-30, 40, -20);
light.castShadow = true;
scene.add(light);
// 设置相机看向场景远点(空间坐标系原点)
camera.lookAt(scene.position);
// 添加到HTML中
document.getElementById('three-container').appendChild(renderer.domElement);
// 为了更加直观,这里设置一下空间坐标系
let axes = new THREE.AxisHelper(30);
scene.add(axes);
// 动起来吧!添加动画
function animation() {
// 比如移动转动方块, 这里设置在x、y轴转动平面
cube.rotation.x += 0.1;
cube.rotation.y += 0.1;
requestAnimationFrame(animation);
renderer.render(scene, camera);
}
requestAnimationFrame(animation);
</script>
</body>
</html>

尾巴

即使是这样一个简单的例子,我如今也没有办法在不参考官方文档的情况下一口气写下来,原因无非在于,虽然其简化了开发,但是概念还是偏多并且需要记住每个API也是在有难度。

但是,如果我们能够十分清楚的理解制作3D应用的流程,至少是使用Three.js的流程,按照流程十分有条理的写下去,代码总归是十分清晰的。

time waiting for no one,这是我最近在看《穿越时空的少女》看到的。对啊,时间不等人,珍惜好为说不多的’自由’而’枯燥’的时间吧!

分享到 评论

从Decorator到Mobx

最近在开发一款视频编辑器,其中就用到了Mobx作为状态管理工具。Mobx中很重要的概念例如可观察(observable)的状态,可计算(computed)的值都用到了decorator(当然在使用Mobx时可以不用)。Decorator作为ES7引入的新特性,用于给类/属性添加新行为。对于不少初学者而言,可能对其并不是很了解,所以在这里从装饰器开始,聊聊我对Decorator和Mobx的理解。如果你正在学习Mobx,希望能对你快速上手Mobx能有所帮助。

先说装饰器(Decorator)

装饰器是ES7中引入的,其目的在于修改类/方法的行为。例如我们可以在不修改“类”的情况下为其增加新的功能。

例如:我们定义了一个学生“类”,其中有nameage两个属性,以及showInfo一个方法。

1
2
3
4
5
6
7
8
9
class Student {
constructor(name, age) {
this.name = name;
this.age = age;
}
showInfo = () => {
console.log(`name:${this.name}, age: ${this.age}`)
}
}

如果此时我们想为这个类添加一个属性school用于标明学校,,在不修改“类”的情况下,我们可以使用装饰器这么做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function addSchool(target) {
target.prototype.school = 'SDU';
}
@addId
class Student {
// ...
}

/**
@decorator
class A{}
等价于
A = decorator(A);
*/

let limoer = new Student('limoer', 21);
console.log(limoer.school); // > SDU

addSchool()给Student“类”的原型对象上添加了一个属性,现在所有实例都可以取到school这个属性。

更深入一步,上面看到用于装饰的函数只接收一个目标“类”作为参数,如果我们有多个参数的话,可以写成高阶函数的形式(即返回一个函数)。同样是上面的例子,现在学校由参数指定,我们可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
function addSchool(school_name) {
return function(target) {
target.prototype.school = school_name;
}
}

@addSchool('CQMU')
class Student {
// ...
}

let lin = new Student('lin', 20);
console.log(lin.school); // > CQMU

装饰器不但可以装饰“类”,也可以对方法(…属性)进行修饰,使用的方式类似于对“类”的修饰,不过用于修饰的函数接收三个参数,target将要被修饰的对象, name被修饰的属性名, descriptor被修饰的属性的描述对象(ES5中详细介绍过)。 写一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

function showCount(target, name, descriptor) {
let prev = descriptor.value;
descriptor.value = function() {
console.log('count:' + StudentList.list.length);
prev.call(null, arguments);
}
return descriptor;
}

class StudentList {

static list = ['limoer', 'lin'];

@showCount
showNames () {
console.log(StudentList.list.join(' '));
}
}

let list = new StudentList();
list.showNames(); // count:2 \n limoer lin

上面的代码给StudentList类的showNames方法添加了打印数量的功能,并且是在不改变原有“类”结构的情况下。

说明,在现有的浏览器环境和Node都不能运行上面的代码(暂不支持装饰器),如果想运行的话,可以借用babel 并且使用相关插件(babel-plugin-transform-decorators-legacy)的前提下进行compile,之后就可以进行了。推荐开发过程中webpack和babel结合使用,效果更佳!

好了,关于Decorator简单介绍到此到一段落,更多的相关知识请自行发掘和学习。接下来,是时候了解并使用Mobx了!

Mobx?想说爱你不容易!

在文章最开头谈到我在最近的学习开发中使用了Mobx作为状态管理工具,最主要的原因是其相比Redux,学习和快速上手成本的确消了很多,并且它足够简单。但是在后来的开发过程中,虽然其可以没有redux中action,也不存在reducer,更是告别了单一而庞大的store,我们可以定义多个state用于保存状态,让每个状态或者是每个类属性添加注解,让其编程可观察的状态,而为了能够自动的更新值,我们可以通过使用computed这个装饰器或者autorun函数来完成。可是,在使用过程中,定义多少个状态,每个状态的结构又是如何,等等等等,都困扰着我,远没有使用redux来得清晰和直观。这也许是因为我对mobx目前刚好达到基本使用的程度,并没有深入的了解。基于此,接下来,我只谈谈Mobx入门,至于该如何优雅的使用,请自行摸索。

几个概念

  1. 可观察的状态

这也许是Mobx最基础也是最重要的概念了。我们可以使用Mobx提供的observable装饰器,让基本的数据结构(数组、对象、原始值等)变成可观察的。使用的方式如下:

1
2
3
4
5
6
7
8
9
10
let TimeState = observable({
currentTime: Date.now()
})
TimeState.set("currentTime", new Date().toString());

class AppState {
@observable list = ['limoer', 'lin'];
}
let state = new AppState();
console.log(state.list.length); // > 2

好了,最简单的例子就是这样,我们使用ES5和ES6 decorator的方式分别创建了两个state,第一个state我们适应装饰器让一个对象(Map)变得可观察,而第二个我们则是对一个“类”属性(为一个数组)进行了修饰,让其变成可观察的。

这里值得注意的是,如果一个数据结构变得可观察,那么其类型也会发生改变,例如我们让一个数据变得可观察,此时其已经变成了一个 Observable Array, 这是一种Mobx定义的数据结构,拥有其独特的API,此时使用Array.isArray(state.list)讲返回false,因为Observable Array 并不是一种数组类型。

好了,当看到这里,你是否有这样一个疑问:让一个数据结构变得可观察,其作用到底在哪里呢?其实很简单,我们都知道Mobx是React的小伙伴,其目的是在于替换React本身的state,我们都知道对于React而言,如果一旦state发生改变,就将导致页面更新并且重新渲染,基于此,让数据结构变得可观察,其目的是在于当被观察的数据发生改变,React也能做出相应的更新和重绘操作等,并且,这样的重绘是经过Mobx优化的,只进行必要的重绘来增加性能!

  1. 可计算值

可计算值是通过现有状态和其它可计算值派生出来的值。这很好理解,我们在使用React的时候,往往要通过state衍生出很多的值,例如如果state的一部分是一个数组,那么我们通过衍生得到的数组长度就是一个计算值,并且在Mobx中,一旦可观察的state或者其他computed value 发生改变,可计算值就会重新计算。其实,在实际的React项目中,我们在很多地方都使用到了计算值。

还是上面AppState的例子,现在我们给其增加一个计算值,

1
2
3
4
5
6
7
8
9
10
class AppState {
@observable list = ['limoer', 'lin'];
@computed get count() {
return this.list.length;
}
}
let state = new AppState();
console.log(state.count); // > 2
state.list.push('lindo');
console.log(state.count); // > 3

count是一个计算值,一旦list发生变化,其就会自动重新计算,可以保证,count的值每次都是最新的,并且都是等于list数组的长度。

  1. autorun

其作用和函数名一样好理解,其会自动执行;autorun其本身是一个响应式函数,其使用到的依赖关系state/computed value等一旦发生改变,其就会自动执行一次,效果和计算值类似,但是计算值和autorun的应用场景是不一样的,computed value通常会产生一个新值而autorun达到某种目的而不产生新值,例如生成日志,处理网络请求等。
还是上面的例子,我们继续扩展:

1
2
3
4
class AppState {
// ...省略前面的代码
let logcount = autorun(() => {console.log('count: ' + this.count)});
}

这里我们在autorun中使用了computed value, 一旦发生count改变,就会自动打印出新的count值;当然,初始化state实例对象的时候,就会先执行一次。

  1. action

动作是用来修改状态的。并且只应该对修改状态的函数使用action,要使用动作很简单,使用@action修饰一个函数或者使用action(fn),把要修饰的函数作为参数即可。继续上面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13

class AppState {
// 省略上面的代码
@action.bound
addOne(name) {
this.list.push(name);
}
// 或者
@action
addOne = (name) => {
this.list.push(name);
}
}

上面我们定义了一个函数,用于向列表中添加一个姓名。请注意,ES6 class的写法无法自动绑定到对象,所以使用`@action.bound` 或者是使用ES6中引入的箭头函数(推荐)。

与React使用

  1. observer
    observer是由mobx-react包(需独立安装)提供的用于让组件变成响应式组件的decorator。官方文档中写到:它用 mobx.autorun 包装了组件的 render 函数以确保任何组件渲染中使用的数据变化时都可以强制刷新组件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import React, { Component } from 'react';
import { render } from 'react-dom';
// 其余依赖省略
@observer
class NameList extends Component {
addUser = (e) => {
e.preventDefault();
if(this.uname.value){
this.props.appstate.addOne(this.uname.value);
}else{
console.log('must input user name!');
}
}
render() {
return <div>
<ul>
{
this.props.appstate.list.map((index, name) => {
return <li key={index + 10}>{name}</li>
})
}
</ul>
<div>
<p>当前用户人数:{this.props.appstate.count}</p>
<label for="uname">姓名</label>
<input type="text" name="uname" ref={(ref) => this.uname = ref}/>
<button onClick={this.addUser}>+</button>
</div>
</div>
}
}

render(<NameList appstate={appstate} />, document.getElementById('app'));

上面是一个响应式组件的例子,结合了上面定义的状态,我们可以查看所有的姓名、数量,并且可以通过点击按钮来改变state。其实observer对非响应式组件仍然有效,同样是上面的例子:

1
2
3
4
5
6
7
const List = observer(({appstate}) => {
return <ul>
appstate.list.map((index, name) => {
return <li key={index + 19}>{name}</li>
})
</ul>
})

好了,对于observer的介绍就告一段落,更多的Mobx和React连接的方式,以及Mobx提供的生命钩子函数等相关知识你可以查看官方文档来了解。

尾巴

自从放了暑假回了家,效率下降特别多,在学校的时候以为回家可以安心学习,到了家才知道一切都变了,该做的事情还没做,还有更多的知识要学习。所以,早早回学校也许是一个不错的选择!所以再过几天,就要启程回学校了,在最后一年里,期待所有的努力都没有白费,期待一个新(好)的开始!

分享到 评论