灯盏细辛

灯盏细辛 查看完整档案

北京编辑  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

灯盏细辛 收藏了文章 · 2019-07-30

为什么我们要熟悉这些通信协议? 【精读】

打个广告,欢迎加入我们的前端开发交流群:

  • 微信群:

clipboard.png

  • QQ群

clipboard.png

前端的最重要的基础知识点是什么?

  • 原生javaScriptHTML,CSS.
  • Dom操作
  • EventLoop和渲染机制
  • 各类工程化的工具原理以及使用,根据需求定制编写插件和包。(webpack的plugin和babel的预设包)
  • 数据结构和算法(特别是IM以及超大型高并发网站应用等,例如B站
  • 最后便是通信协议
在使用某个技术的时候,一定要去追寻原理和底层的实现,长此以往坚持,只要自身底层的基础扎实,无论技术怎么变化,学习起来都不会太累,总的来说就是拒绝5分钟技术

从输入一个url地址,到显示页面发生了什么出发:

  • 1.浏览器向 DNS 服务器请求解析该 URL 中的域名所对应的 IP 地址;
  • 2.建立TCP连接(三次握手);
  • 3.浏览器发出读取文件(URL 中域名后面部分对应的文件)的HTTP 请求,该请求报文作为 TCP 三次握手的第三个报文的数据发送给服务器;
  • 4.服务器对浏览器请求作出响应,并把对应的 html 文本发送给浏览器;
  • 5.浏览器将该 html 文本并显示内容;
  • 6.释放 TCP连接(四次挥手);

目前常见的通信协议都是建立在TCP链接之上

那么什么是TCP

TCP是因特网中的传输层协议,使用三次握手协议建立连接。当主动方发出SYN连接请求后,等待对方回答

TCP三次握手的过程如下:
  • 客户端发送SYN报文给服务器端,进入SYN_SEND状态。
  • 服务器端收到SYN报文,回应一个SYN(SEQ=y)ACK(ACK=x+1)报文,进入SYN_RECV状态。
  • 客户端收到服务器端的SYN报文,回应一个ACK(ACK=y+1)报文,进入Established状态。
  • 三次握手完成,TCP客户端和服务器端成功地建立连接,可以开始传输数据了。
如图所示:

clipboard.png

TCP的四次挥手:
  • 建立一个连接需要三次握手,而终止一个连接要经过四次握手,这是由TCP的半关闭(half-close)造成的。具体过程如下图所示。
  • 某个应用进程首先调用close,称该端执行“主动关闭”(active close)。该端的TCP于是发送一个FIN分节,表示数据发送完毕。
  • 接收到这个FIN的对端执行 “被动关闭”(passive close),这个FIN由TCP确认。

注意:FIN的接收也作为一个文件结束符(end-of-file)传递给接收端应用进程,放在已排队等候该应用进程接收的任何其他数据之后,因为,FIN的接收意味着接收端应用进程在相应连接上再无额外数据可接收。

  • 一段时间后,接收到这个文件结束符的应用进程将调用close关闭它的套接字。这导致它的TCP也发送一个FIN。
  • 接收这个最终FIN的原发送端TCP(即执行主动关闭的那一端)确认这个FIN。 [3]

既然每个方向都需要一个FIN和一个ACK,因此通常需要4个分节。

特别提示: SYN报文用来通知,FIN报文是用来同步的

clipboard.png

以上就是面试官常问的三次握手,四次挥手,但是这不仅仅面试题,上面仅仅答到了一点皮毛,学习这些是为了让我们后续方便了解他的优缺点。

TCP连接建立后,我们可以有多种协议的方式通信交换数据:

最古老的方式一:http 1.0

  • 早先1.0的HTTP版本,是一种无状态、无连接的应用层协议。
  • HTTP1.0规定浏览器和服务器保持短暂的连接,浏览器的每次请求都需要与服务器建立一个TCP连接,服务器处理完成后立即断开TCP连接(无连接),服务器不跟踪每个客户端也不记录过去的请求(无状态)。
  • 这种无状态性可以借助cookie/session机制来做身份认证和状态记录。而下面两个问题就比较麻烦了。
  • 首先,无连接的特性导致最大的性能缺陷就是无法复用连接。每次发送请求的时候,都需要进行一次TCP的连接,而TCP的连接释放过程又是比较费事的。这种无连接的特性会使得网络的利用率非常低。
  • 其次就是队头阻塞(headoflineblocking)。由于HTTP1.0规定下一个请求必须在前一个请求响应到达之前才能发送。假设前一个请求响应一直不到达,那么下一个请求就不发送,同样的后面的请求也给阻塞了。
Http 1.0的致命缺点,就是无法复用TCP连接和并行发送请求,这样每次一个请求都需要三次握手,而且其实建立连接和释放连接的这个过程是最耗时的,传输数据相反却不那么耗时。还有本地时间被修改导致响应头expires的缓存机制失效的问题~(后面会详细讲)
  • 常见的请求报文~

clipboard.png

于是出现了Http 1.1,这也是技术的发展必然结果~

  • Http 1.1出现,继承了Http1.0的优点,也克服了它的缺点,出现了keep-alive这个头部字段,它表示会在建立TCP连接后,完成首次的请求,并不会立刻断开TCP连接,而是保持这个连接状态~进而可以复用这个通道
  • Http 1.1并且支持请求管道化,“并行”发送请求,但是这个并行,也不是真正意义上的并行,而是可以让我们把先进先出队列从客户端(请求队列)迁移到服务端(响应队列)
例如:客户端同时发了两个请求分别来获取html和css,假如说服务器的css资源先准备就绪,服务器也会先发送html再发送css。
  • B站首页,就有keep-alive,因为他们也有IM的成分在里面。需要大量复用TCP连接~

clipboard.png

  • HTTP1.1好像还是无法解决队头阻塞的问题
实际上,现阶段的浏览器厂商采取了另外一种做法,它允许我们打开多个TCP的会话。也就是说,上图我们看到的并行,其实是不同的TCP连接上的HTTP请求和响应。这也就是我们所熟悉的浏览器对同域下并行加载6~8个资源的限制。而这,才是真正的并行!

Http 1.1的致命缺点:

  • 1.明文传输
  • 2.其实还是没有解决无状态连接的
  • 3.当有多个请求同时被挂起的时候 就会拥塞请求通道,导致后面请求无法发送
  • 4.臃肿的消息首部:HTTP/1.1能压缩请求内容,但是消息首部不能压缩;在现今请求中,消息首部占请求绝大部分(甚至是全部)也较为常见.
我们也可以用dns-prefetch和 preconnect tcp来优化~
<link rel="preconnect" href="//example.com" crossorigin>
<link rel="dns=prefetch" href="//example.com">
  • Tip: webpack可以做任何事情,这些都可以用插件实现

基于这些缺点,出现了Http 2.0

相较于HTTP1.1,HTTP2.0的主要优点有采用二进制帧封装,传输变成多路复用,流量控制算法优化,服务器端推送,首部压缩,优先级等特点。

HTTP1.x的解析是基于文本的,基于文本协议的格式解析存在天然缺陷,文本的表现形式有多样性,要做到健壮性考虑的场景必然很多。而HTTP/2会将所有传输的信息分割为更小的消息和帧,然后采用二进制的格式进行编码,HTTP1.x的头部信息会被封装到HEADER frame,而相应的RequestBody则封装到DATAframe里面。不改动HTTP的语义,使用二进制编码,实现方便且健壮。

多路复用

  • 所有的请求都是通过一个 TCP 连接并发完成。HTTP/1.x 虽然通过 pipeline 也能并发请求,但是多个请求之间的响应会被阻塞的,所以 pipeline 至今也没有被普及应用,而 HTTP/2 做到了真正的并发请求。同时,流还支持优先级和流量控制。当流并发时,就会涉及到流的优先级和依赖。即:HTTP2.0对于同一域名下所有请求都是基于流的,不管对于同一域名访问多少文件,也只建立一路连接。优先级高的流会被优先发送。图片请求的优先级要低于 CSS 和 SCRIPT,这个设计可以确保重要的东西可以被优先加载完

流量控制

  • TCP协议通过sliding window的算法来做流量控制。发送方有个sending window,接收方有receive window。http2.0的flow control是类似receive window的做法,数据的接收方通过告知对方自己的flow window大小表明自己还能接收多少数据。只有Data类型的frame才有flow control的功能。对于flow control,如果接收方在flow window为零的情况下依然更多的frame,则会返回block类型的frame,这张场景一般表明http2.0的部署出了问题。

服务器端推送

  • 服务器端的推送,就是服务器可以对一个客户端请求发送多个响应。除了对最初请求的响应外,服务器还可以额外向客户端推送资源,而无需客户端明确地请求。当浏览器请求一个html,服务器其实大概知道你是接下来要请求资源了,而不需要等待浏览器得到html后解析页面再发送资源请求。

首部压缩

  • HTTP 2.0 在客户端和服务器端使用“首部表”来跟踪和存储之前发送的键-值对,对于相同的数据,不再通过每次请求和响应发送;通信期间几乎不会改变的通用键-值对(用户代理、可接受的媒体类型,等等)只 需发送一次。事实上,如果请求中不包含首部(例如对同一资源的轮询请求),那么 首部开销就是零字节。此时所有首部都自动使用之前请求发送的首部。
  • 如果首部发生变化了,那么只需要发送变化了数据在Headers帧里面,新增或修改的首部帧会被追加到“首部表”。首部表在 HTTP 2.0 的连接存续期内始终存在,由客户端和服务器共同渐进地更新 。
  • 本质上,当然是为了减少请求啦,通过多个js或css合并成一个文件,多张小图片拼合成Sprite图,可以让多个HTTP请求减少为一个,减少额外的协议开销,而提升性能。当然,一个HTTP的请求的body太大也是不合理的,有个度。文件的合并也会牺牲模块化和缓存粒度,可以把“稳定”的代码or 小图 合并为一个文件or一张Sprite,让其充分地缓存起来,从而区分开迭代快的文件。
Demo的性能对比:

clipboard.png

Http的那些致命缺陷,并没有完全解决,于是有了https,也是目前应用最广的协议之一

HTTP+ 加密 + 认证 + 完整性保护 =HTTPS ?

可以这样认为~HTTP 加上加密处理和认证以及完整性保护后即是 HTTPS

  • 如果在 HTTP 协议通信过程中使用未经加密的明文,比如在 Web 页面中输入信用卡号,如果这条通信线路遭到窃听,那么信用卡号就暴露了。
  • 另外,对于 HTTP 来说,服务器也好,客户端也好,都是没有办法确认通信方的。

因为很有可能并不是和原本预想的通信方在实际通信。并且还需要考虑到接收到的报文在通信途中已经遭到篡改这一可能性。

  • 为了统一解决上述这些问题,需要在 HTTP 上再加入加密处理和认证等机制。我们把添加了加密及认证机制的 HTTP 称为 HTTPS
不加密的重要内容被wireshark这类工具抓到包,后果很严重~

HTTPS 是身披 SSL 外壳的 HTTP

  • HTTPS 并非是应用层的一种新协议。只是 HTTP 通信接口部分用 SSL(SecureSocket Layer)和 TLS(Transport Layer Security)协议代替而已。

通常,HTTP 直接和 TCP 通信。

  • 当使用 SSL 时,则演变成先和 SSL 通信,再由 SSL和 TCP 通信了。简言之,所谓 HTTPS,其实就是身披 SSL 协议这层外壳的HTTP。
  • 在采用 SSL 后,HTTP 就拥有了 HTTPS 的加密、证书和完整性保护这些功能。SSL 是独立于 HTTP 的协议,所以不光是 HTTP 协议,其他运行在应用层的 SMTP和 Telnet 等协议均可配合 SSL 协议使用。可以说 SSL 是当今世界上应用最为广泛的网络安全术。

clipboard.png

相互交换密钥的公开密钥加密技术 -----对称加密

clipboard.png

  • 在对 SSL 进行讲解之前,我们先来了解一下加密方法。SSL 采用一种叫做公开密钥加密(Public-key cryptography)的加密处理方式。
  • 近代的加密方法中加密算法是公开的,而密钥却是保密的。通过这种方式得以保持加密方法的安全性。
加密和解密都会用到密钥。没有密钥就无法对密码解密,反过来说,任何人只要持有密钥就能解密了。如果密钥被攻击者获得,那加密也就失去了意义。

HTTPS 采用混合加密机制

  • HTTPS 采用共享密钥加密和公开密钥加密两者并用的混合加密机制。
  • 但是公开密钥加密与共享密钥加密相比,其处理速度要慢。所以应充分利用两者各自的优势,将多种方法组合起来用于通信。在交换密钥环节使用公开密钥加密方式,之后的建立通信交换报文阶段则使用共享密钥加密方式。

HTTPS虽好,非对称加密虽好,但是不要滥用

HTTPS 也存在一些问题,那就是当使用 SSL 时,它的处理速度会变慢。

SSL 的慢分两种。一种是指通信慢。另一种是指由于大量消耗 CPU 及内存等资源,导致处理速度变慢。

  • 和使用 HTTP 相比,网络负载可能会变慢 2 到 100 倍。除去和 TCP 连接、发送 HTTP 请求 ? 响应以外,还必须进行 SSL 通信,因此整体上处理通信量不可避免会增加。
  • 另一点是 SSL 必须进行加密处理。在服务器和客户端都需要进行加密和解密的运算处理。因此从结果上讲,比起 HTTP 会更多地消耗服务器和客户端的硬件资源,导致负载增强。

针对速度变慢这一问题,并没有根本性的解决方案,我们会使用 SSL 加速器这种(专用服务器)硬件来改善该问题。该硬件为 SSL 通信专用硬件,相对软件来讲,能够提高数倍 SSL 的计算速度。仅在 SSL 处理时发挥 SSL加速器的功效,以分担负载。

为什么不一直使用 HTTPS

  • 既然 HTTPS 那么安全可靠,那为何所有的 Web 网站不一直使用 HTTPS?

其中一个原因是,因为与纯文本通信相比,加密通信会消耗更多的 CPU 及内存资源。如果每次通信都加密,会消耗相当多的资源,平摊到一台计算机上时,能够处理的请求数量必定也会随之减少。

  • 因此,如果是非敏感信息则使用 HTTP 通信,只有在包含个人信息等敏感数据时,才利用 HTTPS 加密通信。

特别是每当那些访问量较多的 Web 网站在进行加密处理时,它们所承担着的负载不容小觑。在进行加密处理时,并非对所有内容都进行加密处理,而是仅在那些需要信息隐藏时才会加密,以节约资源。

  • 除此之外,想要节约购买证书的开销也是原因之一。

要进行 HTTPS 通信,证书是必不可少的。而使用的证书必须向认证机构(CA)购买。证书价格可能会根据不同的认证机构略有不同。通常,一年的授权需要数万日元(现在一万日元大约折合 600 人民币)。那些购买证书并不合算的服务以及一些个人网站,可能只会选择采用HTTP 的通信方式。

clipboard.png

复习完了基本的协议,介绍下报文格式:

  • 请求报文格式

clipboard.png

  • 响应报文格式

clipboard.png

所谓响应头,请求头,其实都可以自己添加字段,只要前后端给对应的处理机制即可

Node.js代码实现响应头的设置


  if (config.cache.expires) {
                        res.setHeader("expries", new Date(Date.now() + (config.cache.maxAge * 1000)))
                    }
                    if (config.cache.lastModified) {
                        res.setHeader("last-modified", stat.mtime.toUTCString())
                    }
                    if (config.cache.etag) {
                        res.setHeader('Etag', etagFn(stat))
                    }
}

响应头的详解:

clipboard.png

本人的开源项目,手写的Node.js静态资源服务器,https://github.com/JinJieTan/...,欢迎 star~

浏览器的缓存策略:

  • 首次请求:

clipboard.png

  • 非首次请求:

clipboard.png

  • 用户行为与缓存:

clipboard.png

不能缓存的请求:

无法被浏览器缓存的请求如下:

  • HTTP信息头中包含Cache-Control:no-cache,pragma:no-cache(HTTP1.0),或Cache-Control:max-age=0等告诉浏览器不用缓存的请求
  • 需要根据Cookie,认证信息等决定输入内容的动态请求是不能被缓存的
  • 经过HTTPS安全加密的请求(有人也经过测试发现,ie其实在头部加入Cache-Control:max-age信息,firefox在头部加入Cache-Control:Public之后,能够对HTTPS的资源进行缓寸)
  • 经过HTTPS安全加密的请求(有人也经过测试发现,ie其实在头部加入Cache-Control:max-age信息,firefox在头部加入Cache-Control:Public之后,能够对HTTPS的资源进行缓存,参考《HTTPS的七个误解》)
  • POST请求无法被缓存
  • HTTP响应头中不包含Last-Modified/Etag,也不包含Cache-Control/Expires的请求无法被缓存

即时通讯协议

从最初的没有websocket协议开始:

传统的协议无法服务端主动push数据,于是有了这些骚操作:
  • 轮询,在一个定时器中不停向服务端发送请求。
  • 长轮询,发送请求给服务端,直到服务端觉得可以返回数据了再返回响应,否则这个请求一直挂起~
  • 以上两种都有瑕疵,而且比较明显,这里不再描述。

为了解决实时通讯,数据同步的问题,出现了webSocket.

  • webSockets的目标是在一个单独的持久连接上提供全双工、双向通信。在Javascript创建了Web Socket之后,会有一个HTTP请求发送到浏览器以发起连接。在取得服务器响应后,建立的连接会将HTTP升级从HTTP协议交换为WebSocket协议。
  • webSocket原理: 在TCP连接第一次握手的时候,升级为ws协议。后面的数据交互都复用这个TCP通道。
  • 客户端代码实现:
  const ws = new WebSocket('ws://localhost:8080');
        ws.onopen = function () {
            ws.send('123')
            console.log('open')
        }
        ws.onmessage = function () {
            console.log('onmessage')
        }
        ws.onerror = function () {
            console.log('onerror')
        }
        ws.onclose = function () {
            console.log('onclose')
        }
  • 服务端使用 Node.js语言实现
const express = require('express')
const { Server } = require("ws");
const app = express()
const wsServer = new Server({ port: 8080 })
wsServer.on('connection', (ws) => {
    ws.onopen = function () {
        console.log('open')
    }
    ws.onmessage = function (data) {
        console.log(data)
        ws.send('234')
        console.log('onmessage' + data)
    }
    ws.onerror = function () {
        console.log('onerror')
    }
    ws.onclose = function () {
        console.log('onclose')
    }
});

app.listen(8000, (err) => {
    if (!err) { console.log('监听OK') } else {
        console.log('监听失败')
    }
})

webSocket的报文格式有一些不一样:

![图片上传中...]

  • 客户端和服务端进行Websocket消息传递是这样的:

    • 客户端:将消息切割成多个帧,并发送给服务端。
    • 服务端:接收消息帧,并将关联的帧重新组装成完整的消息。

即时通讯的心跳检测:

pingandpong

  • 服务端Go实现:
package main

import (
    "net/http"
    "time"

    "github.com/gorilla/websocket"
)

var (
    //完成握手操作
    upgrade = websocket.Upgrader{
       //允许跨域(一般来讲,websocket都是独立部署的)
       CheckOrigin:func(r *http.Request) bool {
            return true
       },
    }
)

func wsHandler(w http.ResponseWriter, r *http.Request) {
   var (
         conn *websocket.Conn
         err error
         data []byte
   )
   //服务端对客户端的http请求(升级为websocket协议)进行应答,应答之后,协议升级为websocket,http建立连接时的tcp三次握手将保持。
   if conn, err = upgrade.Upgrade(w, r, nil); err != nil {
        return
   }

    //启动一个协程,每隔5s向客户端发送一次心跳消息
    go func() {
        var (
            err error
        )
        for {
            if err = conn.WriteMessage(websocket.TextMessage, []byte("heartbeat")); err != nil {
                return
            }
            time.Sleep(5 * time.Second)
        }
    }()

   //得到websocket的长链接之后,就可以对客户端传递的数据进行操作了
   for {
         //通过websocket长链接读到的数据可以是text文本数据,也可以是二进制Binary
        if _, data, err = conn.ReadMessage(); err != nil {
            goto ERR
     }
     if err = conn.WriteMessage(websocket.TextMessage, data); err != nil {
         goto ERR
     }
   }
ERR:
    //出错之后,关闭socket连接
    conn.Close()
}

func main() {
    http.HandleFunc("/ws", wsHandler)
    http.ListenAndServe("0.0.0.0:7777", nil)
}

客户端的心跳检测(Node.js实现):

this.heartTimer = setInterval(() => {
      if (this.heartbeatLoss < MAXLOSSTIMES) {
        events.emit('network', 'sendHeart');
        this.heartbeatLoss += 1;
        this.phoneLoss += 1;
      } else {
        events.emit('network', 'offline');
        this.stop();
      }
      if (this.phoneLoss > MAXLOSSTIMES) {
        this.PhoneLive = false;
        events.emit('network', 'phoneDisconnect');
      }
    }, 5000);

自定义即时通信协议:

new Socket开始:

  • 目前即时通讯大都使用现有大公司成熟的SDK接入,但是逼格高些还是自己重写比较好。
  • 打个小广告,我们公司就是自己定义的即时通讯协议~招聘一位高级前端,地点深圳-深南大道,做跨平台IM桌面应用开发的~
  • 客户端代码实现(Node.js):

const {Socket} = require('net') 
const tcp = new Socket()
tcp.setKeepAlive(true);
tcp.setNoDelay(true);
//保持底层tcp链接不断,长连接
指定对应域名端口号链接
tcp.connect(80,166.166.0.0)
建立连接后
根据后端传送的数据类型 使用对应不同的解析
readUInt8 readUInt16LE readUInt32LE readIntLE等处理后得到myBuf 
const myBuf = buffer.slice(start);//从对应的指针开始的位置截取buffer
const header = myBuf.slice(headstart,headend)//截取对应的头部buffer
const body = JSON.parse(myBuf.slice(headend-headstart,bodylength).tostring())
//精确截取数据体的buffer,并且转化成js对象
即时通讯强烈推荐使用Golang,GRPC,Prob传输数据。

上面的一些代码,都在我的开源项目中:

觉得写得不错,可以点个赞支持下,文章也借鉴了一下其他大佬的文章,但是地址都贴上来了~ 欢迎gitHub点个star哦~
查看原文

灯盏细辛 赞了文章 · 2019-07-30

为什么我们要熟悉这些通信协议? 【精读】

打个广告,欢迎加入我们的前端开发交流群:

  • 微信群:

clipboard.png

  • QQ群

clipboard.png

