第 0.1 节 HTML5 和桌面软件开发的碰撞
当我们谈论桌面软件开发技术的时候,你会想到什么?如果不对技术本身进行更为深入的探讨,在我的世界里,有这么多技术概念可以被罗列出来(请原谅我本质上是一个 Windows 程序员的事实)。
操作系统 API 。操作系统发展到今日,几乎桌面应用的所有功能,都是基于系统 API 构建的。调用 API 和语言及技术无关,哪怕是使用汇编。例如(代码来源于网络,本地重新编译):
; 我的第一个 win32 汇编程序 ; 一个经典的 hello world ! 程序 ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> .386 .model flat,stdcall option casemap:none ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ; 头文件的定义 ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> include windows.inc include user32.inc include kernel32.inc includelib user32.lib includelib kernel32.lib ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ; 数据段 ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> .data szCaption db ' 我的第一个 win32 程序 ',0 szText db 'hello world !',0 ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ; 代码段 ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> .code start: invoke MessageBox,NULL,offset szText,offset szCaption,MB_OK invoke ExitProcess,NULL ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> end start
代码清单 0-1 汇编 MessageBox
在代码清单 0-1 中,通过汇编调用 MessageBox Api 来呈现一个简单窗口程序。
代码清单 0-1 的运行结果如下:
图 0-1 代码清单 0-1 运行结果
同样的,我们使用 c/c++ 来调用这样一个 win32 API ,代码可能是如下这样的:
#include "windows.h"
int main()
{
MessageBox(NULL, (LPCWSTR)L"Hello world!",
(LPCWSTR)L" 我的第一个 win32 应用程序 ", MB_OK);
return 0;
}
代码清单 0-2 c/c++ 版 MessageBox
代码清单 0-2 运行结果如下图:
图 0-2 代码清单 0-2 运行结果
在系统 API 之上,经过抽象与封装在各个操作系统上,形成了各自的所谓的库和框架。比如 windows 的 MFC 和 Delphi 等, Linux 的 Gnome 、 GTK+ 、 KDE 等, Max OS X 平台的 Cocoa 开发库。对于系统 API 的强依赖性,直接导致的问题是桌面应用的可移植性,开发人员不得不针对不同平台的操作系统(即使同一平台也不一定能良好兼容)编写不同的代码。另外即使你已经编写了不同的代码来适配不同的操作系统和平台,仍然没有办法保证桌面应用的 UI 和交互是一致的,这一点上有的开发者认为一致反而是障碍,因为不同平台下的用户的桌面应用的使用习惯是不一样的。但是 UI 呢?我觉得保证 UI 一致是极其有必要的。
笔者接触到的最早的跨平台桌面 UI 库是 Qt 。
Qt 是一个跨平台的 C++ 图形用户界面库,由挪威 TrollTech 公司出品,目前包括 Qt , 基于 Framebuffer 的 Qt Embedded ,快速开发工具 Qt Designer ,国际化工具 Qt Linguist 等部分 Qt 支持所有 Unix 系统,当然也包括 Linux ,还支持 WinNT/Win2k , Win95/98 平台。
图 0-3 Qt
上文中提到的 Linux 的 KDE 就是 Qt 的杰作。 Qt 做出了两方面的努力,都很成功,一个是软件 UI , Qt 在 UI 方面展现了独特的效果,这种效果脱离了所依赖的操作系统的桌面风格,提现了桌面软件在交互体验方面的需求;另一个方面是跨平台性,它同时支持 windows 和 Linux ,在跨平台的同时保证了自身 UI 和交互效果的独立性。
值的一提的是,对于桌面软件的 UI 和用户体验, Linux 和 Os X 从一开始就做得很好,相反 windows 一直在快速开发上做文章,这一点一直到 .NET 的 Winform 都没有什么大的改变。我们不能说在 windows 上做不出炫酷的或者交互良好的桌面软件,毕竟强大的系统 API 能让我们无所不能,但是这是开发者的追求,不是这个技术体系的给我们的引导,结果是大多数 windows 桌面软件都是灰色的,几乎没什么好的交互效果(这可能有点偏激)。
现在我们简单总结下,桌面软件开发有两方面的问题成为制约:
1) 跨平台性
2) 低成本的 UI 和交互自定义
对于跨平台性,上面我们提到应用程序的底层是系统 API ,系统 API 具有天然的系统隔离性,对于开发人员处理这种兼容问题难度往往要大于实现应用程序本身。即使是 Qt 这样的 UI 库,也根本解决不了问题, UI 库可以移植,单应用程序本身不能移植。随着 python 和 Java 这样的具有独立运行时的框架出现之后,跨平台的问题似乎看到了曙光。在操作系统 API 和应用之间加了一个隔离层,解放了开发者。微软的 .NET 也模仿了 Java ,但是只是实现了在 windows 各个不同的系统之间的可移植性(微软现在也加入了开源大军, .NET 也可以支持在 Linux , OS X 上运行了)。虽然运行时本身还具有系统的强依赖性,但是大多数开发者而言我们可以忽略这些,关注框架提供的基础类库而不是系统 API 。
跨平台性似乎暂时得到了好的解决方案(虽然并不完美,但是从生产力的角度确实得到了空前的提高,我们暂且认为问题得到了解决),那么 UI 和交互呢?顺着刚才的路线去想,在可跨平台的语言基础上,构建强大的 UI 库是不是就解决了这个问题呢?确实有人在这样做,但是却没有真正的成功者。问题出在哪里了呢?
在语言和框架发展的过程中,尤其是互联网的发展,专家们抽象和发展了应用程序的基础功能,比如文件访问、网络请求、压缩解压缩、加密解密等等,这些内容都被集成到了可跨平台的基础类库中, UI 和交互一直做为附属品,在这些语言和框架中没有得到足够的重视。但是是人们不重视 UI 和交互吗?答案是否定的,随着互联网的发展, UI 和交互越发的得到重视,而且空前发展, UI 和交互有了单独的语言来处理和定义 —— HTML 和 CSS 。可是遗憾的是这两门语言并没有运用到桌面应用里来,在编程领域出现了前端和后端的划分,出现了 C/S 和 B/S 的划分,出现了专门的前端程序员和后端程序员,却没有桌面程序员。这是历史的发展,我们无可厚非,而且要快乐的接受。 HTML 和 CSS 是全新的语言,和 c/c++ 、 Java/C# 、 Python 都有本质的区别,首先它面向 UI 和交互,可以近乎精准的还原设计;其次它们是声明性语言,不是命令性语言。声明性语言为设计而生,你只需告诉它我要个黑色背景就可以了,这是语言层级的支持,而不像命令式语言想的是如何实现一个黑色背景。除了 HTML 和 CSS 之外,和它们绑定到一起的还有 Javascript ,一门很长一段时间只能运行在浏览器中同 DOM 进行交互的语言。
现在我们再回头看桌面软件开发,在 UI 和交互方面没有办法和网页端应用相比,这是从诞生开始就注定的宿命。在网页端应用飞速发展这些年里,尤其是 HTML5 出现之后,人们仿佛觉得桌面应用已经日落西山了,早晚有一天会消亡。虽然桌面应用的开发者数量在减少,构建在纯桌面环境的的应用也越来越少,但是桌面环境并没有要消失的迹象,即使是浏览器本身也仍然是一个桌面应用,它也只能完成桌面应用的一小部分功能,只要你要使用桌面,就会有桌面应用的需求。
桌面应用开发技术也没有止步,并和浏览器技术一步步融合。
融入互联网,融入 web 是人类生活的需求,同时也是桌面软件开发技术的需求,在软件内部嵌入和控制网页成为最初的诉求。于是浏览器的功能被精简,成为组件被引入桌面软件中,微软凭借自家浏览器技术的强项在 .NET 中引入了 WebBrowser 控件,这一举措方便了开发者,同时因为 WebBrowser 控件强依赖系统安装的浏览器,微软的浏览器又和系统依赖过强,导致控件在不同的客户系统上的展现行为也会有差别。当然离跨平台又远了一步。
图 0-4 WebBrower 控件示例
同时我们也应该看到控件的方式虽然精简了浏览器功能,但是也扩展了 Web 应用的能力,控件是可以和调用者进行通信的,也就意味着控件是可以通过“后端代码 ” 访问本地资源的。但是在这一方面并没有长足的发展。同时 Google 开源了 Chromium 项目,基于 C++ 的 CEF 项目,将 Chromium 进行改造使之成为一个控件,相对于微软的 WebBrowser 控件,这一举措意义很大。 Chromium 是开源的,可以更好的和调用代码进行交互,甚至可以扩展 javascript 接口,使之可以调用操作系统资源。
随着 web 应用的发展,浏览器由于本身的定位和安全特性的限制,很多需要和客户端交互的功能无法完成,于是出现了浏览器扩展的概念,但是扩展也不是无限制的。这方面微软对浏览器的扩展最为粗暴,它直接支持 Activex 控件,几乎可以无限制访问本地资源,但是同时也打破了浏览器安全特性,这也是一直到现在很多银行的网银只支持 IE 浏览器的原因。其他浏览器也在这一方面做出了妥协,浏览器的 Js 或者本地扩展功能都被支持起来,不过仅仅是妥协而已,因为浏览器的使命不是开发桌面应用。
在这期间,微软做了很大的尝试,首先是基于 .NET 框架的 WPF ,微软推出了 XAML 语言,全新的声明性语言,想让开发者像写 HTML 一样编写软件的界面和交互,这不正是广大开发者的心声吗?可以说 WPF 是很成功的产品,使用 WPF 我们已经可以能够开发出炫酷的桌面软件了。但是从跨平台性角度讲,受 .NET 本身的制约,另外并没有斩掉开发者和设计师之间的鸿沟。它仍然是传统桌面软件的延伸,面向的是仍然是后端开发人员,前端开发、交互设计师、 UI 设计师并没有被引入进来。
图 0-5 WPF
微软在这个方向上并没有止步,随着 windows8 操作系统的推出, Windows Runtime 浮出水面。微软运行使用 HTML5 和 Javascript 开发 WinRT 的应用,看起来非常美好的一件事情,但是在微软手里却多出了很多遗憾。虽然我们可以使用 HTML5 和 Javascript 开发应用,甚至在移动端,但是这些应用只能运行于 Windows Runtime 环境,连 Windows8 的传统桌面环境都不可以,更不要谈什么跨平台了。原因是微软直接扩展了 Javascript 类库,映射到 Windows Runtime 的底层 API 上。
图 0-6 Windows Runtime
这期间很多人也在尝试直接把 B/S 开发模型转移到桌面开发中,简单理解就是在本地启动一个 WebServer 负责访问本地文件系统, UI 端通过扩展将请求发送到 Server 再回调回来。这种方式看起来简单,实则实现起来很负责,涉及到通信机制的改造。豆瓣曾经发布 OneRing 项目,使用类似的机制,后端使用 Python 来处理业务。
不论在两个方向上如何融合,前与后的本质区别并没有被打破。因为通过修改浏览器代码取一点点扩展 Javascript 使之成为超级浏览器,也不是不可取,只不过这期间的工程量还是很大的,腾讯的 Webtop 项目就是基于这个想法进行的,不过已经夭折了。本质上还是由于 HTML 的发展制约,浏览器厂商不去让浏览器足够强大,第三方很难做到。所幸 HTML5 和 Node.js 出现了,并被认可和发展壮大起来。
关于 Html5 的新特性,这里我不展开论述,读者自行搜索,总之一句话, Html5 带来了翻天覆地的变化,使 web 应用在功能上可以更像桌面应用了。而 Node.js 的诞生,直接打破了 Javascript 只能寄宿于浏览器端的限制,直接走到了大后方,在 Node 运行时上, Javascript 可以和其他后端语言一样访问本地资源, “ 为所欲为 ” (目前 Node Js 的基础类库还没有办法和其他后端语言相比,但是语言的功能本质发生了变化,在一个方向上了)。
图 0-7 Html5 新特性
图 0-8 Node js
这里要再次提到 Google 开源,它开源了浏览器引擎及 Javascript 引擎 V8 ,开源使得很多有梦想的程序员可以插上翅膀。于是乎这样的想法 —— 打破浏览器的安全沙箱,让浏览器支持 Node Js ,前后端通吃 —— 也就正常了。
因为 Node Js 使用的也是 V8 引擎,所以改造浏览器去兼容 Node Js ,同时再根据桌面窗口的特性去扩展些 API 出来,从技术上讲小团队也是可以实现的。前端开发者也很容易加入到桌面软件开发的大潮中。同样一款应用, web 端和桌面端可以共享一套设计和交互,甚至是同样的 HTML 和 CSS 以及负责交互的 Javascript 代码。基于 Node Js 去实现后端业务逻辑,可以和前端代码无缝整合,这是目前理想状态下的桌面软件开发环境。我们可以写类似这样的代码:
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8" />
<title></title>
</head>
<body>
<script>
var process = require('./addon')
console.log(process.getProcessList());
</script>
</body>
</html>
代码清单 0-3 在 html 页面中调用 nodejs
在浏览器中直接集成 Node Js ,是我们目前看起来技术实现上难度不高,同时可以为桌面软件开发带来新希望的方式。下面我们来看看开源界都做了什么。
hex 。官方网站 http://hex.youdao.com/ 。 heX 提供了一种全新的构建桌面应用的方式,可以使用 web 技术快速构建跨平台的桌面应用。 heX 基于 CEF 并且融合了 Chromium 与 Node.js ,所以我们可以在 web 页面中使用各种 Node.js 原生模块及第三方扩展,同时在这些模块及扩展中还可以访问到 HTML 中的 DOM 元素。此外, heX 甚至可以以一种 web 容器的方式嵌入到桌面应用的工程中。
项目目前处于停滞状态。
图 0-9 hex
appjs 。官方网站 http://appjs.com/ 。实现了 Html+nodejs 开发桌面软件的功能,项目目前处于停滞状态。
nw.js 。官方网站 http://nwjs.io/ 。引用作者话说 “ 通过 Node.js 和 WebKit 技术的融合,开发者可以用 HTML5 技术编写 UI ,同时又能利用 Node.js 平台上众多 library 访问本地 OS 的能力,最终达到用 Web 技术就可以编写桌面应用的目的。实现上是基于 Chromium 项目的 Content Layer 构建 (Chromium Browser 也同样基于 Content Layer );实现上的特点是把 Node.js 的消息循环( libuv )和 Chromium Renderer 进程的消息循环合并到一起,因为这样才能从 DOM ( HTML )中直接调用 Node.js 提供的函数;把 Node.js 使用的 V8 引擎和 Chromium 的 V8 引擎合并,使得 Node.js 的 Javascript 和 DOM 里面的 Javascript 可以互相访问;另外因为是支持本地应用,所以安全模型和 Web 程序有很大不同: nw.js 程序可以做 web 应用不允许做的很多事情,除了通过 node.js 访问本地 OS 以外,还可以进行跨域访问等操作。 ”
图 0-10 nw.js
nw.js 目前是该方向上受关注度最高的项目,而且一直在持续更新。
electron.js 。官方网站 http://electron.atom.io/ 。 electron.js 和 nw.js 有着千丝万缕的关系,其前身是 github 大名鼎鼎的 atom shell 。是目前最活跃的使用 web 技术开发桌面软件的开源项目。包括 github 的 atom 和微软的 Visual Studio Code 都基于 electron.js 开发。
图 0-11 electron.js
目前为止,我觉得值得和大家介绍的项目就这么多(当然可能有更好的),这些项目为桌面软件开发打开了新天地,让 web 开发技术和桌面软件开发技术完美的融合在一起。大家可以到相关的网站去了解项目的详细信息。从此桌面软件开发有了新的技术体系, html5+node.js 。当然探索还没有止步,比如 edge.js ( https://github.com/tjanczuk/edge ) , 打通 了 node js 和 .NET 运行时,可以实现互调,那么我们也是可以 node js 为桥梁把复杂的业务逻辑封装到 .NET 中。微软的开源项目 .NET Core ,也让很多人产生了新的想法,是否可以将 .NET Core 运行时直接打包到浏览器中,将 .NET 类库直接生成 Javascript 接口供网页中的 js 调用呢?
图 0-12 .NET Core 5
在将近两年的 HTML5 桌面软件开发过程中,虽然整体过程是愉快的,但是不可避免的遇到很多问题甚至是无法克服只能绕过的“坑”。两年里我主要使用的框架是 nw.js( 那时还叫 node-webkit) ,也在博客上零星的写了一些 nw.js 入门的教程,虽然不成体系,文章数量也不多,但是仍然是国内最全的教程了。 nw.js 也在不断的迭代更新,于是我产生了重新动手,写一本完整的书来记录两年来的开发经验,这里面重要的不是 nw.js 如何使用,重要的是使用 Html5 和 node.js 开发桌面应用我们应该怎么做,会遇到什么问题,如何去解决。
在下一节,会给大家阐述下 nw.js 的基本实现原理。