首页 > 编程语言 > 详细

firekaka-用java实现一个简易浏览器

时间:2021-04-15 23:48:01      阅读:36      评论:0      收藏:0      [点我收藏+]

浏览器的工作过程

从文本字符串到形形色色的网页,浏览器大概要做以下工作

  1. 构建DOM树

    解析文本字符串为DOM

  2. 解析CSS样式

    将CSS文本字符串解析为样式树

  3. 根据DOM树和CSS样式树,计算每个节点的样式

  4. 构建布局树

    想要渲染一个完整的页面,除了获知每个节点的具体样式,还需要获知每一个节点在页面上的位置,布局其实是找到所有元素的几何关系的过程。

  5. 绘制

具体步骤

准备工作

由于需要解析的文本内容有HTML和CSS2种,所以,需要将处理字符串的方法抽取出来。

定义TextVisitor类,根据可能要用到的场景,定义判断是否解析到文件末尾,获取当前字符,下一个字符,上一个字符,消耗一个字符等方法

public class TextVisitor
{
    private String text; // 要解析的HTML文本
    private int pos; // 当前位置

    public TextVisitor(String text)
    {
        this.text=text;
        this.pos=0;
    }

    public void reset()
    {
        this.pos=0;
    }

    // 判断是否解析到了文本末尾
    public boolean eof()
    {
        return pos>=text.length();
    }

    // 获取当前位置的字符
    public char peekCur()
    {
        if(pos>=text.length())
        {
            return text.charAt(text.length()-1);
        }
        return text.charAt(pos);
    }

    // 获取下一个字符
    public char peekNext()
    {
        return text.charAt(pos+1);
    }

    // 获取前一个字符
    public char peekPre()
    {
        return text.charAt(pos-1);
    }

    // 获取从当前位置开始的n个字符
    public String peekMany(int n)
    {
        return text.substring(pos,n);
    }

    // 消耗多个符合给定正则规则的字符
    // 消耗1个指定字符字符
    public void consumeChar(char c)
    {
        char cur=peekCur();
        if(cur!=c)
        {
            String msg=String.format("字符不匹配: expect [%c], provide [%c]",peekCur(),c);
            throw new IllegalArgumentException(msg);
        }
        pos++;
    }
}

构建DOM树

解析HTML文本,构建出DOM树,先定义需要用到的数据结构

首先定枚举类,用来表示节点类型

public enum NodeType
{
    ELEMENT, // 元素节点
    ATTR,    // 属性节点
    TEXT,    // 文本节点
    COMMENT,  // 注释节点
}

然后,定义节点类及其子类

定义节点类的抽象类,其它类型从它继承

public abstract class Node
{
    private String name;
    private final NodeType nodeType;
    private ArrayList<Node> children;

    public Node(NodeType nodeType)
    {
        this.nodeType=nodeType;
    }

    public Node(String name,NodeType nodeType)
    {
        this(nodeType);
        this.name=name;
        this.children=new ArrayList<>();
    }
}

定义元素节点类

public class ElementNode extends Node
{
    private LinkedHashMap<String,ArrayList<String>> attrs;

    public ElementNode()
    {
        super(NodeType.ELEMENT);
    }

    public ElementNode(String name)
    {
        super(name,NodeType.ELEMENT);
        this.attrs=new LinkedHashMap<>();
    }

定义文本节点类

public class TextNode extends Node
{
    private String text;

    public TextNode()
    {
        super("text",NodeType.TEXT);
    }

}

接下来,就开始解析HTML,构建DOM树

定义parse()方法

public Node parse(String input)
{
    visitor=new TextVisitor(html);
    ArrayList<Node> nodes=parseNodes();
    Node root=nodes.get(0);
    return root;
}

这个方法是入口,实际进行解析的是接下来要讲的parseNodes()方法,该方法返回一个Node的列表,由于根节点只有一个,所以,返回第0个元素,就是根元素。

在parseNodes()方法中,只要没有解析到文件末尾,就一直进行解析;

根据当前字符是否为"<"号,决定解析的是元素节点,还是文本节点;

如果是元素节点,要根据下一个字符是否是"/",决定是开标签,还是闭标签;

代码如下

private ArrayList<Node> parseNodes()
{
    ArrayList<Node> nodes=new ArrayList<>();

    Node node=null;
    while(!visitor.eof())
    {
        char cur=visitor.peekCur();
        if(cur==‘<‘) // 元素节点
        {
            char next=visitor.peekNext();
            if(next==‘/‘) // 元素节点:闭标签
            {
                break;
            }
            node=parseElement(); // 解析元素节点
        }
        else // 文本节点
        {
            node=parseText(); // 解析文本节点
        }
        nodes.add(node);
    }

    return nodes;
}

解析元素节点时,分为以下步骤

