解析html文件(htmlcss网页设计代码案例)

版权声明

很早的时候有个框架叫 Jasonette,火了一阵子,打出来的宣传语是用 JSON 写出纯 native 的 app。

前一阵子,天猫又开源了跨多个平台的 Tangram,一套通用的 UI 解决方案,仔细阅读文档我们会发现,他们也是在用 JSON 来实现这套七巧板布局。一套灵活的跨平台的 UI 解决方案。

Jasonette 的牛皮其实有点大,很多人看到动态用 JSON 写出纯 native 的 App,就很激动,仿佛客户端也能有 H5 那样的能力,但其实他只是 focus 在解决 app 中的界面的问题。Tangram 的定位就很精准了,是一套为业务出发的通用跨平台 UI 解决方案,把布局渲染性能与多段一致性考虑在框架内的 UI 框架。

这二者有个共同点都是用 JSON 来描述界面与内容,从而用 native 进行呈现,JSON 这种数据是一种天然便于下发与动态更新的数据,因此这些其实都能让客户端做到类似 H5 网页一样的赶脚。虽然没有使用 WebView,但他们的设计思路和网页技术的发展历史如出一辙,因此 @响马大叔说过”这其实是最纯正的网页技术,虽然他是 native 的”。

顺着这个话题继续问几个问题:

DSL,为什么 Jasonette 与 Tangram 都是用 JSON?

布局排版,为什么 Jasonette 写出来的 JSON 有些属性看着很像 css?padding & align(拿 Jasonette 举例)

渲染,Jasonette 调用 UIKit 进行渲染,H5 用 WebView 渲染,所以 Jasonette 就叫 native?

从 DSL 说起

DSL 是 Domain Specific Language 的缩写,意思就是特定领域下的语言,与 DSL 对应的就是通用编程语言,比如 Java/C/C 这种。换个通俗易懂的说法,DSL 是为了解决某些特定场景下的任务而专门设计的语言。

举几个很著名的 DSL 的例子:

正则表达式,通过一些规定好的符号和组合规则,通过正则表达式引擎来实现字符串的匹配。

HTML&CSS,虽然写的是类似 XML 或者 .{} 一样的字符规则,但是最终都会被浏览器内核转变成 Dom 树,从而渲染到 Webview 上。

SQL,虽然是一些诸如 create select insert 这种单词后面跟上参数,这样的语句实现了对数据库的增删改查一系列程序工作。

计算机领域需要用代码解决很多专业问题,往往需要同时具备编码能力以及专业领域的能力,为了提高工作于生产效率,需要把一些复杂但更偏向专业领域的处理,以一种更简单,更容易学习的语言或者规范(即 DSL),抽象提供给领域专家,交给不懂编码的领域专家编写。

然后代码编程能力者通过读取解析自己定制出来的这些语言规范,来领会领域专家的意图,最终转化成真正的通用编程语言代码实现,接入底层代码框架,从而实现让领域专家只需要学习更简单的 DSL,就能影响代码程序的最终结果。

(虽然 DSL 的原始定义是为了非编程的专业领域人才使用,但到后来直接交给程序员使用,但能大幅度提高程序员编写效率的非通用语言,也被当做是 DSL 的一种)

DSL 在设计上的应用

设计师会设计出很多精美的界面,最后交给程序员去用代码编写成漂亮的网页或者 App。每个界面如果纯用代码编写,都会面临大量的代码工作量,这里面可能更多的是一些重复的机械性的代码工作。诸如:某个元素设置长宽,设置居中,设置文字,设置距离上个元素 XX 像素,N 个元素一起纵向,横向平均排列等等。对于代码实现界面开发来说,程序员需要编写一系列诸如:setFrame,setTitle,setColor,addSubview 这样的代码,一边写这样的代码,一边查阅设计师给出的各种标注。

为了提高工作效率,如果能把一些设计师产出的长宽,色值,文字,居中,距上等设计元数据(设计的标注信息等),以一种约定的简洁的语言规则(即 DSL)输入给程序代码,由程序和代码自动的分析和处理,从而生成真正的界面开发代码 setFrame,setTitle,setColor,addSubview, 这样就可以大幅度的减少代码量与工作量,程序员来写这种简洁的语法规则会更快更高效,甚至可以把这种简洁的语法规则教会设计师,让设计师有能力直接写出 DSL,然后输入给底层程序,这样界面就自然完成。

HTML/CSS,是网页开发普遍使用的,他们也是一种 DSL,你写出的每一个 HTML 的 DIV 以及 CSS 的属性样式,最后都不是通过.html .css 文件渲染到屏幕到浏览器上的,都是通过浏览器内核最后调用 OpenGL,C 代码渲染上去的。从这个层面讲,Jasonette 客户端框架用的 JSON,native 客户端开发的 XIB,与网页浏览器的 HTML/CSS 是一回事。

Jasonette 里的 JSON 如何工作

扯淡了这么多,亲自看看 Jasonette 源码是如何执行 DSL 的。

