当前位置: 首页 > news >正文

WebKit Inside: CSS 样式表的解析

CSS 全称为层叠样式表(Cascading Style Sheet),用来定义 HTML 文件最终显示的外观。

为了理解 CSS 的加载与解析,需要对 CSS 样式表的组成,尤其是 CSS Selector 有所了解,相关部分可以参看这里。

HTML 文件里面引入 CSS 样式表有 3 种方式:

1 外部样式表

2 内部样式表

3 行内样式

不同的引入方式,CSS 加载与解析不一样。

CSS 样式表的引入方式可以参看这里。

1 外部样式表

1.1 相关类图

外部样式表加载与解析相关的类图如下所示:

1.2 加载

对于外部样式表,需要将样式表从服务器下载回来才能进行解析。

当构建 DOM 树解析到   <link>   标签的  href  属性时,就会发起下载样式表的请求,发起请求的调用栈如下:

下载外部样式表的请求会从渲染进程传递到网络进程,由网络进程进行下载。

下载的过程中,DOM 树会继续构建,CSS 下载请求并不会阻塞 DOM 树的构建。

1.3 解析

当网络进程下载完样式表数据之后,会将数据传递回渲染进程,由  HTMLLinkElement  对象进行处理,处理的入口函数调用栈如下:

从网络下载回的样式表数据存储在  cachedStyleSheet  对象中,然后在  setCSSStyleSheet  方法中创建  CSSStyleSheet  对象和  StyleSheetContents  对象,最后调用  StyleSheetContents  对象的  parseAuthorStyleSheet  方法开始对 CSS 样式表进行解析,相关代码如下:

 1 void HTMLLinkElement::setCSSStyleSheet(const String& href, const URL& baseURL, const String& charset, const CachedCSSStyleSheet* cachedStyleSheet)2 {3     ...4     CSSParserContext parserContext(document(), baseURL, charset);5     ...6     // 1. 创建 StyleSheetContents 对象7     auto styleSheet = StyleSheetContents::create(href, parserContext);8     // 2. 创建 CSSStyleSheet 对象9     initializeStyleSheet(styleSheet.copyRef(), *cachedStyleSheet, MediaQueryParserContext(document()));
10 
11     // FIXME: Set the visibility option based on m_sheet being clean or not.
12     // Best approach might be to set it on the style sheet content itself or its context parser otherwise.
13     // 3. 在 if 语句这里开始进行 CSS 样式表解析
14     if (!styleSheet.get().parseAuthorStyleSheet(cachedStyleSheet, &document().securityOrigin())) {
15        ...
16         return;
17     }
18     ...
19 }

 StyleSheetContents::parseAuthorStyleSheet  方法内部调用  CSSParser::parseSheet  方法, CSSParser::parseSheet  方法接收样式表内容字符串进行解析,代码如下所示:

 1  bool StyleSheetContents::parseAuthorStyleSheet(const CachedCSSStyleSheet* cachedStyleSheet, const SecurityOrigin* securityOrigin)2 {3     ...4     // 1. 获取从网上下载回来的样式表内容字符串5     String sheetText = cachedStyleSheet->sheetText(mimeTypeCheckHint, &hasValidMIMEType);6     ...7     // 2. 解析样式表字符串8     CSSParser(parserContext()).parseSheet(*this, sheetText);9     return true;
10 1 }

上面代码注释 1 处  cachedStyleSheet  对象中获取样式表字符串。

注释2 处开始对样式表字符串进行解析。

样式表字符串的解析分成 3 个步骤:

1 分词

2 解析样式表 Rule

3 添加样式表 Rule

具体流程如下图所示:

相关代码如下图所示:

 1 void CSSParserImpl::parseStyleSheet(const String& string, const CSSParserContext& context, StyleSheetContents& styleSheet)2 {3     // 1. CSSParserImpl 内部持有 CSSTokenizer 对象,再 CSSParserImpl 对象构建的时对样式表字符串进行分词4     CSSParserImpl parser(context, string, &styleSheet, nullptr);5     // 2. parser.consumeRuleList 解析样式表的 rule6     bool firstRuleValid = parser.consumeRuleList(parser.tokenizer()->tokenRange(), TopLevelRuleList, [&](Ref<StyleRuleBase> rule) {7         ...8         // 3. 将解析好的 rule 添加到 StyleSheetContents 对象中,StyleSheetContents 使用 Vector 存储 rule9         styleSheet.parserAppendRule(WTFMove(rule));
10     });
11 }

上面代码注释 1 处进行分词操作。

注释 2 处对 CSS Rule 进行解析。

注释 3 处 将解析好的 CSS Rule 添加到  StyleSheetContents  , StyleSheetContents  对象内部有 Vector 用来存储解析出来的 CSS Rule。

 CSSParserImpl 内部持有  CSSTokenizer  对象,该对象负责对样式表字符串进行分词。

上面代码注释 1 创建  CSSParserImpl  对象时,就会同时创建  CSSTokenizer  对象,分词过程在  CSSTokenizer  对象内部完成。

分词的结果就是产生一个个  CSSParserToken  对象,存储到  CSSTokenizer  对象内部的数组中:

1 class CSSTokenizer {
2     ...
3     // 1. 存储分词结果
4     Vector<CSSParserToken, 32> m_tokens;
5 };

所谓分词就是根据 CSS 语法,顺序遍历整个样式表字符串的每个字符,将相关字符组成一个个  CSSParserToken  对象。

比如有如下样式表字符串:

div,p {background-color: red;font-size: 24px;
}div.item {margin-top: 2px;
}#content.item {padding-top: 2px;
}div+p {border-width: 2px;
}

 CSSTokenizer 发现第一个字符为 'd' ,而字母在 CSS 语法里面属于标识符,因此扫描继续。

下一个字符为 'i',也属于标识符,扫描继续,直到碰到字符 ','

字符 ',' 在 CSS 语法里是 Selector List 的分割符,因此第一个 Token 扫描结束。 CSSTokenizer  创建一个  CSSParserToken  用来存储 div,同时也会创建另一个  CSSParserToken  用来存储字符 ','

整个分词流程结束之后,就会产生如下的  CSSParserToken  数组(忽略换行符):

为了解析出整个样式表的 Rule List,需要遍历整个  CSSParserToken  数组,遍历的的时候使用  CSSParserTokenRange  对象。

 CSSParserTokenRange  内部有两个指针, m_first  指向第一个  CSSParserToken , m_last  指向最后一个  CSSParserToken 。同时,CSSParserTokenRange  还有 2 个方法:

 peek  方法返回  m_first  指针指向的  CSSParserToken  对象;

 comsume  方法向后移动  m_first  指针。

 遍历整个  CSSParserToken  数组的方法如下:

 1 // 参数 Range 指向整个 CSSParserToken 数组2 template<typename T>3 bool CSSParserImpl::consumeRuleList(CSSParserTokenRange range, RuleListType ruleListType, const T callback)4 {5     // 1. 遍历整个 CSSParserToken 数组6     while (!range.atEnd()) {7         ...8         // 2. 解析 at-rule9         case AtKeywordToken:
10             rule = consumeAtRule(range, allowedRules);
11             break;
12         ...
13         default:
14             // 3. 解析 Qualified Rule
15             rule = consumeQualifiedRule(range, allowedRules);
16             break;
17         }
18         ...
19         if (rule) {
20             // 4. callback 函数将解析出来的 Rule 天健到 SytleSheetContents 对象
21             callback(Ref { *rule });
22         }
23     }
24 
25     return firstRuleValid;
26 }

上面代码注释 1 处遍历整个  CSSParserToken  数组。

代码注释 2 处解析样式表中的  At-Rule .

代码注释 3 处解析样式表中的  Qualified Rule 。从 CSS 样式表的组成可以知道,组成样式表的 Rule 是  At-Rule  和  Style Rule ,而  Style Rule  是一种特殊的  Qualified Rule 。因此 注释 2 和 注释 3 可以解析出 CSS 样式表中的所有 Rule。

代码注释 4 处调用回调函数  callback ,这个回调函数将解析出来的 CSS Rule 添加到  StyleSheetContents  对象中。

1.3.1 解析 Style Rule

如果  Qualified Rule  的  prelude  是  Selector List ,那么  Qualified Rule  就是  Style Rule 。因此,解析  Style Rule  分为 2 步:

1 解析  Style Rule  中的  Selector List 。

2 解析  Style Rule  中的  声明块 。

 相关代码如下:

 1 RefPtr<StyleRuleBase> CSSParserImpl::consumeQualifiedRule(CSSParserTokenRange& range, AllowedRulesType allowedRules)2 {3     ...4     // 1. prelude 变量表示 Qualified Rule 的 Prelude 范围5     CSSParserTokenRange prelude = range.makeSubRange(preludeStart, &range.peek());6     // 2. block 变量表示 Qualified Rule 的声明块范围7     CSSParserTokenRange block = range.consumeBlockCheckingForEditability(m_styleSheet.get());8 9     if (allowedRules <= RegularRules)
10         // 3. 1
11         return consumeStyleRule(prelude, block);
12     
13     ...
14     return nullptr;
15 }

上面代码注释 1 处首先找到当前  Qualified Rule  的  Prelude ,如果是  Style Rule , Prelude  就是  Selector List 。

注释 2 处获取  Qualifed Rule  声明块的范围。

注释 3 处 开始解析  Style Rule 。

解析  Style Rule  的代码如下所示:

 1 RefPtr<StyleRuleBase> CSSParserImpl::consumeStyleRule(CSSParserTokenRange prelude, CSSParserTokenRange block)2 {3     // 1. 解析 Selector List4     auto selectorList = parseCSSSelector(prelude, m_context, m_styleSheet.get(), isNestedContext() ? 5     ...6     RefPtr<StyleRuleBase> styleRule;7     runInNewNestingContext([&] {8         {9             // 2. 解析声明块
10             consumeStyleBlock(block, StyleRuleType::Style);
11         }
12         ...
13         // 3. 注释 2 解析声明块的值存储在 m_parsedProperties 变量中,
14         // 此处使用 m_parsedProperties 变量创建 ImmutableStyleProperties 对象,
15         // ImmutableStyleProperties 对象存储 Style Rule 声明块的值
16         auto properties = createStyleProperties(topContext().m_parsedProperties, m_context.mode);
17 
18         // We save memory by creating a simple StyleRule instead of a heavier StyleRuleWithNesting when we don't need the CSS Nesting features.
19         if (nestedRules.isEmpty() && !selectorList->hasExplicitNestingParent() && !isNestedContext())
20             // 4. 使用解析出来的 elector List 和声明块值创建一个 Style Rule 对象
21             styleRule = StyleRule::create(WTFMove(properties), m_context.hasDocumentSecurityOrigin, WTFMove(*selectorList));
22         else {
23             ...
24         }
25     });
26     
27     return styleRule;
28 }

上面代码注释 1 处解析出  Style Rule  的  Selector List 。

注释 2 处解析  Style Rule  的声明块,解析出来的值存储在  m_parsedProperties  变量中。

注释 3 处根据解析出来的声明块的值创建  ImmutableStyleProperties  对象,该对象最终存储声明块的值。

注释 4 处是有那个解析出来的  Selector List  和声明块的值,创建了  Style Rule  对象。

上面的函数  CSSParserImpl::consumeStyleRule  内部调用  parseCSSSelector  函数解析  Style Rule  的  Selector List 。

从 CSS 样式表的组成可以知道,CSS 的 Selector 分为 4 类:

1 简单 Selector (Simple Selector)

2 复合 Selector (Compound Selector)

3 组合 Selector (Complex Selector)

4 Selector List

 Selector List  由前面 3 类 Selector 任意组合,通过逗号  ','  连接而来,比如下面就是 2 种类型的  Selector List :

div /* 简单 Selector */, div + p/*组合 Selector */, p#item/*复合 Selector */div/* 简单 Selector */, p/* 简单 Selector */

而复合 Selector 和组合 Selector 由简单 Selector 构成,因此为了理解函数  parseCSSSelector  的过程,首先需要理解简单 Selector 的解析过程。

1.3.2 解析简单 Selector

简单 Selector 有 6 种:

1 类型 Selector (Type Selector),比如:  div 。

2 通用 Selector (Universal Selector),比如:  * 。

3 属性 Selector (Attribute Selector),比如:  [attr=val] 。

4 类 Selector (Class Selector),比如:  .item 。

5 ID Selector,比如:  #item 。

6 伪类 Selector (Pseudo-Class Selector),比如:  :hover 。

和伪类 Selector 类似的,还有伪元素 Selector (Pseudo-Element Selector),比如:  ::first-letter 。

解析出来的简单 Selector 由类  CSSParserSelector  和  CSSSelector  表示,其中  CSSParserSelector  内部通过  m_selector  属性持有  CSSSelector 。

 CSSSelector  类有一个属性   Match m_match ,代表这个简单 Selector 使用何种方式进行匹配。

 Match  的定义如下:

// 定义在 CSSSelector.h 文件
enum class Match : uint8_t {Unknown = 0, // 初始化值Tag, // 类型 Selector,比如 divId, // ID Selector,比如 #itemClass, // 类 Selector,比如 .itemExact, // 属性 Selector 中的 [attr=val]Set, // 属性 Selector 中的 [attr]List, // 属性 Selector 中的 [atr~=val]Hyphen, // 属性 Selector 中的 [attr|=val]PseudoClass, // 伪类 Selector,比如 :hoverPseudoElement, // 伪类 Selector,比如 ::first-letterContain, // 属性 Selector 中的 [attr*=val]Begin, // 属性 Selector 中的 [attr^=val]End, // 属性 Selector 中的 [attr$=val]PagePseudoClass, // 与 @page Rule 相关NestingParent, // 与嵌套 CSS Rule 相关ForgivingUnknown, // 与伪类函数,比如 :is() 相关ForgivingUnknownNestContaining // 与伪类函数,比如 :is() 相关
};

