Category Archives: Web

Web通信协议

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

  • 原生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">
  • Tipwebpack可以做任何事情,这些都可以用插件实现

基于这些缺点,出现了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传输数据。

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

from:https://segmentfault.com/a/1190000019891825

从 0 到 1 再到 100, 搭建、编写、构建一个前端项目

1. 选择现成的项目模板还是自己搭建项目骨架

搭建一个前端项目的方式有两种:选择现成的项目模板、自己搭建项目骨架。

选择一个现成项目模板是搭建一个项目最快的方式,模板已经把基本的骨架都搭建好了,你只需要向里面填充具体的业务代码,就可以通过内置的工具与命令构建代码、部署到服务器等。

一般来说,一个现成的项目模板会预定义一定的目录结构、书写方式,在编写项目代码时需要遵循相应的规范;也会内置必要的工具,比如 .editorconfigeslintstylelintprettierhuskylint-staged 等;也会内置必要的命令(package.json | scripts),比如 本地开发:npm run dev本地预览:npm run start构建:npm run build部署:npm run deploy等。

社区比较好的项目模板:

这些模板的使用又分为两种:使用 git 直接克隆到本地、使用命令行创建。

(使用现有模板构建的项目,可以跳过第 2 ~ 7 步)

1.1 使用 git 直接克隆到本地

这是一种真正意义上的模板,可以直接到模板项目的 github 主页,就能看到整个骨架,比如 react-boilerplateant-design-provue-element-adminreact-starter-kit

以 react-boilerplate 为例:

克隆到本地:

git clone --depth=1 https://github.com/react-boilerplate/react-boilerplate.git <你的项目名字>

切换到目录下:

cd <你的项目名字>

一般来说,接下来运行 npm run install 安装项目的依赖后,就可以运行;有些模板可能有内置的初始化命令,比如 react-boilerplate

npm run setup

启动应用:

npm start

这时,就可以在浏览器中预览应用了。

1.2 使用命令行创建

这种方式需要安装相应的命令,然后由命令来创建项目。

以 create-react-app 为例:

安装命令:

npm install -g create-react-app

创建项目:

create-react-app my-app

运行应用:

cd my-app
npm start

1.3 自己搭建项目骨架

如果你需要定制化,可以选择自己搭建项目的骨架,但这需要开发者对构建工具如 webpacknpmnode 及其生态等有相当的了解与应用,才能完美的把控整个项目。

下面将会一步一步的说明如何搭建一个定制化的项目骨架。

2. 选择合适的规范来写代码

js 模块化的发展大致有这样一个过程 iife => commonjs/amd => es6,而在这几个规范中:

  • iifejs 原生支持,但一般不会直接使用这种规范写代码
  • amdrequirejs 定义的加载规范,但随着构建工具的出现,便一般不会用这种规范写代码
  • commonjsnode 的模块加载规范,一般会用这种规范写 node 程序
  • es6ECMAScript2015 定义的模块加载规范,需要转码后浏览器才能运行

这里推荐使用 es6 的模块化规范来写代码,然后用工具转换成 es5 的代码,并且 es6 的代码可以使用 Tree shaking 功能。

参考:

3. 选择合适的构建工具

对于前端项目来说,构建工具一般都选用 webpackwebpack 提供了强大的功能和配置化运行。如果你不喜欢复杂的配置,可以尝试 parcel

参考:

4. 确定是单页面应用(SPA)还是多页面应用

因为单页面应用与多页面应用在构建的方式上有很大的不同,所以需要从项目一开始就确定,使用哪种模式来构建项目。

4.1 多页面应用

传统多页面是由后端控制一个 url 对应一个 html 文件,页面之间的跳转需要根据后端给出的 url 跳转到新的 html 上。比如:

http://www.example.com/page1 -> path/to/page1.html
http://www.example.com/page2 -> path/to/page2.html
http://www.example.com/page3 -> path/to/page3.html

这种方式的应用,项目里会有多个入口文件,搭建项目的时候就需要对这种多入口模式进行封装。另外,也可以选择一些封装的多入口构建工具,如 lila

4.2 单页面应用

单页面应用(single page application),就是只有一个页面的应用,页面的刷新和内部子页面的跳转完全由 js 来控制。

一般单页面应用都有以下几个特点:

  • 本地路由,由 js 定义路由、根据路由渲染页面、控制页面的跳转
  • 所有文件只会加载一次,最大限度重用文件,并且极大提升加载速度
  • 按需加载,只有真正使用到页面的时候,才加载相应的文件

这种方式的应用,项目里只有一个入口文件,便无需封装。

参考:

5. 选择合适的前端框架与 UI 库

一般在搭建项目的时候就需要定下前端框架与 UI 库,因为如果后期想更换前端框架和 UI 库,代价是很大的。

比较现代化的前端框架:

一些不错的组合:

参考:

6. 定好目录结构

一个好的目录结构对一个好的项目而言是非常必要的。

一个好的目录结构应当具有以下的一些特点:

  1. 解耦:代码尽量去耦合,这样代码逻辑清晰,也容易扩展
  2. 分块:按照功能对代码进行分块、分组,并能快捷的添加分块、分组
  3. 编辑器友好:需要更新功能时,可以很快的定位到相关文件,并且这些文件应该是很靠近的,而不至于到处找文件

比较推荐的目录结构:

多页面应用

|-- src/ 源代码目录

    |-- page1/ page1 页面的工作空间(与这个页面相关的文件都放在这个目录下)
        |-- index.html html 入口文件
        |-- index.js js 入口文件
        |-- index.(css|less|scss) 样式入口文件
        |-- html/ html 片段目录
        |-- (css|less|scss)/ 样式文件目录
        |-- mock/ 本地 json 数据模拟
        |-- images/ 图片文件目录
        |-- components/ 组件目录(如果基于 react, vue 等组件化框架)
        |-- ...
        
    |-- sub-dir/ 子目录
        |-- page2/ page2 页面的工作空间(内部结构参考 page1)
            |-- ...
        
    |-- ...
    