{
“$jason”: {
“head”: {
“title”: “{ ??????????????}”,
“actions”: {
“$foreground”: {
“type”: “$reload”
},
“$pull”: {
“type”: “$reload”
}
}
},
“body”: {
“header”: {
“style”: {
“background”: “#ffffff”
}
},
“style”: {
“background”: “#ffffff”,
“border”: “none”
},
“sections”: [
{
“items”: [
{
“type”: “vertical”,
“style”: {
“padding”: “30”,
“spacing”: “20”,
“align”: “center”
},
“components”: [
{
“type”: “label”,
“text”: “It’s ALIVE!”,
“style”: {
“align”: “center”,
“font”: “Courier-Bold”,
“size”: “18”
}
},
{
…… 省略
}
]
},{
…… 省略
},
{
“type”: “label”,
“style”: {
“align”: “right”,
“padding”: “10”,
“color”: “#000000”,
“font”: “HelveticaNeue”,
“size”: “12”
},
“text”: “Watch the tutorial video”,
“href”: {
“url”: “https://www.youtube.com/watch?v=hfevBAAfCMQ”,
“view”: “Web”
}
}
]
}
]
}
}
}

这是 demo 里的页面 JSON 代码,你会看到很多很像网页开发的东西,head,body,padding,align 等等,是不是觉得和 CSS 很像。

Application didFinishLaunchingWithOptions

程序初始化,触发 [[Jason client] start:nil],初始化 Jason,在这个 start 里面,会创建 JasonViewController,并且给这个 VC 设置 rootUrl,设置这个 VC 作为 Window 的 Key,从而进行 App 展现

VC viewWillAppear

这个 KeyVC,当 viewWillAppear 的时候,触发 [[Jason client] attach:self],这个函数内会调用 [self reload] 来进行网络数据拉取,刚刚说的 rootUrl 其实是一个 JSON 网络文件(也可以设置成 bundle 内文件),换句话说这个 vc 的 JSON 文件可以每次从网络上拉取最新的 JSON 文件来实现动态更新的(跟网页实际上是一样的),这个过程就是触发网络框架 AF 去拉取最新的 JSON

AFNetworking download

在网络数据拉取回来后,会经过一系列的处理,包括请求异步的其他相关 JSON(像不像异步请求其他 css),把请求到的 JSON 字典经过 JasonParser 这个类的一些其他处理最后生成最终的 Dom 字典(Dom 这个词写在 Jason drawViewFromJason 的源码里,源码就将这个数据字典的变量起名叫 dom,可见他做的和网页工作原理是一个思路)

Jason drawViewFromJason 进行主线程渲染

找到 Jason 类的 drawViewFromJason: 函数,这才是我们 DSL 之所以能渲染成界面的最重要的一步,前面都是一直在下载 DSL,处理 DSL,结果就是 JSON 生成了最终需要的元数据字典–Dom 字典,这一步就是将 DSL 转变成 App 界面

Dom 字典生成界面的过程

简单的看看这个流程都分别依次调用了哪些函数,不一一讲解了,最后我们挑最有代表的进行说明。

[Jason drawViewFromJason:DomDic]

[JasonViewController reload:DomDic]

Set Stylesheet //CSS

[JasonViewController setupSections:DomDic]

[JasonViewController setupLayers:DomDic]

setupSections 与 setupLayers 基本上涵盖了页面主元素的所有渲染方式。

先以简单的 setupLayers 的代码逻辑举例,先按着约定的标签从 Dom 字典中有目的的读取需要的数据字段 Layers,循环遍历 Layers 字段数组下的所有数据,每一次都先判断子节点的 Type 属性,如果 Type 写了 Image,就会创建 UIImageView,如果 Type 写了 Label,就会创建 UILabel,根据子节点其他属性一一设置不同的 UIView 的属性,最后 AddSubview 到界面上。(我会略过大量实际代码,以伪代码形式进行说明,实际代码可以看源码查看)

NSArray *layer_items = body[@”layers”];
NSMutableArray *layers = [[NSMutableArray alloc] init];
// 循环遍历 Dom 树下的 layer 字段
if(layer_items && layer_items.count > 0){
for(int i = 0 ; i < layer_items.count ; i ){
NSDictionary *layer = layer_items[i];
layer = [self applyStylesheet:layer];
// 设置 Css

// 判断 type 字段是否为 image,是否有 image url
if(layer[@”type”] && [layer[@”type”] isEqualToString:@”image”] && layer[@”url”]){

//NEW 一个 UIImageView
// 设置 UIImageView 的 style
// 设置 UIImageView 的 image URL
// 将 UIImageView Add subview
// 异步拉取图片回来后,通过 style,运算 UIImageView 的 frame

}
// 判断 type 字段是否为 label,是否有 text
else if(layer[@”type”] && [layer[@”type”] isEqualToString:@”label”] && layer[@”text”]){
//NEW 一个 TTTAttributedLabel

// 设置 TTTAttributedLabel 的 style
// 设置文本
//addSubview
}
}
}

