平台化容器API釋放
接上文:(閱讀本文前,建議閱讀前三篇文章先)
之前設計Hybrid整塊交互的時候,受眾都是自己的團隊,沒有想往“公司化”和“平台化”方向發展,而近期業務的發展逐漸超出預期了,慢慢會有第三方網站接入我們的APP,而且第三方網站還會用一些Native的能力,這個時候之前的使用似乎就不太合適了,所謂JS-SDK就需要存在了。
類似這種需求,做的最完善的當屬微信的native容器了,微信這種屬於海量容器,對所有的接入方基本一視同仁,就算內部團隊會有一些“特權”能力,使用方式與第三方接入都是一套體系,可能只是文檔有所不同罷了。微信容器中,最常用的端能力要屬:
① 統一登錄,獲取微信的登錄態&使用微信登錄,微信方給予第三方應用有限的用戶信息(非JS-SDK)
② 分享接口
③ 微信支付
我們今天以微信(中間可能參考其他APP平台)為范本,思考下我們自己的容器如何處理第三方的情況。
我們做這塊設計之前,首先需要明確一個定位:
我們的Native容器,應該給第三方網站提供哪些能力?
如果不加限制的提供能力,就屬於“內部”項目了,如微信容器一般,對外提供的能力屈指可數,這里是我們一個比較常見的第三方容器:
這個當history過深(不為1時)后也會產生一個關閉按鈕,直接回到上一個native頁面。
我們這里約定,對於第三方網站,這些Native UI我們都不可定制化(但是能通過釋放接口配置增添菜單項目),甚至風格色彩都是不可定制的,如果哪天我們的APP風格變了,那么第三方網站只能適配我們的APP(雖然APP便整體風格這種事情有點操蛋,但是在中小公司還是比較常見)
這里可定制的是:
① 標題展示
② 分享出去的文字、圖片等(這塊參考微信)
③ 不同的APP這塊可能會有定制化需求,這塊也最好標准化,不讓產品有過多的瞎想
除了登錄之外,另一個比較常用的服務就是喚起支付(APP中的支付),然后APP公共頁面再引導用戶選擇微信或者支付寶支付。
其實喚起支付屬於H5跳Native(或者Hybrid)的一類,與之類似的有:
① 打開某一個native頁面,比較典型的是在網頁貼吧中打開一個網址如果安裝了貼吧APP,會直接打開那個頁面
② 這里也有可能是打開第三方APP,而打開第三方APP這種功能,對於比較大的APP平台會做嚴格的限制
因為圖片上傳什么對H5來說是一個比較麻煩的功能,完全依賴H5可能體驗不好,於是native方可能會釋放一些圖片操作的接口如:
① H5調用native的選取相冊&圖片接口
② 圖片預覽接口(查看大圖)
③ 上傳圖片
④ 下載圖片
有些比較特別的網站也許還會有獲取當前網絡狀態的接口或者地理位置接口或者調用掃一掃功能(場景較少),而對於UI層面的操作又會包含關閉當前窗口(同點擊關閉按鈕)的接口。
所有的這些功能,我們最初就應該設想清楚,並且清晰的知道每一個接口適用於什么場景,在能力列表出來后,我們就要做另一個事情了:權限限制。
權限限制
微信使用了一個appid,去限制每一個接入方的能力,甚至可以以收費的形式釋放某些接口,可以預見,如果微信開放打開第三方App的收費功能,會有很多公司爭相埋單。這種appid的行為,相當於一種統一收口的動作,雖然接入方千奇百怪,但是所有的接入方如果要使用容器的能力,甚至想在容器中展示,就必須有一個appid。
另一方面,appid的使用其實成本比較高,前端需要額外的接口訪問不說,平台方還需要提供一個第三方網站讓第三方網站管理自己的應用,這個對於一些小平台甚至偽平台有一些得不償失,我們來簡單看看一個demo:
1 //第三方網站要用什么接口,必須先聲明 2 wx.config({ 3 debug: false, 4 appId: 'wxf8b4f85f3a794e77',//服務器端讀出 5 timestamp: 1485169627,//服務器端讀出 6 nonceStr: 'jhGQ4jiN4CQpaGPC',//服務器端讀出 7 signature: 'a19573b7f65427a33a96f2a57a4f40075135a5b4',//服務器端讀出 8 jsApiList: [ 9 'onMenuShareTimeline', 10 'onMenuShareAppMessage', 11 'onMenuShareQQ', 12 'onMenuShareWeibo', 13 'onMenuShareQZone' 14 ] 15 });
1 // 2. 分享接口 2 // 2.1 監聽“分享給朋友”,按鈕點擊、自定義分享內容及分享結果接口 3 document.querySelector('#onMenuShareAppMessage').onclick = function () { 4 wx.onMenuShareAppMessage({ 5 title: '分享標題', 6 desc: '分享描述', 7 link: 'http://movie.douban.com/subject/25785114/', 8 imgUrl: 'http://demo.open.weixin.qq.com/jssdk/images/p2166127561.jpg', 9 trigger: function (res) { 10 // 不要嘗試在trigger中使用ajax異步請求修改本次分享的內容,因為客戶端分享操作是一個同步操作,這時候使用ajax的回包會還沒有返回 11 alert('用戶點擊發送給朋友'); 12 }, 13 success: function (res) { 14 alert('已分享'); 15 }, 16 cancel: function (res) { 17 alert('已取消'); 18 }, 19 fail: function (res) { 20 alert(JSON.stringify(res)); 21 } 22 }); 23 alert('已注冊獲取“發送給朋友”狀態事件'); 24 };
如果沒有第一段代碼的聲明,第二段代碼就沒用;如果appid沒有相關接口權限,這里就注冊不了,也不能調用接口,比如我們就可以為某一個appid賦予打開第三方app的權限,或者我們定義某個appid具有app,其他應用可以根據打開對應app,沒有的話就引導下載:
wx.openApp(appid);
這里的實現方案可以是這樣,一般來說,我們對外釋放的接口都是比較通用的,像一些私密的接口才會有白名單維護,比如我們自己的app對應的幾個圖片操作接口:
1 //選取圖片接口 2 wx.chooseImage({ 3 count: 1, // 默認9 4 sizeType: ['original', 'compressed'], // 可以指定是原圖還是壓縮圖,默認二者都有 5 sourceType: ['album', 'camera'], // 可以指定來源是相冊還是相機,默認二者都有 6 success: function (res) { 7 var localIds = res.localIds; // 返回選定照片的本地ID列表,localId可以作為img標簽的src屬性顯示圖片 8 } 9 }); 10 //上傳圖片接口 11 wx.uploadImage({ 12 localId: '', // 需要上傳的圖片的本地ID,由chooseImage接口獲得 13 isShowProgressTips: 1, // 默認為1,顯示進度提示 14 success: function (res) { 15 var serverId = res.serverId; // 返回圖片的服務器端ID 16 } 17 }); 18 //預覽圖片接口 19 wx.previewImage({ 20 current: '', // 當前顯示圖片的http鏈接 21 urls: [] // 需要預覽的圖片http鏈接列表 22 });
其中預覽我把他作為私密接口不予釋放,需要特定的appid才能使用,就可以這樣:
1 //1代表公共接口,0代表私密接口 2 var apilist = { 3 'chooseImage': 1, 4 'uploadImage': 1, 5 'previewImage': 0 6 }; 7 //所有的應用id 8 var appids = [1, 2, 3, 4]; 9 //白名單 10 var whitList = [{1: ['previewImage', '其他私密能力']}];
實話實說維護一個appid,這樣做的成本比較高,單單做appid和秘鑰對於調用者來說也挺麻煩,對於有些比較小的平台來說,可以采取域名白名單的方法,后端維護一個列表:
1 //域名白名單 2 var whitList = [ 3 {'domain.com': ['previewImage', '其他私密能力']}, 4 {'domain2.com': ['previewImage', 'uploadImage', '其他私密能力']} 5 ];
這種方法比較做起來成本較低,一些小一點的平台可以這樣做,而這樣做的話,需要考慮每個域名對應的App打開協議,可能會有打開需求,我們這里采用的比較簡單的方案,域名白名單,表設計大概這樣:
1 var whitList = [ 2 {id: 'domain.com', apis: ['previewImage', '其他私密能力'], schema: 'xxxx://'}, 3 {id: 'domain2.com', apis: ['previewImage', 'uploadImage', '其他私密能力'], schema: 'xxxx://'} 4 ];
我們明確知道某個域名具有哪些能力,如果不具有這些能力就不予理睬,我們也知道某個域名具有打開某個app的權限。
能力列表
明確了能力,以及能力限制,接下來我們便來整理一下幾個核心的對外接口。
header的定義
我們這里做的第一件事情,依舊是header的定義,並且對於第三方,我們要求header只能是這個樣子:
針對header我們有以下約定:
① 進入一個頁面默認包含,返回+title+功能菜單三個按鈕
② 返回按鈕默認執行history.back()的操作,如果history.length為1,則退到native上一步操作
③ title默認讀取html中的title標簽,可使用接口更改
④ 關閉按鈕默認不存在,在history比較深並且點擊過一次返回按鈕后展示出來,防止頁面死循環假死
⑤ 功能菜單默認彈出以下菜單項,其中分享文案默認讀取當前tdk(title+description)標簽,和第一張圖片,也可讀取頁面標簽定制(也可以使用接口定制,事實上容器不會做這種業務工作,是業務框架層bridge做的工作),比如:
1 <meta name="med-title" content="分享標題"> 2 <meta name="med-description" content="分享內容"> 3 <meta name="med-link" content="http://...."> 4 <meta name="med-img" content="http://....">
我們業務層代碼,或者bridge代碼會將之翻譯為:
1 wx.onMenuShareTimeline({ 2 title: medTitle, 3 link: medLink, 4 imgUrl: medImg, 5 trigger: function (res) { 6 }, 7 success: function (res) { 8 }, 9 cancel: function (res) { 10 }, 11 fail: function (res) { 12 } 13 });
分享到朋友圈&QQ空間
分享到朋友圈前端代碼為:
1 MED.origin = MED.origin || {}; 2 //shareTimeline分享到朋友圈;shareAppMessage分享給朋友;shareQQ分享給qq好友;shareQZone分享到空間,設置方面稍作更改即可 3 MED.origin.medShareXXX = MED.medShareXXX = function (o) { 4 _.requestHybrid({ 5 tagname: 'shareTimeline', 6 param: { 7 title: o.title, 8 desc: o.desc, 9 image: o.img, 10 url: o.link 11 }, 12 callback: function(data) { 13 if(data.code === 0) { 14 o.success && o.success(data.data); 15 } else { 16 o.cancel && o.cancel(data.data); 17 } 18 } 19 }); 20 };
H5上傳圖片方面的體驗很差,這塊我們在H5情況下依舊使用file上傳,但是在容器里面釋放幾個圖片操作接口
圖片操作
1 //選取圖片,最初想把選取和上傳合並的,后面想想還是分開合適 2 _.requestHybrid({ 3 tagname: 'chooseImage', 4 param: { 5 //1-9張限制 6 count: 1, 7 sizeType: ['original', 'compressed'], // 可以指定是原圖還是壓縮圖,默認二者都有 8 sourceType: ['album', 'camera'], // 可以指定來源是相冊還是相機,默認二者都有 9 }, 10 callback: function(data) { 11 if(data.code === 0) { 12 //這塊有一些疑問,選擇和上傳還是連着一起算了 13 o.success && o.success(data.data.localIds); // 返回選定照片的本地ID列表,localId可以作為img標簽的src屬性顯示圖片; 14 } else { 15 o.error && o.error(data.data); 16 } 17 } 18 }); 19 20 //上傳圖片 21 _.requestHybrid({ 22 tagname: 'uploadImage', 23 param: { 24 //由chooseImage獲取 25 localId: 1, 26 isShowProgressTips: 1 // 默認為1,顯示進度提示 27 }, 28 callback: function(data) { 29 if(data.code === 0) { 30 o.success && o.success(data.data.url); // 返回src; 31 } else { 32 o.error && o.error(data.data); 33 } 34 } 35 }); 36 37 //圖片預覽,預覽的地方做個圖片下載的功能 38 _.requestHybrid({ 39 tagname: 'previewImage', 40 param: { 41 current: '', // 當前顯示圖片的http鏈接 42 urls: [] // 需要預覽的圖片http鏈接列表 43 } 44 });
獲取網絡狀態
1 //獲取網絡狀態 2 _.requestHybrid({ 3 tagname: 'getNetworkType', 4 callback: function(data) { 5 //data.networkType 2g 3g 4g wifi 6 } 7 });
地圖操作
Native的地理操作一塊相對H5體驗要好一些,特別是地圖展示一塊的體驗要好得多,所以這兩塊也需要釋放API:
1 //獲取經緯度信息 2 _.requestHybrid({ 3 tagname: 'getLocation', 4 callback: function(data) { 5 if(data.code !== 0) return; 6 var res = data.res; 7 var latitude = res.latitude; // 緯度,浮點數,范圍為90 ~ -90 8 var longitude = res.longitude; // 經度,浮點數,范圍為180 ~ -180。 9 var speed = res.speed; // 速度,以米/每秒計 10 var accuracy = res.accuracy; // 位置精度 11 } 12 }); 13 14 //根據經緯度等信息打開native地圖 15 _.requestHybrid({ 16 tagname: 'openLocation', 17 params: { 18 latitude: 0, // 緯度,浮點數,范圍為90 ~ -90 19 longitude: 0, // 經度,浮點數,范圍為180 ~ -180。 20 name: '', // 位置名 21 address: '', // 地址詳情說明 22 scale: 1, // 地圖縮放級別,整形值,范圍從1~28。默認為最大 23 infoUrl: '' // 在查看位置界面底部顯示的超鏈接,可點擊跳轉 24 } 25 });
所謂的name和地址是指下面信息框這一坨:
界面操作
關閉當前webview,回到native上一次操作:
1 _.requestHybrid({ 2 tagname: 'closeWindow' 3 });
native鍵盤
H5在文字輸入一塊可以說是弱爆了,比Native體驗差遠了,所以我們在native鍵盤這塊也做了一個nativeUI,如果需求允許可以使用:
1 //喚起輸入文字的軟鍵盤 2 //這塊代碼有一些業務耦合,需要如何處理下???? 3 _.requestHybrid({ 4 tagname: 'showKeyboard', 5 param: { 6 hasImg: 1, //是否需要上傳圖片區域,如果需要則為1,不需要為0 7 count: 1, //如果需要圖片上傳,這里限制圖片選擇的數量,1-9 8 sizeType: ['original', 'compressed'], // 可以指定是原圖還是壓縮圖,默認二者都有 9 sourceType: ['album', 'camera'], // 可以指定來源是相冊還是相機,默認二者都有 10 textMin: 20, //文字要求最少輸入字符數 11 textMax: 500 //文字要求最多輸入字符數 12 }, 13 //輸入結束的回調或者說點擊發送時候的回調 14 callback: function (data) { 15 var content = data.content;//文字內容 16 var urls = data.urls;//圖片地址 17 } 18 });
結語
今天分析了一下第三方webview需要釋放的接口,接下來我這邊開始落地,我們真實工作中可能還要考慮新老容器過渡等問題,本文含金量相對較小,各位謹慎閱讀吧。