Deepin + CrossWalk 跨平台 Web 應用開發 - 2

简介

上一篇我们介绍了什么是 CrossWalk, 并做了个最简单的 Hello World 例子, 本篇中我们来学习下如何扩展 CrossWalk 的功能.

扩展开发

如果 HTML5 + Codova 还没法满足应用开发的要求, 前面提到, CrossWalk 可以多种方式进行扩展开发, 让 web 应用能更接地气 (使用更多特定平台上的功能). 在 Deepin 上开发扩展的话需要以 C 进程完成 (至少在接口的集成部份是如此), 下面的例子说明如何让 JavaScript 可以控制 GTK+ 的窗口显示跟隐藏.

首先装上开发 C 扩展需要的工具

$ sudo apt-get install build-essential pkg-config libgtk-3-dev

接着下载扩展接口定义的头文档

$ wget https://raw.githubusercontent.com/crosswalk-project/crosswalk/master/extensions/public/XW_Extension.h

扩展接口是跟 CrossWalk 沟通重要的桥梁, 进入点是 XW_Initialize() 同时也是唯一需要对外曝露的部份. 在这个函式中要做的事是将扩展自身的信息介绍给 CrossWalk, 代码片段如下

#include <stdio.h>
#include <glib.h>
#include <gtk/gtk.h>
#include <string.h>
#include "XW_Extension.h"
...
static const XW_CoreInterface *core_iface = NULL;
static const XW_MessagingInterface *msg_iface = NULL;
...
int32_t XW_Initialize(XW_Extension extension,
                      XW_GetInterface get_interface)
{
    static const char* api =
      "exports.show_win = function() {"
      "  extension.postMessage('show');"
      "};"
      "exports.hide_win = function() {"
      "  extension.postMessage('hide');"
      "};";

    core_iface = get_interface(XW_CORE_INTERFACE);
    core_iface->SetExtensionName(extension, "hello_ext");
    core_iface->SetJavaScriptAPI(extension, api);
    core_iface->RegisterInstanceCallbacks(extension, inst_created, inst_destroyed);

    msg_iface = get_interface(XW_MESSAGING_INTERFACE);
    msg_iface->Register(extension, inst_handle_message);

    gtk_init(NULL, NULL);

    return XW_OK;
}

以上代码配合图解说明如下

  1. CrossWalk 加载扩展, 调用 XW_Initialize() 让扩展有机会进行初始工作, 此时扩展可以取得 XW_CORE_INTERFACE 的实例并告知名字为 hello_ext
  2. CrossWalk 为每个应用产生一个扩产的实例, 在 XW_Initialize() 中可以 RegisterInstanceCallbacks() 注册回调, 以便于实例产生及消毁时做相应的初始及清理动作
  3. 加载应用, 关联扩展
  4. 应用中的 JavaScript 可调用扩展曝露的接口. 这个接口是经由传入 JavaScript 代码给 core_iface->SetJavaScriptAPI(), 在其中, 将要开放出来的变量或函式加到 exports 对像中
    • 为了让应用有最高的响应速度, 同时也避免多线程间的同步问题, 对扩展的调用不是立即就处理, 而是以消息的方式发给了扩展, 扩展要处理消息的话, 需要调用 msg_iface->Register() 注册处理回调

接到产生实例通知时, 建立一个 Gtk+ 窗口

static void inst_created(XW_Instance inst)
{
    GtkWidget *win = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    core_iface->SetInstanceData(inst, win);
}

接到消毁实例通知时, 释放相应 Gtk+ 窗口

static void inst_destroyed(XW_Instance inst)
{
    GtkWidget *win = GTK_WIDGET(core_iface->GetInstanceData(inst));
    core_iface->SetInstanceData(inst, NULL);
    gtk_widget_destroy(win);
}

收到 showhide 消息时, 分别显示及隐藏 Gtk+ 窗口

static void inst_handle_message(XW_Instance inst, const char *msg)
{
    if(! strcmp("show", msg)) {
        g_idle_add(inst_handle_show, GINT_TO_POINTER(inst));
    }
    else if(! strcmp("hide", msg)) {
        g_idle_add(inst_handle_hide, GINT_TO_POINTER(inst));
    }
}

showhide 消息是由扩展自已开放出去的 JavaScript 代码被应用调用时所发出

    static const char* api =
      "exports.show_win = function() {"
      "  extension.postMessage('show');"
      "};"
      "exports.hide_win = function() {"
      "  extension.postMessage('hide');"
      "};";

完整的 C 代码如下

#include <stdio.h>
#include <glib.h>
#include <gtk/gtk.h>
#include <string.h>
#include "XW_Extension.h"

static const XW_CoreInterface *core_iface = NULL;
static const XW_MessagingInterface *msg_iface = NULL;

static void inst_created(XW_Instance inst)
{
    GtkWidget *win = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    core_iface->SetInstanceData(inst, win);
}