再说说 setupSections,他其实充分利用了 tableview 的能力,首先将 Dom 字典下的 sections 字段进行保存与整理,然后并不立刻进行渲染,而是直接调用 [UITableview reloadData],触发 heightForRowAtIndexPath 与 cellForRowAtIndexPath。(我会略过大量实际代码,以伪代码形式进行说明,实际代码可以看源码查看)

heightForRowAtIndexPath 获取 cell 高度

// 取出 indexPath.section 对应的 dom 节点数据
NSArray *rows = [[self.sections objectAtIndex:indexPath.section] valueForKey:@”items”];
// 取出 indexPath.row 对应的 dom 节点数据
NSDictionary *item = [rows objectAtIndex:indexPath.row];
// 取出样式属性
item = [JasonComponentFactory applyStylesheet:item];
NSDictionary *style = item[@”style”];
// 通过 JasonHelper 传入 style[@”height”] 样式属性计算宽高
// 一些样式算法算出
return [JasonHelper pixelsInDirection:@”vertical” fromExpression:style[@”height”]];

cellForRowAtIndexPath 获取 cell

NSDictionary *s = [self.sections objectAtIndex:indexPath.section];
NSArray *rows = s[@”items”];
// 获取对应的 Dom 节点数据
iNSDictionary *item = [rows objectAtIndex:indexPath.row];
// 渲染竖着滑的 CELL
// 只支持 SWTableViewCell 这一种客户端预先写好的这种通用 cell
// 支持 Dom 节点循环内嵌 stackview,按着内嵌形式,横竖布局都支持
//stackview 内的子元素通过 JasonComponentFactory 创建对应的 UIKit UIView
// 创建方式如同 layer,判断 type 等于’image’创建 UIImageView,判断等于’text’创建 UILabel
//frame 通过 style 等字段,进行系统 autolayout 计算
return [self getVerticalSectionItem:item forTableView:tableView atIndexPath:indexPath];

上面讲的其实力度很粗,并且很多代码没有详细展开,其实目的是让大家发现,Jasonette 的源码持续在干一件事情:

从 Dom 字典中,读取约定好的固定字段

循环遍历 Dom 字典,遍历所有设计数据

然后用字符串匹配去判断每个节点的 key 与值,指引 OC 代码应该怎么调用

匹配出 label 就创建 UILabel

匹配出 iamge 就创建 UIImageView

匹配出 style 就调用 autolayout 赋值属性进行 autolayout 计算,或者进行自行算法计算。

Jasonette 的 DSL 工作特点就是这样,先从设计师给出的元数据入手,把所有的元数据抽象抽离出来,约定成固定的标签与值,然后客户端一一遍历整个 Dom 元数据的节点,一一解读这些标签与值,走入对应的客户端代码,从而调用对应的客户端代码功能。

客户端的这套框架写完之后,以后在写全新的界面,其实是无需再重复写一套客户端代码,而是直接写全新的 DSL 也就是 Jasonette 的 JSON 文件就可以了。

DSL 小结

拿 JSON/HTML/CSS 举例子其实,这些都是一种外部 DSL,与之对应的还有某些语言支持的内部 DSL,这里也就不展开了。

XML DSL很多常见的 XML 配置文件实际上就是 DSL,但不是所有的配置文件都是 DSL。比如“属性列表”和 DSL 是不同的,那只是一份简单的“键 – 值对”列表,可能再加上分类。XML 不是编程语言,是一种没有语义的语法结构。XML 是 DSL 的承载语法,但是它又引入了太多语法噪音—太多的尖括号、引号和斜线,每个嵌套元素都必须有开始标签和结束标签。自定义的外部 DSL 也带来了一个烦恼:它们处理引用、字符转义之类事情的方式总是难以统一。via:http://xfhnever.com/2014/08/08/dsl-implementouter/

所以 DSL 叫特殊领域语言,离开了为某一 DSL 专门开发的语言环境或者代码框架,DSL 是无法运行的,没有效果的,没有正则表达引擎的源码,你写出来的正则表达式没人认识。没有底层数据库框架,sql 语句就只是一行字符串,没法进行数据管理。没有 Jasonette 这个框架,你写出来 JSON 也不可能生成界面,有了 Jasonette 这个框架,你不按着约定的标签写,自己单纯的在 JSON 里凭空创建标签,也是不可能正确生成你想要的东西。

在最后,笔者想说的是,当我们在某一个领域经常需要解决重复性问题时,可以考虑实现一个 DSL 专门用来解决这些类似的问题。via:http://draveness.me/dsl.html布局与排版

既然说到动态界面,那一定得聊屏幕适配,这其实不管是不是动态界面,不管用不用到 DSL,做客户端都要考虑的一点,其实网页在这方面发展的更完善,毕竟客户端的屏幕尺寸就那么几种,就算安卓碎片化,也比不上 PC 电脑上,桌面浏览器用户可以任意伸缩窗口的大小,因此对于在不同尺寸的限定屏幕大小(即排版区域)内,把设计出来的元素以最美观的形式进行展现,这就是布局与排版。

