跨端組件實(shí)踐 - 移動(dòng)時(shí)代的前端
背景
***不變的就是變化
從業(yè)十多年,互聯(lián)網(wǎng)的變化非常大:最初使用的電腦只有 8M 內(nèi)存、32M 硬盤,現(xiàn)在口袋里裝的手機(jī)已經(jīng)是 2G 內(nèi)存、16G 閃存,網(wǎng)絡(luò)也從 56K 變成了 1.5M+。這個(gè)時(shí)代的人是幸福的……
這個(gè)期間也見證了 Web 時(shí)代的繁榮,從 C/S 走到 B/S。
現(xiàn)在無論是郵件、購物還是游戲、社交、工作等等,在電腦上都能找到滿意的 Web 應(yīng)用或站點(diǎn)。
可是這種景象在移動(dòng)時(shí)代并沒有看到。
現(xiàn)場小調(diào)查:請問你在手機(jī)上和 PC 上用什么方式刷微博?
- 大部分的人不會(huì)在 PC 上用客戶端刷微博
- 大部分的人不會(huì)在手機(jī)上用瀏覽器刷微博
結(jié)論符合預(yù)期,先從變化上分析問題
移動(dòng)互聯(lián)網(wǎng)發(fā)生了什么變化?
屏幕更小
- 顯示區(qū)更寶貴,廣告區(qū)更難擺放
- 頁面布局更講究,內(nèi)容主次更為重要
隨身攜帶
- 24 小時(shí)待機(jī)
- 根據(jù)地理位置提供更精準(zhǔn)的服務(wù)
觸摸操作
- 雙手很難并行
- 虛擬鍵盤沒有物理鍵盤便捷
更豐富的內(nèi)置設(shè)備
- 前后置攝像頭 / 閃關(guān)燈
- 麥克風(fēng) / 揚(yáng)聲器
- 振動(dòng)器,靜音狀態(tài)也可以知道有消息到達(dá)
- 電子指南針 / 陀螺儀
- 藍(lán)牙 / WiFi / NFC
離線使用場景
- 在沒有信號
- 資費(fèi)不足
沒有持久能源
- 電池需要充電
- 計(jì)算能力和待機(jī)時(shí)間沖突
設(shè)備碎片化 * 特別是 Android 各種屏幕尺寸、各種 ROM
移動(dòng)互聯(lián)網(wǎng)的變化帶來了新的機(jī)遇和挑戰(zhàn)
機(jī)遇
移動(dòng)市場高速增長
艾瑞咨詢數(shù)據(jù)顯示,2013 年中國移動(dòng)互聯(lián)網(wǎng)市場規(guī)模達(dá)到
1059.8
億元,同比增速81.2%
, 預(yù)計(jì)到 2017 年,市場規(guī)模將增長約4.5
倍,接近6000
億。移動(dòng)互聯(lián)正在深刻影響人們的日常生活,移動(dòng)互聯(lián)網(wǎng)市場進(jìn)入高速發(fā)展通道。【查看來源】
挑戰(zhàn)
HTML5 / CSS3 技術(shù)在移動(dòng)端受限
What stops developers from using HTML5?【查看來源】
為什么開發(fā)者不選擇 HTML5 構(gòu)建移動(dòng)應(yīng)用? 前三個(gè)原因是:
- 性能問題,流暢度與 Native 差距較大
- 硬件接口缺失,不能控制藍(lán)牙、閃關(guān)燈、振動(dòng)、WiFi、NFC 等等
- 難以集成本地元素,不能使用桌面圖標(biāo)、訂閱推送等
這是我們用主流的機(jī)型做的性能測試
不難看出 Native 和 Web 的性能依舊差距很大,包括主流韓國和國產(chǎn)機(jī)型。
人眼刷新率平均是 24 幀 / 秒,低于這個(gè)值用戶就會(huì)感覺到跳幀。
當(dāng)然這些問題在 PC 時(shí)代也碰到過!那時(shí)是怎么解決的?
影響前端的技術(shù)
通過瀏覽器擴(kuò)展本地能力
JavaScript Engine 進(jìn)化
- V8 出現(xiàn)后,JavaScript 的性能提升了數(shù)倍
- 結(jié)合高性能的引擎 NodeJS 也使 JavaScript 在后端獲得了新生
HTML5 / CSS3
- 擴(kuò)展了本地能力,如地理定位、錄音錄像、本地存儲等
但這些影響在移動(dòng)端是有限的
移動(dòng)時(shí)代前端的現(xiàn)狀
Flash 不能使用
NPAPI 即將退役
Google 今年開始屏蔽 NPAPI 插件【查看來源】
瀏覽器插件可以擴(kuò)展本地能力的同時(shí),也會(huì)帶來穩(wěn)定性和安全性的問題。
怎么解決性能瓶頸和本地能力缺失的問題?
JS Binding,通過 JavaScript 直接調(diào)用 Native API
- 從 iOS7 開始,可以使用 JavaScriptCore 接口
- 常見的框架和技術(shù)
JS Translate,通過編譯器將 JavaScript 翻譯成 Native 語言
- 如號稱上帝語言的 haXe 可以翻譯成 Java、JavaScript、C++、PHP 的語言
Native App,直接使用 Native 技術(shù),從頭再來
- 廣義的前端就是要面向用戶界面和交互
- 前端技術(shù)也有向全端和全棧的發(fā)展趨勢
選擇手游創(chuàng)業(yè)的 @大城小胖 近期做了一個(gè)教學(xué)視頻,專門介紹 JSBinding 大家可以參考:When iOS loves JS
- PC 時(shí)代 JSBinding 可以用 MSScriptControl
以上技術(shù)可以解決問題,但不能發(fā)揮 Web 自然跨端、迭代方便(不同等待漫長的上架時(shí)間)的優(yōu)勢
我們還得尋找一些適合自己的方案。
Hybrid 混合應(yīng)用方案
本地服務(wù),網(wǎng)頁通過 HTTP / WebSocket 與本地服務(wù)通信,使用本地能力
- 在 Android 里寫一個(gè)不難,參考 NanoHttpd DIY 一個(gè)移動(dòng)版的 HTTP 服務(wù)
- 優(yōu)勢:能夠無縫兼容所有瀏覽器
- 劣勢:通信容易被嗅探和偽造;很難利用 UI 組件
加殼,這是最常用的技術(shù)
- 有較成熟的框架可以使用,如:Cordova
- 通過使用和擴(kuò)展插件,獲得本地能力
Google 也有投入 Cordova 的項(xiàng)目 Chrome apps on Android and iOS
本地服務(wù)和加殼方式,都能訪問本地能力。但后者本地能力在同一個(gè)進(jìn)程里調(diào)度,安全性和便利性相對要高。
回到主題,什么是跨端組件?
自動(dòng)響應(yīng)端能力的組件
- 受到響應(yīng)式網(wǎng)頁設(shè)計(jì)理念的啟發(fā),界面布局可以根據(jù)運(yùn)行環(huán)境自動(dòng)響應(yīng)和調(diào)整,那么本地能力也可以這樣
- 如在普通瀏覽器里使用 HTML5 / CSS3 構(gòu)造組件,在提供本地能力的環(huán)境里使用 Native View 構(gòu)造組件。
- 在提供本地能力的環(huán)境里,界面會(huì)更流暢;在沒有本地能力的環(huán)境里應(yīng)用是完整的。
跨端組件解決的問題:
- 滿足 UI 需要局部流暢的需求
- 滿足運(yùn)行在各種環(huán)境的需求
特點(diǎn)
- 同一套 API
- 更好地使用運(yùn)行環(huán)境提供的能力
PC 時(shí)代也有這樣的組件,如:Raphaël 一款矢量圖組件,在具 VML 的環(huán)境里使用 VML,其他環(huán)境里使用 SVG,并保持同一套 API。發(fā)散一想:jQuery、WebUploader(適配 Flash 和 HTML5)也都是自動(dòng)響應(yīng)各種運(yùn)行環(huán)境。
成本總是伴隨著收益,解決老問題就會(huì)帶來新的問題
當(dāng)頁面發(fā)生滾動(dòng)時(shí),Native View 怎么和網(wǎng)頁元素一起滾動(dòng)?還有 Reflow 時(shí)怎么調(diào)整 Native View 的位置?
UI 融合的問題
滾動(dòng)的問題在 Android 中處理比較方便。因?yàn)?WebView 繼承至:ViewGroup / AbsoluteLayout,我們只需要將 WebView 作為 Native View 的容器就可以搞定這個(gè)問題。
Reflow 發(fā)生的頻率不高,就用了定時(shí)器這種簡單粗暴的方法。
#p#
跨端組件研發(fā)的步驟
確定需求
哪些組件適合做跨端組件?
計(jì)算量大,需要流暢
- 圖冊瀏覽
- 地圖
- 多媒體播放器
- 3D渲染
- 圖像識別,二維碼識別、手勢識別
減少操作步驟,省去授權(quán)
- 錄像、錄音
HTML5能力增強(qiáng)
- 地理定位增強(qiáng),結(jié)合 WiFi
- Canvas 性能增強(qiáng)(參考:FastCanvas)
開發(fā)環(huán)境
天朝的網(wǎng)絡(luò)大家知道的,主要找一些代理和鏡像
設(shè)計(jì) API
發(fā)現(xiàn)很多前端團(tuán)隊(duì)都開始使用和關(guān)注 Web Components
在跨端組件的落地上,我們也選擇這種方式來提供 API,原因是:
- 降低學(xué)習(xí)成本,保留原生 Web 組件的使用方式
- 降低業(yè)務(wù)代碼維護(hù)工作
目前移動(dòng)端原生還不支持這個(gè)標(biāo)準(zhǔn),還得選用框架適配,如: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()
設(shè)置屬性
組件開發(fā)
Cordova Plugin 開發(fā)
plugin.xml 配置需要的權(quán)限、JavaScript 命名空間、文件對應(yīng)的工程目錄等待。細(xì)節(jié)請參考官方文檔
- <?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>
我就自己寫一個(gè)閃光燈插件 實(shí)現(xiàn)非常簡單,供大家參考
- JavaScript 關(guān)鍵部分
- var cordova = require('cordova'),
- exec = require('cordova/exec');
- var flashlight = flashlight || {};
- function torch(successCallback, errorCallback) {
- exec(successCallback, errorCallback, 'Flashlight', 'torch', []); // 調(diào)用 Native 的提供的方法,指定回調(diào)、Native 對應(yīng)的類名和動(dòng)作
- };
- flashlight.torch = torch;
- module.exports = flashlight;
- Android 關(guān)鍵部分
- 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)) { // 打開手電的動(dòng)作
- Parameters parameters = mCamera.getParameters();
- parameters.setFlashMode(Parameters.FLASH_MODE_TORCH);
- mCamera.setParameters(parameters);
- callbackContext.success(null); // 回調(diào) JavaScript
- } else {
- return false;
- }
- return true;
- }
- }
百度地圖 提供了 Android、JS、iOS 三個(gè)版本,正好適合用來做地圖跨端組件
- 地圖跨度組件 Cordova Plugin JavaScript 部分
- var cordova = require('cordova'),
- exec = require('cordova/exec');
- var baidumap = baidumap || {};
- /**
- * 初始化
- * @param{Object} options 配置項(xiàng),顯示位置
- * @param{Function} callback 回調(diào)
- */
- 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 在頁面中的顯示區(qū)域
- 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 調(diào)用 Native 會(huì)在子線程,如果操作 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 上,自然就響應(yīng)頁面滾動(dòng)
- mMaps.put(mGuid, mapView);
- }
- }.config(guid, left, top, width, height, lng, lat, zoom));
- }
- }
- Web Component,注意適配 runtime 環(huán)境
- 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);
- // 判斷當(dāng)前的運(yùn)行環(huán)境
- var runtime = (typeof cordova != 'undefined')
- && (typeof light != 'undefined') // 有可能插件沒有安裝或者當(dāng)前版本不支持
- && (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); // 創(chuàng)建Map實(shí)例
- map.enableScrollWheelZoom(); // 啟用滾輪放大縮小
- map.addControl(new BMap.ScaleControl()); // 添加比例尺控件
- map.addControl(new BMap.OverviewMapControl()); // 添加縮略地圖控件
- map.centerAndZoom(new BMap.Point(center[0], center[1]), zoom); // 初始化地圖,設(shè)置中心點(diǎn)坐標(biāo)和地圖級別
- 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); // 等待設(shè)備初始化完成
- } else {
- init();
- }
- }();
調(diào)試
- 這是一款能在瀏覽器里模擬移動(dòng)設(shè)備的調(diào)試工具,包括模擬 GPS、陀螺儀 等本地能力
- 能夠在 Chrome 開發(fā)者工具里,遠(yuǎn)程調(diào)試的工具
- 優(yōu)勢:適用各種設(shè)備和瀏覽器
- 不足:加載之前的狀態(tài)不能獲知、不能斷點(diǎn)調(diào)試
- iOS 6 和 Android 4.4 開始,可以原生適用 Remote Debug
- Android 4.4 不僅能打斷點(diǎn),而且還能映射 Web UI (Chrome dev 版本才支持)。
另外大家在移動(dòng)端還用過啥 NB 的調(diào)試工具,歡迎留言推薦
安全考慮
用戶主動(dòng)操作才開啟重要功能
- 類似 Flash 里訪問剪貼板,需要用戶主動(dòng) Click 才可以訪問
- 相比彈出個(gè)小黃條讓用戶授權(quán),這種設(shè)計(jì)體驗(yàn)要好很多
明確提示狀態(tài)
- 如:錄音和錄像時(shí),有明確的狀態(tài)顯示
參考資料
- 本期分享 QCon 鏈接
- 艾瑞:2013中國移動(dòng)互聯(lián)網(wǎng)市場規(guī)模1059億
- HTML5 performance is fine, what we are missing is tools
- Adobe 將停止開發(fā)移動(dòng)版 Flash
- Google將于2014年1月開始屏蔽NPAPI插件
- When iOS loves JS
- Chrome apps on Android and iOS
- 響應(yīng)式網(wǎng)頁設(shè)計(jì)
- Web Components
- Polymer
- Ripple
- Weinre
- Remote Debug
- How to use Ripple Emulator for Windows to test PhoneGap application?