admin管理员组

文章数量:1530824

2024年1月5日发(作者:)

深入微型浏览器 (Micro Browser) 三之一

前言

微型浏览器 (Micro Browser) 主要的应用层面,是在嵌入式系统中,如信息家电、PDA 等。现今由于行动通信的蓬勃发展,在行动通信上也开始有微型浏览器的相关应用出现,使得行动电话使用者也能够透过无线宽带网络尽情浏览网络,使用因特网上的信息与服务。本文将以一个实际运作的微型浏览器 Rock Browser 为主轴,介绍微型浏览器的设计理念与方法,同时讲述微型浏览器中各个模块如何互相协同运作,来完成一个呈现网页的工作。

现况分析

近年来,行动电话和因特网应用的整合,带来了无限商机。包括 Microsoft、IBM、Oracle 等软件大厂,以及许多新兴业者,无不竞相投入无线网络应用服务器 (Wireless Application Server)、网关器 (Wireless

Application Gateway) 等伺服端相关软件平台的开发。就其中微型浏览器软件开发而言, 我们列出几家知名的微型浏览器厂商与 Rock Browser,就其所支持的规格作为比较分析:

表1:微型浏览器功能支持 ( 注:O 表示支持,X 表示未支持 )

就各家微型浏览器支持规格上来看,其规格支持差异不大,主要以 XHTML Mobile Profile 和 WML 2.0 为主要支持内容,搭配 WCSS 加以变化,其中又以 Openwave 和 Teleca 的支持最完整。而近来,打得火热的 MMS

也因各家厂商为争取大中华地区的订单而新增的功能。

就微型浏览器的市场来说,目前是各家微型浏览器各占一方,尚无一家独大的趋势。例如邻近的日本,ACCESS 由于 i-mode 的经营成功而拥有大半的日本市场;而全球行动电话的领导厂商 Nokia,则采用知名浏览器公司 Opera 所提供的微型浏览器;Openwave 更是因为已被多家行动电话厂商所采用而占有一块不小的市场;以大陆市场而言,Teleca 近日(6/18) 和中国行动电话制造商 Levono (前身为 legend)签署了一份合作同意书,未来将在大陆市场上协同作战。其中还有可怕的敌人--微软,微软虽然目前并没有倾全力抢占微型浏览器市场,但是随着 WinCE 操作系统的畅销,其它微型浏览器似乎也只能在『非 WinCE』的市场上争个长短,谁也不敢妄想在 WinCE 上动脑筋。

系统需求

根据上述的结果,我们整理出一个微型浏览器所该具备的基本功能,包含:

● XML 的支援

XML「可扩展标示语言」(eXtensible Markup Language) 是用于标示具有结构性信息的子文件的标示语言。XML

的规格是由『全球信息网标准制定组织』 (W3C) 制定,并于 1998 年 2 月成为推荐规格。目前已有许多家厂商采用,且视为关键性技术。例如:Adobe,IBM,微软,Netscape,Oracle,Sun 及这个领域中的重要厂商。。XML 与 HTML 都是从 SGML 衍生出来的语言,因此它们两者在某些特性上看起来都很相似,例如类似的语法,全部都使用成对的标签等等。然而一个很重要的差异是,HTML 是 SGML 的一种应用系统(application),而

XML 则是 SGML 的子集(subset)。XML 主要提供网页编辑上的可携带性与平台发展的独立性。

● DOM 的支援

DOM 是一个跨平台的应用程序接口。当微型浏览器加载一个标准的 XML 的网页时,微型浏览器会根据其网页内容建立一个文件对象的相关模块即 DOM,来作为标准化的存取与操作。

● Script 的支援

Script 主要用来提供动态网页与网页的互动效果。以往这些网页上的互动效果需要透过 CGI 的方式交由网页服务器来运作,往往会造成服务器得负担。而 Script 将这些动态网页的互动效果放入 XML 网页中,当微型浏览器加载 XML 网页时,能由网页中的 Script 内容自行作运算产生动态网页的效果,来减少服务器的负担。

● Plug-in 的支援

提供一个标准接口,供其它 Three Party 协助开发其它应用软件如 Flash 等应用与微型浏览器作结合,提高微型浏览器的功能与兼容性。

● CSS 的支援

CSS「串联式排版样式」(Cascading Style Sheets)为 W3C 在 1996 年底所提倡使用的,其为一群模板样式,依序为定义 HTML 组件如何出现在浏览器上的属性;比方说字型颜色的变化、大小或是斜体、粗体等。主要的功能是让 Web 建置者利用link or import a Style Sheet 的方式一次控制一份或多份网页的呈现配置及样式。

由上述的技术要项中,读者或许会觉得微型浏览器的技术要项与桌上型浏览器似乎是大同小异。事实上,微型浏览器异于桌上型浏览器的最大不同在于发展平台上。通常微型浏览器是被烧录在行动电话或个人数字助理 (PDA) 这一类的机器上,使用者无法或根本不会主动去更换这类程序,因此这些浏览器厂商主要关心的是制造商和内容供货商需求,例如能否浏览内容供货商自己开发的网站取得服务或是产品能否快速整合及所占内存的大小等等,因此这些浏览器开发厂商在推销产品时,通常都以支持规格为辅,主要推销产品的诉求是在于微型浏览器是否能够存取特定的服务网站、可移植性、效能及在内存中动态和静态所占的空间,用来获取制造商和内容供货商的青睐。因此在微型浏览器的设计上,除了考虑浏览器的功能外,还需要在有限的硬件资源下发展具高性能,高效能,低耗电的设计。

技术上的挑战

Memory的限制

由于非 PC 平台的内存,扣除掉操作系统所需的内存,其它应用程序的可用内存都相当有限,因此在开发微型浏览器时,要降低程序代码本身(Code Storage)的大小,以及执行时期对内存的需求(Run-time Memory

Requirement),同时兼顾到浏览器执行时与系统的整体效能。因此需要考虑到以下两点。

● 微型浏览器的数据结构

在制订浏览器所需的数据结构时,考虑各个字段的必要性,并选择语言层次(C 语言)中「得以包含该字段信息」的最小数据型别,使得编译器能以最少量的内存空间来编译程序。

● 对象的重复使用 (Reuse)

由于浏览器执行时的某些对象(或数据结构)是数个内部组件都需使用到的,为避免同一份信息被产生出多份复本,因此我们让所有需要用到该份对象的组件,都共享同一份对象(仅配置一块内存),并利用 Reference

Counting 的方法对每个共享对象保留一个「参用计数」,当有组件欲使用/释放该共享对象时,就将参用计数的值加/减 1,只有在参用计数的值为 0 时,才会真的删除该共享对象,避免共享对象在仍有组件使用时,就被其它组件给删除。倘若有某个组件想更改共享信息的内容,才需要再另外复制一份信息供组件修改使用。

另外,在浏览器的网络传输与 Script Interpreter 执行过程中,经常会需要产生大量的相似对象,然而频繁的配置内存会造成程序执行效能的降低。为提升程序执行效能且节省内存的使用,我们有限度的使用 Object

Reuse的技巧,来重复利用这些相似对象。把使用完毕的相似对象暂存于一个Free-Object List 中,当需要产生新的相似对象时,就可以直接取用 Free-Object List 中的对象,而不用再配置新的内存。

速度的限制

在非 PC 平台上,处理器的效能不若 PC 平台上强大。因此,微型浏览器的效能必须要被提升。就这方面来说,我们设计两个能够提升整体效能的方法:

● 以C语言实作核心:

Rock Browser 是以 UML 的观点设计,因此在架构上是以对象导向为概念,但是在实作之初,考虑到对象导向语言在实作继承、多形和封装时,额外的机制会造成程序代码变大变慢,且在移植平台的语言支持上,C 语言是较常被支持的。因此决定以 C 语言来实作对象导向观念所设计的项目。这样不但可以制作快速而小的程序,同时也具备了较高的移植性。

● 数组堆栈:

在微型浏览器中,数组堆栈是一个经常被使用的组件,因此这部分的效能会影响整个微型浏览器的效能。所谓的数组堆栈是指先配置一块内存,用来存放每次推入(push)和取出(pop)的数据,可是这样有个先天的缺点就是必须是固定长度的堆栈,若是超过堆栈的长度,程序就很可能出问题。所以我们使用 realloc 的方式来改善这个缺点。底下的程序代码是我们实作的原理,我们将每个堆栈的数据当成只有四个字节:

我们在开始处先判断堆栈的指针是否为真,若为空的,先配置一块预设大小的内存,下次再有数据需要推入时,我们会去检查他的大小,若是配置的内存超过他所能够推入的数据,那我们就利用 realloc 重新配置一块大小为两倍内存空间在同一个指针位置。观念很简单,主要的用意是要达成利用数组的方式实作堆栈,如此存取快速,又可以克服数组只能用在固定大小的堆栈,而取出数据时只需要将内存空间写成默认值,不需要将内存释放。我们测试效能之后,发现和一般利用动态配置内存串成堆栈的方式相比,存取 1000000 比数据快了将近有 40 倍之多。因篇幅有限,整套API实作程序代码的,也就不再详述。

可移植性

以可移植性而言,在微型浏览器的设计中,我们将微型浏览器中与平台相关的部分切割出来成为单一模块。当微型浏览器需要被移植到各种不同平台时,只要修改该模块即能移植至其它异质平台上来达到微型浏览器的高移植性,减少软件移植的时间。这些模块包括:

● 使用者接口方面:

微型浏览器所使用到的使用者界面会依据不同的窗口操作系统而被更动。如按钮键 (Button) 的产生等窗口组件的绘制,或是窗口组件的事件产生,都会随着不同的窗口操作系统而不同。因此在微型浏览器的设计中,为了高移植性需要将这部分的处理单一模块化。