解析简单 Selector 的代码如下所示:

 1 std::unique_ptr<CSSParserSelector> CSSSelectorParser::consumeSimpleSelector(CSSParserTokenRange& range)2 {3     const CSSParserToken& token = range.peek();4     // 1. 返回值是 CSSParserSelector 对象5     std::unique_ptr<CSSParserSelector> selector;6     if (token.type() == HashToken)7         // 2. ID Selector8         selector = consumeId(range);9     else if (token.type() == DelimiterToken && token.delimiter() == '.')
10         // 3. 类 Selector
11         selector = consumeClass(range);
12     else if (token.type() == DelimiterToken && token.delimiter() == '&' && m_context.cssNestingEnabled)
13         // 4. 嵌套 CSS Rule 标识符也在这里解析,嵌套 CSS 后面介绍
14         selector = consumeNesting(range);
15     else if (token.type() == LeftBracketToken)
16         // 5. 属性 Selector 
17         selector = consumeAttribute(range);
18     else if (token.type() == ColonToken)
19         // 6. 伪类或者伪元素 Selector
20         // consumePseudo 内部判断如果只有一个 ':' 就解析成伪类 Selector,
21         // 如果连着 2 个 ':' 就解析成伪元素 Selector.
22         selector = consumePseudo(range);
23     else
24         return nullptr;
25     ...
26     return selector;
27 }

从代码注释 1 处看到,解析后的简单 Selector 是一个  CSSParserSelector  对象。

注释 2-6 分别解析了 4 种简单 Selector 和嵌套 CSS Rule 表示符  '&' 。类型 Selector 和通用 Selector 在代码实现上并没有在这里进行解析,而是放到了别的地方。

嵌套 CSS Rule 后文有介绍。

由于类型 Selector 和通用 Selector 可以结合命名空间使用,它们的解析放在了  CSSSelectorParser::consumeName  函数中:

 1 // 参数 name 是一个引用,它用来存储解析出来的类型 Selector 标签名或者通用 Selector 名 "*"2 bool CSSSelectorParser::consumeName(CSSParserTokenRange& range, AtomString& name, AtomString& namespacePrefix)3 {4     ...5     const CSSParserToken& firstToken = range.peek();6     if (firstToken.type() == IdentToken) {7         // 1. 解析类型 Selector 名8         name = firstToken.value().toAtomString();9         range.consume();
10     } else if (firstToken.type() == DelimiterToken && firstToken.delimiter() == '*') {
11         // 2. 解析通用 Selector 名
12         name = starAtom();
13         range.consume();
14     } 
15     ...
16     return true;
17 }

上面代码参数  name  是一个引用,用来存储解析出来的名字: 要么是类型 Selector 的 HTML 标签名,要么是通用 Selector 名  "*" 。

代码注释 1 解析类型 Selector HTML 标签名。

代码注释 2 解析通用 Selector 名  "*" 。

由于本质上说,通用 Selector 是一种特殊的类型 Selector,因此通用 Selector 的  m_match  属性也是  Match::Tag 。

1.3.3 解析复合 Selector

复合 Selector 由一个或者多个简单 Selector 连接而成,这些简单 Selector 之间不能有其他字符,包括空格。

比如  div#item  就是一个复合 Selector,而  div #item  不是一个复合 Selector,而是一个组合 Selector。

解析复合 Selector 的代码如下:

std::unique_ptr<CSSParserSelector> CSSSelectorParser::consumeCompoundSelector(CSSParserTokenRange& range)
{...// 1. elementName 存储类型 Selector 对应的 HTML 标签名,或者通用 Selector 名 "*"AtomString elementName;// 2. 解析类型 Selector 名或者通用 Selector 名const bool hasName = consumeName(range, elementName, namespacePrefix);if (!hasName) {// 3. 对于 #item.news 这样的复合 Selector,并没有类型 Selector 和通用 Selector,// 因此 hasName = false,这里解析出第一个 ID Selector #itemcompoundSelector = consumeSimpleSelector(range);...}// 4. 循环解析后续的简单 Selector,因为一个复合 Selector 可能包含许多个简单 Selector,比如 div#item.newswhile (auto simpleSelector = consumeSimpleSelector(range)) {...if (compoundSelector)// 5. CSSParserSelector 是一个链表结构,这里将复合 Selector 里面解析出来的简单 Selector 串成一个链表compoundSelector->appendTagHistory(CSSSelector::RelationType::Subselector, WTFMove(simpleSelector));elsecompoundSelector = WTFMove(simpleSelector);}...if (!compoundSelector) {// 6. 如果复合 Selector 只有类型 Seledtor 或者通用 Selector,比如 div 或者 *,那么就直接返回这个 Selectorreturn makeUnique<CSSParserSelector>(QualifiedName(namespacePrefix, elementName, namespaceURI));}// 7. 如果复合 Selector 是 div#item.news 这种以类型 Selector 开头, 或者 *#item.news 这种以通用 Selector 开头,// 那么根据 CSS 语法,类型 Selector 和通用 Selector 应该位于复合 Selector 的最前面,// 因此这个方法会根据 elementName 创建一个 CSSParserSelector,并添加到复合 Selector 链最前面。// 如果注释 2 处没有解析出 elementName,也就是 #item.news 这种形式的复合 Selector,这个函数什么也不做prependTypeSelectorIfNeeded(namespacePrefix, elementName, *compoundSelector);// 8. 这个函数大部分场景会直接返回上面解析出来的复合 Selector 链, 只会在一些特殊场景下重新排列复合 Selector 链,然后返回.return splitCompoundAtImplicitShadowCrossingCombinator(WTFMove(compoundSelector), m_context);
}

上面代码注释 1 处的变量  elementName  就是用来存储  consumeName  方法解析出来的类型 Selector 对应的 HTML 标签名,或者通用 Selector 名  '*' 。

代码注释 2 处是在解析类型 Selector 对应的 HTML 标签名或者通用 Selector 名。

如果复合 Selector 里面没有类型 Selector 或者通用 Selector,比如  #item.news ,那么就会运行到注释 3 处,解析出 ID Selector  #item 。

代码注释 4 处遍历复合 Selector 的其他简单 Selector。

代码注释 5 将从复合 Selector 里面解析出来的简单 Selector 串起来。类 CSSParserSelector  里面有一个属性  m_tagHistory ,它的类型是一个  CSSParserSelector * ,这样  CSSParserSelector  就是一个链表。比如复合 Selector

 div#item.news  解析完成之后,就会形成  div -> #item -> .news   这样的链表结构:

注释 5 在构成复合 Selector 链表时,还为构成复合 Selector 的简单 Selector 之间设置了  Relation :  CSSSelector::RelationType::Subselector 。构成复合 Selector 的简单 Selector 之间的  Relation  都是  CSSSelector::RelationType::Subselector ,其他类型的  Relation  在解析组合 Selector 可以看到。

