如何制定有效的Lint规范

在我们的项目中,lint工具总是提示着各种各样的error、warning,但是我们共同维护的代码库里依然有着各种不同风格的实现,为此很多时候我们只能不断的修改我们的lint规则,防止魔法代码的提交,但为此我们提出了更多的lint规则,到最后我们lint规则配置的繁琐而又不起作用。

在社区中,大家也越来越推崇用严格的lint规则来避免不断的浪费时间去设定、争论lint规则是否合理,但是我认为我们确实不应该花费太多的精力去争论这些没有意义的事情,但是我也不赞同使用一些较流行的严格的lint规则,在我们选择这些严格的lint规范的时候,我觉得我们是为了避免一个问题而引入了另一个问题,使用团队成员都不熟悉的lint规范,会让效率大大降低不说,还可能导致很多时候为了避免报error,而书写一些魔法代码。比如,我们如果配置了禁止在 componentDidMount 中调用 setState,那么很多时候,我们的实现都会发生报错,或者为了避免报错,把setState放在一个定时器里,这样是避免了lint报错,但是这个实现是在让人捉急。

那么我们怎么去制订一个不仅可以提高项目质量又可以保持大家效率的lint规则呢?

我觉得我们可以从以下几个方面出发:

一、禁止让人理解困难的代码

这一条算是比较好理解的,在多人/长期维护的项目中不要写一些让人摸不到头脑的“炫技”代码,比如位运算,如果我在项目中写了如下代码let a = ~~num;,这可能在一些经验丰富或者计算机基础扎实的工程师眼里根本不算什么,但是还是让很多人费解他要表达什么,但是我如果使用let a = parseInt(num);这样的话是不是就很理解了,其实大多数位运算都有更易于理解的实现,而且并不见的性能有啥损耗或者文件会增大多少字节,所以我们在lint中需要禁止一些让人理解困难的代码风格。

二、团队内推广最佳实践

我们首先需要明确一件事,那就是:lint不是万能的。所以我们怎么样让我们的项目保持一个高可维护的状态呢?我觉得我们还需要不断推广各种最佳实践,在 wiki 或设计指南里分享有用的知识,把各种最佳实践梳理出文档或工具,通过一条命令或者一键就可以生成我们需要的目录/代码结构,我觉得比我们去设置按字母序排列之类的规则有用的多,这样我们在多个项目中切换,或者多个文件中查看代码,都可以预期知道哪里会有我们需要的代码,或者哪里会出现解决问题的地方,这回让我们的效率更高,让规范在项目的每个角落落地,

三、让可修复的问题自动修复

在开发过程中你完全不需要一个工具告诉你得在这加一个空格,而且很多提示都可以通过使用 Prettier 或 Exlint —fix来修复问题,我们可以在项目/编辑器中配置自动执行来修复一些规则提示,提供效率。

四、提高代码设计能力

不论多少缩进或按字母序排列,都不能修复糟糕的设计,所以我们需要提高代码设计能力,如何行之有效的设计代码?这是一个很大的话题,不便展开来讲,在《代码大全》还有《重构——改善既有代码的设计》中已经提到了很多好的代码设计原则,而且业界比较出名的设计原则,我们可以坚持,比如:

  • 不要重复自己原则,系统的每一个功能都应该有唯一的实现。也就是说,如果多次遇到同样的问题,就应该抽象出一个共同的解决方法,不要重复开发同样的功能。
  • 你不会需要它原则,指的是你自以为有用的功能,实际上都是用不到的。
  • 单一职责原则,大到一个系统、模块的职责边界划分,小到一个类、一个方法的职责。清晰的划分类、方法的职责,保持其单一、稳定的功能,能够有效的降低日后代码越来越臃肿时带来的维护成本增加。
  • 开放封闭原则,对扩展开放,对修改关闭。也就是在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。
  • 最少知道原则,就是说:一个实体应当尽量少的与其他实体之间发生相互作用,使得系统功能模块相对独立。
  • 合成复用原则,简言之:要尽量使用组合/聚合关系,少用继承。

还有很多好的设计规范我们可以在开发中不断实践,去追求实现的代码模块都能达到高内聚、低耦合、易扩展、易替换的优秀设计。

五、对一些规则追问:这条规则有帮我们找到过 bug 吗

我们不应该假设每一条规则都会提供给我们有效的建议,我们应该把团队内的成员组织到一起,逐条过一遍lint规则,把没有用的规则剔除出去,让我们的规则就是风格指南,一眼就可以明白哪些实现方式或者书写格式是合理的,是行之有效让编码风格一致的。

六、组内使用统一lint的规范

我觉得这最后一条也算是团队合作比较核心的一条——使用统一的lint规范,只有大家都是用统一的lint规范,在不同的项目间切换开发的时候才能保持一致的编程体验,降低“过敏反应”!

vue虚拟DOM渲染成Canvas

前言

在刷github的时候偶然发现一个有意思的库vnode2canvas,因为关于转canvas我们通常都会用到html2canvas。我们知道vue是通过vnode实现渲染工作,所以vnode2canvas实现了vnode -> canvas,简化了html2canvas vnode -> html -> canvas。

背景

下面是关于canvas的概述