前端的最重要的基础知识点是什么?

  • 原生javaScriptHTML,CSS.
  • Dom操作
  • EventLoop和渲染机制
  • 各类工程化的工具原理以及使用,根据需求定制编写插件和包。(webpack的plugin和babel的预设包)
  • 数据结构和算法(特别是IM以及超大型高并发网站应用等,例如B站
  • 最后便是通信协议
在使用某个技术的时候,一定要去追寻原理和底层的实现,长此以往坚持,只要自身底层的基础扎实,无论技术怎么变化,学习起来都不会太累,总的来说就是拒绝5分钟技术

从输入一个url地址,到显示页面发生了什么出发:

  • 1.浏览器向 DNS 服务器请求解析该 URL 中的域名所对应的 IP 地址;
  • 2.建立TCP连接(三次握手);
  • 3.浏览器发出读取文件(URL 中域名后面部分对应的文件)的HTTP 请求,该请求报文作为 TCP 三次握手的第三个报文的数据发送给服务器;
  • 4.服务器对浏览器请求作出响应,并把对应的 html 文本发送给浏览器;
  • 5.浏览器将该 html 文本并显示内容;
  • 6.释放 TCP连接(四次挥手);

目前常见的通信协议都是建立在TCP链接之上

那么什么是TCP

TCP是因特网中的传输层协议,使用三次握手协议建立连接。当主动方发出SYN连接请求后,等待对方回答

TCP三次握手的过程如下:
  • 客户端发送SYN报文给服务器端,进入SYN_SEND状态。
  • 服务器端收到SYN报文,回应一个SYN(SEQ=y)ACK(ACK=x+1)报文,进入SYN_RECV状态。
  • 客户端收到服务器端的SYN报文,回应一个ACK(ACK=y+1)报文,进入Established状态。
  • 三次握手完成,TCP客户端和服务器端成功地建立连接,可以开始传输数据了。
如图所示:

clipboard.png

TCP的四次挥手:
  • 建立一个连接需要三次握手,而终止一个连接要经过四次握手,这是由TCP的半关闭(half-close)造成的。具体过程如下图所示。
  • 某个应用进程首先调用close,称该端执行“主动关闭”(active close)。该端的TCP于是发送一个FIN分节,表示数据发送完毕。
  • 接收到这个FIN的对端执行 “被动关闭”(passive close),这个FIN由TCP确认。

注意:FIN的接收也作为一个文件结束符(end-of-file)传递给接收端应用进程,放在已排队等候该应用进程接收的任何其他数据之后,因为,FIN的接收意味着接收端应用进程在相应连接上再无额外数据可接收。

  • 一段时间后,接收到这个文件结束符的应用进程将调用close关闭它的套接字。这导致它的TCP也发送一个FIN。
  • 接收这个最终FIN的原发送端TCP(即执行主动关闭的那一端)确认这个FIN。 [3]

既然每个方向都需要一个FIN和一个ACK,因此通常需要4个分节。

特别提示: SYN报文用来通知,FIN报文是用来同步的

clipboard.png

以上就是面试官常问的三次握手,四次挥手,但是这不仅仅面试题,上面仅仅答到了一点皮毛,学习这些是为了让我们后续方便了解他的优缺点。

TCP连接建立后,我们可以有多种协议的方式通信交换数据:

最古老的方式一:http 1.0

  • 早先1.0的HTTP版本,是一种无状态、无连接的应用层协议。
  • HTTP1.0规定浏览器和服务器保持短暂的连接,浏览器的每次请求都需要与服务器建立一个TCP连接,服务器处理完成后立即断开TCP连接(无连接),服务器不跟踪每个客户端也不记录过去的请求(无状态)。
  • 这种无状态性可以借助cookie/session机制来做身份认证和状态记录。而下面两个问题就比较麻烦了。
  • 首先,无连接的特性导致最大的性能缺陷就是无法复用连接。每次发送请求的时候,都需要进行一次TCP的连接,而TCP的连接释放过程又是比较费事的。这种无连接的特性会使得网络的利用率非常低。
  • 其次就是队头阻塞(headoflineblocking)。由于HTTP1.0规定下一个请求必须在前一个请求响应到达之前才能发送。假设前一个请求响应一直不到达,那么下一个请求就不发送,同样的后面的请求也给阻塞了。
Http 1.0的致命缺点,就是无法复用TCP连接和并行发送请求,这样每次一个请求都需要三次握手,而且其实建立连接和释放连接的这个过程是最耗时的,传输数据相反却不那么耗时。还有本地时间被修改导致响应头expires的缓存机制失效的问题~(后面会详细讲)
  • 常见的请求报文~

clipboard.png

于是出现了Http 1.1,这也是技术的发展必然结果~

  • Http 1.1出现,继承了Http1.0的优点,也克服了它的缺点,出现了keep-alive这个头部字段,它表示会在建立TCP连接后,完成首次的请求,并不会立刻断开TCP连接,而是保持这个连接状态~进而可以复用这个通道
  • Http 1.1并且支持请求管道化,“并行”发送请求,但是这个并行,也不是真正意义上的并行,而是可以让我们把先进先出队列从客户端(请求队列)迁移到服务端(响应队列)
例如:客户端同时发了两个请求分别来获取html和css,假如说服务器的css资源先准备就绪,服务器也会先发送html再发送css。
  • B站首页,就有keep-alive,因为他们也有IM的成分在里面。需要大量复用TCP连接~

clipboard.png

  • HTTP1.1好像还是无法解决队头阻塞的问题
实际上,现阶段的浏览器厂商采取了另外一种做法,它允许我们打开多个TCP的会话。也就是说,上图我们看到的并行,其实是不同的TCP连接上的HTTP请求和响应。这也就是我们所熟悉的浏览器对同域下并行加载6~8个资源的限制。而这,才是真正的并行!

Http 1.1的致命缺点:

  • 1.明文传输
  • 2.其实还是没有解决无状态连接的
  • 3.当有多个请求同时被挂起的时候 就会拥塞请求通道,导致后面请求无法发送
  • 4.臃肿的消息首部:HTTP/1.1能压缩请求内容,但是消息首部不能压缩;在现今请求中,消息首部占请求绝大部分(甚至是全部)也较为常见.
我们也可以用dns-prefetch和 preconnect tcp来优化~
<link rel="preconnect" href="//example.com" crossorigin>
<link rel="dns=prefetch" href="//example.com">
  • Tip: webpack可以做任何事情,这些都可以用插件实现

基于这些缺点,出现了Http 2.0

相较于HTTP1.1,HTTP2.0的主要优点有采用二进制帧封装,传输变成多路复用,流量控制算法优化,服务器端推送,首部压缩,优先级等特点。

HTTP1.x的解析是基于文本的,基于文本协议的格式解析存在天然缺陷,文本的表现形式有多样性,要做到健壮性考虑的场景必然很多。而HTTP/2会将所有传输的信息分割为更小的消息和帧,然后采用二进制的格式进行编码,HTTP1.x的头部信息会被封装到HEADER frame,而相应的RequestBody则封装到DATAframe里面。不改动HTTP的语义,使用二进制编码,实现方便且健壮。

多路复用

  • 所有的请求都是通过一个 TCP 连接并发完成。HTTP/1.x 虽然通过 pipeline 也能并发请求,但是多个请求之间的响应会被阻塞的,所以 pipeline 至今也没有被普及应用,而 HTTP/2 做到了真正的并发请求。同时,流还支持优先级和流量控制。当流并发时,就会涉及到流的优先级和依赖。即:HTTP2.0对于同一域名下所有请求都是基于流的,不管对于同一域名访问多少文件,也只建立一路连接。优先级高的流会被优先发送。图片请求的优先级要低于 CSS 和 SCRIPT,这个设计可以确保重要的东西可以被优先加载完

流量控制

  • TCP协议通过sliding window的算法来做流量控制。发送方有个sending window,接收方有receive window。http2.0的flow control是类似receive window的做法,数据的接收方通过告知对方自己的flow window大小表明自己还能接收多少数据。只有Data类型的frame才有flow control的功能。对于flow control,如果接收方在flow window为零的情况下依然更多的frame,则会返回block类型的frame,这张场景一般表明http2.0的部署出了问题。

服务器端推送

  • 服务器端的推送,就是服务器可以对一个客户端请求发送多个响应。除了对最初请求的响应外,服务器还可以额外向客户端推送资源,而无需客户端明确地请求。当浏览器请求一个html,服务器其实大概知道你是接下来要请求资源了,而不需要等待浏览器得到html后解析页面再发送资源请求。

首部压缩

  • HTTP 2.0 在客户端和服务器端使用“首部表”来跟踪和存储之前发送的键-值对,对于相同的数据,不再通过每次请求和响应发送;通信期间几乎不会改变的通用键-值对(用户代理、可接受的媒体类型,等等)只 需发送一次。事实上,如果请求中不包含首部(例如对同一资源的轮询请求),那么 首部开销就是零字节。此时所有首部都自动使用之前请求发送的首部。
  • 如果首部发生变化了,那么只需要发送变化了数据在Headers帧里面,新增或修改的首部帧会被追加到“首部表”。首部表在 HTTP 2.0 的连接存续期内始终存在,由客户端和服务器共同渐进地更新 。
  • 本质上,当然是为了减少请求啦,通过多个js或css合并成一个文件,多张小图片拼合成Sprite图,可以让多个HTTP请求减少为一个,减少额外的协议开销,而提升性能。当然,一个HTTP的请求的body太大也是不合理的,有个度。文件的合并也会牺牲模块化和缓存粒度,可以把“稳定”的代码or 小图 合并为一个文件or一张Sprite,让其充分地缓存起来,从而区分开迭代快的文件。
Demo的性能对比:

clipboard.png

Http的那些致命缺陷,并没有完全解决,于是有了https,也是目前应用最广的协议之一

HTTP+ 加密 + 认证 + 完整性保护 =HTTPS ?

可以这样认为~HTTP 加上加密处理和认证以及完整性保护后即是 HTTPS

  • 如果在 HTTP 协议通信过程中使用未经加密的明文,比如在 Web 页面中输入信用卡号,如果这条通信线路遭到窃听,那么信用卡号就暴露了。
  • 另外,对于 HTTP 来说,服务器也好,客户端也好,都是没有办法确认通信方的。

因为很有可能并不是和原本预想的通信方在实际通信。并且还需要考虑到接收到的报文在通信途中已经遭到篡改这一可能性。

  • 为了统一解决上述这些问题,需要在 HTTP 上再加入加密处理和认证等机制。我们把添加了加密及认证机制的 HTTP 称为 HTTPS
不加密的重要内容被wireshark这类工具抓到包,后果很严重~

HTTPS 是身披 SSL 外壳的 HTTP

  • HTTPS 并非是应用层的一种新协议。只是 HTTP 通信接口部分用 SSL(SecureSocket Layer)和 TLS(Transport Layer Security)协议代替而已。

通常,HTTP 直接和 TCP 通信。

  • 当使用 SSL 时,则演变成先和 SSL 通信,再由 SSL和 TCP 通信了。简言之,所谓 HTTPS,其实就是身披 SSL 协议这层外壳的HTTP。
  • 在采用 SSL 后,HTTP 就拥有了 HTTPS 的加密、证书和完整性保护这些功能。SSL 是独立于 HTTP 的协议,所以不光是 HTTP 协议,其他运行在应用层的 SMTP和 Telnet 等协议均可配合 SSL 协议使用。可以说 SSL 是当今世界上应用最为广泛的网络安全术。

clipboard.png

相互交换密钥的公开密钥加密技术 -----对称加密

clipboard.png

  • 在对 SSL 进行讲解之前,我们先来了解一下加密方法。SSL 采用一种叫做公开密钥加密(Public-key cryptography)的加密处理方式。
  • 近代的加密方法中加密算法是公开的,而密钥却是保密的。通过这种方式得以保持加密方法的安全性。
加密和解密都会用到密钥。没有密钥就无法对密码解密,反过来说,任何人只要持有密钥就能解密了。如果密钥被攻击者获得,那加密也就失去了意义。

HTTPS 采用混合加密机制

  • HTTPS 采用共享密钥加密和公开密钥加密两者并用的混合加密机制。
  • 但是公开密钥加密与共享密钥加密相比,其处理速度要慢。所以应充分利用两者各自的优势,将多种方法组合起来用于通信。在交换密钥环节使用公开密钥加密方式,之后的建立通信交换报文阶段则使用共享密钥加密方式。

HTTPS虽好,非对称加密虽好,但是不要滥用

HTTPS 也存在一些问题,那就是当使用 SSL 时,它的处理速度会变慢。

SSL 的慢分两种。一种是指通信慢。另一种是指由于大量消耗 CPU 及内存等资源,导致处理速度变慢。

  • 和使用 HTTP 相比,网络负载可能会变慢 2 到 100 倍。除去和 TCP 连接、发送 HTTP 请求 ? 响应以外,还必须进行 SSL 通信,因此整体上处理通信量不可避免会增加。
  • 另一点是 SSL 必须进行加密处理。在服务器和客户端都需要进行加密和解密的运算处理。因此从结果上讲,比起 HTTP 会更多地消耗服务器和客户端的硬件资源,导致负载增强。

针对速度变慢这一问题,并没有根本性的解决方案,我们会使用 SSL 加速器这种(专用服务器)硬件来改善该问题。该硬件为 SSL 通信专用硬件,相对软件来讲,能够提高数倍 SSL 的计算速度。仅在 SSL 处理时发挥 SSL加速器的功效,以分担负载。

为什么不一直使用 HTTPS

  • 既然 HTTPS 那么安全可靠,那为何所有的 Web 网站不一直使用 HTTPS?

其中一个原因是,因为与纯文本通信相比,加密通信会消耗更多的 CPU 及内存资源。如果每次通信都加密,会消耗相当多的资源,平摊到一台计算机上时,能够处理的请求数量必定也会随之减少。

  • 因此,如果是非敏感信息则使用 HTTP 通信,只有在包含个人信息等敏感数据时,才利用 HTTPS 加密通信。

特别是每当那些访问量较多的 Web 网站在进行加密处理时,它们所承担着的负载不容小觑。在进行加密处理时,并非对所有内容都进行加密处理,而是仅在那些需要信息隐藏时才会加密,以节约资源。

  • 除此之外,想要节约购买证书的开销也是原因之一。

要进行 HTTPS 通信,证书是必不可少的。而使用的证书必须向认证机构(CA)购买。证书价格可能会根据不同的认证机构略有不同。通常,一年的授权需要数万日元(现在一万日元大约折合 600 人民币)。那些购买证书并不合算的服务以及一些个人网站,可能只会选择采用HTTP 的通信方式。

clipboard.png

复习完了基本的协议,介绍下报文格式:

  • 请求报文格式

clipboard.png

  • 响应报文格式

clipboard.png

所谓响应头,请求头,其实都可以自己添加字段,只要前后端给对应的处理机制即可

Node.js代码实现响应头的设置


  if (config.cache.expires) {
                        res.setHeader("expries", new Date(Date.now() + (config.cache.maxAge * 1000)))
                    }
                    if (config.cache.lastModified) {
                        res.setHeader("last-modified", stat.mtime.toUTCString())
                    }
                    if (config.cache.etag) {
                        res.setHeader('Etag', etagFn(stat))
                    }
}

响应头的详解:

clipboard.png

本人的开源项目,手写的Node.js静态资源服务器,https://github.com/JinJieTan/...,欢迎 star~

浏览器的缓存策略:

  • 首次请求:

clipboard.png

  • 非首次请求:

clipboard.png

  • 用户行为与缓存:

clipboard.png

不能缓存的请求:

无法被浏览器缓存的请求如下:

  • HTTP信息头中包含Cache-Control:no-cache,pragma:no-cache(HTTP1.0),或Cache-Control:max-age=0等告诉浏览器不用缓存的请求
  • 需要根据Cookie,认证信息等决定输入内容的动态请求是不能被缓存的
  • 经过HTTPS安全加密的请求(有人也经过测试发现,ie其实在头部加入Cache-Control:max-age信息,firefox在头部加入Cache-Control:Public之后,能够对HTTPS的资源进行缓寸)
  • 经过HTTPS安全加密的请求(有人也经过测试发现,ie其实在头部加入Cache-Control:max-age信息,firefox在头部加入Cache-Control:Public之后,能够对HTTPS的资源进行缓存,参考《HTTPS的七个误解》)
  • POST请求无法被缓存
  • HTTP响应头中不包含Last-Modified/Etag,也不包含Cache-Control/Expires的请求无法被缓存

即时通讯协议

从最初的没有websocket协议开始:

传统的协议无法服务端主动push数据,于是有了这些骚操作:
  • 轮询,在一个定时器中不停向服务端发送请求。
  • 长轮询,发送请求给服务端,直到服务端觉得可以返回数据了再返回响应,否则这个请求一直挂起~
  • 以上两种都有瑕疵,而且比较明显,这里不再描述。

为了解决实时通讯,数据同步的问题,出现了webSocket.

  • webSockets的目标是在一个单独的持久连接上提供全双工、双向通信。在Javascript创建了Web Socket之后,会有一个HTTP请求发送到浏览器以发起连接。在取得服务器响应后,建立的连接会将HTTP升级从HTTP协议交换为WebSocket协议。
  • webSocket原理: 在TCP连接第一次握手的时候,升级为ws协议。后面的数据交互都复用这个TCP通道。
  • 客户端代码实现:
  const ws = new WebSocket('ws://localhost:8080');
        ws.onopen = function () {
            ws.send('123')
            console.log('open')
        }
        ws.onmessage = function () {
            console.log('onmessage')
        }
        ws.onerror = function () {
            console.log('onerror')
        }
        ws.onclose = function () {
            console.log('onclose')
        }
  • 服务端使用 Node.js语言实现
const express = require('express')
const { Server } = require("ws");
const app = express()
const wsServer = new Server({ port: 8080 })
wsServer.on('connection', (ws) => {
    ws.onopen = function () {
        console.log('open')
    }
    ws.onmessage = function (data) {
        console.log(data)
        ws.send('234')
        console.log('onmessage' + data)
    }
    ws.onerror = function () {
        console.log('onerror')
    }
    ws.onclose = function () {
        console.log('onclose')
    }
});

app.listen(8000, (err) => {
    if (!err) { console.log('监听OK') } else {
        console.log('监听失败')
    }
})

webSocket的报文格式有一些不一样:

![图片上传中...]

  • 客户端和服务端进行Websocket消息传递是这样的:

    • 客户端:将消息切割成多个帧,并发送给服务端。
    • 服务端:接收消息帧,并将关联的帧重新组装成完整的消息。

即时通讯的心跳检测:

pingandpong

  • 服务端Go实现:
package main

import (
    "net/http"
    "time"

    "github.com/gorilla/websocket"
)

var (
    //完成握手操作
    upgrade = websocket.Upgrader{
       //允许跨域(一般来讲,websocket都是独立部署的)
       CheckOrigin:func(r *http.Request) bool {
            return true
       },
    }
)

func wsHandler(w http.ResponseWriter, r *http.Request) {
   var (
         conn *websocket.Conn
         err error
         data []byte
   )
   //服务端对客户端的http请求(升级为websocket协议)进行应答,应答之后,协议升级为websocket,http建立连接时的tcp三次握手将保持。
   if conn, err = upgrade.Upgrade(w, r, nil); err != nil {
        return
   }

    //启动一个协程,每隔5s向客户端发送一次心跳消息
    go func() {
        var (
            err error
        )
        for {
            if err = conn.WriteMessage(websocket.TextMessage, []byte("heartbeat")); err != nil {
                return
            }
            time.Sleep(5 * time.Second)
        }
    }()

   //得到websocket的长链接之后,就可以对客户端传递的数据进行操作了
   for {
         //通过websocket长链接读到的数据可以是text文本数据,也可以是二进制Binary
        if _, data, err = conn.ReadMessage(); err != nil {
            goto ERR
     }
     if err = conn.WriteMessage(websocket.TextMessage, data); err != nil {
         goto ERR
     }
   }
ERR:
    //出错之后,关闭socket连接
    conn.Close()
}

func main() {
    http.HandleFunc("/ws", wsHandler)
    http.ListenAndServe("0.0.0.0:7777", nil)
}

客户端的心跳检测(Node.js实现):

this.heartTimer = setInterval(() => {
      if (this.heartbeatLoss < MAXLOSSTIMES) {
        events.emit('network', 'sendHeart');
        this.heartbeatLoss += 1;
        this.phoneLoss += 1;
      } else {
        events.emit('network', 'offline');
        this.stop();
      }
      if (this.phoneLoss > MAXLOSSTIMES) {
        this.PhoneLive = false;
        events.emit('network', 'phoneDisconnect');
      }
    }, 5000);

自定义即时通信协议:

new Socket开始:

  • 目前即时通讯大都使用现有大公司成熟的SDK接入,但是逼格高些还是自己重写比较好。
  • 打个小广告,我们公司就是自己定义的即时通讯协议~招聘一位高级前端,地点深圳-深南大道,做跨平台IM桌面应用开发的~
  • 客户端代码实现(Node.js):

const {Socket} = require('net') 
const tcp = new Socket()
tcp.setKeepAlive(true);
tcp.setNoDelay(true);
//保持底层tcp链接不断,长连接
指定对应域名端口号链接
tcp.connect(80,166.166.0.0)
建立连接后
根据后端传送的数据类型 使用对应不同的解析
readUInt8 readUInt16LE readUInt32LE readIntLE等处理后得到myBuf 
const myBuf = buffer.slice(start);//从对应的指针开始的位置截取buffer
const header = myBuf.slice(headstart,headend)//截取对应的头部buffer
const body = JSON.parse(myBuf.slice(headend-headstart,bodylength).tostring())
//精确截取数据体的buffer,并且转化成js对象
即时通讯强烈推荐使用Golang,GRPC,Prob传输数据。

上面的一些代码,都在我的开源项目中:

觉得写得不错,可以点个赞支持下,文章也借鉴了一下其他大佬的文章,但是地址都贴上来了~ 欢迎gitHub点个star哦~
查看原文

赞 180 收藏 143 评论 0

灯盏细辛 赞了文章 · 2019-03-21

白话es6系列一:Array.of()和Array.from()

es6新增了二种方法:Array.of()和Array.from(),它们有什么用途呢?在平时的开发中能给我们带来什么方便呢?本篇将从一个创建数组的小问题开始,逐步揭开它们的面纱。

一个问题

首先,我们来看一个问题,我需要创建一个共81项的数组,有9行,每行9个数(从1-9),在页面上进行展示,如下:
clipboard.png

怎么做呢?可以这样:

let arr = [1, 2, 3, 4, 5, 6, 7, 8, 9]
arr = arr.concat(arr)
arr = arr.concat(arr)
arr = arr.concat(arr)
arr = arr.concat([1, 2, 3, 4, 5, 6, 7, 8, 9])

console.table(arr)

真的不优雅!一看就是菜鸡程序员写的,但不管怎么样,的确实现了功能。
那规模加大,需要的是810项的数组呢?那就需要换一种实现方式了,如下:

let arr = Array.apply(null, { length: 810 })
  .map((item, index) => {
    return {
      id: index,
      number: index % 9 + 1
    }
  })

看上去是不错,但容易让人困惑,阅读起来也不直观,又是apply,又是null的,有点让人抓狂。
可能有人会说,用es6的Array.from()来做啊:

let arr = Array.from({ length: 810 }, (item, index) => ({
  id: index,
  number: index % 9 + 1
}))

不错哦,代码量变少了。更重要的是,代码意图也直观:数组长度810,每一项按照约定的规则进行初始化。

看完了问题,接下来具体说说这二个方法的诞生背景和用法,先来看Array.of

Array.of

es6之前,最常用的创建数组有如下两种方式,一种是字面量方式,还一种是new Array()方式:

var arr1 = [1, 3, 5]
var arr2 = new Array(1, 3, 5)

这里要重点提一下new Array()方式,因为它有一个缺陷,如果要用这种方式创建一个数组,其中就只有一个数字3,它居然无法做到!

var arr = new Array(3)

上面只会得到一个长度为3的数组(各项都是empty),而不是一个长度为1的数组(其中的项为数字3)。
我给你一个数字3,让你给我存到数组中,你给我一个长度为3的数组?惊不惊喜,意不意外?

还好,ES6引入了Array.of()方法来解决这个问题。

let arr = Array.of(3)

上面代码创建了一个长度为1的数组(其中的项为数字3)。
相比new Array()这种方式,Array.of()的方式显得更明了,我需要存什么,扔给它,它就给我存到数组中,不会给我意外和惊吓。

Array.from

可能会有人问,既然已经新增了Array.of()这种方式,还需要Array.from()方式吗?他们有区别呢?
简单点说,Array.from()适用于将非数组对象转换为数组的场景,它的初衷就是为了解决将非数组对象转换为数组的问题。
我们来看一个例子:

function makeArray() {
  return Array.from(arguments);
}
let arr = makeArray('a', 'b', 'c');

我们知道,JS函数中有一个arguments对象,arguments是一个类数组对象。
上面代码中,用Array.from()将arguments转换成真正的数组并返回。多方便啊,就一个方法搞定。
而在es6之前,如果要将arguments转换为数组,你得自己写一个类似的转换方法,多麻烦啊。

Array.from()的强大不止于此,它还能接受一个映射函数:

function cube() {
  return Array.from(arguments, value => value ** 3);
}
let arr = cube(1, 3, 5);

上面代码中,arguments被直接传递给Array.from()方法,从而将它包含的值转换成了数组。
映射函数对每个数都进行了立方运算,因此目标数组的内容就是[ 1, 27, 125 ],Array.from()不仅能够将非数组对象转换为数组,还能按照我们想要的方式进行转换,强大!

注意:Array.from()方法不仅可用于类数组对象,也可用于可迭代对象,更多请参考Array.from()

总结

准确来说,Array.from()并不能算创建数组的一种方式,毕竟它的初衷是为了解决将非数组对象转换为数组的问题。
但话说回来,既然它能将非数组对象转换为数组,所以也可以说,它算创建数组的一种方式。
恭喜!读完本篇,你知道了至少四种创建数组的方式了。😱

查看原文

赞 21 收藏 12 评论 2

灯盏细辛 赞了文章 · 2019-03-20

React SSR 技术摘要

单页面应用(SPA)模式被越来越多的站点所采用,这种模式意味着使用 JavaScript 直接在浏览器中渲染页面。所有逻辑,数据获取,模板和路由都在客户端处理,势必面临着首次有效绘制(FMP)耗时较长和不利于搜索引擎优化(SEO)的问题。

“同构(Universal)” 是指一套代码可以在服务端和客户端两种环境下运行,通过用这种灵活性,可以在服务端渲染初始内容输出到页面,后续工作交给客户端来完成,最终来解决SEO的问题并提升性能。“同构应用” 就像是精灵,可以游刃有余的穿梭在服务端与客户端之间各尽其能。

但是想驾驭 “同构应用” 往往会面临一系列的问题,下面针对一个示例进行一些细节介绍。

示例代码:https://github.com/xyyjk/reac...

构建配置

选择一个灵活的脚手架为项目后续的自定义功能及配置是十分有利的,Neutrino 提供了一些常用的 Webpack 预设配置,这些预设中包含了开发过程中常见的一些插件及配置使初始化和构建项目的过程更加简单。

下面基于预设做一些自定义配置,你可以随时通过运行 node_modules/.bin/neutrino --inspect 来了解最终完整的 Webpack 配置。

客户端配置

这里基于 @neutrinojs/react 预设做一些定义用于开发

.neutrinorc.js
const isDev = process.env.NODE_ENV !== 'production';
const isSSR = process.argv.includes('--ssr');

module.exports = {
  use: [
    ['@neutrinojs/react', {
      devServer: {
        port: isSSR ? 3000 : 5000,
        host: '0.0.0.0',
        disableHostCheck: true,
        contentBase: `${__dirname}/src`,
        before(app) { if(isSSR) { require('./src/server')(app); } },
      },
      manifest: true,
      html: isSSR ? false: {},
      clean: { paths: ['./node_modules/.cache']},
    }],

    ({ config }) => {
      if (isDev) { return; }

      config
        .output
          .filename('assets/[name].[chunkhash].js')
          .chunkFilename('assets/chunk.[chunkhash].js')
          .end()
        .optimization
          .minimize(false)
          .end();
    },
  ],
};

为了达到开发环境下可以选择 SSR(服务端渲染)、CSR(客户端渲染) 任意一种渲染模式,通过定义变量 isDevisSSR 用以做差异配置:

devServer.before 方法可以在服务内部的所有其他中间件之前,提供执行自定义中间件的功能。

SSR 模式 下加入一个中间件,稍后用于进行处理服务端组件内容渲染,同时很好的利用到了 devServer.hot 热更新功能。

SSR 模式 下使用动态定义 html 模板(src/server/template.js),这里把底层使用的 html-webpack-plugin 去掉。

启用 manifest 插件,打包后生成资源映射文件用于服务端渲染时模板中引入。

服务端配置

构建用于服务端运行的配置项稍有不同,由于 SSR 模式 最终代码要运行在 node 环境,这里需要对配置再做一些调整:

  • target 调整为 node,编译为类 Node 环境可用
  • libraryTarget 调整为 commonjs2,使用 Node 风格导出模块
  • @babel/preset-env 运行环境调整为 node,编译结果为 ES6 代码
  • 排除组件中 css/sass 资源的引用,生产环境直接使用通过 manifest 插件构建出的映射文件来读取资源

在打包的时候通过 webpack-node-externals 排除 node_modules 依赖模块,可以使服务器构建速度更快,并生成较小的 bundle 文件。

webpack.server.config.js
const Neutrino = require('neutrino/Neutrino');
const nodeExternals = require('webpack-node-externals');
const NormalPlugin = require('webpack/lib/NormalModuleReplacementPlugin');
const babelMerge = require('babel-merge');
const config = require('./.neutrinorc');

const neutrino = new Neutrino();

neutrino.use(config);

neutrino.config
  .target('node')

  .entryPoints
    .delete('index')
    .end()

  .entry('server')
    .add(`${__dirname}/src/server`)
    .end()

  .output
    .path(`${__dirname}/build`)
    .filename('server.js')
    .libraryTarget('commonjs2')
    .end()

  .externals([nodeExternals()])

  .plugins
    .delete('clean')
    .delete('manifest')
    .end()

  .plugin('normal')
    .use(NormalPlugin, [/\.css$/, 'lodash/noop'])
    .end()

  .optimization
    .minimize(false)
    .runtimeChunk(false)
    .end()

  .module
    .rule('compile')
    .use('babel')
    .tap(options => babelMerge(options, {
      presets: [
        ['@babel/preset-env', {
          targets: { node: true },
        }],
      ],
    }));

module.exports = neutrino.config.toConfig();

环境差异

由于运行环境和平台 API 的差异,当运行在不同环境中时,我们的代码将不会完全相同。

Webpack 全局对象中定义了 process.browser,可以在开发环境中来判断当前是客户端还是服务端。

自定义中间件

开发环境 SSR 模式 下,如果我们在组件中引入了图片或样式资源,不经过 webpack-loader 进行编译,Node 环境下是无法直接运行的。在 Node 环境下,通过 ignore-styles 可以把这些资源进行忽略。

此外,为了让 Node 环境下能够运行 ES6 模块的组件,需要引入 @babel/register 来做一些转换:

src/server/register.js
require('ignore-styles');

require('@babel/register')({
  presets: [
    ['@babel/preset-env', {
      targets: { node: true },
    }],
    '@babel/preset-react',
  ],
  plugins: [
    '@babel/plugin-proposal-class-properties',
  ],
});

如果 Webpack 中配置了 resolve.alias,与之对应的还需要增加 babel-plugin-module-resolver 插件来做解析。

清除模块缓存

由于 require() 引入方式模块将会被缓存, 为了使组件内的修改实时生效,通过 decache 模块从 require() 缓存中删除模块后再次重新引用:

src/server/dev.js
require('./register');

const decache = require('decache');
const routes = require('./routes');
let render = require('./render');

const handler = async (req, res, next) => {
  decache('./render');
  render = require('./render');
  res.send(await render({ req, res }));
  next();
};

module.exports = (app) => {
  app.get(routes, handler);
};

服务端渲染

在服务端通过 ReactDOMServer.renderToString() 方法将组件渲染为初始 HTML 字符串。

获取数据往往需要从 querycookie 中取一些内容作为接口参数,
Node 环境下没有 windowdocument 这样的浏览器对象,可以借助 Express 的 req 对象来拿到一些信息:

  • href: ${req.protocol}://${req.headers.host}${req.url}
  • cookie: req.headers.cookie
  • userAgent: req.headers['user-agent']
src/server/render.js
const React = require('react');
const { renderToString } = require('react-dom/server');

...

module.exports = async ({ req, res }) => {
  const locals = {
    data: await fetchData({ req, res }),
    href: `${req.protocol}://${req.headers.host}${req.url}`,
    url: req.url,
  };

  const markup = renderToString(<App locals={locals} />);
  const helmet = Helmet.renderStatic();

  return template({ markup, helmet, assets, locals });
};

入口文件

前端调用 ReactDOM.hydrate() 方法把服务端返回的静态 HTML 与事件相融合绑定。

src/index.jsx
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

const renderMethod = ReactDOM[module.hot ? 'render' : 'hydrate'];
renderMethod(<App />, document.getElementById('root'));

根组件

在服务端使用 StaticRouter 组件,通过 location 属性设置服务器收到的URL,并在 context 属性中存入渲染期间所需要的数据。

src/App.jsx
import React from 'react';
import { BrowserRouter, StaticRouter, Route } from 'react-router-dom';
import { hot } from 'react-hot-loader/root';

...

const Router = process.browser ? BrowserRouter : StaticRouter;

const App = ({ locals = {} }) => (
  <Router location={locals.url} context={locals}>
    <Layout>
      <Route exact path="/" component={Home}/>
      <Route path="/about" component={About}/>
      <Route path="/contact" component={Contact}/>
      <Route path="/character/:key" component={Character}/>
    </Layout>
  </Router>
);

export default hot(App);

内容数据

通过 constructor 接收 StaticRouter 组件传入的数据,客户端 URL 与服务端请求地址相一致时直接使用传入的数据,否则再进行客户端数据请求。

src/comps/Content.jsx
import React from 'react';
import { withRouter } from 'react-router-dom';
import fetchData from '../utils/fetchData';

function isCurUrl() {
  if (!window.__INITIAL_DATA__) { return false; }
  return document.location.href === window.__INITIAL_DATA__.href;
}

class Content extends React.Component {
  constructor(props) {
    super(props);

    const { staticContext = {} } = props;
    let { data = {} } = staticContext;

    if (process.browser && isCurUrl()) {
      data = window.__INITIAL_DATA__.data;
    }

    this.state = { data };
  }

  async componentDidMount() {
    if (isCurUrl()) { return; }
    
    const { match } = this.props;
    const data = await fetchData({ match });

    this.setState({ data });
  }

  
  render() {
    return this.props.render(this.state);
  }
}

export default withRouter(Content);

自定义标记

通常在不同页面中需要输出不同的页面标题、页面描述,HTML 属性等,可以借助 react-helmet 来处理此类问题:

模板设置

const markup = ReactDOMServer.renderToString(<Handler />);
const helmet = Helmet.renderStatic();

const template = `
<!DOCTYPE html>
<html ${helmet.htmlAttributes.toString()}>
  <head>
    <meta charset="UTF-8">
    ${helmet.title.toString()}
    ${helmet.meta.toString()}
    ${helmet.link.toString()}
  </head>
  <body ${helmet.bodyAttributes.toString()}>
    <div id="root">${markup}</div>
  </body>
</html>
`;

