浏览器工作原理之从URL到页面显示

前言

这篇博客,我使用node.js环境在代码逻辑层面描述浏览器从接收一个URL到渲染出一个网页这里面究竟都发生了什么。
我们知道在浏览器中把一个URL渲染出一个网页可以简化为五个步骤:

  1. 从URL到HTML
  2. 从HTML到DOM树
  3. 从DOM树到CSSOM树
  4. 从CSSOM树到布局树
  5. 从布局树到渲染显示

从URL到HTML

这一步我们是建立在HTTP请求的基础上,即服务器发送一个HTTP的URL请求,这个请求里面包含HTML文本信息,然后客户端通过接收到的数据进行解析,最终得到服务器所发送的HTML文本。
server.js:

const http = require('http');

http.createServer((request, response) => {
    let body = [];
    request.on('error', (err) => {
        console.error(err);
    }).on('data', (chunk) => {
        console.log(chunk);
        body.push(chunk);
    }).on('end', () => {
        body = Buffer.concat(body).toString();
        console.log('body:', body);
        response.writeHead(200, { 'Content-type': 'text/html' });
        response.end(
`<html maaa=a >
<head class="my-head">
    <style>
#container {
    width:500px;
    height:300px;
    display:flex;
    flex-wrap: wrap;
    justify-content: space-around;
    align-items: center;
    background-color:rgb(255,255,255);
}
#container #my-id {
    width:200px;
    height:100px;
    background-color:rgb(255,0,0);
}
#container .c1 {
    width: 400px;
    height: 100px;
    background-color:rgb(0,255,0);
}
    </style>
</head>
<body>
    <div id="container">
       <div id="my-id"></div>
       <div class="c1" ></div>
    </div>
</body>
</html>`
        );
    });
}).listen(8088);

client.js:

async function main() {
    const request = new Request({
        method: "POST",
        host: "127.0.0.1",
        port: "8088",
        path: "/",
        headers: {
            ["X-Foo2"]: "customed"
        },
        body: {
            name: "rookie"
        }
    });
    const response = await request.send();
    console.log(response);
}

request.send()里面包含了对HTTP协议的解析算法,这个算法的基本思路是通过有限状态机进行处理。最终,response.body即是服务器所发送的HTML文本内容。

从HTML到DOM树

这一步我们需要对response.body进行解析,返回一个DOM树。
由于在HTML标准中以及规定了HTML的状态,所以解析HTML文本的思路也是通过有限状态机进行实现。官方链接在解析的过程中,我们通过在不同的状态机中执行不同的逻辑,把每一次需要保存的数据(token)都返回到一个回调函数中,token不仅包括标签的类型也包含该标签所具有的属性和文本内容。最后,全局定义一个栈,将此回调函数中的所有有效标签合成一棵DOM树。
parser.js

let stack = [{
    type: 'document',
    children: []
}];
function emit(token) {
    let top = stack[stack.length - 1];
    if (token.type == 'startTag') {
        let element = {
            type: 'element',
            children: [],
            attributes: []
        };
        element.tagName = token.tagName;
        for (let p in token) {
            if (p != 'type' && p != 'tagName') {
                element.attributes.push({
                    name: p,
                    value: token[p]
                });
            }
        }

        top.children.push(element);
        element.parent = top;

        if (!token.isSelfClosing) {
            stack.push(element);
        }

        currentTextNode = null;
    } else if (token.type == 'endTag') {
        if (top.tagName != token.tagName) {
            throw new Error("Tag start end doesn't match!");
        } else {
            stack.pop();
        }
        currentTextNode = null;
    } else if (token.type === 'text') {
        if (currentTextNode == null) {
            currentTextNode = {
                type: 'text',
                content: ''
            }
            top.children.push(currentTextNode);
        }
        currentTextNode.content += token.content;
    }
}
...

module.exports.parserHTML = function parserHTML(html) {
    let state = data;
    for (let c of html) {
        state = state(c);
    }
    state = state(EOF);
    return stack[0];
}

最终,得到的stack[0]就是我们所需要的DOM树。

从DOM树到CSSOM树