如果复合 Selector 本身只是一个类型 Selector,比如  div  或者 是一个通用 Selector  '*' ,那么注释 6 处就直接返回这个  CSSParserSelector 。

根据 CSS 语法,如果复合 Selector 里面的简单 Selector 有类型 Selector 或者通用 Selector,那么它们需要在复合 Selector 的最前面,注释 7 正是处理这种情况。

代码注释 8 正常情形下会直接返回解析出来的复合 Selector 对象,只有在一些特殊情况会调整复合 Selector 链表的顺序。特殊情形在  splitCompoundAtImplicitShadowCrossingCombinator  方法内部的注释里面有解释:

// The tagHistory is a linked list that stores combinator separated compound selectors// from right-to-left. Yet, within a single compound selector, stores the simple selectors// from left-to-right.//// ".a.b > div#id" is stored in a tagHistory as [div, #id, .a, .b], each element in the// list stored with an associated relation (combinator or Subselector).//// ::cue, ::shadow, and custom pseudo elements have an implicit ShadowPseudo combinator// to their left, which really makes for a new compound selector, yet it's consumed by// the selector parser as a single compound selector.//// Example: input#x::-webkit-inner-spin-button -> [ ::-webkit-inner-spin-button, input, #x ]//

1.3.4 解析组合 Selector

组合 Selector 由  Combinator  连接复合 Selector 组成,根据 CSS 语法  Combinator  有 4 种:

1  空格 : div p 

2  > :  div > p 

3  + :  div + p 

4  ~ :  div ~ p 

解析组合 Selector 的相关代码如下:

 1 std::unique_ptr<CSSParserSelector> CSSSelectorParser::consumeComplexSelector(CSSParserTokenRange& range)2 {3     // 1. 从组合 Selector 里面解析出一个复合 Selector4     auto selector = consumeCompoundSelector(range);5     if (!selector)6         return nullptr;7     ...8     while (true) {9         // 2. 解析 Combinator
10         auto combinator = consumeCombinator(range);
11         // 3. 如果 CSS Rule 是 div{background-color: red;},那么 consumeCombinator 方法
12         // 返回 CSSSelector::RelationType::Subselector
13         if (combinator == CSSSelector::RelationType::Subselector)
14             // 4. 在注释 3 这种情形下,直接跳出循环,返回 Selector div
15             break;
16 
17         // 5. 代码运行到这里可能碰到两种 CSS Rule 情形:
18         // Case 1: div {background-color: red;}
19         // Case 2: div + p {background-color: red;}
20         // 在 Case 2 下,可以解析到下一个 Selector p,此时 Combinator 是 '+',
21         // 在 Case 1 下,Combinator 是空格,但是确没有下一个 Selector
22         auto nextSelector = consumeCompoundSelector(range);
23         if (!nextSelector)
24             // 6. 如果是 Case 1,则直接返回 Selector div
25             return isDescendantCombinator(combinator) ? WTFMove(selector) : nullptr;
26         ...
27         CSSParserSelector* end = nextSelector.get();
28         ...
29         // 7. 如果能解析到下一个复合 Selector,由于复合 Selector 是一个链表结构,这里遍历链表到末尾,
30         // 遍历结束,end 是 nextSelector 的末尾
31         while (end->tagHistory()) {
32             end = end->tagHistory();
33             ...
34         }
35         // 8. 根据 Combinator 设置 Selector 之间的关系
36         end->setRelation(combinator);
37         ...
38         // 9. 组合 Selector 之间也构成了链表关系
39         end->setTagHistory(WTFMove(selector));
40         selector = WTFMove(nextSelector);
41     }
42 
43     return selector;
44 }

上面代码注释 1 首先解析出一个复合 Selector。

注释 2 处尝试解析  Combinator 。 Combinator 表示的是 Selector 之间的关系,在代码实现上将   Subselector  也当成了  Combinator  的一种,如注释 3 所示。

