跨端组件实践 - 移动时代的前端

标签: 实践 移动 时代 | 发表时间:2014-05-01 15:00 | 作者:
出处:http://fex.baidu.com
作者:zswang

跨端组件实践 - 移动时代的前端

上周六在 QCon 分享了这个主题,说好的要有文档……

背景

唯一不变的就是变化

从业十多年,互联网的变化非常大:最初使用的电脑只有 8M 内存、32M 硬盘,现在口袋里装的手机已经是 2G 内存、16G 闪存,网络也从 56K 变成了 1.5M+。这个时代的人是幸福的……

这个期间也见证了 Web 时代的繁荣,从 C/S 走到 B/S。

现在无论是邮件、购物还是游戏、社交、工作等等,在电脑上都能找到满意的 Web 应用或站点。

可是这种景象在移动时代并没有看到。

现场小调查:请问你在手机上和 PC 上用什么方式刷微博?

  • 大部分的人不会在 PC 上用客户端刷微博
  • 大部分的人不会在手机上用浏览器刷微博

结论符合预期,先从变化上分析问题

移动互联网发生了什么变化?

屏幕更小

  • 显示区更宝贵,广告区更难摆放
  • 页面布局更讲究,内容主次更为重要

随身携带

  • 24 小时待机
  • 根据地理位置提供更精准的服务

触摸操作

  • 双手很难并行
  • 虚拟键盘没有物理键盘便捷

更丰富的内置设备

  • 前后置摄像头 / 闪关灯
  • 麦克风 / 扬声器
  • 振动器,静音状态也可以知道有消息到达
  • 电子指南针 / 陀螺仪
  • 蓝牙 / WiFi / NFC

离线使用场景

  • 在没有信号
  • 资费不足

没有持久能源

  • 电池需要充电
  • 计算能力和待机时间冲突

设备碎片化 * 特别是 Android 各种屏幕尺寸、各种 ROM

移动互联网的变化带来了新的机遇和挑战

机遇

移动市场高速增长

艾瑞咨询数据显示,2013 年中国移动互联网市场规模达到 1059.8 亿元,同比增速 81.2%, 预计到 2017 年,市场规模将增长约 4.5 倍,接近 6000 亿。移动互联正在深刻影响人们的日常生活,移动互联网市场进入高速发展通道。 【查看来源】

挑战

HTML5 / CSS3 技术在移动端受限

What stops developers from using HTML5? 【查看来源】

性能测试

为什么开发者不选择 HTML5 构建移动应用? 前三个原因是:

  • 性能问题,流畅度与 Native 差距较大
  • 硬件接口缺失,不能控制蓝牙、闪关灯、振动、WiFi、 NFC 等等
  • 难以集成本地元素,不能使用桌面图标、订阅推送等

这是我们用主流的机型做的性能测试

性能测试

不难看出 Native 和 Web 的性能依旧差距很大,包括主流韩国和国产机型。

人眼刷新率平均是 24 帧 / 秒,低于这个值用户就会感觉到跳帧。

当然这些问题在 PC 时代也碰到过!那时是怎么解决的?

影响前端的技术

通过浏览器扩展本地能力

  • 使用 ActiveX / NPAPI 技术
  • 最经典的插件就是 Flash,虽然它已经淡出了移动时代

JavaScript Engine 进化

  • V8 出现后,JavaScript 的性能提升了数倍
  • 结合高性能的引擎 NodeJS 也使 JavaScript 在后端获得了新生

HTML5 / CSS3

  • 扩展了本地能力,如地理定位、录音录像、本地存储等

但这些影响在移动端是有限的

移动时代前端的现状

Flash 不能使用

Adobe 将停止开发移动版 Flash

NPAPI 即将退役

Google 今年开始屏蔽 NPAPI 插件 【查看来源】

浏览器插件可以扩展本地能力的同时,也会带来稳定性和安全性的问题。

怎么解决性能瓶颈和本地能力缺失的问题?

JS Binding,通过 JavaScript 直接调用 Native API

JS Translate,通过编译器将 JavaScript 翻译成 Native 语言

  • 如号称上帝语言的 haXe 可以翻译成 Java、JavaScript、C++、PHP 的语言

Native App,直接使用 Native 技术,从头再来

  • 广义的前端就是要面向用户界面和交互
  • 前端技术也有向全端和全栈的发展趋势

选择手游创业的 @大城小胖 近期做了一个教学视频,专门介绍 JSBinding 大家可以参考: When iOS loves JS

以上技术可以解决问题,但不能发挥 Web 自然跨端、迭代方便(不同等待漫长的上架时间)的优势