组件中的使用

import React from 'react';
import Helmet from 'react-helmet';

const Contact = () => (
  <>
    <h2>This is the contact page</h2>
    <Helmet>
      <title>Contact Page</title>
      <meta name="description" content="This is a proof of concept for React SSR" />
    </Helmet>
  </>
);

总结

想要做好 “同构应用” 并不简单,需要了解非常多的概念。好消息是目前 React 社区有一些比较著名的同构方案 Next.jsRazzleElectrode 等,如果你想快速入手 React SSR 这些或许是不错的选择。如果面对复杂应用,自定义完整的体系将会更加灵活。

查看原文

赞 12 收藏 9 评论 0

灯盏细辛 评论了文章 · 2019-03-20

白话es6系列一:Array.of()和Array.from()

es6新增了二种方法:Array.of()和Array.from(),它们有什么用途呢?在平时的开发中能给我们带来什么方便呢?本篇将从一个创建数组的小问题开始,逐步揭开它们的面纱。

一个问题

首先,我们来看一个问题,我需要创建一个共81项的数组,有9行,每行9个数(从1-9),在页面上进行展示,如下:
clipboard.png

怎么做呢?可以这样:

let arr = [1, 2, 3, 4, 5, 6, 7, 8, 9]
arr = arr.concat(arr)
arr = arr.concat(arr)
arr = arr.concat(arr)
arr = arr.concat([1, 2, 3, 4, 5, 6, 7, 8, 9])

console.table(arr)

真的不优雅!一看就是菜鸡程序员写的,但不管怎么样,的确实现了功能。
那规模加大,需要的是810项的数组呢?那就需要换一种实现方式了,如下:

let arr = Array.apply(null, { length: 810 })
  .map((item, index) => {
    return {
      id: index,
      number: index % 9 + 1
    }
  })

看上去是不错,但容易让人困惑,阅读起来也不直观,又是apply,又是null的,有点让人抓狂。
可能有人会说,用es6的Array.from()来做啊:

let arr = Array.from({ length: 810 }, (item, index) => ({
  id: index,
  number: index % 9 + 1
}))

不错哦,代码量变少了。更重要的是,代码意图也直观:数组长度810,每一项按照约定的规则进行初始化。

看完了问题,接下来具体说说这二个方法的诞生背景和用法,先来看Array.of

Array.of

es6之前,最常用的创建数组有如下两种方式,一种是字面量方式,还一种是new Array()方式:

var arr1 = [1, 3, 5]
var arr2 = new Array(1, 3, 5)

这里要重点提一下new Array()方式,因为它有一个缺陷,如果要用这种方式创建一个数组,其中就只有一个数字3,它居然无法做到!

var arr = new Array(3)

上面只会得到一个长度为3的数组(各项都是empty),而不是一个长度为1的数组(其中的项为数字3)。
我给你一个数字3,让你给我存到数组中,你给我一个长度为3的数组?惊不惊喜,意不意外?

还好,ES6引入了Array.of()方法来解决这个问题。

let arr = Array.of(3)

上面代码创建了一个长度为1的数组(其中的项为数字3)。
相比new Array()这种方式,Array.of()的方式显得更明了,我需要存什么,扔给它,它就给我存到数组中,不会给我意外和惊吓。

Array.from

可能会有人问,既然已经新增了Array.of()这种方式,还需要Array.from()方式吗?他们有区别呢?
简单点说,Array.from()适用于将非数组对象转换为数组的场景,它的初衷就是为了解决将非数组对象转换为数组的问题。
我们来看一个例子:

function makeArray() {
  return Array.from(arguments);
}
let arr = makeArray('a', 'b', 'c');

我们知道,JS函数中有一个arguments对象,arguments是一个类数组对象。
上面代码中,用Array.from()将arguments转换成真正的数组并返回。多方便啊,就一个方法搞定。
而在es6之前,如果要将arguments转换为数组,你得自己写一个类似的转换方法,多麻烦啊。

Array.from()的强大不止于此,它还能接受一个映射函数:

function cube() {
  return Array.from(arguments, value => value ** 3);
}
let arr = cube(1, 3, 5);

上面代码中,arguments被直接传递给Array.from()方法,从而将它包含的值转换成了数组。
映射函数对每个数都进行了立方运算,因此目标数组的内容就是[ 1, 27, 125 ],Array.from()不仅能够将非数组对象转换为数组,还能按照我们想要的方式进行转换,强大!

注意:Array.from()方法不仅可用于类数组对象,也可用于可迭代对象,更多请参考Array.from()

总结

准确来说,Array.from()并不能算创建数组的一种方式,毕竟它的初衷是为了解决将非数组对象转换为数组的问题。
但话说回来,既然它能将非数组对象转换为数组,所以也可以说,它算创建数组的一种方式。
恭喜!读完本篇,你知道了至少四种创建数组的方式了。😱

查看原文

灯盏细辛 收藏了文章 · 2019-01-29

大前端时代安全性如何做

之前在上家公司的时候做过一些爬虫的工作,也帮助爬虫工程师解决过一些问题。然后我写过一些文章发布到网上,之后有一些人就找我做一些爬虫的外包,内容大概是爬取小红书的用户数据和商品数据,但是我没做。我觉得对于国内的大数据公司没几家是有真正的大数据量,而是通过爬虫工程师团队不断的去各地爬取数据,因此不要以为我们的数据没价值,对于内容型的公司来说,数据是可信竞争力。那么我接下来想说的就是网络和数据的安全性问题。
对于内容型的公司,数据的安全性很重要。对于内容公司来说,数据的重要性不言而喻。比如你一个做在线教育的平台,题目的数据很重要吧,但是被别人通过爬虫技术全部爬走了?如果核心竞争力都被拿走了,那就是凉凉。再比说有个独立开发者想抄袭你的产品,通过抓包和爬虫手段将你核心的数据拿走,然后短期内做个网站和 App,短期内成为你的劲敌。

背景

目前通过 App 中的 网页分析后,我们的数据安全性做的较差,有以下几个点存在问题:

  1. 网站的数据通过最早期的前后端分离来实现。稍微学过 Web 前端的工程师都可以通过神器 Chrome 分析网站,进而爬取需要的数据。打开 「Network」就可以看到网站的所有网络请求了,哎呀,不小心我看到了什么?没错就是网站的接口信息都可以看到了。比如 “detail.json?itemId=141529859”。或者你的网站接口有些特殊的判断处理,将一些信息存储到 sessionStorage、cookie、localStorage 里面,有点前端经验的爬虫工程师心想”嘿嘿嘿,这不是在裸奔数据么“。或者有些参数是通过 JavaScript 临时通过函数生成的。问题不大,工程师也可以对网页元素进行查找,找到关键的 id、或者 css 类名,然后在 "Search“ 可以进行查找,找到对应的代码 JS 代码,点击查看代码,如果是早期前端开发模式那么代码就是裸奔的,跟开发者在自己的 IDE 里面看到的内容一样,有经验的爬虫就可以拿这个做事情,因此安全性问题亟待解决。

想知道 Chrome 更多的调试使用技巧,看看这篇文章

Chrome调试1
Chrome调试2
Chrome调试3

  1. App 的数据即使采用了 HTTPS,但是对于专业的抓包工具也是可以直接拿到数据的,因此 App 的安全问题也可以做一些提高,具体的策略下文会讲到。

想知道 Charles 的更多使用技巧,可以看看这篇文章

爬虫手段

  • 目前爬虫技术都是从渲染好的 html 页面直接找到感兴趣的节点,然后获取对应的文本
  • 有些网站安全性做的好,比如列表页可能好获取,但是详情页就需要从列表页点击对应的 item,将 itemId 通过 form 表单提交,服务端生成对应的参数,然后重定向到详情页(重定向过来的地址后才带有详情页的参数 detailID),这个步骤就可以拦截掉一部分的爬虫开发者

解决方案

制定出Web 端反爬技术方案

本人从这2个角度(网页所见非所得、查接口请求没用)出发,制定了下面的反爬方案。

  • 使用HTTPS 协议
  • 单位时间内限制掉请求次数过多,则封锁该账号
  • 前端技术限制 (接下来是核心技术)
# 比如需要正确显示的数据为“19950220”

1. 先按照自己需求利用相应的规则(数字乱序映射,比如正常的0对应还是0,但是乱序就是 0 <-> 1,1 <-> 9,3 <-> 8,...)制作自定义字体(ttf)
2. 根据上面的乱序映射规律,求得到需要返回的数据 19950220 -> 17730220
3. 对于第一步得到的字符串,依次遍历每个字符,将每个字符根据按照线性变换(y=kx+b)。线性方程的系数和常数项是根据当前的日期计算得到的。比如当前的日期为“2018-07-24”,那么线性变换的 k 为 7,b 为 24。
4. 然后将变换后的每个字符串用“3.1415926”拼接返回给接口调用者。(为什么是3.1415926,因为对数字伪造反爬,所以拼接的文本肯定是数字的话不太会引起研究者的注意,但是数字长度太短会误伤正常的数据,所以用所熟悉的 Π)

​```
1773 -> “1*7+24” + “3.1415926” + “7*7+24” + “3.1415926” + “7*7+24” + “3.1415926” + “3*7+24” -> 313.1415926733.1415926733.141592645
02 -> "0*7+24" + "3.1415926" + "2*7+24" -> 243.141592638
20 -> "2*7+24" + "3.1415926" + "0*7+24" -> 383.141592624
​```

# 前端拿到数据后再解密,解密后根据自定义的字体 Render 页面
1. 先将拿到的字符串按照“3.1415926”拆分为数组
2. 对数组的每1个数据,按照“线性变换”(y=kx+b,k和b同样按照当前的日期求解得到),逆向求解到原本的值。
3. 将步骤2的的到的数据依次拼接,再根据 ttf 文件 Render 页面上。
  • 后端需要根据上一步设计的协议将数据进行加密处理

