从文本字符串到形形色色的网页,浏览器大概要做以下工作
构建DOM树
解析文本字符串为DOM
解析CSS样式
将CSS文本字符串解析为样式树
根据DOM树和CSS样式树,计算每个节点的样式
构建布局树
想要渲染一个完整的页面,除了获知每个节点的具体样式,还需要获知每一个节点在页面上的位置,布局其实是找到所有元素的几何关系的过程。
绘制
由于需要解析的文本内容有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++;
}
}
解析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;
}
解析元素节点时,分为以下步骤
解析开标签
解析元素属性
解析子节点
解析子节点,只需要递归调用上面的parseNodes()方法即可。
解析闭标签
代码如下
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与解析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树中,每个节点都会有标签名,或者还有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>
接下来,就可以进行布局了。
所谓布局,就是根据元素节点中的尺寸相关属性,计算盒子的大小。
绘制只是根据盒子的尺寸与颜色信息,创建图片。
原文:https://www.cnblogs.com/lypzzzzz/p/14664507.html