Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >HTML 转原生 HTN 项目开发记录

HTML 转原生 HTN 项目开发记录

作者头像
用户7451029
发布于 2020-06-16 09:04:20
发布于 2020-06-16 09:04:20
92700
代码可运行
举报
文章被收录于专栏:戴铭的博客戴铭的博客
运行总次数:0
代码可运行

前言

本文主要是记录 HTN 项目开发的过程。关于这个项目先前在 Swift 开发者大会上我曾经演示过,不过当时项目结构不完善,不易扩展,也没有按照标准来。所以这段时间,我研究了下 W3C 的标准和 WebKit 的一些实现,对于这段时间的研究也写了篇文章深入剖析 WebKit。重构了下这个项目,我可以先说下已经完成的部分,最后列下后面的规划。项目已经放到了 Github 上:https://github.com/ming1016/HTN 后面可以对着代码看。

项目使用介绍

通过解析 html 生成 DOM 树,解析 CSS,生成渲染树,计算布局,最终生成原生 Textrue 代码。下面代码可以看到完整的过程的各个方法。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
let treeBuilder = HTMLTreeBuilder(htmlStr) //htmlStr 就是 需要转的 html 代码
_ = treeBuilder.parse() //解析 html 生成 DOM 树
let cssStyle = CSSParser(treeBuilder.doc.allStyle()).parseSheet() //解析 CSS
let document = StyleResolver().resolver(treeBuilder.doc, styleSheet: cssStyle) //生成渲染树

//转 Textrue
let layoutElement = LayoutElement().createRenderer(doc: document) //计算布局
_ = HTMLToTexture(nodeName:"Flexbox").converter(layoutElement); //生成原生 Textrue 代码

比如有下面的 html

在浏览器里显示是这样

通过 HTN 生成的原生代码

在 iPhone X 模拟器的效果如下

下面详细介绍下具体的实现关键点

HTML

这部分最关键的部分是在 HTML/HTMLTokenizer.swift 里。首先会根据 W3C 里的 Tokenization 的标准 https://dev.w3.org/html5/spec-preview/tokenization.html 来定义一个状态的枚举,如下,可以目前完成这些状态的情况

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//枚举
    enum S: HTNStateType {
        case DataState //half done
        case CharacterReferenceInDataState
        case RCDATAState
        case CharacterReferenceInRCDATAState
        case RAWTEXTState
        case ScriptDataState
        case PLAINTEXTState
        case TagOpenState //half done
        case EndTagOpenState
        case TagNameState //half done
        
        case RCDATALessThanSignState
        case RCDATAEndTagOpenState
        case RCDATAEndTagNameState
        
        case RAWTEXTLessThanSignState
        case RAWTEXTEndTagOpenState
        case RAWTEXTEndTagNameState
        
        //Script
        case ScriptDataLessThanSignState
        case ScriptDataEndTagOpenState
        case ScriptDataEndTagNameState
        case ScriptDataEscapeStartState
        case ScriptDataEscapeStartDashState
        case ScriptDataEscapedState
        case ScriptDataEscapedDashState
        case ScriptDataEscapedDashDashState
        case ScriptDataEscapedLessThanSignState
        case ScriptDataEscapedEndTagOpenState
        case ScriptDataEscapedEndTagNameState
        case ScriptDataDoubleEscapeStartState
        case ScriptDataDoubleEscapedState
        case ScriptDataDoubleEscapedDashState
        case ScriptDataDoubleEscapedDashDashState
        case ScriptDataDoubleEscapedLessThanSignState
        case ScriptDataDoubleEscapeEndState
        
        //Tag
        case BeforeAttributeNameState
        case AttributeNameState //half done
        case AfterAttributeNameState
        case BeforeAttributeValueState
        case AttributeValueDoubleQuotedState //half done
        case AttributeValueSingleQuotedState
        case AttributeValueUnquotedState
        case CharacterReferenceInAttributeValueState
        case AfterAttributeValueQuotedState //half done
        case SelfClosingStartTagState
        case BogusCommentState
        case ContinueBogusCommentState
        case MarkupDeclarationOpenState //half done
        
        //Comment
        case CommentStartState //half done
        case CommentStartDashState
        case CommentState
        case CommentEndDashState //half done
        case CommentEndState //half done
        case CommentEndBangState
        
        //DOCTYPE
        case DOCTYPEState //half done
        case BeforeDOCTYPENameState //half done
        case DOCTYPENameState
        case AfterDOCTYPENameState //half done
        case AfterDOCTYPEPublicKeywordState //half done
        case BeforeDOCTYPEPublicIdentifierState //half done
        case DOCTYPEPublicIdentifierDoubleQuotedState //half done
        case DOCTYPEPublicIdentifierSingleQuotedState
        case AfterDOCTYPEPublicIdentifierState //half done
        case BetweenDOCTYPEPublicAndSystemIdentifiersState
        case AfterDOCTYPESystemKeywordState
        case BeforeDOCTYPESystemIdentifierState
        case DOCTYPESystemIdentifierDoubleQuotedState
        case DOCTYPESystemIdentifierSingleQuotedState
        case AfterDOCTYPESystemIdentifierState
        case BogusDOCTYPEState
        
        case CDATASectionState
        case CDATASectionRightSquareBracketState
        case CDATASectionDoubleRightSquareBracketState
    }