下面以 Node.js 为例讲解后端需要做的事情

  • 首先后端设置接口路由
  • 获取路由后面的参数
  • 根据业务需要根据 SQL 语句生成对应的数据。如果是数字部分,则需要按照上面约定的方法加以转换。
  • 将生成数据转换成 JSON 返回给调用者

    // json
    var JoinOparatorSymbol = "3.1415926";
    function encode(rawData, ruleType) {
      if (!isNotEmptyStr(rawData)) {
        return "";
      }
      var date = new Date();
      var year = date.getFullYear();
      var month = date.getMonth() + 1;
      var day = date.getDate();
    
      var encodeData = "";
      for (var index = 0; index < rawData.length; index++) {
        var datacomponent = rawData[index];
        if (!isNaN(datacomponent)) {
          if (ruleType < 3) {
            var currentNumber = rawDataMap(String(datacomponent), ruleType);
            encodeData += (currentNumber * month + day) + JoinOparatorSymbol;
          }
          else if (ruleType == 4) {
            encodeData += rawDataMap(String(datacomponent), ruleType);
          }
          else {
            encodeData += rawDataMap(String(datacomponent), ruleType) + JoinOparatorSymbol;
          }
        }
        else if (ruleType == 4) {
          encodeData += rawDataMap(String(datacomponent), ruleType);
        }
    
      }
      if (encodeData.length >= JoinOparatorSymbol.length) {
        var lastTwoString = encodeData.substring(encodeData.length - JoinOparatorSymbol.length, encodeData.length);
        if (lastTwoString == JoinOparatorSymbol) {
          encodeData = encodeData.substring(0, encodeData.length - JoinOparatorSymbol.length);
        }
      }
    //字体映射处理
    function rawDataMap(rawData, ruleType) {
    
      if (!isNotEmptyStr(rawData) || !isNotEmptyStr(ruleType)) {
        return;
      }
      var mapData;
      var rawNumber = parseInt(rawData);
      var ruleTypeNumber = parseInt(ruleType);
      if (!isNaN(rawData)) {
        lastNumberCategory = ruleTypeNumber;
        //字体文件1下的数据加密规则
        if (ruleTypeNumber == 1) {
          if (rawNumber == 1) {
            mapData = 1;
          }
          else if (rawNumber == 2) {
            mapData = 2;
          }
          else if (rawNumber == 3) {
            mapData = 4;
          }
          else if (rawNumber == 4) {
            mapData = 5;
          }
          else if (rawNumber == 5) {
            mapData = 3;
          }
          else if (rawNumber == 6) {
            mapData = 8;
          }
          else if (rawNumber == 7) {
            mapData = 6;
          }
          else if (rawNumber == 8) {
            mapData = 9;
          }
          else if (rawNumber == 9) {
            mapData = 7;
          }
          else if (rawNumber == 0) {
            mapData = 0;
          }
        }
        //字体文件2下的数据加密规则
        else if (ruleTypeNumber == 0) {
    
          if (rawNumber == 1) {
            mapData = 4;
          }
          else if (rawNumber == 2) {
            mapData = 2;
          }
          else if (rawNumber == 3) {
            mapData = 3;
          }
          else if (rawNumber == 4) {
            mapData = 1;
          }
          else if (rawNumber == 5) {
            mapData = 8;
          }
          else if (rawNumber == 6) {
            mapData = 5;
          }
          else if (rawNumber == 7) {
            mapData = 6;
          }
          else if (rawNumber == 8) {
            mapData = 7;
          }
          else if (rawNumber == 9) {
            mapData = 9;
          }
          else if (rawNumber == 0) {
            mapData = 0;
          }
        }
        //字体文件3下的数据加密规则
        else if (ruleTypeNumber == 2) {
    
          if (rawNumber == 1) {
            mapData = 6;
          }
          else if (rawNumber == 2) {
            mapData = 2;
          }
          else if (rawNumber == 3) {
            mapData = 1;
          }
          else if (rawNumber == 4) {
            mapData = 3;
          }
          else if (rawNumber == 5) {
            mapData = 4;
          }
          else if (rawNumber == 6) {
            mapData = 8;
          }
          else if (rawNumber == 7) {
            mapData = 3;
          }
          else if (rawNumber == 8) {
            mapData = 7;
          }
          else if (rawNumber == 9) {
            mapData = 9;
          }
          else if (rawNumber == 0) {
            mapData = 0;
          }
        }
        else if (ruleTypeNumber == 3) {
    
          if (rawNumber == 1) {
            mapData = "&#xefab;";
          }
          else if (rawNumber == 2) {
            mapData = "&#xeba3;";
          }
          else if (rawNumber == 3) {
            mapData = "&#xecfa;";
          }
          else if (rawNumber == 4) {
            mapData = "&#xedfd;";
          }
          else if (rawNumber == 5) {
            mapData = "&#xeffa;";
          }
          else if (rawNumber == 6) {
            mapData = "&#xef3a;";
          }
          else if (rawNumber == 7) {
            mapData = "&#xe6f5;";
          }
          else if (rawNumber == 8) {
            mapData = "&#xecb2;";
          }
          else if (rawNumber == 9) {
            mapData = "&#xe8ae;";
          }
          else if (rawNumber == 0) {
            mapData = "&#xe1f2;";
          }
        }
        else{
          mapData = rawNumber;
        }
      } else if (ruleTypeNumber == 4) {
        var sources = ["年", "万", "业", "人", "信", "元", "千", "司", "州", "资", "造", "钱"];
        //判断字符串为汉字
        if (/^[\u4e00-\u9fa5]*$/.test(rawData)) {
    
          if (sources.indexOf(rawData) > -1) {
            var currentChineseHexcod = rawData.charCodeAt(0).toString(16);
            var lastCompoent;
            var mapComponetnt;
            var numbers = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"];
            var characters = ["a", "b", "c", "d", "e", "f", "g", "h", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"];
    
            if (currentChineseHexcod.length == 4) {
              lastCompoent = currentChineseHexcod.substr(3, 1);
              var locationInComponents = 0;
              if (/[0-9]/.test(lastCompoent)) {
                locationInComponents = numbers.indexOf(lastCompoent);
                mapComponetnt = numbers[(locationInComponents + 1) % 10];
              }
              else if (/[a-z]/.test(lastCompoent)) {
                locationInComponents = characters.indexOf(lastCompoent);
                mapComponetnt = characters[(locationInComponents + 1) % 26];
              }
              mapData = "&#x" + currentChineseHexcod.substr(0, 3) + mapComponetnt + ";";
            }
          } else {
            mapData = rawData;
          }
    
        }
        else if (/[0-9]/.test(rawData)) {
          mapData = rawDataMap(rawData, 2);
        }
        else {
          mapData = rawData;
        }
    
      }
      return mapData;
    }
    //api
    module.exports = {
        "GET /api/products": async (ctx, next) => {
            ctx.response.type = "application/json";
            ctx.response.body = {
                products: products
            };
        },
    
        "GET /api/solution1": async (ctx, next) => {
    
            try {
                var data = fs.readFileSync(pathname, "utf-8");
                ruleJson = JSON.parse(data);
                rule = ruleJson.data.rule;
            } catch (error) {
                console.log("fail: " + error);
            }
    
            var data = {
                code: 200,
                message: "success",
                data: {
                    name: "@杭城小刘",
                    year: LBPEncode("1995", rule),
                    month: LBPEncode("02", rule),
                    day: LBPEncode("20", rule),
                    analysis : rule
                }
            }
    
            ctx.set("Access-Control-Allow-Origin", "*");
            ctx.response.type = "application/json";
            ctx.response.body = data;
        },
    
    
        "GET /api/solution2": async (ctx, next) => {
            try {
                var data = fs.readFileSync(pathname, "utf-8");
                ruleJson = JSON.parse(data);
                rule = ruleJson.data.rule;
            } catch (error) {
                console.log("fail: " + error);
            }
    
            var data = {
                code: 200,
                message: "success",
                data: {
                    name: LBPEncode("建造师",rule),
                    birthday: LBPEncode("1995年02月20日",rule),
                    company: LBPEncode("中天公司",rule),
                    address: LBPEncode("浙江省杭州市拱墅区石祥路",rule),
                    bidprice: LBPEncode("2万元",rule),
                    negative: LBPEncode("2018年办事效率太高、负面基本没有",rule),
                    title: LBPEncode("建造师",rule),
                    honor: LBPEncode("最佳奖",rule),
                    analysis : rule
                }
            }
            ctx.set("Access-Control-Allow-Origin", "*");
            ctx.response.type = "application/json";
            ctx.response.body = data;
        },
    
        "POST /api/products": async (ctx, next) => {
            var p = {
                name: ctx.request.body.name,
                price: ctx.request.body.price
            };
            products.push(p);
            ctx.response.type = "application/json";
            ctx.response.body = p;
        }
    };
    //路由
    const fs = require("fs");
    
    function addMapping(router, mapping){
        for(var url in mapping){
            if (url.startsWith("GET")) {
                var path = url.substring(4);
                router.get(path,mapping[url]);
                console.log(`Register URL mapping: GET: ${path}`);
            }else if (url.startsWith('POST ')) {
                var path = url.substring(5);
                router.post(path, mapping[url]);
                console.log(`Register URL mapping: POST ${path}`);
            } else if (url.startsWith('PUT ')) {
                var path = url.substring(4);
                router.put(path, mapping[url]);
                console.log(`Register URL mapping: PUT ${path}`);
            } else if (url.startsWith('DELETE ')) {
                var path = url.substring(7);
                router.del(path, mapping[url]);
                console.log(`Register URL mapping: DELETE ${path}`);
            } else {
                console.log(`Invalid URL: ${url}`);
            }
    
        }
    }
    
    
    function addControllers(router, dir){
        fs.readdirSync(__dirname + "/" + dir).filter( (f) => {
            return f.endsWith(".js");
        }).forEach( (f) => {
            console.log(`Process controllers:${f}...`);
            let mapping = require(__dirname + "/" + dir + "/" + f);
            addMapping(router,mapping);
        });
    }
    
    module.exports = function(dir){
        let controllers = dir || "controller";
        let router = require("koa-router")();
    
        addControllers(router,controllers);
        return router.routes();
    };
    
    
  • 前端根据服务端返回的数据逆向解密

    $("#year").html(getRawData(data.year,log));
    
    // util.js
    var JoinOparatorSymbol = "3.1415926";
    function isNotEmptyStr($str) {
      if (String($str) == "" || $str == undefined || $str == null || $str == "null") {
        return false;
      }
      return true;
    }
    
    function getRawData($json,analisys) {
      $json = $json.toString();
      if (!isNotEmptyStr($json)) {
        return;
      }
      
      var date= new Date();
      var year = date.getFullYear();
      var month = date.getMonth() + 1;
      var day = date.getDate();
      var datacomponents = $json.split(JoinOparatorSymbol);
      var orginalMessage = "";
      for(var index = 0;index < datacomponents.length;index++){
        var datacomponent = datacomponents[index];
          if (!isNaN(datacomponent) && analisys < 3){
              var currentNumber = parseInt(datacomponent);
              orginalMessage += (currentNumber -  day)/month;
          }
          else if(analisys == 3){
             orginalMessage += datacomponent;
          }
          else{
            //其他情况待续,本 Demo 根据本人在研究反爬方面的技术并实践后持续更新
          }
      }
      return orginalMessage;
    }
    

比如后端返回的是323.14743.14743.1446,根据我们约定的算法,可以的到结果为1773

  • 根据 ttf 文件 Render 页面
    自定义字体文件
    上面计算的到的1773,然后根据ttf文件,页面看到的就是1995
  • 然后为了防止爬虫人员查看 JS 研究问题,所以对 JS 的文件进行了加密处理。如果你的技术栈是 Vue 、React 等,webpack 为你提供了 JS 加密的插件,也很方便处理

    JS混淆工具

  • 个人觉得这种方式还不是很安全。于是想到了各种方案的组合拳。比如

反爬升级版

个人觉得如果一个前端经验丰富的爬虫开发者来说,上面的方案可能还是会存在被破解的可能,所以在之前的基础上做了升级版本

  1. 组合拳1: 字体文件不要固定,虽然请求的链接是同一个,但是根据当前的时间戳的最后一个数字取模,比如 Demo 中对4取模,有4种值 0、1、2、3。这4种值对应不同的字体文件,所以当爬虫绞尽脑汁爬到1种情况下的字体时,没想到再次请求,字体文件的规则变掉了 😂
  2. 组合拳2: 前面的规则是字体问题乱序,但是只是数字匹配打乱掉。比如 1 -> 4, 5 -> 8。接下来的套路就是每个数字对应一个 unicode 码 ,然后制作自己需要的字体,可以是 .ttf、.woff 等等。

网页检察元素得到的效果
接口返回数据

这几种组合拳打下来。对于一般的爬虫就放弃了。

反爬手段再升级

上面说的方法主要是针对数字做的反爬手段,如果要对汉字进行反爬怎么办?接下来提供几种方案

  1. 方案1: 对于你站点频率最高的词云,做一个汉字映射,也就是自定义字体文件,步骤跟数字一样。先将常用的汉字生成对应的 ttf 文件;根据下面提供的链接,将 ttf 文件转换为 svg 文件,然后在下面的“字体映射”链接点进去的网站上面选择前面生成的 svg 文件,将svg文件里面的每个汉字做个映射,也就是将汉字专为 unicode 码(注意这里的 unicode 码不要去在线直接生成,因为直接生成的东西也就是有规律的。我给的做法是先用网站生成,然后将得到的结果做个简单的变化,比如将“e342”转换为 “e231”);然后接口返回的数据按照我们的这个字体文件的规则反过去映射出来。
  2. 方案2: 将网站的重要字体,将 html 部分生成图片,这样子爬虫要识别到需要的内容成本就很高了,需要用到 OCR。效率也很低。所以可以拦截掉一部分的爬虫
  3. 方案3: 看到携程的技术分享“反爬的最高境界就是 Canvas 的指纹,原理是不同的机器不同的硬件对于 Canvas 画出的图总是存在像素级别的误差,因此我们判断当对于访问来说大量的 canvas 的指纹一致的话,则认为是爬虫,则可以封掉它”。

    本人将方案1实现到 Demo 中了。

关键步骤

  1. 先根据你们的产品找到常用的关键词,生成词云
  2. 根据词云,将每个字生成对应的 unicode 码
  3. 将词云包括的汉字做成一个字体库
  4. 将字体库 .ttf 做成 svg 格式,然后上传到 icomoon 制作自定义的字体,但是有规则,比如 “年” 对应的 unicode 码“u5e74” ,但是我们需要做一个 恺撒加密 ,比如我们设置 偏移量 为1,那么经过恺撒加密“年”对应的 unicode 码是“u5e75” 。利用这种规则制作我们需要的字体库
  5. 在每次调用接口的时候服务端做的事情是:服务端封装某个方法,将数据经过方法判断是不是在词云中,如果是词云中的字符,利用规则(找到汉字对应的 unicode 码,再根据凯撒加密,设置对应的偏移量,Demo 中为1,将每个汉字加密处理)加密处理后返回数据
  6. 客户端做的事情:

    • 先引入我们前面制作好的汉字字体库
    • 调用接口拿到数据,显示到对应的 Dom 节点上
    • 如果是汉字文本,我们将对应节点的 css 类设置成汉字类,该类对应的 font-family 是我们上面引入的汉字字体库
//style.css
@font-face {
  font-family: "NumberFont";
  src: url('http://127.0.0.1:8080/Util/analysis');
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

@font-face {
  font-family: "CharacterFont";
  src: url('http://127.0.0.1:8080/Util/map');
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

h2 {
  font-family: "NumberFont";
}

h3,a{
  font-family: "CharacterFont";
}

接口效果
审查元素效果

传送门

字体制作的步骤ttf转svg字体映射规则

实现的效果

  1. 页面上看到的数据跟审查元素看到的结果不一致
  2. 去查看接口数据跟审核元素和界面看到的三者不一致
  3. 页面每次刷新之前得出的结果更不一致
  4. 对于数字和汉字的处理手段都不一致

这几种组合拳打下来。对于一般的爬虫就放弃了。

数字反爬-网页显示效果、审查元素、接口结果情况1
数字反爬-网页显示效果、审查元素、接口结果情况2
数字反爬-网页显示效果、审查元素、接口结果情况3
数字反爬-网页显示效果、审查元素、接口结果情况4
汉字反爬-网页显示效果、审查元素、接口结果情况1
汉字反爬-网页显示效果、审查元素、接口结果情况2


前面的 ttf 转 svg 网站当 ttf 文件太大会限制转换,让你购买,下面贴出个新的链接。

ttf转svg

Demo 地址

效果演示

运行步骤

//客户端。先查看本机 ip 在 Demo/Spider-develop/Solution/Solution1.js 和 Demo/Spider-develop/Solution/Solution2.js  里面将接口地址修改为本机 ip

$ cd Demo
$ ls
REST        Spider-release    file-Server.js
Spider-develop    Util        rule.json
$ node file-Server.js 
Server is runnig at http://127.0.0.1:8080/

//服务端 先安装依赖
$ cd REST/
$ npm install
$ node app.js 

App 端安全的解决方案

  • 目前 App 的网络通信基本都是用 HTTPS 的服务,但是随便一个抓包工具都是可以看到 HTTPS 接口的详细数据,为了做到防止抓包和无法模拟接口的情况,我们采取以下措施:

    1. 中间人盗用数据,我们可以采取 HTTPS 证书的双向认证,这样子实现的效果就是中间人在开启抓包软件分析 App 的网络请求的时候,网络会自动断掉,无法查看分析请求的情况
    2. 对于防止用户模仿我们的请求再次发起请求,我们可以采用 「防重放策略」,用户再也无法模仿我们的请求,再次去获取数据了。
    3. 对于 App 内的 H5 资源,反爬虫方案可以采用上面的解决方案,H5 内部的网络请求可以通过 Hybrid 层让 Native 的能力去完成网络请求,完成之后将数据回调给 JS。这么做的目的是往往我们的 Native 层有完善的账号体系和网络层以及良好的安全策略、鉴权体系等等。
    4. 后期会讨论 App 安全性的更深层次玩法,比如从逆向的角度出发如何保护 App 的安全性。提前给出一篇逆向安全方面的文章

关于 Hybrid 的更多内容,可以看看这篇文章 Awesome Hybrid

  • 比如 JS 需要发起一个网络请求,那么按照上面将网络请求让 Native 去完成,然后回调给 JS

    JS 端代码

    var requestObject = {
      url: arg.Api + "SearchInfo/getLawsInfo",
      params: requestparams,
      Hybrid_Request_Method: 0
    };
    requestHybrid({
      tagname: 'NativeRequest',
      param: requestObject,
      encryption: 1,
      callback: function (data) {
        renderUI(data);
      }
    })

    Native 代码(iOS为例)

    [self.bridge registerHandler:@"NativeRequest" handler:^(id data, WVJBResponseCallback responseCallback) {
          
        NSAssert([data isKindOfClass:[NSDictionary class]], @"H5 端不按套路");
        if ([data isKindOfClass:[NSDictionary class]]) {
            
            NSDictionary *dict = (NSDictionary *)data;
            RequestModel *requestModel = [RequestModel yy_modelWithJSON:dict];
            NSAssert( (requestModel.Hybrid_Request_Method == Hybrid_Request_Method_Post) || (requestModel.Hybrid_Request_Method == Hybrid_Request_Method_Get ), @"H5 端不按套路");
            
            [HybridRequest requestWithNative:requestModel hybridRequestSuccess:^(id responseObject) {
                
                NSDictionary *json = [NSJSONSerialization JSONObjectWithData:responseObject options:NSJSONReadingMutableLeaves error:nil];
                responseCallback([self convertToJsonData:@{@"success":@"1",@"data":json}]);
                
            } hybridRequestfail:^{
                
                LBPLog(@"H5 call Native`s request failed");
                responseCallback([self convertToJsonData:@{@"success":@"0",@"data":@""}]);
            }];
        }
    }];

以上是第一阶段的安全性总结,后期应该会更新(App逆向、防重放、服务端等)。

查看原文

灯盏细辛 赞了文章 · 2018-01-02

JavaScript 优雅的实现方式包含你可能不知道的知识点

有些东西很好用,但是你未必知道;有些东西你可能用过,但是你未必知道原理。

实现一个目的有多种途径,俗话说,条条大路通罗马。很多内容来自平时的一些收集以及过往博客文章底下的精彩评论,收集整理拓展一波,发散一下大家的思维以及拓展一下知识面。

茴字有四种写法,233333..., 文末有彩蛋有惊喜。

1、简短优雅地实现 sleep 函数

很多语言都有 sleep 函数,显然 js 没有,那么如何能简短优雅地实现这个方法?

1.1 普通版

function sleep(sleepTime) {
    for(var start = +new Date; +new Date - start <= sleepTime;) {}
}
var t1 = +new Date()
sleep(3000)
var t2 = +new Date()
console.log(t2 - t1)

优点:简单粗暴,通俗易懂。
缺点:这是最简单粗暴的实现,确实 sleep 了,也确实卡死了,CPU 会飙升,无论你的服务器 CPU 有多么 Niubility。

1.2 Promise 版本

function sleep(time) {
  return new Promise(resolve => setTimeout(resolve, time))
}

const t1 = +new Date()
sleep(3000).then(() => {
  const t2 = +new Date()
  console.log(t2 - t1)
})

优点:这种方式实际上是用了 setTimeout,没有形成进程阻塞,不会造成性能和负载问题。
缺点:虽然不像 callback 套那么多层,但仍不怎么美观,而且当我们需要在某过程中需要停止执行(或者在中途返回了错误的值),还必须得层层判断后跳出,非常麻烦,而且这种异步并不是那么彻底,还是看起来别扭。

1.3 Generator 版本

function sleep(delay) {
  return function(cb) {
    setTimeout(cb.bind(this), delay)
  };
}

function* genSleep() {
  const t1 = +new Date()
  yield sleep(3000)
  const t2 = +new Date()
  console.log(t2 - t1)
}

async(genSleep)

function async(gen) {
  const iter = gen()
  function nextStep(it) {
    if (it.done) return
    if (typeof it.value === "function") {
      it.value(function(ret) {
        nextStep(iter.next(ret))
      })
    } else {
      nextStep(iter.next(it.value))
    }
  }
  nextStep(iter.next())
}

优点:同 Promise 优点,另外代码就变得非常简单干净,没有 then 那么生硬和恶心。
缺点:但不足也很明显,就是每次都要执行 next() 显得很麻烦,虽然有 co(第三方包)可以解决,但就多包了一层,不好看,错误也必须按 co 的逻辑来处理,不爽。

co 之所以这么火并不是没有原因的,当然不是仅仅实现 sleep 这么无聊的事情,而是它活生生的借着generator/yield 实现了很类似 async/await 的效果!这一点真是让我三观尽毁刮目相看。

const co = require("co")
function sleep(delay) {
  return function(cb) {
    setTimeout(cb.bind(this), delay)
  }
}

co(function*() {
  const t1 = +new Date()
  yield sleep(3000)
  const t2 = +new Date()
  console.log(t2 - t1)
})

1.4 Async/Await 版本

function sleep(delay) {
  return new Promise(reslove => {
    setTimeout(reslove, delay)
  })
}

!async function test() {
  const t1 = +new Date()
  await sleep(3000)
  const t2 = +new Date()
  console.log(t2 - t1)
}()

优点:同 Promise 和 Generator 优点。 Async/Await 可以看做是 Generator 的语法糖,Async 和 Await 相较于 * 和 yield 更加语义,另外各个函数都是扁平的,不会产生多余的嵌套,代码更加清爽易读。
缺点: ES7 语法存在兼容性问题,有 babel 一切兼容性都不是问题

至于 Async/AwaitPromiseGenerator 的好处可以参考这两篇文章:
Async/Await 比 Generator 的四个改进点
关于Async/Await替代Promise的6个理由

1.5 不要忘了开源的力量

在 javascript 优雅的写 sleep 等于如何优雅的不优雅,2333

这里有 C++ 实现的模块:https://github.com/ErikDubbel...
const sleep = require("sleep")

const t1 = +new Date()
sleep.msleep(3000)
const t2 = +new Date()
console.log(t2 - t1)

优点:能够实现更加精细的时间精确度,而且看起来就是真的 sleep 函数,清晰直白。
缺点:缺点需要安装这个模块,^_^,这也许算不上什么缺点。

从一个间简简单单的 sleep 函数我们就就可以管中窥豹,看见 JavaScript 近几年来不断快速的发展,不单单是异步编程这一块,还有各种各样的新技术和新框架,见证了 JavaScript 的繁荣。

你可能不知道的前端知识点:Async/Await是目前前端异步书写最优雅的一种方式

2、获取时间戳

上面第一个用多种方式实现 sleep 函数,我们可以发现代码有 +new Date()获取时间戳的用法,这只是其中的一种,下面就说一下其他两种以及 +new Date()的原理。

2.1 普通版

var timestamp=new Date().getTime()

优点:具有普遍性,大家都用这个
缺点:目前没有发现

2.2 进阶版

var timestamp = (new Date()).valueOf()
valueOf 方法返回对象的原始值(Primitive,'Null','Undefined','String','Boolean','Number'五种基本数据类型之一),可能是字符串、数值或 bool 值等,看具体的对象。

优点:说明开发者原始值有一个具体的认知,让人眼前一亮。
缺点: 目前没有发现

2.3 终极版

var timestamp = +new Date()

优点:对 JavaScript 隐式转换掌握的比较牢固的一个表现
缺点:目前没有发现

现在就简单分析一下为什么 +new Date() 拿到的是时间戳。

一言以蔽之,这是隐式转换的玄学,实质还是调用了 valueOf() 的方法。

我们先看看 ECMAScript 规范对一元运算符的规范:

一元+ 运算符

一元 + 运算符将其操作数转换为 Number 类型并反转其正负。注意负的 +0 产生 -0,负的 -0 产生 +0。产生式 UnaryExpression : - UnaryExpression 按照下面的过程执行。

  1. 令 expr 为解释执行 UnaryExpression 的结果 .
  2. 令 oldValue 为 ToNumber(GetValue(expr)).
  3. 如果 oldValue is NaN ,return NaN.
  4. 返回 oldValue 取负(即,算出一个数字相同但是符号相反的值)的结果。

+new Date() 相当于 ToNumber(new Date())

我们再来看看 ECMAScript 规范对 ToNumber 的定义:

我们知道 new Date() 是个对象,满足上面的 ToPrimitive(),所以进而成了 ToPrimitive(new Date())

接着我们再来看看 ECMAScript 规范对 ToPrimitive 的定义,一层一层来,抽丝剥茧。

这个 ToPrimitive 刚开始可能不太好懂,我来大致解释一下吧:

ToPrimitive(obj,preferredType)

JavaScript 引擎内部转换为原始值 ToPrimitive(obj,preferredType) 函数接受两个参数,第一个 obj 为被转换的对象,第二个preferredType 为希望转换成的类型(默认为空,接受的值为 NumberString

在执行 ToPrimitive(obj,preferredType) 时如果第二个参数为空并且 objDate 的实例时,此时 preferredType 会被设置为 String,其他情况下 preferredType 都会被设置为Number 如果 preferredTypeNumberToPrimitive 执行过程如下:

  1. 如果obj为原始值,直接返回;
  2. 否则调用 obj.valueOf(),如果执行结果是原始值,返回之;
  3. 否则调用 obj.toString(),如果执行结果是原始值,返回之;
  4. 否则抛异常。

如果 preferredTypeString,将上面的第2步和第3步调换,即:

  1. 如果obj为原始值,直接返回;
  2. 否则调用 obj.toString(),如果执行结果是原始值,返回之;
  3. 否则调用 obj.valueOf(),如果执行结果是原始值,返回之;
  4. 否则抛异常。

首先我们要明白 obj.valueOf()obj.toString() 还有原始值分别是什么意思,这是弄懂上面描述的前提之一:

toString 用来返回对象的字符串表示。

var obj = {};
console.log(obj.toString());//[object Object]

var arr2 = [];
console.log(arr2.toString());//""空字符串

var date = new Date();
console.log(date.toString());//Sun Feb 28 2016 13:40:36 GMT+0800 (中国标准时间)

valueOf 方法返回对象的原始值,可能是字符串、数值或 bool 值等,看具体的对象。

var obj = {
  name: "obj"
}
console.log(obj.valueOf()) //Object {name: "obj"}

var arr1 = [1]
console.log(arr1.valueOf()) //[1]

var date = new Date()
console.log(date.valueOf())//1456638436303
如代码所示,三个不同的对象实例调用valueOf返回不同的数据

原始值指的是 'Null','Undefined','String','Boolean','Number','Symbol' 6种基本数据类型之一,上面已经提到过这个概念,这里再次申明一下。

最后分解一下其中的过程:+new Date():

  1. 运算符 new 的优先级高于一元运算符 +,所以过程可以分解为:

var time=new Date();
+time

  1. 根据上面提到的规则相当于:ToNumber(time)
  2. time 是个日期对象,根据 ToNumber 的转换规则,所以相当于:ToNumber(ToPrimitive(time))
  3. 根据 ToPrimitive 的转换规则:ToNumber(time.valueOf())time.valueOf() 就是 原始值 得到的是个时间戳,假设 time.valueOf()=1503479124652
  4. 所以 ToNumber(1503479124652) 返回值是 1503479124652 这个数字。
  5. 分析完毕

你可能不知道的前端知识点:隐式转换的妙用

3、数组去重

注:暂不考虑对象字面量,函数等引用类型的去重,也不考虑 NaN, undefined, null等特殊类型情况。

数组样本:[1, 1, '1', '2', 1]
3.1 普通版

无需思考,我们可以得到 O(n^2) 复杂度的解法。定义一个变量数组 res 保存结果,遍历需要去重的数组,如果该元素已经存在在 res 中了,则说明是重复的元素,如果没有,则放入 res 中。

var a = [1, 1, '1', '2', 1]
function unique(arr) {
    var res = []
    for (var i = 0, len = arr.length; i < len; i++) {
        var item = arr[i]
        for (var j = 0, len = res.length; j < jlen; j++) {
            if (item === res[j]) //arr数组的item在res已经存在,就跳出循环
                break
        }
        if (j === jlen) //循环完毕,arr数组的item在res找不到,就push到res数组中
            res.push(item)
    }
    return res
}
console.log(unique(a)) // [1, 2, "1"]

优点: 没有任何兼容性问题,通俗易懂,没有任何理解成本
缺点: 看起来比较臃肿比较繁琐,时间复杂度比较高O(n^2)

3.2 进阶版

var a =  [1, 1, '1', '2', 1]
function unique(arr) {
    return arr.filter(function(ele,index,array){
        return array.indexOf(ele) === index//很巧妙,这样筛选一对一的,过滤掉重复的
    })
}
console.log(unique(a)) // [1, 2, "1"]

优点:很简洁,思维也比较巧妙,直观易懂。
缺点:不支持 IE9 以下的浏览器,时间复杂度还是O(n^2)

3.3 时间复杂度为O(n)

var a =  [1, 1, '1', '2', 1]
function unique(arr) {
    var obj = {}
    return arr.filter(function(item, index, array){
        return obj.hasOwnProperty(typeof item + item) ? 
        false : 
        (obj[typeof item + item] = true)
    })
}

console.log(unique(a)) // [1, 2, "1"]

优点:hasOwnProperty 是对象的属性(名称)存在性检查方法。对象的属性可以基于 Hash 表实现,因此对属性进行访问的时间复杂度可以达到O(1);
filter 是数组迭代的方法,内部还是一个 for 循环,所以时间复杂度是 O(n)
缺点:不兼容 IE9 以下浏览器,其实也好解决,把 filter 方法用 for 循环代替或者自己模拟一个 filter 方法。

3.4 终极版

以 Set 为例,ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。
const unique = a => [...new Set(a)]

优点:ES6 语法,简洁高效,我们可以看到,去重方法从原始的 14 行代码到 ES61 行代码,其实也说明了 JavaScript 这门语言在不停的进步,相信以后的开发也会越来越高效。
缺点:兼容性问题,现代浏览器才支持,有 babel 这些都不是问题。

你可能不知道的前端知识点:ES6 新的数据结构 Set 去重

4、数字格式化 1234567890 --> 1,234,567,890

4.1 普通版

function formatNumber(str) {
  let arr = [],
    count = str.length

  while (count >= 3) {
    arr.unshift(str.slice(count - 3, count))
    count -= 3
  }

  // 如果是不是3的倍数就另外追加到上去
  str.length % 3 && arr.unshift(str.slice(0, str.length % 3))

  return arr.toString()

}
console.log(formatNumber("1234567890")) // 1,234,567,890

优点:自我感觉比网上写的一堆 for循环 还有 if-else 判断的逻辑更加清晰直白。
缺点:太普通

4.2 进阶版

function formatNumber(str) {

  // ["0", "9", "8", "7", "6", "5", "4", "3", "2", "1"]
  return str.split("").reverse().reduce((prev, next, index) => {
    return ((index % 3) ? next : (next + ',')) + prev
  })
}

console.log(formatNumber("1234567890")) // 1,234,567,890

优点:把 JS 的 API 玩的了如指掌
缺点:可能没那么好懂,不过读懂之后就会发出我怎么没想到的感觉

4.3 正则版

function formatNumber(str) {
  return str.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
}

console.log(formatNumber("123456789")) // 1,234,567,890

下面简单分析下正则/\B(?=(\d{3})+(?!\d))/g

  1. /\B(?=(\d{3})+(?!\d))/g:正则匹配边界\B,边界后面必须跟着(\d{3})+(?!\d);
  2. (\d{3})+:必须是1个或多个的3个连续数字;
  3. (?!\d):第2步中的3个数字不允许后面跟着数字;
  4. (\d{3})+(?!\d):所以匹配的边界后面必须跟着3*n(n>=1)的数字。

最终把匹配到的所有边界换成,即可达成目标。

优点:代码少,浓缩的就是精华
缺点:需要对正则表达式的位置匹配有一个较深的认识,门槛大一点

4.4 API版

(123456789).toLocaleString('en-US')  // 1,234,567,890

如图,你可能还不知道 JavaScripttoLocaleString 还可以这么玩。

还可以使用 Intl对象 - MDN

Intl 对象是 ECMAScript 国际化 API 的一个命名空间,它提供了精确的字符串对比,数字格式化,日期和时间格式化。Collator,NumberFormat 和 DateTimeFormat 对象的构造函数是 Intl 对象的属性。
new Intl.NumberFormat().format(1234567890) // 1,234,567,890

优点:简单粗暴,直接调用 API
缺点:Intl兼容性不太好,不过 toLocaleString的话 IE6 都支持

你可能不知道的前端知识点:Intl对象 和 toLocaleString的方法。

5、交换两个整数

let a = 3,b = 4 变成 a = 4, b = 3

5.1 普通版

首先最常规的办法,引入一个 temp 中间变量

let a = 3,b = 4
let temp = a
a = b
b = temp
console.log(a, b)

优点:一眼能看懂就是最好的优点
缺点:硬说缺点就是引入了一个多余的变量

5.2 进阶版

在不引入中间变量的情况下也能交互两个变量

let a = 3,b = 4
a += b
b = a - b
a -= b
console.log(a, b)

优点:比起楼上那种没有引入多余的变量,比上面那一种稍微巧妙一点
缺点:当然缺点也很明显,整型数据溢出,比如说对于32位字符最大表示有符号数字是2147483647,也就是Math.pow(2,31)-1,如果是2147483645和2147483646交换就失败了。

5.3 终极版

利用一个数异或本身等于0和异或运算符合交换率。

let a = 3,b = 4
  a ^= b
  b ^= a
  a ^= b
console.log(a, b)

下面用竖式进行简单说明:(10进制化为二进制)

    a = 011
(^) b = 100
则  a = 111(a ^ b的结果赋值给a,a已变成了7)
(^) b = 100
则  b = 011(b^a的结果赋给b,b已经变成了3)
(^) a = 111
则  a = 100(a^b的结果赋给a,a已经变成了4)

从上面的竖式可以清楚的看到利用异或运算实现两个值交换的基本过程。

下面从深层次剖析一下:

1.对于开始的两个赋值语句,a = a ^ b,b = b ^ a,相当于b = b ^ (a ^ b) = a ^ b ^ b,而b ^ b 显然等于0。因此b = a ^ 0,显然结果为a。

  1. 同理可以分析第三个赋值语句,a = a ^ b = (a ^ b) ^ a = b

注:

  1. ^ 即”异或“运算符。
它的意思是判断两个相应的位值是否为”异“,为”异"(值不同)就取真(1);否则为假(0)。
  1. ^ 运算符的特点是与0异或,保持原值;与本身异或,结果为0。

优点:不存在引入中间变量,不存在整数溢出
缺点:前端对位操作这一块可能了解不深,不容易理解

5.4 究极版

熟悉 ES6 语法的人当然不会对解构陌生

var a = 3,b = 4;
[b, a] = [a, b]

其中的解构的原理,我暂时还没读过 ES6的规范,不知道具体的细则,不过我们可以看看 babel 是自己编译的,我们可以看出点门路。

哈哈,简单粗暴,不知道有没有按照 ES 的规范,其实可以扒一扒 v8的源码,chrome 已经实现这种解构用法。

这个例子和前面的例子编写风格有何不同,你如果细心的话就会发现这两行代码多了一个分号,对于我这种编码不写分号的洁癖者,为什么加一个分号在这里,其实是有原因的,这里就简单普及一下,建议大家还是写代码写上分号

5.4 ECMAScript 自动分号;插入(作为补充,防止大家以后踩坑)

尽管 JavaScript 有 C 的代码风格,但是它不强制要求在代码中使用分号,实际上可以省略它们。

JavaScript 不是一个没有分号的语言,恰恰相反上它需要分号来就解析源代码。 因此 JavaScript 解析器在遇到由于缺少分号导致的解析错误时,会自动在源代码中插入分号。

5.4.1例子
var foo = function() {
} // 解析错误,分号丢失
test()

自动插入分号,解析器重新解析。

var foo = function() {
}; // 没有错误,解析继续
test()
5.4.2工作原理

下面的代码没有分号,因此解析器需要自己判断需要在哪些地方插入分号。

(function(window, undefined) {
    function test(options) {
        log('testing!')

        (options.list || []).forEach(function(i) {

        })

        options.value.test(
            'long string to pass here',
            'and another long string to pass'
        )

        return
        {
            foo: function() {}
        }
    }
    window.test = test

})(window)

(function(window) {
    window.someLibrary = {}
})(window)

下面是解析器"猜测"的结果。

(function(window, undefined) {
    function test(options) {

        // 没有插入分号,两行被合并为一行
        log('testing!')(options.list || []).forEach(function(i) {

        }); // <- 插入分号

        options.value.test(
            'long string to pass here',
            'and another long string to pass'
        ); // <- 插入分号

        return; // <- 插入分号, 改变了 return 表达式的行为
        { // 作为一个代码段处理
            foo: function() {}
        }; // <- 插入分号
    }
    window.test = test; // <- 插入分号

// 两行又被合并了
})(window)(function(window) {
    window.someLibrary = {}; // <- 插入分号
})(window); //<- 插入分号

解析器显著改变了上面代码的行为,在另外一些情况下也会做出错误的处理。

5.4.3 ECMAScript对自动分号插入的规则

我们翻到7.9章节,看看其中插入分号的机制和原理,清楚只写以后就可以尽量以后少踩坑

必须用分号终止某些 ECMAScript 语句 ( 空语句 , 变量声明语句 , 表达式语句 , do-while 语句 , continue 语句 , break 语句 , return 语句 ,throw 语句 )。这些分号总是明确的显示在源文本里。然而,为了方便起见,某些情况下这些分号可以在源文本里省略。描述这种情况会说:这种情况下给源代码的 token 流自动插入分号。



还是比较抽象,看不太懂是不是,不要紧,我们看看实际例子,总结出几个规律就行,我们先不看抽象的,看着头晕,看看具体的总结说明, 化抽象为具体

首先这些规则是基于两点:

  1. 以换行为基础;
  2. 解析器会尽量将新行并入当前行,当且仅当符合ASI规则时才会将新行视为独立的语句。
5.4.3.1 ASI的规则

1. 新行并入当前行将构成非法语句,自动插入分号。

if(1 < 10) a = 1
console.log(a)
// 等价于
if(1 < 10) a = 1;
console.log(a);

2. 在continue,return,break,throw后自动插入分号

return
{a: 1}
// 等价于
return;
{a: 1};

3. ++、--后缀表达式作为新行的开始,在行首自动插入分号

a
++
c
// 等价于
a;
++c;

4. 代码块的最后一个语句会自动插入分号

function(){ a = 1 }
// 等价于
function(){ a = 1; }
5.4.3.2 No ASI的规则

1. 新行以 ( 开始

var a = 1
var b = a
(a+b).toString()
// 会被解析为以a+b为入参调用函数a,然后调用函数返回值的toString函数
var a = 1
var b =a(a+b).toString()

2. 新行以 [ 开始

var a = ['a1', 'a2']
var b = a
[0,1].slice(1)
// 会被解析先获取a[1],然后调用a[1].slice(1)。
// 由于逗号位于[]内,且不被解析为数组字面量,而被解析为运算符,而逗号运算符会先执
行左侧表达式,然后执行右侧表达式并且以右侧表达式的计算结果作为返回值
var a = ['a1', 'a2']
var b = a[0,1].slice(1)

3. 新行以 / 开始

var a = 1
var b = a
/test/.test(b)
// /会被解析为整除运算符,而不是正则表达式字面量的起始符号。浏览器中会报test前多了个.号
var a = 1
var b = a / test / .test(b)

4. 新行以 + 、 - 、 % 和 * 开始

var a = 2
var b = a
+a
// 会解析如下格式
var a = 2
var b = a + a

5. 新行以 , 或 . 开始

var a = 2
var b = a
.toString()
console.log(typeof b)
// 会解析为
var a = 2
var b = a.toString()
console.log(typeof b)

到这里我们已经对ASI的规则有一定的了解了,另外还有一样有趣的事情,就是“空语句”。

// 三个空语句
;;;

// 只有if条件语句,语句块为空语句。
// 可实现unless条件语句的效果
if(1>2);else
  console.log('2 is greater than 1 always!');

// 只有while条件语句,循环体为空语句。
var a = 1
while(++a < 100);
5.4.4 结论

建议绝对不要省略分号,同时也提倡将花括号和相应的表达式放在一行, 对于只有一行代码的 if 或者 else 表达式,也不应该省略花括号。 这些良好的编程习惯不仅可以提到代码的一致性,而且可以防止解析器改变代码行为的错误处理。
关于JavaScript 语句后应该加分号么?(点我查看)我们可以看看知乎上大牛们对着个问题的看法。

你可能不知道的前端知识点:原来 JavaScript 还有位操作以及分号的使用细则

6、将 argruments 对象(类数组)转换成数组

{0:1,1:2,2:3,length:3}这种形式的就属于类数组,就是按照数组下标排序的对象,还有一个 length 属性,有时候我们需要这种对象能调用数组下的一个方法,这时候就需要把把类数组转化成真正的数组。

6.1 普通版

var makeArray = function(array) {
  var ret = []
  if (array != null) {
    var i = array.length
    if (i == null || typeof array === "string") ret[0] = array
    else while (i) ret[--i] = array[i];
  }
  return ret
}
makeArray({0:1,1:2,2:3,length:3}) //[1,2,3]

优点:通用版本,没有任何兼容性问题
缺点:太普通

6.2 进阶版

var arr = Array.prototype.slice.call(arguments);

这种应该是大家用过最常用的方法,至于为什么可以这么用,很多人估计也是一知半解,反正我看见大家这么用我也这么用,要搞清为什么里面的原因,我们还是从规范和源码说起。

照着规范的流程,自己看看推演一下就明白了:
英文版15.4.4.10 Array.prototype.slice (start, end)
中文版15.4.4.10 Array.prototype.slice (start, end)
如果你想知道 JavaScriptsort 排序的机制,到底是哪种排序好,用的哪种,也可以从规范看出端倪。

在官方的解释中,如[mdn]

The slice() method returns a shallow copy of a portion of an array into a new array object.

简单的说就是根据参数,返回数组的一部分的 copy。所以了解其内部实现才能确定它是如何工作的。所以查看 V8 源码中的 Array.js 可以看到如下的代码:

方法 ArraySlice源码地址,第 660 行,直接添加到 Array.prototype 上的“入口”,内部经过参数、类型等等的判断处理,分支为 SparseSliceSimpleSlice 处理。

slice.call 的作用原理就是,利用 call,将 slice 的方法作用于 arrayLikeslice 的两个参数为空,slice 内部解析使得 arguments.lengt 等于0的时候 相当于处理 slice(0) : 即选择整个数组,slice 方法内部没有强制判断必须是 Array 类型,slice 返回的是新建的数组(使用循环取值)”,所以这样就实现了类数组到数组的转化,call 这个神奇的方法、slice 的处理缺一不可。

直接看 slice 怎么实现的吧。其实就是将 array-like 对象通过下标操作放进了新的 Array 里面:

// This will work for genuine arrays, array-like objects, 
    // NamedNodeMap (attributes, entities, notations),
    // NodeList (e.g., getElementsByTagName), HTMLCollection (e.g., childNodes),
    // and will not fail on other DOM objects (as do DOM elements in IE < 9)
    Array.prototype.slice = function(begin, end) {
      // IE < 9 gets unhappy with an undefined end argument
      end = (typeof end !== 'undefined') ? end : this.length;

      // For native Array objects, we use the native slice function
      if (Object.prototype.toString.call(this) === '[object Array]'){
        return _slice.call(this, begin, end); 
      }

      // For array like object we handle it ourselves.
      var i, cloned = [],
        size, len = this.length;

      // Handle negative value for "begin"
      var start = begin || 0;
      start = (start >= 0) ? start : Math.max(0, len + start);

      // Handle negative value for "end"
      var upTo = (typeof end == 'number') ? Math.min(end, len) : len;
      if (end < 0) {
        upTo = len + end;
      }

      // Actual expected size of the slice
      size = upTo - start;

      if (size > 0) {
        cloned = new Array(size);
        if (this.charAt) {
          for (i = 0; i < size; i++) {
            cloned[i] = this.charAt(start + i);
          }
        } else {
          for (i = 0; i < size; i++) {
            cloned[i] = this[start + i];
          }
        }
      }

      return cloned;
    };

优点:最常用的版本,兼容性较强
缺点:ie 低版本,无法处理 dom 集合的 slice call 转数组。(虽然具有数值键值、length 符合ArrayLike 的定义,却报错)搜索资料得到 :因为 ie 下的 dom 对象是以 com 对象的形式实现的,js 对象与com对象不能进行转换 。

6.3 ES6 版本

使用 Array.from, 值需要对象有 length 属性, 就可以转换成数组

var arr = Array.from(arguments);

扩展运算符

var args = [...arguments];

ES6 中的扩展运算符...也能将某些数据结构转换成数组,这种数据结构必须有便利器接口。
优点:直接使用内置 API,简单易维护
缺点:兼容性,使用 babel 的 profill 转化可能使代码变多,文件包变大

你可能不知道的前端知识点:slice 方法的具体原理

7、数字取整 2.33333 => 2

7.1 普通版

const a = parseInt(2.33333)

parseInt() 函数解析一个字符串参数,并返回一个指定基数的整数 (数学系统的基础)。这个估计是直接取整最常用的方法了。
更多关于 parseInt() 函数可以查看 MDN 文档

7.2 进阶版

const a = Math.trunc(2.33333)

Math.trunc() 方法会将数字的小数部分去掉,只保留整数部分。
特别要注意的是:Internet Explorer 不支持这个方法,不过写个 Polyfill 也很简单:

Math.trunc = Math.trunc || function(x) {
  if (isNaN(x)) {
    return NaN;
  }
  if (x > 0) {
    return Math.floor(x);
  }
  return Math.ceil(x);
};

数学的事情还是用数学方法来处理比较好。

7.3 黑科技版

7.3.1 ~~number

双波浪线 ~~ 操作符也被称为“双按位非”操作符。你通常可以使用它作为代替 Math.trunc() 的更快的方法。

console.log(~~47.11)  // -> 47
console.log(~~1.9999) // -> 1
console.log(~~3)      // -> 3
console.log(~~[])     // -> 0
console.log(~~NaN)    // -> 0
console.log(~~null)   // -> 0

失败时返回0,这可能在解决 Math.trunc() 转换错误返回 NaN 时是一个很好的替代。
但是当数字范围超出 ±2^31−1 即:2147483647 时,异常就出现了:

// 异常情况
console.log(~~2147493647.123) // -> -2147473649 ?
7.3.2 number | 0

| (按位或) 对每一对比特位执行或(OR)操作。

console.log(20.15|0);          // -> 20
console.log((-20.15)|0);       // -> -20
console.log(3000000000.15|0);  // -> -1294967296 ?
7.3.3 number ^ 0

^ (按位异或),对每一对比特位执行异或(XOR)操作。

console.log(20.15^0);          // -> 20
console.log((-20.15)^0);       // -> -20
console.log(3000000000.15^0);  // -> -1294967296 ?
7.3.4 number << 0

<< (左移) 操作符会将第一个操作数向左移动指定的位数。向左被移出的位被丢弃,右侧用 0 补充。

console.log(20.15 < < 0);     // -> 20
console.log((-20.15) < < 0);  //-20
console.log(3000000000.15 << 0);  // -> -1294967296 ?

上面这些按位运算符方法执行很快,当你执行数百万这样的操作非常适用,速度明显优于其他方法。但是代码的可读性比较差。还有一个特别要注意的地方,处理比较大的数字时(当数字范围超出 ±2^31−1 即:2147483647),会有一些异常情况。使用的时候明确的检查输入数值的范围。

8、数组求和

8.1 普通版

let arr = [1, 2, 3, 4, 5]
function sum(arr){
    let x = 0
    for(let i = 0; i < arr.length; i++){
        x += arr[i]
    }
    return x
}
sum(arr) // 15

优点:通俗易懂,简单粗暴
缺点:没有亮点,太通俗

8.2 优雅版

let arr = [1, 2, 3, 4, 5]
function sum(arr) {
return arr.reduce((a, b) => a + b)
}
sum(arr) //15

优点:简单明了,数组迭代器方式清晰直观
缺点:不兼容 IE 9以下浏览器

8.3 终极版

let arr = [1, 2, 3, 4, 5]
function sum(arr) {
return eval(arr.join("+"))
}
sum(arr) //15

优点:让人一时看不懂的就是"好方法"。
缺点:

eval 不容易调试。用 chromeDev 等调试工具无法打断点调试,所以麻烦的东西也是不推荐使用的…

性能问题,在旧的浏览器中如果你使用了eval,性能会下降10倍。在现代浏览器中有两种编译模式:fast path和slow path。fast path是编译那些稳定和可预测(stable and predictable)的代码。而明显的,eval 不可预测,所以将会使用 slow path ,所以会慢。

更多关于 eval 的探讨可以关注这篇文章: JavaScript 为什么不推荐使用 eval?

你可能不知道的前端知识点:eval的使用细则

最后

祝大家圣诞快乐?,欢迎补充和交流。


查看原文

赞 34 收藏 75 评论 14

灯盏细辛 回答了问题 · 2017-10-24

display:table很好用,而且能实现垂直自适应的效果,兼容性强于flex,怎么使用率这么低呢?

table 对结构的限制太死了,必须是 tr td 复杂布局还要算 rowspan 和 colspan,虽然可以用 thead tbody th 这些,但远远不够用阿

如果你用 vue / React 自己拼过表格,就知道这是多么的痛了。。。

关注 9 回答 7

灯盏细辛 赞了回答 · 2017-09-26

解决为什么window.onload比$(document).ready()先执行?

顺着问题,我自己尝试了一下。
应该是jquery3更新的问题。

恰巧也找了一个issues 讨论的这个。
机票:https://github.com/jquery/jqu...

关注 9 回答 9

灯盏细辛 赞了文章 · 2017-09-20

去哪儿网迷你React的研发心得

去哪儿网迷你React是年初立项的新作品,在这前,去哪儿网已经深耕多年,拥有QRN(react-native的公司制定版),HY(基于React的hybird方案), yo(基于React的移动UI库),QRN-web(基于React的三端合一移植方案),此外,像机票等部门也大规模将React用于前台页面,后台页面就更不在话下。

如此广泛地应用React,我们熟晓其优缺点。优点是代码的可维护性大大提高,性能卓然!但缺点也明显,由于体积太大,React.js+React-DOM.js超过3万行,体量过3MB,已经加上immutable.js , redux, redux-react, react-router等全家桶,工程师一行代码没有写,已经好几MB了。这在过去绝对不可想象,要知道,体积意味着流量,流量代表着金钱,越大越烧钱,越大下载速度越慢,用户体验越差,用户就会流失。现在问题摆在我们面前,我们就得解决。虽然webpack官网有各种瘦身方案,但瘦死的大象也比马大。这是一个如此普遍存在的问题,因此外国人肯定也遇到过,思考过,提出什么新点子——所有这一切,都指向一个名词,“迷你React”。

作为一个生态圈成熟的标志,一个库出名了,就各种偏门补丁,闪光效果往上加,库难免会膨胀,不爽的人就会推出迷你化方案。上一个世代是jQuery与zepto。React的迷你方案也有不少,preact, inferno, react-lite, dio, rax……

clipboard.png

但问题不是简单到直接从github找一个迷你React库替换上就能搞定。之所以称为迷你,肯定有所欠缺,如果是内部实现的改良也罢,如果阉割了功能可不是闹着玩。恰恰他们就好这口,因此现有方案不能满足我们。我们需要一个能直接替换,或至少95%的业务代码不用动。这意味着这迷你框架,需要与React的接口完全一致,并且全面支持它的全家桶。当然细化一下来的需来就更多了,早期说只要支持移动,因此取名为react-mobile,后来连iE8也要支持,因此这活不能指望别人,我们自己动手撸了。

clipboard.png

我们先来一趟竞品分析。为了加快产出速度,能借鉴的尽可能借鉴。React推出以后,针对其性能研究衍生出不少库,针对其体积也诞生出大量仿品。它比jQuery更加缤纷多采。市面上竟然拥有100多个虚拟DOM库。虚拟DOM库,就是React出来后的一种新式库,以虚拟DOM与diff算法为核心,屏蔽DOM操作,操作数据即操作视图。这听起来有点像MVVM,MVVM也是屏蔽DOM操作,操作数据即操作视图,但它是以VM为核心。

clipboard.png

React及其他虚拟DOM库已经将虚拟DOM的生成交由JSX与babel处理了,因此不同点是,虚拟DOM的结构与diff算法。虚拟DOM万宗不离其变是三大属性,type, props, children,当然也可以改一下别名,babel可以做相应配置。此外,虚拟DOM可以加入更多冗余标识,以帮diff算法的改良。

React最初推出时也不火,那时的招牌与现在性能不什么两样,也是高性能。但是JSX离经背道,与业界宣扬了多年的前后端分离,数据结构样式分离等教条差太远了,一直默默在角落里画圈。直到RN出来,解决原生编写界面的痛苦才一炮而红。大家才留意它的性能,它的性能背后的diff算法。最早研究React的diff算法是virtual-dom这个库,是基于经典的DFS算法。后来相应的算法就多起来。最后才是从接口进行模拟,就是所谓的React-like框架。因此虚拟DOM库是分为两大派系:算法派与拟态派。

clipboard.png

下面将从这两大派系进行扼要的描述。

将前端回归到算法上的探索是前端框架史上的一个巨大进步。之前是MVVM将数据从繁复的DOM操作分离出来。正因为有了纯数据,并且数据结构可控,那么算法才有发挥的余地。

最开始出现的是 virtual-dom这个库,是大家好奇React为什么这么快而搞鼓出来的。它的实现是非常学院风格,通过深度优先搜索与in-order tree来实现高效的diff。它与React后来公开出来的算法是很不一样。

然后是cito.js的横空出世,它对今后所有虚拟DOM的算法都有重大影响。它采用两端同时进行比较的算法,将diff速度拉高到几个层次。

紧随其后的是kivi.js,在cito.js的基出提出两项优化方案,使用key实现移动追踪及基于key的编辑长度矩离算法应用(算法复杂度 为O(n^2))。

但这样的diff算法太过复杂了,于是后来者snabbdom将kivi.js进行简化,去掉编辑长度矩离算法,调整两端比较算法。速度略有损失,但可读性大大提高。再之后,就是著名的vue2.0 把sanbbdom整个库整合掉了。

当然算法派的老大是inferno,它使用多种优化方案将性能提至最高,因此其作者便邀请进react core team,负责react的性能优化了。这个我后面会详细。

clipboard.png

再看拟态派。React的接口并不多,但是其组件的实现是相当有难度。它的生命周期是如何运作,需要对源码有深刻的理解,因此它们出来得比较晚。我们的学习对象也就是它们几个。

clipboard.png

先说虚拟DOM。虚拟DOM就是一个普通的JS对象,通常拥有三个属性,type, props, children。但无状态组件出来后,children改放到props中。此外,有些元素还有ref属性,可以是字符串与函数。在数组里,为了提高diff速度,又多出了key属性。bable会将JSX这些属性转换为一个VNode对象。这是虚拟DOM的最小单元。所有虚拟DOM会组成一棵树,叫虚拟DOM树。

为了防止每次都是整个树进行diff,需要形成子树的概念,于是出现组件了。组件有render方法,会返回一个虚拟DOM,这个虚拟DOM及其子孙,就形成一棵子树。但render方法不是虚拟DOM的东西,于是我们规定当虚拟DOM为一个函数时,如果这个函数继承于React.Component,这个方法的实例必须有render方法。于是我们就像虚拟DOM与组件统合起来了。或者衍生这两个称呼,原子虚拟DOM与组件虚拟DOM。原子虚拟DOM对应元素节点,而组件则是用来产出原子虚拟DOM。此外,原子虚拟DOM还能包含一些东西,字符串与数字与null。字符串与数字对应文本节点,null对应注释节点。

clipboard.png

一个组件虚拟DOM实例化为组件后,会返回原子虚拟DOM或另一个组件虚拟DOM。这就形成函数式编程上的高阶函数的机制,因此进行出无状态函数组件,就是虚拟DOM的type属性就是一个函数,不继承其他东西了。

组件虚拟DOM的实例化过程是非常复杂,如果能简化这过程,简化其结构,这性能就上去了。

clipboard.png

此外组件的实例本身就巨耗性能,因此官方推荐页面的结构如下,通过最少量的有状态组件(smart component)控制无状态组件(dumb component)的变化,所有状态通过redux在路由进行分发。

clipboard.png

经过一番比较后,我们着手开发自己的迷你React。这个过程也比较坎坷,这还是有前人参照物的情况下,可想而知,当初facebook开发出React这样一个独行特立的框架时,是多么艰辛。我们在这半年总共搞了三个东西,第一还孵化失败。

clipboard.png

最初是基于react-lite,考虑时当时是我们母公司的人搞的,方便交流。但后来发现它的事件系统太鸡肋,难以扩展,最后放弃了。

第二代是基于preact,代号qreact, 国内也有许多公司基于它做自己的迷你化方案,因为官方提供了一个preact-compat的模块。但是preact-compat是使用Object.defineProperty来实现一些属性名映射与同步的,因此不支持IE8,并且使用了Object.defineProperty会严重拖慢速度。preact本身也有不少BUG,最著名的有三个,生命周期的unmount钩子不能保证在mount之前执行,元素节点的重复利用没有清理样式会导致出错,同一组孩子下可能存在同名的key导致排序失败。这些我们都为官方提issue,并且在我们的版本中进行修复了。第二代也公司内部几个项目中试水落地,反映不错。

第三代是我们团队独力开发,a内部代号anu,正式名称仍然是 QReact,不过演进到了 QReact 1.0 了 (现在的版本使用了 1.0 大版本,因为之前这个版本没被占用,所以没跳到 2.0)。在对preact缝缝补补的过程,掌握不少核心知识,新的框架是使用全新的算法,全新的结构。由于不使用高级API,理论上能支持到IE6,但我们公司只需支持到IE8。

Qreact1.0使用requestAnimationFrame来稳定它的运行帧数,保证在60帧每秒的流畅速度。由于bable会对type进行打补丁,内部统一用typeNumber代替type进行类型判定。使用列队保证生命周期钩子按顺序执行。使用__rerender标识一个组件在一次大的更新只会被render一次。凡此种种,经过大量测试,它们的接口与React别无二致,甚至React废弃的接口createClass, PropTypes,我们都有相应的polyfill。

下面是Qreact1.0的测试数据。
从性能上,直追preact。

clipboard.png

从体积上,是官方react的1/4至1/5之间。

clipboard.png

这里透露一下React性能爆表的秘密,除了diff算法外,setState的合并操作也是一个关键。当组件没有插入到DOM树前,用户在componentWillMount方法多次执行setState,这些state是不会触发更新,而是存放到一个数组中,然后在render方法里进行合并与应用。当一个组件进行更新,可能是用户在componentDidMount或者事件回调执行setState,这时更新是即时的,同步的,但这之次再setState,它就不会更新了,它的state会进入列队。此外,如果用户在componentWillReceiveProps多次执行setState,也会产生延迟。React这种行为是保证页面的更新次数最少,同时用户不会察觉它没有更新。它只是将state进行了合并。Qreact1.0是完全遵循了这些规则,从而实现了高性能。

而在体积上,则通过删除一些对线上没用的代码实现迷你化效果。

clipboard.png

但仅是这样不足以大吹特吹了。为了超越React的性能,从inferno与preact借签了不少手段。

clipboard.png

最值得一提的是hydrate机制,通过合并相邻字符串,从而减少文本节点的生成,从而减少diff次数。

clipboard.png

最后还有一个压轴大戏,不做测试不知道,原来高级API是如何耗性能。通过去掉Object.freeze, Object.defineProperty这些es5方法, 框架就有10帧的提升!

clipboard.png

说了这么多,来些实际可运行的例子吧。目前,Qreact跑通内部几套测试,已经在金融与大搜的项目使用。它的第二版也在机票一个拥有820个模块的大项目中试水,在IE8下良好运行。

clipboard.png

如果大家想在项目中使用qreact,可以在webpack或ykit的config中如下配置:


resolve: {
   alias: {
      'react': 'anujs',
      'react-dom': 'anujs',
        // 若要兼容 IE 请使用以下配置
        // 'react': 'anujs/dist/ReactIE',
        // 'react-dom': 'anujs/dist/ReactIE',
    
        // 如果引用了 prop-types 或 create-react-class
        // 需要添加如下别名
        'prop-types': 'anujs/lib/ReactPropTypes',
        'create-react-class': 'anujs/lib/createClass'
        //如果你在移动端用到了onTouchTap事件
        'react-tap-event-plugin': 'anujs/lib/injectTapEventPlugin',  
   }
},

如果大家对qreact在感兴趣,欢迎与我联系,也欢迎大家为我的框架加星。

https://github.com/RubyLouvre/anu/
https://github.com/YMFE/qreact

查看原文

赞 142 收藏 381 评论 27

灯盏细辛 回答了问题 · 2017-09-12

直接下载音频的按钮需要怎么处理?

https://developer.mozilla.org...

<a> 标签有个 download 属性的,具体看上面链接。

关注 7 回答 4

灯盏细辛 赞了文章 · 2017-08-29

Webpack 3,从入门到放弃

原文首发于:Webpack 3,从入门到放弃

Update (2017.8.27) : 关于 output.publicPathdevServer.contentBasedevServer.publicPath的区别。如下:

  • output.publicPath: 对于这个选项,我们无需关注什么绝对相对路径,因为两种路径都可以。我们只需要知道一点:这个选项是指定 HTML 文件中资源文件 (字体、图片、JS文件等) 的文件名的公共 URL 部分的。在实际情况中,我们首先会通过output.filename或有些 loader 如file-loadername属性设置文件名的原始部分,webpack 将文件名的原始部分和公共部分结合之后,HTML 文件就能获取到资源文件了。
  • devServer.contentBase: 设置静态资源的根目录,html-webpack-plugin生成的 html 不是静态资源。当用 html 文件里的地址无法找到静态资源文件时就会去这个目录下去找。
  • devServer.publicPath: 指定浏览器上访问所有 打包(bundled)文件 (在dist里生成的所有文件) 的根目录,这个根目录是相对服务器地址及端口的,比devServer.contentBaseoutput.publicPath优先。

前言

Tips
如果你用过 webpack 且一直用的是 webpack 1,请参考 从v1迁移到v2 (v2 和 v3 差异不大) 对版本变更的内容进行适当的了解,然后再选择性地阅读本文。

首先,这篇文章是根据当前最新的 webpack 版本 (即 v3.4.1) 撰写,较长一段时间内无需担心过时的问题。其次,这应该会是一篇极长的文章,涵盖了基本的使用方法,有更高级功能的需求可以参考官方文档继续学习。再次,即使是基本的功能,也内容繁多,我尽可能地解释通俗易懂,将我学习过程中的疑惑和坑一一解释,如有纰漏,敬请雅正。再次,为了清晰有效地讲解,我会演示从零编写 demo,只要一步步跟着做,就会清晰许多。最后,官方文档也是个坑爹货!

Webpack,何许人也?

借用官方的说法:

webpack is a module bundler. Its main purpose is to bundle JavaScript files for usage in a browser, yet it is also capable of transforming, bundling, or packaging just about any resource or asset.

简言之,webpack 是一个模块打包器 (module bundler),能够将任何资源如 JavaScript 文件、CSS 文件、图片等打包成一个或少数文件。

为什么要用介个 Webpack?

首先,定义已经说明了 webpack 能将多个资源模块打包成一个或少数文件,这意味着与以往的发起多个 HTTP 请求来获得资源相比,现在只需要发起少量的 HTTP 请求。

Tips
想了解合并 HTTP 请求的意义,请见 这里

其次,webpack 能将你的资源转换为最适合浏览器的“格式”,提升应用性能。比如只引用被应用使用的资源 (剔除未被使用的代码),懒加载资源 (只在需要的时候才加载相应的资源)。再次,对于开发阶段,webpack 也提供了实时加载和热加载的功能,大大地节省了开发时间。除此之外,还有许多优秀之处之处值得去挖掘。不过,webpack 最核心的还是打包的功能。

webpack,gulp/grunt,npm,它们有什么区别?

webpack 是模块打包器(module bundler),把所有的模块打包成一个或少量文件,使你只需加载少量文件即可运行整个应用,而无需像之前那样加载大量的图片,css文件,js文件,字体文件等等。而gulp/grunt 是自动化构建工具,或者叫任务运行器(task runner),是把你所有重复的手动操作让代码来做,例如压缩JS代码、CSS代码,代码检查、代码编译等等,自动化构建工具并不能把所有模块打包到一起,也不能构建不同模块之间的依赖图。两者来比较的话,gulp/grunt 无法做模块打包的事,webpack 虽然有 loader 和 plugin可以做一部分 gulp/grunt 能做的事,但是终究 webpack 的插件还是不如 gulp/grunt 的插件丰富,能做的事比较有限。于是有人两者结合着用,将 webpack 放到 gulp/grunt 中用。然而,更好的方法是用 npm scripts 取代 gulp/grunt,npm 是 node 的包管理器 (node package manager),用于管理 node 的第三方软件包,npm 对于任务命令的良好支持让你最终省却了编写任务代码的必要,取而代之的,是老祖宗的几个命令行,仅靠几句命令行就足以完成你的模块打包和自动化构建的所有需求。

准备开始

先来看看一个 webpack 的一个完备的配置文件,是 介样 的,当然啦,这里面有很多配置项是即使到这个软件被废弃你也用不上的:),所以无需担心。

基本配置

开始之前,请确定你已经安装了当前 Node 的较新版本。

然后执行以下命令以新建我们的 demo 目录:

$ mkdir webpack-demo && cd webpack-demo && npm init -y
$ npm i --save-dev webpack
$ mkdir src && cd src && touch index.js

我们使用工具函数库 lodash 来演示我们的 demo。先安装之:

$ npm i --save lodash

src/index.js

import _ from 'lodash';

function component() {
  const element = document.createElement('div');
    
  element.innerHTML = _.join(['Hello', 'webpack'], ' ');
    
  return element;
}

document.body.appendChild(component());

Tips
importexport 已经是 ES6 的标准,但是仍未得到大多数浏览器的支持 (可喜的是, Chrome 61 已经开始默认支持了,见 ES6 modules),不过 webpack 提供了对这个特性的支持,但是除了这个特性,其他的 ES6 特性并不会得到 webpack 的特别支持,如有需要,须借助 Babel 进行转译 (transpile)。

然后新建发布版本目录:

$ cd .. && mkdir dist && cd dist && touch index.html 

dist/index.html

<!DOCTYPE html>
<html>
<head>
    <title>webpack demo</title>
</head>
<body>
    <script data-original="bundle.js"></script>
</body>
</html>

现在,我们运行 webpack 来打包 index.jsbundle.js,本地安装了 webpack 后可以通过 node_modules/.bin/webpack 来访问 webpack 的二进制版本。

$ cd ..
$ ./node_modules/.bin/webpack src/index.js dist/bundle.js # 第一个参数是打包的入口文件,第二个参数是打包的出口文件

咻咻咻,大致如下输出一波:

Hash: de8ed072e2c7b3892179
Version: webpack 3.4.1
Time: 390ms
    Asset    Size  Chunks                    Chunk Names
bundle.js  544 kB       0  [emitted]  [big]  main
   [0] ./src/index.js 225 bytes {0} [built]
   [2] (webpack)/buildin/global.js 509 bytes {0} [built]
   [3] (webpack)/buildin/module.js 517 bytes {0} [built]
    + 1 hidden module

现在,你已经得到了你的第一个打包文件 (bundle.js) 了。

使用配置文件

像上面这样使用 webpack 应该是最挫的姿势了,所以我们要使用 webpack 的配置文件来提高我们的姿势水平。

$ touch webpack.config.js

webpack.config.js

const path = require('path');

module.exports = {
  entry: './src/index.js', // 入口起点,可以指定多个入口起点
  output: { // 输出,只可指定一个输出配置
    filename: 'bundle.js', // 输出文件名
    path: path.resolve(__dirname, 'dist') // 输出文件所在的目录
  }
};

执行:

$ ./node_modules/.bin/webpack --config webpack.config.js # `--config` 制定 webpack 的配置文件,默认是 `webpack.config.js`

所以这里可以省却 --config webpack.config.js。但是每次都要写 ./node_modules/.bin/webpack 实在让人不爽,所以我们要动用 NPM Scripts

package.json

{
  ...
  "scripts": {
    "build": "webpack"
  },
  ...
}

Tips
npm scripts 中我们可以通过包名直接引用本地安装的 npm 包的二进制版本,而无需编写包的整个路径。

执行:

$ npm run build

一波输出后便得到了打包文件。

Tips
bulid 并不是 npm scripts 的内置属性,需要使用 npm run 来执行脚本,详情见 npm run

打包其他类型的文件

因为其他文件和 JS 文件类型不同,要把他们加载到 JS 文件中就需要经过加载器 (loader) 的处理。

加载 CSS

我们需要安装两个 loader 来处理 CSS 文件:

$ npm i --save-dev style-loader css-loader

style-loader 通过插入 <style> 标签将 CSS 加入到 DOM 中,css-loader 会像解释 import/require() 一样解释 @import 和 url()。

const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js', 
    path: path.resolve(__dirname, 'dist')
  },
  module: { // 如何处理项目中不同类型的模块
    rules: [ // 用于规定在不同模块被创建时如何处理模块的规则数组
      {
        test: /\.css$/, // 匹配特定文件的正则表达式或正则表达式数组
        use: [ // 应用于模块的 loader 使用列表
          'style-loader',
          'css-loader'
        ]
      }
    ]
  }
};

我们来创建一个 CSS 文件:

$ cd src && touch style.css

src/style.css

.hello {
  color: red;
}

src/index.js

import _ from 'lodash';
import './style.css'; // 通过`import`引入 CSS 文件

function component() {
  const element = document.createElement('div');
    
  element.innerHTML = _.join(['Hello', 'webpack'], ' ');
  element.classList.add('hello'); // 在相应元素上添加类名
    
  return element;
}

document.body.appendChild(component());

执行npm run build,然后打开index.html,就可以看到红色的字体了。CSS 文件此时已经被打包到 bundle.js 中。再打开浏览器控制台,就可以看到 webpack 做了些什么。

加载图片

$ npm install --save-dev file-loader

file-loader 指示 webpack 以文件格式发出所需对象并返回文件的公共URL,可用于任何文件的加载。

webpack.config.js

const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js', 
    path: path.resolve(__dirname, 'dist')
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          'style-loader',
          'css-loader'
        ]
      },
      { // 增加加载图片的规则
        test: /\.(png|svg|jpg|gif)$/,
        use: [
          'file-loader'
        ]
      }
    ]
  }
};