这一步分为两小步,第一步收集style标签中的CSS规则,第二步将CSS规则添加到DOM树中形成CSSOM树。这两步都在emit函数中进行,收集CSS的时机为在endTag中为遇到style标签时,而应用CSS规则的时机在startTag为DOM元素时。
更新后的emit函数:

function emit(token) {
    let top = stack[stack.length - 1];
    if (token.type == 'startTag') {
        let element = {
            type: 'element',
            children: [],
            attributes: []
        };
        element.tagName = token.tagName;
        for (let p in token) {
            if (p != 'type' && p != 'tagName') {
                element.attributes.push({
                    name: p,
                    value: token[p]
                });
            }
        }

        computeCSS(element);
        top.children.push(element);
        element.parent = top;

        if (!token.isSelfClosing) {
            stack.push(element);
        }

        currentTextNode = null;
    } else if (token.type == 'endTag') {
        if (top.tagName != token.tagName) {
            throw new Error("Tag start end doesn't match!");
        } else {
            if (top.tagName === 'style') {
                addCSSRules(top.children[0].content);
            }
            stack.pop();
        }
        currentTextNode = null;
    } else if (token.type === 'text') {
        if (currentTextNode == null) {
            currentTextNode = {
                type: 'text',
                content: ''
            }
            top.children.push(currentTextNode);
        }
        currentTextNode.content += token.content;
    }
}

我们通过npm的css包来处理原始的css规则,然后通过一个全局的rules进行保存,最后在computeCSS中使用并添加到DOM树中。(computedStyle)

let rules = [];
function addCSSRules(text) {
    const ast = css.parse(text);
    console.log(JSON.stringify(ast, null, " "));
    rules.push(...ast.stylesheet.rules);
}
function computeCSS(element) {
    const elements = stack.slice().reverse();// div dov #myid=>#myid div div
    if (!element.computedStyle) {
        element.computedStyle = {};
    }
    for (let rule of rules) {
        const selectorPars = rule.selectors[0].split(" ").reverse();
        if (!matchSelector(element, selectorPars[0])) {
            continue;
        }
        let matched = false;
        let j = 1;
        for (let i = 0; i < elements.length; i++) {
            if (matchSelector(elements[i], selectorPars[j])) {
                j++;
            }
            if (j >= selectorPars.length) {
                matched = true;
                break;
            }
        }
        if (matched) {
            const sp = specificity(rule.selectors[0]);
            const computedStyle = element.computedStyle;
            for (let declaration of rule.declarations) {
                if (!computedStyle[declaration.property]) {
                    computedStyle[declaration.property] = {};
                }
                if (!computedStyle[declaration.property].specificity) {
                    computedStyle[declaration.property].specificity = sp;
                    computedStyle[declaration.property].value = declaration.value;
                } else if (!compare(computedStyle[declaration.property].specificity, sp)) {
                    computedStyle[declaration.property].specificity = sp;
                    computedStyle[declaration.property].value = declaration.value;
                }
            }
            console.log(element.computedStyle);
        }
    }
}

到此我们得到的stack[0]就是一棵CSSOM树,其中DOM元素的computedStyle属性里保存了其CSS样式。

从CSSOM树到布局树

首先,布局的时机是在元素标签的endTag状态,将该元素的CSSOM传进layout函数。然后我们需要知道llayout函数里面究竟做了什么,这里,我已flex布局为例子进行描述。
在flex布局里,预想知道每个元素的位置,我们只需要知道每个元素的width,height,left/right,top/bottom即可。而layout函数里面就是就是确定每个DOM元素的这几个重要指标。

layout.js:

function getStyle(element) {
    if (!element.style) {
        element.style = {};
    }
    for (let prop in element.computedStyle) {
        element.style[prop] = element.computedStyle[prop].value;
        if (element.style[prop].toString().match(/px$/)) {
            element.style[prop] = parseInt(element.style[prop]);
        }
        if (element.style[prop].toString().match(/^[0-9\.]+$/)) {
            element.style[prop] = parseInt(element.style[prop]);
        }
    }
    return element.style;
}