处理这些状态采用的是状态机原理。根据状态机数学模型提取出需要的状态集合,事件集合,事件集合在这里是所遇字符的集合做了一个状态机,具体实现在 HTNFundation/HTNStateMachine.swift。状态转移函数我定义的是 func listen(_ event: E, transit fromState: S, to toState: S, callback: @escaping (HTNTransition) -> Void) ,这里的 block 是在状态转移时需要做的事情定义 。为了能够减少状态转移太多太碎,也多写了几个函数来处理比如一组来源状态到同一个转移状态和针对某些事件状态不变的函数。

有了状态机后面的处理就会很方便,这里的事件就是一个一个的字符,不同字符在不同的状态下的处理。下面可以举个多状态转同一状态的实现,具体代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
let anglebracketRightEventFromStatesArray = [S.DOCTYPEState,
                                             S.CommentEndState,
                                             S.TagOpenState,
                                             S.EndTagOpenState,
                                             S.AfterAttributeValueQuotedState,
                                             S.BeforeDOCTYPENameState,
                                             S.AfterDOCTYPEPublicIdentifierState]
stateMachine.listen(E.AngleBracketRight, transit: anglebracketRightEventFromStatesArray, to: S.DataState) { (t) in
    if t.fromState == S.TagOpenState || t.fromState == S.EndTagOpenState {
        if self._bufferStr.count > 0 {
            self._bufferToken.data = self._bufferStr.lowercased()
        }
    }
    self.addHTMLToken()
    self.advanceIndexAndResetCurrentStr()
}

W3C 也定义每个状态的处理,非常详细完整,WebKit 基本把这些定义都实现了,HTN 目前只实现了能够满足构建 DOM 树的部分。W3C 的定义可以举个 StartTags 的状态如下图

在进入构建 DOM 树之前我们需要设计一些类和结构来记录我们的内容,这里采用了 WebKit 类似的类结构设计,下图是 WebKit 的 DOM 树相关的类设计图

完成了这些状态处理,接下来就可以根据这些 HTMLToken 来组装我们的 DOM 树了。这部分的实现在 HTML/HTMLTreeBuilder.swift 里。构建 DOM 树同样使用了先前的写的状态机,只是这里的状态集和事件集不同而已,W3C 也定义一些状态可以用

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
enum S: HTNStateType {
    case InitialModeState
    case BeforeHTMLState
    case BeforeHeadState
    case InHeadState
    case AfterHeadState
    case InBodyState
    case AfterBodyState
    case AfterAfterBodyState
}

从名字就能很方便的看出每个状态的意思。这里的事件集使用的是 HTMLToken 里的类型,根据不同类型来放置到合适的位置。树的父级子级是通过定义的一个堆栈来控制,具体构建实现可以看 func parse() -> [HTMLToken] 这个函数。

CSS

解析 CSS 需要先了解下 CSS 的 BNF,它的定义如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
ruleset
  : selector [ ',' S* selector ]*
    '{' S* declaration [ ';' S* declaration ]* '}' S*
  ;
selector
  : simple_selector [ combinator selector | S+ [ combinator selector ] ]
  ;
simple_selector
  : element_name [ HASH | class | attrib | pseudo ]*
  | [ HASH | class | attrib | pseudo ]+
  ;
class
  : '.' IDENT
  ;
element_name
  : IDENT | '*'
  ;
attrib
  : '[' S* IDENT S* [ [ '=' | INCLUDES | DASHMATCH ] S*
    [ IDENT | STRING ] S* ] ']'
  ;
