• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

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

武飞扬头像
51CTO
帮助0

背景

***不变的就是变化

从业十多年,互联网的变化非常大:最初使用的电脑只有 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 发生的频率不高,就用了定时器这种简单粗暴的方法。

#p#

跨端组件研发的步骤

确定需求

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

计算量大,需要流畅

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

减少操作步骤,省去授权

  • 录像、录音

HTML5能力增强

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

开发环境

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

设计 API

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

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

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

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

跨端组件 HTML5 示例代码:

  1. <body> 
  2.     <div id="mapBox"> 
  3.         <light-map width="350" height="400" center="116.404,39.915" zoom="11"></light-map> 
  4.     </div> 
  5. </body> 

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

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

组件开发

Cordova Plugin 开发

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

  1. <?xml version="1.0" encoding="UTF-8"?> 
  2. <plugin xmlns="http://apache.org/cordova/ns/plugins/1.0" 
  3.       id="com.百度.light.flashlight" 
  4.       version="0.2.7"> 
  5.     <name>Flashlight</name> 
  6.     <description>Cordova Flashlight Plugin</description> 
  7.     <license>Apache 2.0</license> 
  8.     <keywords>cordova,battery</keywords> 
  9.     <repo>https://github.com/zswang/light-flashlight.git</repo> 
  10.     <issue>https://github.com/zswang/light-flashlight/issue</issue> 
  11.  
  12.     <js-module src="www/flashlight.js" name="flashlight"> 
  13.         <clobbers target="light.flashlight" /> 
  14.     </js-module> 
  15.     <!-- android --> 
  16.     <platform name="android"> 
  17.         <config-file target="res/xml/config.xml" parent="/*"> 
  18.             <feature name="Flashlight" > 
  19.                 <param name="android-package" value="com.百度.light.flashlight.Flashlight"/> 
  20.             </feature> 
  21.         </config-file> 
  22.  
  23.         <config-file target="AndroidManifest.xml" parent="/*"> 
  24.             <uses-permission android:name="android.permission.CAMERA" /> 
  25.             <uses-permission android:name="android.permission.FLASHLIGHT" /> 
  26.         </config-file> 
  27.  
  28.         <source-file src="src/android/Flashlight.java" target-dir="src/com/百度/light/flashlight" /> 
  29.     </platform> 
  30. </plugin> 

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

    • JavaScript 关键部分
  1. var cordova = require('cordova'), 
  2.     exec = require('cordova/exec'); 
  3.  
  4. var flashlight = flashlight || {}; 
  5.  
  6. function torch(successCallback, errorCallback) { 
  7.     exec(successCallback, errorCallback, 'Flashlight''torch', []); // 调用 Native 的提供的方法,指定回调、Native 对应的类名和动作 
  8. }; 
  9.  
  10. flashlight.torch = torch; 
  11. module.exports = flashlight; 
    • Android 关键部分
  1. public class Flashlight extends CordovaPlugin { 
  2.     private Camera mCamera; 
  3.     public boolean execute(String action, JSONArray args, 
  4.             CallbackContext callbackContext) throws JSONException { 
  5.         if (mCamera == null) { 
  6.             mCamera = Camera.open(); 
  7.         } 
  8.         if ("torch".equals(action)) { // 打开手电的动作 
  9.             Parameters parameters = mCamera.getParameters(); 
  10.             parameters.setFlashMode(Parameters.FLASH_MODE_TORCH); 
  11.             mCamera.setParameters(parameters);  
  12.             callbackContext.success(null); // 回调 JavaScript 
  13.         } else { 
  14.             return false
  15.         } 
  16.         return true
  17.     } 

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

    • 地图跨度组件 Cordova Plugin JavaScript 部分
  1. var cordova = require('cordova'), 
  2.     exec = require('cordova/exec'); 
  3.  
  4. var 百度map = 百度map || {}; 
  5.  
  6. /** 
  7.  * 初始化 
  8.  * @param{Object} options 配置项,显示位置 
  9.  * @param{Function} callback 回调 
  10.  */ 
  11. function init(options, callback) { 
  12.     exec(callback, function() { 
  13.     }, 'BaiduMap''init', [options]); 
  14. }; 
  15.  
  16. 百度map.init = init; 
  17.  
  18. module.exports = 百度map; 
    • 地图跨度组件 Cordova Plugin Android 部分
  1. public class BaiduMap extends CordovaPlugin { 
  2.     private CallbackContext mCallbackContext = null
  3.  
  4.     @SuppressWarnings("unchecked"
  5.     public boolean execute(String action, JSONArray args, 
  6.             CallbackContext callbackContext) throws JSONException { 
  7.         if ("init".equals(action)) { 
  8.             if (args == null) { 
  9.                 return false
  10.             } 
  11.  
  12.             JSONObject params = args.optJSONObject(0); 
  13.             JSONArray center = params.optJSONArray("center"); 
  14.             // Native View 在页面中的显示区域 
  15.             int left = params.optInt("left"); 
  16.             int top = params.optInt("top"); 
  17.             int width = params.optInt("width"); 
  18.             int height = params.optInt("height"); 
  19.             String guid = params.optString("id"); 
  20.             int zoom = params.optInt("zoom"); 
  21.             createMap(guid, left, top, width, height, 
  22.                     (float) center.optDouble(0), (float) center.optDouble(1), 
  23.                     zoom); 
  24.             mCallbackContext = callbackContext; 
  25.  
  26.         } 
  27.         return true
  28.     } 
  29.  
  30.     private static Handler mHandler = new Handler(Looper.getMainLooper()); 
  31.     private static Hashtable<String, MapView> mMaps = new Hashtable<String, MapView>(); 
  32.  
  33.     public void initialize(CordovaInterface cordova, CordovaWebView webView) { 
  34.         super.initialize(cordova, webView); 
  35.         // 初始化百度地图 Android 版本 
  36.         BMapManager 百度MapManager = new BMapManager(webView.getContext() 
  37.                 .getApplicationContext()); 
  38.         百度MapManager.init(new MKGeneralListener() { 
  39.             @Override 
  40.             public void onGetNetworkState(int state) { 
  41.             } 
  42.  
  43.             @Override 
  44.             public void onGetPermissionState(int state) { 
  45.             } 
  46.         }); 
  47.     } 
  48.  
  49.     public void createMap(String guid, int left, int top, int width, 
  50.             int height, float lng, float lat, int zoom) { 
  51.         mHandler.post(new Runnable() { // 注意 JavaScript 调用 Native 会在子线程,如果操作 UI 需放到 主线程中 
  52.             private String mGuid; 
  53.             private int mLeft; 
  54.             private int mTop; 
  55.             private int mWidth; 
  56.             private int mHeight; 
  57.             private float mLng; 
  58.             private float mLat; 
  59.             private int mZoom; 
  60.  
  61.             public Runnable config(String guid, int left, int top, int width, 
  62.                     int height, float lng, float lat, int zoom) { 
  63.                 mGuid = guid; 
  64.                 mLeft = left; 
  65.                 mTop = top; 
  66.                 mHeight = height; 
  67.                 mWidth = width; 
  68.                 mLng = lng; 
  69.                 mLat = lat; 
  70.                 mZoom = zoom; 
  71.                 return this
  72.             } 
  73.  
  74.             @SuppressWarnings("deprecation"
  75.             @Override 
  76.             public void run() { 
  77.                 MapView mapView = new MapView(BaiduMap.this.webView 
  78.                         .getContext()); 
  79.                 MapController mapController = mapView.getController(); 
  80.                 GeoPoint point = new GeoPoint((int) (mLat * 1E6), 
  81.                         (int) (mLng * 1E6)); 
  82.                 mapController.setCenter(point); 
  83.                 mapController.setZoom(mZoom); 
  84.  
  85.                 float scale = BaiduMap.this.webView.getScale(); 
  86.  
  87.                 LayoutParams params = new LayoutParams((int) (mWidth * scale), 
  88.                         (int) (mHeight * scale), (int) (mLeft * scale), 
  89.                         (int) (mTop * scale)); 
  90.                 mapView.setLayoutParams(params); 
  91.                 BaiduMap.this.webView.addView(mapView); // 大家注意这一句,将 Native View 添加在 WebView 上,自然就响应页面滚动 
  92.  
  93.                 mMaps.put(mGuid, mapView); 
  94.             } 
  95.  
  96.         }.config(guid, left, top, width, height, lng, lat, zoom)); 
  97.     } 
    • Web Component,注意适配 runtime 环境
  1. void function() { 
  2.     var instances = {}; 
  3.     var guid = 0; 
  4.  
  5.     var LightMapPrototype = Object.create(HTMLDivElement.prototype); 
  6.     LightMapPrototype.createdCallback = function() { 
  7.         var self = this
  8.         var div = document.createElement('div'); 
  9.         var zoom = 11; 
  10.         var center = [ 116.404, 39.915 ]; 
  11.         this.setZoom = function(value) { 
  12.             zoom = value; 
  13.             map.setZoom(zoom); 
  14.         }; 
  15.         this.setCenter = function(value) { 
  16.             center = String(value).split(','); 
  17.             map.setCenter(new BMap.Point(center[0], center[1])); 
  18.         }; 
  19.  
  20.         div.style.width = (this.getAttribute('width') || '300')   'px'
  21.         div.style.height = (this.getAttribute('height') || '300')   'px'
  22.         this.appendChild(div); 
  23.  
  24.         // 判断当前的运行环境 
  25.         var runtime = (typeof cordova != 'undefined'
  26.                 && (typeof light != 'undefined'// 有可能插件没有安装或者当前版本不支持 
  27.                 && (typeof light.map != 'undefined') ? 'cordova' : 'browser'
  28.  
  29.         var map; 
  30.         switch (runtime) { 
  31.         case 'cordova'
  32.             var obj = div.getBoundingClientRect() 
  33.             light.map.init({ 
  34.                 guid : guid, 
  35.                 center : center, 
  36.                 zoom : zoom, 
  37.                 left : obj.left   window.pageXOffset, 
  38.                 top : obj.top   window.pageYOffset, 
  39.                 width : Math.round(obj.width), 
  40.                 height : Math.round(obj.height) 
  41.             }); 
  42.  
  43.             instances[guid] = this
  44.             guid ; 
  45.             break
  46.         case 'browser'
  47.             map = new BMap.Map(div); // 创建Map实例 
  48.             map.enableScrollWheelZoom(); // 启用滚轮放大缩小 
  49.             map.addControl(new BMap.ScaleControl()); // 添加比例尺控件 
  50.             map.addControl(new BMap.OverviewMapControl()); // 添加缩略地图控件 
  51.             map.centerAndZoom(new BMap.Point(center[0], center[1]), zoom); // 初始化地图,设置中心点坐标和地图级别 
  52.  
  53.             map.addEventListener('moveend'function() { 
  54.                 var value = map.getCenter(); 
  55.                 center = [ value.lng, value.lat ]; 
  56.                 self.setAttribute('center', center); 
  57.                 var e = document.createEvent('Event'); 
  58.                 e.initEvent('moveend'truetrue); 
  59.                 self.dispatchEvent(e); 
  60.             }); 
  61.             map.addEventListener('zoomend'function() { 
  62.                 var value = map.getZoom(); 
  63.                 zoom = value; 
  64.                 self.setAttribute('zoom', zoom); 
  65.                 var e = document.createEvent('Event'); 
  66.                 e.initEvent('zoomend'truetrue); 
  67.                 self.dispatchEvent(e); 
  68.             }); 
  69.  
  70.             break
  71.         } 
  72.         this.map = map; 
  73.     }; 
  74.     LightMapPrototype.attributeChangedCallback = function(attributeName, 
  75.             oldValue, newValue) { 
  76.         var self = this
  77.         switch (attributeName) { 
  78.         case 'center'
  79.             self.setCenter(newValue); 
  80.             break
  81.         case 'zoom'
  82.             self.setZoom(newValue); 
  83.             break
  84.         default
  85.             return false
  86.         } 
  87.         return true
  88.     }; 
  89.     document.registerElement = document.registerElement || document.register; 
  90.     function init() { 
  91.         var LightMap = document.registerElement('light-map', { 
  92.             prototype : LightMapPrototype 
  93.         }); 
  94.     } 
  95.     if (typeof cordova != 'undefined') { 
  96.         document.addEventListener('deviceready', init, false); // 等待设备初始化完成 
  97.     } else { 
  98.         init(); 
  99.     } 
  100. }(); 

调试

Ripple

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

Weinre

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

Remote Debug

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

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

安全考虑

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

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

明确提示状态

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

参考资料

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /news/detail/tanhcafjac
系列文章
更多 icon
同类精品
更多 icon
继续加载