canvas是一种立即模式的渲染方式,不会存储额外的渲染信息。从而Canvas润徐直接发送绘图命令到GPU。但若用它来构建用户界面,需要进行一个更高层次的抽象。例如一些简单的处理,比如当绘制一个异步加载的资源到一个元素上会出现问题,如在图片上绘制文本。在HTML中,由于元素存在顺序,以及css中存在z-index,因此是很容易实现的。dom渲染是一种保留模式,保留模式是一种声明性的API,用于维护绘制到其中对象的层次结构中。保留模式API的优点是,对于你的应用程序,他们通常更容易构建复杂的场景,例如DOM。通常这都带来性能成本,需要额外的内存来保存场景和更新场景,这可能会减慢速度。

源码分析

vnode处理

Vue.mixin({
  // ...
  created() {
    if (this.$options.renderCanvas) {
      // ...
      // 监听vnode中引用的变化,重新渲染
      this.$watch(this.updateCanvas, this.noop)
      // ...
    }
  },
  methods: {
    updateCanvas() {
      // 模拟Vue render 函数
      // 寻找实例中定义的 renderCanva 方法,并传入 createElement 方法
      let vnode = this.$options.renderCanvas.call(this._renderProxy, this.$createElement)
    }
  }
})

Vue通过render函数,传入createElement方法创造vnode,这里就是加了个监听函数,混入到Vue实例中。

canvas元素处理

同时作者还对render的vnode做了额外的约束,比如:

view/scrollView/scrollItem --> fillRect
text --> fillText
image --> drawImage

其中这些元素类分别都继承于一个Super类,并且由于它们各有不同的展示方式,因此它们分别实现自己的draw方法,做定制化的展示。

调用

renderCanvas(h) {
  return h('view', {
     style: {
       left: 10,
       top: 10,
       width: 100,
       height: 100
     }
  })
}

通过这样绘制 canvas 布局最基础的写法是为canvas 元素传入一系列坐标点和相关的基础宽高。

通过作者相关文章,了解到作者还有别的方案去处理,因为上面写法还是有些不方便,其一就是写一个webpack loader 加载外部css

const css = require('css')
module.exports = function (source, other) {
  let cssAST = css.parse(source)
  let parseCss = new ParseCss(cssAST)
  parseCss.parse()
  this.cacheable();
  this.callback(null, parseCss.declareStyle(), other);
};

class ParseCss {
  constructor(cssAST) {
    this.rules = cssAST.stylesheet.rules
    this.targetStyle = {}
  }

  parse () {
    this.rules.forEach((rule) => {
      let selector = rule.selectors[0]
      this.targetStyle[selector] = {}
      rule.declarations.forEach((dec) => {
        this.targetStyle[selector][dec.property] = this.formatValue(dec.value)
      })
    })
  }

  formatValue (string) {
    string = string.replace(/"/g, '').replace(/'/g, '')
    return string.indexOf('px') !== -1 ? parseInt(string) : string
  }

  declareStyle (property) {
    return `window.${property || 'vStyle'} = ${JSON.stringify(this.targetStyle)}`
  }
}

简单的来说:主要也就是将 css 文件转成AST语法树,之后再对语法树做转换,转成canvas需要的定义形式。并以变量的形式注入到组件中。

结语

上面就是我与这个库的理解,这个库在一些卡片生成的业务场景下还是有很大的应用空间。

下面是例子地址:

Kubernetes 有状态服务

概述

在kubernetes容器编排系统中,RC、Deployment、DaemonSet都是面向无状态的服务,它们所管理的Pod的IP、名字,启停顺序等都是随机的,而StatefulSet是什么?顾名思义,有状态的集合,管理所有有状态的服务,比如MySQL、MongoDB集群等。StatefulSet本质上是Deployment的一种变体,在v1.9版本中已成为GA版本,它为了解决有状态服务的问题,它所管理的Pod拥有固定的Pod名称,启停顺序,在StatefulSet中,Pod名字称为网络标识(hostname),还必须要用到共享存储。在Deployment中,与之对应的服务是service,而在StatefulSet中与之对应的headless service,headless service,即无头服务,与service的区别就是它没有Cluster IP,解析它的名称时将返回该Headless Service对应的全部Pod的Endpoint列表。除此之外,StatefulSet在Headless Service的基础上又为StatefulSet控制的每个Pod副本创建了一个DNS域名,这个域名的格式为:

$(podname).(headless server name)   
FQDN: $(podname).(headless server name).namespace.svc.cluster.local
例如: web-0.account.default.svc.cluster.local

StatefulSet使用场景
– 稳定的持久化存储,即Pod重新调度后还是能访问到相同的持久化数据,基于PVC来实现。
– 稳定的网络标识符,即Pod重新调度后其PodName和HostName不变。
– 有序、优雅的部署和扩展,即通过initContainer实现
– 有序的自动滚动更新。

如使用场景描述,stable与Pod(重新)调度的持久性意思一致。如果应用程序不需要任何稳定网络标识符或有序部署、删除或扩展,则应使用提供一组无状态副本的控制器部署应用程序。Deployment或ReplicaSet等控制器可能更适合此种无状态需求。

StatefulSet示例

接下来看一些示例,演示下上面所说的特性,以加深理解。

apiVersion: v1
kind: Service
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  ports:
  - port: 80
    name: web
  clusterIP: None
  selector:
    app: nginx
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: web
spec:
  selector:
    matchLabels:
      app: nginx 
  # 声明它属于哪个Headless Service.
  serviceName: "nginx"  
  replicas: 3 
  template:
    metadata:
      labels:
        app: nginx
    spec:
      terminationGracePeriodSeconds: 10
      containers:
      - name: nginx
        image: nginx
        ports:
        - containerPort: 80
          name: web
        volumeMounts:
        - name: www
          mountPath: /usr/share/nginx/html
  # 可看作pvc的模板
  volumeClaimTemplates:
  - metadata:
      name: www
    spec:
      accessModes: [ "ReadWriteOnce" ]
      storageClassName: "glusterfs-ssd"
      resources:
        requests:
          storage: 1Gi

通过该配置文件,可看出StatefulSet的三个组成部分:

  • Headless Service:名为nginx,用来定义Pod网络标识( DNS domain)。
  • StatefulSet:定义具体应用,名为Nginx,有三个Pod副本,并为每个Pod定义了一个域名。
  • volumeClaimTemplates: 存储卷申请模板,创建PVC,指定pvc名称大小,将自动创建pvc,且pvc必须由存储类(storageClass)供应。

为什么需要 headless service 无头服务?

在用Deployment时,每一个Pod名称是没有顺序的,是随机字符串,因此是Pod名称是无序的,但是在statefulset中要求必须是有序 ,每一个pod不能被随意取代,pod重建后pod名称还是一样的。而pod IP是变化的,所以是以Pod名称来识别。pod名称是pod唯一性的标识符,必须持久稳定有效。这时候要用到无头服务,它可以给每个Pod一个唯一的名称 。

为什么需要volumeClaimTemplate?

对于有状态的副本集都会用到持久存储,对于分布式系统来讲,它的最大特点是数据是不一样的,所以各个节点不能使用同一存储卷,每个节点有自已的专用存储,但是如果在Deployment中的Pod template里定义的存储卷,是所有副本集共用一个存储卷,数据是相同的,因为是基于模板来的 ,而statefulset中每个Pod都要有自已的专有存储卷,所以statefulset的存储卷就不能再用Pod模板来创建了,于是statefulSet使用volumeClaimTemplate,称为卷申请模板,它会为每个Pod生成不同的pvc,并绑定pv, 从而实现各pod有专用存储。这就是为什么要用volumeClaimTemplate的原因。

创建:


$ kubectl create -f nginx.yaml service "nginx" created statefulset "web" created

看下这三个Pod创建过程:

# 第一个是创建web-0
$ kubectl get pod
web-0                     1/1       ContainerCreating   0          51s
# 待web-0 running且ready时,创建web-1
$ kubectl get pod
web-0                     1/1       Running             0          51s
web-1                     0/1       ContainerCreating   0          42s
# 待web-1 running且ready时,创建web-2
$ kubectl get pod
web-0                     1/1       Running             0          1m
web-1                     1/1       Running             0          45s
web-2                     1/1       ContainerCreating   0          36s
# 最后三个Pod全部running且ready
$ kubectl get pod
NAME                      READY     STATUS    RESTARTS   AGE
web-0                     1/1       Running   0          4m
web-1                     1/1       Running   0          3m
web-2                     1/1       Running   0          1m

根据volumeClaimTemplates自动创建的PVC

$ kubectl get pvc
NAME              STATUS    VOLUME                                  CAPACITY   ACCESS MODES   STORAGECLASS     AGE
www-web-0         Bound     pvc-ecf003f3-828d-11e8-8815-000c29774d39   2G        RWO          glusterfs-ssd         7m
www-web-1         Bound     pvc-0615e33e-828e-11e8-8815-000c29774d39   2G        RWO          glusterfs-ssd         6m
www-web-2         Bound     pvc-43a97acf-828e-11e8-8815-000c29774d39   2G        RWO          glusterfs-ssd         4m

如果集群中没有StorageClass的动态供应PVC的机制,也可以提前手动创建多个PV、PVC,手动创建的PVC名称必须符合之后创建的StatefulSet命名规则:(volumeClaimTemplates.name)-(pod_name)

Statefulset名称为web 三个Pod副本: web-0,web-1,web-2,volumeClaimTemplates名称为:www,那么自动创建出来的PVC名称为www-web[0-2],为每个Pod创建一个PVC。
规律总结:

  • 匹配Pod name(网络标识)的模式为:$(statefulset名称)-$(序号),比如上面的示例:web-0,web-1,web-2。
  • StatefulSet为每个Pod副本创建了一个DNS域名,这个域名的格式为: $(podname).(headless server name),也就意味着服务间是通过Pod域名来通信而非Pod IP,因为当Pod所在Node发生故障时,Pod会被飘移到其它Node上,Pod IP会发生变化,但是Pod域名不会有变化。
  • StatefulSet使用Headless服务来控制Pod的域名,这个域名的FQDN为:$(headless service name).$(namespace).svc.cluster.local,其中,“cluster.local”指的是集群的域名。
  • 根据volumeClaimTemplates,为每个Pod创建一个pvc,pvc的命名规则匹配模式:(volumeClaimTemplates.name)-(pod_name),比如上面的volumeMounts.name=www, Pod name=web-[0-2],因此创建出来的PVC是www-web-0、www-web-1、www-web-2。
  • 删除Pod不会删除其pvc,手动删除pvc将自动释放pv。

Statefulset的启停顺序:

  • 有序部署:部署StatefulSet时,如果有多个Pod副本,它们会被顺序地创建(从0到N-1)并且,在下一个Pod运行之前所有之前的Pod必须都是Running和Ready状态。
  • 有序删除:当Pod被删除时,它们被终止的顺序是从N-1到0。
  • 有序扩展:当对Pod执行扩展操作时,与部署一样,它前面的Pod必须都处于Running和Ready状态。 

Statefulset Pod管理策略:
在v1.7以后,通过允许修改Pod排序策略,同时通过.spec.podManagementPolicy字段确保其身份的唯一性。
– OrderedReady:上述的启停顺序,默认设置。
– Parallel:告诉StatefulSet控制器并行启动或终止所有Pod,并且在启动或终止另一个Pod之前不等待前一个Pod变为Running and Ready或完全终止。

更新策略

在Kubernetes 1.7及更高版本中,通过.spec.updateStrategy字段允许配置或禁用Pod、labels、source request/limits、annotations自动滚动更新功能。

  • OnDelete:通过.spec.updateStrategy.type 字段设置为OnDelete,StatefulSet控制器不会自动更新StatefulSet中的Pod。用户必须手动删除Pod,以使控制器创建新的Pod。
  • RollingUpdate:通过.spec.updateStrategy.type 字段设置为RollingUpdate,实现了Pod的自动滚动更新,如果.spec.updateStrategy未指定,则此为默认策略。StatefulSet控制器将删除并重新创建StatefulSet中的每个Pod。它将以Pod终止(从最大序数到最小序数)的顺序进行,一次更新每个Pod。在更新下一个Pod之前,必须等待这个Pod Running and Ready。
  • Partitions:通过指定 .spec.updateStrategy.rollingUpdate.partition 来对 RollingUpdate 更新策略进行分区,如果指定了分区,则当 StatefulSet 的 .spec.template 更新时,具有大于或等于分区序数的所有 Pod 将被更新。具有小于分区的序数的所有 Pod 将不会被更新,即使删除它们也将被重新创建。如果 StatefulSet 的 .spec.updateStrategy.rollingUpdate.partition 大于其 .spec.replicas,则其 .spec.template 的更新将不会传播到 Pod。在大多数情况下,不需要使用分区,但如果要进行灰度更新,可以尝试金丝雀部署或执行分阶段部署,它们更有用。

浅谈AFNetworking之《一》总览

AFNetworking框架是当下iOS网络开发中使用最广的开源框架。今天起,和大家一起从源码的角度来重新认识一下他。
AFNetworking的核心是对URLSession的封装。而URLSession正式引入iOS是从iOS7开始,用来取代URLConnection。直到iOS9,苹果完全放弃了URLConnection,全面使用URLSession.

首先我们要认识的第一个概念就是URLSession。
简单总结一下:
1、URLSession实例是线程安全的。
2、URLSession的创建是伴随着一个 NSURLSessionConfiguration 的,这个配置控制着网络凭证、缓存、cookie等等。在URLSession创建时就需要提供,创建之后再给就不起作用了
3、URLSession可以是各种URLSessionTask的管理者。
4、URLSessionTask对象默认被创建于挂起状态,当需要执行时,必须要调用resume

第二个概念是URLSessionTask
用苹果的话讲 URLSessionTask — a cancelable object that refers to the lifetime of processing a given request. 也就是 掌控了一个请求的整个处理生命周期的一个可取消的对象。所以,它的整个存在的意义,就是对请求生命周期的掌控。 这中间包含了对请求的唯一定位、优先设定、记录将要(/已经/对端期待等等)发出/收到的包以及请求的当前状态等、请求的挂起、恢复、取消。 总之一句话,掌控请求生命周期。
至于它的子类,Data/Update/Download/Stream 则是根据不通的情形所做的特定处理。

回到我们的AFNetworking.
前面也说了,其实AFNetworking做的主要的事情就是针对URLSession框架的一个封装,以便于我们业务上的使用。

结构如上:分为AFNetworking.h + URLSession + Reachability + Security + Serialization + UI
我们后面分析的模块顺序从URLSession -> Serialization -> Security -> Reachability -> UIKit
URLSession模块中核心类是AFURLSesssionManager, AFHTTPURLSesssionManager只是对它的一个包装,核心实现都在AFURLSesssionManager中。AFURLSesssionManager承接管理了所有URLSessionTask的生命周期,以及对处理结果的一个异步处理,整体分发。
Serialization模块的核心是AFURLResponseSerialization和AFURLRequestSerialization,AFURLResponseSerialization是解析处理的核心抽象类,目前AF支持JSON、XML、Plist、简单图片、混合5中解析方式。
模块安全、网络连通性类相对少,按顺序来就是
最后的UI模块,我们准备挑几个有代表性的来进行研究。

接入服务器的负载均衡技术剖析

为什么需要负载均衡技术

负责均衡技术是用来解决下面两个问题的:
1、服务器的高可用问题,通过负责均衡将流量按一定的规则转发到后端的业务服务器,保证后端服务都可以多副本部署,从而解决服务部署的单点问问题;

2、单台服务器的性能瓶颈问题,单台物理机器的性能瓶颈是有上限的,当访问流量达到一定程度后,单机是无法处理的,通过负载均衡将流量按一定规则转发的后端多台业务服务器上,从而达到性能的提升;

使用DNS进行负载均衡

客户端在向服务器发送http请求之前,通常会使用dns将服务器的域名地址解析为ip,所以使用dns进行负载均衡是非常简单可行的。一般的方式是在dns的域名解析中提供多个A纪录或者AAAA纪录,客户端中解析的时候任意选择一个ip地址使用。这个方式虽然简单,但是有一下三个问题:
1、这个机制对客户端的行为的控制力很弱:
因为客户端是随机选择一个ip地址,所以理论上会导致所有的接入服务器都会分流等量的流量,这个是一个很大的问题,它会导致接入服务器或者集群只能平均部署。虽然目前SRV协议支持设置每一个解析ip的优先级与权重,但是http协议目前不支持;

2、不能根据用户的地理位置来返回最近的接入服务器ip:
理论上,数据在光纤中以光速折线运行,所以离接入服务器越近,访问的时延会越低。但是根据dns协议,用户很少与权威域名服务器直接联系,而是通过一个递归解析器来代理解析,并且递归解析器通常还有缓存,这样会带来下面几个问题:
a、递归的方式解析ip地址,导致权威服务器拿不到用户的ip,只能拿到递归解析器的ip地址,因而不能根据用户的信息来返回离用户最近的接入服务器ip。虽然edns0扩展协议中在递归解析器代理解析的时候,会带上用户ip的子网段,但是该扩展协议目前并没有正式通过,并且国内域名解析服务的混乱,很难保证能正常运行;
b、域名解析结构缓存的问题。域名递归解析器缓存了解析纪录,导致一次解析的结果可能只给一个用户,也可能是数万个,导致对接入服务器流量控制很慢。一个解决方案是通过分析流量的变化,来评估解析器的预期影响,从而来纠正缓存导致的数据偏差;

3、不能立即删除dns解析纪录。由于dns权威服务器不提供清除解析纪录缓存的行为,只能给dns纪录保持一个比较低的ttl,更有问题的是不是所有的dns解析都遵守权威解析服务器的ttl,这样都解析纪录中的某一个接入服务器出故障的时候,还是有一段时间用户通过dns解析访问到有故障的接入服务器。

4、dns解析结果的长度是有限制的,rfc1035将dns回复长度限制为512字节,这导致如果接入服务器过多的时候,导致接入服务器的ip不能完全返回,实际是限制了接入服务器的数量,这个对于流量大的业务是有很大问题的。

从上面可以看出,dns虽然可以进行负载均衡,但是有很多的问题,那有没有办法来解决这些问题呢?当然有,在dns负载均衡与接入服务器之间加上一层虚拟ip就可以解决这个问题。

虚拟ip

虚拟ip是通过一个调度节点来接收外地流量,然后调度节点将流量转发给后端的接入服务器来进行处理,调度节点的ip地址就是虚拟ip。在linux环境下,虚拟ip实现的技术一般都是lvs来实现的,下面来分析虚拟ip实现的几种方案:

1、nat模式:

nat模式简单来说就是调度节点收到数据包后,通过修改数据包的源ip为调度节点的ip,目的ip为后端接入服务器的ip(按一定负载均衡的策略),并且纪录好映射关系,这样后端服务器就可以收到调度节点转发过来的数据包。同时,由于后源ip是调度节点的ip,所以后端服务器处理完后,数据包会先发到调度服务器,调度服务器再按映射关系将数据包的源地址修改为调度节点的IP,目的地址修改为用户的ip地址,从而达到负载均衡的目的。这种方式存在以下问题:
a、调度服务器需要维护好映射关系,这个在高并发高qps的情况下是一个不小的负担;
b、来回的数据包都需要在调度服务器进行nat的映射,一般web服务器都是用户请求的流量比较小,服务器响应的流量比较大,这个会极大的影响调度服务器的性能;
nat模式上面的两个问题,我们可以通过dr模式来解决。

2、dr模式:

dr模式是在调度节点收到数据包后,不再修改数据包的源ip地址和目的ip地址,而是直接将数据包需要转发的mac地址修改为后端接入服务器的mac地址(按一定负载均衡的策略),然后直接发送到网卡,接入服务器收到后进行回包的时候,由于数据包的源ip地址和目的ip地址都没有修改,所以数据包直接从接入服务器发送出去,到达用户。从上面的描述中,可以看出,调度节点和接入服务器节点之前二层是需要连通的,所以这种方法存在一个问题,调度节点与接入服务器的数据包都必须在一个子网络中进行广播,当流量足够大的时候,这将是整个系统的瓶颈,因而不能应用在大流量的环境中。
dr模式导致在一个子网冲突域的问题,我们可以通过tull模式来解决。

3、tull模式:

tull模式是在调度节点收到数据包后,再进行一次封装,在原来的数据包再加一个包头,源IP为调度节点的IP,目的ip为后端接入服务器的ip(按一定负载均衡的策略),接入服务收到数据包后,去掉调度节点的封装头,按数据包中的源ip和目的ip进行回包,即回包的源ip为调度节点ip,目的ip为用户的IP,数据包直接发送给用户而不需要经过调度节点,并且由于再进行了一次封装,数据包从调度节点到接入服务器直接是通过ip层进行路由的,所有后端接入服务可以处于不同的网络,避免了dr模式只能在一个子网的限制。

总结

从上面的分析可以得出,前端接入服务的负载均衡的一个最佳实践为;利用dns解析进行第一层负载均衡,将用户的流量按一定规则负载均衡到数据中心的虚拟ip,然后虚拟ip通过tull模式再将流量负载均衡到真实的接入服务器上,如果能将虚拟ip的实时流量负载和状态反馈到dns解析服务器,实时调整虚拟ip的权重,那将是最优的方式了(google就是这么干的)。

Vue extend 与$mount学习总结

写 Vue.js 时,不论是用 CDN 的方式还是在 Webpack 里用 npm 引入的 Vue.js,都会有一个根节点,并且创建一个根实例,比如:

    <body> 
        <div id="app"></div> 
    </body> 
    <script> 
        const app = new Vue({ el: '#app' }); 
    </script> 

Webpack 也类似,一般在入口文件 main.js 里,最后会创建一个实例;

    import Vue from 'vue'; 
    import App from './app.vue'; 
    new Vue({ 
        el: '#app', 
        render: h => h(App) 
    }); 

用 Webpack 基本都是前端路由的,它的 html 里一般都只有一个根节点

,其余都是通过 JavaScript 完成,也就是许多的 Vue.js 组件(每个页面也是一个组件)。

有了初始化的实例,之后所有的页面,都由 vue-router 管理,组件也都是用 import 导入后局部注册,也有在main.js全局注册的。这种组件使用方式,有几个特点:

1.所有的内容,都是在 #app 节点内渲染的;

2.组件的模板,是事先定义好的;

3.由于组件的特性,注册的组件只能在当前位置渲染。

比如你要使用一个组件 ,渲染时,这个自定义标签就会被替换为组件的内容,而且在哪写的自定义标签,就在哪里被替换。换句话说,常规的组件使用方式,只能在规定的地方渲染组件,这在一些特殊场景下就比较局限了,例如:
组件的模板是通过调用接口从服务端获取的,需要动态渲染组件;

实现其实类似于原生 window.alert() 的提示框组件,它的位置是在 下,而非

<

div id=”app”>,并且不会通过常规的组件自定义标签的形式使用,而是像 JS 调用函数一样使用。

用法

创建一个 Vue 实例时,都会有一个选项el,来指定实例的根节点,如果不写 el 选项,那组件就处于未挂载状态。Vue.extend的作用,就是基于Vue构造器,创建一个“子类”,它的参数跟 new Vue 的基本一样,但 data 要跟组件一样,是个函数,再配合 $mount ,就可以让组件渲染,并且挂载到任意指定的节点上,比如 body。

比如说的window.alert的场景,就可以这样写:

import Vue from 'vue'; 
const AlertComponent = Vue.extend({ 
    template: '<div>{{ message }}</div>',
    data () { 
        return { 
            message: 'Hello, banyu'
        }; 
    }, 
}); 

这一步,我们创建了一个构造器,这个过程就可以解决异步获取 template 模板的问题,下面要手动渲染组件,并把它挂载到 body 下:

const component = new AlertComponent().$mount(); 

这一步,我们调用了$mount方法对组件进行了手动渲染,但它仅仅是被渲染好了,并没有挂载到节点上,也就显示不了组件。此时的component已经是一个标准的Vue组件实例,因此它的 $el 属性也可以被访问:

document.body.appendChild(component.$el); 

当然,除了 body,你还可以挂载到其它节点上。

$mount 也有一些快捷的挂载方式,以下两种都是可以的:

// 在 $mount 里写参数来指定挂载的节点 
new AlertComponent().$mount('#app'); 
// 不用 $mount,直接在创建实例时指定 el 选项 
new AlertComponent({ 
    el: '#app' 
}); 

实现同样的效果,除了用 extend 外,也可以直接创建 Vue 实例,并且用一个 Render 函数来渲染一个 .vue 文件:

import Vue from 'vue'; 
import Notification from './notification.vue'; 
const props = {}; 
// 这里可以传入一些组件的 props 选项 
const Instance = new Vue({ 
    render (h) { 
        return h(Notification, {
            props: props 
        }); 
    } 
}); 
const component = Instance.$mount(); 
document.body.appendChild(component.$el); 

这样既可以使用 .vue 来写复杂的组件,还可以根据需要传入适当的 props。渲染后,如果想操作 Render 的 Notification 实例,也是很简单的:

const notification = Instance.$children[0]; 

因为 Instance 下只 Render 了 Notification 一个子组件,所以可以用 $children[0] 访问到。

需要注意的是,我们是用 $mount 手动渲染的组件,如果要销毁,也要用 $destroy 来手动销毁实例,必要时,也可以用 removeChild 把节点从 DOM 中移除。

科学上网

hi, 互联网的同学们基本都会遇到的难点是什么呢,想必大家都能猜到一、二、三,😄 不兜圈子了,直插主题,那就是通过互联网得到我们想要的答案。

我今天分享的主题是如何在浏览器、终端科学上网。
我介绍一下目前我用之最稳定的科学上网方式

不管是浏览器、还是终端,科学上网依赖的是Shadowsocks(简称SS)

Shadowsocks的运行原理是什么?
原理自己百度

我今天主要介绍技术实现的方式
首先需要准备一台阿里云香港的linux server

yum -y install epel-release -y
yum install python-pip -y
pip install shadowsocks
nohup ssserver -s 公网ip -k 密码 -m aes-256-cfb  > /dev/null 2>&1 &
nohup sslocal -s 公网ip —-l 1080 -k 密码 -m aes-256-cfb  > /dev/null 2>&1 &

然后client telnet测试

telnet 公网ip 1080

如果测试通过,下载科学上网工具Shadowsocks,根据提示输入相应配置,这样浏览器就可以科学上网。

但是这只是浏览器可以科学上网,好多码农,尤其是golang工程师,经常需要下载一些宇宙的包,就需要终端实现科学上网。

终端科学上网用到的工具是polipo

polipo运行原理是什么?
原理自己百度

主要介绍一下实现方式,首先在阿里云香港的server

yum install texinfo -y
git clone https://github.com/jech/polipo.git
cd polipo
make all
make install

mkdir /etc/polipo
vim /etc/polipo/config

daemonise = true
proxyAddress = "0.0.0.0"
socksParentProxy = "127.0.0.1:1080"
socksProxyType = socks5
chunkHighMark = 50331648
objectHighMark = 16384

polipo -c /etc/polipo/config

然后client telnet测试

telnet 公网ip 8123

如果测试通过,在终端设置proxy

export http_proxy="http://公网ip:8123"
export https_proxy="https://公网ip:8123"

最后golang的工程师可以尝试一下这种方式看是否可以满足你们的大部门科学上网的场景呢,如果可以,请不要记得我,因为我是雷锋

浏览器资源加载优先级

前端资源加载优先级

我们经常会思考一个经典问题,”一个页面从输入URL到页面加载显示完成,这个过程都发生什么?”, 此问题涉及的范围可谓非常之广,今天我们只关注浏览器加载资源的过程。

资源加载优先级

首先,浏览器会根据资源重要性,给资源设定优先级(实际上会更复杂,浏览器会根据资源类型进行猜测,例如先加载css然后再加载js和img),我们可以通过Chrome Devtools查看资源加载优先级,如果你的Network面板没有Priority选项,可以在表头上点击右键显示,下图是我们生产环境一个页面的JS 和 CSS加载优先级

可以看到浏览器将资源优先级分为以下5类

Highest 最高
Hight 高
Medium 中等
Low 低
Lowest 最低

这五类规则见下图

img

手动控制加载优先级

虽然浏览器会自动判断资源重要性,但通常情况下,浏览器会因为获取的信息不足,导致做出错误的判断

那么如何手动控制浏览器加载优先级呢?

主要有3种指令:

  • preload 预加载
  • preconnect 预连接
  • prefetch 预获取

preload

link rel="preload" 告知浏览器当前导航需要某个资源,应尽快开始提取

可以注意到link上使用了属性“as”, 该属性允许你告知浏览器您将加载的资源类型,以便浏览器可以正确处理该资源

link rel="preload" 是强制浏览器执行的指令;与我们将探讨的其他资源提示不同,它是浏览器必须执行的指令,而不只是可选提示.

preconnect

link rel="preconnect" 告知浏览器您的页面打算与另一个起点建立连接,以及您希望尽快启动该过程。

预连接的作用是提前进行DNS查找,TCP连接,TLS协商(https)

但请注意,preconnet虽然成本很低,但依然会占用CPU时间,而且如果10s内没有使用此连接,情况尤为糟糕,因为当浏览器关闭连接时,所有已完成的连接都将遭到浪费。

除此之外,还有另外一个标签可用于回退方案,而且被浏览器厂商广泛支持

link rel="dns-prefetch" href="//s04.cdn.ipalfish.com"

prefetch

link rel="prefetch" 与以上2个指令不同,他不会发生资源抢占,而是在带宽空闲(idle)时发生

此指令通常用于此资源用于未来,例如其他页面跳转,或者用户交互后才会出现的行为,通常此资源的优先级会标记为 Lowest

prefetch 不会降低现有资源的优先级,即如果此资源很快会被加载,则尽量不要使用prefetch,否则会导致资源会被加载多次。

项目实践

绘本项目使用vue-cli 3搭建,已在生产环境实践使用prefetch,preload加载资源

Vue-cli3 默认会进行以上优化,方案是:

  1. 根据入口文件依赖添加PreloadWebpackPlugin ,以便初始化渲染

  2. 根据async chunk生成的文件 添加PrefetchWebpackPlugin 插件

开学季活动页面举例

路由规则如下:

routes: [
    {
      path: '/',
      name: 'Index',
      component: Index
    },
    {
      path: '/record',
      name: 'Record',
      component: Record
    },
    {
      path: '/rule',
      name: 'Rule',
      component: Rule
    },
    {
      path: '*',
      redirect: '/'
    }
  ]

根据以上对prefetch的描述,我们知道首屏加载资源使用prefetch很可能会导致资源会被多次加载,对此我们可以使用2种方案:

  1. 首页(首屏)加载资源不进行async处理
  2. 有选择的进行prefetch

现在项目中使用的是第二种方案,先在webpack中去掉prefetch插件

Object.keys(pages).forEach((page) =&gt; {
    // 去掉prefetch 业务自己决定 https://cli.vuejs.org/zh/guide/html-and-static-assets.html#prefetch
    config.plugins.delete(`prefetch-${pages[page].path}`)
  })

然后业务方根据自己需要添加prefetch指令

const Index = () =&gt; import(
  /* webpackChunkName: "school-season-index" */
  './index')

const Record = () =&gt; import(
  /* webpackChunkName: "school-season-record" */
  /* webpackPrefetch: true */
  './record')

const Rule = () =&gt; import(
  /* webpackChunkName: "school-season-rule" */
  /* webpackPrefetch: true */
  './rule')

以上可通过开学季活动页面查看

除此之外还有prerender 可进行页面预渲染,但支持的浏览器厂商较少,暂不做考虑

参考

https://github.com/joshbuchea/HEAD

https://www.w3.org/TR/resource-hints

https://developers.google.com/web/fundamentals/performance/resource-prioritization

https://medium.com/reloading/preload-prefetch-and-priorities-in-chrome-776165961bbf

首页弹窗业务逻辑简单设计

1.业务描述
app首页会在不同时期、根据不同的需求弹出业务处理逻辑不同的弹窗,比如1v1外教课,奖学金红包,每日学弹窗等。

2.业务需求
app有ios,和android两个版本,因客户端需要发版,且发版后,并不一定所有人都会更新。所以首页弹窗需要尽量通用且在是服务端可配置的、或者服务端更新上线可以支持新的弹窗。

3.实现需求前的简单分析
弹框的共同点
a.弹框都是1-2张图+跳转route
b.都有自己的弹窗类型记为 gtype
c.都有一个弹窗的优先级,因为不管何种弹窗,用户每次只能看到一个弹窗 priority
d.都有弹出类型,从目前来看,弹窗弹出类型暂时有两种,一共只弹一次 和 一天弹一次 repeattype
e.都应该有一个开关 switch
f.都应该有安卓和ios对应的启用版本号。 因为对于未升级的老版本app并没有弹窗上说的那些内容。 iosversion,androidversion
g.都应该有测试账号列表。 方便线上测试 testuids
h.都应该有点击状态的记录,是点了关闭弹框,还是点了弹框内容

弹框的不同点
a.不同的弹框,弹出的条件不同。
b.弹框内容不同,也就是图片的url和route不同

4.需求的实现
根据上述的共同点和不同点,需求的实现逻辑为
a.将开关打开的弹框,根据优先级从高到低排序,遍历一遍
b.先根据共同的部分,包括弹窗类型,版本号,测试账号列表,过滤
c.再根据不同的gtype做不同的过滤逻辑
d.对符合条件的第一个弹框,针对不同的弹窗类型,处理并补充一些数据,返回给客户端
e.客户端展示弹框,用户点击后上报弹框点击状态

Vue中jsx的使用

标签(空格分隔): Vue jsx


背景

目前的项目主要都是依赖在Vue框架下进行开发的,一般情况下,在Vue文件中都是使用template来进行编写代码,但是有一些需要定制配置的页面,这样写起来就特别蛋疼,比如下面的代码:

    import preview from './preview'
    const config = {
        preview: {
            name: '名称',
            preIcon: 'el-icon-view',
            dialog: {
                slot: [
                    { key: 'body', child: preview, data: {name: '预览'}},
                ]
            },
            click: () => {
                console.log('触发点击')
            }
        },
    }

上面是一个渲染 按钮 – 弹窗 的组件,点击按钮可以触发一个弹窗,dialog里面是弹窗的具体内容,其中slot是自定义渲染的内容,渲染一个自定义的插槽来满足定制化的业务场景。这种写法可以满足业务封装的需求,使得项目在一定程度上可以配置化编写,有利于后期项目的拓展和维护。但是其实我的preview功能可能仅仅是这样:

    // preview.js
    <template>
        <div>data.name</div>
    </template>
    <script>
        export default {
            props: {
                data: Object
            }
        }
    </script>

其实我仅仅需要的就是一个div标签而已,但是使用vue的template却只能新创建一个vue文件来进行引入。如果换成jsx那么我们直接就可以这样写:


const config = { preview: { name: '名称', preIcon: 'el-icon-view', dialog: { slot: [ { key: 'body', child: <div>预览</div>}, ] }, click: () => { console.log('触发点击') } }, }

这样就简单很多了,而且也不需要单独创建一个.vue文件。

引申

从上述可以看出,有些情况下vue jsx确实会带来一定的优越性(特别对于写惯了react的切图仔)。下面介绍一下写vue jsx的时候需要注意的点,以及与react的不同。

与react jsx的不同

  1. React中父子之间传递的所有数据都是属性,即所有数据均挂载在props下(style, className, children, value, onChange等等。
  2. Vue则不然,仅仅属性就有三种:组件属性props,普通html属性attrs,Dom属性domProps。

如下示例所示:

const ButtonCounter = {
  name: "button-counter",
  props: ["count"],
  methods: {
    onClick() {
      this.$emit("change", this.count + 1);
    }
  },
  render() {
    return (
      <button>You clicked me {this.count} times.</button>
    );
  }
};

export default {
  name: "button-counter-container",
  data() {
    return {
      count: 0
    };
  },
  methods: {
    onChange(val) {
      this.count = val;
    }
  },
  render() {
    const { count, onChange } = this;
    return (
      <div>


      </div>
    );
  }
};

通过上述我们可以区分出这几种属性的区别:

  1. 组件属性props:指组件声明的属性,即上述示例中声明的props: [‘count’]。

  2. 普通html属性attrs: 指组件未声明的属性,即上述示例中的type=”button”,该属性默认会直接挂载到组件根节点的上,如果不需要挂载到根节点,可声明 inheritAttrs: false。

  3. Dom属性domProps:指的Dom属性,如上述示例中的innerHTML,它会覆盖组件内部的children, 这类属性我们一般很少使用到。

抽象程度较高的动态属性

一般情况下,在已知属性和方法的时候,我们可以直接将属性值传入已经定义好的组件即可,但是如果想高度抽象来复用一些组件,那静态属性传入就不太合适了,这个时候一般使用如下方法将属性传入子组件:

    const dynamicProps = {
        props: {},
        on: {},
    }
    if(hasValue) dynamicProps.props.value = value
    if(hasChange) dynamicProps.on.change = onChange

本文关于vue jsx的介绍就到这里,如果想了解更多,建议查看官方文档。