pseudo
  : ':' [ IDENT | FUNCTION S* [IDENT S*] ')' ]
  ;

根据 BNF 来确定状态集和事件集。下面是我定义的状态集和事件集

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
enum S: HTNStateType {
    case UnknownState     //
    case SelectorState    // 比如 div p, #id
    case PropertyKeyState   // 属性的 key
    case PropertyValueState // 属性的 value
    
    //TODO:以下后期支持,优先级2
    case PseudoClass      // :nth-child(2)
    case PseudoElement    // ::first-line
    
    //TODO:以下后期支持,优先级3
    case PagePseudoClass
    case AttributeExact   // E[attr]
    case AttributeSet     // E[attr|="value"]
    case AttributeHyphen  // E[attr~="value"]
    case AttributeList    // E[attr*="value"]
    case AttributeContain // E[attr^="value"]
    case AttributeBegin   // E[attr$="value"]
    case AttributeEnd
    //TODO:@media 这类 @规则 ,后期支持,优先级4
}
enum E: HTNEventType {
    case SpaceEvent   //空格
    case CommaEvent   // ,
    case DotEvent     // .
    case HashTagEvent // #
    case BraceLeftEvent  // {
    case BraceRightEvent // }
    case ColonEvent // :
    case SemicolonEvent  // ;
}

同样在状态的处理过程中也需要一个合理的类结构关系设计来满足,这里也参考了 WebKit 里的设计,如下:

布局

布局处理目前 HTN 主要是将样式属性和 DOM 树里的 Element 对应上。具体实现是在 Layout/StyleResolver.swift 里。思路是先将所有 CSSRule 和对应的 CSSSelector 做好映射,接着在递归 DOM 树的过程中与每个 Element 对应上。主要代码实现如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public func resolver(_ doc:Document, styleSheet:CSSStyleSheet) -> Document{
    //样式映射表
    //这种结构能够支持多级 Selector
    var matchMap = [String:[String:[String:String]]]()
    for rule in styleSheet.ruleList {
        for selector in rule.selectorList {
            guard let matchLast = selector.matchList.last else {
                continue
            }
            var matchDic = matchMap[matchLast]
            if matchDic == nil {
                matchDic = [String:[String:String]]()
                matchMap[matchLast] = matchDic
            }
            
            //这里可以按照后加入 rulelist 的优先级更高的原则进行覆盖操作
            if matchMap[matchLast]![selector.identifier] == nil {
                matchMap[matchLast]![selector.identifier] = [String:String]()
            }
            for a in rule.propertyList {
                matchMap[matchLast]![selector.identifier]![a.key] = a.value
            }
        }
    }
    for elm in doc.children {
        self.attach(elm as! Element, matchMap: matchMap)
    }
    
    return doc
}
//递归将样式属性都加上
func attach(_ element:Element, matchMap:[String:[String:[String:String]]]) {
    guard let token = element.startTagToken else {
        return
    }
    if matchMap[token.data] != nil {
        //TODO: 还不支持 selector 里多个标签名组合,后期加上
        addProperty(token.data, matchMap: matchMap, element: element)
    }
    
    //增加 property 通过处理 token 里的属性列表里的 class 和 id 在 matchMap 里找
    for attr in token.attributeList {
        if attr.name == "class" {
            addProperty("." + attr.value.lowercased(), matchMap: matchMap, element: element)
        }
        if attr.name == "id" {
            addProperty("#" + attr.value.lowercased(), matchMap: matchMap, element: element)
        }
    }
    
    if element.children.count > 0 {
        for element in element.children {
            self.attach(element as! Element, matchMap: matchMap)
        }
    }
}

func addProperty(_ key:String, matchMap:[String:[String:[String:String]]], element:Element) {
    guard let dic = matchMap[key] else {
        return
    }
    for aDic in dic {
        var selectorArr = aDic.key.components(separatedBy: " ")
        if selectorArr.count > 1 {
            //带多个 selector 的情况
            selectorArr.removeLast()
            if !recursionSelectorMatch(selectorArr, parentElement: element.parent as! Element) {
                continue
            }
        }
        guard let ruleDic = dic[aDic.key] else {
            continue
        }
        //将属性加入 element 的属性列表里
        for property in ruleDic {
            element.propertyMap[property.key] = property.value
        }
    }
    
}