● 操作系统相关方面:

如 Thread、Socket、File 等跟系统相关的运作,通常这些操作会随着平台的操作系统不同而改变其使用方式或是行为的动作,因此这部分也需要单一模块化。

● 工具接口方面:

像是 List、String 等常使用的公用工具,将它单一模块化除了为了移植性的考虑,也可独立出来供其它应用程序使用。

微型浏览器的架构

以下将为读者开始讲述我们独力开发的微型浏览器 Rock Browser。下图为微型浏览器的架构图:

图1:Rock Browser 架构图

我们将微型浏览器切割成五大模块,分别叙述如下:

● 可移植性 (Portable Interface) 模块

将跟系统平台有关的程序,切割成 Portable Interface 模块,以便微型浏览器能在最短时间能移植至异质平台上。因此这部分的模块程序多为与系统平台相关操作,包含了:GUI Interface、Thread Interface、Network

Interface、Storage Interface、Utility Interface 等。

● 使用者接口 (Browser main and User Interface) 模块

此模块主要负责与使用者间的互动讯息以及使用者接口的呈现与管理。这部分包含了 Skin Manager (管理浏览器的使用者接口的样式) 、Bookmark Manager (管理使用者所设定的 Bookmark) 、Browser User Interface

(微型浏览器的使用者接口)、Browser Main (使用者的事件处理)等。

● 核心(Browser Engine) 模块

此为微型浏览器的核心部分,主要用来处理与管理微型浏览器所接收到的 XML 网页。包含有:XML Tag 处理、CSS 的Style 处理、Script 的处理、网页图形处理,Layout Manager、Storage Manager、Parser、History

Manager 等。

● 网络 (Network Protocol) 模块

此为网络实际运作模块。如 HTTP,WAP 1.x 等通讯协议。

微型浏览器的运作方法

在介绍完 Rock Browser 的架构后,接下来,我们将带领读者进入 Rock Browser 的运作流程。我们将从一个 XML 的网页的加载开始,一步一步带领读者进入微型浏览器的运作流程中,并于各个流程中探讨微型浏览器的设计秘辛。从微型浏览器的开启、网页的抓取、进入 Parser 的处理、Layout 的处理、网页的呈现等程序来介绍微型浏览器的设计。

开启微型浏览器

乍看这个标题,读者或许会问,微型浏览器的开启会有什么的特别的技巧。事实上,在非 PC 平台上,嵌入式系统处理器效能不似PC平台上那样快速,往往为了加载一个应用程序而消耗一大段系统时间,有时甚至会让使用者误以为系统当机。在人机接口的设计上,为了解决这个问题,我们将微型浏览器的初始化步骤细分成几个部分:

1.使用者接口初始化:

为了让使用者能够有系统正在执行的感觉,我们先再画面上呈现微型浏览器的外框,包括一些基本的视觉对象,如标题列、工具列状态列等对象。如下图所示:

2.载入首页:

接下来,微型浏览器会处理一些跟使用者接口较无关系的动作。根据使用者所设定的首页或经由别的程序传入值,决定要加载的首页,并执行抓取、编排和显示网页。这部分流程包含抓取网页内容等动作,还有一些必要的对象如快取机制(cache)和历史纪录(history)功能等。其结果如下面画面所示:

3.使用者设定:

一般而言,供使用者设定微型浏览器选项的接口画面,并不需要在微型浏览器被加载时,就被先计算且呈现在画面上。一般都是等到使用者点选某个按钮或功能才会处理画面的呈现。因此,我们将这部分的画面加载处理程序,移至使用者去触发时,才处理,以便增加为型处理器加载的速度。如下两图所示:

对于这样的加载流程,可以减少使用者等待系统反应的等待时间,而使得整个微型浏览器的加载流程会更为流畅,产生加速的效果。

抓取网页

浏览器最基本的功能,就是能够将因特网上的众多资源,例如网页上的内容与信息或是在线服务等,呈现给使用者并让使用者得以使用存在于因特网上的各项服务。因此透过网络传输数据对浏览器来说,是相当重要且必备的一项能力。为了让微型浏览器能够存取因特网上的各项资源并适应各种不同的网络环境,我们开发出 WAP

2.0 Protocol Stack 提供 WP-HTTP/WP-TCP 与 WSP/WTP/WDP 这两大网络通讯协议架构,让浏览器可以因应不同网络环境的需要,使用适当的网络通讯协议来抓取因特网上的数据。

图6:微型浏览器利用 WP-HTTP/WP-TCP/IP 等通讯协议去抓取数据

图7:微型浏览器利用WSP/WTP/WDP等通讯协议去抓取数据

浏览器除了要能透过网络去抓取数据外,亦需提供数据快取(Cache)功能,将抓回的网页数据选择性的储存在手持装置上,并提供快取管理机制,将已过时(expired)的网页数据从快取储存区中移除,避免让使用者浏览到无效的信息。然而快取在手持装置上的数据也应有总容量的限制,让使用者可以根据手持装置的配备能力,来决定可储存多少的快取数据。

此外目前因特网上常用的 Cookie 机制,也是微型浏览器应支持的一项功能。Cookie 是使用者透过浏览器在网上浏览时,网络服务器「要求」浏览器所记录下来的数据,可以用来追踪使用者或对重返的使用者进行确认,而网络服务器也会告知浏览器,有哪些人可以看到这个 Cookie,以及 Cookie 资料有效的时间长度。Cookie 的内容可储存使用者上网的许多个人信息,虽然可以让使用者浏览时更为方便,但也由于 Cookie 能够追踪使用者在网络上的许多动作,因此也有其安全上的顾虑。因此浏览器在提供 Cookie 功能的同时,也需提供一定程度的安全机制来保护使用者的个人信息不会被任意侵犯。

待续

截至目前为止,我们已经就微型浏览器的加载以及网页的抓取流程作简单的介绍,由于篇幅有限,我们将在下一篇中继续为读者剖析介绍微型浏览器的核心技术。

深入微型浏览器 (Micro Browser) 三之二

信息工业策进会 嵌入式系统实验室

WAP 技术小组

前言

在上一篇的文章中,我们介绍了目前各家微型浏览器的现况,以及在嵌入式系统上开发微型浏览器所需要的考虑与技术上的挑战等等。并且简介了我们自行开发的微型浏览器的使用者接口以及初始化的运作流程。紧接着在本篇文章中,我们将以节录部分程序代码配合概念解说的方式,带领读者进一步的深入了解微型浏览器的核心技术。

深入微型浏览器核心

微型浏览器的最重要核心技术,便是将网页设计者撰写的 HTML、XML、WML…等标示语言(Markup

Language),经过处理还原成具有多媒体呈现效果的网页。这个过程主要可以区分成两步骤,先将网页的原始码经由 Parser 处理成具有意义的数据结构,再由 Layout Manager 来进行网页页面的编排,将标示语言还原成原来的面貌。因此,本篇文章的前半部将先从 Parser 处理及数据结构的概念谈起,介绍标示语言的处理方法及需要特别注意的地方;后半部则从 C 仿真 C++ 语言谈起,以实作的方式介绍我们如何以 C 语言为核心,建构出 C++ 语言的继承关系,然后到整个网页的完整呈现,为读者做一个完整的介绍。

微型浏览器核心运作流程

微型浏览器从网络下载回数据后,首先会交由一个称为 "Dispatcher" 的对象来处理(图一)。Dispatcher

对象是由 Queue(队列)的数据结构所组成,当网络层有数据送回来的时候,会透过 Storage Manager 放进

Dispatcher 的 Queue 中,再由 Dispatcher 分辨来源资料的 MIME Type。然后依据不同的 MIME Type 交由不同的组件进行处理。

Dispatcher 会将 MIME Type 属于卷标语言类型的数据送至 Parser 进行处理。现今处理此种卷标语言的标准主要分为两大主流,其一是强调效能和使用简单的 Simple API for XML (SAX),另一个是功能齐全且被广泛应用在 XML 和 HTML 文件的标准程序存取接口,Document Object Model (DOM)。SAX的好处在于只需较少的内存空间且具有较佳的执行效率。但是相对的,它在对于数据结构存取方面的能力则比较不足,而微型浏览器这类型的应用程序又是属于需要频繁的存取文件以及相关的信息。所以我们在考虑设计的弹性和完整性后,决定采用 DOM 的存取接口。

什么是 DOM ( Document object model )?

DOM(文件对象模型),顾名思义就是用对象的观念来描述一份文件,以便在内存中储存 XML、HTML 这类结构性文件,让应用程序能有效率的存取其中相关信息。它在 W3C 的建议案中,被定位为一种与语言和操作系统平台独立的对象模型。依据功能的完整性,DOM 可被分为 Level 1~ Level 3 三种等级。Level 1 提供了最基本的对 XML、HTML文 件浏览(Navigation)和操作的功能。Level 2 加入了 Style Sheet Object Model、Traversal Document 的支持,以及支持 XML 的 Namespace 属性。最新拟出草稿的 Level 3 加入了 Document

Validate、和一些 Event 如 Key Event、Event Groups 的支持。

DOM 最主要的功能是利用对象的观念,把结构性文件建立成类似 Tree (或是说 Forest)的数据结构,以便透过操作 Tree 结构的方式加速数据存取的速度。举例来说,图二(a)这个 HTML 文件,可以转换成为如图二(b)这样的一个 Tree 的结构。在图二(b)中左边灰色的 TR 这个节点,我们可以透过 DOM 定义的

getParentNode()这个接口去找到它的父节点,用 getFirstChild()和 getLastChild()分别可以取得它的第一个子节点和最后一个子节点。其它详细有关 DOM 定义出的接口,读者可以到 W3C 的网站上找到需要的数据。