我们在当前项目的目录中如下增加图片:

  webpack-demo
  |- package.json
  |- webpack.config.js
  |- /dist
    |- bundle.js
    |- index.html
  |- /src
+   |- icon.jpg
    |- style.css
    |- index.js
  |- /node_modules

src/index.js

import _ from 'lodash';
import './style.css';
import Icon from './icon.jpg'; // Icon 是图片的 URL

function component() {
  const element = document.createElement('div');
    
  element.innerHTML = _.join(['Hello', 'webpack'], ' ');
  element.classList.add('hello');
  
  const myIcon = new Image();
  myIcon.src = Icon;

  element.appendChild(myIcon);
  
  return element;
}

document.body.appendChild(component());

src/style.css

.hello {
  color: red;
  background: url(./icon.jpg);
}

npm run build之。现在你可以看到单独的图片和以图片为基础的背景图了。

加载字体

加载字体用的也是 file-loader。

webpack.config.js

const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js', 
    path: path.resolve(__dirname, 'dist')
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          'style-loader',
          'css-loader'
        ]
      },
      {
        test: /\.(png|svg|jpg|gif)$/,
        use: [
          'file-loader'
        ]
      },
      { // 增加加载字体的规则
        test: /\.(woff|woff2|eot|ttf|otf)$/,
        use: [
          'file-loader'
        ]
      }
    ]
  }
};

在当前项目的目录中如下增加字体:

  webpack-demo
  |- package.json
  |- webpack.config.js
  |- /dist
    |- bundle.js
    |- index.html
  |- /src
+   |- my-font.ttf
    |- icon.jpg
    |- style.css
    |- index.js
  |- /node_modules

src/style.css

@font-face {
  font-family: MyFont;
  src: url(./my-font.ttf);
}

.hello {
  color: red;
  background: url(./icon.jpg);
  font-family: MyFont;
}

运行打包命令之后便可以看到打包好的文件和发生改变的页面。

加载 JSON 文件

因为 webpack 对 JSON 文件的支持是内置的,所以可以直接添加。

src/data.json

{
  "name": "webpack-demo",
  "version": "1.0.0",
  "author": "Sam Yang"
}

src/index.js

import _ from 'lodash';
import './style.css';
import Icon from './icon.jpg';
import Data from './data.json'; // Data 变量包含可直接使用的 JSON 解析得到的对象

function component() {
  const element = document.createElement('div');
    
  element.innerHTML = _.join(['Hello', 'webpack'], ' ');
  element.classList.add('hello');

  const myIcon = new Image();
  myIcon.src = Icon;

  element.appendChild(myIcon);

  console.log(Data);
    
  return element;
}

document.body.appendChild(component());

关于其他文件的加载,可以寻求相应的 loader。

输出管理

前面我们只有一个输入文件,但现实是我们往往有不止一个输入文件,这时我们就需要输入多个入口文件并管理输出文件。我们在 src 目录下增加一个 print.js 文件。

src/print.js

export default function printMe() {
  console.log('I get called from print.js!');
}

src/index.js

import _ from 'lodash';
import printMe from './print.js';
// import './style.css';
// import Icon from './icon.jpg';
// import Data from './data.json';

function component() {
  const element = document.createElement('div');
  const btn = document.createElement('button');
    
  element.innerHTML = _.join(['Hello', 'webpack'], ' ');
  // element.classList.add('hello');

  // const myIcon = new Image();
  // myIcon.src = Icon;

  // element.appendChild(myIcon);

  // console.log(Data);

  btn.innerHTML = 'Click me and check the console!';
  btn.onclick = printMe;

  element.appendChild(btn);
    
  return element;
}

document.body.appendChild(component());

dist/index.html

<!DOCTYPE html>
<html>
<head>
    <title>webpack demo</title>
    <script data-original="./print.bundle.js"></script>
</head>
<body>
    <!-- <script data-original="bundle.js"></script> -->
    <script data-original="./app.bundle.js"></script>
</body>
</html>

webpack.config.js

const path = require('path');

module.exports = {
  // entry: './src/index.js',
  entry: {
    app: './src/index.js',
    print: './src/print.js'
  },
  output: {
    // filename: 'bundle.js',
    filename: '[name].bundle.js', // 根据入口起点名动态生成 bundle 名,可以使用像 "js/[name]/bundle.js" 这样的文件夹结构
    path: path.resolve(__dirname, 'dist')
  },
  // ...
};