刚才在讲解 DSL,讲解 Jasonette 的时候其实回避一些问题,我们光提到了通过 Dom 的 type 信息,来创建不同的 UIView,但是每一个 UIView 应该摆放在屏幕的什么位置,在哪进行展现,在上面的文章中被一带而过,有的描述,读取 style 字段后分别赋值给对应的 autolayout,有的被我说成了进行一定的算法从而算出高度。这背后其实都是布局与排版的算法。

做 iOS 客户端的同学很多会有感触,早些年的时候写绝对坐标,那时候 iOS 的屏幕尺寸还不是太多,用代码手写 frame 进行元素定位,试想一下如果纯用 frame 进行 app 开发,那么去开发一套对应的 DSL 动态界面其实更容易,我们只需要给每个字典节点,规定上{x:N,y:N,w:N,h:N}的属性,然后在框架里别的跟布局相关的 style 都不需要写了,只需要用 xywh 生成 CGRect,然后调用 setFrame 就好了,想开发出这样一种绝对布局的动态界面框架其实还真是挺简单的。

到后来有了 iPad,有了 iPhone5,有了 iPhone6,6Plus,iOS 的屏幕尺寸变的碎片化,如果继续使用 frame,客户端同学开发工作量会变的异常繁琐,于是在 IOS7 引入了苹果的 autolayout,引入了 VFL 语言 Visual Format Language。其实 VFL 也应该算是一种 DSL 吧,他不是用来绘制出一个个的界面元素,而是用来在绘制前,计算清楚每一个元素在动态的屏幕区域下的最终位置。我们学会了如何写 VFL,或者说我们学会了如何用 masnory 这个框架实现 autolayout,但我们并不需要深入去了解这里面的排版布局算法。

需要记住的一点是,最终渲染一定是通过 frame 去页面上进行绘制,有了明确的坐标才能绘制出 UI,手写 frame 式的绝对布局代码,直接由程序员指定,因此一定是性能开销最小的,可以说没有或者少量的布局运算开销直接进行渲染,但在多屏适配的需求下才引入了一整套庞大的布局算法体系(不一定非得是苹果的 autolayout),引入庞大布局算法的目的是希望根据可排区域动态的计算 frame,但并不代表采用自动布局,就与 frame 无关,自动布局算法只是间接的运算出 frame 再渲染。

布局排版的流程:

RenderTree parse

Jasonette 的方案是

反序列化 JSON,直接生成 Dom 字典

解析 HTML,生成 Dom

解析 CSS,生成 style rules

浏览器内核的方案是

attach Render Tree CSS 与 HTML 挂载到一起

RenderTree layout

从 RenderTree RootNode 遍历

不同节点对应调用不同 layout 算法

运算出每个可显示界面元素的位置信息

RenderTree render

遍历 Tree

渲染

布局排版信息解析浏览器解析 HTML/CSS 生成 RenderTree

将 HTML 文件以字符串的形式输入,经过解析,生成了 Dom 树,Dom 树就好比是 iOS 开发里面页面 View 的层级树,但是每个 View/div 里面并没有 css 信息,只写了每个 div 所对应的 css 的名字。

将 CSS 文件以字符串形式输入,经过解析,得到了一系列不同名字的 style rules,样式规则。

Dom 树上的 div 并不包含样式信息,而是只记录了样式的名字,然后从 style rules 里找到对应名字的具体样式信息,Attech 到一起,生成了 Render Tree 渲染树,此时的渲染树只是 Dom 与 CSS 的合并,他依然不包含真正可以用于渲染的位置信息,因为他还没经过布局排版。

Jasonette 直接生成 RenderTree

网页将 View 的层级,与 View 的样式进行了分离,View 就是 HTML,样式是 CSS,但是 Jasonette 的 JSON 没有做这样的分离,JSON 直接描述的就是 view 与 style 归并到一起的数据,因此在 Jasonette 经过了 parse 解析后直接就拿到了样式与视图的合体结构信息。我们在 JSON 里明显可以看到 head,footer,layers,sections 这种字段其实就是 HTML 里面的类似 Dom 的对象,而 style 这个字段其实就是 CSS 里面的对象。Jasonette 的源码里直把这个字典起名叫 NSDictionary * dom,其实就可以感知到,虽然 Jasonette 使用的是 JSON,但是他的思路跟浏览器内核是一样的。

iOS autolayout 的操作过程

当你使用代码执行 addsubview 的操作的时候,你其实就是在对一个 view(一种节点),添加了一个子 view(一个子节点),当所有 subview 添加完成的时候,你已经创建好了一个界面层级树,你 addSubview 一个子 view 以后,会对这个 view 要么设置 VFL,要么使用 masnory,总之会对这个 view 设置样式属性(其实就是在用 oc 代码,attach css),之后在 layoutIfNeeded 的时候,autolayout 开始自己闷头计算排版