function layout(element) {
    if (!element.computedStyle) {
        return;
    }
    const elementStyle = getStyle(element);
    if (elementStyle.display !== 'flex') {
        return;
    }
    const items = element.children.filter(e => e.type === 'element');
    items.sort((a, b) => (a.order || 0) - (b.order || 0));

    const style = elementStyle;
    ['width', 'height'].forEach(size => {
        if (style[size] === 'auto' || style[size] === '') {
            style[size] = null;
        }
    });

    if (!style["flex-direction"] || style["flex-direction"] === 'auto') {
        style["flex-direction"] = 'row';
    }
    if (!style["align-items"] || style["align-items"] === 'auto') {
        style["align-items"] = 'stretch';
    }
    if (!style["justify-content"] || style["justify-content"] === 'auto') {
        style["justify-content"] = 'flex-start';
    }
    if (!style["flex-wrap"] || style["flex-wrap"] === 'auto') {
        style["flex-wrap"] = 'nowrap'
    }
    if (!style["align-content"]|| style["align-content"]=== 'auto') {
        style["align-content"]= 'stretch';
    }

    let mainSize; //主轴元素大小
    let mainStart; //主轴元素起点
    let mainEnd; //主轴元素终点
    let mainSign; // 排布方向
    let mainBase; // 记录那些已经显示设定大小的元素值
    let crossSize;
    let crossStart;
    let crossEnd;
    let crossSign;
    let crossBase;
    if (style["flex-direction"] === 'row') {
        mainSize = 'width';
        mainStart = 'left';
        mainEnd = 'right';
        mainSign = +1;
        mainBase = 0;

        crossSize = 'height';
        crossStart = 'top';
        crossEnd = 'bottom';
    }
    if (style["flex-direction"] === 'row-reverse') {
        mainSize = 'width';
        mainStart = 'right';
        mainEnd = 'left';
        mainSign = -1;
        mainBase = style.width;

        crossSize = 'height';
        crossStart = 'top';
        crossEnd = 'bottom';
    }
    if (style["flex-direction"] === 'column') {
        mainSize = 'height';
        mainStart = 'top';
        mainEnd = 'bottom';
        mainSign = +1;
        mainBase = 0;

        crossSize = 'width';
        crossStart = 'left';
        crossEnd = 'right';
    }
    if (style["flex-direction"] === 'column-reverse') {
        mainSize = 'height';
        mainStart = 'bottom';
        mainEnd = 'top';
        mainSign = -1;
        mainBase = style.height;

        crossSize = 'width';
        crossStart = 'left';
        crossEnd = 'right';
    }
    // 只受交叉轴的影响
    if (style["flex-wrap"] === 'wrap-reverse') {
        const tmp = crossStart;
        crossStart = crossEnd;
        crossEnd = tmp;
        crossSize = -1;
    } else {
        crossBase = 0;
        crossSign = 1;
    }

    let isAutoMainSize = false;
    if (!style[mainSize]) { // auto sizing
        elementStyle[mainSize] = 0;
        items.forEach(item => {
            const itemStyle = getStyle(item);
            if (itemStyle[mainSize] !== null || itemStyle[mainSize] !== (void 0)) {
                elementStyle[mainSize] = elementStyle[mainSize] + itemStyle[mainSize];
            }
        })
        isAutoMainSize = true;
    }

    let flexLine = []; // 一行
    const flexLines = [flexLine];
    let mainSpace = elementStyle[mainSize];//剩余空间
    let crossSpace = 0;// 交叉轴的高度(行高)

    items.forEach(item => {
        const itemStyle = getStyle(item);
        if (itemStyle[mainSize] === null) {
            itemStyle[mainSize] = 0;
        }

        if (itemStyle.flex) {// flex属性 如flex:1
            flexLine.push(item);
        } else if (style["flex-wrap"] === 'nowrap' && isAutoMainSize) {
            mainSpace -= itemStyle[mainSize];
            if (itemStyle[crossSize] !== null && itemStyle[crossSize] !== (void 0)) {
                crossSpace = Math.max(crossSpace, itemStyle[crossSize]);
            }
            flexLine.push(item);
        } else {
            if (itemStyle[mainSize] > style[mainSize]) { // 子元素比父元素大
                itemStyle[mainSign] = style[mainSize];
            }
            if (mainSpace < itemStyle[mainSize]) {
                flexLine.mainSpace = mainSpace;
                flexLine.crossSpace = crossSpace;

                flexLine = [];
                flexLine.push(item);
                flexLines.push(flexLine);
                mainSpace = style[mainSize];
                crossSpace = 0;
            } else {
                flexLine.push(item);
            }
            if (itemStyle[crossSize] !== null && itemStyle[crossSize] !== (void 0)) {
                crossSpace = Math.max(crossSpace, itemStyle[crossSize]);
            }
            mainSpace -= itemStyle[mainSize];
        }
    });
    flexLine.mainSpace = mainSpace; // 最后一行的剩余空间

    if (style["flex-wrap"] === 'nowrap' || isAutoMainSize) {
        flexLine.crossSpace = (style[crossSize] !== (void 0)) ? style[crossSize] : crossSpace;
    } else {
        flexLine.crossSpace = crossSpace;
    }

    if (mainSpace < 0) {
        // 等比压缩 (只可能是单行即最后一行)
        const scale = style[mainSize] / (style[mainSize] - mainSpace);
        let currentMain = mainBase;
        items.forEach(item => {
            const itemStyle = getStyle(item);
            if (itemStyle.flex) {
                itemStyle[mainSize] = 0;
            }

            itemStyle[mainSize] = itemStyle[mainSize] * scale;
            itemStyle[mainStart] = currentMain;
            itemStyle[mainEnd] = itemStyle[mainStart] + mainSign * itemStyle[mainSize];
            currentMain = itemStyle[mainEnd];
        });
    } else {
        // 多行
        flexLines.forEach(items => {
            let mainSpace = items.mainSpace;
            let flexTotal = 0;
            items.forEach(item => {
                const itemStyle = getStyle(item);
                if ((itemStyle.flex !== null) && (itemStyle.flex !== (void 0))) {
                    flexTotal += itemStyle.flex;
                }
            });

            if (flexTotal > 0) {
                // 均匀分配
                let currentMain = mainBase;
                items.forEach(item => {
                    const itemStyle = getStyle(item);
                    if (itemStyle.flex) {
                        itemStyle[mainSize] = (mainSpace / flexTotal) * itemStyle.flex;
                    }
                    itemStyle[mainStart] = currentMain;
                    itemStyle[mainEnd] = itemStyle[mainStart] + mainSign * itemStyle[mainSize];
                    currentMain = itemStyle[mainEnd];
                });
            } else {
                // 根据justifyContent进行分配
                let currentMain;
                let step; //间隔
                if (style["justify-content"] === 'flex-start') {
                    currentMain = mainBase;
                    step = 0;
                }
                if (style["justify-content"] === 'flex-end') {
                    currentMain = mainSpace * mainSign + mainBase;
                    step = 0;
                }
                if (style["justify-content"] === 'center') {
                    currentMain = mainSpace / 2 * mainSign + mainBase;
                    step = 0;
                }
                if (style["justify-content"] === 'space-between') {
                    currentMain = mainBase;
                    step = mainSpace / (items.length - 1) * mainSign;
                }
                if (style["justify-content"] === 'space-around') {
                    step = mainSpace / items.length * mainSign;
                    currentMain = step / 2 + mainBase;
                }
                items.forEach(item => {
                    const itemStyle = getStyle(item);
                    itemStyle[mainStart] = currentMain;
                    itemStyle[mainEnd] = itemStyle[mainStart] + mainSign * itemStyle[mainSize];
                    currentMain = itemStyle[mainEnd] + step;
                })
            }
        })
    }

    //计算交叉轴尺寸
    if (!style[crossSize]) { // 自动填充
        crossSpace = 0;
        elementStyle[crossSize] = 0;
        flexLines.forEach(flexLine => {
            elementStyle[crossSize] = elementStyle[crossSize] + flexLine.crossSpace;
        });
    } else {
        crossSpace = style[crossSize];
        flexLines.forEach(flexLine => {
            crossSpace -= flexLine.crossSpace;
        });
    }

    // 分配 crossSpace
    if (style["flex-wrap"] === 'wrap-reverse') {
        crossBase = style[crossSize];
    } else {
        crossBase = 0;
    }

    let step;
    if (style["align-content"]=== 'flex-start') {
        crossBase += 0;
        step = 0;
    }
    if (style["align-content"]=== 'flex-end') {
        crossBase += crossSign * crossSpace;
        step = 0;
    }
    if (style["align-content"]=== 'center') {
        crossBase += crossSign * crossSpace / 2;
        step = 0;
    }
    if (style["align-content"]=== 'space-between') {
        crossBase += 0;
        step = crossSpace / (flexLines.length - 1);
    }
    if (style["align-content"]=== 'space-around') {
        crossBase += crossSign * step / 2;
        step = crossSpace / (flexLines.length);
    }
    if (style["align-content"]=== 'stretch') {
        crossBase += 0;
        step = 0;
    }
    flexLines.forEach(items => {
        const lineCrossSize = style["align-content"]=== 'stretch'
            ? items.crossSpace + crossSpace / flexLines.length
            : items.crossSpace;
        items.forEach(item => {
            const itemStyle = getStyle(item);
            const align = itemStyle["align-self"] || style["align-items"];
            if (itemStyle[crossSize] === null) {
                itemStyle[crossSize] = (align === 'stretch') ? lineCrossSize : 0;
            }
            if (align === 'flex-start') {
                itemStyle[crossStart] = crossBase;
                itemStyle[crossEnd] = itemStyle[crossStart] + crossSign * itemStyle[crossSize];
            }
            if (align === 'flex-end') {
                itemStyle[crossEnd] = crossBase + crossSign * lineCrossSize;
                itemStyle[crossStart] = itemStyle[crossEnd] - crossSign * itemStyle[crossSize];
            }
            if (align === 'center') {
                itemStyle[crossStart] = crossBase + crossSign * (lineCrossSize - itemStyle[crossSize]) / 2;
                itemStyle[crossEnd] = itemStyle[crossStart] + crossSign * itemStyle[crossSize];
            }
            if (align === 'stretch') {
                itemStyle[crossStart] = crossBase;
                itemStyle[crossEnd] = crossBase + crossSign * ((itemStyle[crossSize] !== null && itemStyle[crossSize] !== (void 0)) ? itemStyle[crossSize] : lineCrossSize);
                itemStyle[crossSize] = crossSign * (itemStyle[crossEnd] - itemStyle[crossStart]);
            }
        });
        crossBase += crossSign*(lineCrossSize+step);
    })
}