|-- html/ 公共 html 片段
|-- less/ 公共 less 目录
|-- components/ 公共组件目录
|-- images/ 公共图片目录
|-- mock/ 公共 api-mock 文件目录
|-- ...

单页面应用

|-- src/ 源代码目录
    |-- page1/ page1 页面的工作空间
        |-- index.js 入口文件
        |-- services/ service 目录
        |-- models/ model 目录
        |-- mock/ 本地 json 数据模拟
        |-- images/ 图片文件目录
        |-- components/ 组件目录(如果基于 react, vue 等组件化框架)
        |-- ...
        
    |-- module1/ 子目录
        |-- page2/ page2 页面的工作空间(内部结构参考 page1)
        
    |-- ...
    
|-- images/ 公共图片目录
|-- mock/ 公共 api-mock 文件目录
|-- components/ 公共组件目录   
|-- ... 

参考:

7. 搭建一个好的脚手架

搭建一个好的脚手架,能够更好的编写代码、构建项目等。

可以查看 搭建自己的前端脚手架 了解一些基本的脚手架文件与工具。

比如:

|-- /                              项目根目录
    |-- src/                       源代码目录
    |-- package.json               npm 项目文件
    |-- README.md                  项目说明文件
    |-- CHANGELOG.md               版本更新记录
    |-- .gitignore                 git 忽略配置文件
    |-- .editorconfig              编辑器配置文件
    |-- .npmrc                     npm 配置文件
    |-- .npmignore                 npm 忽略配置文件
    |-- .eslintrc                  eslint 配置文件
    |-- .eslintignore              eslint 忽略配置文件
    |-- .stylelintrc               stylelint 配置文件
    |-- .stylelintignore           stylelint 忽略配置文件
    |-- .prettierrc                prettier 配置文件
    |-- .prettierignore            prettier 忽略配置文件
    
    |-- .babelrc                   babel 配置文件
    |-- webpack.config.js          webpack 配置文件
    |-- rollup.config.js           rollup 配置文件
    |-- gulpfile.js                gulp 配置文件
    
    |-- test/                      测试目录
    |-- docs/                      文档目录
    |-- jest.config.js             jest 配置文件
    |-- .gitattributes             git 属性配置
  • .editorconfig: 用这个文件来统一不同编辑器的一些配置,比如 tab 转 2 个空格、自动插入空尾行、去掉行尾的空格等,http://editorconfig.org
  • eslintstylelintprettier: 规范化代码风格、优化代码格式等
  • huskylint-staged: 在 git 提交之前对代码进行审查,否则不予提交
  • .gitlab-ci.ymlgitlab ci 持续集成服务

参考:

=================================================

到这里为止,一个基本的项目骨架就算搭建好了。

8. 使用版本控制系统管理源代码(git)

项目搭建好后,需要一个版本控制系统来管理源代码。

比较常用的版本管理工具有 gitsvn,现在一般都用 git

一般开源的项目可以托管到 http://github.com,私人的项目可以托管到 https://gitee.comhttps://coding.net/,而企业的项目则需要自建版本控制系统了。

自建版本控制系统主要有 gitlabgogsgiteagitlab 是由商业驱动的,比较稳定,社区版是免费的,一般建议选用这个;gogs, gitea 是开源的项目,还不太稳定,期待进一步的更新。

所以,git + gitlab 是不错的配合。

9. 编写代码

编写代码时,js 选用 es6 的模块化规范来写(如果喜欢用 TypeScript,需要加上 ts-loader),样式可以用 lessscsscss 来写。

写 js 模块文件时,注释可以使用 jsdoc 的规范来写,如果配置相应的工具,可以将这些注释导出接口文档。

因为脚手架里有 huskylint-staged 的配合,所以每次提交的代码都会进行代码审查与格式优化,如果不符合规范,则需要把不规范的代码进行修改,然后才能提交到代码仓库中。

比如 console.log(haha.hehe); 这段代码就会遇到错误,不予提交:

这个功能定义在 package.json 中:

{
  "devDependencies": {             工具依赖
    "babel-eslint": "^8.2.6",
    "eslint": "^4.19.1",
    "husky": "^0.14.3",
    "lint-staged": "^7.2.0",
    "prettier": "^1.14.0",
    "stylelint": "^9.3.0",
    "eslint-config-airbnb": "^17.0.0",
    "eslint-config-prettier": "^2.9.0",
    "eslint-plugin-babel": "^5.1.0",
    "eslint-plugin-import": "^2.13.0",
    "eslint-plugin-jsx-a11y": "^6.1.0",
    "eslint-plugin-prettier": "^2.6.2",
    "eslint-plugin-react": "^7.10.0",
    "stylelint-config-prettier": "^3.3.0",
    "stylelint-config-standard": "^18.2.0"
  },
  "scripts": {                     可以添加更多命令
    "precommit": "npm run lint-staged",
    "prettier": "prettier --write \"./**/*.{js,jsx,css,less,sass,scss,md,json}\"",
    "eslint": "eslint .",
    "eslint:fix": "eslint . --fix",
    "stylelint": "stylelint \"./**/*.{css,less,sass,scss}\"",
    "stylelint:fix": "stylelint \"./**/*.{css,less,sass,scss}\" --fix",
    "lint-staged": "lint-staged"
  },
  "lint-staged": {                 对提交的代码进行检查与矫正
    "**/*.{js,jsx}": [
      "eslint --fix",
      "prettier --write",
      "git add"
    ],
    "**/*.{css,less,sass,scss}": [
      "stylelint --fix",
      "prettier --write",
      "git add"
    ],
    "**/*.{md,json}": [
      "prettier --write",
      "git add"
    ]
  }
}
  • 如果你想禁用这个功能,可以把 scripts 中 "precommit" 改成 "//precommit"
  • 如果你想自定 eslint 检查代码的规范,可以修改 .eslintrc, .eslintrc.js 等配置文件
  • 如果你想自定 stylelint 检查代码的规范,可以修改 .stylelintrc, .stylelintrc.js 等配置文件
  • 如果你想忽略某些文件不进行代码检查,可以修改 .eslintignore, .stylelintignore 配置文件

参考:

10. 组件化

当项目拥有了一定量的代码之后,就会发现,有些代码是很多页面共用的,于是把这些代码提取出来,封装成一个组件,供各个地方使用。

当拥有多个项目的时候,有些组件需要跨项目使用,一种方式是复制代码到其他项目中,但这种方式会导致组件代码很难维护,所以,一般是用另一种方式:组件化。

组件化就是将组件独立成一个项目,然后在其他项目中安装这个组件,才能使用。

一般组件化会配合私有 npm 仓库一起用。

|-- project1/ 项目1
    |-- package.json
    
|-- project2/ 项目2
    |-- package.json    

|-- component1/ 组件1
    |-- package.json

|-- component2/ 组件2
    |-- package.json

在 project1 中安装 component1, component2 组件:

# package.json
{
  "dependencies": {
    "component1": "^0.0.1",
    "component2": "^0.0.1"
  }
}
import compoennt1 from 'compoennt1';
import compoennt2 from 'compoennt2';

如果想要了解怎样写好一个组件(npm package),可以参考 从 1 到完美,写一个 js 库、node 库、前端组件库

参考:

11. 测试

测试的目的在于能以最少的人力和时间发现潜在的各种错误和缺陷,这在项目更新、重构等的过程中尤其重要,因为每当更改一些代码后,你并不知道这些代码有没有问题、会不会影响其他的模块。如果有了测试,运行一遍测试用例,就知道更改的代码有没有问题、会不会产生影响。

一般前端测试分以下几种:

  1. 单元测试:模块单元、函数单元、组件单元等的单元块的测试
  2. 集成测试:接口依赖(ajax)、I/O 依赖、环境依赖(localStorage、IndexedDB)等的上下文的集成测试
  3. 样式测试:对样式的测试
  4. E2E 测试:端到端测试,也就是在实际生产环境测试整个应用

一般会用到下面的一些工具:

另外,可以参考 聊聊前端开发的测试

12. 构建

一般单页面应用的构建会有 npm run build 的命令来构建项目,然后会输出一个 html 文件,一些 js/css/images ... 文件,然后把这些文件部署到服务器就可以了。

多页面应用的构建要复杂一些,因为是多入口的,所以一般会封装构建工具,然后通过参数传入多个入口:

npm run build -- page1 page2 dir1/* dir2/all --env test/prod
  • page1, page2 确定构建哪些页面;dir1/*, dir2/all 某个目录下所有的页面;all, * 整个项目所有的页面
  • 有时候可能还会针对不同的服务器环境(比如测试机、正式机)做出不同的构建,可以在后面加参数
  • -- 用来分割 npm 本身的参数与脚本参数,参考 npm – run-script 了解详情

多页面应用会导出多个 html 文件,需要注意这些导出的 html 不要相冲突了。

当然,也可以用一些已经封装好的工具,如 lila

13. 部署

在构建好项目之后,就可以部署到服务器了。

传统的方式,可以用 ftp, sftp 等工具,手动传到服务器,但这种方式比较笨拙,不够自动化。

自动化的,可以用一些工具部署到服务器,如 gulpgulp-ssh,当然也可以用一些封装的工具,如 md-synclila 等

以 md-sync 为例:

npm install md-sync --save-dev

md-sync.config.js 配置文件:

module.exports = [
  {
    src: './build/**/*',
    remotePath: 'remotePath',
    server: {
      ignoreErrors: true,
      sshConfig: {
        host: 'host',
        username: 'username',
        password: 'password'
      }
    },
  },
  {
    src: './build/**/*.html',
    remotePath: 'remotePath2',
    server: {
      ignoreErrors: true,
      sshConfig: {
        host: 'host',
        username: 'username',
        password: 'password'
      }
    },
  },
];

在 package.json 的 scripts 配置好命令:

"scripts": {
  "deploy": "md-sync"
}
npm run deploy

另外,一般大型项目会使用持续集成 + shell 命令(如 rsync)部署。

14. 持续集成测试、构建、部署

一般大型工程的的构建与测试都会花很长的时间,在本地做这些事情的话就不太实际,这就需要做持续集成测试、构建、部署了。

持续集成工具用的比较多的:

jenkins 是通用型的工具,可以与 githubbitbucketgitlab 等代码托管服务配合使用,优点是功能强大、插件多、社区活跃,但缺点是配置复杂、使用难度较高。

gitlab ci 是 gitlab 内部自带的持续集成功能,优点是使用简单、配置简单,但缺点是不及 jenkins 功能强大、绑定 gitlab 才能使用。

以 gitlab 为例(任务定义在 .gitlab-ci.yml 中):

stages:
  - install
  - test
  - build
  - deploy

# 安装依赖
install:
  stage: install
  only:
    - dev
    - master
  script:
    - npm install

# 运行测试用例
test:
  stage: test
  only:
    - dev
    - master
  script:
    - npm run test

# 编译
build:
  stage: build
  only:
    - dev
    - master
  script:
    - npm run clean
    - npm run build

# 部署服务器
deploy:
  stage: deploy
  only:
    - dev
    - master
  script:
    - npm run deploy

以上配置表示只要在 dev 或 master 分支有代码推送,就会进行持续集成,依次运行:

  • npm install
  • npm run test
  • npm run clean
  • npm run build
  • npm run deploy

最终完成部署。如果中间某个命令失败了,将停止接下的命令的运行,并将错误报告给你。

这些操作都在远程机器上完成。

=================================================

到这里为止,基本上完成了一个项目的搭建、编写、构建。

15. 清理服务器上过期文件

现在前端的项目基本上都会用 webpack 打包代码,并且文件名(html 文件除外)都是 hash 化的,如果需要清除过期的文件而又不想把服务器上文件全部删掉然后重新构建、部署,可以使用 sclean 来清除过期文件。

16. 收集前端错误反馈

当用户在用线上的程序时,怎么知道有没有出 bug;如果出 bug 了,报的是什么错;如果是 js 报错,怎么知道是那一行运行出了错?

所以,在程序运行时捕捉 js 脚本错误,并上报到服务器,是非常有必要的。