static void inst_destroyed(XW_Instance inst)
{
    GtkWidget *win = GTK_WIDGET(core_iface->GetInstanceData(inst));
    core_iface->SetInstanceData(inst, NULL);
    gtk_widget_destroy(win);
}

static void inst_handle_message(XW_Instance inst, const char *msg)
{
    GtkWidget *win = core_iface->GetInstanceData(inst);
    if(! strcmp("show", msg)) {
        gtk_widget_show(win);
    }
    else if(! strcmp("hide", msg)) {
        gtk_widget_hide(win);
    }
}

int32_t XW_Initialize(XW_Extension extension,
                      XW_GetInterface get_interface)
{
    static const char* api =
      "exports.show_win = function() {"
      "  extension.postMessage('show');"
      "};"
      "exports.hide_win = function() {"
      "  extension.postMessage('hide');"
      "};";

    core_iface = get_interface(XW_CORE_INTERFACE);
    core_iface->SetExtensionName(extension, "hello_ext");
    core_iface->SetJavaScriptAPI(extension, api);
    core_iface->RegisterInstanceCallbacks(extension, inst_created, inst_destroyed);

    msg_iface = get_interface(XW_MESSAGING_INTERFACE);
    msg_iface->Register(extension, inst_handle_message);

    gtk_init(NULL, NULL);

    return XW_OK;
}

Makefile 如下

CFLAGS=-fPIC $(shell pkg-config --cflags gtk+-3.0)
LIBS=$(shell pkg-config --libs gtk+-3.0)

libhello-ext.so: hello-ext.o
	$(CC) -o [email protected] $< -shared $(CFLAGS) $(LIBS)

hello-ext.o: hello-ext.c

index.html 修改为, 页面上有个按钮, 接连接下会不断的显示/隐藏 Gtk+ 窗口

<html>
<body>
    <script>
        var win_visible = false;
        function toggleVisibility()
        {
            if(! win_visible) {
                hello_ext.show_win();
            }
            else {
                hello_ext.hide_win();
            }
            win_visible = !win_visible;
        }
    </script>
    <button type="button" onclick="toggleVisibility()">Show GTK+ Win</button>
</body>
</html>

Manifest 不用做修改. 执行应用前, 先产生出扩展. 在 Deepin 上扩展是以共享库的方式存在, 名称格式需为 lib*.so, 执行 make 配合上面的 Makefile 即可简单产生出 libhello-ext.so

$ make
$ ls *so
libhello-ext.so

执行应用时, 以参数 --external-extensions-path 告知 CrossWalk 扩展搜寻位置

$ xwalk --external-extensions-path=$PWD manifest.json

提问: 扩展能不能在 Gtk+ 窗口被用户关闭时, 释放相应资源, 并通知应用 (上图 5 的部份), 让按钮变灰点下时不作用? 这留给读者自行研究. 提示: msg_iface->PostMessage().

打包

开发测试后, 最重要的, 就是如何打包并发布到商店了. Deepin 商店用的是 deb 包格式, 以下说明 web 应用加上扩展打包后发布到 Deepin 商店的流程.

首先, 安装打包工具的依赖

$ sudo apt-get install npm devscripts

安装基础打包工具及对 Deepin 的支持

$ git clone https://github.com/crosswalk-project/crosswalk-app-tools.git
$ cd crosswalk-app-tools
$ npm install
$ cd node_modules
$ git clone https://github.com/crosswalk-project/crosswalk-app-tools-deb.git crosswalk-app-tools-backend-deb
$ cd crosswalk-app-tools-backend-deb
$ npm install
$ cd ../..

打包工具脚本 src/crosswalk-appsrc/crosswalk-pkg 中调用 node.js 的命令是 node, Deepin 上的是 nodejs, 需要修改下

$ printf 'diff --git a/src/crosswalk-app b/src/crosswalk-app
index a8b9ca3..2d6aa67 100755
--- a/src/crosswalk-app
+++ b/src/crosswalk-app
@@ -1,4 +1,4 @@
-#!/usr/bin/env node
+#!/usr/bin/env nodejs
 
 // Copyright © 2014 Intel Corporation. All rights reserved.
 // Use  of this  source  code is  governed by  an Apache v2
diff --git a/src/crosswalk-pkg b/src/crosswalk-pkg
index 19409de..06480ea 100755
--- a/src/crosswalk-pkg
+++ b/src/crosswalk-pkg
@@ -1,4 +1,4 @@
-#!/usr/bin/env node
+#!/usr/bin/env nodejs
 
 // Copyright © 2014 Intel Corporation. All rights reserved.
 // Use  of this  source  code is  governed by  an Apache v2
' | patch -p1

确认下支持的平台

$ src/crosswalk-app platforms
  * deb
  * android
  * windows

