整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

HTML框架分层设计

算机网络中的OSI七层模型

计算机网络中的OSI(Open Systems Interconnection)七层模型是一种理论框架,用于描述计算机网络中数据通信的过程。OSI模型将计算机网络通信过程划分为七个层次,每个层次都有其特定的功能和协议。这种分层结构有助于研究和理解计算机网络中的通信原理。以下是OSI七层模型的各个层次及其主要功能:

应用层是OSI模型的第七层,也是网络应用程序和网络协议之间的接口。应用层主要负责为用户提供各类应用服务,如文件传输、电子邮件、Web浏览等。

表示层是OSI模型的第六层,主要负责处理在网络中传输的数据的表示方式,如数据加密、解密、压缩、解压缩等。表示层确保了不同系统之间的数据兼容性。

会话层是OSI模型的第五层,主要负责建立、维护和终止应用程序之间的通信会话。会话层提供了数据交换的同步和确认机制。

传输层是OSI模型的第四层,主要负责在源主机和目标主机之间提供可靠的、端到端的数据传输服务。传输层通过分段、封装和重组数据来实现可靠的数据传输。常见的传输层协议包括TCP(传输控制协议)和UDP(用户数据报协议)。

网络层是OSI模型的第三层,主要负责将数据包从源主机路由到目标主机。网络层主要负责逻辑寻址、路由选择和分组转发。常见的网络层协议包括IP(互联网协议)和ICMP(互联网控制报文协议)。

数据链路层是OSI模型的第二层,主要负责将网络层传来的数据包封装成帧(Frame),并在同一局域网内进行传输。数据链路层主要负责物理寻址、数据成帧、错误检测和流量控制。常见的数据链路层协议包括以太网(Ethernet)、令牌环(Token Ring)和无线局域网(Wi-Fi)等。

物理层是OSI模型的第一层,主要负责在物理介质上实现比特流的透明传输。物理层主要关注硬件接口、电气特性、光纤、无线传输等方面的问题。

OSI七层模型提供了一个通用的框架,帮助研究和理解计算机网络中的通信原理。实际应用中,我们通常使用TCP/IP四层模型,它包括了应用层、传输层、网络层和链路层,与OSI模型有一定的对应关系。

HTML框架的必要性

HTML框架进行分层设计的主要原因是为了提高代码的可读性、可维护性和可重用性。将HTML框架分层可以提高整体项目的结构和逻辑,便于开发者更好地理解和修改代码。分层设计具有以下优点:

  1. 提高可读性:通过将HTML框架划分为不同的层次,可以使代码结构更清晰,有助于开发者快速理解代码的功能。
  2. 便于维护:分层设计有助于将功能模块化,这样可以方便地修改或替换某个模块,而不会影响其他部分的代码。这有助于提高项目的可维护性。
  3. 可重用性:将HTML框架分层可以将公共部分提取为可重用的组件,这样可以在不同项目中重复使用这些组件,提高开发效率。
  4. 适应性:分层设计可以让HTML框架更容易适应不同的设备和屏幕尺寸,提高项目的兼容性。
  5. 便于协作:在大型项目中,通常会有多个开发者参与。通过分层设计,开发者可以专注于自己的模块,减少代码冲突和沟通成本。

HTML框架的组成

HTML框架包括Application层``middleware层``route层``codec层``transport层 Application层 应用层通常包括与业务逻辑相关的代码,如Web应用程序的控制器(Controller)、视图(View)和模型(Model)。应用层的主要作用是处理用户请求并返回相应的响应。

Middleware层 中间件层是介于应用层和底层框架之间的一层,负责处理一些通用的功能,如身份验证、授权、缓存、日志记录等。中间件层有助于将业务逻辑与通用功能分离,使得应用层更加简洁和易于维护。

Route层 路由层负责处理HTTP请求的URL和HTTP方法(如GET、POST等),将请求分发到相应的控制器和方法。路由层的主要作用是根据URL映射来定位具体的功能代码。

Codec层 编解码层负责处理数据的编码和解码。在Web开发中,编码和解码通常涉及到HTML、CSS、JavaScript等前端技术的处理,以及JSON、XML等数据交换格式的处理。编解码层的主要作用是将数据转换为特定的格式,以便在不同层之间进行传输和处理。

Transport层 传输层负责处理底层的网络通信,如TCP、UDP等协议的使用。在Web开发中,传输层通常涉及到HTTP协议的处理,包括请求和响应的创建、发送和接收。传输层的主要作用是确保数据的可靠传输和在网络中的正确路由。

这些层次在实际应用中可能因框架和场景的不同而有所差异。但是,从您提供的描述来看,它们分别负责处理Web应用程序中的不同功能,共同构成了一个完整的Web开发框架。

HTML框架和服务端客户端之间的通信对比

Application层应用层设计

应用层设计主要是设置各种接口,用于路由使用。

例如在字节后端进阶版中的大项目中的注册接口。

/douyin/user/register/ - 用户注册接口