如果解析的 CSS Rule 为  div{background-color: red;} ,注意  div  和  {  之间没有空格。此时方法  consumeCombinator  解析出来的  Combinator  为  CSSSelector::RelationType::Subselector ,那么就会进入到注释 4 处跳出循环,直接返回 Selector  div 。

如果不是上面注释 3 的情形,则如注释 5所示,CSS Rule 又有两种 Case:

Case 1:  div {background-color: red;} , 注意  div  和  {  之间有空格。

Case 2:  div + p { background-color: red;} 。

在 Case 1 下,由于解析不到后续的 Selector,将进入注释 6,注释 6 会直接返回 Selector  div 。

在 Case 2 下, Combinator  解析为  + ,而且可以解析出下一个 Selector  p ,因此代码运行到注释 7 处。

由于 Selector 本身是一个链表结构,注释 7 遍历这个链表,并且将链表最后一个 Selector 存入变量  end 。

注释 8 根据  Combinator  设置 Selector 之间的关系,由于  Combinator  有 4 种,对应的关系也有 4种:

1 // 定义在 CSSSelector.h
2 enum class RelationType : uint8_t {
3     Subselector = 0, // 复合 Selector 使用
4     DescendantSpace, // 空格
5     Child, // >
6     DirectAdjacent, // +
7     IndirectAdjacent, // ~
8     ...
9 };

注释 9 和复合 Selecto 一样,也将解析出来的 Selector 构造成一个列表。

不一样的是,复合 Selector 链表顺序就是简单 Selector 的排列顺序,比如复合 Selector  div#item.news  解析出来的链表结构为  div -> #item -> .news 。而对于组合 Selector,链表顺序和 复合Selector 顺序是相反的,比如组合 Selector  p#content + div#item.news ,解析出来的链表结构为  div -> #item -> .news -> p -> #content

1.3.5 解析 Selector List

有了上面的介绍,下面来看解析  Selector List  的代码。解析  Selector List  的代码位于函数  parseCSSSelector :

 1  std::optional<CSSSelectorList> parseCSSSelector(CSSParserTokenRange range, const CSSSelectorParserContext& context, StyleSheetContents* styleSheet, CSSParserEnum::IsNestedContext isNestedContext)2  {3      // 1. 创建 Selector 解析器4      CSSSelectorParser parser(context, styleSheet, isNestedContext);5      range.consumeWhitespace();6      auto consume = [&] {7          ...8          // 2. 解析组合 Selector List9          return parser.consumeComplexSelectorList(range);
10     };
11     CSSSelectorList result = consume();
12     ...
13     return result;
14 }

上面代码注释 1 创建 CSS Selector 解析器。

上面代码注释 2 解析组合 Selector List,相关代码如下:

CSSSelectorList CSSSelectorParser::consumeComplexSelectorList(CSSParserTokenRange& range)
{return consumeSelectorList(range, [&] (CSSParserTokenRange& range) {// 1. 解析组合 Selectorreturn consumeComplexSelector(range);});
}template <typename ConsumeSelector>
CSSSelectorList CSSSelectorParser::consumeSelectorList(CSSParserTokenRange& range, ConsumeSelector&& consumeSelector)
{// 2. 存储解析出来的 Selector ListVector<std::unique_ptr<CSSParserSelector>> selectorList;// 3. consumeSelector 是一个回调函数,就是上面代码注释 1 中的 consumeComplexSelectorauto selector = consumeSelector(range);if (!selector)return { };// 4. 将解析出来的 Selector 添加到数组中selectorList.append(WTFMove(selector));// 5. 遍历 CSS Selector Tokens,如果遇到逗号 CommaToken,说明还有下一个 Selector 需要解析while (!range.atEnd() && range.peek().type() == CommaToken) {range.consumeIncludingWhitespace();// 6. 解析逗号后面的下一个 Selector,也就是调用函数 consumeComplexSelectorselector = consumeSelector(range);if (!selector)return { };// 7. 继续添加解析出来的 Selector 到数组中selectorList.append(WTFMove(selector));}...return CSSSelectorList { WTFMove(selectorList) };
}

上面代码注释 2 的 变量 selectorList  存储所有解析出来的 CSS Selector。

代码注释 3 调用了一个回调函数,回调函数就是注释 1 的  consumeComplexSelector ,用来解析组合 Selector。

注释 4 将解析出来的组合 Selector 添加到数组中。

注释 5 遍历 CSS Selector Tokens,如果遇到了逗号  ,  说明还有下一个 Selector 需要解析,那么就会运行到注释 6 调用  consumeComplexSelector  继续解析。

注释 7 继续添加解析出来的 Selector。

综合整个  Selector List  的解析过程,函数调用栈如下:

1.3.6 解析声明块

解析声明块的函数是  CSSParserImpl::consumeStyleBlock ,这个函数内部调用  CSSParserImpl::consumeBlockContent  函数,这个函数内部解析声明块:

 1 void CSSParserImpl::consumeBlockContent(CSSParserTokenRange range, StyleRuleType ruleType, OnlyDeclarations onlyDeclarations, ParsingStyleDeclarationsInRuleList isParsingStyleDeclarationsInRuleList)2 {3     ...4     // 1. 声明块里面会有多个声明,这里进行遍历5     while (!range.atEnd()) {6         ...7         case IdentToken: {8             const auto declarationStart = &range.peek();9             ...
10             // 2. 获取一条声明的范围
11             const auto declarationRange = range.makeSubRange(declarationStart, &range.peek());
12             // 3. 解析一条声明
13             auto isValidDeclaration = consumeDeclaration(declarationRange, ruleType);
14             ...
15             break;
16         }
17         ..
18     }
19     ...
20 }

由于声明块中会有多条声明,上面代码注释 1 处就是循环遍历所有声明。

注释 2 获取一条声明的 Token 范围。

注释 3 对这条声明进行解析,函数  consumeDeclaration  相关代码如下所示:

 1 bool CSSParserImpl::consumeDeclaration(CSSParserTokenRange range, StyleRuleType ruleType)2 {3     ...4     // 1. 获取属性名 Token5     auto& token = range.consumeIncludingWhitespace();6     // 2. 获取属性名对应的属性 ID7     auto propertyID = token.parseAsCSSPropertyID();8     ...9     if (propertyID != CSSPropertyInvalid)
10         // 3. 解析属性值
11         consumeDeclarationValue(range.makeSubRange(&range.peek(), declarationValueEnd), propertyID, important, ruleType);
12     ...
13     return didParseNewProperties();
14 }

上面代码注释 1 获取声明中属性名 Token。

属性名解析出来并不是直接存储属性名字符串,而是存储其对应的属性 ID,如注释 2 所示。 CSSPropertyID  定义在  CSSPropertyNames.h  文件中,下面截取部分定义:

// 定义在 CSSPropertyNames.h
enum CSSPropertyID : uint16_t {CSSPropertyInvalid = 0,CSSPropertyCustom = 1,CSSPropertyColorScheme = 2,CSSPropertyWritingMode = 3,CSSPropertyWebkitRubyPosition = 4,CSSPropertyColor = 5,CSSPropertyDirection = 6,CSSPropertyDisplay = 7,CSSPropertyFontFamily = 8,...
}

需要注释的是  CSSPropertyNames.h  头文件是 WebKit 工程使用 Python 脚本动态生成,需要运行 WebKit 工程才可以看到。

上面代码注释 3 解析属性的值,解析出来的属性值存储在  CSSValue  对象中,相关代码如下:

 1 void CSSParserImpl::consumeDeclarationValue(CSSParserTokenRange range, CSSPropertyID propertyID, bool important, StyleRuleType ruleType)2 {3     // 1. 调用 CSSPropertyParser 类方法解析属性值4     CSSPropertyParser::parseValue(propertyID, important, range, m_context, topContext().m_parsedProperties, ruleType);5 }6 7 bool CSSPropertyParser::parseValue(CSSPropertyID propertyID, bool important, const CSSParserTokenRange& range, const CSSParserContext& context, ParsedPropertyVector& parsedProperties, StyleRuleType ruleType)8 {9     ...
10     // 2. 创建属性解析器
11     CSSPropertyParser parser(range, context, &parsedProperties);
12     bool parseSuccess;
13     if (ruleType == StyleRuleType::FontFace)
14     ...
15     else
16         // 3. 解析属性值为 CSSValue 对象,然后使用 propertyID 和 CSSValue 对象创建 CSSProperty 对象,
17         // CSSProperty 对象会被存储在 m_parsedProperties 中
18         parseSuccess = parser.parseValueStart(propertyID, important);
19     ...
20     return parseSuccess;
21 }

上面代码注释 1 调用  CSSPropertyParser  类方法  parseValue  解析属性值。

注释 2 创建  CSSPropertyParser 。

注释 3 将属性值解析为  CSSValue  对象,然后使用  propertyID  和  CSSValue  对象创建  CSSProperty  对象,这个  CSSProperty  对象存储到数组  m_parsedProperties  中。

1.3.7 解析 At-Rule

CSSParserImpl::consumeRuleList  方法中解析 At-Rule,At-Rule  的解析和  Qualifed Rule  的解析类似,也是先解析  Prelude ,然后解析声明块。

不同的  At-Rule  语法上有所差异,比如有些  At-Rule  只有  Prelude  部分,没有声明块,比如:

@charset "UTF-8";

有些  At-Rule  有声明块但是没有  Prelude ,比如:

@font-face {font-family: "Trickster";
}

而有些 At-Rule 既有  Prelude  也有声明块,比如:

@media screen, print {body {line-height: 1.2;}
}

无论何种形式,解析  At-Rule  的  Prelude  和声明块原理,与解析  Qualified Rule  类似。

相关代码如下:

 1 RefPtr<StyleRuleBase> CSSParserImpl::consumeAtRule(CSSParserTokenRange& range, AllowedRulesType allowedRules)2 {3 4    ...5    // 1. 获取 prelude 范围6    CSSParserTokenRange prelude = range.makeSubRange(preludeStart, &range.peek());7    // 2. 根据 @ 符号后面的 name,获取对应的 ID 值,8    // 比如对于 @charset,cssAtRuleID 方法根据 "charset" 字符串,返回 CSSAtRuleCharset9    CSSAtRuleID id = cssAtRuleID(name);
10    
11    // 3. 有些 At-Rule 没有 block,比如 @charset "UTF-8; 因此直接解析 prelude 生成对应的 Rule 对象
12    if (range.atEnd() || range.peek().type() == SemicolonToken) {
13        range.consume();
14        if (allowedRules == AllowCharsetRules && id == CSSAtRuleCharset)
15            // 4. 解析 @charset At-Rule,返回对应的 Rule 对象
16            return consumeCharsetRule(prelude);
17        ...
18    }
19    
20    // 5. 获取声明块范围
21    CSSParserTokenRange block = range.consumeBlock();
22    ...
23    // 6. 根据对应的 CSSAtRuleID,解析相应的 At-Rule
24    switch (id) {
25    ...
26    case CSSAtRuleFontFace:
27        // 7. 解析生成 @font-face At-Rule,返回对应的 Rule 对象
28        return consumeFontFaceRule(prelude, block);
29    ...
30 }

上面代码注释 1 处先获取  prelude  范围。

注释 2 根据  '@'  符号后面的 name,获取对应的 ID。

比如  @charset  的 name 是  "charset"  字符串,调用函数  cssAtRuleID  返回值  CSSAtRuleIDCharset 。

 enum CSSAtRuleID  定义在  CSSAtRuleID.h  头文件中,截取部分代码如下:

// 定义在 CSSAtRuleID.h
enum CSSAtRuleID {CSSAtRuleInvalid = 0,CSSAtRuleCharset,CSSAtRuleFontFace,CSSAtRuleImport,CSSAtRuleKeyframes,CSSAtRuleMedia,...
}

因为  At-Rule  分为  Statement At-Rule  和  块式 At-Rule 。比如  @charset "UTF-8";  就是  Statement At-Rule ,而  @font-face {font-family: "Trickster";}  就是一个  块式 At-Rule 。

由于  Statement At-Rule  没有声明块,所以注释 3 处就是专门解析这些  Statement At-Rule 。

如果是  块式 At-Rule ,那么在注释 5 处会获取声明块的范围。

注释 6 处根据  CSSAtRuleID  调用对应的函数,解析出对应的  At-Rule  对象。

1.3.8 解析嵌套 CSS Rule

CSS Style Rule 支持嵌套,比如:

.foo {color: red;/* 嵌套的 CSS Rule */a {color: blue;}
}

这种嵌套的 CSS Rule 等价于 2 条 CSS Rule:

.foo {color: red;
}.foo a {color: blue;
}

与嵌套 CSS Rule 相关的一个 Selector 是  Nesting Selector ,就是之前解析简单 Selector 遇到的  '&'  符号, '&'  符号代表父 Rule 的  Selector List 。

相关例子如下:

.foo {color: red;/* & 符号 */&:hover { color: blue; }
}

上面这条嵌套 Rule 等价于下面 2 条 Rule:

.foo {color: red;
}.foo:hover {color: blue;
}

这里只是简单介绍了嵌套 Rule 的规则,更详细的介绍可以参看这里。

在代码实现上,如果一条 CSS Rule 包含嵌套 Rule,那么解析这条 Rule 返回的对象就不再是  StyleRule ,而是  StyleRuleWithNesting 。 StyleRuleWithNesting  继承自  StyleRule ,它内部有一个数组属性  m_nestedRules  存储所有的嵌套子 Rule。

 相关代码如下:

 1 RefPtr<StyleRuleBase> CSSParserImpl::consumeStyleRule(CSSParserTokenRange prelude, CSSParserTokenRange block)2 {3     ...4     RefPtr<StyleRuleBase> styleRule;5 6     runInNewNestingContext([&] {7         ...8         if (nestedRules.isEmpty() && !selectorList->hasExplicitNestingParent() && !isNestedContext())9             // 1. 创建非嵌套的 Style Rule
10             styleRule = StyleRule::create(WTFMove(properties), m_context.hasDocumentSecurityOrigin, WTFMove(*selectorList));
11         else {
12             // 2. 创建嵌套的 Style Rule
13             styleRule = StyleRuleWithNesting::create(WTFMove(properties), m_context.hasDocumentSecurityOrigin, WTFMove(*selectorList), WTFMove(nestedRules));
14             m_styleSheet->setHasNestingRules();
15         }
16     });
17     
18     return styleRule;
19 }

上面代码注释 1 创建非嵌套  Style Rule 。

代码注释 2 创建嵌套  StyleRuleWithNesting 。

2 内部样式表

内部样式表直接写在 HTML 文件的  <style>  中,当 WebKit 解析完  </style>  标签,就会解析  <style>  标签包围的样式表字符串。

2.1 相关类图

2.2 解析

 inlineStyleSheetOwner  解析内部样式表与外部样式表一样,首先创建  CSSStyleSheet  对象和  StyleSheetContents  对象,然后由  StyleSheetContents  发起解析流程。

相关代码如下:

 1 // 参数 text 就是 <style> 标签包围的样式表字符串2 void InlineStyleSheetOwner::createSheet(Element& element, const String& text)3 {4     ...5     // 1. 创建 StyleSheetContents6     auto contents = StyleSheetContents::create(String(), parserContextForElement(element));7     // 2. 创建 CSSStyleSheet8     m_sheet = CSSStyleSheet::createInline(contents.get(), element, m_startTextPosition);9     ...
10     // 3. 由 StyleSheetContents 对象发起解析
11     contents->parseString(text);
12     ...
13 }

上面代码的  text  参数就是  <style>  标签包围的样式表字符串。

代码注释 1 创建  StyleSheetContents  对象。

代码注释 2 创建  CSSStyleSheet  对象。

代码注释 3 调用  StyleSheetContents::parseString  方法开始解析,其代码如下:

1 bool StyleSheetContents::parseString(const String& sheetText)
2 {
3     CSSParser p(parserContext());
4     // 1. 创建解析器,并调用了和外部样式表一样的方法开始解析内部样式表
5     p.parseSheet(*this, sheetText);
6     return true;
7 }

上面代码注释 1 调用了和解析外部样式表一样的方法,来解析内部样式表,因此,两者的解析流程一样。

3 行内样式

行内样式位于 HTML 标签的  style  属性中,当 WebKit 在构建 DOM 树解析到某个 HTML 标签的  style  属性时,就会将  style  属性的值进行解析。

由于  style  属性值只是一系列声明,因此只需要进行声明的解析。解析的结果存储在这个 HTML 元素上。

3. 1 相关类图

类图以  <div>  标签为例

3.2 解析

行内样式的解析比较简单。

因为行内样式只有属性 List,所以只需要解析对应的属性即可,相关代码如下:

 1 // 参数 newStyleString 就是 style 属性对应的值2 void StyledElement::setInlineStyleFromString(const AtomString& newStyleString)3 {4     // inlineStyle 是一个引用类型,最后解析出来的属性值存储在这里5     auto& inlineStyle = elementData()->m_inlineStyle;6     ...7     if (!inlineStyle)8         // 2. 调用 CSSParser:parserInlineStyleDeclaration 开始解析9         inlineStyle = CSSParser::parseInlineStyleDeclaration(newStyleString, this);
10     ...
11 }

