├── _config.yml
├── fb.md
├── spider-for-ssr.md
├── this.md
├── viewport.md
├── webpack-loader.md
├── xss.md
├── zhihu-spider.md
└── 答题救不了前端新人.md
/_config.yml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/fb.md:
--------------------------------------------------------------------------------
1 | # JavaScript 函数式编程到底是个啥
2 |
3 | 随着大前端时代的到来,在产品开发过程中,前端所占业务比重越来越大、交互越来越重。传统的老夫拿起JQuery就是一把梭应付当下重交互页面已经十分乏力。于是乎有了Angular,React,Vue这些现代框架。
4 |
5 | 但随之而来的还有大量的新知识新名词,如MVC,MVVM,Flux这些设计模式就弄得很多同学傻傻分不清。这时候又见到别人讨论什么函数式编程,更是一脸懵逼了。
6 |
7 | 我们大多听过面向对象编程,面向过程编程,那啥又是函数式编程呢?在我们前端开发中又有哪些应用场景?我抱着这个疑惑,初步的学习了下。
8 |
9 | ## 函数式编程
10 |
11 | ### 定义
12 |
13 | 函数式编程(Functional Programming,后面简称FP),维基百科的定义是:
14 |
15 | >是一种编程范型,它将电脑运算视为数学上的函数计算,并且避免使用程序状态以及易变对象。函数编程语言最重要的基础是λ演算(lambda calculus)。而且λ演算的函数可以接受函数当作输入(引数)和输出(传出值)。比起命令式编程,函数式编程更加强调程序执行的结果而非执行的过程,倡导利用若干简单的执行单元让计算结果不断渐进,逐层推导复杂的运算,而不是设计一个复杂的执行过程。
16 |
17 | 我来尝试理解下这个定义,好像就是说,在敲代码的时候,我要把过程逻辑写成函数,定义好输入参数,只关心它的输出结果。而且可以把函数作为输入输出。感觉好像平常写js时,就是这样的嘛!
18 |
19 | ### 特性
20 |
21 | 网上FP的定义与特性琳琅满目。各种百科、博客、一些老师的网站上都有大同小异的介绍。为了方便阅读,我列下几个好像比较重要的特性,并附上我的第一眼理解。
22 |
23 | 1. **函数是一等公民**。就是说函数可以跟其他变量一样,可以作为其他函数的输入输出。喔,回调函数就是典型应用。
24 |
25 | 2. **不可变量**。就是说,不能用var跟let咯。按这要求,我似乎有点难写代码。
26 |
27 | 3. **纯函数**。就是没有副作用的函数。这个好理解,就是不修改函数外部的变量。
28 |
29 | 4. **引用透明**。这个也好理解,就是说同样的输入,必定是同样的输出。函数内部不依赖外部状态,如一些全局变量。
30 |
31 | 5. **惰性计算**。大意就是:一个表达式绑定的变量,不是声明的时候就计算出来,而是真正用到它的时候才去计算。
32 |
33 | 还有一些衍生的特性,如柯里化与组合,三言两语说不清,就不阐述了,有兴趣的同学可以自己再了解了解。
34 |
35 | ## FP在JavaScript中的应用
36 |
37 | React就是典型的FP。它不同于Vue这样的MVVM框架,它仅仅是个View层。
38 | `ReactView = render(data)` 它只关心你的输入,最终给你返回相应视图。所以你休想在react组件中去修改父组件的状态,更没有与dom的双向绑定。
39 |
40 | 这个是框架上的应用,那么在我们平常书写JavaScript时有哪些应用呢?换句话说,平常书写js时候,遇到什么情况,我们采用FP会更好。
41 |
42 | 从最常见的入手吧,如典型的操作数组:
43 | ```javascript
44 | // 从users中筛选出年龄大于15岁的人的名字
45 | const users = [
46 | {
47 | age: 10,
48 | name: '张三',
49 | }, {
50 | age: 20,
51 | name: '李四'
52 | }, {
53 | age: 30,
54 | name: '王五'
55 | }
56 | ];
57 |
58 | // 过程式
59 | const names = [];
60 | for (let i = 0; i < users.length; i++) {
61 | if (users[i].age > 15) {
62 | names.push(users[i].name);
63 | }
64 | }
65 | // 函数式
66 | const names = users.filter(u => u.age > 15).map(u => u.name);
67 |
68 | ```
69 |
70 | 嗯,代码精简了很多,但是貌似带来了更大的开销。如果是非常大的数据,非常多的筛选工作,那就会循环多次。
71 |
72 | 这里得想到刚刚的惰性计算。按照惰性求值的要求,应该是要最后返回结果时,才真正去筛选年纪并得到姓名数组。
73 |
74 | 然而JavaScript的数组并不支持惰性求值。这时候我们得上一些工具库,如[Lodash](https://lodash.com/)。可以看下它文档中的例子:[_.chain](https://lodash.com/docs/4.17.4#chain)。
75 |
76 | 好像也没好到哪里去啊,不就是把多行代码变一行嘛?说的那么玄乎,还多了性能开销,然后又跟我说得上个工具库。。。
77 |
78 | 说的好像很有道理,但是for循环是有个弊端的,它产生了变量i,而这个变量又是不可控的,如果业务逻辑一复杂,谁知道它循环到什么时候i有没有发生变化,然后导致循环出问题呢?
79 |
80 | 我们再看一个与DOM交互的场景:
81 | 假如页面有一个按钮`button`,我们需要求出用户点击了几次,但是一秒钟内重复点击的不算。传统方法会这么写。
82 | ``` javascript
83 | var count = 0;
84 | var rate = 1000;
85 | var lastClick = Date.now() - rate;
86 | var button = document.querySelector('button');
87 | button.addEventListener('click', () => {
88 | if (Date.now() - lastClick >= rate) {
89 | console.log(`Clicked ${++count} times`);
90 | lastClick = Date.now();
91 | }
92 | });
93 | ```
94 | 妥,完全没问题。但是发现多了很多状态,count,rate,lastClick,还得对比来对比去。那如果用FP会是怎么样的呢?
95 |
96 | 抱歉。。。没法写。。。除非很强大的编程能力,自己封装好方法去处理。所以在这里,我们可以上个工具---[Rx.js](http://reactivex.io/rxjs/manual/overview.html),上述的例子就是rxjs中引用的,我们看它是如何优雅地处理的。
97 |
98 | ``` javascript
99 | var button = document.querySelector('button');
100 | Rx.Observable.fromEvent(button, 'click')
101 | .throttleTime(1000) // 每隔1000毫秒才能触发事件
102 | .scan(count => count + 1, 0) // 求值,默认值是0
103 | .subscribe(count => console.log(`Clicked ${count} times`)); // 订阅结果、输出值
104 | ```
105 | 巧夺天工!再也不用去管理状态了,不需要声明一堆变量,修改来修改去,判断来判断去,简直完美。
106 |
107 | 平常我们有很多需要更新dom的异步操作,如搜索行为:用户连续输入查询值,如果停顿半秒就执行搜索,如果搜索了多次,发起了多次请求,那只返回最终输入的那次搜索结果。
108 |
109 | 闭上眼想想,你之前是怎么实现的。反正我都是设置开始时间,结束时间,上次时间,等等变量。繁琐,而且不可控。
110 |
111 | 当我们以FP的思想去实现时,就会想方设法的减少变量,来优雅程序。最常见的方法就是用下别人的工具库来实现它。当然有些简单的场景也可以自己实现,最主要的还是要有这个意识。
112 |
113 | 其实我们平常已经写了一些FP了,只是我们没意识到,或者没怎么写好。就好比闭包,很多人都不了解闭包的概念,但实际上已经写了很多闭包代码。其实闭包本身也是函数式编程的一个应用。
114 |
115 | 鉴于我自己理解也不深,没法多阐述FP的应用,大家如果有兴趣,可以多了解了解。
116 |
117 | ## FP在JavaScript中的优劣势
118 |
119 | 总结一下FP的优劣,以便于我们在实际开发中,能更好的抉择是否采用FP。
120 |
121 | ### 优势
122 |
123 | 1. **更好的管理状态**。因为它的宗旨是无状态,或者说更少的状态。而平常DOM的开发中,因为DOM的视觉呈现依托于状态变化,所以不可避免的产生了非常多的状态,而且不同组件可能还相互依赖。以FP来编程,能最大化的减少这些未知、优化代码、减少出错情况。
124 |
125 | 2. **更简单的复用**。极端的FP代码应该是每一行代码都是一个函数,当然我们不需要这么极端。我们尽量的把过程逻辑以更纯的函数来实现,固定输入->固定输出,没有其他外部变量影响,并且无副作用。这样代码复用时,完全不需要考虑它的内部实现和外部影响。
126 |
127 | 3. **更优雅的组合**。往大的说,网页是由各个组件组成的。往小的说,一个函数也可能是由多个小函数组成的。参考上面第二点,更强的复用性,带来更强大的组合性。
128 |
129 | 4. 隐性好处。减少代码量,提高维护性。
130 |
131 | ### 劣势
132 |
133 | 1. JavaScript不能算是严格意义上的函数式语言,很多函数式编程的特性并没有。比如上文说的数组的惰性链求值。为了实现它就得上工具库,或者自己封装实现,提高了代码编写成本。
134 |
135 | 2. 跟过程式相比,它并没有提高性能。有些地方,如果强制用FP去写,由于没有中间变量,还可能会降低性能。
136 |
137 | 3. 代码不易读。这个因人而异,因码而已。特别熟悉FP的人可能会觉得这段代码一目了然。而不熟悉的人,遇到写的晦涩的代码,看着一堆堆lambda演算跟匿名函数 `() => () => ()` 瞬间就懵逼了。看懂代码,得脑子里先演算半小时。
138 |
139 | 4. 学习成本高。一方面继承于上一点。另一方面,很多前端coder,就是因为相对不喜欢一些底层的抽象的编程语言,才来踏入前端坑,你现在又让他们一头扎入FP,显得手足无措。
140 |
141 | ## 总结
142 |
143 | 个人觉得,FP还是好的。对于开发而言,确确实实能优化我们的代码,熟悉之后,也能提高编程效率。对于编程本身而言,也能拓展我们的思维,不局限在过程式的编程代码。
144 |
145 | 在编写JS中,可以尽量的运用FP的思维,如不可变量、纯函数、惰性求值。但也不必教条式的遵循函数式编程,一定要怎样怎样。比如我们看下知乎大V某温的一个回答:[传送门](https://www.zhihu.com/question/59871249/answer/171201717)。
146 |
147 | 唉,做个页面仔不容易啊。但是不想当大牛的页面仔不是好页面仔!
148 |
149 |
150 | ## 参考
151 |
152 | 1. [函数式编程入门教程-阮一峰](http://www.ruanyifeng.com/blog/2017/02/fp-tutorial.html)
153 | 2. [函数编程语言-维基百科](https://zh.wikipedia.org/wiki/%E5%87%BD%E6%95%B8%E7%A8%8B%E5%BC%8F%E8%AA%9E%E8%A8%80)
154 | 3. [前端开发js函数式编程真实用途体现在哪里?-知乎答者](https://www.zhihu.com/question/59871249)
155 |
--------------------------------------------------------------------------------
/spider-for-ssr.md:
--------------------------------------------------------------------------------
1 | 前端发展到现在,SPA应该已经被应用的非常广了。可惜的是,我们前进的是快,而人家搜索引擎爬虫跟用户的浏览器设备还跟不上脚步。辛辛苦苦写好的单页应用,结果到了SEO跟浏览器兼容这一步懵逼了。
2 |
3 | 很多同学肯定都想过服务端渲染的问题。然而一看vue、react关于服务端渲染的文档,可能就被唬住了。之前写好的并不能无缝迁移。而且,每当有个项目,就需要去run一套node服务。当然,架构能力好些的朋友,可以做好集中化管理。
4 |
5 | 所以,当我想在项目中,采用vue或者react的时候,就遇到这些非常大的阻力。正当我头疼脑热的时候呢,我发现了一条新途径。
6 |
7 | 在前不久呢,同事在群里分享了[puppeteer](https://github.com/GoogleChrome/puppeteer),它GitHub的介绍如下:
8 |
9 | > Puppeteer is a Node library which provides a high-level API to control headless Chrome over the DevTools Protocol. It can also be configured to use full (non-headless) Chrome.
10 |
11 | 大意就是说,一个提供操作Headless Chrome的API的node库。
12 |
13 | 再具体的说,就是能在node环境中,通过一些API,来“模拟”真实chrome访问页面,并对其进行模拟用户操作、获取DOM等。
14 |
15 | 那既然它能够像真实Chrome那样去访问页面并且输出渲染后的html,我为什么不能通过它来给我们做服务端渲染呢?
16 |
17 | 设想一下,我们有这样一个服务A,它能够像chrome一样访问指定页面,并把最终页面上的dom返回给你。
18 |
19 | 而你原本的业务服务器B,只需要判断是爬虫,或者低版本IE来访问时,调取该服务,得到html,将html返回给用户,这就实现了服务端渲染。大致流程图如下:
20 |
21 | 
22 |
23 | 有这样一个思路后,我们就想办法来实践它。实践的过程,就是解决问题的过程。仔细想想,我们会遇到如下几个问题:
24 |
25 | > Q1: 即使是模拟Chrome去请求页面,很多时候视图也是异步渲染的。比如先请求列表接口,得到数据再渲染出列表DOM。这个时间,我们并没有办法把控。那这个服务,到底时候才应该把加载完成的HTML返回呢?
26 |
27 | 遇到问题时,首先可以看看人家的文档 [Puppeteer API](https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md)。欣喜的是,我们找到了如下几个方法:
28 | ```javascript
29 | page.waitFor(selectorOrFunctionOrTimeout[, options[, ...args]])
30 | page.waitForFunction(pageFunction[, options[, ...args]])
31 | page.waitForNavigation(options)
32 | page.waitForSelector(selector[, options])
33 | ```
34 | 我们可以通过一些设定,让页面在某种情况下才返回。比如我们通过设定 `page.waitForSelector('#app')`, 让页面出现 `id="app"` 的元素时,才把html内容返回。
35 |
36 | 或者通过设定 `page.waitForFunction('window.innerWidth < 100')`,当页面宽度小于100px时,才将此时的html内容返回。
37 |
38 | 通过这些方法,我们就能有办法控制,想要输给爬虫的,是什么时候、什么样的页面。
39 |
40 | > Q2: 如果IE用户访问量比较大怎么办。我们虽然通过这样的系统,让本渲染不出页面的部分浏览器(IE9以下)能够渲染出页面了。但这样的请求过程相对而言会更耗时,这不是很合理。
41 |
42 | 那我们只要做一个缓存系统便好。每次请求,都会去判断此请求是否存在未过期的缓存HTML,如果存在,则直接返回缓存HTML,否则再去请求页面,保存缓存。
43 |
44 | > Q3: 虽然页面是出来了,IE用户还是没办法做一些JS的交互。
45 |
46 | 这个我们没办法在服务层上去解决了,但我们可以在前端上做更友好的交互提示。如果判断用户是低版本IE,则出现一个小Tip,提示用户下载更好的浏览器,获取更好的体验。
47 |
48 | > Q4: 单页应用的路由多是用锚点(哈希模式)来做的,而哈希参数,服务端无法获取,那就没办法请求正确的页面了。
49 |
50 | 这个有办法解决,可以采用HTML History模式的路由,如[vue-router](https://router.vuejs.org/zh-cn/essentials/history-mode.html),然后路由链接最好以生成a标签+href的模式写在页面中,而不是`onclick`后js跳转,这样爬虫能最好的爬取整站页面。
51 |
52 | 当问题都想到办法解决后,我们就能开始真正coding了。
53 |
54 | 啪啪啪,啪啪啪 => [SSR-SERVICE](https://github.com/DXY-F2E/ssr-service)
55 |
56 | 好,然后就好了,不到200行的代码,我们就实现了一个 通用化的、服务化的、单页应用服务端渲染解决方案。
57 |
58 |
59 | --[阅读原文](https://github.com/wuomzfx/blog/blob/master/spider-for-ssr.md) @[相学长](https://www.zhihu.com/people/xiang-xue-zhang)
60 |
61 | --转载请先经过本人授权。
62 |
--------------------------------------------------------------------------------
/this.md:
--------------------------------------------------------------------------------
1 | 日常开发中,我们经常用到this。例如用Jquery绑定事件时,this指向触发事件的DOM元素;编写Vue、React组件时,this指向组件本身。对于新手来说,常会用一种意会的感觉去判断this的指向。以至于当遇到复杂的函数调用时,就分不清this的真正指向。
2 |
3 | 本文将通过两道题去慢慢分析this的指向问题,并涉及到函数作用域与对象相关的点。最终给大家带来真正的理论分析,而不是简简单单的一句话概括。
4 |
5 | 相信若是对this稍有研究的人,都会搜到这句话:**this总是指向调用该函数的对象**。
6 |
7 | 然而箭头函数并不是如此,于是大家就会遇到如下各式说法:
8 |
9 | 1. 箭头函数的this指向外层函数作用域中的this。
10 | 2. 箭头函数的this是定义函数时所在上下文中的this。
11 | 3. 箭头函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。
12 |
13 | 各式各样的说法都有,乍看下感觉说的差不多。废话不多说,凭着你之前的理解,来先做一套题吧(非严格模式下)。
14 |
15 | ```javascript
16 | /**
17 | * Question 1
18 | */
19 |
20 | var name = 'window'
21 |
22 | var person1 = {
23 | name: 'person1',
24 | show1: function () {
25 | console.log(this.name)
26 | },
27 | show2: () => console.log(this.name),
28 | show3: function () {
29 | return function () {
30 | console.log(this.name)
31 | }
32 | },
33 | show4: function () {
34 | return () => console.log(this.name)
35 | }
36 | }
37 | var person2 = { name: 'person2' }
38 |
39 | person1.show1()
40 | person1.show1.call(person2)
41 |
42 | person1.show2()
43 | person1.show2.call(person2)
44 |
45 | person1.show3()()
46 | person1.show3().call(person2)
47 | person1.show3.call(person2)()
48 |
49 | person1.show4()()
50 | person1.show4().call(person2)
51 | person1.show4.call(person2)()
52 | ```
53 | 大致意思就是,有两个对象`person1`,`person2`,然后花式调用person1中的四个show方法,预测真正的输出。
54 |
55 | 你可以先把自己预测的答案按顺序记在本子上,然后再往下拉看正确答案。
56 |
57 |
58 | ***
59 | ***
60 | 正确答案选下:
61 | ```javascript
62 | person1.show1() // person1
63 | person1.show1.call(person2) // person2
64 |
65 | person1.show2() // window
66 | person1.show2.call(person2) // window
67 |
68 | person1.show3()() // window
69 | person1.show3().call(person2) // person2
70 | person1.show3.call(person2)() // window
71 |
72 | person1.show4()() // person1
73 | person1.show4().call(person2) // person1
74 | person1.show4.call(person2)() // person2
75 | ```
76 |
77 | 对比下你刚刚记下的答案,是否有不一样呢?让我们尝试来最开始那些理论来分析下。
78 |
79 | `person1.show1()`与`person1.show1.call(person2)`好理解,验证了**谁调用此方法,this就是指向谁**。
80 |
81 | `person1.show2()`与`person1.show2.call(person2)`的结果用上面的定义解释,就开始让人不理解了。
82 |
83 | 它的执行结果说明this指向的是window。那就不是所谓的定义时所在的对象。
84 |
85 | 如果说是外层函数作用域中的this,实际上并没有外层函数了,外层就是全局环境了,这个说法也不严谨。
86 |
87 | 只有**定义函数时所在上下文中的this**这句话算能描述现在这个情况。
88 |
89 | `person1.show3`是一个高阶函数,它返回了一个函数,分步走的话,应该是这样:
90 | ```javascript
91 | var func = person3.show()
92 |
93 | func()
94 | ```
95 | 从而导致最终调用函数的执行环境是window,但并不是window对象调用了它。所以说,**this总是指向调用该函数的对象**,这句话还得补充一句:**在全局函数中,this等于window**。
96 |
97 | `person1.show3().call(person2)` 与 `person1.show3.call(person2)()` 也好理解了。前者是通过person2调用了最终的打印方法。后者是先通过person2调用了person1的高阶函数,然后再在全局环境中执行了该打印方法。
98 |
99 | `person1.show4()()`,`person1.show4().call(person2)`都是打印person1。这好像又印证了那句:**箭头函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象**。因为即使我用过person2去调用这个箭头函数,它指向的还是person1。
100 |
101 | 然而`person1.show4.call(person2)()`的结果又是person2。this值又发生改变,看来上述那句描述又走不通了。一步步来分析,先通过person2执行了show4方法,此时show4第一层函数的this指向的是person2。所以箭头函数输出了person2的name。也就是说,箭头函数的this指向的是**谁调用箭头函数的外层function,箭头函数的this就是指向该对象,如果箭头函数没有外层函数,则指向window**。这样去理解show2方法,也解释的通。
102 |
103 | 这句话就对了么?在我们学习的过程中,我们总是想以总结规律的方法去总结结论,并且希望结论越简单越容易描述就越好。实际上可能会错失真理。
104 |
105 | 下面我们再做另外一个相似的题目,通过构造函数来创建一个对象,并执行相同的4个show方法。
106 | ```javascript
107 | /**
108 | * Question 2
109 | */
110 | var name = 'window'
111 |
112 | function Person (name) {
113 | this.name = name;
114 | this.show1 = function () {
115 | console.log(this.name)
116 | }
117 | this.show2 = () => console.log(this.name)
118 | this.show3 = function () {
119 | return function () {
120 | console.log(this.name)
121 | }
122 | }
123 | this.show4 = function () {
124 | return () => console.log(this.name)
125 | }
126 | }
127 |
128 | var personA = new Person('personA')
129 | var personB = new Person('personB')
130 |
131 | personA.show1()
132 | personA.show1.call(personB)
133 |
134 | personA.show2()
135 | personA.show2.call(personB)
136 |
137 | personA.show3()()
138 | personA.show3().call(personB)
139 | personA.show3.call(personB)()
140 |
141 | personA.show4()()
142 | personA.show4().call(personB)
143 | personA.show4.call(personB)()
144 | ```
145 | 同样的,按照之前的理解,再次预计打印结果,把答案记下来,再往下拉看正确答案。
146 |
147 | ***
148 | ***
149 | 正确答案选下:
150 | ```javascript
151 | personA.show1() // personA
152 | personA.show1.call(personB) // personB
153 |
154 | personA.show2() // personA
155 | personA.show2.call(personB) // personA
156 |
157 | personA.show3()() // window
158 | personA.show3().call(personB) // personB
159 | personA.show3.call(personB)() // window
160 |
161 | personA.show4()() // personA
162 | personA.show4().call(personB) // personA
163 | personA.show4.call(personB)() // personB
164 | ```
165 |
166 | 我们发现与之前字面量声明的相比,show2方法的输出产生了不一样的结果。为什么呢?虽然说构造方法Person是有自己的函数作用域。但是对于personA来说,它只是一个对象,在直观感受上,它跟第一道题中的person1应该是一模一样的。 `JSON.stringify(new Person('person1')) === JSON.stringify(person1)`也证明了这一点。
167 |
168 | 说明构造函数创建对象与直接用字面量的形式去创建对象,它是不同的,构造函数创建对象,具体做了什么事呢?我引用红宝书中的一段话。
169 |
170 | > 使用 new 操作符调用构造函数,实际上会经历一下4个步骤:
171 | > 1. 创建一个新对象;
172 | > 2. 将构造函数的作用域赋给新对象(因此this就指向了这个新对象);
173 | > 3. 执行构造函数中的代码(为这个新对象添加属性);
174 | > 4. 返回新对象。
175 |
176 | 所以与字面量创建对象相比,很大一个区别是它多了构造函数的作用域。我们用chrome查看这两者的作用域链就能清晰的知道:
177 |
178 | 
179 |
180 | 
181 |
182 | personA的函数的作用域链从构造函数产生的闭包开始,而person1的函数作用域仅是global,于是导致this指向的不同。我们发现,要想真正理解this,先得知道到底什么是作用域,什么是闭包。
183 |
184 | 有简单的说法称闭包就是能够读取其他函数内部变量的函数。然而这是一种闭包现象的描述,而不是它的本质与形成的原因。
185 |
186 | 我再次引用红宝书的文字(便于理解,文字顺序稍微调整),来描述这几个点:
187 |
188 | >...每个函数都有自己的执行环境(execution context,也叫执行上下文),每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。
189 |
190 | >...当执行流进入一个函数时,函数的环境就会被推入一个环境栈中。当代码在环境中执行时,会创建一个作用域链,来保证对执行环境中的所有变量和函数的有序访问。函数执行之后,栈将环境弹出。
191 |
192 | >...函数内部定义的函数会将包含函数的活动对象添加到它的作用域链中。
193 |
194 | 具体来说,当我们 `var func = personA.show3()` 时,`personA`的`show3`函数的活动对象,会一直保存在`func`的作用域链中。只要不销毁`func`,那么`show3`函数的活动对象就会一直保存在内存中。(chrome的v8引擎对闭包的开销会有优化)
195 |
196 | 而构造函数同样也是闭包的机制,`personA`的`show1`方法,是构造函数的内部函数,因此执行了 `this.show3 = function () { console.log(this.name) }`时,已经把构造函数的活动对象推到了show3函数的作用域链中。
197 |
198 | 我们再回到this的指向问题。我们发现,单单是总结规律,或者用一句话概括,已经难以正确解释它到底指向谁了,我们得追本溯源。
199 |
200 | 红宝书中说道:
201 | >...this引用的是函数执行的环境对象(便于理解,贴上英文原版:It is a reference to the context object that the function is operating on)。
202 | >...每个函数被调用时都会自动获取两个特殊变量:this和arguments。内部在搜索这个两个变量时,只会搜索到其活动对象为止,永远不可能直接访问外部函数中的这两个变量。
203 |
204 |
205 | 我们看下MDN中箭头函数的概念:
206 | >一个箭头函数表达式的语法比一个函数表达式更短,并且不绑定自己的 `this`,`arguments`,`super`或 `new.target`。...箭头函数会捕获其所在上下文的 `this` 值,作为自己的 `this` 值。
207 |
208 | 也就是说,普通情况下,this指向调用函数时的对象。在全局执行时,则是全局对象。
209 |
210 | 箭头函数的this,因为没有自身的this,所以this只能根据作用域链往上层查找,直到找到一个绑定了this的函数作用域(即最靠近箭头函数的普通函数作用域,或者全局环境),并指向调用该普通函数的对象。
211 |
212 | 或者从现象来描述的话,即**箭头函数的this指向声明函数时,最靠近箭头函数的普通函数的this。但这个this也会因为调用该普通函数时环境的不同而发生变化。导致这个现象的原因是这个普通函数会产生一个闭包,将它的变量对象保存在箭头函数的作用域中**。
213 |
214 | 故而`personA`的`show2`方法因为构造函数闭包的关系,指向了构造函数作用域内的this。而
215 |
216 | ```javascript
217 | var func = personA.show4.call(personB)
218 |
219 | func() // print personB
220 | ```
221 | 因为personB调用了personA的show4,使得返回函数func的作用域的this绑定为personB,进而调用func时,箭头函数通过作用域找到的第一个明确的this为personB。进而输出personB。
222 |
223 | 讲了这么多,可能还是有点绕。总之,想充分理解this的前提,必须得先明白js的执行环境、闭包、作用域、构造函数等基础知识。然后才能得出清晰的结论。
224 |
225 | **我们平常在学习过程中,难免会更倾向于根据经验去推导结论,或者直接去找一些通俗易懂的描述性语句。然而实际上可能并不是最正确的结果。如果想真正掌握它,我们就应该追本溯源的去研究它的内部机制。**
226 |
227 | 我上述所说也是我自己推导出的结果,即使它不一定正确,但这个推断思路跟学习过程,我觉得可以跟大家分享分享。
228 |
229 |
230 |
231 | --[阅读原文](https://github.com/wuomzfx/blog/blob/master/this.md) @[相学长](https://www.zhihu.com/people/xiang-xue-zhang)
232 |
233 | --转载请先经过本人授权。
234 |
--------------------------------------------------------------------------------
/viewport.md:
--------------------------------------------------------------------------------
1 | # 移动端适配初探
2 |
3 | IE时代,每一个前端同学都要费尽心力兼容各代版本IE。如今IE虽渐渐淘汰,但移动互联网崛起,我们又有了新的麻烦。因为各个手机的屏幕不同,为了保证产品视觉呈现与体验更加完美和谐,移动端的适配也显得较为棘手。
4 |
5 | 目前较为知名的一个方案是手淘的flexible。它具体如何实现移动端的适配,我们后面再阐述,因为在此之前,我们需要先理清一些基础概念。
6 |
7 | * **screen.width**
8 | 设备屏幕的宽度,以像素计。这个属性只读,不会因为浏览器的拖拉缩放而改变。
9 |
10 | * **window.innerWidth**
11 | 浏览器窗口的内部宽度。它包含滚动条,并且会随着浏览器拖拉缩放而改变。具体表现为呈现出内容的浏览器窗口的宽度。
12 |
13 | * **clientWidth**
14 | 元素的可见宽度(不计滚动条)。document.documentElement.clientWidth 即整个html的可见宽度。
15 |
16 | * **offetWidth**
17 | 元素的实际宽度(包含滚动条)。document.documentElement.offsetWidth即整个html的实际宽度。
18 |
19 | 然后我们来看一个莫名其妙的事情。我手上是一只三星s7e,号称屏幕分辨率1440*2560。
20 |
21 | 现在我写了一个页面,body里设置了一个div,设置宽高为300px。并打印了刚阐述的那些宽度尺寸与div的宽度,没看到结果前,乍一想,应该是4个1440和一个300吧。然后屏幕蹦跶出这样的结果。
22 |
23 | 
24 |
25 | 莫名其妙吧,跟想像中完全不一样啊。屏幕宽度咋成412px了,说好的2k屏呢?还有980px又是哪儿蹦达出来的。为了搞清这当中的奥秘,我们又得来学习一些概念了。
26 |
27 | * **设备像素**
28 | 顾名思义,就是设备的真实物理像素,指每英寸屏幕所拥有的像素(pixel)数目。
29 |
30 | * **设备独立像素**
31 | device independent pixel,独立于设备的用于逻辑上衡量像素的单位。
32 |
33 | * **CSS像素**
34 | 指的是CSS样式代码中使用的逻辑像素,比如上面那个div,我设置了宽度300px,就是CSS像素,它并不一定等于它真实的物理像素。所以CSS像素即是用在CSS语言中的设备独立像素。
35 |
36 | * **设备像素比**
37 | device pixel ratio,即设备像素/设备独立像素,即物理像素/逻辑像素。在网页中就是设备像素与CSS像素的比值。设备像素比能通过window.devicePixelRatio获取。
38 |
39 | 通过dpr我知道了s7e此比值为3.5。那么screen.width 就是这手机的独立像素的值,412*3.5,确实等于1442。
40 |
41 | 可是问题又来了,980px又是如何来的呢?为什么告诉我逻辑分辨率是412*731了,怎么html的宽度又是980呢?
42 |
43 | ## Viewport(视区)
44 | Viewport可以理解成浏览器用来呈现网页的那一部分区域,它是一个移动端的特性,可以通过Meta标签来设定。
45 |
46 | ### Layout viewport
47 | 因为移动设备的逻辑分辨率一般都是比PC小的,为了能让移动设备也能比较好的呈现PC网页内容,各大移动端浏览器就把viewport的默认值设成了980px或者1024px。
48 |
49 | 也就是说,不管你设备屏幕多大,分辨率多少,浏览器窗口的逻辑宽度,就是设定的值(比如980px),如果一个元素宽度是980px,那就刚好占满屏幕宽度。如果html宽度超出980px,那窗口就出现滚动条。除非隐藏内容,或者缩放屏幕。
50 |
51 | 这个呈现网页的区域,就叫做layout viewport。于是,我做的页面出现了上述结果。(至于为什么会多了1px成了981,这个不同手机有不同结果,我还未探索到确切原因,猜测是分辨率不同导致处理策略不同特意设的偏差值。)
52 |
53 | ### Visual viewport
54 | 上面陈述到,layout viewport可能是会比浏览器可视区域大的,这个visual viewport指的就是可视的viewport区域。简单的理解,就是屏幕可视区域。可以在visual viewport上拖拉缩放,来查看layout viewport的内容。
55 |
56 | ### Ideal viewport
57 | layout默认值是980px,可是我们很多页面就是专门为了移动端设计的,我们不希望用户要缩放、横屏滚动才能看到内容,而且内容还很小。最好viewport就是屏幕宽度,这样viewport就是ideal vieaport 。
58 |
59 | ### Viewport meta
60 | 我们希望可以自己设定viewport值,来呈现自己想呈现的内容,那就得通过meta标签来定义viewport属性,具体如下:
61 |
62 | Name | Value | Description
63 | --------------|------------------------|----
64 | width | 正整数或device-width | 定义视口的宽度,单位为像素
65 | height | 正整数或device-height | 定义视口的高度,单位为像素
66 | initial-scale | [0.0-10.0] | 定义初始缩放值
67 | minimum-scale | [0.0-10.0] | 定义缩小最小比例,它必须小于或等于maximum-scale设置
68 | maximum-scale | [0.0-10.0] | 定义放大最大比例,它必须大于或等于minimum-scale设置
69 | user-scalable | yes/no | 定义是否允许用户手动缩放页面,默认值yes
70 |
71 | 最常用的写法就是
72 | ```html
73 |
76 | ```
77 |
78 | 意思就是,设置viewport视区大小为设备宽度,即理想视区,默认缩放1.0倍(就是不缩放),并且禁止用户缩放。
79 |
80 | 还有一种常用的写法,就是根据自己设备的dpr来还原设备本来宽度,如假设手机devicePixelRatio为2,则设置
81 | ```html
82 |
86 | ```
87 |
88 | ## 开始适配
89 | 了解了这些,前端适配工作总算可以开始了。但马上出现了一道大山。我们让所有设备的viewport都等于device-width,带来了一个新的问题。不同设备的device-width并不相同。即使不考虑pad,只考虑各大主流手机,也从320-400多不等。我们希望不同尺寸设备能呈现几乎一致的内容来保证用户体验。
90 |
91 | 在布局上,我们可以通过诸如flex布局等方法来解决。但是当涉及元素尺寸大小的时候,如果还是用px做单位,那么将会呈现天差地别的差异。那该怎么办呢?
92 |
93 | ### CSS单位rem
94 | 传统页面尺寸我们都用px定义。而rem是相对于根元素的font-size来做计算的。举个例子:根元素的font-size:16px; 那么1rem=16px,0.5rem = 8px。这样一来,我们只要根据不同屏幕尺寸设定不同的font-size。后面只要采用rem做尺寸单位,就能保持视觉一致。那该如何判断不同尺寸屏幕并设置相应font-size呢?
95 |
96 | ### 媒体查询
97 | 媒体查询允许我们来根据设备不同来设定不同的css样式,最常用的就是根据不同屏幕尺寸来设定不同样式。举个栗子:
98 |
99 | ```css
100 | @media screen and (max-width: 320px) {
101 | html {
102 | font-size: 20px;
103 | }
104 | }
105 | ```
106 |
107 | 意思就是当layout viewport小于320px时,根节点的font-size为20px;
108 |
109 | ### JS判断
110 | 在页面加载时判断当前屏幕尺寸,并根据尺寸设定相应的font-size到根节点。
111 |
112 | 这样一来,我们做前端适配的思路就清晰了。
113 |
114 | ## 适配步骤
115 |
116 | 1. **设置viewport为device-width。**
117 |
118 | 2. **通过媒体查询、或者js,判断不同屏幕尺寸,设定根节点font-size。**
119 |
120 | 3. **使用rem来设定元素尺寸单位。**
121 |
122 | 这样我们再去看手淘的[解决方案](https://github.com/amfe/article/issues/17)[5],就能大致清晰它的原理了。
123 | 但对比上述的几点,可以发现flexible并不是采用viewport设置为device-width,而是采用还原成设备物理分辨率,这样的好处是为了解决高分屏下的1px问题。具体就不展开讨论了。
124 |
125 |
126 | ## 参考
127 | 1. [W3CSchool](http://www.w3school.com.cn)(http://www.w3school.com.cn)
128 | 2. [A pixel is not a pixel is not a pixel](http://www.quirksmode.org/blog/archives/2010/04/a_pixel_is_not.html)(http://www.quirksmode.org/blog/archives/2010/04/a_pixel_is_not.html)
129 | 3. [移动前端开发之viewport的深入理解](http://www.cnblogs.com/2050/p/3877280.html)(http://www.cnblogs.com/2050/p/3877280.html)
130 | 4. [移动前端第一弹:viewport详解](http://www.kancloud.cn/jaya1992/fe-notes/86799)(http://www.kancloud.cn/jaya1992/fe-notes/86799)
131 | 5. [使用Flexible实现手淘H5页面的终端适配](https://github.com/amfe/article/issues/17)(https://github.com/amfe/article/issues/17)
132 |
--------------------------------------------------------------------------------
/webpack-loader.md:
--------------------------------------------------------------------------------
1 | # 编写自己的Webpack Loader
2 |
3 | 本文将简单介绍webpack loader,以及如何去编写一个loader来满足自身的需求,从而也能提高对webpack的认识与使用,努力进阶为webpack配置工程师。
4 |
5 | ## Webpack Loader
6 |
7 | [webpack](https://github.com/webpack/webpack)想必前端圈的人都知道了,大多数人也都或多或少的用过。简单的说就是它能够加载资源文件,并对这些文件进行一些处理,诸如编译、压缩等,最终一起打包到指定的文件中。可以说,它作为一个打包工具,在前端工程化浪潮中,起到了中流砥柱的作用。
8 |
9 | 那webpack其中非常重要的一环就是,能够对加载的资源文件,进行一些处理。比如把less、sass文件编译成css文件,负责这个处理过程的,就是webpack的loader。
10 |
11 | > loader 用于对模块的源代码进行转换。loader 可以使你在 import 或"加载"模块时预处理文件。因此,loader 类似于其他构建工具中“任务(task)”,并提供了处理前端构建步骤的强大方法。
12 |
13 | 
14 |
15 | 举个稍微复杂的例子,[vue-loader](https://github.com/vuejs/vue-loader),它官网介绍如下:
16 |
17 | > vue-loader 是一个 Webpack 的 loader,可以将指定格式编写的 Vue 组件转换为 JavaScript 模块。
18 |
19 | Vue组件默认分成三部分,``、``。那么再打开此链接,就会直接执行alert。当然搜狗肯定是做了安全处理了。
16 |
17 | 这种攻击都是一次性的,得先找到漏洞地址后,设置好url,然后发给别人,诱使别人点击,从而通过执行脚本,获取对方的cookie。你得到对方的cookie后,就可以为所欲为了。
18 |
19 | 比如什么教务系统啦,发个xss注入的链接给你的老师,他一点击,跳到教务系统,神不知鬼不觉的把他的cookie发到你的服务器上。
20 |
21 | 你马上用老师的cookie登陆教务系统,改成绩,谦虚点,改个99分。
22 |
23 | 然后过几天被开除了。
24 |
25 | #### 2. 持久性攻击
26 | 这种攻击就不是在url上下手了,而是直接把注入代码写到网站数据库中。
27 |
28 | 有些网站呢,是内容生成网站,比如很多的博客站,有非常多的用户输入页。用户敲了一篇博客,存到网站数据库,然后网站读出内容,呈现给其他用户。
29 |
30 | 此时,如果不对用户输出的内容加以过滤,就可以注入一些js脚本内容。这样,别人看到这篇博客时,已经在执行他写的js脚本了。
31 |
32 | 之前新浪微博就爆发过这样的漏洞,一些大V先是中了招,然后自动向粉丝发送被攻击的页面地址,于是不断循环。
33 |
34 | ### 危害
35 | xss注入后,本来你的网站页面js能做的事情,它都可以做了。除了上面所述的,我再随便举几个例子。
36 |
37 | 1. 获取他人隐私信息。
38 | 2. 破坏、修改网站原本页面内容。
39 | 3. 跳转到其他恶意页面。
40 | 4. 如果页面影响大,可以对其他网站发起DDoS攻击。
41 |
42 |
43 | ### 如何防范
44 |
45 | 其实现在大多成熟的web框架,自带过滤XSS脚本。很多浏览器如Chrome,也自带了XSS过滤器。但也正因为如此,开发过程中就更容易忽略这个问题。
46 |
47 | #### 1. 过滤用户输入
48 | 千万不要相信用户的任何输入,不要认为用户都是无害的。他们会想尽办法的绕弯来攻击。所以,任何用户的任何输出,都是不可信的。对于网站上有用户输入的部分,如各种表单内容、富文本内容,都应该对js脚本进行过滤,直接去除或者替换修改。
49 |
50 | #### 2. 对不可信输出编码
51 | 虽然已经过滤了用户输入,但总有可能百密一疏。所以还是不能相信任何用户输入的内容。如果网站需要将这些内容输出到页面上,必须得对这些数据先进行转义、编码。
52 |
53 | #### 3. 安全Cookie
54 | 之所以XSS能干很多坏事,有一部分是因为获取到了其他用户的cookie。所以将cookie设置HttpOnly后,js就无法获取到该网站的cookie。自然也没办法将其他用户的隐私信息传到自己的服务器。
55 |
56 | #### 4. 提高防范意识、多测试
57 | 如XSS、CSRF这样的攻击,已经有了很多成熟的防范手段,相信大家随便搜搜都能找到。但重点还是得培养这个防范意识,对于任何有可能执行脚本的地方,都应该多提防。对于任何用户输入的地方,都应该多测试,现在也有很多XSS测试工具。
58 |
59 |
60 | ## CSRF
61 | CSRF全称跨站请求伪造(Cross-site request forgery)。一听好像跟XSS没什么差,确实XSS也是实现CSRF的一种手段。XSS点是在于跨站注入脚本,进而干坏事。CSRF点在于利用各种手段,实现伪造其他网站请求,不一定是通过XSS。
62 |
63 | ### 常见攻击途径
64 |
65 | #### 1. 通过GET请求
66 | 假如某博客网站发布留言的请求是 `get: http://www.blog.com/message?content=留言内容`。
67 |
68 | 现在我登录了此博客网站A,然后又访问了另外一个网站B,B网站直接跳转到:`http://www.blog.com/message?content=嘿嘿嘿`。那么你就在A网站自动的留言了一条“嘿嘿嘿”这样的内容。
69 |
70 | 所以说一切操作资源的请求,都不应该是GET请求。
71 |
72 | #### 2. 通过XSS
73 | 之前也阐述过,如果cookie设置的不安全,就可以通过xss获取他人的cookie,有了别人的cookie,服务端再也分不清我是敌是友了,便可以为所欲为。切记,通过xss获取到cookie,发起的CSRF攻击,理论上无法防御。但可以通过一些手段提高技术门槛。
74 |
75 | ### 防御手段
76 | #### 1.规范请求类型。
77 | 任何资源操作的请求,必须是POST、PUT、DELETE,总之不能是GET
78 | #### 2.检查Referer
79 | 即检查请求头中的来源网站,从而保证此次请求来源于信任的网站。
80 | #### 3.设置请求Token
81 | 当我访问页面时,服务端会在页面写入一个随机token值,并设置token生命周期。之后我的请求就必须带上此次token值,请求过的token就会失效,无法再用。更加安全性的页面,如登录页面,应该加验证码。
82 | #### 4.防住第一道防线-XSS
83 | 再次强调,如果cookie被别人拿走,任何防御都将在理论上失效。上述的防御手段仅仅是提高攻击门槛。有了你的cookie,我可以直接请求你的页面,获取你的token,获取你的验证码图片并解析出来,然后再发起请求。而服务器还以为这是你本人。
84 |
85 | ## 参考
86 | 1.[跨站请求伪造-维基百科](https://zh.wikipedia.org/wiki/%E8%B7%A8%E7%AB%99%E8%AF%B7%E6%B1%82%E4%BC%AA%E9%80%A0)
87 |
88 | 2.[跨站脚本-维基百科](https://zh.wikipedia.org/wiki/%E8%B7%A8%E7%B6%B2%E7%AB%99%E6%8C%87%E4%BB%A4%E7%A2%BC)
89 |
--------------------------------------------------------------------------------
/zhihu-spider.md:
--------------------------------------------------------------------------------
1 | # 一只node爬虫的升级打怪之路
2 |
3 | 我一直觉得,爬虫是许多web开发人员难以回避的点。我们也应该或多或少的去接触这方面,因为可以从爬虫中学习到web开发中应当掌握的一些基本知识。而且,它还很有趣。
4 |
5 | 我是一个知乎轻微重度用户,之前写了一只爬虫帮我爬取并分析它的数据,我感觉这个过程还是挺有意思,因为这是一个不断给自己创造问题又去解决问题的过程。其中遇到了一些点,今天总结一下跟大家分享分享。
6 |
7 | ## 它都爬了什么?
8 |
9 | 先简单介绍下我的爬虫。它能够定时抓取一个问题的关注量、浏览量、回答数,以便于我将这些数据绘成图表展现它的热点趋势。为了不让我错过一些热门事件,它还会定时去获取我关注话题下的热门问答,并推送到我的邮箱。
10 |
11 | 作为一个前端开发人员,我必须为这个爬虫系统做一个界面,能让我登陆知乎帐号,添加关注的题目、话题,看到可视化的数据。所以这只爬虫还有登陆知乎、搜索题目的功能。
12 |
13 | 然后来看下界面。
14 |
15 | 
16 |
17 | 
18 |
19 | 下面正儿八经讲它的开发历程。
20 |
21 | ## 技术选型
22 |
23 | Python得益于其简单快捷的语法、以及丰富的爬虫库,一直是爬虫开发人员的首选。可惜我不熟。当然最重要的是,作为一名前端开发人员,node能满足爬虫需求的话,自然更是首选。而且随着node的发展,也有许多好用的爬虫库,甚至有[puppeteer](https://github.com/GoogleChrome/puppeteer)这样直接能模拟Chrome访问网页的工具的推出,node在爬虫方面应该是妥妥能满足我所有的爬虫需求了。
24 |
25 | 于是我选择从零搭建一个基于koa2的服务端。为什么不直接选择egg,express,thinkjs这些更加全面的框架呢?因为我爱折腾嘛。而且这也是一个学习的过程。如果以前不了解node,又对搭建node服务端有兴趣,可以看我之前的一篇文章-[从零搭建Koa2 Server](https://segmentfault.com/a/1190000009494041)。
26 |
27 | 爬虫方面我选择了[request](https://github.com/request/request)+[cheerio](https://github.com/cheeriojs/cheerio)。虽然知乎有很多地方用到了react,但得益于它绝大部分页面还是服务端渲染,所以只要能请求网页与接口(request),解析页面(cherrio)即可满足我的爬虫需求。
28 |
29 | 其他不一一举例了,我列个技术栈
30 |
31 | ### 服务端
32 |
33 | 1. [koajs](https://github.com/koajs/koa) 做node server框架;
34 | 2. [request](https://github.com/request/request) + [cheerio](https://github.com/cheeriojs/cheerio) 做爬虫服务;
35 | 3. [mongodb](https://github.com/mongodb/mongo) 做数据存储;
36 | 4. [node-schedule](https://github.com/node-schedule/node-schedule) 做任务调度;
37 | 5. [nodemailer](https://github.com/nodemailer/nodemailer) 做邮件推送。
38 |
39 | ### 客户端
40 |
41 | 1. [vuejs](https://github.com/vuejs/vue) 前端框架;
42 | 2. [museui](https://github.com/museui/muse-ui) Material Design UI库;
43 | 3. [chart.js](https://github.com/chartjs/Chart.js) 图表库。
44 |
45 | 技术选型妥善后,我们就要关心业务了。首要任务就是真正的爬取到页面。
46 |
47 | ## 如何能爬取网站的数据?
48 |
49 | 知乎并没有对外开放接口能让用户获取数据,所以想获取数据,就得自己去爬取网页信息。我们知道即使是网页,它本质上也是个GET请求的接口,我们只要在服务端去请求对应网页的地址(客户端请求会跨域),再把html结构解析下,获取想要的数据即可。
50 |
51 | 那为什么我要搞一个登陆呢?因为非登陆帐号获取信息,知乎只会展现有限的数据,而且也无法得知自己知乎帐户关注的话题、问题等信息。而且若是想自己的系统也给其他朋友使用,也必须搞一个帐户系统。
52 |
53 | ### 模拟登陆
54 |
55 | 大家都会用Chrome等现代浏览器看请求信息,我们在知乎的登录页进行登陆,然后查看捕获接口信息就能知道,登陆无非就是向一个登陆api发送账户、密码等信息,如果成功。服务端会向客户端设置一个cookie,这个cookie即是登陆凭证。
56 |
57 | 所以我们的思路也是如此,通过爬虫服务端去请求接口,带上我们的帐号密码信息,成功后再将返回的cookie存到我们的系统数据库,以后再去爬取其他页面时,带上此cookie即可。
58 |
59 | 当然,等我们真正尝试时,会受到更多挫折,因为会遇到token、验证码等问题。不过,由于我们有客户端了,可以将验证码的识别交给真正的**人**,而不是服务端去解析图片字符,这降低了我们实现登陆的难度。
60 |
61 | 一波三折的是,即使你把正确验证码提交了,还是会提示验证码错误。如果我们自己做过验证码提交的系统就能够迅速的定位原因。如果没做过,我们再次查看登陆时涉及的请求与响应,我们也能猜到:
62 |
63 | 在客户端获取验证码时,知乎服务端还会往客户端设置一个新cookie,提交登陆请求时,必须把验证码与此cookie一同提交,来验证此次提交的验证码确实是当时给予用户的验证码。
64 |
65 | 语言描述有些绕,我以图的形式来表达一个登陆请求的完整流程。
66 |
67 | 
68 |
69 | > 注:我编写爬虫时,知乎还部分采取图片字符验证码,现已全部改为“点击倒立文字”的形式。这样会加大提交正确验证码的难度,但也并非无计可施。获取图片后,由**人工**识别并点击倒立文字,将点击的坐标提交到登陆接口即可。当然有兴趣有能力的同学也可以自己编写算法识别验证码。
70 |
71 | ### 爬取数据
72 |
73 | 上一步中,我们已经获取到了登陆后的凭证cookie。用户登陆成功后,我们把登陆的帐户信息与其凭证cookie存到mongo中。以后此用户发起的爬取需求,包括对其跟踪问题的数据爬取都根据此cookie爬取。
74 |
75 | 当然cookie是有时间期限的,所以当我们存cookie时,应该把过期时间也记录下来,当后面再获取此cookie时,多加一步过期校验,若过期了则返回过期提醒。
76 |
77 | 爬虫的基础搞定后,就可以真正去获取想要的数据了。我的需求是想知道某个知乎问题的热点趋势。先用浏览器去看看一个问题页面下都有哪些数据,可以被我爬取分析。举个例子,比如这个问题:[有哪些令人拍案叫绝的推理桥段](https://www.zhihu.com/question/37248069)。
78 |
79 | 打开链接后,页面上最直接展现出来的有**关注者**,**被浏览**,**1xxxx个回答**,还要默认展示的几个高赞回答及其点赞评论数量。右键查看网站源代码,确认这些数据是服务端渲染出来的,我们就可以通过request请求网页,再通过cherrio,使用css选择器定位到数据节点,获取并存储下来。代码示例如下:
80 | ```javascript
81 | async getData (cookie, qid) {
82 | const options = {
83 | url: `${zhihuRoot}/question/${qid}`,
84 | method: 'GET',
85 | headers: {
86 | 'Cookie': cookie,
87 | 'Accept-Encoding': 'deflate, sdch, br' // 不允许gzip,开启gzip会开启知乎客户端渲染,导致无法爬取
88 | }
89 | }
90 | const rs = await this.request(options)
91 | if (rs.error) {
92 | return this.failRequest(rs)
93 | }
94 | const $ = cheerio.load(rs)
95 | const NumberBoard = $('.NumberBoard-item .NumberBoard-value')
96 | const $title = $('.QuestionHeader-title')
97 | $title.find('button').remove()
98 | return {
99 | success: true,
100 | title: $title.text(),
101 | data: {
102 | qid: qid,
103 | followers: Number($(NumberBoard[0]).text()),
104 | readers: Number($(NumberBoard[1]).text()),
105 | answers: Number($('h4.List-headerText span').text().replace(' 个回答', ''))
106 | }
107 | }
108 | }
109 | ```
110 | 这样我们就爬取了一个问题的数据,只要我们能够按一定时间间隔不断去执行此方法获取数据,最终我们就能绘制出一个题目的数据曲线,分析起热点趋势。
111 |
112 | 那么问题来了,如何去做这个定时任务呢?
113 |
114 | ### 定时任务
115 |
116 | 我使用了[node-schedule](https://github.com/node-schedule/node-schedule)做任务调度。如果之前做过定时任务的同学,可能对其类似cron的语法比较熟悉,不熟悉也没关系,它提供了not-cron-like的,更加直观的设置去配置任务,看下文档就能大致了解。
117 |
118 | 当然这个定时任务不是简单的不断去执行上述的爬取方法`getData`。因为这个爬虫系统不仅是一个用户,一个用户不仅只跟踪了一个问题。
119 |
120 | 所以我们此处的完整任务应该是遍历系统的每个cookie未过期用户,再遍历每个用户的跟踪问题,再去获取这些问题的数据。
121 |
122 | 系统还有另外两个定时任务,一个是定时爬取用户关注话题的热门回答,另一个是推送这个话题热门回答给相应的用户。这两个任务跟上述任务大致流程一样,就不细讲了。
123 |
124 | 但是在我们做定时任务时会有个细节问题,就是如何去控制爬取时的并发问题。具体举例来说:如果爬虫请求并发太高,知乎可能是会限制此IP的访问的,所以我们需要让爬虫请求一个一个的,或者若干个若干个的进行。
125 |
126 | 简单思考下,我们会采取循环await。我不假思索的写下了如下代码:
127 | ```javascript
128 | // 爬虫方法
129 | async function getQuestionData () {
130 | // do spider action
131 | }
132 |
133 | // questions为获取到的关注问答
134 | questions.forEach(await getQuestionData)
135 | ```
136 | 然而执行之后,我们会发现这样其实还是并发执行的,为什么呢?其实仔细想下就明白了。forEach只是循环的语法糖,如果没有这个方法,让你来实现它,你会怎么写呢?你大概也写的出来:
137 | ```javascript
138 | Array.prototype.forEach = function (callback) {
139 | for (let i = 0; i < this.length; i++) {
140 | callback(this[i], i, this)
141 | }
142 | }
143 | ```
144 | 虽然`forEach`本身会更复杂点,但大致就是这样吧。这时候我们把一个异步方法作为参数`callback`传递进去,然后循环执行它,这个执行依旧是并发执行,并非是同步的。
145 |
146 | 所以我们如果想实现真正的同步请求,还是需要用for循环去执行,如下:
147 | ```javascript
148 | async function getQuestionData () {
149 | // do spider action
150 | }
151 | for (let i = 0; i < questions.length; i++) {
152 | await getQuestionData()
153 | }
154 | ```
155 | 除了for循环,还可以通过for-of,如果对这方面感兴趣,可以去多了解下数组遍历的几个方法,顺便研究下ES6的迭代器`Iterator`。
156 |
157 | 其实如果业务量大,即使这样做也是不够的。还需要更加细分任务颗粒度,甚至要加代理IP来分散请求。
158 |
159 | ## 合理搭建服务端
160 |
161 | > 下面说的点跟爬虫本身没有太大关系了,属于服务端架构的一些分享,如果只关心爬虫本身的话,可以不用再往下阅读了。
162 |
163 | 我们把爬虫功能都写的差不多了,后面只要编写相应的路由,能让前端访问到数据就好了。但是编写一个没那么差劲的服务端,还是需要我们深思熟虑的。
164 |
165 | ### 合理分层
166 |
167 | 我看过一些前端同学写的node服务,经常就会把系统所有的接口(router action)都写到一个文件中,好一点的会根据模块分几个对于文件。
168 |
169 | 但是如果我们接触过其他成熟的后端框架、或者大学学过一些J2EE等知识,就会本能意识的进行一些分层:
170 | 1. `model` 数据层。负责数据持久化,通俗说就是连接数据库,对应数据库表的实体数据模型;
171 | 2. `service` 业务逻辑层。顾名思义,就是负责实现各种业务逻辑。
172 | 3. `controller` 控制器。调取业务逻辑服务,实现数据传递,返回客户端视图或数据。
173 |
174 | 当然也有些框架或者人会将业务逻辑`service`实现在`controller`中,亦或者是`model`层中。我个人认为一个稍微复杂的项目,应该是单独抽离出抽象的业务逻辑的。
175 |
176 | 比如在我这个爬虫系统中,我将数据库的添删改查操作按`model`层对应抽离出`service`,另外再将爬取页面的服务、邮件推送的服务、用户鉴权的服务抽离到对应的`service`。
177 |
178 | 最终我们的`api`能够设计的更加易读,整个系统也更加易拓展。
179 |
180 | ### 分层在koa上的实践
181 |
182 | 如果是直接使用一个成熟的后端框架,分层这事我们是不用多想的。node这样的框架也有,我之前介绍的我厂开源的[api-mocker](https://github.com/DXY-F2E/api-mocker)采用的[egg.js](https://eggjs.org/),也帮我们做好了合理的分层。
183 |
184 | 但是如果自己基于koa从零搭建一个服务端,在这方面上就会遇到一些挫折。koa本身逻辑非常简单,就是调取一系列中间件(就是一个个function),来处理请求。官方自己提供的[koa-router](https://github.com/alexmingoia/koa-router),即是帮助我们识别请求路径,然后加载对应的接口方法。
185 |
186 | 我们为了区分业务模块,会把一些接口方法写在同一个`controller`中,比如我的[questionController](https://github.com/wuomzfx/zhihu-spider/blob/master/server/app/controller/question.js)负责处理问题相关的接口;[topicController](https://github.com/wuomzfx/zhihu-spider/blob/master/server/app/controller/topic.js)负责处理话题相关的接口。
187 |
188 | 那么我们可能会这样编写路由文件:
189 | ```javascript
190 | const Router = require('koa-router')
191 | const router = new Router()
192 |
193 | const question = require('./controller/question')
194 | const topic = require('./controller/topic')
195 |
196 | router.post('/api/question', question.create)
197 | router.get('/api/question', question.get)
198 |
199 | router.get('/api/topic', topic.get)
200 | router.post('/api/topic/follow', topic.follow)
201 |
202 | module.exports = router
203 | ```
204 | 我的question文件可能是这样写的:
205 | ```javascript
206 | class Question {
207 | async get () {
208 | // return data
209 | }
210 | async create () {
211 | // create question and return data
212 | }
213 | }
214 |
215 | module.exports = new Question()
216 | ```
217 |
218 | #### 那么问题就来了
219 |
220 | 单纯这样写是没有办法真正的以面向对象的形式来编写`controller`的。为什么呢?
221 |
222 | 因为我们将question对象的属性方法作为中间件传递到了`koa-router`中,然后由`koa`底层来合并这些中间件方法,作为参数传递到`http.createServer`方法中,最终由node底层监听请求时调用。那这个`this`到底会是谁,不进行调试,或者查看koa与node源代码,是无从得知的。但是无论如何方法调用者肯定不是这个对象自身了(实际上它会是`undefined`)。
223 |
224 | 也就是说,我们不能通过`this`来获取对象自身的属性或方法。
225 |
226 | 那怎么办呢?有的同学可能会选择将自身一些公共方法,直接写在`class`外部,或者写在某个`utils`文件中,然后在接口方法中使用。比如这样:
227 |
228 | ```javascript
229 |
230 | const error = require('utils/error')
231 |
232 | const success = (ctx, data) => {
233 | ctx.body = {
234 | success: true,
235 | data: data
236 | }
237 | }
238 |
239 | class Question {
240 | async get () {
241 | success(data)
242 | }
243 | async create () {
244 | error(result)
245 | }
246 | }
247 |
248 | module.exports = new Question()
249 | ```
250 |
251 | 这样确实ok,但是又会有新的问题---这些方法就不是对象自己的属性,也就没办法被子类继承了。
252 |
253 | 为什么需要继承呢?因为有时候我们希望一些不同的`controller`有着公共的方法或属性,举个例子:我希望我所有的成功or失败都是这样的格式:
254 |
255 | ```javascript
256 | {
257 | success: false,
258 | message: '对应的错误消息'
259 | }
260 | {
261 | success: true,
262 | data: '对应的数据'
263 | }
264 | ```
265 |
266 | 按照`koa`的核心思想,这个通用的格式转化,应该是专门编写一个中间件,在路由中间件之后(即执行完controller里的方法之后)去做专门处理并response。
267 |
268 | 然而这样会导致每有一个公共方法,就必须要加一个中间件。而且`controller`本身已经失去了对这些方法的控制权。这个中间件是执行自身还是直接`next()`将会非常难判断。
269 |
270 | 如果是抽离成`utils`方法再引用,也不是不可以,就是方法多的话,声明引用稍微麻烦些,而且没有抽象类的意义。
271 |
272 | 更理想的状态应该是如刚才所说的,大家都继承一个抽象的父类,然后去调用父类的公共相应方法即可,如:
273 | ```javascript
274 | class AbstractController {
275 | success (ctx, data) {
276 | ctx.body = {
277 | success: true,
278 | data: data
279 | }
280 | }
281 | error (ctx, error) {
282 | ctx.body = {
283 | success: false,
284 | msg: error
285 | }
286 | }
287 | }
288 | class Question extends AbstractController {
289 | async get (ctx) {
290 | const data = await getData(ctx.params.id)
291 | return super.success(ctx, data)
292 | }
293 | }
294 | ```
295 | 这样就方便多了,不过如果写过koa的人可能会有这样的烦恼,一个上下文`ctx`总是要作为参数传递来传递去。比如上述控制器的所有中间件方法都得传`ctx`参数,调用父类方法时,又要传它,还会使得方法损失一些可读性。
296 |
297 | 所以总结一下,我们有如下问题:
298 |
299 | 1. `controller`中的方法无法调用自身的其他方法、属性;
300 | 2. 调用父类方法时,需要传递上下文参数`ctx`。
301 |
302 | #### 解决它
303 |
304 | 其实解决的办法很简单,我们只要想办法让`controller`方法中的this指向实例化对象自身,再把`ctx`挂在到这个`this`上即可。
305 |
306 | 怎么做呢?我们只要再封装一下`koa-router`就好了,如下所示:
307 |
308 | ```javascript
309 | const Router = require('koa-router')
310 | const router = new Router()
311 | const question = require('./controller/question')
312 | const topic = require('./controller/topic')
313 |
314 | const routerMap = [
315 | ['post', '/api/question', question, 'create'],
316 | ['get', '/api/question', question, 'get'],
317 | ['get', '/api/topic', topic, 'get'],
318 | ['post', '/api/topic/follow', topic, 'follow']
319 | ]
320 |
321 | routerMap.map(route => {
322 | const [ method, path, controller, action ] = route
323 |
324 | router[method](path, async (ctx, next) =>
325 | controller[action].bind(Object.assign(controller, { ctx }))(ctx, next)
326 | )
327 | })
328 |
329 | module.exports = router
330 | ```
331 | 大意就是在路由传递`controller`方法时,将`controller`自身与`ctx`合并,通过`bind`指定该方法的`this`。这样我们就能通过`this`获取方法所属`controller`对象的其他方法。此外子类方法与父类方法也能通过`this.ctx`来获取上下文对象`ctx`。
332 |
333 | 但是`bind`之前我们其实应该考虑以下,其他中间件以及koa本身会不会也干了类似的事,修改了this的值。如何判断呢,两个办法:
334 |
335 | 1. 调试。在我们未`bind`之前,在中间件方法中打印一下`this`,是`undefined`的话自然就没被绑定。
336 | 2. 看koa-router/koa/node的源代码。
337 |
338 | 事实是,自然是没有的。那我们就放心的`bind`吧。
339 |
340 | ## 写在最后
341 |
342 | 上述大概就是编写这个小工具时,遇到的一些点,感觉可以总结的。也并没有什么技术难点,不过可以借此学习学习一些相关的知识,包括网站安全、爬与反爬、、koa底层原理等等。
343 |
344 | 这个工具本身非常的个人色彩,不一定满足大家的需要。而且它在半年前就写好了,只不过最近被我挖坟拿出来总结。而且就在我即将写完文章时,我发现知乎提示我的账号不安全了。我估计是以为同一IP同一账户发起过多的网络请求,我这台服务器IP已经被认为是不安全的IP了,在这上面登录的账户都会被提示不安全。所以我不建议大家将其直接拿来使用。
345 |
346 | 当然,如果还是对其感兴趣,本地测试下或者学习使用,还是没什么大问题的。或者还有更深的兴趣的话,可以自己尝试去绕开知乎的安全策略。
347 |
348 | 最后的最后附上 [项目GitHub地址](https://github.com/wuomzfx/zhihu-spider)
349 |
350 | --[阅读原文](https://github.com/wuomzfx/blog/blob/master/zhihu-spider.md)
351 |
352 | --转载请先经过本人授权。
353 |
--------------------------------------------------------------------------------
/答题救不了前端新人.md:
--------------------------------------------------------------------------------
1 | # 2017前端现状----答题救不了前端新人
2 |
3 | 众所周知,前端近几年闹了一场革命。**前端**在编程领域也掀起了学习潮。至少在中国,从2013年下半年至今,在搜索指数上,有着爆炸式的增长。可以看下数据:[百度指数](http://index.baidu.com/?tpl=trend&word=%C7%B0%B6%CB),[Google指数](https://trends.google.com.hk/trends/explore?date=all&geo=CN&q=%E5%89%8D%E7%AB%AF)。
4 |
5 | 一边是日新月异的新知识不断的冲击着前端,一边是各种新人疯狂涌入前端领域。这两股新流冲击下,倒是火了很多技术社区、问答网站,如:[SegmentFault](https://segmentfault.com/)。
6 |
7 | 打开`SegmentFault`,首页问答流上,前端相关的题目(包括Node),不说占十之八九,那也是半壁江山。即使是在老牌问答网站[StackOverflow](https://stackoverflow.com/tags)上,`javascript`相关的问答也是最多的。
8 |
9 | 我是很喜欢回答各种问题的人,一直逛着SegmentFault。几个月下来,我看到的出现最频繁的前端问题如下:
10 |
11 | 1. Vue、React哪里哪里报错了,求看(React相关的少很多,angular更少)。
12 | 2. 框架相关的一些UI、插件,如ElementUI什么什么组件怎么用、vue-router哪里哪里有问题。
13 | 3. javascript this指向的问题、闭包的问题、数组循环相关问题等。
14 | 4. webpack、npm等工程化相关配置问题。
15 |
16 | ### 那这些问题的本身有什么问题呢?
17 |
18 | 90%的问题,都可以通过文档查询到。而且大多不是文档偏僻、篇幅少的角落,更多的是诸如`vue的父子组件通信`这类文档长篇幅说明的问题。
19 |
20 | 而如this指向、闭包这类问题,以及其他语法上的问题,都是基础知识,而且大多都被网上各种博客讲烂了。想要理解它,闭着眼睛都能搜到。
21 |
22 | 还有很多问题都是重复式的问题,只要自己网上搜搜,就能找到答案。
23 |
24 | ### 这些问题反应了什么现象?
25 |
26 | 1. 确实前端很火,引来了一批批新人。
27 | 2. 前端小白们如火如荼的学习着现在前端流行的新技术栈。
28 | 3. 但大多忽视了基础知识的学习。
29 | 4. 而且不知道提问题的正确姿势。
30 | 5. 甚至有些过分的伸手党。
31 |
32 | ### 为什么会这样?
33 |
34 | 我猜测是如下原因:
35 |
36 | 1. 很多人学前端并非是对前端感兴趣,而是觉得前端门槛低,市场火爆,于是学了前端。没了兴趣就少了耐心,少了耐心就不太想看文档、书籍。
37 | 2. 还有些新人并非科班出生,导致完全不明白编程学习的流程,以为就是靠问。
38 | 3. 培训班也有锅。标榜着几周就能精通前端、搞定大公司。在教学上轻基础、重工具,导致前端学习者急功近利,以为熟练操作vue了就是精通前端了。
39 | 4. 前端新人本身对新知识非常渴望,因为培训班或者自学一段时间后,发现没掌握的知识太多太多,在市场并没有竞争力。所以也会显得比较着急。
40 |
41 | 如果同学你正好符合我上述所说的,要注意了,你已经误入歧途了。前端门槛是低,但学习曲线不短。想找到一份好工作,也不是会用vue就够的。
42 |
43 | 我曾经听一个学弟说,自己的目标就是把Vue掌握了写溜了,我表示非常不赞同。
44 |
45 | 可能很多新人也是这个想法,但是我知道,大多数人所说的掌握,写溜,并非是真正的掌握。其实就是把文档背熟了,熟悉了语法糖,了解了生命周期过程。学习vue是非常好的,但真正的学习它不是就学习它怎么用,而是要深入学习它的设计理念、实现方式、阅读理解源码。
46 |
47 | 可惜的是,你可能根本看不懂它的源码。为什么?因为基础就没掌握。所以你的目标走偏了,目标应该是要先掌握好Javascript本身。在此之上,你才有可能说自己掌握了Vue。
48 |
49 | ### 那什么是正确的学习姿势?
50 |
51 | 首先打基础,html,css,js->es6,这些已经够吃一壶。怎么学?读书、看它们的参考文档,掌握基础用法。
52 |
53 | 在此同时,可以用vue这些框架去做一些工程化的项目实践,遇到不懂的地方,不必过分深究,会浪费很多时间。等你基础掌握到一定阶段,蓦然回首时,会顿时大悟。
54 |
55 | 基础掌握后,工具也能熟练运用了,甚至能快速的掌握它。之后再怎么深入学习,这里就不探讨了。
56 |
57 | 另外,当我们学习一个工具的时候,我们最基础的应该要知道人家到底是个什么?比如学习Vue,首先会介绍它是MVVM框架,你要是连MVVM都不知道,又怎么去学Vue呢?
58 |
59 | 我偶尔会看到一些问题:问在vue中如何去获取dom页面上的数据。原来是用着vue,然后以Jquery操作dom的方式去开发。这就是因为自己都不知道自己用的是什么。
60 |
61 | 我知道,前端知识实在是太多了,比如说node。又够大家吃一壶,难免会遇到问题,解决不了。但又确实需要解决它才能进行项目实践。不可避免的得提问题。
62 |
63 | ### 那什么是正确的提问题姿势?
64 |
65 | 1. 遇到出错,首先应该想到的是查阅文档。很多时候我们遇到的问题,是因为自己用的姿势不对,到底怎么用,文档上可能已经写的一清二楚。比如我要在vue2.x的子组件中去修改父组件的传值,我一修改就报错,该怎么办?怎么办?查下文档就一清二楚。
66 | 2. 文档查不到该怎么办?确实有时候,一些问题是文档中没暴露的,或者说没描述清楚的。这时候我们应该先在网上搜索该问题。哪儿搜?Google,项目本身的Issue,百度虽然大家都不喜欢,但真的去搜百度也无可厚非。
67 | 3. 别人也没遇到怎么办?如果是工具本身的问题,首先想到的应该是看源码,源码实在看不懂或者理解不了,再去提问。
68 | 4. 其他一些业务上的实现问题,自己确实没能力实现,可以去提问。
69 | 5. 但提问题不仅仅是为了解决当下问题,更重要的是提高自己解决问题的能力。
70 |
71 | 第五点,我再单独说一下。有时候我会看到一些问题的最佳答案,并非是真正的最佳答案。很多题主,就想着能直接帮他解决当下问题就好。而有的答案虽然没有直接给出代码,却给了非常好的思路或解释,亦或者给了其他更好的设计方案,亦或者是给了某些相关的文档链接。
72 |
73 | 本来是授人以鱼不如授人以渔,但有些人却只要现成的鱼。
74 |
75 | ### 写在最后
76 |
77 | 所以啊,在目睹了很多白痴问题后,我终于发出了一声呐喊,**答题救不了前端新人**。希望这篇文章能稍微的让一些真正爱好前端的同学,更好的去学习前端。拿来主义,不是把别人的代码拿来,而是要把别人的知识拿来。
78 |
79 |
--------------------------------------------------------------------------------