新用户注册时提供用户名,密码,昵称即可,用户名需要保证唯一。创建成功后返回用户 id 和权限token.

接口类型

POST

接口定义

go复制代码syntax = "proto2";
package douyin.core;

message douyin_user_register_request {
  required string username = 1; // 注册用户名,最长32个字符
  required string password = 2; // 密码,最长32个字符
}

message douyin_user_register_response {
  required int32 status_code = 1; // 状态码,0-成功,其他值-失败
  optional string status_msg = 2; // 返回状态描述
  required int64 user_id = 3; // 用户id
  required string token = 4; // 用户鉴权token
}
go复制代码func Register(username, password string) (id int64, token int64, err error) {
   if len(username) > 32 {
      return 0, 0, errors.New("用户名过长,不可超过32位")
   }
   if len(password) > 32 {
      return 0, 0, errors.New("密码过长,不可超过32位")
   }
   // 先查布隆过滤器,不存在直接返回错误,降低数据库的压力
   if userNameFilter.TestString(username) {
      return 0, 0, errors.New("用户名已经存在!")
   }
   //雪花算法生成token
   node, err := snowflake.NewNode(1) //这里的userIdInt64就是 User.Id(主键)
   if err != nil {
      log.Println("雪花算法生成id错误!")
      log.Println(err)
   }
   token1 := node.Generate().Int64()
   tokenStr := strconv.FormatInt(token1, 10)
   user := domain.User{}
   // 再查缓存
   data, err := dao.RedisClient.Get(context.Background(), tokenStr).Result()
   if err == redis.Nil {
      fmt.Println("token does not exist")
   } else if err != nil {
      fmt.Println("Error:", err)
   } else {
      num, err := strconv.ParseInt(data, 10, 64)
      if err != nil {
         fmt.Println("Error:", err)
         return num, 0, err
      }

      return num, token1, nil
   }
   //在查数据库
   user = domain.User{}
   dao.DB.Model(&domain.User{}).Where("name = ?", username).Find(&user)
   if user.Id != 0 {
      return 0, 0, errors.New("用户已存在")
   }
   user.Name = username
   // 加密存储用户密码
   user.Salt = randSalt()
   buf := bytes.Buffer{}
   buf.WriteString(username)
   buf.WriteString(password)
   buf.WriteString(user.Salt)
   pwd, err1 := bcrypt.GenerateFromPassword(buf.Bytes(), bcrypt.MinCost)
   if err1 != nil {
      return 0, 0, err
   }
   user.Pwd = string(pwd)

   //存在mysql里边
   dao.DB.Model(&domain.User{}).Create(&user)
   //再把用户id作为键 用户的所有信息作为值存在其中
   //用户信息的缓存是 保存在redis中 一个以id为键 user json为值
   jsonuser, err1 := MarshalUser(user)
   if err1 != nil {
      fmt.Println("err1", err1)
      return 0, 0, err1
   }
   err = dao.RedisClient.Set(context.Background(), strconv.FormatInt(user.Id, 10), jsonuser, 0).Err()
   if err != nil {
      fmt.Println("err", err)
      return 0, 0, err
   }
   // 布隆过滤器中加入新用户
   userIdFilter.AddString(strconv.FormatInt(user.Id, 10))
   userNameFilter.AddString(username)
   return user.Id, token1, nil
}

本接口注册功能实现:把所有信息存在mysql里边当然redis里边也存在这些信息,当然username也存在了布容过滤器中去,当接收到用户的username的时候我们现在布容过滤器中先查询是否存在如果存在则直接返回err,不存在然后再在redis里边查询,因为redis相比于mysql是更为轻量级的所以我们要先在redis里边进行查,如果查不到再进mysql里边查去,查不到说明没有注册过,可以注册。

命名规范

遵循命名规范原则。

Middleware层中间件

gin框架里的中间件分为全局中间件,局部中间件。那么什么是中间件?中间件是为应用提供通用服务和功能的软件。数据管理、应用服务、消息传递、身份验证和 API 管理通常都要通过中间件。在gin框架里,就是我们的所有API接口都要经过我们的中间件,我们可以在中间件做一些拦截处理。

中间件常用模型

全局中间件

这个是在服务启动就开始注册,全局意味着所有API接口都会经过这里。Gin的中间件是通过Use方法设置的,它接收一个可变参数,所以我们同时可以设置多个中间件。

首先定义如下

go复制代码// 1.创建路由
r := gin.Default()  //默认带Logger(), Recovery()这两个内置中间件
r:= gin.New()      //不带任何中间件
// 注册中间件  
r.Use(MiddleWare())
r.Use(MiddleWare2())

注意的是

gin.Default()默认使用了Logger和Recovery中间件,其中:Logger中间件将日志写入gin.DefaultWriter,即使配置了GIN_MODE=release。Recovery中间件会recover任何panic。如果有panic的话,会写入500响应码。如果不想使用上面两个默认的中间件,可以使用gin.New()新建一个没有任何默认中间件的路由。