DOM 在现今被广泛的应用在处理 HTML、XML 这类的结构性文件,同时也被实作成各种作业平台和程序语言的版本,尤其像是 Java/C++ 这种对象导向的程序语言,或是业界常用的 C 语言。(有兴趣的读者可以在下列的网址找到各种不同实做版本的 DOM API。)

/Computers/Programming/Internet/W3C_DOM/

微型浏览器主要应用的平台是在运算资源有限的嵌入式系统,为了执行效率和节省内存空间,我们采用的 DOM

Package 只支持 DOM Level 1 和部分的 Level 2。但这也意味着在功能完整性上是较为不足的,如 Traversal

Document、和 CSS(Cascading Style Sheet)部分的支持,这些部分则必须另外处理。

DOM Parser

DOM Parser 的主要功能,就是把文字型态的卷标语言经过 Tokenize、语法语意检查的过程后,转换成 DOM

Tree 的数据结构,以利微型浏览器进行存取。

以下就Parser的主要功能进行介绍:

ze:

Parser 在取得 Text Stream 型态的数据后,第一件要做的事是根据开头的 Starting Tag,如,和结尾的 End Tag,如,将整个字符串切成一个一个 Token。后续的动作才能根据切出来的 Token,做字码转换、建立 DOM Tree。

2.字符集之间的转换:

HTML 或 XML 文件都是可以支持多国语言编码的文件类型。甚至在同份文件中也可以透过 tag 的

xml:lang 属性指定不同的编码方式,使 HTML 文件中同时包含两种以上的编码方式。为达到这样的目的,parser 在建立 DOM tree 之前会依据文件中内定的编码方式( 的 charset 属性或是其它 tag 的 xml:lang

属性),将文件从 local 的编码方式(Big5 or GB 等)统一转为 UTF-8 的编码方式,然后再建立 DOM。如此一来 DOM tree 的编码方式就会统一是 UTF-8 码,方便微型浏览器程序对 DOM tree 做存取的动作。最后在

layout 阶段,才再转换成 UI介 面能显示的字符集。

Entity Reference 是在处理 HTML 文件中,会遇到的另一个常见的转码问题。举例说,在 HTML 文件中,空白符号的 Entity Reference 表示方法有三种,第一种是十六进制 Numeric character references 表示法 第二种是十进制表示法 第三种是 Character entity references 的格式 。第一和第二种的处理方法,无论是十进制或是十六进制,其数值代表都是 Unicode(UTF-16)的编码数值,所以我们只要把这个整数型态(integer)数值转成字符串文字型态(char *),再转换成UTF-8编码就可以了。至于 Character entity

references 这种方式的表示法,因为没有固定的规则可循,在实做上我们利用查表应对的方式,去转换成相对应的 UTF-8 编码。

3.语法语意检查的容错机制(Error recover schema):

在一般网页中,常常会遇到网页原始码有错误的情形发生,这些错误若不加以处理,轻则可能只是显示错误,重则可能导致微型浏览器当掉。其中最常发生的语法或是语意上的错误,可大略分为几项,如:缺少 End

Tag、错误的语法(Syntax error)、语意不明(Ambiguous)、不符合标准或是某些浏览器自订支持的 Tag。在介绍我们的解决方法之前,我们先举例说明以上提到网页常犯的错误,可能造成的问题:

图四是一个简单的 HTML 片段,网页设计者要表现出两个自动编号的超级链接,其中第一个超级链接

"link1" 以

的格式显示。但是假设网页设计者不小心漏写了

的 End Tag "

",如此一来就会使得

"Outside" 这个字也会变成

型态的黑体字。这种漏写 End Tag 的例子,同时也会造成语意上的不明,使得

Tag 不知何时结束,导致显示结果错误。为了解决这样的问题,同时顾虑程序执行的效能,我们以实做一个纪录状态的机制来解决这个问题,我们称之为 Tag State Machine。

Tag State Machine 机制的基本精神就是利用一个 State Stack 来纪录建立 DOM Tree 的过程中,目前