我们还得寻找一些适合自己的方案。

Hybrid 混合应用方案

本地服务,网页通过 HTTP / WebSocket 与本地服务通信,使用本地能力

本地服务

  • 在 Android 里写一个不难,参考 NanoHttpd DIY 一个移动版的 HTTP 服务
    • 优势:能够无缝兼容所有浏览器
    • 劣势:通信容易被嗅探和伪造;很难利用 UI 组件

加壳,这是最常用的技术

加壳

  • 有较成熟的框架可以使用,如: Cordova
  • 通过使用和扩展插件,获得本地能力

Google 也有投入 Cordova 的项目 Chrome apps on Android and iOS

本地服务和加壳方式,都能访问本地能力。但后者本地能力在同一个进程里调度,安全性和便利性相对要高。

回到主题,什么是跨端组件?

自动响应端能力的组件

跨端组件示意

  • 受到 响应式网页设计理念的启发,界面布局可以根据运行环境自动响应和调整,那么本地能力也可以这样
  • 如在普通浏览器里使用 HTML5 / CSS3 构造组件,在提供本地能力的环境里使用 Native View 构造组件。
  • 在提供本地能力的环境里,界面会更流畅;在没有本地能力的环境里应用是完整的。

跨端组件解决的问题:

  • 满足 UI 需要局部流畅的需求
  • 满足运行在各种环境的需求

特点

  • 同一套 API
  • 更好地使用运行环境提供的能力

PC 时代也有这样的组件,如: Raphaël 一款矢量图组件,在具 VML 的环境里使用 VML,其他环境里使用 SVG,并保持同一套 API。发散一想: jQueryWebUploader(适配 Flash 和 HTML5)也都是自动响应各种运行环境。

成本总是伴随着收益,解决老问题就会带来新的问题

当页面发生滚动时,Native View 怎么和网页元素一起滚动?还有 Reflow 时怎么调整 Native View 的位置?

UI 融合的问题

滚动的问题在 Android 中处理比较方便。因为 WebView 继承至:ViewGroup / AbsoluteLayout,我们只需要将 WebView 作为 Native View 的容器就可以搞定这个问题。

Reflow 发生的频率不高,就用了定时器这种简单粗暴的方法

跨端组件研发的步骤

确定需求

哪些组件适合做跨端组件?

计算量大,需要流畅

  • 图册浏览
  • 地图
  • 多媒体播放器
  • 3D渲染
  • 图像识别,二维码识别、手势识别

减少操作步骤,省去授权

  • 录像、录音