换句话说 iOS autolayout 与 HTML/CSS 在解析上的区别是,iOS 的布局是用代码写死的,生成一种界面层级树形结构,而网页 HTML/CSS 是用可随意下发的字符串,进行解析,从而生成了一种界面层级树形结构(RenderTree)

布局排版

无论使用的是网页,还是 Jasonette,还是 iOS autolayout,当我们拿到没有经过排版的 Render Tree 的时候,虽然里面的节点包含着样式信息,但是并没有具体的绘制位置信息,因此需要从 Tree 的根节点开始依次遍历每个节点,每个节点都根据自己的样式信息以及子节点的样式信息进行排版算法计算。

在排版引擎的设计模式里(一种设计概念,不是指具体某个排版源码实现),一个 RenderTree 上每一个节点是一种 RenderNode,他可能是不同的界面元素,甚至是界面容器,每个 RenderNode 都可以有自己的 layout() 方法用于计算自己和自己的子节点的算法,一个 position 绝对布局的节点,他及内部的子节点布局算法 layout(),肯定与一个 listview,tableview 那种有规律的排布容器节点布局算法 layout() 不一样,从根节点 rootNode 开始,循环遍历递归下去,直到把 Tree 上的所有节点的位置信息都运行了 layout(),就完成了布局排版。

我们知道不同的节点,是可以用不同的算法进行他与内部子节点的布局计算的。

拿 iOS 开发举例子,我们完全可以同一个页面内,有的 view 是用 frame 方式写死的绝对布局,有的 view 是用 masnory 进行的 autolayout,甚至父 view 是写死的绝对布局,子 view 是 autolayou,或者反过来。

拿浏览器 CSS 来说,浏览器内核 C 代码里一个 RenderObject 的基本子类之一就是 RenderBox,该类表示遵从 CSS 盒子模型的对象,每一个盒子有四条边界:外边距边界 margin edge, 边框边界 border edge, 内边距边界 padding edge 与内容边界 content edge。这四层边界,形成一层层的盒子包裹起来。这种基础 RenderBox 有着自己的 layout() 算法。而在新的 CSS 里引入了更多不同的布局方式,比如运用非常广泛的 Flexbox 弹性盒子布局,Grid 布局,多列布局等等。

解析html文件(htmlcss网页设计代码案例)

在排版引擎的设计模式里,如果你想引入一种新的布局算法,或者一种新的专属布局效果锁对应的布局计算,你只需要创建一种新的 RenderNode,并且实现这种 node 的 layout() 函数,你就可以为你的排版引擎,持续扩展支持更多的排版能力了

Jasonette 是怎么做的?

Jasonette 其实根本没自己实现布局算法,也没有抽象出 renderNode 这种树状结构,他直接用原始的 Dom 字典直接开始遍历递归。

遍历到 layers 节点,就调用 [JasonLayer setupLayers] 函数,内部是自己写的一套 xywh 的算法,有那么点像 CSS 盒子模型,但简单的多。

遍历到 sections 节点,就调用 [JasonViewController setupSections] 函数,走系统的 tableview 的 reload 布局,在 heightforrow 的时候,用自己的一套算法计算高度,而在 cellforrow 的时候,他使用系统 stackview 与系统 autolayoutAPI 进行设置,最后走系统 autolayout 布局

Jasonette 的布局过程看起来很山寨,从设计上把 Dom 字典直接快速遍历,识别标签,用 if else 直接对接到不同的 iOS 代码里,有的布局代码是一些简单盒子运算,有的布局代码则是直接接入系统 autolayout,可以看出来他从 DSL 的角度,多快好省的快速实现了一个界面 DSL 框架,但从代码架构设计的角度上,他距离完善庞大的排版引擎,从模块抽象以及功能扩展上,还欠缺不少。

布局排版的几种算法绝对布局

这就不说了,固定精确的坐标,其实不需要计算了

iOS autolayoutAuto Layout 的原理就是对线性方程组或者不等式的求解。via:http://draveness.me/layout-performance.html

这篇文章写得非常非常清楚,我就不详细展开了,简单的说一下就是,iOS 会把父 view,子 view 之间的坐标关系,样式信息,转化成一个 N 元一次方程组,子 view 越多,方程组的元数越多,方程组求解起来越耗时,因此运算性能也会越来越底下,这一点 iOS 的 Auto Layout 其实被广泛吐槽,广受诟病。

CSS BOX 盒子模型

传统的 CSS 盒子模型布局,这个前端开发应该是基本功级别的东西,可以自行查阅。

FlexBox 弹性盒子

CSS3 被引入的更好更快更强的强力布局算法 FlexBox,因为其优秀的算法效率,不仅仅在浏览器标准协议里,被广泛运用,在 natie 的 hyrbid 技术方面,甚至纯 native 技术里也被广泛运用。

Facebook 的 ASDK 也用的是 Flexbox,一套纯 iOS 的完全与系统 UIKit 不同的布局方式。

大前端 Hybrid 技术栈里,RN 与 Weex 中都用的是 FlexBox,阿里的另外一套 LuaView 用 Lua 写热更新 app 的方案也用的是 FlexBox 算法。