这里就要用到 window.onerror 了:

window.onerror = (errorMessage, scriptURI, lineNumber, columnNumber, errorObj) => {
  const data = {
    title: document.getElementsByTagName('title')[0].innerText,
    errorMessage,
    scriptURI,
    lineNumber,
    columnNumber,
    detailMessage: (errorObj && errorObj.message) || '',
    stack: (errorObj && errorObj.stack) || '',
    userAgent: window.navigator.userAgent,
    locationHref: window.location.href,
    cookie: window.document.cookie,
  };

  post('url', data); // 上报到服务器
};

线上的 js 脚本都是压缩过的,需要用 sourcemap 文件与 source-map 查看原始的报错堆栈信息,可以参考 细说 js 压缩、sourcemap、通过 sourcemap 查找原始报错信息 了解详细信息。

参考:

后续

更多博客,查看 https://github.com/senntyou/blogs

作者:深予之 (@senntyou)

版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证

from:https://segmentfault.com/a/1190000017158444

基于SpringCloud的微服务架构演变史?

导读
一段时期以来 “微服务架构 ”一直是一个热门词汇,各种技术类公众号或架构分享会议上,关于微服务架构的讨论和主题也都非常多。对于大部分初创互联网公司来说,早期的单体应用结构才是最合适的选择,只有当业务进入快速发展期,在系统压力、业务复杂度以及人员扩展速度都快速上升的情况下,如何快速、稳妥有序的将整个互联网软件系统升级成微服务架构,以满足业务发展需要及技术组织的重新塑造,才是进行微服务架构的最主要的动力,否则空谈微服务架构是没有意义的。

而一旦决定将整个应用体系按照微服务架构体系进行升级,就需要有组织有计划的进行业务系统、基础架构、运维体系等多个方面的升级配套。而另一个比较尴尬的现实是,一般业务发展进入到需要进行微服务架构层面的时候,业务发展往往又都是非常迅猛的,这种业务快速发展和增长的压力往往又会给整个技术团队带来非常大的挑战,因为此时你需要取舍,是简单方案快速支撑呢?还是选择适当长远一点的方案?当然这种情况大部分是技术细节方面的问题,掌控的“度”大部分情况是掌握在具体的工程师手中。

而如何整体上确保应用体系及组织结构向微服务时代快速、有序的跨越,是一件十分考验团队能力以及架构管理水平的事。能做到80分已然算优秀了,因为这是有其客观规律的!

作者自身亲历了一个快速发展的互联网公司从单体应用~以SpringCloud为技术栈的微服务架构体系的全过程。本文将主要从技术角度与大家探讨如何利用SpringCloud进行微服务架构拆分,以及在这个过程中一点自己的思考。水平有限,不足之处还请包涵!
系统架构演变概述


在公司业务初创时期,面对的主要问题是如何将一个想法变成实际的软件实现,在这个时候整个软件系统的架构并没有搞得那么复杂,为了快速迭代,整个软件系统就是由“App+后台服务”组成,而后台服务也只是从工程角度将应用进行Jar包的拆分。此时软件系统架构如下:

而此时整个软件系统的功能也比较简单,只有基本的用户、订单、支付等功能,并且由于业务流程没有那么复杂,这些功能基本耦合在一起。而随着App的受欢迎程度(作者所在的公司正好处于互联网热点),所以App下载量在2017年迅猛增长,在线注册人数此时也是蹭蹭往上涨。

随着流量的迅猛增长,此时整个后台服务的压力变得非常大,为了抗住压力只能不断的加机器,平行扩展后台服务节点。此时的部署架构如下:

通过这种方式,整个软件系统抗住了一波压力,然而系统往往还是会偶尔出点事故,特别是因为api中的某个接口性能问题导致整个服务不可用,因为这些接口都在一个JVM进程中,虽然此时部署了多个节点,但因为底层数据库、缓存系统都是一套,所以还是会出现一挂全挂的情况。

另一方面,随着业务的快速发展,以往相对简单的功能变得复杂起来,这些功能除了有用户看得见的、也会包括很多用户看不见的,就好像百度搜索,用户能看见的可能只是一个搜索框,但是实际上后台对应的服务可能是成百上千,如有些增长策略相关的功能:红包、分享拉新等。还有些如广告推荐相关的变现功能等。

此外,流量/业务的增长也意味着团队人数的迅速增长,如果此时大家开发各自的业务功能还是用一套服务代码,很难想象百十来号人,在同一个工程在叠加功能会是一个什么样的场景。所以如何划分业务边界、合理的进行团队配置也是一件十分迫切的事情了!

为了解决上述问题,适应业务、团队发展,架构团队决定进行微服务拆分。而要实施微服务架构,除了需要合理的划分业务模块边界外,也需要一整套完整的技术解决方案。

在技术方案的选择上,服务拆分治理的框架也是有很多,早期的有如WebService,近期的则有各种Rpc框架(如Dubbo、Thirft、Grpc)。而Spring Cloud则是基于Springboot提供的一整套微服务解决方案,因为技术栈比较新,并且各类组件的支撑也非常全面,所以Spring Cloud就成为了首选。

经过一系列的重构+扩展,整个系统架构最终形成了以app为中心的一套微服务软件系统,结构如下:

到这里,整个软件系统就基于SpringCloud初步完成了微服务体系的拆分。支付、订单、用户、广告等核心功能抽离成独立的微服务,与此同时各自微服务对应的数据库也按照服务边界进行了拆分。

在完成服务的拆分以后,原来功能逻辑之间的代码调用关系,转换成了服务间网络的调用关系,而各个微服务需要根据各自所承载的功能提供相应的服务,此时服务如何被其他服务发现并调用,就成了整个微服务体系中比较关键的部分,使用过Dubbo框架的同学知道,在Dubbo中服务的注册&发现是依赖于Zookeeper实现的,而在SpringCloud中我们是通过Consul来实现。另外在基于SpringCloud的架构体系中,提供了配置中心(ConfigServer)来帮助各个微服务管理配置文件,而原本的api服务,随着各个功能的抽离,逐步演变成前置网关服务了。

