上周六在 QCon 分享了这个主题,说好的要有文档……
从业十多年,互联网的变化非常大:最初使用的电脑只有 8M 内存、32M 硬盘,现在口袋里装的手机已经是 2G 内存、16G 闪存,网络也从 56K 变成了 1.5M+。这个时代的人是幸福的……
这个期间也见证了 Web 时代的繁荣,从 C/S 走到 B/S。
现在无论是邮件、购物还是游戏、社交、工作等等,在电脑上都能找到满意的 Web 应用或站点。
- 购物:淘宝、京东、当当、苏宁易购 @民工精髓V
- 社交: 贴吧、 微博
- 游戏:各种页游、我常玩的是 Web 红警
- 办公: 脑图、 流程图、 日程安排、 Github、 邮件
可是这种景象在移动时代并没有看到。
现场小调查:请问你在手机上和 PC 上用什么方式刷微博?
- 大部分的人不会在 PC 上用客户端刷微博
- 大部分的人不会在手机上用浏览器刷微博
结论符合预期,先从变化上分析问题
屏幕更小
随身携带
触摸操作
更丰富的内置设备
离线使用场景
没有持久能源
设备碎片化 * 特别是 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 时代也碰到过!那时是怎么解决的?
通过浏览器扩展本地能力
JavaScript Engine 进化
HTML5 / CSS3
但这些影响在移动端是有限的
Flash 不能使用
Adobe 将停止开发移动版 Flash
NPAPI 即将退役
Google 今年开始屏蔽 NPAPI 插件 【查看来源】
- Google 网络商店不会再接受任何包含基于 NPAPI 插件的新应用或拓展。
- 对于需要 NPAPI 替代品的开发者,Google 推荐转向 NaCl、 Apps、 原生消息 API 和 旧版浏览器支持。
浏览器插件可以扩展本地能力的同时,也会带来稳定性和安全性的问题。
JS Binding,通过 JavaScript 直接调用 Native API
JS Translate,通过编译器将 JavaScript 翻译成 Native 语言
Native App,直接使用 Native 技术,从头再来
选择手游创业的 @大城小胖 近期做了一个教学视频,专门介绍 JSBinding 大家可以参考: When iOS loves JS
- PC 时代 JSBinding 可以用 MSScriptControl
以上技术可以解决问题,但不能发挥 Web 自然跨端、迭代方便(不同等待漫长的上架时间)的优势
我们还得寻找一些适合自己的方案。
本地服务,网页通过 HTTP / WebSocket 与本地服务通信,使用本地能力
加壳,这是最常用的技术
Google 也有投入 Cordova 的项目 Chrome apps on Android and iOS
本地服务和加壳方式,都能访问本地能力。但后者本地能力在同一个进程里调度,安全性和便利性相对要高。
自动响应端能力的组件
跨端组件解决的问题:
特点
PC 时代也有这样的组件,如: Raphaël 一款矢量图组件,在具 VML 的环境里使用 VML,其他环境里使用 SVG,并保持同一套 API。发散一想: jQuery、 WebUploader(适配 Flash 和 HTML5)也都是自动响应各种运行环境。
成本总是伴随着收益,解决老问题就会带来新的问题
当页面发生滚动时,Native View 怎么和网页元素一起滚动?还有 Reflow 时怎么调整 Native View 的位置?
滚动的问题在 Android 中处理比较方便。因为 WebView 继承至:ViewGroup / AbsoluteLayout,我们只需要将 WebView 作为 Native View 的容器就可以搞定这个问题。
Reflow 发生的频率不高,就用了定时器这种简单粗暴的方法
计算量大,需要流畅
减少操作步骤,省去授权
HTML5能力增强
天朝的网络大家知道的,主要找一些代理和镜像
发现很多前端团队都开始使用和关注 Web Components
在跨端组件的落地上,我们也选择这种方式来提供 API,原因是:
目前移动端原生还不支持这个标准,还得选用框架适配,如: 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>
我就自己写一个 闪光灯插件 实现非常简单,供大家参考
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;
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 三个版本,正好适合用来做 地图跨端组件
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;
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));
}
}
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
Weinre
Remote Debug
另外大家在移动端还用过啥 NB 的调试工具,欢迎留言推荐
用户主动操作才开启重要功能
明确提示状态