欲知详细可以阅读:由 FlexBox 算法强力驱动的 Weex 布局引擎:

http://www.jianshu.com/p/d085032d4788

Grid 布局

网格布局(CSS Grid Layout)浅谈:https://fe.ele.me/wang-ge-bu-ju-css-grid-layout-qian-tan/

CSS 布局模块:http://www.w3cplus.com/css3/css3-layout-modules.html

Grid 布局被正式的纳入了 CSS3 中的布局模块,但似乎目前浏览器支持情况不佳,看起来从设计上补全了 Flexbox 的一些痛点。

多列布局

CSS 布局模块:http://www.w3cplus.com/css3/css3-layout-modules.html

CSS3 的新布局方式,效果就好像看报刊杂志那样的分栏的效果。

渲染

经过了整个排版过程之后,renderTree 上已经明确知道了每个节点 / 每个界面元素具体的位置信息,剩下的就是按着这个信息渲染到屏幕上。

Jasonette

Jasonette 直接调用的 addSubview 来进行 view 的绘制,Dom 字典遍历完了,view 就已经被 add 到目标的 rootview 里面去了,渲染机制和正常客户端开发没区别,完全交给系统在适当的时候进行屏幕渲染。

ReactNative & Weex

ReactNative iOS 源码解析(二):http://awhisper.github.io/2016/07/02/ReactNative源码分析2/

Weex 是如何在 iOS 客户端上跑起来的:http://www.jianshu.com/p/41cde2c62b81

这两个 Link 其实介绍了,RN 与 Weex 也是通过 addSubview 的方式,调用原生 native 进行渲染,在 iOS 上来说就是 addSubview.

WebKit

在绘制阶段,浏览器内核并不会直接使用 RenderTree 进行绘制,还会进一步将 renderTree 处理成 LayerTree,遍历这个 LayerTree,将内容显示在屏幕上。

浏览器本身并不能直接改变屏幕的像素输出,它需要通过系统本身的 GUI Toolkit。所以,一般来说浏览器会将一个要显示的网页包装成一个 UI 组件,通常叫做 WebView,然后通过将 WebView 放置于应用的 UI 界面上,从而将网页显示在屏幕上。但具体浏览器内核内部的渲染机制是怎么工作的有什么弊端,还取决于各个浏览器的底层实现。

How Rendering Work (in WebKit and Blink):http://blog.csdn.net/rogeryi/article/details/23686609

从这里面可以详细看出来,浏览器内核的渲染其实是可以做到下面这些多种功能的,但不同平台,不懂浏览器内核的支持能力不同,不是所有的 WebView 或者浏览器 App 都是同样的性能与效果

直接调用平台的系统 GUI API

设计自己的高效的 Webview 图形缓存

设计多线程渲染架构

融入硬件加速

图层合成加速

WebGL 网页渲染

仔细想想,真正到渲染这一步,你需要做的都是操作 CPU 和 GPU 去计算图形,然后提交给显示器进行逐帧绘制,webview 与 native 其实殊途同归。

Native 界面?动态? 我们其实一直在聊的是浏览器内核技术

@响马大叔说过”这其实是最纯正的网页技术,虽然他是 native 的”。

本文从 Jasonette 出发,从这个号称纯 native,又动态,又用 JSON 写 app 的技术上入手,看看这 native 动态的巨大吸引力到底有多神奇,挖下来看一看。

我们看到了和浏览器内核一脉相承的技术方案:

通过 DSL,下发设计元数据信息

构建 Dom 树

遍历 Dom 树,排版(计算算法与接入 autolayout)

遍历 Dom 树,渲染(addsubview 接入系统渲染)

浏览器内核

Webkit 浏览器内核就是按着这样的结构分为 2 部分

WebCore

绿色虚线部分是 WebCore,HTML/CSS 都是以 String 的形式输入,经过了 parse,attach,layout,display,最终调用底层渲染 api 进行展现

JSCore(本文之前一直没提)

红色部分是 JSCore,JS 以 string 的形式输入,输入 JS 虚拟机,形成 JS 上下文,将 Dom 的一些事件绑定到 js 上,将操作 Dom 的 api 绑定到 js 上,将一些浏览器底层 native API 绑定到 js 上

动态界面,其实就是浏览器内核的 WebCore

整个 WebCore 不是一个虚拟机,他里面都是 C 代码,因此 HTML/CSS 在执行效率上,从原理上讲和 native 是一回事,没区别。

而我们今天提到的动态界面,无论是 Jasonette 还是 Tangram,甚至把 xib 或者 storyboard 动态下发后动态展示,用 iOS 系统 API 就完全可以做到动态界面(滴滴的 DynamicCocoa 里面提到把 xib 当做资源动态下发与装载),其实都和浏览器内涵的 WebCore 部分是一个思路与设计,没错。

Jasonette 的设计思路和 HTML/CSS 是一回事