HTML5能力增强

  • 地理定位增强,结合 WiFi
  • Canvas 性能增强(参考: FastCanvas

开发环境

天朝的网络大家知道的,主要找一些代理和镜像

设计 API

发现很多前端团队都开始使用和关注 Web Components

在跨端组件的落地上,我们也选择这种方式来提供 API,原因是:

  • 降低学习成本,保留原生 Web 组件的使用方式
  • 降低业务代码维护工作

目前移动端原生还不支持这个标准,还得选用框架适配,如: Polymer

跨端组件 HTML5 示例代码:

   <body>
    <div id="mapBox">
        <light-map width="350" height="400" center="116.404,39.915" zoom="11"></light-map>
    </div>
</body>

将组件的HTML部分放到需要显示的位置,然后就和普通的Element一样使用:

  • var lightMap = document.querySelector('light-map'); 可以通过 DOM 树操作
  • lightMap.addEventLister() 添加事件
  • lightMap.setAttribute()、lightMap.getAttribute() 设置属性

组件开发

Cordova Plugin 开发

plugin.xml 配置需要的权限、JavaScript 命名空间、文件对应的工程目录等待。细节请参考 官方文档

   <?xml version="1.0" encoding="UTF-8"?>
<plugin xmlns="http://apache.org/cordova/ns/plugins/1.0"
      id="com.baidu.light.flashlight"
      version="0.2.7">
    <name>Flashlight</name>
    <description>Cordova Flashlight Plugin</description>
    <license>Apache 2.0</license>
    <keywords>cordova,battery</keywords>
    <repo>https://github.com/zswang/light-flashlight.git</repo>
    <issue>https://github.com/zswang/light-flashlight/issue</issue>

    <js-module src="www/flashlight.js" name="flashlight">
        <clobbers target="light.flashlight" />
    </js-module>
    <!-- android -->
    <platform name="android">
        <config-file target="res/xml/config.xml" parent="/*">
            <feature name="Flashlight" >
                <param name="android-package" value="com.baidu.light.flashlight.Flashlight"/>
            </feature>
        </config-file>

        <config-file target="AndroidManifest.xml" parent="/*">
            <uses-permission android:name="android.permission.CAMERA" />
            <uses-permission android:name="android.permission.FLASHLIGHT" />
        </config-file>

        <source-file src="src/android/Flashlight.java" target-dir="src/com/baidu/light/flashlight" />
    </platform>
</plugin>

我就自己写一个 闪光灯插件 实现非常简单,供大家参考

  • JavaScript 关键部分
   var cordova = require('cordova'),
    exec = require('cordova/exec');

var flashlight = flashlight || {};

function torch(successCallback, errorCallback) {
    exec(successCallback, errorCallback, 'Flashlight', 'torch', []); // 调用 Native 的提供的方法,指定回调、Native 对应的类名和动作
};

flashlight.torch = torch;
module.exports = flashlight;
  • Android 关键部分
   public class Flashlight extends CordovaPlugin {
    private Camera mCamera;
    public boolean execute(String action, JSONArray args,
            CallbackContext callbackContext) throws JSONException {
        if (mCamera == null) {
            mCamera = Camera.open();
        }
        if ("torch".equals(action)) { // 打开手电的动作
            Parameters parameters = mCamera.getParameters();
            parameters.setFlashMode(Parameters.FLASH_MODE_TORCH);
            mCamera.setParameters(parameters); 
            callbackContext.success(null); // 回调 JavaScript
        } else {
            return false;
        }
        return true;
    }
}

百度地图 提供了 Android、JS、iOS 三个版本,正好适合用来做 地图跨端组件

  • 地图跨度组件 Cordova Plugin JavaScript 部分
   var cordova = require('cordova'),
    exec = require('cordova/exec');

var baidumap = baidumap || {};

/**
 * 初始化
 * @param{Object} options 配置项,显示位置
 * @param{Function} callback 回调
 */
function init(options, callback) {
    exec(callback, function() {
    }, 'BaiduMap', 'init', [options]);
};

baidumap.init = init;

module.exports = baidumap;
  • 地图跨度组件 Cordova Plugin Android 部分
   public class BaiduMap extends CordovaPlugin {
    private CallbackContext mCallbackContext = null;

    @SuppressWarnings("unchecked")
    public boolean execute(String action, JSONArray args,
            CallbackContext callbackContext) throws JSONException {
        if ("init".equals(action)) {
            if (args == null) {
                return false;
            }

            JSONObject params = args.optJSONObject(0);
            JSONArray center = params.optJSONArray("center");
            // Native View 在页面中的显示区域
            int left = params.optInt("left");
            int top = params.optInt("top");
            int width = params.optInt("width");
            int height = params.optInt("height");
            String guid = params.optString("id");
            int zoom = params.optInt("zoom");
            createMap(guid, left, top, width, height,
                    (float) center.optDouble(0), (float) center.optDouble(1),
                    zoom);
            mCallbackContext = callbackContext;

        }
        return true;
    }

    private static Handler mHandler = new Handler(Looper.getMainLooper());
    private static Hashtable<String, MapView> mMaps = new Hashtable<String, MapView>();

    public void initialize(CordovaInterface cordova, CordovaWebView webView) {
        super.initialize(cordova, webView);
        // 初始化百度地图 Android 版本
        BMapManager baiduMapManager = new BMapManager(webView.getContext()
                .getApplicationContext());
        baiduMapManager.init(new MKGeneralListener() {
            @Override
            public void onGetNetworkState(int state) {
            }

            @Override
            public void onGetPermissionState(int state) {
            }
        });
    }

    public void createMap(String guid, int left, int top, int width,
            int height, float lng, float lat, int zoom) {
        mHandler.post(new Runnable() { // 注意 JavaScript 调用 Native 会在子线程,如果操作 UI 需放到 主线程中
            private String mGuid;
            private int mLeft;
            private int mTop;
            private int mWidth;
            private int mHeight;
            private float mLng;
            private float mLat;
            private int mZoom;

            public Runnable config(String guid, int left, int top, int width,
                    int height, float lng, float lat, int zoom) {
                mGuid = guid;
                mLeft = left;
                mTop = top;
                mHeight = height;
                mWidth = width;
                mLng = lng;
                mLat = lat;
                mZoom = zoom;
                return this;
            }

            @SuppressWarnings("deprecation")
            @Override
            public void run() {
                MapView mapView = new MapView(BaiduMap.this.webView
                        .getContext());
                MapController mapController = mapView.getController();
                GeoPoint point = new GeoPoint((int) (mLat * 1E6),
                        (int) (mLng * 1E6));
                mapController.setCenter(point);
                mapController.setZoom(mZoom);

                float scale = BaiduMap.this.webView.getScale();

                LayoutParams params = new LayoutParams((int) (mWidth * scale),
                        (int) (mHeight * scale), (int) (mLeft * scale),
                        (int) (mTop * scale));
                mapView.setLayoutParams(params);
                BaiduMap.this.webView.addView(mapView); // 大家注意这一句,将 Native View 添加在 WebView 上,自然就响应页面滚动

                mMaps.put(mGuid, mapView);
            }

        }.config(guid, left, top, width, height, lng, lat, zoom));
    }
}
  • Web Component,注意适配 runtime 环境
   void function() {
    var instances = {};
    var guid = 0;

    var LightMapPrototype = Object.create(HTMLDivElement.prototype);
    LightMapPrototype.createdCallback = function() {
        var self = this;
        var div = document.createElement('div');
        var zoom = 11;
        var center = [ 116.404, 39.915 ];
        this.setZoom = function(value) {
            zoom = value;
            map.setZoom(zoom);
        };
        this.setCenter = function(value) {
            center = String(value).split(',');
            map.setCenter(new BMap.Point(center[0], center[1]));
        };

        div.style.width = (this.getAttribute('width') || '300') + 'px';
        div.style.height = (this.getAttribute('height') || '300') + 'px';
        this.appendChild(div);

        // 判断当前的运行环境
        var runtime = (typeof cordova != 'undefined')
                && (typeof light != 'undefined') // 有可能插件没有安装或者当前版本不支持
                && (typeof light.map != 'undefined') ? 'cordova' : 'browser';

        var map;
        switch (runtime) {
        case 'cordova':
            var obj = div.getBoundingClientRect()
            light.map.init({
                guid : guid,
                center : center,
                zoom : zoom,
                left : obj.left + window.pageXOffset,
                top : obj.top + window.pageYOffset,
                width : Math.round(obj.width),
                height : Math.round(obj.height)
            });

            instances[guid] = this;
            guid++;
            break;
        case 'browser':
            map = new BMap.Map(div); // 创建Map实例
            map.enableScrollWheelZoom(); // 启用滚轮放大缩小
            map.addControl(new BMap.ScaleControl()); // 添加比例尺控件
            map.addControl(new BMap.OverviewMapControl()); // 添加缩略地图控件
            map.centerAndZoom(new BMap.Point(center[0], center[1]), zoom); // 初始化地图,设置中心点坐标和地图级别

            map.addEventListener('moveend', function() {
                var value = map.getCenter();
                center = [ value.lng, value.lat ];
                self.setAttribute('center', center);
                var e = document.createEvent('Event');
                e.initEvent('moveend', true, true);
                self.dispatchEvent(e);
            });
            map.addEventListener('zoomend', function() {
                var value = map.getZoom();
                zoom = value;
                self.setAttribute('zoom', zoom);
                var e = document.createEvent('Event');
                e.initEvent('zoomend', true, true);
                self.dispatchEvent(e);
            });

            break;
        }
        this.map = map;
    };
    LightMapPrototype.attributeChangedCallback = function(attributeName,
            oldValue, newValue) {
        var self = this;
        switch (attributeName) {
        case 'center':
            self.setCenter(newValue);
            break;
        case 'zoom':
            self.setZoom(newValue);
            break;
        default:
            return false;
        }
        return true;
    };
    document.registerElement = document.registerElement || document.register;
    function init() {
        var LightMap = document.registerElement('light-map', {
            prototype : LightMapPrototype
        });
    }
    if (typeof cordova != 'undefined') {
        document.addEventListener('deviceready', init, false); // 等待设备初始化完成
    } else {
        init();
    }
}();

调试

Ripple

  • 这是一款能在浏览器里模拟移动设备的调试工具,包括模拟 GPS、陀螺仪 等本地能力

Weinre

  • 能够在 Chrome 开发者工具里,远程调试的工具
  • 优势:适用各种设备和浏览器
  • 不足:加载之前的状态不能获知、不能断点调试

Remote Debug

  • iOS 6 和 Android 4.4 开始,可以原生适用 Remote Debug
  • Android 4.4 不仅能打断点,而且还能映射 Web UI (Chrome dev 版本才支持)。

另外大家在移动端还用过啥 NB 的调试工具,欢迎留言推荐

安全考虑

用户主动操作才开启重要功能

  • 类似 Flash 里访问剪贴板,需要用户主动 Click 才可以访问
  • 相比弹出个小黄条让用户授权,这种设计体验要好很多

明确提示状态

  • 如:录音和录像时,有明确的状态显示

参考资料

相关 [实践 移动 时代] 推荐:

跨端组件实践 - 移动时代的前端

- - FEX 百度 Web 前端研发部
跨端组件实践 - 移动时代的前端. 上周六在 QCon 分享了这个主题,说好的要有文档……. 从业十多年,互联网的变化非常大:最初使用的电脑只有 8M 内存、32M 硬盘,现在口袋里装的手机已经是 2G 内存、16G 闪存,网络也从 56K 变成了 1.5M+. 这个期间也见证了 Web 时代的繁荣,从 C/S 走到 B/S.

有赞零售移动 CI/CD 实践

- - IT瘾-dev
点击关注“有赞coder”. 随着有赞零售业务的蓬勃发展,为了尽早交付有价值的应用满足客户需求,我们采用了敏捷开发的模式,快速拥抱变化的同时保持竞争优势. 从 2019 年起,零售客户端的发版周期更改为每周一次,这对移动端的持续集成与交付提出更高的要求. 如何根据现有的团队规模,在有限的资源下,快速搭建稳定可靠的持续集成与交付系统,我们有了自己的实践与思考.

5大移动网页设计之最佳实践

- everfly - 互联网的那点事
本文来自SocialBeta内容贡献者wisp,原文译自Mobile Web Design: Best Practices,更多移动产品设计方面的资料,请查看SocialBeta移动互联网栏目. 移动设备的兴起正在给互联网带来一场新的革命,虽然说移动网页设计原则不会有多大变化,但是却有着明显的区别.

淘宝彩票移动项目开发实践

- - Taobao UED Team
作者按:如今越来越多的互联网产品开始在移动终端发力,终端产品越来越丰富. 但是,平台差异带来的开发成本浪费很让人头疼. 一段时间以来,淘宝彩票前端组也在努力寻求移动终端项目开发最佳实践,尽管诸多方面不甚成熟,但抛砖引玉,希望我们的总结整理会对大家有所启发. 淘宝彩票客户端产品目前有两条体系:. 1、 原生应用:包括iPhone和Andoird上原生的客户端应用.

移动应用可用性测试的实践经验总结

- - 互联网的一些事-关注互联网产品管理,交流产品设计、用户体验心得
  如果你不大熟悉移动应用的可用性测试,没关系,这事儿没你想象的那么困难;不过移动应用与传统网站产品在可用性测试方面确实有一些关键的区别需要我们注意.   过去的几年当中,我(英文原文作者)为不少移动产品做过测试,从戒烟应用到移动版的车辆保险网站,其中既包括在实验室使用复杂设备进行的测试,也包括在各种实境化的条件下进行的非正式测试.

浅谈移动前端的最佳实践 - 叶小钗

- - 博客园_首页
这几天,第三轮全站优化结束,测试项目在2G首屏载入速度取得了一些优化成绩,对比下来有10s左右的差距:. 这次优化工作结束后,已经是第三次大规模折腾公司框架了,这里将一些自己知道的移动端的建议提出来分享下,希望对各位有用. 文中有误请您提出,以免误人自误. sap(single page application)也就是我们常常说的web应用程序webapp,被认为是业内的发展趋势,主要有两个优点:.

携程移动端 UI 界面性能优化实践

- - IT瘾-geek
作者简介:南志文,携程研发经理,曾负责App整体技术框架的架构研发与实践,现负责酒店业务的迭代更新及App架构、性能优化. 曾先后就职于阿里巴巴、巨人网络. 《程序员》杂志 10 月刊,作者发表了. 《携程移动端架构演进与优化之路》文章第一部分:架构演进. 紧接着,作者基于前文对携程移动端性能优化实践进行了总结.

移动端 HTML5 video 视频播放实践

- - JDC | 京东设计中心
移动端 HTML5 使用原生

《时代》移动调查称人类已进入“手机化”时代

- - TechWeb 今日焦点 RSS阅读
  腾讯科技讯(刘学通)北京时间8月19日消息,据国外媒体报道,现在全球人类的生活当中,手机成了很多人必不可少的随身物品,而且手机也在改变着我们的生活. 在不知不觉中,我们发现生活中是如此的依赖手机,离不开手机,我们已经进入了“手机化”时代.   移动化的力量如此之大,所有的东西,爱情、学习乃至全球发展都在跟着它快速变化.

在这个移动设备泛滥的时代里

- sy - Engadget 中国版
Pollitico.com漫画. 移动设备是否已经改变了你的生活方式. 引用来源 | 此文章网址 | 转寄此文章 | 回应.