  1. 解析开标签

  2. 解析元素属性

  3. 解析子节点

    解析子节点,只需要递归调用上面的parseNodes()方法即可。

  4. 解析闭标签

代码如下

private Node parseElement()
{
    visitor.consumeChar(‘<‘);

    // 解析开始标签
    String tagName = parseTagName();
    ElementNode element=new ElementNode(tagName);
    visitor.consumeWhitespace();

    // 解析属性
    parseAttrs(element);
    visitor.consumeChar(‘>‘);

    // 解析子节点
    ArrayList<Node> children=parseNodes();
    element.setChildren(children);

    // 解析闭标签
    visitor.consumeChar(‘<‘);
    visitor.consumeChar(‘/‘);
    parseTagName();
    visitor.consumeWhitespace();
    visitor.consumeChar(‘>‘);

    return element;
}

解析属性时,属性名和属性值用“=”分割,属性名只包含大小写字符,数字和-

private void parseAttrs(ElementNode element)
{
    char cur=visitor.peekCur();
    while(cur!=‘>‘)
    {
        // 属性名
        String key=visitor.consumeWhile("[a-zA-Z0-9\\-]");
        cur=visitor.peekCur();
        if(cur==‘=‘)
        {
            visitor.consumeChar(‘=‘);
            visitor.consumeChar(‘"‘);
            String[] values=visitor.consumeWhile("[^\"]").split("\\s+");
            visitor.consumeChar(‘"‘);
            Arrays.stream(values).forEach(value->element.setAttr(key,value));
        }
        visitor.consumeWhitespace();
    }
}

解析文本节点很简单,只要遇到"<"号,就认为文本解析完毕

private Node parseText()
{
    TextNode node=new TextNode();
    String text=visitor.consumeWhile("[^<]");
    text=text.replaceAll("\\s+"," ").trim();
    node.setText(text);
    return node;
}

当程序运行完毕,DOM树就构建完毕!

解析CSS样式

解析CSS与解析HTML类似,只是数据格式不同而已,这里只给出关键步骤

样式文件由一条条的规则Rule构成;

  • 一条规则包含选择器Selector部分和用话括号{}包含的声明部分;

  • 一条规则中可能有多个选择器和多条规则;

  • 简单起见,仅支持通用选择器,标签名选择器,类选择器和id选择器

  • 规则由用分号:分割的key和value构成

创建以下数据结构:

样式表StyleSheet

public class Stylesheet
{
    private ArrayList<Rule> rules;

    public Stylesheet()
    {
        this.rules=new ArrayList<>();
    }
}

规则Rule

public class Rule
{
    private ArrayList<Selector> selectors;
    private ArrayList<Declaration> declarations;

    public Rule()
    {
        this.selectors=new ArrayList<>();
        this.declarations=new ArrayList<>();
    }
}

选择器Selector

public abstract class Selector
{

}

public class SimpleSelector extends Selector
{
    private String tagName; // 标签名选择器
    private String id;  // id选择器
    private ArrayList<String> classes; // 类选择器,支持多个

    public SimpleSelector()
    {
        this.tagName=null;
        this.id=null;
        this.classes=new ArrayList<>();
    }
}

声明Declaration

public class Declaration
{
    private String name;
    private Value value;
}
···

其中,声明的值仅支持3种:关键字,颜色值,尺寸值。用枚举区分

```java
public class Value
{
    private ValueType type;
    private String text;
}

public enum ValueType
{
    KEYWORD,
    SIZE, // 尺寸,单位px,10px
    COLOR // 颜色,6位16进制整数,#cc0000
}

然后,就可以开始解析了,这个比较简单,就不具体讲了。

最终,得到一个Stylesheet对象,其中包行所有的规则。

根据DOM树和CSS样式树,计算每个节点的样式

获取到DOM树和CSS样式树数据结构后,就可以计算每个标签节点的样式了。

在DOM树中,每个节点都会有标签名,或者还有class或id属性,用这个信息与样式表中的每条规则去匹配,如果匹配,就可以为当前节点设置样式信息。

节点有3种类型:块级节点,行内节点和None节点。

用枚举表示

public enum Display
{
    Inline,
    Block,
    None,
}

创建样式节数据结构

public class StyledNode
{
    private Node domNode; // 指向一个dom节点
    private LinkedHashMap<String,Value> specifiedValues; // css信息
    private ArrayList<StyledNode> children; // 孩子节点

    public StyledNode(Node domNode)
    {
        this.domNode=domNode;
        this.specifiedValues=new LinkedHashMap<>();
        this.children=new ArrayList<>();
    }
}

解析的时候,遍历dom树,对其中每个元素节点,都从样式表中搜索匹配的规则

public void parse(StyledNode styledNode)
{
    Node node=styledNode.getDomNode();

    NodeType nodeType=node.getType();
    if(nodeType==NodeType.ELEMENT)
    {
        LinkedHashMap<String,Value> map=matchesRules((ElementNode)node);
        styledNode.addSpecifiedValues(map);

        ArrayList<Node> children=node.getChildren();
        for(Node child: children)
        {
            StyledNode childStyledNode=new StyledNode(child);
            styledNode.addChild(childStyledNode);
            // 递归
            parse(childStyledNode);
        }
    }
    else
    {
        return;
    }
}

对于每一元素,从样式表中搜索某个元素节点的匹配规则

private LinkedHashMap<String,Value> matchesRules(ElementNode element)
{
    LinkedHashMap<String,Value> map=new LinkedHashMap<>();

    for(RuleOfSingleSelector rule: rules)
    {
        // 一条规则中的选择器
        // 选择器按优先级从高到低排序
        SimpleSelector selector=(SimpleSelector)rule.getSelector();
        boolean b=matchesSimpleSelector(element,selector);
        if(b)
        {
            ArrayList<Declaration> declarations=rule.getDeclarations();
            declarations.forEach(declaration->
            {
                String name=declaration.getName();
                if(!map.containsKey(name))
                {
                    map.put(name,declaration.getValue());
                }
            });
        }
    }

    return map;
}

用如下方法匹配选择器

private boolean matchesSimpleSelector(ElementNode element,SimpleSelector selector)
{
    // 检查标签
    String tagName=selector.getTagName();
    if(tagName!=null&&tagName.equals("*")) // 通用选择器
    {
        return true;
    }

    if(tagName!=null&&!tagName.equals(element.getName()))
    {
        return false;
    }

    // 检查id
    String id=selector.getId();
    if(id!=null&&!id.equals(element.id()))
    {
        return false;
    }

    // 检查class
    ArrayList<String> selectorClasses=selector.getClasses();
    ArrayList<String> elementClasses=element.classes();

    if(!selectorClasses.isEmpty()&&elementClasses==null)
    {
        return false;
    }

    if(elementClasses!=null&&!elementClasses.containsAll(selectorClasses))
    {
        return false;
    }

    // 没有匹配的,返回true
    return true;
}

当样式树构建完毕后,获取到的数据类似下面这样

<html>
  <body>
    <h1 margin="auto" color="#cc0000">
      <text></text>
    </h1>
    <div margin-bottom="20px" padding="10px">
      <span>
        <text></text>
      </span>
    </div>
  </body>
</html>

构建布局树

接下来,就可以进行布局了。

所谓布局,就是根据元素节点中的尺寸相关属性,计算盒子的大小。

绘制

绘制只是根据盒子的尺寸与颜色信息,创建图片。

firekaka-用java实现一个简易浏览器

原文:https://www.cnblogs.com/lypzzzzz/p/14664507.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!