deb 就没问题了. 将命令所在位置加到 PATH

$ export PATH+=":$PWD/src"
$ cd ..

打包时需要规范下目录结构, 可以让 crosswalk-app 命令帮我们产生模版再进行修改

$ crosswalk-app create org.deepin.helloworld
$ cd org.deepin.helloworld
$ ls
app  prj

开发工作会在 app 目录下进行, 打包的成果会在 prj 中生成.

先试用下包生成功能

$ crosswalk-app build
$ ls -l
total 14
drwxr-xr-x 3 crosswalk crosswalk     5 12月 19 11:51 app
-rw-r--r-- 1 crosswalk crosswalk 10262 12月 19 11:36 helloworld_0.0.1-1_amd64.deb
drwxr-xr-x 3 crosswalk crosswalk     3 12月 19 11:32 prj

包的内容非常小, 因为它以依赖的方式与其他 web 应用共享一份 CrossWalk (shared mode), 而非自带一个完整的执行环境 (各有利与弊).

安装到系统中并看看包的信息

$ sudo dpkg -i helloworld_0.0.1-1_amd64.deb
$ apt-cache show helloworld
Package: helloworld
Status: install ok installed
Priority: optional
Section: web
Installed-Size: 69
Maintainer: Intel Corporation.
Architecture: amd64
Version: 0.0.1-1
Depends: crosswalk
Pre-Depends: dpkg (>= 1.15.6)
Description: HTML5 web application
 Crosswalk is a web runtime for ambitious HTML5 applications. It provides all the
 features of a modern browser, combined with deep device integration and an API
 for adding native extensions.
Description-md5: d1af537bf036905a52162a1fc8df7630
Homepage: https://crosswalk-project.org/

执行

$ helloworld

接下来将我们的应用及扩展放进新生成的模板中

$ cd app
$ cp ../../{index.html,logo.png,libhello-ext.so} .
$ rm crosswalk.ico icon-48.png icon.png

manifest.json 需要更新一下, 加上更多信息供包生成工具了解我们包的情况

$ cat <<END >manifest.json
{
    "name": "Hello World",
    "start_url": "index.html",
    "icons": [ {
        "src": "logo.png",
        "sizes": "108x124",
        "type": "image/png",
        "density": "4.0"
    } ],
    "xwalk_app_version": "0.0.1",
    "xwalk_package_id": "org.deepin.helloworld",
    "xwalk_target_platforms": ["deb"]
}
END

修改启动脚本让 web 应用能使用我们开发的扩展

$ printf 'diff --git a/prj/deb/helloworld b/prj/deb/helloworld
index 944fff4..f582184 100755
--- a/prj/deb/helloworld
+++ b/prj/deb/helloworld
@@ -20,4 +20,4 @@ if [ ! -f "${MANIFEST}" ]; then
     exit 1
 fi
 
-exec xwalk "${BASEDIR}/www/manifest.json"
+exec xwalk --external-extensions-path="${BASEDIR}/www" "${BASEDIR}/www/manifest.json"
' | patch -p1

重新产生包并安装

$ cd ..
$ rm -r prj/deb/www/ 
$ crosswalk-app build
$ sudo dpkg -i helloworld_0.0.1-1_amd64.deb

执行后, 应该就能重现第二个扩展例手的功能了.

发布

最后的问题, 应用如何上到 Deepin 的商店中? 很简单, 发个帖子到论坛上即可, 网址如下
http://bbs.deepin.org/forum.php?mod=viewthread&tid=34080&extra=page%3D1

在经过 Deepin 的人工审核过程后, 会发送通知, 之后就可以在商店中看到你辛苦的结晶啦!

现况与未来

Intel 表示, 目前已知基于 CrossWalk 开发的应用至少有六千多款, 主要运行于移动平台, 种类从 web 站点的包装, 到专为 CrossWork 开发的应用进程, 还有为数不少精致有趣的游戏, 可以说后势可期.

Deepin 在这中间, 经由长时间的 CrossWalk 项目合作参与, 一方面打造更好的开发平台, 一方面努力与开发商接触, 吸引更多内容丰富商店加大通路可见度, 达成双赢的局面, 相信这样能让愈来愈多人认识 Linux 及开源的好.

最后, 可能有的开发人员对于 JavaScript 的性能持保留态度. 对于有这样想法的朋友, 建议可以了解下 Web Assembly

Yesterday, Brendan Eich dropped a bomb on the web development community: The web platform is getting a new low-level binary compiled format that will do a better job at being a compiler target than JavaScript.

是的, 二进制的 JavaScript 将出现, 能更好的将进程的意图对应到运行设备的机械码, 目前项目还在快速的叠代中, 但很肯定这个标准化一定会发生, 因为它的背后是当前主流浏览器的内核开发商 - Google, Microsoft, Mozilla, 其他的就不多说了.

参考数据