上面代码参数  newStyleString  就是  style  属性对应的值。

代码注释 1 变量  inlineStyle  是一个引用类型,存储解析出来的属性值。

代码注释 2 处的方法解析声明 List,其代码如下:

 1 Ref<ImmutableStyleProperties> CSSParser::parseInlineStyleDeclaration(const String& string, const Element* element)2 {3     // 1. CSSParser 调用 CSSParserImpl 的对应方法进行解析4     return CSSParserImpl::parseInlineStyleDeclaration(string, element);5 }6 7 Ref<ImmutableStyleProperties> CSSParserImpl::parseInlineStyleDeclaration(const String& string, const Element* element)8 {9     CSSParserContext context(element->document());
10     ...
11     CSSParserImpl parser(context, string);
12     // 2. 解析声明 List
13     parser.consumeDeclarationList(parser.tokenizer()->tokenRange(), StyleRuleType::Style);
14     // 3. 创建返回值
15     return createStyleProperties(parser.topContext().m_parsedProperties, context.mode);
16 }

上面代码注释 2 就是解析声明 List 的地方。

代码注释 3 将解析出来的值返回。

参考资料

CSS Nesting Model

相关文章:

WebKit Inside: CSS 样式表的解析

CSS 全称为层叠样式表(Cascading Style Sheet)&#xff0c;用来定义 HTML 文件最终显示的外观。 为了理解 CSS 的加载与解析&#xff0c;需要对 CSS 样式表的组成&#xff0c;尤其是 CSS Selector 有所了解&#xff0c;相关部分可以参看这里。 HTML 文件里面引入 CSS 样式表有 …...