DOM Node 的位置,以决定下一个要加入的 Node 的 Parent Node 是谁。这个 State Stack 的运作方式就是当 Parser 遇到 Start Tag 型态的 Token,如

  • ,就会将
  • 这个 Token Push 到State Stack中,当遇到End Tag
  • 时,就会把 Token 一个一个 Pop 出来,直到 Pop 出来的 Token 是
  • 为止。如果遇到的是 Text 型态的 Node 的话,则不做动作。以图五的 HTML 文件为例,看看在缺少
  • 的 End Tag 的情况下,Tag State Machine 是如何运作,帮助我们建立一个正确的 DOM Tree。

    当 DOM Parser 处理完

    这个 Token 之后,State Stack 中的数据结构如图五(a)所示。此时 Stack 中最上层的 Token 会是

    ,所以我们纪录的 Parent Node 指针会指向

    ,如(b)所示。接着将 link1 这个

    Text Node 加入,使得 DOM Tree 中的数据结构形成(c)所示。

    由于网页设计者漏写了

    的 End Tag,所以接下来遇到的是这个 End Tag。此时根据我们的规则,必须从 State Stack 中 Pop 出这个 Token,所以 Parser 会先把

    给先 pop 出来,检查之后发现

    并不是我们要的 Token,就会继续 Pop 出下一个 Token 再做检查,发现是之后,这个动作才算完成。此时在 State Stack 中最上层的 Token 会变成
  • ,如(d)所示。之后遇到 "Outside" 这个 Token 的时候,就会被加入到
  • 这个 Node 后面,成为
  • 的 Child Node,如(e)所示。

    DOM Tree:

    DOM Parser 就是利用 Tag State Machine 确认完语法和语意之后,呼叫 DOM 提供的标准 API 接口,将一个个 Token 加入到 DOM Tree 中,如此一来我们就会得到一个纪录完整文件信息的DOM Tree了,之后在

    Layout 阶段需要用到的信息就可以从 DOM Tree 取得。这个例子在我们的微型浏览器展现出来的样子就如图六所示。

    以 C 模拟 C++ 的概念

    我们可以将网页上的文字、图片、按钮等等看成是一个一个的对象(Object),然后设计一个可以容纳并且排列这些对象的组件(Component),来进行 Layout。因此利用对象导向(Object-Oriented)的设计是很直觉的。这会使得我们自然而然想到使用 C++ 语言来进行实作。但是在之前的文章提到,在微型浏览器所处的平台大都有一些如 Memory 较少、Computing Power 较弱等资源上的限制。再加上很多嵌入式系统中对于 C++ 语言的发展环境支持性较为不足,为了考虑可移植性,以及保有 C 语言的高执行效能,因此我们选择以 C 语言来仿真 C++ 的对象继承关系,以下将配合程序原始码来说明以 C 语言仿真 C++ 语言继承的概念:

    Way:

    当我们需要一个叫做 Parent 的对象时,使用C语言可以宣告一个 Structure 来实作:

    其中 mClassID 是 structure 里的一个 Field,用途后面会说明。而 (*miFunc1)(void)则是一个 Function

    Pointer,可以指向某个实作的 Function。有了 Parent 对象之后,Child 对象要继承 Parent 对象,就可以这样宣告:

    则 Child 对象在内存里存在的形式就如同上方右图。当 new 出 Child 对象时,就可以利用强制型别转换(Type Casting)的方式使用 Parent 对象或是 Child 对象:

    这种做法的好处是简单明了,以宣告 Data Structure 的方式即可完成继承;但是因为每个 Child 对象都包含了 Parent 对象,当使用量增加时,则不免浪费内存。

    ed Way:

    另一种较为进阶的方法则是将 Data Structure 中的 Object Structure 与 Object Class Structure 分开。Object Structure 指的是 Class 中的 Member Variable 的部分,而 Object Class Structure 则是指

    Class 中 Member Function 的部分。以前面的例子来说,Parent 对象的 Structure 就可以区分成两部分:第一个 Object Structure,里面只有纪录 Parent Structure 中 mClassID 这个 Field,这个字段的数据由系统给予,第一次生成这种对象的时候,会给予一个唯一值。另一个 Object Class Structure 则纪录

    miParentFunc 这个 Function Pointer。两个 Structure 之间的关系则是利用一个 Virtual Table 记录起来,这个 Virtual Table 纪录着每个 ClassID 所用到的 Object Class Structure 的指标。这样之后就可以利用

    mClassID 来当作 Index 在 Virtual Table 里找到真正纪录 unction Pointer 的 Object Class Structure,如图七所示:

    那么这样做的目的是什么呢? 赶紧接着看下去:同样以之前的 Child 对象为例,要继承 Parent 对象时,则形成如图八所示的情形:

    Child 对象也分为 Object Structure 与 Object Class Structure 两类,在这种类型的 Child 对象第一次被 new 出来的同时,会被给予不同的 Class ID,而与 Child 对象的 Object Class Structure 被 new 出来之后,与 Object Structure 的关系,也会一并纪录在 Virtual Table中。

    若是这个 Child 对象被大量使用的时候,这种做法的好处就彰显出来了,因为当同类型的 Child 对象再次被 new 出来时,由于它已经注册过相同类型的 ClassID,因此就不需要再 new 出重复的 Object Class

    Structure,只要 new 出 Object Structure,然后利用 ClassID 到 Virtual Table 里面去找,便可以找到

    Object Class Structure 的指标,进而呼叫到正确的 Function,如图九:

    我们可以 Implement 一个 Macro 叫做 CALL_METHOD(obj),方便程序设计师不必每次要呼叫 Child 对象的 Member Function 都要自行到 Virtual Table 里去寻找。如此一来,程序代码的呼叫就如同下方所述:

    至于多重继承的问题,则 Virtual Table 里利用 ClassID 做 Index 的方式需要更复杂的处理,有兴趣读者,可以参考 Glibc 作更深入研究的研究。

    C语言实作

    有了概念之后,接下来我们以实作一个 Image 对象为例子来说明运作的情形:

    在我们的微型浏览器架构中,Image 对象继承自 Widget 对象,而 Widget 对象又继承自对象根源的

    Object 对象,因此是两层的继承关系。当外界需要一个 Image 对象时,可以这样呼叫:

    RWImage* pWidget= RWImage_miCreate();

    因此我们需要撰写一个 RWImage_miCreate()的 Function 让外面呼叫,其内容是这样的:

    其中第4行呼叫 gRObjectNew 函式的第一个参数 "RWIMAGE" 事实上是一个 Macro:

    #define RWIMAGE (RWImage_mfRegisterType ())

    这个 Macro 会呼叫 RWImage_mfRegisterType ()这个 Function,这个 Function 是撰写 Image 对象的设计者要实作的函式,内容的部分程序代码(如程序代码一):

    其中第3行宣告一个 static 的变量 "classid",这个 classid 在第一次被生成 "Image" 这种对象类型的时候,会透过程序代码第 8~14 行填写一个 "info" 的数据结构,然后透过第 15 行呼叫

    gRegisterTypeInfo()来得到。

    第 16 行则是由于这种对象第一次被生成,所以也必须将 Object Class 给 new 出来,呼叫的方式是在程序代码第 18 行透过 sInitFunc()来呼叫,事实上就是呼叫到第 13 行注册的

    "(RClassInitFunc) RWImageClass_mfInit" 这个函式。然后在第 17 行将 Image 对象的 classid 与 Object

    Class 一起注册到 Virtual Table 里面,以供之后的查询。第6行的判断式则是当有相同类型的对象被生成时,不需要再重复生成重复的 Object Class。

    第 11 行的程序代码指向了一个 RWImage_mfInit 的函式,这个函式的功能如同 C++ 语言里的

    Constructor 函式一样,程序设计师可以将需要初始化的数据如 Class 里面的 Member Variable 等,写在这个函式中:

    第 12 行的程序代码则是指向 RWImageClass_mfInit 的函式,这个函式就是将真正在I mage 对象实作出来的函式指标填到 Object Class 里面,例如 Image 对象继承了 "miDraw" 这个函式,并且实作在 Image

    对象里,这时候就要把指标指向这里实作的位置。程序代码实作如下:

    其中需要特别注意的是.程序代码一里面第 5 行的程序代码:

    RClassID parentclassid = ROCKWIDGET;

    因为 Image 对象是继承自 Widget 对象,因此等号右边的 "ROCKWIDGET" 也是一个 Macro,正如同 Image

    对象一样,Widget 也会有相同的 RWidget_mfRegisterType ()的函式进行 Widget 对象的初始化,所以是一个递归生成的关系,而W idget 继承自 Object,同理 Object 对象也会被生成出来。也就是说:当 Child 对象被生成时,连带的会使得继承的 Parent 对象一起被生成出来。

    读者可以想见最后 Image 组件的对象在内存里形成的形式,一为 Image 对象的 Object Structure,另一则为 Image 对象的 Object Class Structure,两者透过 Virtual Table 相连,而 Object Class Structure

    里则利用 Function Pointer 指向实作的函式实体,如图十所示:

    最后再实作 CALL_RW_METHOD(obj)这个 Macro,帮程序设计师呼叫 Virtual Table 里找出正确的 Object

    Class 指标:

    #define CALL_RW_METHOD(obj) X_CALL_METHOD(obj,RWidgetClass)

    而其中的 X_CALL_METHOD(obj,RWImageClass) 这个 Macro 如下:

    #define X_CALL_METHOD(pObject,ct) ((pObject) ?

    ( (ct*)(gObjectTable[(((RObject*)pObject)->mClassID-1)].mpClass)) :

    ((ct*)(RObject_miDebug(__FILE__, __LINE__, "NULL Function Pointer!!"))))

    至此,Image 对象的实作大致完成。

    接下来我们来看看系统中实际发生的情形:在浏览器中,除了 Image 对象之外,实际还存在有多种的对象是继承自 Widget,例如:Button、Ruler 等等。当浏览器需要将各种不同的 Widget 画到屏幕上显示出来的时候,实际上是透过 Base Class 的指针 Widget 这种型态来处理:

    (CALL_RW_METHOD(widget))->miDraw( widget, area );

    当这个 Widget 是一个 Image 对象的时候,CALL_RW_METHOD 这个 Macro 会根据它的 classid 在

    Virtual Table 中替我们找出正确的 Object Class,也就是 Image 对象的 Object Class,又因为我们已经将 Image 对象继承而来的函式 pParent->miDraw 指标指向 RWImage_miDraw() 就能够呼叫到正确的函式。而呼叫函式时传入的第一个参数就是对象本身,目的是进入函式之后,能够透过这个对象指针正确使用到属于该对象的数据,而不至于与其它的 Image 对象混淆。

    同理,浏览器中其它的 Widget 对象如果也依照正确的方式撰写,那么不同的 Widget 对象,就都能够正确的呼叫到每个对象继承的函式了。各位读者应该已经发现,这就是 C++ 里头所谓的 Polymorphism(多型);呼叫函式时传入的第一个参数,也就是 C++ 书籍里所说的那个隐藏的 "this" 指标。

    Layout的概念

    若我们将浏览器的页面当成是一个架子,而网页内容的文字、图片等等对象看成一个一个包装好的物品。则 Layout 的概念就像是将一件一件的物品整齐的排列到架子上,而这个架子必须能够根据摆上去的物品尺寸随时动态的调整架子的宽高与大小,然后适当地将物品排列出来,如图十一所示。因此,我们可以将 Layout 分成两个事件来处理:

    1.将网页的原始码切成一个一个的 Item(物品),然后包装成统一的格式,以方便之后的处理。

    2.设计出这样的"架子",能够动态调整大小并且排列这些物品。

    在第一点中,我们必须设计一种具有弹性的数据结构,能够将网页上各式各样的对象包装起来,让微型浏览器进行 Layout 的时候能够统一处理。由于我们的微型浏览器受到小型装置资源的限制,所以暂时不考虑复杂的

    Layer 支持,因此我们以 2D 的概念来进行设计,数据结构的内容如图十二(a)所示。我们将各式各样对象的差异性抽离出来,以一个 Item 的概念来看。所有的对象在 Layout 的过程中都被当成是一个一个的 Item,以一个 Rectangle 来看待,然后在每个 Rectangle 上加上标签,让我们知道内容物的组件是什么种类(Text、Widget or Space);另外我们储存一个指针,指向实际的组件;最后还有一个 CSS Object,用来纪录加在这个对象上的 CSS 属性。以网页上一个 20x30 大小的 Button 为例,所形成的 Item 就如同图十二(b)所示。

    Layout Manager 经由T raverse DOM Tree 的方式,依次取出 DOM Node,并根据取出的 Tag,呼叫不同的函式处理。所以在 Layout Manager 中,需要根据不同的 Tag 实做不同的函式,函式的本体则是处理该 Tag

    本身以及 CSS 的属性。例如以 Tag来说,当Layout Manager发现取出的是 Tag的时候,便会呼叫

    RLayout_mfOpenA 的函式来处理,而该函式则将 href 属性所指的位置 Parse 出来加以储存,并且改变文字的颜色以及画底线等等,然后将包含在 Tag里的文字加上该有的属性之后,加入 Page 里进行 Layout 处理。由于在标示语言中,外层 Tag 的特性会影响到被包含在内层的 Tag,因此在 Tag 的属性处理部份,需要利用一个堆栈(Stack)来处理,实做的方式是遇到 Tag 开始时,就 Push 到 Stack 里面,当该 Tag 结束时便

    Pop 出来,而处理到的 Item 就 Apply 目前 Stack 里面的特性。举例来说:

    Test Bold Link

    "Bold" 这个文字被 Tag 包住,外层又被Tag 包住,因此必须同时显现出 Tag 与 Tag 的特性。首先 Tag被 Push 进 Stack,所以接下来的 "Test" 这个字符串会 Apply 到 Stack中 Tag 的属性。然后处理到 Tag 的时候,再将 Tag Push 到 Stack 里面,则接下来的 "Bold" 字符串便会同时 Apply

    到 Stack 里面的两个 Tag 的特性。然后 Tag 结束 Pop 出来,所以接下来的 "Link" 字符串就又只会

    Apply 到 Tag 的属性,最后到 Tag 结束。关于属性以及 CSS 的继承,再下一篇的文章中会有较深入的说明。

    接下来就要针对第二点设计可以摆放 Item 的架子。虽然存在网页中的 Item 种类并不多,但是配合标示语言以及 CSS 语法衍生出来的 Layout 效果就变得非常多。因此设计出来的架子要能够应付不同的 Layout

    需求。由于 Layout 最后要呈现的结果是在浏览器的页面上,因此我们将这样的架子称为一个 "Page",而架子里一格一格的横架则称为 Line。Page 对象里实际进行 Item 的 Layout 排列工作就是计算 Item 实际的大小,然后排列到 Line 里正确的位置,Layout 的流程如图十二所示:

    1.从 DOM Tree 里取出数据后包装成 Item 的对象。

    2.经第二步串到 Page 对象的 Item List 中,然后根据 Item 的内容计算出 Item 的实际大小,例如文字可交给 Font Manager 来计算宽高大小,其它的组件则呼叫其它适当的函式来计算宽高大小。

    3.将 Item 尝试加到目前的 Line 中,并进入图六右方的流程中。若是该 Item 可以被放进目前的 Line 中,就直接放入。Line 的数据结构会纪录属于该行的 Item Index, 在发生 Re-Layout 以及最后画到页面上的时候会用得到。

    4.当上一个 Line 容纳不下,或是遇到如
    的 Tag,需要换行的时候,Page 就必须 New 出一个新的 Line

    结构,然后串到 Line Lis 中。则接下来的 Item 就放到新的 Line 里面。

    放到 Line 里面之后,必须根据 Item 的 CSS 属性,例如有置中对齐或是向右对齐等等特殊的属性,就要在这时候考虑进去,调整 Item 在这个 Line 的位置,而 Line 本身会以该 Line 中最高的 Item 作为

    Line 的高度。最后再将排列好的位置信息更新到 Item 的数据域位里面。

    我们以一个实际的例子来看看 Layout 出来的效果:

    Page 组件是以 Container 的概念来进行设计,因此实际网页上并不是只有一个 Page 组件。以常见的

    Tag来说,每一个
    Tag里面的
    Tag,都是用一个 Page 组件来进行 Layout,然后整个 Table

    再视为是一个 Widget,加入到底层的 Page 组件里,因此可以视为是一个递归的 Container 关系。

    Re-Layout 的时机

    在绝大多数的情形下,Layout 的机制并不是一次就完成的。例如 Tag,我们必须在 Parse 完网页原始码之后,才能根据 Tag的 src 属性得知真正图形文件所放的位置,然后通知网络层进行抓取。在不知道该图形文件是否能正确被抓取回来,以及不知道图形文件的确切尺寸的情形下,微型浏览器仍然必须进入 Layout 的阶段,否则使用者会觉得缓慢而不耐烦。因此在 Tag 进行 Layout 的时候,事实上是先以一个预设的图档大小先进行 Layout,当图形文件从网络层抓回来知道实际尺寸之后,再通知 Tag所在的 Page 重新进行 Layout。

    Re-Layout 的机制是根据事件触发来引起的。例如使用者改变了微型浏览器的窗口大小,此时由于整个窗口页面大小变动,所以必须从最底层的 Page 组件开始,将每个 Item 重新一个一个进行 Layout。但是若只是一些网页内部的组件发生变动,则可以利用一些技巧避免全面性的重新 Layout。我们以之前图七的网页原始码为例子:

    第一次进行 Layou t的时候,由于 Tag里面的 "" 图形文件尚未抓取回来,若是该图形文件确实无法抓取,或是抓取时间过久导致网络层发生 Time Out, 则 Layout 结束,结果如图十五(a)。但是如果后来图形文件抓取回来,则 Image Widget 会发出 Re-Layout 的 Event,通知 Image Widget 所在的

    Container 发生组件大小变动(Resize)的事件通知,要求进行重新 Layout。此时,由于发生 Resize Event 的

    Widget 所属的位置是 Page 组件的 Line1,因此并不影响原来已经完成 Layout 的 Line0,所以 Page 组件只需要从 Line1 重新开始 Layout 即可。而正确的 Layout 结果就如同之前的图十三所示。

    Re-Layout 的程序代码实作其实很容易,以下是部份的原始码:

    第 4 行的程序代码是递归触发 Re-Layout 的 Event 的 Parent。第 3 行的程序代码是为了避免太多组件同时触发 Re-Layout 事件,因此以一个 mFlag 来纪录,等到适当的时机,如微型浏览器进入 Idle 状态或是第一次 Layout 结束,再由底层的 Page 组件开始重新 Layout。由此可以想见,若是图七的

    Tag 里面的

    Page 组件有 Widget 发生 Resize Event, 则该 Widget 会通知

    Tag 的 Page,然后 Page 会通知 Table

    Widget, 再递归通知 Table 所属的 Page 组件,然后进行 Re-Layout,达到动态调整网页的效果。这也是为什么我们浏览网页时,会出现网页重绘,图形文件后来出现的效果。

    还原网页

    Layout 整个步骤完成的时候,事实上在微型浏览器里,是存在着一堆以 Container 型式存在的数据结构,里面串着一连串的 Line List 与 Item List,而最后的步骤便是要将这些 Layout 的结果画到页面上。为了

    避免使用者在拉动微型浏览器窗口发生闪烁的情形,因此我们的微型浏览器采用 Double-Buffer 的方式来处理。也就是先将 Layout 的结果画到 Memory Buffer上,然后再贴到实际的窗口中。

    我们以左上角为原点,将 Memory Buffer 当成画布,然后从最底层的 Page 开始,依序将 Page 对象中的 Line List 一行一行取出来画到画布上。在这里要注意的是:每一个 Line 在没有特别指定对齐方式的时候,都是向左向下(Bottom)对齐,因此在画出每个 Item 的时候,是先计算出 Line 的左下角坐标,然后依序画出来,如图十六所示。这样一来,也可以利用 Line 的宽高信息,轻松达到 Line 的置中对齐或是向左对齐,甚至是 Item 其它不同的对齐方式。

    当遇到的 Item 种类为 tex t时,可以直接藉由改变画笔(pen)的属性,然后画到 Memory Buffer 上面,如果遇到Item的种类是Widget,则这时候就可以藉由多型的方式呼叫 Widget 的 Draw()函式,传入坐标与区间,让 Widget 递归式的依序将整个网页画出来。

    结语

    在本篇文章中,针对 C 语言仿真 C++ 语言的继承及微型浏览器的 Layout 做了详细的介绍,目的是希望让读者了解微型浏览器的运作流程与核心的技术部份。事实上在微型浏览器中,还有许多与网页呈现相关的技术,如 CSS 的支持,以及攸关版面编排最重要的表格(Table)组件等等,都是微型浏览器里重要的技术。我们将在第三篇的文章中,特别针对这两个部分,再做更详细的介绍,让读者对微型浏览器能够有更完整的认识。

    深入微型浏览器 (Micro Browser) 三之三

    信息工业策进会 嵌入式系统实验室

    WAP 技术小组

    1. HTML中卷标样式的处理

    1.1 简介

    卷标样式(Tag Style),是晚近几年W3C所倡导的一个概念,搭配 XML 卷标语言的推动,以 XML 担纲数据内容的处理,「样式设定」则是负责每一个 XML 文件里面标签的呈现,同样的一份 XM L文件,可以在搭配不同的「样式设定」之下,呈现出完全不一样的风貌。

    举例来说,餐桌上摆着一颗白色煮熟的鸡蛋,如果我们拿一块红色玻璃纸将它包住,我们看到的,便是一颗「红蛋」,若是拿一张七彩的玻璃纸将之裹起,我们看见的,便是一颗「七彩蛋」,蛋的本质并没有变化,但是因为外加的装饰不同,而呈现不同的风貌。

    同样的,一份描述当年度销售数字的 XML 文件,有可能是以表格的形式呈现,也有可能是以图形的方式来说明,这当中的差别,W3C 所推动的「样式」标准就会扮演「外观修饰」的关键角色。

    上面所提到的「样式设定」,正确学名是「Cascading Style Sheets」,暂译为「串联式排版样式」,为

    W3C 在 1996 年底开始倡议使用,该份规格是一群模板样式,依序定义 HTML 所规范的相关组件在浏览器上如何呈现,对于浏览器制作者以及网页制作者而言,该规范则提供了一个基础的样式语言标准,可以赖以进行网页的设计或者浏览器对于样式支持的实作。

    该份文件的内容,具体来说就是每一个 HTML 标签的呈现方法,例如某一个卷标的「字型」、「颜色」的变化,该卷标所包含的文字之大小或是字体变化里面的「斜体」、「粗体」等等。

    这样的规范,可以让让 Web 建置者利用「连结」(Link to)或者「汇入」(Import)样式设定的方式,一次控制一份或者多份网页的呈现配置及显示效果。(有兴趣对于CSS的使用方式进行深入研究的读者,可以参考 W3C REC-CSS2-19980512 的第五章,内中对于使用方式有细部的描述)。

    本节有关于样式设定的讨论,并不打算专注在如何使用上面,我们要想陈述的,是我们在实作在嵌入式浏览器的过程当中,对于卷标样式的处理所遭遇到的困境,以及相应的解法,对于卷标样式有兴趣的读者,相信坊间的各式各样教学书籍必定可以满足您的需要。

    在卷标样式的呈现中,有两个问题是需要一些细微的设计的,其一,是每一个网页内中的卷标样式的决定方法,因着会跟网页结构产生关连的关系,所以此部分需要一些特殊的处理;其二,有关样式来源的选定与合并处理,由于同一份文件可能有不同的样式来源,同一个标签亦有可能被两份以上的样式设定进行外观改造动作,因之这个问题在实作浏览器的时候亦必须详加考虑,在这个章节中,我们将会提到这两个问题。

    另外,我们也会讨论在实作层面所需要注意的部分,包括我们对于一般样式处理的解决方式,以及内存最佳化的相关议题,都会在这个段落中触及。

    1.2 卷标样式的基本概念

    在HTML文件规范当中,对于各个标签都有一个预设的样式,来决定被卷标包覆的内容所呈现的外观。例如被「B」这个卷标所包含的文字,字体将会有加粗的效果,又例如被「A」这个卷标所包覆的文字,除了会将内容变成蓝色,并且加上底线之外,如果被包覆的文字内容被使用者点选到,浏览器还必须触发到另一网址的连结动作,又如被「I」这个卷标所包覆的文字,字体将会有斜体的效果出现,诸如此类的设定,都是 HTML 标签规范文件一开始就给定的设定。

    在卷标样式设定的标准出现之后,如果使用者(网页撰写者)想要改变某一段卷标文字的呈现方式,便可以透过样式设定来进行,这样的动作,是由浏览器在审阅样式设定之后,对于该段文字本来的样式设定进行更动,从而在网页上呈现出使用者所想要的样式设定,经过这一连串的动作完成最后的呈现工作。

    举一个简单的实例来说,如果没有搭配任何的样式设定,那么在下面左方的这段卷标语言,所对应的网页呈现将会是右方的图:

    可是我们希望把「B」标签所涵盖的「Bold Text」换成不要粗体,同时间我们希望把「Italic Text」显示成粗体,于是乎,我们修改了 HTML 文件,加入了样式设定,修改之后的原始码在左边,效果呈现在右方:

    如同各位读者所见到的,经过样式的调整,我们的确得到了我们想要的效果,也就是让原本的粗体字不再粗体,斜体字却是有粗体效果。

    真正的样式设定,可以运用的项目相当的多,有兴趣的读者,可以参考坊间有关样式设定的书籍,书本里面会对样式设定的运用以及变化,相信会有比较详细的介绍。

    1.3 样式设定的处理

    1.3.1 样式对象(Style Object)的定义

    在浏览器讨论的第二篇当中,我们曾经提到每个 Item(物品)会有其对应的样式对象(Style Object)来纪录所要呈现的效果属性,这个所谓的「样式对象」,作用就是用来纪录相对于该物品(Item)的样式设定,对于每一个物品都必须要存在这样一个纪录,如此浏览器再处理网页呈现的时候,才能够根据正确的数据与设定呈现对应的效果给使用者观看。

    对应于每一个物品的样式对象,内容的决定并不只限于卷标本身的预设样式,同时间参与决定的,还有包覆在它外围的卷标,以及样式设定的内容针对该特定卷标所设定的样式,这几项因素都会影响最后该物品所欲呈现的外观。

    如果我们单纯以个别卷标特有的属性来处理样式呈现,将有可能会造成卷标样式的错误呈现。举例来说,「B」卷标的预设样式只需处理字型加粗,并不需要去改变内容的颜色,但是当它被「A」标签给包覆着时,实际上还需要另外处理「A」卷标的属性变化,亦即有关颜色的改变。

    左边的结构定义,便是我们在设计样式对象的部分定义内容,可以稍稍理解对于每一个样式设定,都要有对应的字段来纪录,才可以在显示的时候正确呈现使用者所想要看到的效果。

    那卷标样式对象的内容又是怎么决定的呢?在决定的过程当中,共有三个来源会影响属性的值。

    这三个来源分别是,「卷标的预设属性」、「父卷标的属性」以及「CSS 对于该标签的设定」,包含嵌入于卷标内的,以及规则集合内的设定,对于设定的优先权顺序是:

    嵌入于卷标内的 CSS 设定 > 规则集合内的 CSS 设定>

    卷标本身的特色属性 > 父卷标属性

    因此在样式对象(Style Object)中,除了对于每个属性的属性值必须纪录下来之外,还必须纪录这个属性值的"优先权"设定值。

    1.3.2 卷标样式的决定

    为了处理 CSS 的规则集合,浏览器必须有对应的 Parser,将所有的规则置放在一个 List 中。在处理卷标时,将卷标对应的 DOM 节点及样式对象传入查询规则的函式中,如果符合某个选择子的条件,则将对应的宣告区块中的属性设入样式对象。

    我们以卷标为单位。作下面步骤:

    (a)至样式对象的 Stack 中取得父元素的样式对象并复制一份,并且在

    此时将所有属性的优先权值都设成最低。

    (b)根据标签的设定优先权原则决定替换样式对象中的属性。

    (c)将样式对象存入样式对象的 Stack 中。

    当在对样式对象的属性值作修改时,须先检查欲修改属性的优先权是否高于已纪录在对象中的属性优先权,才允许作修改。

    因此,任一属性如果以被优先权较高的来源设定之后,即不会被优先权较低的来源更改。如果规则集合的

    CSS 是来自于外部来源,不需要等到 CSS 设定档案抓回来才能进行样式对象的计算,即可进行第一次 layout。

    底下针对这样的处理,举一个简单的例子。

    在上图的例子中在处理(3)这段时,首先至样式对象的 Stack 中复制一份 Body 的样式对象。因为嵌入在标签的 CSS 的优先权最高,所以更改样式对象的颜色为绿色其它属性则使用 A Tag 的预设属性。因此这段(3)的执行结果为文字为绿色且有底线。

    1.3.3 内存的最佳化

    由于支持 CSS 之后,样式对象所占的内存大小骤增。观察 CSS 的规格中,其实绝大部分的属性值都是特定的字符串,例如在控制字型的粗细使用"font-weight" 属性,其值可能为"normal", "bold", "bolder",

    "lighter" 及 "100"到 "900"。为了节省内存的使用量,我们将样式对象中记录 font-weight 的数据型态从字符串指针改成使用一个只占 1 byte 的整数,将字符串转换为整数值:

    利用这个技巧,可以大幅节省样式对象使用的内存,更适合嵌入式系统上的使用。

    在这个章节里面,我们对样式设定的浏览器处理方式作了概略的描述,相信透过这样的文字,您大概可以对样式的处理有一定程度的理解,我们也阐述了一些在实作层面上的考虑,希望这样的信息对读者有些许帮助。

    2. HTML 中表格(Table)的处理

    2.1 简介

    在前面的介绍当中,我们已经针对网页的取得以及显示技术,做过深入的探讨,在接下来的这个章节,我们将会针对网页设计经常会用到的 Table(「表格」),就实作层面的技术作一番较为深入的研究。

    之所以要另外一个段落来进行 Table 的讨论,原因就在于目前 Table 在网页设计当中的角色已经越来越吃重,从以前纯粹只是一般表格的显示,例如销售数据、格式化数据呈现等等,到现在举凡牵涉到网页版面的编排、定位问题,几乎都是以 Table 的功能来完成,换句话说,目前 Table 的角色,已经不再是单纯的数据呈现那样简单,而是要精确的定位出每一个组件在网页里面,网页作者想要让该组件出现的位置。

    读者如果有兴趣,可以到「Yahoo!奇摩」的网站上,利用一般浏览器都会提供的检视原始码功能,稍微计算一下 HTML 原始码中,有多少是以 Table 来完成的版面设计,应该可以想见 Table 的重要性。

    但是对于浏览器来说,要让 Table 可以正确显示,背后隐含的坐标计算以及相对应的微调动作,难度不可谓不低,因为要考虑的状况不少,加上 Table 本身又可以无限制的包含更多的 Table ,所以在浏览器的设计以及实作上,一直是一项相当令人伤脑筋的问题。

    不过,在真正探讨实作技术之前,还是让我们先对这个顽皮的组件进行一些基础的了解吧。

    2.1.1 Table 的基本组成

    在 W3C 的 HTML 对于 Table 的规范当中,主要组成 Table 的,有下列几个组件:

    真正在规格书里面提到的,还有其它的标签(Tag),不过因为在 Table 的写作上不常用到,所以就没有在这边提起。

    最外围的 Table 标签,用来规范一个 Table 的范围,凡被一个 Table 标签包住的,里面的内容就都是该 Table 要负责显示的部分,举例来说:

    底下这一段 HTML 代码:

    在网页显示的效果上,就会被转化成:

    可以看到,从 Table 的开始到结束,都是属于 Table 显示的范围,在这个例子当中,同时也可以看到 TR、TH、TD 三个标签的作用,两个 TR 分别定义了两列(Row),第一列包含了一个 TH(Header Cell),第二列则是包含了一个 TD (Normal Cell),其中, Header Cell 的部分字体有被加粗,用来作为强调之用,在一般的浏览器显示中, Header Cell 所在的位置通常也会被以置中的排版方式处理。

    2.1.2 进阶的 Table 用法

    在上面那一段文章当中,我们示范了一个 Table 基本的用法,但是 Table 的使用方式还有比较不一样的,请看下面这一段 HTML 代码:

    这一段代码,转化出来的显示效果,如下面的图示:

    可以看到,「Average」那一格横跨了两行(Column),而「Red Eyes」那一格则是横跨了两个列(Row),其中最主要产生这样效果的,就在于 ColSpan 与 RowSpan 这两个属性上, ColSpan 是告诉浏览器,该 Cell

    需要横跨某一特定数目字的 Column ,也就是单一 Cell 的宽度会占用数个 Column 的宽度;RowSpan 的意义雷同,不过是作用在 Row 上,也就是一个 Cell 的高度,将会跨越数列的总和高度。

    读者可以转换一下角色,如果您是浏览器的撰写者,要怎么处理这样的问题,每一个 Cell 的高度与宽度将会决定整个 Table 的大小,而这种会横跨的Cell 其宽度与高度又取决于其它 Column & Row 的宽度与高度总和,再加上可能的微调动作,这样的显示效果对浏览器来说是一项挑战。

    对于 Table 在网页设计上的应用,其实还有更多的功能,不过我们并没有打算在这边多做介绍,若读者有兴趣,可以参考市面上有关于 HTML 网页写作的书籍,相信里面对于表格的制作与特殊应用,将会解释得更为详尽。

    2.1.3 Table 处理与呈现的挑战

    经由上面稍微提及的 Table 应用,以及进阶一些的应用,不难发现,对于网页制作者来说,Table 将会是相当好的数据显示与定位工具,因为表格的特性就是方正整齐,可以依照这样的特性来切割区块,将偌大的版面分割成数个可以掌控的小方块,也可以透过格式化的方式呈现想要让网页浏览者理解的数据或其它需要格式化的数据。

    不过,如果今天不是以网页制作者的角度出发,而是以浏览器撰写者的角度来思考,那么,对于 Table 的呈现动作,将会是一项相当伤脑筋的事情。

    首先要面对的,就是内部数据储存的问题,如何利用简单的数据结构,就可以把所有 Table 相关的资料储存下来,包括了 Table 有哪一些 Cell ,其中对于栏(Column)跟列(Row)的排列方式又是如何,把数据储存下来,才能够在后续的处理动作中随时取用。

    进一步,就得要处理上面所提到的进阶应用,也就是有关ColSpan与RowSpan的部分,这部分牵涉到坐标的重新计算,以及对于每一个 Column 宽度的变动、每一个 Row 高度的变动处理,是另一项挑战。

    另外,一定要处理的,就是对于网页作者无心的错误所造成的逻辑修正,不论是在表格结构上的可能错误,诸如多一格少一格等等,或者是在数字上的不合理错误,都要能够进行某种程度的修正,尽管没有办法百分之百修复到原作者所希望的格式,至少可以让网页看起来接近正常的显示效果,错误校正这一部份,也是需要投注相当心力处理。

    说了这么多关于 Table 所需要做的部分,在底下即将登场的文章当中,我们将会为各位读者一一揭开这诸多问题的神秘面纱,带领读者一探 Table 实作层面的底层原理与技巧。

    2.2 表格实作原理

    2.2.1 基本观念

    表格是一种包含各式各样 Widget 的 Container Widget,所谓 Container Widget,就是可以将各式各样的『对象』放到他自己里面的一种组件,以表格为例,就是各个字段中会置放各种组件,例如:文字、图片、按钮等,甚至是另外一个表格,例如最常见的 Google Search,首页上的logo便是透过表格来呈现的,那一个 Logo 是一个表格,被分割成很多个字段,每个字段只放部分的图片,最后组成一张完整的图。

    在 RockBrowser 中,表格实作会用到的部分,主要有 Table、TableSub、TableCell、TableCellState 四部份,分别执掌不同阶段的处理需求,下面的段落将会仔细说明这四部份所代表的意义。

    首先登场的是 Table 这个结构,它是整个表格实作的主要结构与储存地点,它纪录了其它三个部分的数据结构,同时间也储存了各种表格属性,方便之后的进阶存取;第二个是 TableSub,为 Sub Table 的实作结构,Sub Table 的观念,就是一个复杂的大表格经过拆解,可以简化成一个个的基本小表格,而这些拆解完所产生的小表格便称为 Sub Table。

    更进一步的解说,可以参考下图所示,图中的大表格经过拆解之后,产生出 A、B、C 三个 Sub Table,其中的 Sub Table B 具有 ColSpan 属性,亦即合并了两个 Column 的数据。

    透过这些 Sub Table(s)的协助,将可以大大简化表格的处理动作,使数据结构更为简单易懂,不至于坠入复杂的表格单元(Cell)迷雾之中。

    接着,我们介绍表格中的每一个小格子,称之为一个 TableCell,TableCell 结构内中纪录着各个 Cell 的相关属性数据,为整个表格处理过程当中最基本的一个结构,一个表格是由一个以上的 TableCell 所组成。

    最后一个要登场的,是 TableCellState 的一个结构,负责纪录一个Cell所应该产生的效果,以及属于该Cell各种属性的数据结构,透过这个结构,每个 Cell 可以得知他应该透过什么样的方式呈现,也可以知道相关于它的属性有哪一些。

    透过以上这四个部份的协助,加上负责运作的算法,RockBrowser 即可绘制出合乎语法效果的表格。

    而一个表格是如何从 HTML 来源码经由浏览器到实际呈现出来呢?以下为表格处理的示意图:

    图中标示「DOM」的部分,是所需处理的 HTML 网页中有关表格的各种卷标和属性,「Table」负责处理被表格卷标包含的部分,「Table」接收到「DOM」所传递过来有关表格的数据,透过运用上述四个结构,利用相对应的函式来建构出整个表格架构,包括各个Cell的坐标计算,最后经由 QT 系统的 Widget 来进行绘制动作,把表格应该产生的 Widget,在对应的位置上正确的产生出来,最后显示在浏览器上。

    2.2.2 程序处理流程

    那么,整个表格的处理过程中,又是经过哪些函式而产生出整个表格呢?这个部分大概可以分成「GetLayoutSize」、「Allocate」、「SetWidth」、「SetAscent」、「SetDecent」、「GetExtremes」、「Draw」、

    「Show」等函式,而这些函式所负责的工作以及如何把这些函式连贯起来的详细流程,将会透过下页的流程图进行说明

    以下简单说明这张流程图中,各步骤所负责处理的工作:

    (1) Table Initiation:主要负责整个表格结构的基本设定,参数初始化

    及内存分配。

    (2) Get Layout Size:主要负责整个表格的宽度和高度设定、分配与各

    种效果,包含所有的Cell也在其负责的范围内。

    (3) Get Table Extremes:主要计算出每个表格所能使用的总宽度和高

    度,这个值受限于HTML标签中所设定的值,表格的宽度和高度不

    能超越标签中所定义的值或百分比,之后会针对这个情形进行说

    明。

    (4) Set Table Width:计算并设定整个表格所应该使用合适的宽度。

    (5) Set Table Ascent:计算并设定整个表格所应该使用的基准线以上高

    度值。

    (6) Set Table Descent:计算并设定整个表格所应该使用的基准线以下

    高度值。

    (7) Allocate Table Position:主要负责整个表格所应该显示的XY坐标值

    位置,包含每个Cell在表格中显示的位置,透过这个函式才能使所

    有的 Cell 定位。

    (8) Draw Table:顾名思义,这个步骤主要负责整个表格实际绘制的函

    式,而除了把表格绘制出来之外,还需要处理一些表格的视觉效

    果,例如:是否有边框、边框大小等。

    (9) Show Table:负责把整个建构好的表格显示到浏览器上。

    经由以上的各个函式处理之后,表格数据结构将能够接收 DOM 所传递的卷标或属性数据,并赖以建构出整个符合 HTML 语法的表格,在显示阶段经由 Show 函式把表格显示在浏览器上,也就是一般使用者最后看到的样式。

    上面所提到的,主要是表格显示部分的处理,有关 Sub Table 的部份,将在下面的文章中说明。

    在下页的说明图里面,有一个具有 ColSpan=2 属性的表格,其中含有 A~H 共八个 Cell,我们最后的目标,是希望把表格拆解成只有一个个只有单行的 Sub Table,在分解简化表格为 Sub Table 的过程中,函式会逐步对表格进行简化动作,化约到最后一步,便可以转成我们所希望的,只含有单行的 Sub Table。

    依照这样的处理程序,一开始,下图的 A、C、F 三个 Cell 会先分解成一个 Sub Table,依照我们上面所提过的的标准来看,这已经是单行的 Sub Table,所以这部分简化到此结束,接下来就轮到右半边的部分。

    右半边由于有 B 这个具有 ColSpan 属性的特殊 Cell,因此在处理的时候会先把 B、D、E、G、H 等五个

    Cell 所组成的部分,全部当成一个 Sub Table,之后再进行处理。

    接着,先把 B 这个特殊 Cell 先从这个 Sub Table 中移除,剩下的部分做进一步的简化分割,结果将会产生由 D、G 和 E、H 所组成的两个单行 Sub Table,由于这两个 Sub Table 也符合我们对于 Sub Table 的单行要求,于是整个表格分割完毕,不再进行简化动作。

    接下来的处理流程,就是按照之前所提及 Table 处理的流程,继续往下,直到把表格显示在浏览器上为止,由于前面已经提及,在此不再赘述。

    接着,我们将讨论在表格的处理过程当中,会遇上的一些较为特殊之问题,以及在 RockBrowser 中我们大致的解决方法。

    2.3 特殊问题处理

    2.3.1 ColSpan 和 RowSpan 属性

    在 HTML 中有关表格的属性有很多,其中较为特殊,常用来设计整个表格架构的,大概就是 ColSpan 及

    RowSpan 这两个属性,这两个属性对整个表格的设计上有什么影响,以及能呈现什么样的效果呢?以下先用图示进行说明:

    在图一里所绘制的表格,为一般没有特殊属性的表格,这个例子中的表格一共有三行三列,直的为行、横的为列。

    图二所表示的为图一加入 ColSpan 属性后所产生的表格,其中原本第一列的第二和第三字段被一个具有

    ColSpan=2 属性的 Cell 所取代,形成一个跨越两个 Column 的大 Cell。

    图三则是在图一中加入了 RowSpan 属性后所产生的表格,其中第一行的第二和第三字段被一个具有

    RowSpan=2 属性的 Cell 取代;利用以上这两个属性,能够让表格中的 Cell 得以进行各种千变万化的组合,用以产生出使用者所需的各种表格格式。

    然而为了处理这两种属性,表格的处理上必须加入特殊的方法来做对应的处置,首先,要在处理基本数据的时候,在每个 CellState 里纪录是否有使用到 ColSpan 或 RowSpan 属性,若有则一并纪录从 Span 开始到终止的 Cell 索引值(Index),这份数据,对于之后在处理分割简化表格时,将会对表格显示的正确性有甚大的帮助。

    每个 Sub Table 会保留具有 Span 属性的部分,然后进行 Span Cell 宽度、高度、位置等属性值的计算,这些属性值计算正确,将来的表格显示也就不会产生错误,特别是最后显示到浏览器时,Span Cell 网格线绘制问题也需要靠这部分的数据来处理。

    2.3.2 巢状表格

    HTML 有关表格的语法中,表格内中允许包含多个表格,因此巢状表格也成为表格处理及绘制上会面临的难题。

    为了解决这个棘手的问题,在微型浏览器中使用了 Page 这个组件(详见第二篇),而表格正是利用这种特性来进行 Layout,左图中空心方框代表表格,实心方框代表 Page,在每个表格里都先放入一个 Page,而 Page

    也可以包含表格。

    利用这个数据结构,表格可以放入其它的表格,而其它的表格里也有包含 Page 这个结构,因此也可以包含其它的表格,如此循环下去,直到内存用完为止,如下图所示,因此利用 Page 这个结构即能解决巢状表格的难题。

    2.3.3 表格宽度高度属性使用百分比表示法

    HTML 表格语法中包含宽度和高度的属性,透过这两个属性可以调整整个表格或是某行某列的宽度或高度,网页作者在设定这些数值时,可以使用绝对的数值,同时间也可以使用相对的百分比值,例如作者可以这样写:

    -> 表示表格的宽度是目前浏览器的50%

    使用百分比数值,对浏览器而言在表格的处理上会是一个难度较高的问题,因为百分比值是一个相对的数值,实际需要把表格绘制并显示出来时,在真实数值与相对数值之间的计算与转换,还有一些边界值的微调动作,都需要额外付出心力去处理。

    假如浏览器被使用者重新调整大小,一开始透过计算所求出的实际数值变会失去意义,实际宽度或高度值需要动态根据目前浏览器的状态进行改变,以符合原先网页里面 HTML 语法对于表格的定义,为了达成这个目的,在 RockBrowser 中使用一些特殊数据结构与算法来处理这类的问题。

    首先,利用一个取得表格高度与宽度临界值的函式,获得表格所能够使用的「最大、最小」宽度和高度,由于「Table」和「TableCellState」这两个结构都会纪录该表格所设定的百分比数值,接着就会利用这两个数值,根据目前表格的状态取出的相应高度与宽度值,进行相乘的动作,这样可以顺利取得该表格宽度或高度实际上应该配置的大小。

    当使用者动态的改变浏览器大小,RockBrowser 即会马上进行重新绘制的动作,当然表格也会依照百分比设定,以及现在浏览器的实际可视范围大小再次重新计算与调整,并且动态的改变表格宽度或高度,呈现出表格该有的效果。

    2.4 结语

    在 HTML 中,表格算是一个比较复杂的部分,因为它具有动态改变大小、巢状表格、ColSpan 及 RowSpan

    等特性,也因此浏览器在处理上,需要比较复杂的数据结构及函式的配合。

    现今大多数的网站几乎都是使用表格进行各种编排,如果能够完整的呈现出表格该有的样貌,那浏览器的

    Layout 功能可以说是完成了一大半,就现在的网页制作情形而言,若称表格为完整呈现网站的核心部分想来是一点也不为过。

    在 RockBrowser 的撰写过程当中,我们采用了许多特殊的结构和函式来处理表格的绘制及显示问题,经由这些步骤,使得表格能依照使用者所设定语法,完美的呈现在浏览器上。

    经过上面的说明,相信您对于表格的呈现以及实际运作的方式,必定已经有一定程度的了解,当然,限于篇幅的关系,我们并没有办法将实作过程当中遇到的所有问题在这边完整呈现,我们也不敢说我们对于表格的处理的方式就是一个很好的方法,倒是希望能够借着这样的机会,提出一个真实可以运行的实作方法,特别是在表格处理的部分,达到技术交流的目的,至于真正在实作层面会遇到的,较为琐碎的问题,或许之后有更多的机会,将会说明得更为深入一些。

    2.5 总结

    在现今的网络应用中,浏览器已经成为必备的工具之一,而随着行动通信与无线网络的发展,在嵌入式平台置入微型浏览器来整合通信与网络,更是未来的趋势。然而坊间对于介绍微型浏览器的书籍着墨不多,相关的资料也是屈指可数。有鉴于此,我们希望将我们发展微型浏览器的经验,以浅显的文字,简单的概念,帮读者揭开微型浏览器神秘的面纱。在这三篇专栏文章中,我们从微型浏览器的起源,技术的分析,核心问题的探讨等等,尽力的加以描述,希望带给读者一个整体的概念。不过碍于篇幅的关系,无法针对详细的实作部份做更进一步的交流,但仍期望引起读者的共鸣,发展自有的技术,在可预见的未来占有重要的一席之地。

    本文标签: 浏览器表格对象样式

    更多相关文章

    xp系统

    Python+selenium 自动化-启用带插件的chrome浏览器,调用浏览器带插件,浏览器加载配置信息。

    20小时前

    正常的话我们启用的 chrome 浏览器是不带插件的,如果你能登陆 chrome 的话,你会发现登陆信息也没有,还有不管你怎样设置每次新打开的 chrome 都是默认设置的。

    chrome浏览器调用摄像头

    20小时前

    chrome浏览器调用摄像头 var 是定义变量var canvansdocument.getElementById("canvas");var videodocument.getElementById(&q

    chrome浏览器的各个历史版本下载

    20小时前

    最近做一个项目,要使用到chrome浏览器比较久远的版本,在网上查找资源时,发现chrome比较老的版本的安装包特别难找,几经寻找,总算找到,具体方法如下 打开百度,搜索关键字【chrome版本号‘浏览迷’】,例如“

    使用 selenium 连接已经打开的 chrome 浏览器

    20小时前

    最近做在一些 web 自动化(其实是用 web 端来配置网络设备) 编写脚本时经常用到 debug 去提取网页中的一些元素并做测试,但是每次需要 debug 时都要打开新的浏

    chrome浏览器怎么模拟手机访问网页(已測OK)

    20小时前

    谷歌浏览器怎么模拟手机访问网页?简单几步,让你轻松解决。 工具原料 谷歌浏览器网络环境方法步骤 1.   双击图标,打开谷歌浏览器,如下图

    在Chrome浏览器查看控制台提交的参数

    20小时前

    在Chrome浏览器查看控制台提交的参数 向下滑动

    将该项添加到 Chrome 浏览器时出错。请刷新此页面,然后重试

    20小时前

    在下一个扩展的时候发现装了好久还是显示“正在检查”,就手动刷新了一下页面,准备重新安装,然后就跳出了标题中的错误,一直安装不上,捉

    Chrome浏览器如何格式化查看JSON数据?使用方法分享Chrome浏览器

    20小时前

    1.添加浏览器插件,JSONVIEW 这样数据直接就能自动Json格式化了 2、如何格式化显示JSON数据? 按下F12(我的电脑是FnF12),打开开发者工具&

    谷歌chrome浏览器设置成深色(dark)模式

    20小时前

    两步实现: 1.在谷歌浏览器输入网址的对话框,输入:chrome:flags 回车 进入下图:2.在搜索框内输入dark&#xff0c

    chrome浏览器发送POST请求

    20小时前

    fetch(new Request(url,{method:POST, headers: {Content-Type: applicationx-www-form-urlencoded},body:"param1value1&a

    在Linux上使用Selenium驱动Chrome浏览器无头模式

    20小时前

    大家好,我们平时在做UI自动化测试的时候,经常会用到Chrome浏览器的无头模式(无界面模式),并且将测试代码部署到Linux系统

    谷歌浏览器内存不足 Chrome浏览器内存释放技巧分享

    20小时前

    谷歌Chrome浏览器内存占用过高的问题一直困扰着不少使用者,除了少用占用内存的扩展插件及控制打开的标签页数量外,还有不少方法可以解决这一问题!下面小编就来分享一个快速释放Chrome浏览器内存的

    有关chrome浏览器F12看不到请求头的解决方法

    20小时前

    学html的时候发现F12看不到请求头,当时就懵了,然后谷歌刷刷刷告诉我答案了,防止以后忘记,以此记录下来。 先F12,发送请求&

    最新版本的Google Chrome浏览器如何设置网页编码?

    20小时前

    前言:由于使用频率较低,以及促进网页编码规范,Google Chrome浏览器在55这个版本以后删除了手动设置网站编码的功能。但是对于部分没有设置编码或编码设置不正确的网站,新版Google Chrome浏览器可能会因为无法准确判断其使用的

    转:Chrome浏览器查看网站登录 Cookie 信息的方法

    20小时前

    当我们使用自动签到等程序的时候一般都要用到网站Cookie,我们可以借助浏览器的扩展来获取Cookie信息,但其实通过浏览器本身的功能就可以查看Cookie信息。以Chrome类浏览器为例有以下三

    Chrome 浏览器如何完美实现滚动截图技巧,在电脑上实现长截屏

    20小时前

    一、前言 我们平时在浏览网页时,想把碰到好的网页内容或者文章截屏保存,但是网页的长度常常会超出屏幕高度,一般的截屏功能只能截取显示在屏幕上的内容,那我们

    解决chrome浏览器ERR_CONNECTION_RESET报错

    20小时前

    如题: 解决方法:刷新DNS解析缓存即可。 解决步骤: 1、windowsr ; 2、输入cmd打开后; 3、输入ipconfi

    Chrome浏览器控制网速

    20小时前

    在测试的时候有时候需要控制网络速度,可以在浏览器,F12打开开发者模拟后,在NetWork面板中去设置网络的通信方式。默认是正常模式,这里可以设置断网模

    Python使用selenium附加已经打开的Chrome浏览器

    20小时前

    网上的代码基本上都是新打开一个浏览器示例,这样的打开方式有一个缺点,就是如果网页需要输入账号密码的时候比较麻烦,每次都需要输入。我们平时使用浏览器的时候&#xff0c

    ubuntu16.04安装Google浏览器和搜狗拼音输入法_ubuntu一站式配置教程(二)

    3小时前

    Google浏览器和搜狗拼音输入法安装教程 Google浏览器和搜狗拼音输入法安装教程Ubuntu一站式配置系列链接:Google浏览器安装:sougou:Ubuntu一站式配置系列链接&am

    发表评论

    全部评论 0
    暂无评论

    最新文章