Tips
filename: '[name].bundle.js'中的[name]会替换为对应的入口起点名,其他可用的替换请参见 output.filename

现在可以打包文件了。但是如果我们修改了入口文件名或增加了入口文件,index.html是不会自动引用新文件的,而手动修改实在太挫。是时候使用插件 (plugin) 来完成这一任务了。我们使用 HtmlWebpackPlugin 自动生成 html 文件。

loader 和 plugin,有什么区别?
loader (加载器),重在“加载”二字,是用于预处理文件的,只用于在加载不同类型的文件时对不同类型的文件做相应的处理。而 plugin (插件),顾名思义,是用来增加 webpack 的功能的,作用于整个 webpack 的构建过程。在 webpack 这个大公司中,loader 是保安大叔,负责对进入公司的不同人员的处理,而 plugin 则是公司里不同职位的职员,负责公司里的各种不同业务,每增加一种新型的业务需求,我们就需要增加一种 plugin。

安装插件:

$ npm i --save-dev html-webpack-plugin

webpack.config.js

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  // entry: './src/index.js',
  entry: {
    app: './src/index.js',
    print: './src/print.js'
  },
  output: {
    // filename: 'bundle.js',
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  plugins: [ // 插件属性,是插件的实例数组
    new HtmlWebpackPlugin({
      title: 'webpack demo',  // 生成 HTML 文档的标题
      filename: 'index.html' // 写入 HTML 文件的文件名,默认 `index.html`
    })
  ],
  // ...
};

你可以先把 dist 文件夹的index.html文件删除,然后执行打包命令。咻咻咻,我们看到 dist 目录下已经自动生成了一个index.html文件,但即使不删除原先的index.html,该插件默认生成的index.html也会替换原本的index.html

此刻,当你细细观察 dist 目录时,虽然现在生成了新的打包文件,但原本的打包文件bundle.js及其他不用的文件仍然存在在 dist 目录中,所以在每次构建前我们需要晴空 dist 目录,我们使用 CleanWebpackPlugin 插件。

$ npm i clean-webpack-plugin --save-dev

webpack.config.js

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');

module.exports = {
  // entry: './src/index.js',
  entry: {
    app: './src/index.js',
    print: './src/print.js'
  },
  output: {
    // filename: 'bundle.js',
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  plugins: [
    new HtmlWebpackPlugin({
      title: 'webpack demo',
      filename: 'index.html'
    }),
    new CleanWebpackPlugin(['dist']) // 第一个参数是要清理的目录的字符串数组
  ],
  // ...
};

打包之,现在,dist 中只存在打包生成的文件。

开发环境

webpack 提供了很多便于开发时使用的功能,来一一看看吧。

使用代码映射 (source map)

当你的代码被打包后,如果打包后的代码发生了错误,你很难追踪到错误发生的原始位置,这个时候,我们就需要代码映射 (source map) 这种工具,它能将编译后的代码映射回原始的源码,你的错误是起源于打包前的b.js的某个位置,代码映射就能告诉你错误是那个模块的那个位置。webpack 默认提供了 10 种风格的代码映射,使用它们会明显影响到构建 (build) 和重构建 (rebuild,每次修改后需要重新构建) 的速度,十种风格的差异可以参看 devtool。关于如何选择映射风格可以参看 Webpack devtool source map。这里,我们为了准确显示错误位置,选择速度较慢的inline-source-map

webpack.config.js

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');

module.exports = {
  devtool: 'inline-source-map', // 控制是否生成以及如何生成 source map
  // entry: './src/index.js',
  entry: {
    app: './src/index.js',
    print: './src/print.js'
  },
  // ...
};

现在来手动制造一些错误:

src/print.js

  export default function printMe() {
-   console.log('I get called from print.js!');
+   cosnole.log('I get called from print.js!');
  }

打包之后打开index.html再点击按钮,你就会看到控制台显示如下报错:

 Uncaught ReferenceError: cosnole is not defined
    at HTMLButtonElement.printMe (print.js:2)

现在,我们很清楚哪里发生了错误,然后轻松地改正之。

使用 webpack-dev-server

你一定有这样的体验,开发时每次修改代码保存后都需要重新手动构建代码并手动刷新浏览器以观察修改效果,这是很麻烦的,所以,我们要实时加载代码。可喜的是,webpack 提供了对实时加载代码的支持。我们需要安装 webpack-dev-server 以获得支持。

$ npm i --save-dev webpack-dev-server

webpack.config.js

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');

module.exports = {
  devtool: 'inline-source-map',
  devServer: { // 检测代码变化并自动重新编译并自动刷新浏览器
    contentBase: path.resolve(__dirname, 'dist') // 设置静态资源的根目录
  },
  // entry: './src/index.js',
  entry: {
    app: './src/index.js',
    print: './src/print.js'
  },
  // ...
};

package.json

{
  ...
  "scripts": {
    "build": "webpack",
    "start": "webpack-dev-server --open"
  },
  ...
}

Tips
使用 webpack-dev-server 时,webpack 并没有将所有生成的文件写入磁盘,而是放在内存中,提供更快的内存内访问,便于实时更新。

现在,可以直接运行npm start (start是 npm scripts 的内置属性,可直接运行),然后浏览器自动加载应用的页面,默认在localhost:8080显示。

模块热替换 (HMR, Hot Module Replacement)

webpack 提供了对模块热替换 (或者叫热加载) 的支持。这一特性能够让应用运行的时候替换、增加或删除模块,而无需进行完全的重载。想进一步地了解其工作机理,可以参见 Hot Module Replacement,但这并不是必需的,你可以选择跳过机理部分继续往下阅读。

Tips
模块热替换(HMR)只更新发生变更(替换、添加、删除)的模块,而无需重新加载整个页面(实时加载,LiveReload),这样可以显著加快开发速度,一旦打开了 webpack-dev-server 的 hot 模式,在试图重新加载整个页面之前,热模式会尝试使用 HMR 来更新。

webpack.config.js

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const webpack = require('webpack'); // 引入 webpack 便于调用其内置插件

module.exports = {
  devtool: 'inline-source-map',
  devServer: {
    contentBase: path.resolve(__dirname, 'dist'),
    hot: true, // 告诉 dev-server 我们在用 HMR
    hotOnly: true // 指定如果热加载失败了禁止刷新页面 (这是 webpack 的默认行为),这样便于我们知道失败是因为何种错误
  },
  // entry: './src/index.js',
  entry: {
    app: './src/index.js',
    // print: './src/print.js'
  },
  // ...
  plugins: [
    new HtmlWebpackPlugin({
      title: 'webpack demo',
      filename: 'index.html'
    }),
    new CleanWebpackPlugin(['dist']),
    new webpack.HotModuleReplacementPlugin(), // 启用 HMR
    new webpack.NamedModulesPlugin() // 打印日志信息时 webpack 默认使用模块的数字 ID 指代模块,不便于 debug,这个插件可以将其替换为模块的真实路径
  ],
  // ...
};

Tips
webpack-dev-server 会为每个入口文件创建一个客户端脚本,这个脚本会监控该入口文件的依赖模块的更新,如果该入口文件编写了 HMR 处理函数,它就能接收依赖模块的更新,反之,更新会向上冒泡,直到客户端脚本仍没有处理函数的话,webpack-dev-server 会重新加载整个页面。如果入口文件本身发生了更新,因为向上会冒泡到客户端脚本,并且不存在 HMR 处理函数,所以会导致页面重载。

我们已经开启了 HMR 的功能,HMR 的接口已经暴露在module.hot属性之下,我们只需要调用 HMR API 即可实现热加载。当“被加载模块”发生改变时,依赖该模块的模块便能检测到改变并接收改变之后的模块。

src/index.js

import _ from 'lodash';
import printMe from './print.js';
// import './style.css';
// import Icon from './icon.jpg';
// import Data from './data.json';

function component() {
  const element = document.createElement('div');
  const btn = document.createElement('button');
    
  element.innerHTML = _.join(['Hello', 'webpack'], ' ');
  // element.classList.add('hello');

  // const myIcon = new Image();
  // myIcon.src = Icon;

  // element.appendChild(myIcon);

  // console.log(Data);

  btn.innerHTML = 'Click me and check the console!';
  btn.onclick = printMe;

  element.appendChild(btn);
    
  return element;
}

document.body.appendChild(component());

if(module.hot) { // 习惯上我们会检查是否可以访问 `module.hot` 属性
  module.hot.accept('./print.js', function() { // 接受给定依赖模块的更新,并触发一个回调函数来对这些更新做出响应
    console.log('Accepting the updated printMe module!');
    printMe();
  });
}

npm start之。为了演示效果,我们做如下修改:

src/print.js

  export default function printMe() {
-   console.log('I get called from print.js!');
+   console.log('Updating print.js...');
  }

我们会看到控制台打印出的信息中含有以下几行:

index.js:33 Accepting the updated printMe module!
print.js:2 Updating print.js...
log.js:23 [HMR] Updated modules:
log.js:23 [HMR]  - ./src/print.js
log.js:23 [HMR] App is up to date.

Tips
webpack-dev-server 在 inline mode (此为默认模式) 时,会为每个入口起点 (entry) 创建一个客户端脚本,所以你会在上面的输出中看到有些信息重复输出两次。

但是当你点击页面的按钮时,你会发现控制台输出的是旧的printMe函数输出的信息,因为onclick事件绑定的仍是原始的printMe函数。我们需要在module.hot.accept里更新绑定。

src/index.js

import _ from 'lodash';
import printMe from './print.js';
// import './style.css';
// import Icon from './icon.jpg';
// import Data from './data.json';

// ...

// document.body.appendChild(component());
var element = component();
document.body.appendChild(element);

if(module.hot) {
  module.hot.accept('./print.js', function() {
    console.log('Accepting the updated printMe module!');
    // printMe();
    
    document.body.removeChild(element);
    element = component();
    document.body.appendChild(element);
  });
}

Tips
uglifyjs-webpack-plugin 升级到 v0.4.6 时无法正确压缩 ES6 的代码,所以上面有些代码采用 ES5 以暂时方便后面的压缩,详见 #49

模块热替换也可以用于样式的修改,效果跟控制台修改一样一样的。

src/index.js

import _ from 'lodash';
import printMe from './print.js';
import './style.css';
// import Icon from './icon.jpg';
// import Data from './data.json';

// ...

npm start之,做如下修改:

/* ... */

body {
  background-color: yellow;
}

可以发现在不重载页面的前提下我们对样式的修改进行了热加载,棒!

生产环境

自动方式

我们只需要运行webpack -p (相当于 webpack --optimize-minimize --define process.env.NODE_ENV="'production'")这个命令,便可以自动构建生产版本的应用,这个命令会完成以下步骤:

  • 使用 UglifyJsPlugin (webpack.optimize.UglifyJsPlugin) 压缩 JS 文件 (此插件和 uglifyjs-webpack-plugin 相同)
  • 运行 LoaderOptionsPlugin 插件,这个插件是用来迁移的,见 document
  • 设置 NodeJS 的环境变量,触发某些 package 包以不同方式编译

值得一提的是,webpack -p设置的process.env.NODE_ENV环境变量,是用于编译后的代码的,只有在打包后的代码中,这一环境变量才是有效的。如果在 webpack 配置文件中引用此环境变量,得到的是 undefined,可以参见 #2537。但是,有时我们确实需要在 webpack 配置文件中使用 process.env.NODE_ENV,怎么办呢?一个方法是运行NODE_ENV='production' webpack -p命令,不过这个命令在Windows中是会出问题的。为了解决兼容问题,我们采用 cross-env 解决跨平台的问题。

$ npm i --save-dev cross-env

package.json

{
  ...
  "scripts": {
    "build": "cross-env NODE_ENV=production webpack -p",
    "start": "webpack-dev-server --open"
  },
  ...
}

现在可以在配置文件中使用process.env.NODE_ENV了。

webpack.config.js

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const webpack = require('webpack');

module.exports = {
  // ...
  output: {
    // filename: 'bundle.js',
    // filename: '[name].bundle.js',
    filename: process.env.NODE_ENV === 'production' ? '[name].[chunkhash].js' : '[name].bundle.js', // 在配置文件中使用`process.env.NODE_ENV`
    path: path.resolve(__dirname, 'dist')
  },
  plugins: [
    new HtmlWebpackPlugin({
      title: 'webpack demo',
      filename: 'index.html'
    }),
    new CleanWebpackPlugin(['dist']),
    // new webpack.HotModuleReplacementPlugin(), // 关闭 HMR 功能
    new webpack.NamedModulesPlugin()
  ],
  // ...
};

Tips
[chunkhash]不能和 HMR 一起使用,换句话说,不应该在开发环境中使用 [chunkhash] (或者 [hash]),这会导致许多问题。详情见 #2393#377

build 之,我们得到了生产版本的压缩好的打包文件。

多配置文件配置

有时我们会需要为不同的环境配置不同的配置文件,可以选择 简易方法,这里我们采用较为先进的方法。先准备一个基本的配置文件,包含了所有环境都包含的配置,然后用 webpack-merge 将它和特定环境的配置文件合并并导出,这样就减少了基本配置的重复。

$ npm i --save-dev webpack-merge

webpack.common.js

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');

module.exports = {
  entry: {
    app: './src/index.js',
    print: './src/print.js'
  },
  output: {
    path: path.resolve(__dirname, 'dist')
  },
  plugins: [
    new HtmlWebpackPlugin({
      title: 'webpack demo',
      filename: 'index.html'
    }),
    new CleanWebpackPlugin(['dist'])
  ],
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          'style-loader',
          'css-loader'
        ]
      },
      {
        test: /\.(png|svg|jpg|gif)$/,
        use: [
          'file-loader'
        ]
      },
      {
        test: /\.(woff|woff2|eot|ttf|otf)$/,
        use: [
          'file-loader'
        ]
      }
    ]
  }
};

webpack.dev.js

const path = require('path');
const webpack = require('webpack');
const Merge = require('webpack-merge');
const CommonConfig = require('./webpack.common.js');

module.exports = Merge(CommonConfig, {
  devtool: 'cheap-module-eval-source-map',
  devServer: {
    contentBase: path.resolve(__dirname, 'dist'),
    hot: true,
    hotOnly: true
  },
  output: {
    filename: '[name].bundle.js'
  },
  plugins: [
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify('development') // 在编译的代码里设置了`process.env.NODE_ENV`变量
    }),
    new webpack.HotModuleReplacementPlugin(),
    new webpack.NamedModulesPlugin()
  ]
});

webpack.prod.js

const path = require('path');
const webpack = require('webpack');
const Merge = require('webpack-merge');
const CommonConfig = require('./webpack.common.js');

module.exports = Merge(CommonConfig, {
  devtool: 'cheap-module-source-map',
  output: {
    filename: '[name].[chunkhash].js'
  },
  plugins: [
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify('production')
    }),
    new webpack.optimize.UglifyJsPlugin()
  ]
});

package.json

{
  ...
  "scripts": {
    "build": "cross-env NODE_ENV=production webpack -p",
    "start": "webpack-dev-server --open",
    "build:dev": "webpack-dev-server --open --config webpack.dev.js",
    "build:prod": "webpack --progress --config webpack.prod.js"
  },
  ...
}

现在只需执行npm run build:devnpm run build:prod便可以得到开发版或者生产版了!

Tips
webpack 命令行选项见 Command Line Interface

代码分离

入口分离

我们先创建一个新文件:

$ cd src && touch another.js

src/another.js

import _ from 'lodash';

console.log(_.join(['Another', 'module', 'loaded!'], ' '));

webpack.config.js

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const webpack = require('webpack');

module.exports = {
  // ...
  entry: {
    app: './src/index.js',
    // print: './src/print.js'
    another: './src/another.js'
  },
  // ...
};

cd .. && npm run build之,我们发现用入口分离的代码得到了两个大文件,这是因为两个入口文件都引入了lodash,这很大程度上造成了冗余,在同一个页面中我们只需要引入一个lodash就可以了。

抽取相同部分

我们使用 CommonsChunkPlugin 插件来将相同的部分提取出来放到一个单独的模块中。

webpack.config.js

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const webpack = require('webpack');

module.exports = {
  // devtool: 'inline-source-map',
  // ...
  output: {
    // filename: 'bundle.js',
    filename: '[name].bundle.js',
    // filename: process.env.NODE_ENV === 'production' ? '[name].[chunkhash].js' : '[name].bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  plugins: [
    new HtmlWebpackPlugin({
      title: 'webpack demo',
      filename: 'index.html'
    }),
    new CleanWebpackPlugin(['dist']),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'common' // 抽取出的模块的模块名
    }),
    // new webpack.HotModuleReplacementPlugin(),
    // new webpack.NamedModulesPlugin()
  ],
  // ...
};

build 之,可以看到结果中包含以下部分:

    app.bundle.js    6.14 kB       0  [emitted]  app
another.bundle.js  185 bytes       1  [emitted]  another
 common.bundle.js    73.2 kB       2  [emitted]  common
       index.html  314 bytes          [emitted]

我们把lodash分离出来了。

动态引入

我们还可以选择以动态引入的方式来实现代码分离,借助 import() 实现之。

webpack.config.js

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
// const webpack = require('webpack');

module.exports = {
  // ...
  entry: {
    app: './src/index.js',
    // print: './src/print.js'
    // another: './src/another.js'
  },
  output: {
    // filename: 'bundle.js',
    filename: '[name].bundle.js',
    chunkFilename: '[name].bundle.js', // 指定非入口块文件输出的名字
    // filename: process.env.NODE_ENV === 'production' ? '[name].[chunkhash].js' : '[name].bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  plugins: [
    new HtmlWebpackPlugin({
      title: 'webpack demo',
      filename: 'index.html'
    }),
    new CleanWebpackPlugin(['dist'])
    // new webpack.optimize.CommonsChunkPlugin({
    //   name: 'common'
    // }),
    // new webpack.HotModuleReplacementPlugin(),
    // new webpack.NamedModulesPlugin()
  ],
  // ...
};

src/index.js

// import _ from 'lodash';
import printMe from './print.js';
// import './style.css';
// import Icon from './icon.jpg';
// import Data from './data.json';

function component() {
  // 此函数原来的内容全部注释掉...

  return import(/* webpackChunkName: "lodash" */ 'lodash').then(function(_) {
    const element = document.createElement('div');
    const btn = document.createElement('button');

    element.innerHTML = _.join(['Hello', 'webpack'], ' ');

    btn.innerHTML = 'Click me and check the console!';
    btn.onclick = printMe;

    element.appendChild(btn);

    return element;
  }).catch(function(error) {
    console.log('An error occurred while loading the component')
  });
}

// document.body.appendChild(component());
// var element = component();
// document.body.appendChild(element);

// 原本热加载的部分全部注释掉...

component().then(function(component) {
   document.body.appendChild(component);
 });

Tips
注意上面中的/* webpackChunkName: "lodash" */这段注释,它并不是可有可无的,它能帮助我们结合output.chunkFilename把分离出的模块最终命名为lodash.bundle.js而非[id].bundle.js

现在 build 之看看吧。

懒加载 (lazy loading)

既然有了import(),我们可以选择在需要的时候才加载相应的模块,减少了应用初始化时加载大量暂不需要的模块的压力,这能让我们的应用更高效地运行。

src/print.js

console.log('The print.js module has loaded! See the network tab in dev tools...');

export default function printMe() {
  // console.log('Updating print.js...');
  console.log('Button Clicked: Here\'s "some text"!');
}

src/index.js

import _ from 'lodash';
// 其他引入注释...

function component() {
  const element = document.createElement('div');
  const btn = document.createElement('button');
    
  element.innerHTML = _.join(['Hello', 'webpack'], ' ');
  // element.classList.add('hello');

  // const myIcon = new Image();
  // myIcon.src = Icon;

  // element.appendChild(myIcon);

  // console.log(Data);

  btn.innerHTML = 'Click me and check the console!';
  // btn.onclick = printMe;

  element.appendChild(btn);

  btn.onclick = function() {
    import(/* webpackChunkName: "print" */ './print')
    .then(function(module) {
      const printMe = module.default; // 引入模块的默认函数

      printMe();
    });
  };
    
  return element;

  // 原本的动态引入注释...
}

document.body.appendChild(component());
// var element = component();
// document.body.appendChild(element);

// 热加载部分注释

// component().then(function(component) {
//    document.body.appendChild(component);
//  });

构建之,控制台此时并无输出,点击按钮,会看到控制台如下输出:

print.bundle.js:1 The print.js module has loaded! See the network tab in dev tools...
print.bundle.js:1 Button Clicked: Here's "some text"!

说明 print 模块只在我们点击时才引入了,すっげえ!

缓存 (caching)

浏览器在初次加载网站时,会下载很多文件,为了较少下载大量资源的压力,浏览器会对资源进行缓存 (caching),这样浏览器便可以更迅速地加载网站,但是我们需要在文件内容发生改变时更新文件。

我们可以在输出文件名上下手脚:

webpack.config.js

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
// const webpack = require('webpack');

module.exports = {
  // ...
  output: {
    // filename: 'bundle.js',
    filename: '[name].[chunkhash].js',
    // chunkFilename: '[name].bundle.js',
    // filename: process.env.NODE_ENV === 'production' ? '[name].[chunkhash].js' : '[name].bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  // ...
};

Tips
[chunkhash] 是内容相关的,只要内容发生了改变,构建后文件名的 hash 就会发生改变。

还有一个要点是提取出第三方库放到单独模块中,因为它们是不太可能频繁发生改变的,所以无需多次加载这些模块,提取的方法用 CommonsChunkPlugin 插件,这个插件上文中提到过,指定入口文件名时它会提取改入口文件为单个文件,不指定则会提取 webpack 的运行时代码。

webpack.config.js

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const webpack = require('webpack');

module.exports = {
  // ...
  entry: {
    app: './src/index.js',
    vendor: [ // 第三方库可以统一放在这个入口一起合并
      'lodash'
    ]
    // print: './src/print.js'
    // another: './src/another.js'
  },
  output: {
    // filename: 'bundle.js',
    filename: '[name].[chunkhash].js',
    chunkFilename: '[name].bundle.js',
    // filename: process.env.NODE_ENV === 'production' ? '[name].[chunkhash].js' : '[name].bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  plugins: [
    new HtmlWebpackPlugin({
      title: 'webpack demo',
      filename: 'index.html'
    }),
    new CleanWebpackPlugin(['dist']),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor' // 将 vendor 入口处的代码放入 vendor 模块
    }),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'runtime' // 将 webpack 自身的运行时代码放在 runtime 模块
    })
    // new webpack.HotModuleReplacementPlugin(),
    // new webpack.NamedModulesPlugin()
  ],
  // ...
};

Tips
包含 vendor 的 CommonsChunkPlugin 实例必须在包含 runtime 的之前,否则会报错。

src/index.js

// import _ from 'lodash';
// ...

// ...

如果我们在 src 下新建一个文件h.js,再在index.js中引入它,保存,构建之,我们发现有些没改变的模块的 hash 也发生了改变,这是因为加入h.js后它们的module.id变了,但这明显是不合理的。在开发环境,我们可以用 NamedModulesPlugin 将 id 换成具体路径名。而在生产环境,我们可以使用 HashedModuleIdsPlugin

webpack.config.js

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const webpack = require('webpack');

module.exports = {
  // ...
  plugins: [
    new HtmlWebpackPlugin({
      title: 'webpack demo',
      filename: 'index.html'
    }),
    new webpack.HashedModuleIdsPlugin(), // 替换掉原来的`module.id`
    new CleanWebpackPlugin(['dist']),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor'
    }),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'runtime'
    })
    // new webpack.HotModuleReplacementPlugin(),
    // new webpack.NamedModulesPlugin()
  ],
  // ...
};

再来执行刚才那波操作,就会发现无关修改的模块 hash 未变了。

Shimming

Tips
你可以将 shim 简单理解为是用于兼容 API 的小型库。

使用 jQuery 时我们习惯性地使用$jQuery变量,每次都使用const $ = require(“jquery”)引入的话太麻烦,如果能直接把这两个变量设置为全局变量岂不美滋滋?这样就可以在每个模块中直接使用这两个变量了。为了兼容这一做法,我们使用 ProvidePlugin 插件为我们完成这一任务。

$ npm i --save jquery

webpack.config.js

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const webpack = require('webpack');

module.exports = {
  // ...
  plugins: [
    new HtmlWebpackPlugin({
      title: 'webpack demo',
      filename: 'index.html'
    }),
    new webpack.ProvidePlugin({ // 设置全局变量
      $: 'jquery',
      jQuery: 'jquery'
    }),
    new webpack.HashedModuleIdsPlugin(),
    new CleanWebpackPlugin(['dist']),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor'
    }),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'runtime'
    })
    // new webpack.HotModuleReplacementPlugin(),
    // new webpack.NamedModulesPlugin()
  ],
  // ...
};

src/print.js

console.log('The print.js module has loaded! See the network tab in dev tools...');
console.log($('title').text()); // 使用 jQuery

export default function printMe() {
  // console.log('Updating print.js...');
  console.log('Button Clicked: Here\'s "some text"!');
}

build,点击页面按钮,成功了。

另外,如果你需要在某些模块加载时设置该模块的全局变量,请看 这里

结尾的一点废话

终于写完了 :),也感谢你能耐心看到这里。webpack 这个工具的配置还是有些麻烦的。但是呢,某人说这个东东前期会花比较多时间,后期会大大提高你的效率。所以呢,还是拿下这个东东吧。有其他需求的话可以继续看官方的文档。遇到困难可以找:

我写好的 demo 文件放在了这里

Reference

查看原文

赞 99 收藏 480 评论 16

灯盏细辛 赞了文章 · 2017-08-15

React 最佳实践——那些 React 没告诉你但很重要的事

前言:对很多 react 新手来说,网上能找到的资源大都是些简单的 tutorial ,它们能教会你如何使用 react ,但并不会告诉你怎么在实际项目中优雅的组织和编写 react 代码。用谷歌搜中文“ React 最佳实践”发现前两页几乎全都是同一篇国外文章的译文...所以我总结了下自己过去那个项目使用 React 踩过的一些坑,也整理了一些别人的观点,希望对部分 react 使用者有帮助。

React 与 AJAX

React只负责处理View这一层,它本身不涉及网络请求/AJAX,所以这里我们需求考虑两个问题:

  • 第一,用什么技术从服务端获取数据;
  • 第二,获取到的数据应该放在react组件的什么位置。

React官方提供了一种解决方案:Load Initial Data via AJAX

使用jQuery的Ajax方法,在一个组件的componentDidMount()中发ajax请求,拿到的数据存在组件自己的state中,并调用setState方法去更新UI。如果是异步获取数据,则在componentWillUnmount中取消发送请求。

如果只是为了使用jQuery的Ajax方法就引入整个jQuery库,既是一种浪费又加大了整个应用的体积。那我们还有什么其他的选择吗?事实上是有很多的:fetch()fetch polyfillaxios...其中最需要我们关注的是window.fetch(),它是一个简洁、标准化的javascript的Ajax API。在Chrome和Firefox中已经可以使用,如果需要兼容其他浏览器,可以使用fetch polyfill。

React官方文档只告诉了我们在一个单一组件中如何通过ajax从服务器端获取数据,但并没有告诉我们在一个完整的实际项目中到底应该把数据存在哪些组件中,这部分如果缺乏规范的话,会导致整个项目变得混乱、难以维护。下面给出三种比较好的实践:

1. 所有的数据请求和管理都存放在唯一的一个根组件

让父组件/根组件集中发送所有的ajax请求,把从服务端获取的数据统一存放在这个组件的state中,再通过props把数据传给子组件。这种方法主要是针对组件树不是很复杂的小型应用。缺点就是当组件树的层级变多了以后,需要把数据一层一层地传给子组件,写起来麻烦,性能也不好。

2. 设置多个容器组件专门处理数据请求和管理

其实跟第一种方法类似,只不过设置多个容器组件来负责数据请求和状态管理。这里我们需要区分两种不同类型的组件,一种是展示性组件(presentational component),另一种是容器性组件(container component)。展示性组件本身不拥有任何状态,所有的数据都从容器组件中获得,在容器组件中发送ajax请求。两者更详细的描述,可以阅读下这篇文章:Presentational and Container Components

一个具体的例子:

假设我们需要展示用户的姓名和头像,首先创建一个展示性组件<UserProfile />,它接受两个Props:nameprofileImage。这个组件内部没有任何关于Ajax的代码。

然后创建一个容器组件<UserProfileContainer />,它接受一个userId的参数,发送Ajax请求从服务器获取数据存在state中,再通过props传给<UserProfile />组件。

3. 使用Redux或Relay的情况

Redux管理状态和数据,Ajax从服务器端获取数据,所以很显然当我们使用了Redux时,应该把所有的网络请求都交给redux来解决。具体来说,应该是放在Async Actions。如果用其他类Flux库的话,解决方式都差不多,都是在actions中发送网络请求。

Relay是Facebook官方推出的一个库。如果用它的话,我们只需要通过GraphQL来声明组件需要的数据,Relay会自动地把下载数据并通过props往下传递。不过想要用Relay,你得先有一个GraphQL的服务器...