这里通过 recursionSelectorMatch 来按照 CSS Selector 从右到左的递归出是否匹配路径,具体实现代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//递归找出匹配的多路径
func recursionSelectorMatch(_ selectors:[String], parentElement:Element) -> Bool {
    var selectorArr = selectors
    guard var last = selectorArr.last else {
        //表示全匹配了
        return true
    }
    guard let parent = parentElement.parent else {
        return false
    }
    
    var isMatch = false
    
    if last.hasPrefix(".") {
        last.characters.removeFirst()
        //TODO:这里还需要考虑attribute 空格多个 class 名的情况
        guard let startTagToken = parentElement.startTagToken else {
            return false
        }
        if startTagToken.attributeDic["class"] == last {
            isMatch = true
        }
    } else if last.hasPrefix("#") {
        last.characters.removeFirst()
        guard let startTagToken = parentElement.startTagToken else {
            return false
        }
        if startTagToken.attributeDic["id"] == last {
            isMatch = true
        }
    } else {
        guard let startTagToken = parentElement.startTagToken else {
            return false
        }
        if startTagToken.data == last {
            isMatch = true
        }
    }
    
    if isMatch {
        //匹配到会继续往前去匹配
        selectorArr.removeLast()
    }
    return recursionSelectorMatch(selectorArr, parentElement: parent as! Element)
    
}

转原生

已完成一部分简单布局属性转换 Texture 原生代码。具体实现部分可以参看 HTMLToTexture.swift 文件。

已完成

  • 解析 HTML 构建 DOM 树,解析 CSS 构建渲染树
  • CSS Selector 的 Tag 路径支持,Tag 和 class,id 的组合选择。
  • flexbox 属性,margin 和 padding 映射 Texture 原生代码