iOS 的 xib/storyboard 的设计思路和 HTML/CSS 是一回事

动态界面,可以界面热更新,但不是 app 功能热更新

本文从开头到现在,重点围绕着 WebCore 的设计思路,讲了 N 多,但是看到 Webkit 结构图的时候,你会发现,有个东西我始终没提到过–JSCore,但我在开头提到了一句话

Jasonette 牛皮其实有点大,其实只是写动态界面,完全不是写动态 App

界面动态这个词与 App 功能动态有什么区别呢?

一个 APP 不仅仅需要有漂亮的界面,还需要有业务处理的逻辑。

是否要执行一些业务逻辑,处理一些数据,然后返回来刷新界面?

是否要保存一些数据到本地存储?

是否要向服务器发起请求?

服务器请求回来后怎么做?

是否刷新数据和界面?

发现服务器接口请求错误,客户端做业务处理?

Jasonette 号称是用 JSON 开发 native app,但是 JSON 只是一种 DSL,DSL 是不具备命令和运算的能力的,DSL 被誉为一种声明式编程,但这些业务逻辑运算,DSL 这种领域专用语言是不可能满足的,他需要的是通用编程语言。

换个说法你就理解了,Jasonette 在技术上相当于用 iOS 的 native 代码,仿写了一个处于刀耕火种的原始时代的浏览器内核思路,一个还没有诞生 js 技术,只是纯 HTML 的超文本链接的上个世纪的浏览器技术。那个时候网页里每一个超链接,点进去都是一个新的网页。

所以这不叫 App 功能动态,充其量只是界面动态。

JSCore 的引入给浏览器内核注入了动态执行逻辑脚本代码的能力,先不说脚本引擎执行起来效率不如 native,但脚本引擎至少是一个通用编程语言,通用编程语言就有能力执行动态的通用代码(JS/LUA 等),通用代码比 DSL 有更强大的逻辑与运算能力,因此可以更加灵活的扩展,甚至还可以将脚本语言对接 native,这就是 webkit 架构图里提到的 jsbinding。

将脚本语言对接到本地 localstorage,js 就有了本地存储能力,将脚本语言对接到 network,js 就有了网络的能力,将脚本语言对接上 dom api,js 就有了修改 WebCore Dom 树,从而实现业务逻辑二次改变界面的能力。

因此 ReactNative & Weex 可以算作 App 功能动态,他们不仅仅巨有 WebCore 的能力,同时还巨有 JSCore 的能力。这里面其实有个区别,浏览器内核的 WebCore 是纯 native 环境 C 代码,不依赖 js 虚拟机,但 RN 与 Weex 负责实现 WebCore 能力的代码,都是 js 代码,是运行在虚拟机环境之下的,但他们的渲染部分是 bridge 到 native 调用的系统原生 api,有兴趣看我写的 RN 源码详解吧:

ReactNative iOS 源码解析(一):http://awhisper.github.io/2016/06/24/ReactNative流程源码分析/

ReactNative iOS 源码解析(二):http://awhisper.github.io/2016/07/02/ReactNative源码分析2/

阿里的 LuaView 我没细看过源码,但其实内部机制和 RN&WEEX 没啥区别,用的是 FlexBox 排版,但是选用的是 Lua Engine 这个脚本引擎,而非 JSCore.

在 native 动态化的道路上,不论大家走哪条路,有一个共识还是大家都找到了的,那就是看齐 Web 的几个标准。因为 Web 的技术体系在 UI 的描述能力以及灵活度上确实设计得很优秀的,而且相关的开发人员也好招。所以,如果说混合开发指的是 Native 里运行一个 Web 标准,来看齐 Runtime 来写 GUI,并桥接一部分 Native 能力给这个 Runtime 来调用的话,那么它应该是一个永恒的潮流。via:http://awhisper.github.io/2016/06/16/前端10年读后感/

虽然有点扯远了,但是这句话确实又回到响马叔的那个思路,Jasonette 用 JSON 写出 native app,他的思路依然是 web 思路,RN 用 js 写出 native app,也不能改变他一整套 web 技术的基因。一个界面最终渲染是以 native 系统级 Api 实现,并不能说明什么,渲染只是庞大 Web 内核技术的末端模块,把末端渲染模块换成 native 的,其实说明不了什么。

Webview 性能真的比 native 慢很多么?

这里就要强调一下了,浏览器内核在界面这块是纯 C 实现,没有使用任何虚拟机,所谓的浏览器内核下的 Dom 环境是纯 C 环境,也就是纯 native 环境,所以浏览器在单次渲染性能上,不见得比 native 慢。

CSS 的布局排版兼容很多种布局算法,有些算法在保证前端开发人员以高素质高质量的开发前提下,同样的界面,其性能是完全可能碾压 autolayout 的,所以单说布局这块,webview 也不见得慢。