一个标准组件的组织结构

1 class definition
    1.1 constructor
        1.1.1 event handlers
    1.2 'component' lifecycle events
    1.3 getters
    1.4 render
2 defaultProps
3 proptypes

示例:

class Person extends React.Component {
  constructor (props) {
    super(props);

    this.state = { smiling: false };

    this.handleClick = () => {
      this.setState({smiling: !this.state.smiling});
    };
  }

  componentWillMount () {
    // add event listeners (Flux Store, WebSocket, document, etc.)
  },

  componentDidMount () {
    // React.getDOMNode()
  },

  componentWillUnmount () {
    // remove event listeners (Flux Store, WebSocket, document, etc.)
  },

  get smilingMessage () {
    return (this.state.smiling) ? "is smiling" : "";
  }

  render () {
    return (
      <div onClick={this.handleClick}>
        {this.props.name} {this.smilingMessage}
      </div>
    );
  },
}

Person.defaultProps = {
  name: 'Guest'
};

Person.propTypes = {
  name: React.PropTypes.string
};

以上示例代码的来源:https://github.com/planningce...

使用 PropTypes 和 getDefaultProps()

  1. 一定要写PropTypes,切莫为了省事而不写
  2. 如果一个Props不是requied,一定在getDefaultProps中设置它

React.PropTypes主要用来验证组件接收到的props是否为正确的数据类型,如果不正确,console中就会出现对应的warning。出于性能方面的考虑,这个API只在开发环境下使用。

基本使用方法:

propTypes: {
    myArray: React.PropTypes.array,
    myBool: React.PropTypes.bool,
    myFunc: React.PropTypes.func,
    myNumber: React.PropTypes.number,
    myString: React.PropTypes.string,
     
     // You can chain any of the above with `isRequired` to make sure a warning
    // is shown if the prop isn't provided.
    requiredFunc: React.PropTypes.func.isRequired
}

假如我们props不是以上类型,而是拥有复杂结构的对象怎么办?比如下面这个:

{
  text: 'hello world',
  numbers: [5, 2, 7, 9],
}

当然,我们可以直接用React.PropTypes.object,但是对象内部的数据我们却无法验证。

propTypes: {
  myObject: React.PropTypes.object,
}

进阶使用方法:shape()arrayOf()

propTypes: {
  myObject: React.PropTypes.shape({
    text: React.PropTypes.string,
    numbers: React.PropTypes.arrayOf(React.PropTypes.number),
  })
}

下面是一个更复杂的Props:

[
  {
    name: 'Zachary He',
    age: 13,
    married: true,
  },
  {
    name: 'Alice Yo',
    name: 17,
  },
  {
    name: 'Jonyu Me',
    age: 20,
    married: false,
  }
]

综合上面,写起来应该就不难了:

propTypes: {
    myArray: React.PropTypes.arrayOf(
        React.propTypes.shape({
            name: React.propTypes.string.isRequired,
            age: React.propTypes.number.isRequired,
            married: React.propTypes.bool
        })
    )
}

把计算和条件判断都交给 render() 方法吧

1. 组件的state中不能出现props
 // BAD:
  constructor (props) {
    this.state = {
      fullName: `${props.firstName} ${props.lastName}`
    };
  }

  render () {
    var fullName = this.state.fullName;
    return (
      <div>
        <h2>{fullName}</h2>
      </div>
    );
  }
// GOOD: 
render () {
  var fullName = `${this.props.firstName} ${this.props.lastName}`;
}

当然,复杂的display logic也应该避免全堆放在render()中,因为那样可能导致整个render()方法变得臃肿,不优雅。我们可以把一些复杂的逻辑通过helper function移出去。

// GOOD: helper function
renderFullName () {
  return `${this.props.firstName} ${this.props.lastName}`;
}

render () {
  var fullName = this.renderFullName();
}
2. 保持state的简洁,不要出现计算得来的state
// WRONG:
  constructor (props) {
    this.state = {
      listItems: [1, 2, 3, 4, 5, 6],
      itemsNum: this.state.listItems.length
    };
  }
  render() {
      return (
          <div>
              <span>{this.state.itemsNum}</span>
          </div>
      )
  }
// Right:
render () {
  var itemsNum = this.state.listItems.length;
}
3. 能用三元判断符,就不用If,直接放在render()里
// BAD: 
renderSmilingStatement () {
    if (this.state.isSmiling) {
        return <span>is smiling</span>;
    }else {
        return '';
    }
},

render () {
  return <div>{this.props.name}{this.renderSmilingStatement()}</div>;
}
// GOOD: 
render () {
  return (
    <div>
      {this.props.name}
      {(this.state.smiling)
        ? <span>is smiling</span>
        : null
      }
    </div>
  );
}
4. 布尔值都不能搞定的,交给IIFE吧

Immediately-invoked function expression

return (
  <section>
    <h1>Color</h1>
    <h3>Name</h3>
    <p>{this.state.color || "white"}</p>
    <h3>Hex</h3>
    <p>
      {(() => {
        switch (this.state.color) {
          case "red":   return "#FF0000";
          case "green": return "#00FF00";
          case "blue":  return "#0000FF";
          default:      return "#FFFFFF";
        }
      })()}
    </p>
  </section>
);
5. 不要把display logic写在componentWillReceivePropscomponentWillMount中,把它们都移到render()中去。

如何动态处理 classNames

1. 使用布尔值
// BAD:
constructor () {
    this.state = {
      classes: []
    };
  }

  handleClick () {
    var classes = this.state.classes;
    var index = classes.indexOf('active');

    if (index != -1) {
      classes.splice(index, 1);
    } else {
      classes.push('active');
    }

    this.setState({ classes: classes });
  }
// GOOD:
  constructor () {
    this.state = {
      isActive: false
    };
  }

  handleClick () {
    this.setState({ isActive: !this.state.isActive });
  }
2. 使用classnames这个小工具来拼接classNames:
// BEFORE:
var Button = React.createClass({
  render () {
    var btnClass = 'btn';
    if (this.state.isPressed) btnClass += ' btn-pressed';
    else if (this.state.isHovered) btnClass += ' btn-over';
    return <button className={btnClass}>{this.props.label}</button>;
  }
});
// AFTER:
var classNames = require('classnames');

var Button = React.createClass({
  render () {
    var btnClass = classNames({
      'btn': true,
      'btn-pressed': this.state.isPressed,
      'btn-over': !this.state.isPressed && this.state.isHovered
    });
    return <button className={btnClass}>{this.props.label}</button>;
  }
});

未完待续...

查看原文

赞 85 收藏 329 评论 14

灯盏细辛 赞了回答 · 2017-07-24

解决jquery add()和append()的区别是什么?各自的使用场景是什么?

.add()主要是操作元素集合用的,跟它反向的操作是.not(),功能相似的还有.end().addBack()这些,个人觉得比较主要的应用场景是在写大段的链式时,打个比方,我可以用:

$('#part1').addClass('music').add('#part2').not('.selected').hide();

来表现一段比较复杂的操作。


.append()主要是操作DOM树用的,当你需要把什么东西插到页面里时,用它,除此之外好像没啥可说的了……好吧强行说一个,之前看有人写类似$('#targ').append('<div>');这种,其实也可以反过来写,就是$('<div>').appendTo('#targ');这样的。

关注 3 回答 3

灯盏细辛 赞了文章 · 2017-05-24

浅谈 Vue 项目优化

好久不写博文了,本文作为我使用半年 vue 框架的经验小结,随便谈谈,且本文只适用于 vue-cli 初始化的项目或依赖于 webpack 打包的项目。

前几天看到大家说 vue 项目越大越难优化,带来很多痛苦,这是避免不了的,问题终究要解决,框架的性能是没有问题的,各大测试网站都有相关数据。下面进入正题

基础优化

所谓的基础优化是任何 web 项目都要做的,并且是问题的根源。HTML,CSS,JS 是第一步要优化的点

分别对应到 .vue 文件内的,<template>,<style>,<script>,下面逐个谈下 vue 项目里都有哪些值得优化的点

template

语义化标签,避免乱嵌套,合理命名属性等等标准推荐的东西就不谈了。

模板部分帮助我们展示结构化数据,vue 通过数据驱动视图,主要注意一下几点

  • v-show,v-if 用哪个?在我来看要分两个维度去思考问题,第一个维度是权限问题,只要涉及到权限相关的展示无疑要用 v-if,第二个维度在没有权限限制下根据用户点击的频次选择,频繁切换的使用 v-show,不频繁切换的使用 v-if,这里要说的优化点在于减少页面中 dom 总数,我比较倾向于使用 v-if,因为减少了 dom 数量,加快首屏渲染,至于性能方面我感觉肉眼看不出来切换的渲染过程,也不会影响用户的体验。
  • 不要在模板里面写过多的表达式与判断 v-if="isShow && isAdmin && (a || b)",这种表达式虽说可以识别,但是不是长久之计,当看着不舒服时,适当的写到 methods 和 computed 里面封装成一个方法,这样的好处是方便我们在多处判断相同的表达式,其他权限相同的元素再判断展示的时候调用同一个方法即可。
  • 循环调用子组件时添加 key,key 可以唯一标识一个循环个体,可以使用例如 item.id 作为 key,假如数组数据是这样的 ['a' , 'b', 'c', 'a'],使用 :key="item" 显然没有意义,更好的办法就是在循环的时候 (item, index) in arr,然后 :key="index"来确保 key 的唯一性。

style

  • 将样式文件放在 vue 文件内还是外?讨论起来没有意义,重点是按模块划分,我的习惯是放在 vue 文件内部,方便写代码是在同一个文件里跳转上下对照,无论内外建议加上 <style scoped> 将样式文件锁住,目的很简单,再好用的标准也避免不了多人开发的麻烦,约定命名规则也可能会冲突,锁定区域后尽量采用简短的命名规则,不需要 .header-title__text 之类的 class,直接 .title 搞定。
  • 为了和上一条作区分,说下全局的样式文件,全局的样式文件,尽量抽象化,既然不在每一个组件里重复写,就尽量通用,这部分抽象做的越好说明你的样式文件体积越小,复用率越高。建议将复写组件库如 Element 样式的代码也放到全局中去。
  • 不使用 float 布局,之前看到很多人封装了 .fl -- float: left 到全局文件里去,然后又要 .clear,现在的浏览器还不至于弱到非要用 float 去兼容,完全可以 flex,grid 兼容性一般,功能其实 flex 布局都可以实现,float 会带来布局上的麻烦,用过的都知道不相信解释坑了。

至于其他通用的规范这里不赘述,相关文章很多。

script

这部分也是最难优化的点,说下个人意见吧。

  • 多人开发时尽量保持每个组件 export default {} 内的方法顺序一致,方便查找对应的方法。我个人习惯 data、props、钩子、watch、computed、components。
  • data 里要说的就是初始化数据的结构尽量详细,命名清晰,简单易懂,避免无用的变量,isEditing 实际可以代表两个状态,true 或 false,不要再去定义 notEditing 来控制展示,完全可以在模板里 {{ isEditing ? 编辑中 : 保存 }}
  • props 父子组件传值时尽量 :width="" :heigth="" 不要 :option={},细化的好处是只传需要修改的参数,在子组件 props 里加数据类型,是否必传,以及默认值,便于排查错误,让传值更严谨。
  • 钩子理解好生命周期的含义就好,什么时间应该请求,什么时间注销方法,哪些方法需要注销。简单易懂,官网都有写。
  • metheds 中每一个方法一定要简单,只做一件事,尽量封装可复用的简短的方法,参数不易过多。如果十分依赖 lodash 开发,methed 看着会简洁许多,代价就是整体的 bundle 体积会大,假如项目仅仅用到小部分方法可以局部引入 loadsh,不想用 lodash 的话可以自己封装一个 util.js 文件
  • watch 和 computed 用哪个的问题看官网的例子,计算属性主要是做一层 filter 转换,切忌加一些调用方法进去,watch 的作用就是监听数据变化去改变数据或触发事件如 this.$store.dispatch('update', { ... })

组件优化

vue 的组件化深受大家喜爱,到底组件拆到什么程度算是合理,还要因项目大小而异,小型项目可以简单几个组件搞定,甚至不用 vuex,axios 等等,如果规模较大就要细分组件,越细越好,包括布局的封装,按钮,表单,提示框,轮播等,推荐看下 Element 组件库的代码,没时间写这么详细可以直接用 Element 库,分几点进行优化

  • 组件有明确含义,只处理类似的业务。复用性越高越好,配置性越强越好。
  • 自己封装组件还是遵循配置 props 细化的规则。
  • 组件分类,我习惯性的按照三类划分,page、page-item 和 layout,page 是路由控制的部分,page-item 属于 page 里各个布局块如 banner、side 等等,layout 里放置多个页面至少出现两次的组件,如 icon, scrollTop 等

vue-router 和 vuex 优化

vue-router 除了切换路由,用的最多的是处理权限的逻辑,关于权限的控制这里不赘述,相关 demo 和文章有许多,那么说到优化,值得一提的就是组件懒加载

中文官网链接如上,例子如下

const Foo = r => require.ensure([], () => r(require('./Foo.vue')), 'group-foo')
const Bar = r => require.ensure([], () => r(require('./Bar.vue')), 'group-foo')
const Baz = r => require.ensure([], () => r(require('./Baz.vue')), 'group-foo')

这段代码将 Foo, Bar, Baz 三个组件打包进了名为 group-foo 的 chunk 文件,当然啦是 js 文件

其余部分正常写就可以,在网站加载时会自动解析需要加载哪个 chunk,虽然分别打包的总体积会变大,但是单看请求首屏速度的话,快了好多。

vuex 面临的问题和解决方案有几点

  • 当网站足够大时,一个状态树下,根的部分字段繁多,解决这个问题就要模块化 vuex,官网提供了模块化方案,允许我们在初始化 vuex 的时候配置 modules。每一个 module 里面又分别包含 state 、action 等,看似是多个状态树,其实还是基于 rootState 的子树。细分后整个 state 结构就清晰了,管理起来也方便许多。
  • 由于 vuex 的灵活性,带来了编码不统一的情况,完整的闭环是 store.dispatch('action') -> action -> commit -> mutation -> getter -> computed,实际上中间的环节有的可以省略,因为 API 文档提供了以下几个方法 mapState、mapGetters、mapActions、mapMutations,然后在组件里可以直接调取任何一步,还是项目小想怎么调用都可以,项目大的时候,就要考虑 vuex 使用的统一性,我的建议是不论多简单的流程都跑完整个闭环,形成代码的统一,方便后期管理,在我的组件里只允许出现 dispatch 和 mapGetters,其余的流程都在名为 store 的 vuex 文件夹里进行。
  • 基于上面一条,说下每个过程里面要做什么,前后端数据一定会有不一致的地方,或是数据结构,或是字段命名,那么究竟应该在哪一步处理数据转换的逻辑呢?有人会说其实哪一步都可以实现,其实不然,我的建议如下
  1. 在发 dispatch 之前就处理好组件内需要传的参数的数据结构和字段名
  2. 到了 action 允许我们做的事情很多,因为这部支持异步,支持 state, rootState, commit, dispatch, getters,由此可见责任重大,首先如果后端需要部分其他 module 里面的数据,要通过 rootState 取值再整合到原有数据上,下一步发出请求,建议(async await + axios),拿到数据后进行筛选转换,再发送 commit 到 mutation
  3. 这一步是将转换后的数据更新到 state 里,可能会有数据分发的过程(传进一个 object 改变多个 state 中 key 的 value),可以转换数据结构,但是尽量不做字段转换,在上一步做
  4. 此时的 store 已经更新,使用 getter 方法来取值,token: state => state.token,单单的取值,尽量不要做数据转换,需要转换的点在于多个地方用相同的字段,但是结构不同的情况(很少出现)。
  5. 在组件里用 mapGetters 拿到对应的 getter 值。

打包优化

上面说了代码方面的规范和优化,下面说下重点的打包优化,前段时间打包的 vender bundle 足足 1.4M,app bundle 也有 270K,app bundle 可以通过组件懒加载解决,vender 包该怎么解决?

有人会质疑是不是没压缩或依赖包没去重,其实都做了就是看到的 1.4M。

解决方法很简单,打包 vender 时不打包 vue、vuex、vue-router、axios 等,换用国内的 bootcdn 直接引入到根目录的 index.html 中。

例如:

<script data-original="//cdn.bootcss.com/vue/2.2.5/vue.min.js"></script>
<script data-original="//cdn.bootcss.com/vue-router/2.3.0/vue-router.min.js"></script>
<script data-original="//cdn.bootcss.com/vuex/2.2.1/vuex.min.js"></script>
<script data-original="//cdn.bootcss.com/axios/0.15.3/axios.min.js"></script>

在 webpack 里有个 externals,可以忽略不需要打包的库

externals: {
  'vue': 'Vue',
  'vue-router': 'VueRouter',
  'vuex': 'Vuex',
  'axios': 'axios'
}

此时的 vender 包会非常小,如果不够小还可以拆分其他的库,此时增加了请求的数量,但是远比加载一个 1.4M 的 bundle 快的多。

总结

本文谈的优化可以解决部分性能问题,实际开发细节很多,总之按着规范写代码,团队的编码风格尽量统一,处理细节上多加思考,大部分性能问题都能迎刃而解。

文章出自 orange 的 个人博客 http://orangexc.xyz/
查看原文

赞 91 收藏 203 评论 25

灯盏细辛 赞了文章 · 2017-05-10

vue 实现 ios 原生picker 效果(实现思路分析)

以前最早实现了一个类似的时间选择插件,但是适用范围太窄,索性最近要把这个实现方式发布出来,就重写了一个高复用的vue组件。

支持安卓4.0以上,safari 7以上
图片描述

效果预览

gitHub

滚轮部分主要dom结构

<template>
  <div class="pd-select-item">
    <div class="pd-select-line"></div>
    <ul class="pd-select-list">
      <li class="pd-select-list-item">1</li>
    </ul>
    <ul class="pd-select-wheel">
      <li class="pd-select-wheel-item">1</li>
    </ul>
  </div>
</template>

props

 props: {
      data: {
        type: Array,
        required: true
      },
      type: {
        type: String,
        default: 'cycle'
      },
      value: {}
    }

设置css样式 使其垂直居中

.pd-select-line, .pd-select-list, .pd-select-wheel {
    position: absolute;
    left: 0;
    right: 0;
    top: 50%;
    transform: translateY(-50%);
}
.pd-select-list {
    overflow: hidden;
}

滚轮3d样式设置

/* 滚轮盒子 */
.pd-select-wheel {
    transform-style: preserve-3d;
    height: 30px;
}
/* 滚轮单项 */
.pd-select-wheel-item {
    white-space: nowrap;
    text-overflow: ellipsis;
    backface-visibility: hidden;
    position: absolute;
    top: 0px;
    width: 100%;
    overflow: hidden;
}

图片描述

主要注意2个属性 transform-style: preserve-3d; backface-visibility: hidden;
第一个是3d布局,让界面3D化,第二个是让滚轮背后自动隐藏(上图红色部分,背面的dom节点 会自动隐藏)

如何实现3D 滚轮

盒子主要这句css transform: rotate3d(1, 0, 0, x deg);
item主要运用这句css transform: rotate3d(1, 0, 0, xdeg) translate3d(0px, 0px, [x]px);

图片描述

图片描述
图片描述

上面2张图展示了translate3d(0px, 0px, [x]px);这句话的效果 [x]就是圆的半径

图片描述

从上面的图可以看见,我们只需旋转每个dom自身,然后利用translate3d(0px, 0px, [x]px);把每个dom扩展开
就形成了圆环.α就是每个dom自身旋转的角度,因为这里只用了0到180°,所以用了个盒子在装这些dom

行高 和角度计算

图片描述
已知两边和夹角 算第三边长度 ~=34px
http://tool.520101.com/calcul...

无限滚轮实现

/* 滚轮展示大小限定 */
spin: {start: 0, end: 9, branch: 9}

/* 获取spin 数据 */
 getSpinData (index) {
   index = index % this.listData.length
   return this.listData[index >= 0 ? index : index + this.listData.length]
 }
 /* 模运算 获取数组有的索引 这样就构成 圆环了 */

touchend做特殊处理

在touchend 里设置setCSS类型 把滚动数据取整,这样停止的时候就是
一格一格的准确转动到位

 // other code ....
 /* 计算touchEnd移动的整数距离 */
        let endMove = margin
        let endDeg = Math.round(updateDeg / deg) * deg
        if (type === 'end') {
          this.setListTransform(endMove, margin)
          this.setWheelDeg(endDeg)
        } else {
          this.setListTransform(updateMove, margin)
          this.setWheelDeg(updateDeg)
        }
  // other code ....

惯性缓动

// other code ....
setWheelDeg (updateDeg, type, time = 1000) {
        if (type === 'end') {
          this.$refs.wheel.style.webkitTransition = `transform ${time}ms cubic-bezier(0.19, 1, 0.22, 1)`
          this.$refs.wheel.style.webkitTransform = `rotate3d(1, 0, 0, ${updateDeg}deg)`
        } else {
          this.$refs.wheel.style.webkitTransition = ''
          this.$refs.wheel.style.webkitTransform = `rotate3d(1, 0, 0, ${updateDeg}deg)`
        }
      }
setListTransform (translateY = 0, marginTop = 0, type, time = 1000) {
        if (type === 'end') {
          this.$refs.list.style.webkitTransition = `transform ${time}ms cubic-bezier(0.19, 1, 0.22, 1)`
          this.$refs.list.style.webkitTransform = `translateY(${translateY - this.spin.branch * 34}px)`
          this.$refs.list.style.marginTop = `${-marginTop}px`
          this.$refs.list.setAttribute('scroll', translateY)
          console.log('end')
        } else {
          this.$refs.list.style.webkitTransition = ''
          this.$refs.list.style.webkitTransform = `translateY(${translateY - this.spin.branch * 34}px)`
          this.$refs.list.style.marginTop = `${-marginTop}px`
          this.$refs.list.setAttribute('scroll', translateY)
        }
}
// other code ....

获取当前选中值


 /* 在设置完css后获取值  */
 
setStyle (move, type, time) {
   // ...other code
   /* 设置$emit 延迟 */
   setTimeout(() => this.getPickValue(endMove), 1000)
  // ...other code
}

/* 获取选中值 */
      getPickValue (move) {
        let index = Math.abs(move / 34)
        let pickValue = this.getSpinData(index)
        this.$emit('input', pickValue)
      }