聊到这里,基于SpringCloud我们进行了微服务拆分,而在这个体系结构中,分别提到了Consul、ConfigServer、网关服务这几个关键组件,那么这几个关键组件具体是如何支撑这个庞大的服务体系的呢?

SpringCloud关键组件

Consul

Consul是一个开源的,使用go语言开发的注册中心服务。它里面内置了服务发现与注册框架、分布一致性协议实现、健康检查、Key/Value存储、多数据中心等多个方案。在SpringCloud框架中还可以选择Eurke作为注册中心,这里之所以选择Consul主要原因在于Consul对异构的服务的支持,如:grpc服务。

事实上,在后续的系统架构演进中,在某些服务模块进一步向子系统化拆分的过程中,就采用了grpc作为子系统服务间的调用方式。例如,支付模块的继续扩张,对支付服务本身又进行了微服务架构的拆分,此时支付微服务内部就采用了grpc的方式进行调用,而服务注册与发现本身则还是依赖于同一套Consul集群。

此时的系统架构演进如下:

原有微服务架构中的模块服务在规模达到一定程度或复杂性达到一定程度后,都会朝着独立的体系发展,从而将整个微服务的调用链路变的非常长,而从Consul的角度看,所有的服务又都是扁平的。

随着微服务规模的越来越大,Consul作为整个体系的核心服务组件,在整个体系中处于关键的位置,一旦Consul挂掉,所有的服务都将停止服务。那么Consul到底是什么样服务?其容灾机制又该如何设计呢?

要保证Consul服务的高可用,在生产环境Consul应该是一个集群(关于Consul集群的安装与配置可以参考网络资料),这是毫无疑问的。而在Consul集群中,存在两种角色:Server、Client,这两种角色与Consul集群上运行的应用服务并没有什么关系,只是基于Consul层面的一种角色划分。实际上,维持整个Consul集群状态信息的还是Server节点,与Dubbo中使用Zookeeper实现注册中心一样,Consul集群中的各个Server节点也需要通过选举的方式(使用GOSSIP协议、Raft一致性算法,这里不做详细展开,在后面的文章中可以和大家单独讨论)来选举整个集群中的Leader节点来负责处理所有查询和事务,并向其他节点同步状态信息。

Client角色则是相对无状态的,只是简单的代理转发RPC请求到Server节点,之所以存在Client节点主要是分担Server节点的压力,作一层缓冲而已,这主要是因为Server节点的数量不宜过多,因为Server节点越多也就意味着达成共识的过程越慢,节点间同步的代价也就越高。对于Server节点,一般建议3-5台,而Client节点则没有数量的限制,可以根据实际情况部署数千或数万台。事实上,这也只是一种策略,在现实的生产环境中,大部分应用只需要设置3~5台Server节点就够了,作者所在的公司一套生产集群中的Consul集群的节点配置就是5个Server节点,并没有额外再设置Client节点。

另外,在Consul集群中还有一个概念是Agent,事实上每个Server或Client都是一个consul agent,它是运行在Consul集群中每个成员上的一个守护进程,主要的作用是运行DNS或HTTP接口,并负责运行时检查和保持服务信息同步。我们在启动Consul集群的节点(Server或Client)时,都是通过consul agent的方式启动的。例如:

consul agent -server -bootstrap -syslog \
-ui \
-data-dir=/opt/consul/data \
-dns-port=53
-recursor=10.211.55.3
-config-dir=/opt/consul/conf
 \
-pid-file=/opt/consul/run/consul.pid \
-client=10.211.55.4 \
-bind=10.211.55.4 \
-node=consul-server01 \
-disable-host-node-id &

以实际的生产环境为例,Consul集群的部署结构示意图如下:

实际生产案例中并没有设置Client节点,而是通过5个Consul Server节点组成的集群,来服务整套生产集群的应用注册&发现。这里有细节需要了解下,实际上5个Consul Server节点的IP地址是不一样的,具体的服务在连接Consul集群进行服务注册与查询时应该连接Leader节点的IP,而问题是,如果Leader节点挂掉了,相应的应用服务节点,此时如何连接通过Raft选举产生的新Leader节点呢?难道手工切换IP不成?

显然手工切换IP的方式并不靠谱,而在生产实践中,Consul集群的各个节点实际上是在Consul Agent上运行DNS(如启动参数中红色字体部分),应用服务在连接Consul集群时的IP地址为DNS的IP,DNS会将地址解析映射到Leader节点对应的IP上,如果Leader节点挂掉,选举产生的新Leader节点会将自己的IP通知DNS服务,DNS更新映射关系,这一过程对各应用服务则是透明的。

通过以上分析,Consul是通过集群设计、Raft选举算法,Gossip协议等机制来确保Consul服务的稳定与高可用的。如果需要更高的容灾级别,也可以通过设计双数据中心的方式,来异地搭建两个Consul数据中心,组成一个异地灾备Consul服务集群,只是这样成本会更高,这就看具体是否真的需要了。

ConfigServer(配置中心)

配置中心是对微服务应用配置进行管理的服务,例如数据库的配置、某些外部接口地址的配置等等。在SpringCloud中ConfigServer是独立的服务组件,它与Consul一样也是整个微服务体系中比较关键的一个组件,所有的微服务应用都需要通过调用其服务,从而获取应用所需的配置信息。

随着微服务应用规模的扩大,整个ConfigServer节点的访问压力也会逐步增加,与此同时,各个微服务的各类配置也会越来越多,如何管理好这些配置文件以及它们的更新策略(确保不因生产配置随意改动而造成线上故障风险),以及搭建高可用的ConfigServer集群,也是确保微服务体系稳定很重要的一个方面。

在生产实践中,因为像Consul、ConfigServer这样的关键组件,需要搭建独立的集群,并且部署在物理机而不是容器里。在上一节介绍Consul的时候,我们是独立搭建了5个Consul Server节点。而ConfigServer因为主要是http配置文件访问服务,不涉及节点选举、一致性同步这样的操作,所以还是按照传统的方式搭建高可用配置中心。具体结构示意图如下:

我们可以单独通过git来管理应用配置文件,正常来说由ConfigSeever直接通过网络拉取git仓库的配置供服务获取就可以了,这样只要git仓库配置更新,配置中心就能立刻感知到。但是这样做的不稳定之处,就在于git本身是内网开发用的代码管理工具,如果让线上实时服务直接读取,很容易将git仓库拉挂了,所以,我们在实际的运维过程中,是通过git进行配置文件的版本控制,区分线上分支/master与功能开发分支/feature,并且在完成mr后还需要手工(通过发布平台触发)同步一遍配置,过程是将新的master分支的配置同步一份到各个configserver节点所在主机的本地路径,这样configserver服务节点就可以通过其本地目录获取配置文件,而不用多次调用网络获取配置文件了。

而另一方面,随着微服务越来越多,git仓库中的配置文件数量也会越来越多。为了便于配置的管理,我们需要按照一定的组织方式来组织不同应用类型的配置。在早期所有的应用因为没有分类,所以导致上百个微服务的配置文件放在一个仓库目录,这样一来导致配置文件管理成本增加,另外一方面也会影响ConfigServer的性能,因为某个微服务用不到的配置也会被ConfigServer加载。

所以后期的实践是,按照配置的层次关系进行组织,将公司全局的项目配置抽象到顶层,由ConfigServer默认加载,而其他所有的微服务则按照应用类型进行分组(通过git项目空间的方式分组),相同的应用放在一个组,然后这个组下单独设立一个名为config的git仓库来存放这个组下相关微服务的配置文件。层次结构如下:

这样应用加载配置的优先级就是“本地配置->common配置->组公共配置->项目配置”这样的顺序。例如某服务A,在项目工程的默认配置文件(“bootstrap.yml/application.yml”)中配置了参数A,同时也在本地项目配置“application-production.yml”配置了参数B,而与此同时,ConfigServer中的common仓库下的配置文件“application.yml/application-production.yml”又分别存在参数C、参数D,同时有个组叫“pay”,其下的默认配置文件“application.yml/application-production.yml”存在参数E、参数F,具体项目pay-api又存在配置文件“pay-api-production.yml”其覆盖了common仓库中参数C、参数D的值。那么此时如果该应用以“spring.profiles.active=production”的方式启动,那么其能获取到的配置参数(通过链接访问:http://{spring.cloud.config.uri}/pay-api-production.yml)就是A、B、C、D、E、F,其中C、D的参数值为pay-api-production.yml中最后覆盖的值。

而对于ConfigServer服务本身来说,需要按照这样的组织方式进行配置类型匹配,例如上述的例子中,假设还存在finance的配置仓库,而pay组下的服务访问配置中心的时候,是不需要finance空间下的配置文件的,所以ConfigServer可以不用加载。这里就需要在ConfigServer服务配置中进行一些配置。具体如下:

spring:
  application:
    name: @project.artifactId@
    version: @project.version@
    build: @buildNumber@
    branch: @scmBranch@
  cloud:
    inetutils:
      ignoredInterfaces:
        - docker0
    config:
      server:
        health.enabled: false
        git:
          uri: /opt/repos/config
          searchPaths: 'common,{application}'
          cloneOnStart: true
          repos:
            pay:
                pattern: pay-*
                cloneOnStart: true
                uri: /opt/repos/example/config
                searchPaths: 'common,{application}'
            finance:
                pattern: finance-*
                cloneOnStart: true
                uri: /opt/repos/finance/config
                searchPaths: 'common,{application}'

通过在ConfigServer服务本身的application.yml本地配置中,设置其配置搜索方式,来实现这样的目的。

网关服务&服务熔断&监控

通过上面两小节的内容,我们相对详细地介绍了基于SpringCloud体系中比较关键的两个服务组件。然而在微服务架构体系中,还有很多关键的问题需要解决,例如,应用服务在Consul中部署了多个节点,那么调用方如何实现负载均衡?

关于这个问题,在传统的架构方案中是通过Nginx实现的,但是在前面介绍Consul的时候只提到了Consul的服务注册&发现、选举等机制,并没有提到Consul如何在实现服务调用的负载均衡。难道基于SpringCloud的微服务体系中的应用服务都是单节点在提供服务,哪怕即使部署了多个服务节点?事实上,我们在服务消费方通过@EnableFeignClients注解开启调用,通过@FeignClient(“user”)注解进行服务调用时,就已经实现了负载均衡,为什么呢?因为,这个注解默认是会默认开启Robbin代理的,而Robbin是实现客户端负载均衡的一个组件,通过从Consul拉取服务节点信息,从而以轮询的方式转发客户端调用请求至不同的服务端节点来实现负载均衡。而这一切都是在消费端的进程内部通过代码的方式实现的。这种负载方式寄宿于消费端应用服务上,对消费端存在一定的代码侵入性,这是为什么后面会出现Service Mesh(服务网格)概念的原因之一,这里就不展开了,后面有机会再和大家交流。

另一需要解决的关键问题是服务熔断、限流等机制的实现,SpringCloud通过集成Netflix的Hystrix框架来提供这种机制的支持,与负载均衡机制一样也是在消费端实现。由于篇幅的关系,这里也不展开了,在后面的文章中有机会再和大家交流。

此外还有Zuul组件来实现API网关服务,提供路由分发与过滤相关的功能。而其他辅助组件还有诸如Sleuth来实现分布式链路追踪、Bus实现消息总线、Dashboard实现监控仪表盘等。由于SpringCloud的开源社区比较活跃,还有很多新的组件在不断的被集成进来,感兴趣的朋友可以持续关注下!

微服务之运维形态

在微服务体系结构下,随着服务数量大量的增长,线上的部署&维护的工作量会变得非常大,而如果还采用原有的运维模式的话,就能难满足需要了。此时运维团队需要实施Devops策略,开发自动化运维发布平台,打通产品、开发、测试、运维流程,关注研发效能。

另外一方面也需要推进容器化(Docker/Docker Swarm/k8s)策略,这样才能快速对服务节点进行伸缩,这也是微服务体系下的必然要求。

微服务泛滥问题

这里还需要注意一个问题,就是实施微服务架构后,如何在工程上管控微服务的问题。盲目的进行微服务的拆分也不是一件很合理的事情,因为这会导致整个服务调用链路变得深不可测,对问题排查造成难度,也浪费线上资源。

重构问题

在早期单体架构方式向微服务架构的转变过程中,重构是一个非常好的方式,也是确保服务规范化,业务系统应用架构合理化的很重要的手段。但是,一般来说,在快速发展阶段也就意味着团队规模的迅速增长,短时间内如何让新的团队有事可做也是一件非常考验管理水平的事情,因为如果招了很多人,并且他们之间呈现一种过渡的竞争状态的话,就会出现让重构这件事变得有些功利的情况,从而导致重构不彻底、避重就轻,导致表象上看是很高大上的微服务架构,而业务系统实际上比较烂的情况。

另外,重构是在一定阶段后作出的重要决策,不仅仅是重新拆分,也是对业务系统的重新塑造,所以一定要考虑好应用软件的系统结构以及实施它们所需要付出的成本,切不可盲目!

后记

基于SpringCloud的微服务架构体系,通过集成各种开源组件来为整个体系服务支持,但是在负载均衡、熔断、流量控制的方面需要对服务消费端的业务进程进行侵入。所以很多人会认为这不是一件很好的事情,于是出现了Service Mesh(服务网格)的概念,Service Mesh的基本思路就是通过主机独立Proxy进行的部署来解耦业务系统进程,这个Proxy除了负责服务发现和负载均衡(不在需要单独的注册组件,如Consul)外,还负责动态路由、容错限流、监控度量和安全日志等功能。

而在具体的服务组件上目前主要是 Google/IBM 等大厂支持和推进的一个叫做Istio的ServiceMesh 标准化工作组。具体关于Service Mesh的知识,在后面的内容中再和大家交流。以上就是本文的全部内容,由于作者水平有限,还请多多包涵!

from:https://mp.weixin.qq.com/s/NHVJCmVUXcAb_pAXxT8mHA

HTTP 的前世今生

作为互联网通信协议的一员老将,HTTP 协议走到今天已经经历了三次版本的变动,现在最新的版本是 HTTP2.0,相信大家早已耳熟能详。今天就给大家好好介绍一下 HTTP 的前世今生。

HTTP/0.9

HTTP 的最早版本诞生在 1991 年,这个最早版本和现在比起来极其简单,没有 HTTP 头,没有状态码,甚至版本号也没有,后来它的版本号才被定为 0.9 来和其他版本的 HTTP 区分。HTTP/0.9 只支持一种方法—— Get,请求只有一行。

  1. GET /hello.html

响应也是非常简单的,只包含 html 文档本身。

  1. <HTML>
  2. Hello world
  3. </HTML>

当 TCP 建立连接之后,服务器向客户端返回 HTML 格式的字符串。发送完毕后,就关闭 TCP 连接。由于没有状态码和错误代码,如果服务器处理的时候发生错误,只会传回一个特殊的包含问题描述信息的 HTML 文件。这就是最早的 HTTP/0.9 版本。

HTTP/1.0

1996 年,HTTP/1.0 版本发布,大大丰富了 HTTP 的传输内容,除了文字,还可以发送图片、视频等,这为互联网的发展奠定了基础。相比 HTTP/0.9,HTTP/1.0 主要有如下特性:

  •  请求与响应支持 HTTP 头,增加了状态码,响应对象的一开始是一个响应状态行
  •  协议版本信息需要随着请求一起发送,支持 HEAD,POST 方法
  •  支持传输 HTML 文件以外其他类型的内容

一个典型的 HTTP/1.0 的请求像这样:

  1. GET /hello.html HTTP/1.0
  2. User-Agent:NCSA_Mosaic/2.0(Windows3.1)
  3. 200 OK
  4. Date: Tue, 15 Nov 1996 08:12:31 GMT
  5. Server: CERN/3.0 libwww/2.17
  6. Content-Type: text/html
  7. <HTML>
  8. 一个包含图片的页面
  9. <IMGSRCIMGSRC=“/smile.gif”>
  10. </HTML>

HTTP/1.1

在 HTTP/1.0 发布几个月后,HTTP/1.1 就发布了。HTTP/1.1 更多的是作为对 HTTP/1.0 的完善,在 HTTP1.1 中,主要具有如下改进:

  •  可以复用连接
  •  增加 pipeline:HTTP 管线化是将多个 HTTP 请求整批提交的技术,而在传送过程中不需先等待服务端的回应。管线化机制须通过永久连接(persistent connection)完成。浏览器将HTTP请求大批提交可大幅缩短页面的加载时间,特别是在传输延迟(lag/latency)较高的情况下。有一点需要注意的是,只有幂等的请求可以使用 pipeline,如 GET,HEAD 方法。
  •  chunked 编码传输:该编码将实体分块传送并逐块标明长度,直到长度为 0 块表示传输结束, 这在实体长度未知时特别有用(比如由数据库动态产生的数据)
  •  引入更多缓存控制机制:如 etag,cache-control
  •  引入内容协商机制,包括语言,编码,类型等,并允许客户端和服务器之间约定以最合适的内容进行交换
  •  请求消息和响应消息都支持 Host 头域:在 HTTP1.0 中认为每台服务器都绑定一个唯一的 IP 地址,因此,请求消息中的URL并没有传递主机名(hostname)。但随着虚拟主机技术的发展,在一台物理服务器上可以存在多个虚拟主机(Multi-homed Web Servers),并且它们共享一个 IP 地址。因此,Host 头的引入就很有必要了。
  •   新增了 OPTIONS,PUT, DELETE, TRACE, CONNECT 方法

虽然 HTTP/1.1 已经优化了很多点,作为一个目前使用最广泛的协议版本,已经能够满足很多网络需求,但是随着网页变得越来越复杂,甚至演变成为独立的应用,HTTP/1.1 逐渐暴露出了一些问题:

  •  在传输数据时,每次都要重新建立连接,对移动端特别不友好
  •  传输内容是明文,不够安全
  •  header 内容过大,每次请求 header 变化不大,造成浪费
  •  keep-alive 给服务端带来性能压力

为了解决这些问题,HTTPS 和 SPDY 应运而生。

HTTPS

HTTPS 是以安全为目标的 HTTP 通道,简单讲是 HTTP 的安全版,即 HTTP 下加入 SSL 层,HTTPS 的安全基础是 SSL,因此加密的详细内容就需要 SSL。

HTTPS 协议的主要作用可以分为两种:一种是建立一个信息安全通道,来保证数据传输的安全;另一种就是确认网站的真实性。

HTTPS 和 HTTP 的区别主要如下:

  •  HTTPS 协议使用 ca 申请证书,由于免费证书较少,需要一定费用。
  •  HTTP 是明文传输,HTTPS 则是具有安全性的 SSL 加密传输协议。
  • HTTP 和 HTTPS使用的是完全不同的连接方式,用的端口也不一样,前者是 80,后者是 443。

SPDY

其实 SPDY 并不是新的一种协议,而是在 HTTP 之前做了一层会话层。

在 2010 年到 2015 年,谷歌通过实践一个实验性的 SPDY 协议,证明了一个在客户端和服务器端交换数据的另类方式。其收集了浏览器和服务器端的开发者的焦点问题,明确了响应数量的增加和解决复杂的数据传输。在启动 SPDY 这个项目时预设的目标是:

  •  页面加载时间 (PLT) 减少 50%。
  •  无需网站作者修改任何内容。
  •  将部署复杂性降至最低,无需变更网络基础设施。
  •  与开源社区合作开发这个新协议。
  •  收集真实性能数据,验证这个实验性协议是否有效。

为了达到降低目标,减少页面加载时间的目标,SPDY 引入了一个新的二进制分帧数据层,以实现多向请求和响应、优先次序、最小化及消除不必要的网络延迟,目的是更有效地利用底层 TCP 连接。

HTTP/2.0

时间来到 2015 年,HTTP/2.0 问世。先来介绍一下 HTTP/2.0 的特点吧:

  •  使用二进制分帧层:在应用层与传输层之间增加一个二进制分帧层,以此达到在不改动 HTTP 的语义,HTTP 方法、状态码、URI 及首部字段的情况下,突破HTTP1.1 的性能限制,改进传输性能,实现低延迟和高吞吐量。在二进制分帧层上,HTTP2.0 会将所有传输的信息分割为更小的消息和帧,并对它们采用二进制格式的编码,其中 HTTP1.x 的首部信息会被封装到 Headers 帧,而我们的 request body 则封装到 Data 帧里面。

  •  多路复用:对于 HTTP/1.x,即使开启了长连接,请求的发送也是串行发送的,在带宽足够的情况下,对带宽的利用率不够,HTTP/2.0 采用了多路复用的方式,可以并行发送多个请求,提高对带宽的利用率。

  •  数据流优先级:由于请求可以并发发送了,那么如果出现了浏览器在等待关键的 CSS 或者 JS 文件完成对页面的渲染时,服务器却在专注的发送图片资源的情况怎么办呢?HTTP/2.0 对数据流可以设置优先值,这个优先值决定了客户端和服务端处理不同的流采用不同的优先级策略。
  •  服务端推送:在 HTTP/2.0 中,服务器可以向客户发送请求之外的内容,比如正在请求一个页面时,服务器会把页面相关的 logo,CSS 等文件直接推送到客户端,而不会等到请求来的时候再发送,因为服务器认为客户端会用到这些东西。这相当于在一个 HTML 文档内集合了所有的资源。
  •  头部压缩:使用首部表来跟踪和存储之前发送的键值对,对于相同的内容,不会再每次请求和响应时发送。

可以看到 HTTP/2.0 的新特点和 SPDY 很相似,其实 HTTP/2.0 本来就是基于 SPDY 设计的,可以说是 SPDY 的升级版。

但是 HTTP/2.0 仍有和 SPDY 不同的地方,主要有如下两点:

  •  HTTP2.0 支持明文 HTTP 传输,而 SPDY 强制使用 HTTPS。
  •  HTTP2.0 消息头的压缩算法采用 HPACK,而非 SPDY 采用的 DEFLATE。

from:http://developer.51cto.com/art/201811/586932.htm

微信小程序开发资源汇总

QQ交流群

目录

推荐

置顶

官方文档

工具

  • WePY ★12k+ – 支持组件化的小程序开发框架
  • mpvue ★11k+ – 基于 Vue.js 的小程序开发框架,从底层支持 Vue.js 语法和构建工具体系
  • Taro ★3.9k – 使用 React 的方式开发小程序的框架,同时支持生成多端应用
  • Labrador ★1.5k – 支持 ES6/7 的微信小程序组件化开发框架
  • wept ★1.8k – 微信小程序实时运行环境
  • wafer ★1.6k – 快速构建具备弹性能力的微信小程序
  • wechat_web_devtools ★600+ – Linux 下微信开发者工具
  • minapp ★400+ – TypeScript 版小程序开发框架(兼容原生小程序代码)
  • tina ★300+ – 轻巧的渐进式微信小程序框架
  • xpmjs ★100+ – 微信小程序云端增强 SDK
  • WeApp-Workflow ★100+ – 基于 Gulp 的微信小程序前端开发工作流
  • gulp-wxa-copy-npm – 微信小程序 gulp 插件,解决 npm 包管理和 babel-runtime
  • weact – 用 JSX 快速开发小程序
  • socket.io-mp-client – 微信小程序 socket.io 客户端
  • wxa – 方便的小程序开发框架
  • px2rpx – Px 转 Rpx 在线工具
  • wxml-parser – JavaScript WXML parser
  • weappx – 基于 redux 的数据层管理框架
  • weapp-start – 基于插件机制的开发脚手架,改善原生小程序开发体验
  • Egret Wing – 支持微信小程序实时预览的 IDE
  • wxapp-graphql – 小程序GraphQL客户端

插件

教程

视频教程

文章

组件

Demo

from:https://github.com/justjavac/awesome-wechat-weapp