webview 的渲染的时候还存在很多异步资源加载,但这个问题是动态能力带来的代价,啥都远端实时拉最新的资源当然会这样,如果在 App 下以 hybrid 的形式,内置本地静态资源,通过延迟更新本地资源缓存的方式,设计 hybrid 底层 app 框架,那么这种开销也能减少。更何况浏览器新技术 PWA 也好 SW 也好都从浏览器层面深度优化了 WAP APP 的资源与缓存。

webview 性能慢的原因很多,多方面综合来看确实很容写出性能不佳的页面,但话也不能绝对了,web 技术所带来的灵活多变,是会给业务带来巨大收益的。在需要灵活多变,快速响应,敏捷迭代的业务场景下,web 技术(泛指这类用 web 的思路做出来的范 hybrid 技术)所带来的优势也是巨大的

动态界面没那么神秘,意义并不在技术实现

Jasonette 写了这么多,虽然没有深度剖析每一行源码,但把他的实现思路讲解了一下,其实自己实现一个动态界面也不是不可以。

我们的工作业务需要深度处理文字,我们也有一套跨平台的 C 排版引擎内核,思路是一脉相承的,区别是文字排版会比界面区块盒子排版更复杂的多,用的也是 JSON 当做 DSL,但是我们利用我们的文字排版引擎,去实现相对简单的各种在 native 系统上的什么图片环绕,图文绕排,瀑布流界面 UI 等,其实非常的容易,甚至还是跨平台的(比 native 代码实现要容易的多)。就连 Jasonette 代码里也就只支持 section(tableview 布局)和 layer(盒子模型)2 中常见形式,复杂页面一样实现不了。

但是!但是!但是!

DSL 是领域专业语言,DSL 就注定巨有着局限性,你为自己的排版引擎设计出一套 DSL 规则,就算都使用的是 JSON,那又如何,新来的一个人能很快上手写出复杂页面?DSL 的规则越庞大,引擎支持的能力越强,越代表着 DSL 的学习成本直线加大。

HTML/CSS 已经发展成为一个国际标准,甚至是一种被广泛传播和学习的 DSL,因此他有着很多技术资料,技术社区,方便这门语言的发展,并且随着应用越广,语言层面的抽象越来越合理,扩展能力也越来越强。

但是你自己设计出来的 DSL 能走多远?能应用多远?

学习成本大,哪怕只是在自己业务内,也很大的,需要有效的建立文档说明,维护业务迭代带来的功能变化,还要给新来的同事培训如何写这种 DSL。

应用范围小,想应付自己一个业务,可能初步设计出来的接口和功能就满足需求了,但也只能自己使用,如果想推广,必然会带来更大的维护成本,需要更加精细化合理化的 API 设计,扩展性设计

人员的迁移成本大,DSL 的特点是会让写 DSL 的人员屏蔽对底层代码的理解,甚至一些初中是为了能给一些不会编码的专业领域人员学习和运用,如果编程的人员长时间写这种专有的 DSL,迁移到别的公司以后,该公司不用这种 DSL,那么这些技能就彻底废掉,如果开发者自身不保持一些对底层源码的自行探索,那么换工作将会带来很大的损失

前端人员在写各种 HTML/CSS 的时候,想要深刻理解透其中的作用机制,也是需要深入到浏览器内核层面去了解内部机制的。

所以我觉得天猫的 Tangram,是很值得尊敬的,因为想做出一个动态界面框架,没那么难,想做大,做到通用性,扩展性,做到推广,做到持续维护,是非常艰难的,真的很赞!

参考文献

谈谈 DSL 以及 DSL 的应用(以 CocoaPods 为例):http://draveness.me/dsl.html

DSL(五)- 内部 DSL vs 外部 DSL (N 篇系列文章):http://xfhnever.com/2014/08/09/dsl-internalvsouter/

由 FlexBox 算法强力驱动的 Weex 布局引擎:http://www.jianshu.com/p/d085032d4788

从 Auto Layout 的布局算法谈性能:http://draveness.me/layout-performance.html

CSS 布局模块:http://www.w3cplus.com/css3/css3-layout-modules.html

走进 Webkit:http://www.w3cplus.com/css3/css3-layout-modules.html

WebCore 中的渲染机制(一):基础知识:http://blog.csdn.net/tuhuolong/article/details/5879094

理解 WebKit 和 Chromium: WebKit 布局 (Layout):http://blog.csdn.net/milado_nju/article/details/7854312

浏览器渲染原理简介:http://www.cnblogs.com/aaronjs/archive/2013/06/27/3159789.html

Weex 是如何在 iOS 客户端上跑起来的:http://www.jianshu.com/p/41cde2c62b81

How Rendering Work (in WebKit and Blink):http://blog.csdn.net/rogeryi/article/details/23686609

“站在 10 年研发路上,眺望前端未来”:读后感:http://awhisper.github.io/2016/06/16/前端10年读后感/

ReactNative iOS 源码解析(一):http://awhisper.github.io/2016/06/24/ReactNative流程源码分析/

ReactNative iOS 源码解析(二):http://awhisper.github.io/2016/07/02/ReactNative源码分析2/

活动推荐:

发表评论

登录后才能评论