初始化设置

 mounted () {
      /* 事件绑定 */
      this.$el.addEventListener('touchstart', this.itemTouchStart)
      this.$el.addEventListener('touchmove', this.itemTouchMove)
      this.$el.addEventListener('touchend', this.itemTouchEnd)
      /* 初始化状态 */
      let index = this.listData.indexOf(this.value)
      if (index === -1) {
        console.warn('当前初始值不存在,请检查后listData范围!!')
        this.setListTransform()
        this.getPickValue(0)
      } else {
        let move = index * 34
        /* 因为往上滑动所以是负 */
        this.setStyle(-move)
        this.setListTransform(-move, -move)
      }

当展示为非无限滚轮的时

这里我们很好判断,就是滚动的距离不能超过原始数的数组长度*34,且不能小于0(实际代码中涉及方向)

/* 根据滚轮类型 line or cycle 判断 updateMove最大距离 */
        if (this.type === 'line') {
          if (updateMove > 0) {
            updateMove = 0
          }
          if (updateMove < -(this.listData.length - 1) * singleHeight) {
            updateMove = -(this.listData.length - 1) * singleHeight
          }
        }
 /* 根据type 控制滚轮显示效果 */
      setHidden (index) {
        if (this.type === 'line') {
          return index < 0 || index > this.listData.length - 1
        } else {
          return false
        }
      },

dom结构也增加了对应的响应

<div class="pd-select-item">
    <div class="pd-select-line"></div>
    <div class="pd-select-list">
      <ul class="pd-select-ul" ref="list">
        <li class="pd-select-list-item" v-for="el,index in renderData " :class="{'hidden':setHidden(el.index)}" :key="index">{{el.value}}</li>
      </ul>
    </div>
    <ul class="pd-select-wheel" ref="wheel">
      <li class="pd-select-wheel-item" :class="{'hidden':setHidden(el.index)}" :style="setWheelItemDeg(el.index)" :index="el.index" v-for="el,index in renderData " :key="index">{{el.value}}</li>
    </ul>
  </div>

如有不明白的地方,请在下方留言,或者邮箱联系.k1868548@163.com

代码还有优化空间,欢迎提出 谢谢

查看原文

赞 27 收藏 76 评论 32

灯盏细辛 赞了文章 · 2017-04-20

React进阶——使用高阶组件(Higher-order Components)优化你的代码

什么是高阶组件

Higher-Order Components (HOCs) are JavaScript functions which add functionality to existing component classes.

通过函数向现有组件类添加逻辑,就是高阶组件。

让我们先来看一个可能是史上最无聊的高阶组件:

function noId() {
  return function(Comp) {
    return class NoID extends Component {
      render() {
        const {id, ...others} = this.props;
        return (
          <Comp {...others}/>
        )
      }
    }
  }
}

const WithoutID = noId()(Comp);

这个例子向我们展示了高阶组件的工作方式:通过函数和闭包,改变已有组件的行为——这里是忽略id属性——而完全不需要修改任何代码。

之所以称之为高阶,是因为在React中,这种嵌套关系会反映到组件树上,层层嵌套就好像高阶函数的function in function一样,如图:

HOC-img

从图上也可以看出,组件树虽然嵌套了多层,但是实际渲染的DOM结构并没有改变。
如果你对这点有疑问,不妨自己写写例子试下,加深对React的理解。现在可以先记下结论:我们可以放心的使用多层高阶组件,甚至重复地调用,而不必担心影响输出的DOM结构。

借助函数的逻辑表现力,高阶组件的用途几乎是无穷无尽的:

适配器

有的时候你需要替换一些已有组件,而新组件接收的参数和原组件并不完全一致。

你可以修改所有使用旧组件的代码来保证传入正确的参数——考虑改行吧如果你真这么想

也可以把新组件做一层封装:

class ListAdapter extends Component {
    mapProps(props) {
        return {/* new props */}
    }
    render() {
        return <NewList {...mapProps(this.props)} />
    }
}

如果有十个组件需要适配呢?如果你不想照着上面写十遍,或许高阶组件可以给你答案

function mapProps(mapFn) {
    return function(Comp) {
        return class extends Component {
            render() {
                return <Comp {...mapFn(this.props)}/>
            }
        }
    } 
}

const ListAdapter = mapProps(mapPropsForNewList)(NewList);

借助高阶组件,关注点被分离得更加干净:只需要关注真正重要的部分——属性的mapping。

这个例子有些价值,却仍然不够打动人,如果你也这么想,请往下看:

处理副作用

纯组件易写易测,越多越好,这是常识。然而在实际项目中,往往有许多的状态和副作用需要处理,最常见的情况就是异步了。

假设我们需要异步加载一个用户列表,通常的代码可能是这样的:

class UserList extends Component {
  constructor(props) {
    super();
    this.state = {
      list: []
    }
  }
  componentDidMount() {
    loadUsers()
      .then(data=> 
        this.setState({list: data.userList})
      )
  }
  render() {
    return (
      <List list={this.state.list} />
    )
  }
  /* other bussiness logics */
}

实际情况中,以上代码往往还会和其它一些业务函数混杂在一起——我们创建了一个业务副作用混杂的、有状态的组件。

如果再来一个书单列表呢?再写一个BookList然后把loadUsers改成loadBooks ?
不仅代码重复,大量有状态和副作用的组件,也使得应用更加难以测试。

也许你会考虑使用Flux。它确实能让你的代码更清晰,但是在有些场景下使用Flux就像大炮打蚊子。比如一个异步的下拉选择框,如果要考虑复用的话,传统的Flux/Reflux几乎无法优雅的处理,Redux稍好一些,但仍然很难做优雅。关于flux/redux的缺点不深入,有兴趣的可以参考Cycle.js作者的文章

回到问题的本源:其实我们只想要一个能复用的异步下拉列表而已啊!

高阶函数试试?

import React, { Component } from 'react';

const DEFAULT_OPTIONS = {
  mapStateToProps: undefined,
  mapLoadingToProps: loading => ({ loading }),
  mapDataToProps: data => ({ data }),
  mapErrorToProps: error => ({ error }),
};
export function connectPromise(options) {
  return (Comp) => {
    const finalOptions = {
      ...DEFAULT_OPTIONS,
      ...options,
    };
    const {
      promiseLoader,
      mapLoadingToProps,
      mapStateToProps,
      mapDataToProps,
      mapErrorToProps,
    } = finalOptions;

    class AsyncComponent extends Component {
      constructor(props) {
        super(props);
        this.state = {
          loading: true,
          data: undefined,
          error: undefined,
        };
      }
      componentDidMount() {
        promiseLoader(this.props)
          .then(
            data => this.setState({ data, loading: false }),
            error => this.setState({ error, loading: false }),
          );
      }
      render() {
        const { data, error, loading } = this.state;

        const dataProps = data ? mapDataToProps(data) : undefined;
        const errorProps = error ? mapErrorToProps(error) : undefined;

        return (
          <Comp
            {...mapLoadingToProps(loading)}
            {...dataProps}
            {...errorProps}
            {...this.props}
          />
        );
      }
    }

    return AsyncComponent;
  };
}


const UserList = connectPromise({
    promiseLoader: loadUsers,
    mapDataToProps: result=> ({list: result.userList})
})(List); //List can be a pure component

const BookList = connectPromise({
    promiseLoader: loadBooks,
    mapDataToProps: result=> ({list: result.bookList})
})(List);

不仅大大减少了重复代码,还把散落各处的异步逻辑装进了可以单独管理和测试的笼子,在业务场景中,只需要纯组件 + 配置 就能实现相同的功能——而无论是纯组件还是配置,都是对单元测试友好的,至少比异步组件友好多了。

使用curry & compose

高阶组件的另一个亮点,就是对函数式编程的友好。你可能已经注意到,目前我写的所有高阶函数,都是形如:

config => {
    return Component=> {
        return HighOrderCompoent
    }
}

表示为config=> Component=> Component

写成嵌套的函数是为了手动curry化,而参数的顺序(为什么不是Component=> config=> Component),则是为了组合方便。关于curry与compose的使用,可以移步我的另一篇blog

举个栗子,前面讲了适配器和异步,我们可以很快就组合出两者的结合体:使用NewList的异步用户列表

UserList = compose(
  connectPromise({
    promiseLoader: loadUsers,
    mapResultToProps: result=> ({list: result.userList})
  }),
  mapProps(mapPropsForNewList)
)(NewList);

总结

在团队内部分享里,我的总结是三个词 Easy, Light-weight & Composable.

其实高阶组件并不是什么新东西,本质上就是Decorator模式在React的一种实现,但在相当一段时间内,这个优秀的模式都被人忽略。在我看来,大部分使用mixinclass extends的地方,高阶组件都是更好的方案——毕竟组合优于继承,而mixin——个人觉得没资格参与讨论。

使用高阶组件还有两个好处:

  1. 适用范围广,它不需要es6或者其它需要编译的特性,有函数的地方,就有HOC。

  2. Debug友好,它能够被React组件树显示,所以可以很清楚地知道有多少层,每层做了什么。相比之下无论是mixin还是继承,都显得非常隐晦。

值得庆幸的是,社区也明显注意到了高阶组件的价值,无论是大家非常熟悉的react-reduxconnect函数,还是redux-form,高阶组件的应用开始随处可见。

下次当你想写mixinclass extends的时候,不妨也考虑下高阶组件。

查看原文

赞 29 收藏 88 评论 18

灯盏细辛 赞了文章 · 2017-04-06

JSON:如果你愿意一层一层剥开我的心,你会发现...这里水很深——深入理解JSON

我们先来看一个JS中常见的JS对象序列化成JSON字符串的问题,请问,以下JS对象通过JSON.stringify后的字符串是怎样的?先不要急着复制粘贴到控制台,先自己打开一个代码编辑器或者纸,写写看,写完再去仔细对比你的控制台输出,如果有误记得看完全文并评论,哈哈。

var friend={
    firstName: 'Good',
    'lastName': 'Man',
    'address': undefined,
    'phone': ["1234567",undefined],
    'fullName': function(){
        return this.firstName + ' ' + this.lastName;
    }
};

JSON.stringify(friend);//这一行返回什么呢?

第二个问题,如果我想在最终JSON字符串将这个'friend'的姓名全部变成大写字母,也就是把"Good"变成"GOOD",把"Man"变成"MAN",那么可以怎么做?

基于以上两个问题,我们再追本溯源问一下,JSON究竟是什么东西?为什么JSON就是易于数据交换?JSON和JS对象的区别?JS中JSON.parseJSON.stringify和不常见的toJSON,这几个函数的参数和处理细节到底是怎样的?

欢迎进入本次“深挖JSON之旅”,下文将从以下几个方面去理解JSON:

  • 首先是对“JSON是一种轻量的数据交换格式”的理解;

  • 然后来看经常被混为一谈的JSON和JS对象的区别;

  • 最后我们再来看JS中这几个JSON相关函数具体的执行细节。

希望全文能让如之前的我一样对JSON一知半解的亲能说清楚JSON是什么,也能熟练运用JSON,不看控制台就知道JS对象序列化成JSON字符串后输出是啥。

一、JSON是一种格式,基于文本,优于轻量,用于交换数据

如果没有去过JSON的官方介绍可以去一下这里,官方介绍第一、二段已经很清楚地表述了JSON是什么,我将JSON是什么提炼成以下几个方面:

1. 一种数据格式

什么是格式?就是规范你的数据要怎么表示,举个栗子,有个人叫“二百六”,身高“160cm”,体重“60kg”,现在你要将这个人的这些信息传给别人或者别的什么东西,你有很多种选择:

  • 姓名“二百六”,身高“160cm”,体重“60kg”

  • name="二百六"&height="160cm"&weight="60kg"

  • <person><name>二百六</name><height>160</height><weight>60</weight></person>

  • {"name":"二百六","height":160,"weight":60}

  • ... ...

以上所有选择,传递的数据是一样的,但是你可以看到形式是可以各式各样的,这就是各种不同格式化后的数据,JSON是其中一种表示方式。

2. 基于文本的数据格式

JSON是基于文本的数据格式,相对于基于二进制的数据,所以JSON在传递的时候是传递符合JSON这种格式(至于JSON的格式是什么我们第二部分再说)的字符串,我们常会称为“JSON字符串”。

3. 轻量级的数据格式

在JSON之前,有一个数据格式叫xml,现在还是广泛在用,但是JSON更加轻量,如xml需要用到很多标签,像上面的例子中,你可以明显看到xml格式的数据中标签本身占据了很多空间,而JSON比较轻量,即相同数据,以JSON的格式占据的带宽更小,这在有大量数据请求和传递的情况下是有明显优势的。

4. 被广泛地用于数据交换

轻量已经是一个用于数据交换的优势了,但更重要的JSON是易于阅读、编写和机器解析的,即这个JSON对人和机器都是友好的,而且又轻,独立于语言(因为是基于文本的),所以JSON被广泛用于数据交换。

以前端JS进行ajax的POST请求为例,后端PHP处理请求为例:

  1. 前端构造一个JS对象,用于包装要传递的数据,然后将JS对象转化为JSON字符串,再发送请求到后端;

  2. 后端PHP接收到这个JSON字符串,将JSON字符串转化为PHP对象,然后处理请求。

可以看到,相同的数据在这里有3种不同的表现形式,分别是前端的JS对象、传输的JSON字符串、后端的PHP对象,JS对象和PHP对象明显不是一个东西,但是由于大家用的都是JSON来传递数据,大家都能理解这种数据格式,都能把JSON这种数据格式很容易地转化为自己能理解的数据结构,这就方便啦,在其他各种语言环境中交换数据都是如此。

二、JSON和JS对象之间的“八卦”

很多时候都听到“JSON是JS的一个子集”这句话,而且这句话我曾经也一直这么认为,每个符合JSON格式的字符串你解析成js都是可以的,直到后来发现了一个奇奇怪怪的东西...

1. 两个本质不同的东西为什么那么密切

JSON和JS对象本质上完全不是同一个东西,就像“斑马线”和“斑马”,“斑马线”基于“斑马”身上的条纹来呈现和命名,但是斑马是活的,斑马线是非生物。

同样,"JSON"全名"JavaScript Object Notation",所以它的格式(语法)是基于JS的,但它就是一种格式,而JS对象是一个实例,是存在于内存的一个东西。

说句玩笑话,如果JSON是基于PHP的,可能就叫PON了,形式可能就是这样的了['propertyOne' => 'foo', 'propertyTwo' => 42,],如果这样,那么JSON可能现在是和PHP比较密切了。

此外,JSON是可以传输的,因为它是文本格式,但是JS对象是没办法传输的,在语法上,JSON也会更加严格,但是JS对象就很松了。

那么两个不同的东西为什么那么密切,因为JSON毕竟是从JS中演变出来的,语法相近。

2. JSON格式别JS对象语法表现上严格在哪

先就以“键值对为表现的对象”形式上,对比下两者的不同,至于JSON还能以怎样的形式表现,对比完后再罗列。

对比内容JSONJS对象
键名必须是加双引号可允许不加、加单引号、加双引号
属性值只能是数值(10进制)、字符串(双引号)、布尔值和null,
也可以是数组或者符合JSON要求的对象,
不能是函数、NaN, Infinity, -Infinity和undefined
爱啥啥
逗号问题最后一个属性后面不能有逗号可以
数值前导0不能用,小数点后必须有数字没限制

可以看到,相对于JS对象,JSON的格式更严格,所以大部分写的JS对象是不符合JSON的格式的。

以下代码引用自这里

var obj1 = {}; // 这只是 JS 对象

// 可把这个称做:JSON 格式的 JavaScript 对象 
var obj2 = {"width":100,"height":200,"name":"rose"};

// 可把这个称做:JSON 格式的字符串
var str1 = '{"width":100,"height":200,"name":"rose"}';

// 这个可叫 JSON 格式的数组,是 JSON 的稍复杂一点的形式
var arr = [
    {"width":100,"height":200,"name":"rose"},
    {"width":100,"height":200,"name":"rose"},
    {"width":100,"height":200,"name":"rose"},
];
        
// 这个可叫稍复杂一点的 JSON 格式的字符串     
var str2='['+
    '{"width":100,"height":200,"name":"rose"},'+
    '{"width":100,"height":200,"name":"rose"},'+
    '{"width":100,"height":200,"name":"rose"},'+
']';

另外,除了常见的“正常的”JSON格式,要么表现为一个对象形式{...},要么表现为一个数组形式[...],任何单独的一个10进制数值、双引号字符串、布尔值和null都是有效符合JSON格式的。

这里有完整的JSON语法参考

3. 一个有意思的地方,JSON不是JS的子集

首先看下面的代码,你可以copy到控制台执行下:

var code = '"\u2028\u2029"';
JSON.parse(code); // works fine
eval(code); // fails

这两个字符\u2028\u2029分别表示行分隔符和段落分隔符,JSON.parse可以正常解析,但是当做js解析时会报错。

三、这几个JS中的JSON函数,弄啥嘞

在JS中我们主要会接触到两个和JSON相关的函数,分别用于JSON字符串和JS数据结构之间的转化,一个叫JSON.stringify,它很聪明,聪明到你写的不符合JSON格式的JS对象都能帮你处理成符合JSON格式的字符串,所以你得知道它到底干了什么,免得它只是自作聪明,然后让你Debug long time;另一个叫JSON.parse,用于转化json字符串到JS数据结构,它很严格,你的JSON字符串如果构造地不对,是没办法解析的。

而它们的参数不止一个,虽然我们经常用的时候只传入一个参数。

此外,还有一个toJSON函数,我们较少看到,但是它会影响JSON.stringify

1. 将JS数据结构转化为JSON字符串 —— JSON.stringify

这个函数的函数签名是这样的:

JSON.stringify(value[, replacer [, space]])

下面将分别展开带1~3个参数的用法,最后是它在序列化时做的一些“聪明”的事,要特别注意。

1.1 基本使用 —— 仅需一个参数

这个大家都会使用,传入一个JSON格式的JS对象或者数组,JSON.stringify({"name":"Good Man","age":18})返回一个字符串"{"name":"Good Man","age":18}"

可以看到本身我们传入的这个JS对象就是符合JSON格式的,用的双引号,也没有JSON不接受的属性值,那么如果像开头那个例子中的一样,how to play?不急,我们先举简单的例子来说明这个函数的几个参数的意义,再来说这个问题。

1.2 第二个参数可以是函数,也可以是一个数组

  • 如果第二个参数是一个函数,那么序列化过程中的每个属性都会被这个函数转化和处理

  • 如果第二个参数是一个数组,那么只有包含在这个数组中的属性才会被序列化到最终的JSON字符串中

  • 如果第二个参数是null,那作用上和空着没啥区别,但是不想设置第二个参数,只是想设置第三个参数的时候,就可以设置第二个参数为null

这第二个参数若是函数

var friend={
    "firstName": "Good",
    "lastName": "Man",
    "phone":"1234567",
    "age":18
};

var friendAfter=JSON.stringify(friend,function(key,value){
    if(key==="phone")
        return "(000)"+value;
    else if(typeof value === "number")
        return value + 10;
    else
        return value; //如果你把这个else分句删除,那么结果会是undefined
});

console.log(friendAfter);
//输出:{"firstName":"Good","lastName":"Man","phone":"(000)1234567","age":28}

如果制定了第二个参数是函数,那么这个函数必须对每一项都有返回,这个函数接受两个参数,一个键名,一个是属性值,函数必须针对每一个原来的属性值都要有新属性值的返回。

那么问题来了,如果传入的不是键值对的对象形式,而是方括号的数组形式呢?,比如上面的friend变成这样:friend=["Jack","Rose"],那么这个逐属性处理的函数接收到的key和value又是什么?如果是数组形式,那么key是索引,而value是这个数组项,你可以在控制台在这个函数内部打印出来这个key和value验证,记得要返回value,不然会出错。

这第二个参数若是数组

var friend={
    "firstName": "Good",
    "lastName": "Man",
    "phone":"1234567",
    "age":18
};

//注意下面的数组有一个值并不是上面对象的任何一个属性名
var friendAfter=JSON.stringify(friend,["firstName","address","phone"]);

console.log(friendAfter);
//{"firstName":"Good","phone":"1234567"}
//指定的“address”由于没有在原来的对象中找到而被忽略

如果第二个参数是一个数组,那么只有在数组中出现的属性才会被序列化进结果字符串,只要在这个提供的数组中找不到的属性就不会被包含进去,而这个数组中存在但是源JS对象中不存在的属性会被忽略,不会报错。

1.3 第三个参数用于美化输出 —— 不建议用

指定缩进用的空白字符,可以取以下几个值:

  • 是1-10的某个数字,代表用几个空白字符

  • 是字符串的话,就用该字符串代替空格,最多取这个字符串的前10个字符

  • 没有提供该参数 等于 设置成null 等于 设置一个小于1的数

var friend={
    "firstName": "Good",
    "lastName": "Man",
    "phone":{"home":"1234567","work":"7654321"}
};

//直接转化是这样的:
//{"firstName":"Good","lastName":"Man","phone":{"home":"1234567","work":"7654321"}}

var friendAfter=JSON.stringify(friend,null,4);
console.log(friendAfter);
/*
{
    "firstName": "Good",
    "lastName": "Man",
    "phone": {
        "home": "1234567",
        "work": "7654321"
    }
}
*/

var friendAfter=JSON.stringify(friend,null,"HAHAHAHA");
console.log(friendAfter);
/*
{
HAHAHAHA"firstName": "Good",
HAHAHAHA"lastName": "Man",
HAHAHAHA"phone": {
HAHAHAHAHAHAHAHA"home": "1234567",
HAHAHAHAHAHAHAHA"work": "7654321"
HAHAHAHA}
}
*/

var friendAfter=JSON.stringify(friend,null,"WhatAreYouDoingNow");
console.log(friendAfter);
/* 最多只取10个字符
{
WhatAreYou"firstName": "Good",
WhatAreYou"lastName": "Man",
WhatAreYou"phone": {
WhatAreYouWhatAreYou"home": "1234567",
WhatAreYouWhatAreYou"work": "7654321"
WhatAreYou}
}
*/

笑笑就好,别这样用,序列化是为了传输,传输就是能越小越好,加莫名其妙的缩进符,解析困难(如果是字符串的话),也弱化了轻量化这个特点。

1.4 注意这个函数的“小聪明”(重要)

如果有其他不确定的情况,那么最好的办法就是"Have a try",控制台做下实验就明了。

  • 键名不是双引号的(包括没有引号或者是单引号),会自动变成双引号;字符串是单引号的,会自动变成双引号

  • 最后一个属性后面有逗号的,会被自动去掉

  • 非数组对象的属性不能保证以特定的顺序出现在序列化后的字符串中
    这个好理解,也就是对非数组对象在最终字符串中不保证属性顺序和原来一致

  • 布尔值、数字、字符串的包装对象在序列化过程中会自动转换成对应的原始值
    也就是你的什么new String("bala")会变成"bala"new Number(2017)会变成2017

  • undefined、任意的函数(其实有个函数会发生神奇的事,后面会说)以及 symbol 值(symbol详见ES6对symbol的介绍)

    • 出现在非数组对象的属性值中:在序列化过程中会被忽略

    • 出现在数组中时:被转换成 null

JSON.stringify({x: undefined, y: function(){return 1;}, z: Symbol("")});
//出现在非数组对象的属性值中被忽略:"{}"
JSON.stringify([undefined, Object, Symbol("")]);
//出现在数组对象的属性值中,变成null:"[null,null,null]"
  • NaN、Infinity和-Infinity,不论在数组还是非数组的对象中,都被转化为null

  • 所有以 symbol 为属性键的属性都会被完全忽略掉,即便 replacer 参数中强制指定包含了它们

  • 不可枚举的属性会被忽略

2. 将JSON字符串解析为JS数据结构 —— JSON.parse

这个函数的函数签名是这样的:

JSON.parse(text[, reviver])

如果第一个参数,即JSON字符串不是合法的字符串的话,那么这个函数会抛出错误,所以如果你在写一个后端返回JSON字符串的脚本,最好调用语言本身的JSON字符串相关序列化函数,而如果是自己去拼接实现的序列化字符串,那么就尤其要注意序列化后的字符串是否是合法的,合法指这个JSON字符串完全符合JSON要求的严格格式

值得注意的是这里有一个可选的第二个参数,这个参数必须是一个函数,这个函数作用在属性已经被解析但是还没返回前,将属性处理后再返回。

var friend={
    "firstName": "Good",
    "lastName": "Man",
    "phone":{"home":"1234567","work":["7654321","999000"]}
};

//我们先将其序列化
var friendAfter=JSON.stringify(friend);
//'{"firstName":"Good","lastName":"Man","phone":{"home":"1234567","work":["7654321","999000"]}}'

//再将其解析出来,在第二个参数的函数中打印出key和value
JSON.parse(friendAfter,function(k,v){
    console.log(k);
    console.log(v);
    console.log("----");
});
/*
firstName
Good
----
lastName
Man
----
home
1234567
----
0
7654321
----
1
999000
----
work
[]
----
phone
Object
----

Object
----
*/

仔细看一下这些输出,可以发现这个遍历是由内而外的,可能由内而外这个词大家会误解,最里层是内部数组里的两个值啊,但是输出是从第一个属性开始的,怎么就是由内而外的呢?

这个由内而外指的是对于复合属性来说的,通俗地讲,遍历的时候,从头到尾进行遍历,如果是简单属性值(数值、字符串、布尔值和null),那么直接遍历完成,如果是遇到属性值是对象或者数组形式的,那么暂停,先遍历这个子JSON,而遍历的原则也是一样的,等这个复合属性遍历完成,那么再完成对这个属性的遍历返回。

本质上,这就是一个深度优先的遍历。

有两点需要注意:

  • 如果 reviver 返回 undefined,则当前属性会从所属对象中删除,如果返回了其他值,则返回的值会成为当前属性新的属性值。

  • 你可以注意到上面例子最后一组输出看上去没有key,其实这个key是一个空字符串,而最后的object是最后解析完成对象,因为到了最上层,已经没有真正的属性了。

3. 影响 JSON.stringify 的神奇函数 —— object.toJSON

如果你在一个JS对象上实现了toJSON方法,那么调用JSON.stringify去序列化这个JS对象时,JSON.stringify会把这个对象的toJSON方法返回的值作为参数去进行序列化。

var info={
    "msg":"I Love You",
    "toJSON":function(){
        var replaceMsg=new Object();
        replaceMsg["msg"]="Go Die";
        return replaceMsg;
    }
};

JSON.stringify(info);
//出si了,返回的是:'"{"msg":"Go Die"}"',说好的忽略函数呢

这个函数就是这样子的。

其实Date类型可以直接传给JSON.stringify做参数,其中的道理就是,Date类型内置了toJSON方法。

四、小结以及关于兼容性的问题

到这里终于把,JSON和JS中的JSON,梳理了一遍,也对里面的细节和注意点进行了一次遍历,知道JSON是一种语法上衍生于JS语言的一种轻量级的数据交换格式,也明白了JSON相对于一般的JS数据结构(尤其是对象)的差别,更进一步,仔细地讨论了JS中关于JSON处理的3个函数和细节。

不过遗憾的是,以上所用的3个函数,不兼容IE7以及IE7之前的浏览器。有关兼容性的讨论,留待之后吧。如果想直接在应用上解决兼容性,那么可以套用JSON官方的js,可以解决。

如有纰漏,欢迎留言指出。

查看原文

赞 14 收藏 77 评论 7

灯盏细辛 赞了文章 · 2017-02-14

前端面试题小集

一、一个页面上两个div左右铺满整个浏览器,要保证左边的div一直为100px,右边的div跟随浏览器大小变化(比如浏览器为500,右边div为400,浏览器为900,右边div为800),请写出大概的css代码。

1.使用flex
//html
<div class='box'><div class='left'></div> <div class='right'></div></div>
//css
.box{
 width: 400px;
 height: 100px;
 display: flex;
 flex-direction: row;
 align-items: center;
 border: 1px solid #c3c3c3;
}
.left {
 flex-basis:100px;
 -webkit-flex-basis: 100px; /* Safari 6.1+ */
 background-color: red;
 height: 100%;
 
}
.right {
 background-color: blue;
 flex-grow: 1;
}

在线demo

2.浮动布局
<div id="left">Left sidebar</div>
<div id="content">Main Content</div> 
<style type="text/css">
 *{
 margin: 0;
 padding: 0;
}

#left {
 float: left;
 width: 220px;
 background-color: green;
}
#content {
 background-color: orange;
 margin-left: 220px;/*==等于左边栏宽度==*/
}
</style>

demo
更多布局栗子 请移步

二、请写出一些前端性能优化的方式,越多越好

1.减少dom操作
2.部署前,图片压缩,代码压缩
3.优化js代码结构,减少冗余代码
4.减少http请求,合理设置 HTTP缓存
5.使用内容分发cdn加速
6.静态资源缓存
7.图片延迟加载

三、一个页面从输入 URL 到页面加载显示完成,这个过程中都发生了什么?(流程说的越详细越好)

输入地址
1.浏览器查找域名的 IP 地址
2.这一步包括 DNS 具体的查找过程,包括:浏览器缓存->系统缓存->路由器缓存...
3.浏览器向 web 服务器发送一个 HTTP 请求
4.服务器的永久重定向响应(从 http://example.comhttp://www.example.com
5.浏览器跟踪重定向地址
6.服务器处理请求
7.服务器返回一个 HTTP 响应
8.浏览器显示 HTML
9.浏览器发送请求获取嵌入在 HTML 中的资源(如图片、音频、视频、CSS、JS等等)
10.浏览器发送异步请求

四、请大概描述下页面访问cookie的限制条件

1.跨域问题
2.设置了HttpOnly

五、描述浏览器重绘和回流,哪些方法能够改善由于dom操作产生的回流

1.直接改变className,如果动态改变样式,则使用cssText

// 不好的写法
var left = 1;
var top = 1;
el.style.left = left + "px";
el.style.top = top + "px";// 比较好的写法
el.className += " className1";
 
// 比较好的写法
el.style.cssText += "; 
left: " + left + "px; 
top: " + top + "px;";

2.让要操作的元素进行”离线处理”,处理完后一起更新
a) 使用DocumentFragment进行缓存操作,引发一次回流和重绘;
b) 使用display:none技术,只引发两次回流和重绘;
c) 使用cloneNode(true or false) 和 replaceChild 技术,引发一次回流和重绘

六、vue生命周期钩子

1.beforcreate
2.created
3.beformount
4.mounted
5.beforeUpdate
6.updated
7.actived
8.deatived
9.beforeDestroy
10.destroyed

七、js跨域请求的方式,能写几种是几种

1、通过jsonp跨域
2、通过修改document.domain来跨子域
3、使用window.name来进行跨域
4、使用HTML5中新引进的window.postMessage方法来跨域传送数据(ie 67 不支持)
5、CORS 需要服务器设置header :Access-Control-Allow-Origin。
6、nginx反向代理 这个方法一般很少有人提及,但是他可以不用目标服务器配合,不过需要你搭建一个中转nginx服务器,用于转发请求

八、对前端工程化的理解

开发规范
模块化开发
组件化开发
组件仓库
性能优化
项目部署
开发流程
开发工具

九, js深度复制的方式

1.使用jq的$.extend(true, target, obj)
2.newobj = Object.create(sourceObj),// 但是这个是有个问题就是 newobj的更改不会影响到 sourceobj但是 sourceobj的更改会影响到newObj
3.newobj = JSON.parse(JSON.stringify(sourceObj))

十、js设计模式

总体来说设计模式分为三大类:
创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。
结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模

详细说明 请移步alloyteam的文章

十一、图片预览

<input type="file" name="file" onchange="showPreview(this)" />
<img id="portrait" data-original="" width="70" height="75">
function showPreview(source) {
  var file = source.files[0];
  if(window.FileReader) {
      var fr = new FileReader();
      fr.onloadend = function(e) {
        document.getElementById("portrait").src = e.target.result;
      };
      fr.readAsDataURL(file);
  }
}

十二、扁平化多维数组

1、老方法
 var result = []
 function unfold(arr){
     for(var i=0;i< arr.length;i++){
      if(typeof arr[i]=="object" && arr[i].length>1) {
       unfold(arr[i]);
     } else {         
       result.push(arr[i]);
     }
  }
}
 
 var arr = [1,3,4,5,[6,[0,1,5],9],[2,5,[1,5]],[5]];
 unfold(arr)
2、使用tostring
var c=[1,3,4,5,[6,[0,1,5],9],[2,5,[1,5]],[5]];
var b = c.toString().split(',') 
3、使用reduce函数
var arr=[1,3,4,5,[6,[0,1,5],9],[2,5,[1,5]],[5]];
const flatten = arr => arr.reduce((a, b) => a.concat(Array.isArray(b) ? flatten(b) : b), []);
var result = flatten(arr)

十三、iframe有那些缺点?

iframe会阻塞主页面的Onload事件;
搜索引擎的检索程序无法解读这种页面,不利于SEO;
iframe和主页面共享连接池,而浏览器对相同域的连接有限制,所以会影响页面的并行加载。
使用iframe之前需要考虑这两个缺点。如果需要使用iframe,最好是通过javascript动态给iframe添加src属性值,这样可以绕开以上两个问题。

查看原文

赞 11 收藏 102 评论 10

灯盏细辛 赞了文章 · 2017-01-19

写给程序员的 18 幅对联,你能看懂几个?

本文对联纯属虚构,如有雷同关我屁事。

辛酸版

横批:谁能懂我

上联:敲一夜代码,流下两三行泪水,掏空四肢五体,六杯咖啡七桶泡面,还有八个测试九层审批,可谓十分艰难;

下联:经十年苦读,面过九八家公司,渐忘七情六欲,五年相亲四个对象,乃知三番加班两次约会,新年一鸣惊人。

祈福版

横批:鞠躬尽瘁

上联:文档注释一应具全

下联:脊柱腰椎早日康复

生活版

横批:1024

上联:西瓜包子带一斤三个

下联:大米白面少二十四克

新手程序员

横批:!@#$%^&*()

上联:红红火火过大年

下联:烫烫屯屯码三天

高级程序员

横批:一代键客

上联:坐北朝南一个需求满足东西

下联:思前想后几行代码安抚中央

学生版

横批:运鼠帷幄

上联:读码上万行

下联:下键如有神

送产品版(和平版)

横批:团结一致

上联:谈业务定需求必能安内攘外

下联:促稳定寻发展才好升职加薪

送产品版(开战版)

横批:你行你来

上联:去他大爷,十个需求,九处修改,八个扯淡,七番六次急忙上线

下联:改你妈逼,五日凌晨,四点加班,三里灯火,两排一个猝倒桌前

老板送程序员版

横批:画饼充饥

上联:百个功能愿你一气呵成

下联:一年年终奖你十月工资

隔壁老王送程序员

横批:人艰不拆

上联:少赚钱多说话,免得死得早

下联:别加班勤陪聊,不会戴绿帽

前端版

横批:瞬息万变

上联:微博知乎占头条谁与争锋

下联:桌面移动待前端一统江湖

后台版

横批:后方安定

上联:存数据订接口如探囊取物

下联:锁异步释内存似手到擒来

梦想版

横批:人生巅峰

上联:抬头不见八阿哥

下联:低头迎娶白富美

形象版

横批:员媛猿

上联:格子衣,牛仔裤,背跨双肩包

下联:文化衫,运动鞋,眼戴八百度

社区

感谢 Growth 群里的群友

横批:生不如死

上联:一年三百四十五天天天打代码

下联:十兆九千八百七行行行见bug

Phodal 版

横批:没钱买房

上联:待我代码编成

下联:娶你为妻可好

来,我出个上联:上班写 JavaScript 处处见 $

你的下联呢?

Phodal

查看原文

赞 3 收藏 3 评论 0