go复制代码// 定义中间
func MiddleWare() gin.HandlerFunc {
    return func(c *gin.Context) {
        t := time.Now()
        fmt.Println("中间件开始执行了")
        // 设置变量到Context的key中,可以通过Get()取
        c.Set("request", "这是中间件设置的值")
        status := c.Writer.Status()
        fmt.Println("中间件执行完毕", status)        
        t2 := time.Since(t)
        fmt.Println("time:", t2)
    }
}

然后启动我们的服务,访问任意一个接口可以看到输出如下

这是请求先到了中间件,然后在到我们的API接口。在中间件里可以设置变量到Context的key中,然后在我们的API接口取值。

go复制代码        r.GET("/", func(c *gin.Context) {
            // 取值
            req, _ := c.Get("request")
            fmt.Println("request:", req)
            // 页面接收
            c.JSON(200, gin.H{"request": req})
        })

这时候在访问就可以看到中间件设置的值是

next方法是在中间件里面使用,这个是执行后续中间件请求处理的意思(含没有执行的中间件和我们定义的GET方法处理,如果连续注册几个中间件则会是按照顺序先进后出的执行,遇到next就去执行下一个中间件里的next前面方法。

go复制代码        // 执行函数
        c.Next()
        // 中间件执行完后续的一些事情

局部中间件

局部中间件意味着部分接口才会生效,只在局部使用,这时候访问http:127.0.0.1:8000/ 才会看到中间件的日志打印,其他API接口则不会出现。

go复制代码   //局部中间件使用
    r.GET("/", MiddleWare(), func(c *gin.Context) {
        // 取值
        req, _ := c.Get("request")
        fmt.Println("request:", req)
        // 页面接收
        c.JSON(200, gin.H{"request": req})
    })

gin内置中间件

go复制代码
func BasicAuth(accounts Accounts) HandlerFunc

func BasicAuthForRealm(accounts Accounts, realm string) HandlerFunc

func Bind(val interface{}) HandlerFunc

func ErrorLogger() HandlerFunc

func ErrorLoggerT(typ ErrorType) HandlerFunc

func Logger() HandlerFunc

func LoggerWithConfig(conf LoggerConfig) HandlerFunc

func LoggerWithFormatter(f LogFormatter) HandlerFunc

func LoggerWithWriter(out io.Writer, notlogged ...string) HandlerFunc

func Recovery() HandlerFunc

func RecoveryWithWriter(out io.Writer) HandlerFunc

func WrapF(f http.HandlerFunc) HandlerFunc

func WrapH(h http.Handler) HandlerFunc

总结

通过自定义中间件,我们可以很方便的拦截请求,来做一些我们需要做的事情,比如日志记录、授权校验、各种过滤等等。

route层路由层

Gin 是一个标准的 Web 服务框架,遵循 Restful API 接口规范,其路由库是基于 httproute 实现的。

本节将从 Gin 路由开始,详细讲述各种路由场景下,如何通过 Gin 来实现。

基本路由

  1. GET:用于处理从客户端发起的HTTP GET请求。GET请求用于从服务器获取数据,不应对服务器上的数据进行更改。例如,获取用户信息、获取文章列表等。
  2. POST:用于处理从客户端发起的HTTP POST请求。POST请求通常用于向服务器发送数据,用于创建新的资源或更新已有的资源。例如,用户注册、发布文章、更新用户信息等。
  3. PUT:用于处理从客户端发起的HTTP PUT请求。PUT请求通常用于更新服务器上的资源。例如,更新用户信息、更新文章内容等。
  4. DELETE:用于处理从客户端发起的HTTP DELETE请求。DELETE请求通常用于从服务器删除资源。例如,删除用户账户、删除文章等。
  5. PATCH:用于处理从客户端发起的HTTP PATCH请求。PATCH请求通常用于对服务器上的资源进行部分更新。例如,更新用户的部分信息、更新文章标题等。
  6. OPTIONS:用于处理从客户端发起的HTTP OPTIONS请求。OPTIONS请求用于获取服务器支持的HTTP方法。例如,跨域资源共享(CORS)场景。
  7. HEAD:用于处理从客户端发起的HTTP HEAD请求。HEAD请求类似于GET请求,但不返回响应体。主要用于获取响应头信息。例如,检查资源是否存在,但不需要获取资源内容。
  8. ANY:用于处理任何HTTP方法(GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD)的请求。适用于处理多种HTTP方法的情况。
  9. CONNECT:用于处理从客户端发起的HTTP CONNECT请求。CONNECT请求通常用于建立客户端与服务器之间的隧道,用于代理或其他场景。
  10. TRACE:用于处理从客户端发起的HTTP TRACE请求。TRACE请求用于测试或诊断网络连接。服务器应当返回原始的请求信息,以便客户端可以检查中间代理或防火墙是否进行了修改。

示例

字节大项目注册接口

go复制代码syntax = "proto2";
package douyin.core;

message douyin_user_register_request {
  required string username = 1; // 注册用户名,最长32个字符
  required string password = 2; // 密码,最长32个字符
}

message douyin_user_register_response {
  required int32 status_code = 1; // 状态码,0-成功,其他值-失败
  optional string status_msg = 2; // 返回状态描述
  required int64 user_id = 3; // 用户id
  required string token = 4; // 用户鉴权token
}
go复制代码func Register(c *gin.Context) {

   username := c.Query("username")
   password := c.Query("password")
   id, token, err := service.Register(username, password)
   if err != nil {
      c.JSON(http.StatusOK, domain.Response{StatusCode: 1, StatusMsg: err.Error()})
   } else {

      c.JSON(http.StatusOK, domain.UserLoginResponse{
         //可以直接去掉
         Response: domain.Response{StatusCode: 0},
         Id:       id,
         Token:    token,
      })
   }
}

go复制代码package main

import (
   "github.com/gin-gonic/gin"
   "github.com/goTouch/TicTok_SimpleVersion/controller"
)

func initRouter(r *gin.Engine) {
   // public directory is used to serve static resources
   r.Static("/static", "./public")

   apiRouter := r.Group("/douyin")

   // basic apis
   //controller.VerifyToken,
   apiRouter.POST("/user/", controller.UserInfo)
   apiRouter.POST("/user/register/", controller.LoginLimit, controller.Register)
   apiRouter.POST("/user/login/", controller.LoginLimit, controller.Login)
   }

codec层

在Web开发中,编码和解码通常涉及到HTML、CSS、JavaScript等前端技术的处理,以及JSON、XML等数据交换格式的处理。编解码层的主要作用是将数据转换为特定的格式,以便在不同层之间进行传输和处理。

示例

在postman中的示例 json

xml

html复制代码{"status_code":1,"status_msg":"redis: nil"}
{"status_code":2,"status_msg":"no multipart boundary param in Content-Type"}
Text复制代码{"status_code":1,"status_msg":"redis: nil"}
{"status_code":2,"status_msg":"no multipart boundary param in Content-Type"}
Auto复制代码{
    "status_code": 1,
    "status_msg": "redis: nil"
}{
    "status_code": 2,
    "status_msg": "no multipart boundary param in Content-Type"
}

transport层传输层

传输层负责处理底层的网络通信,如TCP、UDP等协议的使用。在Web开发中,传输层通常涉及到HTTP协议的处理,包括请求和响应的创建、发送和接收。传输层的主要作用是确保数据的可靠传输和在网络中的正确路由。

golang语言中net/http这个库中的conn 他是BIO自带阻塞

1. BIO (Blocking I/O)

同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。

1.1 传统 BIO

BIO通信(一请求一应答)模型图如下(图源网络,原出处不明):

采用 BIO 通信模型 的服务端,通常由一个独立的 Acceptor 线程负责监听客户端的连接。我们一般通过在 while(true) 循环中服务端会调用 accept() 方法等待接收客户端的连接的方式监听请求,请求一旦接收到一个连接请求,就可以建立通信套接字在这个通信套接字上进行读写操作,此时不能再接收其他客户端连接请求,只能等待同当前连接的客户端的操作执行完成, 不过可以通过多线程来支持多个客户端的连接,如上图所示。

如果要让 BIO 通信模型 能够同时处理多个客户端请求,就必须使用多线程(主要原因是 socket.accept()、 socket.read()、 socket.write() 涉及的三个主要函数都是同步阻塞的),也就是说它在接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理,处理完成之后,通过输出流返回应答给客户端,线程销毁。这就是典型的 一请求一应答通信模型 。我们可以设想一下如果这个连接不做任何事情的话就会造成不必要的线程开销,不过可以通过 线程池机制 改善,线程池还可以让线程的创建和回收成本相对较低。使用FixedThreadPool 可以有效的控制了线程的最大数量,保证了系统有限的资源的控制,实现了N(客户端请求数量):M(处理客户端请求的线程数量)的伪异步I/O模型(N 可以远远大于 M),下面一节"伪异步 BIO"中会详细介绍到。

我们再设想一下当客户端并发访问量增加后这种模型会出现什么问题?

程是宝贵的资源,线程的创建和销毁成本很高,除此之外,线程的切换成本也是很高的。尤其在 Linux 这样的操作系统中,线程本质上就是一个进程,创建和销毁线程都是重量级的系统函数。如果并发访问量增加会导致线程数急剧膨胀可能会导致线程堆栈溢出、创建新线程失败等问题,最终导致进程宕机或者僵死,不能对外提供服务。 golang实现BIO

NIO

NIO: NIO是一种同步非阻塞IO, 基于Reactor模型来实现的。其实相当于就是一个线程处理大量的客户端的请求,通过一个线程轮询大量的channel,每次就获取一批有事件的channel,然后对每个请求启动一个线程处理即可。这里的核心就是非阻塞,就那个selector一个线程就可以不停轮询channel,所有客户端请求都不会阻塞,直接就会进来,大不了就是等待一下排着队而已。这里面优化BIO的核心就是,一个客户端并不是时时刻刻都有数据进行交互,没有必要死耗着一个线程不放,所以客户端选择了让线程歇一歇,只有客户端有相应的操作的时候才发起通知,创建一个线程来处理请求。
————————————————
NIO:模型图

Reactor模型:

NIO核心组件详细讲解

学习NIO先来搞清楚一些相关的概念,NIO通讯有哪些相关组件,对应的作用都是什么,之间有哪些联系?

多路复用机制实现Selector

首先我们来了解下传统的Socket网络通讯模型。

传统Socket通讯原理图

为什么传统的socket不支持海量连接

每次一个客户端接入,都是要在服务端创建一个线程来服务这个客户端的,这会导致大量的客户端的时候,服务端的线程数量可能达到几千甚至几万,几十万,这会导致服务器端程序负载过高,不堪重负,最终系统崩溃死掉。

  • 接着来看下NIO是如何基于Selector实现多路复用机制支持的海量连接。

NIO原理图

多路复用机制是如何支持海量连接

NIO的线程模型 对Socket发起的连接不需要每个都创建一个线程,完全可以使用一个Selector来多路复用监听N多个Channel是否有请求,该请求是对应的连接请求,还是发送数据的请求,这里面是基于操作系统底层的Select通知机制的,一个Selector不断的轮询多个Channel,这样避免了创建多个线程,只有当莫个Channel有对应的请求的时候才会创建线程,可能说1000个请求, 只有100个请求是有数据交互的, 这个时候可能server端就提供10个线程就能够处理这些请求。这样的话就可以避免了创建大量的线程。

NIO如何通过Buffer来缓冲数据的

NIO中的Buffer是个什么东西 ?

学习NIO,首当其冲就是要了解所谓的Buffer缓冲区,这个东西是NIO里比较核心的一个部分,一般来说,如果你要通过NIO写数据到文件或者网络,或者是从文件和网络读取数据出来此时就需要通过Buffer缓冲区来进行。Buffer的使用一般有如下几个步骤:

写入数据到Buffer,调用flip()方法,从Buffer中读取数据,调用clear()方法或者compact()方法。

Buffer中对应的Position, Mark, Capacity,Limit都啥?

capacity: 缓冲区容量的大小,就是里面包含的数据大小。
limit: 对buffer缓冲区使用的一个限制,从这个index开始就不能读取数据了。
position: 代表着数组中可以开始读写的index, 不能大于limit。
mark: 是类似路标的东西,在某个position的时候,设置一下mark,此时就可以设置一个标记,后续调用reset()方法可以把position复位到当时设置的那个mark上去,把position或limit调整为小于mark的值时,就丢弃这个mark。如果使用的是Direct模式创建的Buffer的话,就会减少中间缓冲直接使用的是DirectorBuffer来进行数据的存储。
————————————————

如何通过Channel和FileChannel读取Buffer数据写入磁盘的

NIO中,Channel是什么?

Channel是NIO中的数据通道,类似流,但是又有些不同,Channel即可从中读取数据,又可以从写数据到通道中,但是流的读写通常是单向的。Channel可以异步的读写。Channel中的数据总是要先读到一个Buffer中,或者从缓冲区中将数据写到通道中。

FileChannel的作用是什么 Buffer有不同的类型,同样Channel也有好几个类型。 FileChannel,DatagramChannel,SocketChannel,ServerSocketChannel。这些通道涵盖了UDP 和 TCP 网络IO,以及文件IO。而FileChannel就是文件IO对应的管道, 在读取文件的时候会用到这个管道。 golang的NIO

前面一篇文章:「高频面试题」浏览器从输入url到页面展示中间发生了什么 中,我们有对浏览器的渲染流程做了一个概括性的介绍,今天这篇文章我们将深入学习这部分内容。

对于很多前端开发来说,平常做工主要专注于业务开发,对浏览器的渲染阶段可能不是很了解。实际上这个阶段很重要,了解浏览器的渲染过程,能让我们知道我们写的HTML、CSS、JS代码是如何被解析,并最终渲染成一个页面的,在页面性能优化的时候有相应的解决思路。

我们先来看一个问题:

HTML、CSS、JS文件在浏览器中是如何转化成页面的?

如果你回答不上来,那就往下看吧。

按照渲染的时间顺序,渲染过程可以分为下面几个子阶段:构建DOM树、样式计算、布局阶段、分层、栅格化和合成显示。

下面详细看下每个阶段都做了哪些事情。

1. 构建DOM树

HTML文档描述一个页面的结构,但是浏览器无法直接理解和使用HTML,所以需要通过HTML解析器将HTML转换成浏览器能够理解的结构——DOM树。

HTML文档中所有内容皆为节点,各节点之间有层级关系,彼此相连,构成DOM树。

构建过程:读取HTML文档的字节(Bytes),将字节转换成字符(Chars),依据字符确定标签(Tokens),将标签转换成节点(Nodes),以节点为基准构建DOM树。参考下图:

打开Chrome的开发者工具,在控制台输入 document 后回车,就能看到一个完整的DOM树结构,如下图所示:

在控制台打印出来的DOM结构和HTML内容几乎一样,但和HTML不同的是,DOM是保存在内存中的树状结构,可以通过JavaScript来查询或修改其内容。

2. 样式计算

样式计算这个阶段,是为了计算出DOM节点中每个元素的表现样式。

2.1 解析CSS

CSS样式可以通过下面三种方式引入:

  • 通过link引用外部的CSS文件
  • style 标签内的CSS
  • 元素的style属性内嵌的CSS

和HTML一样,浏览器无法直接理解纯文本的CSS样式,需要通过CSS解析器将CSS解析成 styleSheets 结构,也就是我们常说的 CSSOM树

styleSheets结构同样具备查询和修改功能:

document.styleSheets

2.2 属性值标准化

属性值标准化看字面意思有点不好理解,我们通过下面一个例子来看看什么是属性值标准化:

在写CSS样式的时候,我们在设置color属性值的时候,经常会用white、red等,但是这种值浏览器的渲染引擎不容易理解,所以需要将所有值转换成渲染引擎容易理解的、标准化的计算值,这个过程就是属性值标准化。

white标准化后的值为 rgb(255, 255, 255)

2.3 计算DOM树中每个节点的样式

完成样式的属性值标准化后,就需要计算每个节点的样式属性,这个阶段CSS有两个规则我们需要清楚:

  • 继承规则:每个DOM节点都包含有父节点的样式
  • 层叠规则:层叠是CSS的一个基本特征,是一个定义了如何合并来自多个源的属性值的算法。

样式计算阶段是为了计算出DOM节点中每个元素的具体样式,在计算过程中需要遵守CSS的继承和层叠两个规则。

该阶段最终输出的内容是每个DOM节点的样式,并被保存在 ComputedStyle 的结构中。

3. 布局阶段

经过上面的两个步骤,我们已经拿到了DOM树和DOM树中元素的样式,接下来需要计算DOM树中可见元素的几何位置,这个计算过程就是布局。

3.1 创建布局树

在DOM树中包含了一些不可见的元素,例如 head 标签,设置了 display:none 属性的元素,所以我们需要额外构建一棵只包含可见元素的布局树。

构建过程:从DOM树的根节点开始遍历,将所有可见的节点加到布局树中,忽略不可见的节点。

3.2 布局计算

到这里我们就有了一棵构建好的布局树,就可以开始计算布局树节点的坐标位置了。从根节点开始遍历,结合上面计算得到的样式,确定每个节点对象在页面上的具体大小和位置,将这些信息保存在布局树中。

布局阶段的输出是一个盒子模型,它会精确地捕获每个元素在屏幕内的确切位置与大小。

4. 分层

现在我们已经有了布局树,也知道了每个元素的具体位置信息,但是还不能开始绘制页面,因为页面中会有像3D变换、页面滚动、或者用 z-index 进行z轴排序等复杂效果,为了更方便实现这些效果,渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的图层树(LayerTree)。

在Chrome浏览器中,我们可以打开开发者工具,选择 Elements-Layers 标签,就可以看到页面的分层情况,如下图所示:

浏览器的页面实际上被分成了很多图层,这些图层叠加后合成了最终的页面。

到这里,我们构建了两棵树:布局树和图层树。下面我们来看下这两棵树之间的关系:

正常情况下,并不是布局树的每个节点都包含一个图层,如果一个节点没有对应的图层,那么这个节点就从属于父节点的图层。

那节点要满足什么条件才会被提升为一个单独的图层?只要满足下面其中一个条件即可:

  • 拥有层叠上下文属性的元素会被提升为单独的一个图层
  • 需要剪裁(clip)的地方也会被创建为图层。

5. 图层绘制

构建好图层树之后,渲染引擎就会对图层树中的每个图层进行绘制。

渲染引擎实现图层绘制,会把一个图层的绘制拆分成很多小的绘制指令,然后将这些指令按照顺序组成一个绘制列表。

6. 栅格化(raster)操作

绘制一个图层时会生成一个绘制列表,这只是用来记录绘制顺序和绘制指令的列表,实际上绘制操作是由渲染引擎中的合成线程来完成的。

通过下图来看下渲染主线程和合成线程之间的关系:

当图层的绘制列表准备好后,主线程会把该绘制列表提交给合成线程,合成线程开始工作。

首先合成线程会将图层划分为图块(tile),图块大小通常是 256256 或者 512512。

然后合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的。所谓栅格化,是指将图块转换为位图。而图块是栅格化执行的最小单位。渲染进程维护了一个栅格化的线程池,所有的图块栅格化都是在线程池内执行的,运行方式如下图所示:

7. 合成和显示

一旦所有图块都被光栅化,合成线程就会生成一个绘制图块的命令——“DrawQuad”,然后将该命令提交给浏览器进程。

浏览器进程里面有一个名字叫做 viz 的组件,用来接收合成线程发过来的 DrawQuad 命令,然后根据命令执行。 DrawQuad 命令,将其页面内容绘制到内存中,最后再将内存显示在屏幕上。

多年开发老码农福利赠送:网页制作,网站开发,web前端开发,从最零基础开始的的HTML+CSS+JavaScript。jQuery,Vue、React、Ajax,node,angular框架等到移动端小程序项目实战【视频+工具+电子书+系统路线图】都有整理,需要的伙伴可以私信我,发送“前端”等3秒后就可以获取领取地址,送给每一位对编程感兴趣的小伙伴

8. 总结

一个完整的渲染流程可以总结如下:

  • 1、渲染进程将HTML内容转换为浏览器能够读懂的DOM树结构。
  • 2、渲染引擎将CSS样式表转化为浏览器可以理解的styleSheets,计算出DOM节点的样式。
  • 3、创建布局树,并计算所需元素的布局信息。
  • 4、对布局树进行分层,并生成分层树。
  • 5、为每个图层生成绘制列表,并将其提交到合成线程。
  • 6、合成线程将图层分图块,并栅格化将图块转换成位图。
  • 7、合成线程发送绘制图块命令给浏览器进程。浏览器进程根据指令生成页面,并显示到显示器上。

渲染过程中还有两个我们经常听到的概念:重排和重绘。在这篇文章中就不细说了,下一篇文章再详细介绍。

象一下,成千上万的人聚集在一个空间里,我们被要求了解更多关于它们的信息,或者收集关于这个群体或一群人的信息。

首先,我认为最好的办法就是将他们分组,并从他们中创建一种与某种事物相关的新型群体。关系取决于我们如何确定将它们分组。例如,我们可以根据家庭,年龄,文化背景,教育背景,性别等将他们分组

这些用于分组的参数是我们所知道的机器学习中的特征。将人或物体分组的方法没有被告知要考虑的参数/特征被称为聚类,并且该方法属于一类机器学习,称为Un-监督学习。

分层聚类通过不断合并两个最相似的组来建立分组的层次结构。每个组从一个项目开始,并且通过计算每个组之间的距离来增加每个组的迭代次数,并且最接近的一个被合并一起形成新的群体,这一直重复,直到他们只有一个群体。

现在我们如何确定亲密度; 我们可以使用计算一组实体之间的相似度的算法来确定接近度。可以使用欧几里得或皮尔逊相关。

但是,大多数情况下,我们将处理的数据比其他数据多,在这种类型的数据中,欧几里德距离将无法正常工作。因此我们使用Pearson相关性,因为它会通过真正试图确定两组数据如何适合一条直线来纠正。

function pearson(v1,v2){

//calculate the pearson correlation

var sum1 = sum(v1);

var sum2 = sum(v2);

var sum1sq,sum2sq,dat1=[],dat2=[];

for(var v in v1){

dat1.push(Math.pow(v1[v],2));//the square if each datapoint in v1

}

sum1sq = sum(dat1);//the sum of data1

for(var v in v2){

dat2.push(Math.pow(v2[v],2));

}

sum2sq = sum(dat2);

//console.log(sum1sq,sum2sq);

var psum,pdata=[];

for(var i in v1){

pdata.push(v1[i]*v2[i]);//calc the product of v1 and v2

}

psum = sum(pdata);

var num = psum - (sum1*sum2/v1.length);

var den = Math.sqrt((sum1sq-Math.pow(sum1,2)/v1.length)*(sum2sq-Math.pow(sum2,2)/v1.length));

if(den==0) return 0;

return 1.0-num/den;//normalize the result between 0 and 1

}

当两个项目完美匹配时,Pearson将始终返回1.0,而在完全没有关系时接近0.0。

在分层聚类中,每个聚类可以是具有两个分支的树中的一个点,也可以是端点。并且该群集还包含有关其位置的数据,该数据可以是端点的行数据,也可以是其他节点类型的两个分支的合并数据。然后我们创建一个包含构建分层树所需的所有这些属性的类。

function bicluster(opt){

//to store the state of data

this.left = opt.left || null;

this.right = opt.right || null;

this.vec = opt.vec;

this.id = opt.id || 0;

this.distance = opt.distance || 0.0;

}

我们通过创建一组/仅仅是原始项目的集群来启动算法。主循环(while循环)通过尝试每个可能的对并计算它们的相关性来搜索两个最佳匹配。

最好的一对群集合成一个群集。新群集的数据是两个群集数据的平均值,并且重复该过程直到只剩下一个群集。由于时间消耗,存储相关结果是很好的,因为它们将始终被重新计算。

合并群集函数

function mergevecs(a,b){//merge two array

var mergdata = [];

for(var i=0;i<a.length;i++){

mergdata.push((a[i] + b[i])/2.0);

}

return mergdata;

}

主函数

function hcluster(rows,distance){

//row=>data,distance =>pearson

var distances = {};

var currentclustid = -1;

var clust = [];

for(var i=0;i<rows.length;i++){

clust.push(new bicluster({vec:rows[i],id:i}));//propagate an array with an object

}

//console.log(distance(clust[1].vec,clust[1].vec));

var store=[];

while(clust.length > 1){//loop until the lengt of the cluster array is greater than 1

let lowestpair= [0,1];//the lowest pair has index 0 and 1 if the array(i.e closest dist)

var closest =distance(clust[0].vec,clust[1].vec);

//console.lo(clust);

for(var i=0;i<clust.length;i++){

for(var j=i+1; j<clust.length; j++ ){

var y = clust[i].id+","+clust[j].id; //store the id has string for object property

if(!( y in distances)){//store the distance

distances[clust[i].id+","+clust[j].id]= distance(clust[i].vec,clust[j].vec);

}

var d = distances[clust[i].id+","+clust[j].id]

if(d < closest){//choose the lowest distance and store the index

closest = d;

lowestpair[0] =i;

lowestpair[1] =j;

}

}

}

var mergevec = mergevecs(clust[lowestpair[0]].vec,clust[lowestpair[1]].vec);

var newcluster = new bicluster({vec:mergevec,left:clust[lowestpair[0]],

right:clust[lowestpair[1]],

distance:closest,id:currentclustid});

currentclustid -=1;//decrease the cluster id

//store.push("("+lowestpair[1]+","+lowestpair[0]+")")

clust.splice(lowestpair[1],1);

clust.splice(lowestpair[0],1);

clust.push(newcluster);

//lend--;

}

return clust[0];

}

分层聚类的可视化图被称为展示层次结构中dendogram which di排列的节点。检查页面开头的图片。我们不会走得太远。但是我们找到了以格式良好的方式打印出簇的方法。

function v(n){

//function to space the output properly

var space =[];

for(var i =0;i< n;i++){

space.push(' ');

}

return space;

}

function printclust(clust,labels,n){

var space = v(n).join('')

if(clust.id < 0){//indicate a group(parent)

console.log(space+'-');

}

else{

console.log(space+labels[clust.id]);// the child

}

if(clust['left'] !=null){

printclust(clust['left'],labels,n+1);

}

if(clust['right'] !=null){

printclust(clust['right'],labels,n+1);

}

}

然后让我们试试这个算法:

var data = [[1.0,8.0],[3.0,8.0],[2.0,7.0],[1.5,1.0],[4.0,2.0]];

var labels=['A','B','C','D','E'];

//euclidean distance is best for this

//since the data have the same number of entry

var euclid= function(v1,v2){

var sum=0;

for(var i=0;i<v1.length;i++){

sum+= Math.pow(v1[i]-v2[i],2);

}

return sum;

};

var clust = hcluster(data,euclid);

printclust(clust,labels);

//output

-

-

B

-

A

C

-

D

E

输出显示B,A,C属于同一个根节点,而D,E属于另一个根节点。

让我们试试真正的数据集上的算法。它是一个包含数据矩阵的文件,关于不同的博客和与之关联的单词(特征)。博客名称被表示为行并且文字具有列。该矩阵基于每个博客文章中每个词的出现

我们创建一个从该文件读取数据的函数,并返回一个包含博客名称的对象,一个与每个博客关联的浮动词的数组以及这些词(特征)的列表。这个文件可以在这里下载(https://github.com/steveoni/Hierarchical_clustering_js/blob/master/bb.txt)。

/**

*@param filename

*@return an object:name of the rows,coulumn and the data

**/

function readfile(filename){

//read file synchronously and split by newline to an array

var lines = fs.readFileSync(filename).toString().split("\n");

var colnames = lines[0].trim().split('\t');//get the first index which is the column(word in each blog)

var p,l=[];

var rownames= [];//store each rownames

var data =[];//store the matrix data

lines.shift();// delete the column from the data.

for(var line in lines){

p= lines[line].trim().split('\t');//convert the string to array base on the tab

rownames.push(p[0]);//index 0 is the name of the row(the title of the blog)

p.shift();//remove the title

data.push(p);//store the values

}

var data2=new Array(data.length);

for(var i=0;i<data2.length;i++){

data2[i] = zeros(data[0].length);//fill the array with zeros

}

for(var i =0;i<data.length;i++){

var i_dat = data[i];

for(var j=0;j<i_dat.length;j++){

data2[i][j] += parseFloat(data[i][j]);//store the array with the float of the actual digit

}

}

return {rownames:rownames,

colnames:colnames,

data:data2};

}

然后让它运行`hcluster`:

var file = readfile('bb.txt')

var clutster = hcluster(file.data,pearson);

printclust(cluster,file.rownames,0);