module.exports = layout;

到此,client.js里面解析得到的就是一棵布局树。

从布局树到渲染显示

这一步的逻辑比较简单直接,我们通过使用各种外界的js库将已完成的布局树绘制出来,这里我只使用npm的images库。
最新的main函数:

async function main() {
    const request = new Request({
        method: "POST",
        host: "127.0.0.1",
        port: "8088",
        path: "/",
        headers: {
            ["X-Foo2"]: "customed"
        },
        body: {
            name: "rookie"
        }
    });
    const response = await request.send();
    console.log(response);
    const dom = parser.parserHTML(response.body);
    // console.log(JSON.stringify(dom, null, " "));
    const viewport = images(800, 600);
    render(viewport, dom);
    viewport.save('viewport.jpg');
}

render.js:

const images = require('images');

function render(viewport, element) {
    if (element.style) {
        const img = images(element.style.width, element.style.height);
        if (element.style['background-color']) {
            const color = element.style['background-color'] || 'rgb(0,0,0)';
            color.match(/rgb\((\d+),(\d+),(\d+)\)/);
            img.fill(Number(RegExp.$1), Number(RegExp.$2), Number(RegExp.$3));
            viewport.draw(img, element.style.left || 0, element.style.top || 0);
        }
    }
    if (element.children) {
        for (let child of element.children) { 
            render(viewport, child);
        }
    }
}

module.exports = render;

最后将代码运行起来,我们可以得到一张viewport.jpg的图片:
在这里插入图片描述
同时,我们可以在浏览器打开此项目服务器的端口,得到如下界面:(将背景色调黑)
在这里插入图片描述
最终我们得到了一个和浏览器解析一模一样的结果!

结语

到此,我们手动实现了一个简易的浏览器的解析渲染过程,但是实际中的浏览器原理比这里所描述的要复杂得多,后续我应该还会继续学习并更新此代码(完整代码地址),有兴趣的朋友也可以尝试着自己手动玩玩。

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值