javaee之Elasticsearch相关知识

简单说一下Elasticsearch相关知识 其余的参考官网文档 我们还可以用下面的方式来查 看一下原始索引库的模板 下面看一下数据库映射关系 下面就是更改了id1的所有数据 下面是我索引库中的内容 说一下查询之后&#xff0c;一些属性的含义 上面案例是这样理解的 match查询类型会对…...

【SpringCloud】微服务技术栈入门3 - Gateway快速上手

目录 GatewayWebFlux网关基本配置过滤器与断言工厂全局过滤器跨域处理 CORS Gateway WebFlux gateway 基于 webflux 构建 WebFlux 是基于反应式流概念的响应式编程框架&#xff0c;用于构建异步非阻塞的 Web 应用程序。它支持响应式编程范式&#xff0c;并提供了一种响应式的方…...

《理解深度学习》2023最新版本+习题答案册pdf

刚入门深度学习或者觉得学起来很困难的同学看过来了&#xff0c;今天分享的这本深度学习教科书绝对适合你。 就是这本已在外网获13.1万次下载的宝藏教科书《理解深度学习》。本书由巴斯大学计算机科学教授Simon J.D. Prince撰写&#xff0c;全书共541页&#xff0c;目前共有21…...

课题学习(五)----阅读论文《抗差自适应滤波的导向钻具动态姿态测量方法》

一、简介 抗差自适应滤波&#xff1a;利用等价权函数和自适应因子合理的分配信息&#xff0c;有效地滤除钻具振动对动态姿态测量的影响。、   针对导向钻井工具动态测量受钻具振动的影响而导致测量不准确的问题&#xff0c;提出一种抗差自适应滤波的动态空间姿态测量方法。通…...

一个CPU是怎么寻址的?

目录 CISC vs RISC 概念和历史 CISC vs RISC 对比举例&#xff1a;X86的CAS(做原子操作的) 对比举例&#xff1a;ARM的CAS(做原子操作的) 指令寻址 指令中的操作数的寻址方式 各语言对象内存布局对比 C内存布局 理解编译单元 Java对象内存布局 python对象模型 CPU …...

提高网站性能的10种方法:加速用户体验和降低服务器负担

在今天的数字时代&#xff0c;网站性能对于吸引和保留用户至关重要。一个快速加载的网站不仅提供更好的用户体验&#xff0c;还有助于降低服务器负担。以下是10种提高网站性能的方法&#xff0c;旨在加速页面加载速度和减少服务器的工作负荷。 压缩网页资源 利用压缩算法如gzi…...

195、SpringBoot--配置RabbitMQ消息Broker的SSL 和 管理控制台的HTTPS

开启Rabbitmq的一些命令&#xff1a; 小黑窗输入&#xff1a; rabbitmq-plugins enable rabbitmq_management 启动控制台插件&#xff0c;就是启动登录rabbitmq控制台的页面 rabbitmq_management 代表了RabbitMQ的管理界面。 rabbitmq-server 启动rabbitMQ服务器 上面这个&…...

确定性执行

确定性执行是指在给定输入的情况下,在有限的时间内产生一致的输出。 也就是输入到输出的运行过程是确定的,输入与输出有如下关系: 输出 = f (输入)。 确定性执行主要涉及以下几个方面: 时间确定性:计算的输出始终在给定的某个时间点之前发生,即程序不能无限制地运行下去…...

docker compose 管理应用服务的常用命令

一 、docker compose 是什么 Docker Compose是一个用来管理多个关联容器的工具&#xff0c;可以根据配置文件自动构建、管理、编排一组容器。 Docker Compose语境下的“服务”是指一组容器共同构成的一个应用服务后端。 Docker Compose语境下的“项目”是由一个或多个应用服务…...

产品安全—CC标准 ISO/IEC 15408:2022

文章目录 1. 变化2. Part1 简介和一般模型3. Part2 安全功能组件4. Part3 安全保障组件5. Part4 评估方法和活动规范框架6. Part5 预定义的安全要求包7. 总结 1. 变化 增加了两个部分&#xff1a;评估方法和活动规范框架 & 预定义的安全要求包 术语已经过审查和更新&#…...

Pytorch笔记之回归

文章目录 前言一、导入库二、数据处理三、构建模型四、迭代训练五、结果预测总结 前言 以线性回归为例&#xff0c;记录Pytorch的基本使用方法。 一、导入库 import numpy as np import matplotlib.pyplot as plt import torch from torch.autograd import Variable # 定义求…...

哪个证券公司可以加杠杆,淘配网是您的杠杆综合网站!

在证券市场中&#xff0c;投资者经常寻求提高资金杠杆以获得更高的回报。杠杆交易可以让您在不必拥有等额本金的情况下&#xff0c;参与更多的交易活动。然而&#xff0c;为了进行杠杆交易&#xff0c;您需要找到一家证券公司或平台&#xff0c;可以为您提供这种服务。本文将介…...

万字解读|怎样激活 TDengine 最高性价比?