规划

  • 支持图片标签,支持 CSS background 背景属性
  • html 的 class 属性还不支持空格多个 class 名
  • text-transform 属性的支持
  • em 转 pt,em 是相对父元素值的乘积值。
  • 支持CSS选择器的 :before 和 :after
  • HTN 的 Objective-C 版。
  • 支持转 Objective-C 的原生代码。
  • 解析转换器内嵌在应用程序内部,支持服务器下发 h5 代码转换。
  • 应用内转换时的缓存的处理,将render树结构体进行缓存的处理
  • HTML 内 JS 解析,支持逻辑控制 HTML
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2017-10-16,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
掌握CSS中的常见选择器
在CSS(层叠样式表)中,选择器是一种强大的工具,允许开发者根据不同的条件选择HTML元素,并对其应用样式。掌握各种选择器是成为一名优秀的前端开发者的必备技能之一。在本文中,我们将介绍CSS中一些常见的选择器,以及它们的用法和示例。
人不走空
2024/03/01
4910
读 Zepto 源码之神奇的 $
根据文章内容撰写摘要总结。
对角另一面
2017/12/27
8530
Java 根据 HTML 生成 DOM 树
訾博ZiBo
2025/01/06
1170
深入剖析 WebKit
1990年 Berners-Lee 发明了 WorldWideWeb 浏览器,后改名 Nexus,在1991年公布了源码。
用户7451029
2020/06/16
3.7K0
深入剖析 WebKit
[WebKit] JavaScriptCore解析--基础篇(一)字节码的生成及抽象语法树的构建详情分析
看到HorkeyChen写的文章《[WebKit] JavaScriptCore解析--基础篇(三)从脚本代码到JIT编译的代码实现》,写的很好,深受启发。想补充一些Horkey没有写到的细节比如字节
程序员互动联盟
2018/03/12
1.6K0
[WebKit] JavaScriptCore解析--基础篇(一)字节码的生成及抽象语法树的构建详情分析
CSS 选择器 — 重学前端
在之前的 《实战中学习浏览器工作原理》中也接触过选择器的优先级的概念了。这里我们深入了解一下选择器优先级的概念。
三钻
2020/10/29
8660
CSS 选择器 — 重学前端
zepto的构造器$
fragmentRE是一个匹配普通标签<xxx>的表达式,关键是zepto.fragment()函数。
菜的黑人牙膏
2019/01/21
6030
浏览器内核之 HTML 解释器和 DOM 模型
此文章是我最近在看的【WebKit 技术内幕】一书的一些理解和做的笔记。 而【WebKit 技术内幕】是基于 WebKit 的 Chromium 项目的讲解。
夜尽天明
2019/11/13
1K0
浏览器内核之 HTML 解释器和 DOM 模型
Swift 项目中涉及到 JSONDecoder,网络请求,泛型协议式编程的一些记录和想法
最近项目开发一直在使用 swift,因为 HTN 项目最近会有另外一位同事加入,所以打算对最近涉及到的一些技术和自己的一些想法做个记录,同时也能够方便同事熟悉代码。
用户7451029
2020/06/16
6.9K0
实战中学习浏览器工作原理 — HTML 解析与 CSS 计算
上一部分我们完成了从 HTTP 发送 Request,到接收到 Response,并且把 Response 中的文本都解析出来。
三钻
2020/10/29
1.6K0
实战中学习浏览器工作原理 — HTML 解析与 CSS 计算
jQuery介绍,一篇就够了!
老猫-Leo
2023/12/11
2820
浏览器渲染原理
一个是HTML/SVG/XHTML,事实上,Webkit有三个C++的类对应这三类文档。解析这三种文件会产生一个DOM Tree。 CSS,解析CSS会产生CSS规则树。 Javascript,脚本,主要是通过DOM API和CSSOM API来操作DOM Tree和CSS Rule Tree.
IMWeb前端团队
2019/12/04
5100
浏览器渲染原理
02.爬虫基础知识与简易爬虫实现
CSS 基础语法 CSS规则:选择器,以及一条或多条声明。 selector {declaration1; ...; desclarationN} 每条声明是由一个属性和一个值组成 property: value 例子 h1 {color: red; font-size: 14px} ---- 元素选择器 直接选择文档元素 比如 head,p <html> <head> <style type="text/css"> h1 {text-decoration: overline} h2
qubianzhong
2018/08/08
4670
02.爬虫基础知识与简易爬虫实现
揭秘字节码到像素的一生!Chromium 渲染流水线
点个关注👆跟腾讯工程师学技术 导语| 本文将深入介绍 Chromium 内核组成结构,并以渲染流水线为主线,从接收字节码开始,按渲染流程来一步一步分析这个字节码究竟是如何转变成屏幕上的像素点的。 现代浏览器架构 在开始介绍渲染流水线之前,我们需要先介绍一下 Chromium 的浏览器架构与 Chromium 的进程模型作为前置知识。 一、两个公式 公式 1:浏览器 = 浏览器内核 + 服务 Safari = WebKit + 其他组件、库、服务 Chrome = Chromium + Google
腾讯云开发者
2022/12/06
1.4K0
揭秘字节码到像素的一生!Chromium 渲染流水线
前端开发知识汇总--HTML、CSS
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/j_bleach/article/details/67063297
j_bleach
2019/07/02
7630
前端开发知识汇总--HTML、CSS
Jquery基础之DOM操作
Dom是Document Object Model的缩写,意思是文档对象模型。DOM是一种与浏览器、平台、语言无关的接口,使用该接口可以轻松访问页面中所有的标准组件。DOM操作可以分为三个方面即DOM Core(核心)、HTM-DOM和CSS-DOM。
张哥编程
2024/12/19
1840
Jquery基础之DOM操作
整理常见 DOM 操作
整理常见 DOM 操作 ⭐️ 更多前端技术和知识点,搜索订阅号 JS 菌 订阅 框架用多了,你还记得那些操作 DOM 的纯 JS 语法吗?看看这篇文章,来回顾一下~ ? 操作 className ad
JS菌
2019/05/06
1.1K0
【小程序开发必读】怎样写出一手好的小程序之多端架构篇
为了大家能更好的开发出一些高质量、高性能的小程序,这里带大家理解一下小程序在不同端上架构体系的区分,更好的让大家理解小程序一些特有的代码写作方式。
极乐君
2019/05/15
1.6K0
【小程序开发必读】怎样写出一手好的小程序之多端架构篇
模拟mui框架编码
//调用方法 /* 1、tm.os.ios/tm.os.android/tm.os.versions().webKit //表示安卓设备/ios设备/webKit内核 */ var tm = (function(document) { "use strict"; var readyRE = /complete|loaded|interactive/, //complete 可返回浏览器是否已完成对图像的加载。 idSelectorRE = /^#([\w-]+)$/,
White feathe
2021/12/08
1.3K0
让我们来构建一个浏览器引擎吧
前端有一个经典的面试题:在浏览器地址栏输入URL到最终呈现出页面,中间发生了什么?
五月君
2021/04/22
1.3K0
相关推荐
掌握CSS中的常见选择器
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验