Skip to content

三.模拟请求->渲染流程

请求报文格式

  • 起始行:[方法][空格][请求 URL][HTTP 版本][换行符]
  • 首部: [首部名称][:][空格][首部内容][换行符]
  • 首部结束:[换行符]
  • 实体

响应报文格式

  • 起始行:[HTTP 版本][空格][状态码][空格][原因短语][换行符]
  • 首部:[首部名称][:][空格][首部内容][换行符]
  • 首部结束: [换行符]
  • 实体

browser

browser

1.基于 TCP 发送 HTTP 请求

js
const net = require("net");
class HTTPRequest {
  constructor(options) {
    this.method = options.method || "GET";
    this.host = options.host || "127.0.0.1";
    this.port = options.port || 80;
    this.path = options.path || "/";
    this.headers = options.headers || {};
  }
  send(body) {
    return new Promise((resolve, reject) => {
      body = Object.keys(body)
        .map((key) => `${key}=${encodeURIComponent(body[key])}`)
        .join("&");
      if (body) {
        this.headers["Content-Length"] = body.length;
      }

      const socket = net.createConnection(
        {
          host: this.host,
          port: this.port,
        },
        () => {
          const rows = [];
          rows.push(`${this.method} ${this.path} HTTP/1.1`);
          Object.keys(this.headers).forEach((key) => {
            rows.push(`${key}: ${this.headers[key]}`);
          });
          let request = rows.join("\r\n") + "\r\n\r\n" + body;
          socket.write(request);
        }
      );

      socket.on("data", function (data) {
        // data 为发送请求后返回的结果
      });
    });
  }
}
async function request() {
  const request = new HTTPRequest({
    method: "POST",
    host: "127.0.0.1",
    port: 3000,
    path: "/",
    headers: {
      name: "zhufeng",
      age: 11,
    },
  });
  let { responseLine, headers, body } = await request.send({ address: "北京" });
}

request();

2.解析响应结果

js
const parser = new HTTPParser();
socket.on("data", function (data) {
  // data 为发送请求后返回的结果
  parser.parse(data);
  if (parser.result) {
    resolve(parser.result);
  }
});

3.解析 HTML

js
let stack = [{ type: "document", children: [] }];
const parser = new htmlparser2.Parser({
  onopentag(name, attributes) {
    let parent = stack[stack.length - 1];
    let element = {
      tagName: name,
      type: "element",
      children: [],
      attributes,
      parent,
    };
    parent.children.push(element);
    element.parent = parent;
    stack.push(element);
  },
  ontext(text) {
    let parent = stack[stack.length - 1];
    let textNode = {
      type: "text",
      text,
    };
    parent.children.push(textNode);
  },
  onclosetag(tagname) {
    stack.pop();
  },
});
parser.end(body);

4.解析CSS

js
const cssRules = [];
const css = require("css");
function parserCss(text) {
  const ast = css.parse(text);
  cssRules.push(...ast.stylesheet.rules);
}
js
const parser = new htmlparser2.Parser({
  onclosetag(tagname) {
    let parent = stack[stack.length - 1];
    if (tagname == "style") {
      parserCss(parent.children[0].text);
    }
    stack.pop();
  },
});

5.计算样式

js
function computedCss(element) {
  let attrs = element.attributes; // 获取元素属性
  element.computedStyle = {}; // 计算样式
  Object.entries(attrs).forEach(([key, value]) => {
    cssRules.forEach((rule) => {
      let selector = rule.selectors[0];
      if (
        (selector == "#" + value && key == "id") ||
        (selector == "." + value && key == "class")
      ) {
        rule.declarations.forEach(({ property, value }) => {
          element.computedStyle[property] = value;
        });
      }
    });
  });
}

6.布局绘制

js
function layout(element) {
  // 计算位置 -> 绘制
  if (Object.keys(element.computedStyle).length != 0) {
    let { background, width, height, top, left } = element.computedStyle;
    let code = `
            let canvas = document.getElementById('canvas');
            canvas.width = window.innerWidth;
            canvas.height = window.innerHeight ;
            let context = canvas.getContext("2d")
            context.fillStyle = "${background}";
            context.fillRect(${top}, ${left}, ${parseInt(width)}, ${parseInt(
      height
    )});
            `;
    fs.writeFileSync("./code.js", code);
  }
}

7.总结:DOM 如何生成的

  • 当服务端返回的类型是text/html时,浏览器会将收到的数据通过HTMLParser进行解析 (边下载边解析)
  • 在解析前会执行预解析操作,会预先加载JSCSS等文件
  • 字节流 -> 分词器 -> Tokens -> 根据 token 生成节点 -> 插入到 DOM 树中
  • 遇到js:在解析过程中遇到script标签,HTMLParser会停止解析,(下载)执行对应的脚本。
  • js执行前,需要等待当前脚本之上的所有CSS加载解析完毕(js是依赖css的加载)

browser

  • CSS样式文件尽量放在页面头部,CSS加载不会阻塞 DOM tree 解析,浏览器会用解析出的DOM TREECSSOM 进行渲染,不会出现闪烁问题。如果CSS放在底部,浏览是边解析边渲染,渲染出的结果不包含样式,后续会发生重绘操作。
  • JS文件放在 HTML 底部,防止JS的加载、解析、执行堵塞页面后续的正常渲染

通过PerformanceAPI 监控渲染流程

Released under the MIT License.