不知不觉间&#xff0c;TDengine 已经 6 岁多了。在这 6 年多的时间里&#xff0c;我们从零开始&#xff0c;在一行又一行代码的淬炼下&#xff0c;TDengine 从 1.6 走过 2.0&#xff0c;终于走到如今的 3.0 时代。 自 2022 年下旬发布以来&#xff0c;经过我们不断地打磨优化…...

【目标检测】大图包括标签切分,并转换成txt格式

前言 遥感图像比较大&#xff0c;通常需要切分成小块再进行训练&#xff0c;之前写过一篇关于大图裁切和拼接的文章【目标检测】图像裁剪/标签可视化/图像拼接处理脚本&#xff0c;不过当时的工作流是先将大图切分成小图&#xff0c;再在小图上进行标注&#xff0c;于是就不考…...

gitlab登录出现的Invalid login or password问题

前提 我是在一个项目里创建的gitlab账号&#xff0c;想在别的项目里登录或者官网登录发现怎么都登陆不上 原因 在GitLab中&#xff0c;有两种不同的账号类型&#xff1a;项目账号和个人账号&#xff08;官网账号&#xff09;。 项目账号&#xff1a;项目账号是在特定GitLab…...

git本地创建分支并推送到远程

1. 创建本地分支并切换到该分支 比如我创建dev分支。git checkout -b相当于把两条命令git branch 分支名、git checkout分支名合成一条&#xff0c;来实现一条命令新建分支切换分支。 git checkout -b dev 2. 将dev分支推送到远程 -u参数与--set-upstream这一串是一个意思&am…...

手机待办事项app哪个好?

手机是日常很多人随身携带的设备&#xff0c;手机除了拥有通讯功能外&#xff0c;还能帮助大家高效管理日常工作&#xff0c;借助手机上的待办事项提醒APP可以快速地帮助大家规划日常事务&#xff0c;提高工作的效率。 过去&#xff0c;我也曾经在寻找一款能够将工作任务清晰罗…...

容器运行elasticsearch安装ik分词非root权限安装报错问题

有些应用默认不允许root用户运行&#xff0c;来确保应用的安全性&#xff0c;这也会导致我们使用docker run后一些操作问题&#xff0c;用es安装ik分词器举例&#xff08;es版本8.9.0&#xff0c;analysis-ik版本8.9.0&#xff09; 1. 容器启动elasticsearch 如挂载方式&…...

UE4游戏客户端开发进阶学习指南

前言 两年多前写过一篇入门指南&#xff0c;教大家在短时间内快速入门UE4的使用&#xff0c;在知乎被很多人收藏了。如今鸡佬使用UE快三年了&#xff0c;是时候更新一下进阶版本的学习指南。本文对于读者的要求&#xff1a; 有一定的C基础已经入门UE&#xff0c;能够用蓝图和…...

23-Oracle 23 ai 区块链表(Blockchain Table)

小伙伴有没有在金融强合规的领域中遇见&#xff0c;必须要保持数据不可变&#xff0c;管理员都无法修改和留痕的要求。比如医疗的电子病历中&#xff0c;影像检查检验结果不可篡改行的&#xff0c;药品追溯过程中数据只可插入无法删除的特性需求&#xff1b;登录日志、修改日志…...

如何在看板中体现优先级变化

在看板中有效体现优先级变化的关键措施包括&#xff1a;采用颜色或标签标识优先级、设置任务排序规则、使用独立的优先级列或泳道、结合自动化规则同步优先级变化、建立定期的优先级审查流程。其中&#xff0c;设置任务排序规则尤其重要&#xff0c;因为它让看板视觉上直观地体…...

c++ 面试题(1)-----深度优先搜索(DFS)实现

操作系统&#xff1a;ubuntu22.04 IDE:Visual Studio Code 编程语言&#xff1a;C11 题目描述 地上有一个 m 行 n 列的方格&#xff0c;从坐标 [0,0] 起始。一个机器人可以从某一格移动到上下左右四个格子&#xff0c;但不能进入行坐标和列坐标的数位之和大于 k 的格子。 例…...

今日学习:Spring线程池|并发修改异常|链路丢失|登录续期|VIP过期策略|数值类缓存

文章目录 优雅版线程池ThreadPoolTaskExecutor和ThreadPoolTaskExecutor的装饰器并发修改异常并发修改异常简介实现机制设计原因及意义 使用线程池造成的链路丢失问题线程池导致的链路丢失问题发生原因 常见解决方法更好的解决方法设计精妙之处 登录续期登录续期常见实现方式特…...

IP如何挑?2025年海外专线IP如何购买?

你花了时间和预算买了IP&#xff0c;结果IP质量不佳&#xff0c;项目效率低下不说&#xff0c;还可能带来莫名的网络问题&#xff0c;是不是太闹心了&#xff1f;尤其是在面对海外专线IP时&#xff0c;到底怎么才能买到适合自己的呢&#xff1f;所以&#xff0c;挑IP绝对是个技…...

【笔记】WSL 中 Rust 安装与测试完整记录

#工作记录 WSL 中 Rust 安装与测试完整记录 1. 运行环境 系统&#xff1a;Ubuntu 24.04 LTS (WSL2)架构&#xff1a;x86_64 (GNU/Linux)Rust 版本&#xff1a;rustc 1.87.0 (2025-05-09)Cargo 版本&#xff1a;cargo 1.87.0 (2025-05-06) 2. 安装 Rust 2.1 使用 Rust 官方安…...

Git 3天2K星标:Datawhale 的 Happy-LLM 项目介绍(附教程)

引言 在人工智能飞速发展的今天&#xff0c;大语言模型&#xff08;Large Language Models, LLMs&#xff09;已成为技术领域的焦点。从智能写作到代码生成&#xff0c;LLM 的应用场景不断扩展&#xff0c;深刻改变了我们的工作和生活方式。然而&#xff0c;理解这些模型的内部…...

【Android】Android 开发 ADB 常用指令

查看当前连接的设备 adb devices 连接设备 adb connect 设备IP 断开已连接的设备 adb disconnect 设备IP 安装应用 adb install 安装包的路径 卸载应用 adb uninstall 应用包名 查看已安装的应用包名 adb shell pm list packages 查看已安装的第三方应用包名 adb shell pm list…...

从“安全密码”到测试体系:Gitee Test 赋能关键领域软件质量保障

关键领域软件测试的"安全密码"&#xff1a;Gitee Test如何破解行业痛点 在数字化浪潮席卷全球的今天&#xff0c;软件系统已成为国家关键领域的"神经中枢"。从国防军工到能源电力&#xff0c;从金融交易到交通管控&#xff0c;这些关乎国计民生的关键领域…...

数学建模-滑翔伞伞翼面积的设计,运动状态计算和优化 !

我们考虑滑翔伞的伞翼面积设计问题以及运动状态描述。滑翔伞的性能主要取决于伞翼面积、气动特性以及飞行员的重量。我们的目标是建立数学模型来描述滑翔伞的运动状态,并优化伞翼面积的设计。 一、问题分析 滑翔伞在飞行过程中受到重力、升力和阻力的作用。升力和阻力与伞翼面…...