JavaScript SDK 設計指南


介紹

本指南為您介紹了在台式機和移動網絡在不同的平台和瀏覽器( < 99.99 %我可能會跳過一些瀏覽器)開發的JavaScript SDK ,對於那些非瀏覽器開發的支持(硬件,嵌入式,節點/ IO js )被排除在本文檔之外,在未來予以考慮。
因為我沒有找到一個關於設計JavaScript SDK的比較好的文檔,所以我在這里收集並記下了我個人的經驗。這份文檔已經寫了好幾個月,有一點我們需要知道,JavaScript的SDK-設計不僅僅是設計SDK本身,這也是有關於開發者與設備瀏覽器中間的聯系。我們寫的越多,越會更多的思考我們真正關心的是不同平台和瀏覽器之間的性能和兼容問題。你可以根據情況自由的更改或者完全放棄我在文章里列出的建議。

 

什么是SDK

我知道它確實是很普通很常見。一般是一些軟件工程師為特定的軟件包、軟件框架、硬件平台、操作系統等建立應用軟件時的開發工具的集合。通常一個SDK包含一個或多個API,編程工具和檔。

 

設計理念

這取決於你的SDK用來干什么的,但是它必須具備原生的,短,速度快,干凈,可讀可測試特性。用原生javascript寫,不要用像Livescript, Coffeescript, Typescript和其它的編譯語言。必須有更好的方法來編寫自己的javascript原生代碼比別人更快。請不要在你的SDK里用JQuery,除非它非常有必要。你可以使用其它的類似jQuery的庫,譬如zetpo.js,用於DOM操作,如果你需要用到HTTP Ajax請求,可以使用另外一種輕量庫像window.fetch。

每一次的SDK版本發布,確保它不僅適用於舊版本而且適應於未來的新版本。所以,記得為你的SDK寫文檔,代碼要寫注釋,同時做好單元測試和用戶場景測試。

 

適應范圍

基於《Third-Party JavaScript》這本書。在何種情況下,你應該為你的應用設計一個JavaScript SDK?

  • 嵌入式組件 – 嵌入在出發布者的網頁中的交互式應用程序(Disqus, Google Maps, Facebook Widget)。
  • 分析與數據 – 搜集網站訪問者以及其與網站互動的數據信息。(GA, Flurry, Mixpanel)
  • web服務API封裝 -對於發展與外部Web服務通信的客戶端應用程序。(Facebook的圖形API)

在什么情況下,我們應該在JavaScript環境中使用SDK呢?大家可以想想還有其它情沒?

 

引入SDK

建議你采用異步加載腳本的方式。我們要優化網站的用戶體驗,所以不希望我們的SDK庫阻塞其它主要進程。
異步加載

(function() {
vars = document.createElement('script');
s.type = 'text/javascript';
s.async = true;
s.src = 'http://xxx.com/sdk.js';
varx = document.getElementsByTagName('script')[0];
x.parentNode.insertBefore(s, x);
})();

在新的現代瀏覽器(chrome)你可以使用

<script asyncsrc="http://xxx.com/sdk.js"></script>

傳統加載方法

<script type="text/javascript"src="http://xxx.com/sdk.js"></script>

對比:

下面是簡單的圖形顯示異步加載和傳統同步加載方式之間的區別

異步:

|----A-----|
      |-----B-----------|
            |-------C------|

同步:

|----A-----||-----B-----------||-------C------|

異步和延遲腳本執行解釋

異步的問題

當你使用異步加載的時候,將會出現,頁面中的函數無法正常調用SDK方法的情況。

<script> (function () { var s =document.createElement('script'); s.type='text/javascript'; s.async=true; s.src='http://xxx.com/sdk.js'; var x =document.getElementsByTagName('script')[0]; x.parentNode.insertBefore(s, x); })(); // execute your script immediately hereSDKName('some arguments'); </script>

結果會報undefined錯誤,因為SDKName()在腳本加載之前執行了。所以我們應該使用點技巧讓腳本正確執行。把事件保存在SDKName.q數組里,SDK初始化的時候執行SDKName.q。

<script> (function () { // add a queue event here SDKName = SDKName ||function () { (SDKName.q=SDKName.q|| []).push(arguments); }; var s =document.createElement('script'); s.type='text/javascript'; s.async=true; s.src='http://xxx.com/sdk.js'; var x =document.getElementsByTagName('script')[0]; x.parentNode.insertBefore(s, x); })(); // execute your script immediately hereSDKName('some arguments'); </script>

或者用 [ ].push

<script> (function () { // add a queue event here SDKName =window.SDKName|| (window.SDKName= []); var s =document.createElement('script'); s.type='text/javascript'; s.async=true; s.src='http://xxx.com/sdk.js'; var x =document.getElementsByTagName('script')[0]; x.parentNode.insertBefore(s, x); })(); // execute your script immediately hereSDKName.push(['some arguments']); </script>

其他方式

還有其它不同方式加載腳本

Import in ES2015

import"your-sdk";

模塊加載

這里有完整的源碼和非常棒的教程. Loading JavaScript Modules

module('sdk.js',['sdk-track.js', 'sdk-beacon.js'],function(track, beacon) { // sdk definitions, split into local and global/exported definitions// local definitions// exports }); // you should contain this "module" method (function () { var modules = {}; // private record of module data// modules are functions with additional informationfunctionmodule(name,imports,mod) { // record module informationwindow.console.log('found module '+name); modules[name] = {name:name, imports: imports, mod: mod}; // trigger loading of import dependenciesfor (var imp in imports) loadModule(imports[imp]); // check whether this was the last module to be loaded// in a given dependency grouploadedModule(name); } // function loadModule// function loadedModulewindow.module=module; })();

vi設計http://www.maiqicn.com 辦公資源網站大全https://www.wode007.com

SDK版本

避免使用自己的特例作為版本名稱像

標識-v<時間戳>.js 標識-v<日期>.js 標識-v1-v2.js
它可能導致使用SDK的開發者很混亂不知道哪個是最新版本。
使用 Semantic Versioning (語義化版本規范)去定義SDK的版本號以”大.小.補丁”形式。
版本以v1.0.0 v1.5.0 v2.0.0的形式,會讓使用者搜索跟蹤日志文件更容易。
通常情況下,我們會有不同的方式去聲明SDK的版本,這取決於具體針對的業務和設計。

使用查詢字符串路徑

http://xxx.com/sdk.js?v=1.0.0

使用文件夾命名

http://xxx.com/v1.0.0/sdk.js

使用主機名或者子域名

http://v1.xxx.com/sdk.js

為了以后版本的升級迭代,建議用stable unstable alpha latest experimental 版本。

http://xxx.com/sdk-stable.js http://xxx.com/sdk-unstable.js http://xxx.com/sdk-alpha.js http://xxx.com/sdk-latest.js http://xxx.com/sdk-experimental.js

 

更新日志文件

你應該注意到如果你升級你的SDK卻沒通知用戶,用戶不會知道。記得寫更新日志來記錄無論是主要、次要甚至bug修復等修改。這將是一個好的開發經驗,我們能快速的跟蹤到SDK某個API的修改。所以保持更新日志 – Keep a Changelog, Github Repo

每個版本的日志應該有:

[新增] 新功能.
[更新] 修改現有的更能
[廢棄] 在即將發布的版本中刪除某個功能.
[刪除] 在這個版本中刪除棄用的功能.
[修正] bug修復
[安全] 邀請用戶對安全進行升級

 

命名空間

在你的SDK里只定義一個全局命名空間,並且不要用太過通用的名字,避免和其它類庫名發生沖突。SDK的主體用(function () { … })()包裹。這種做法越來越普遍的應用於各種流行的javascript類庫譬如jQuery,Node.js等等。這種創建私有的命名空間的技術很重要,有助於避免各種類庫之間命名的沖突。

為了避免命名空間沖突

學習Google Analytics的做法,你可以通過改變 ga的值來定義你自己的命名空間。

(function(i,s,o,g,r,a,m) {i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), m=s.getElementsByTagName(o) [0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) })(window,document,'script','//www.google- analytics.com/analytics.js','ga');

下面的是 openX的做法,支持通過給地址傳遞參數定義命名空間。

<script src="http://your_domain/sdk?namespace=yourcompany"></script>

存儲機制

cookie

使用cookie就會面臨復雜的作用域范圍問題,而且涉及到子域和路徑問題。

比如在路徑 path=/下, cookie first=value1 在域名 http://github.com下, 另外一個 cookie second=value2 在域名 http://sub.github.com下

  http://github.com http://sub.github.com
first=value1
second=value2

有個 cookie first=value1 在 http://github.com下, cookie second=value2 在 http://github.com/path1 另外一個 cookie third=value3 在 http://sub.github.com下,

  http://github.com http://github.com/path1 http://sub.github.com
first=value1
second=value2
third=value3

檢查 Cookie 可讀寫

給定一個域 (默認當前主機域名), 檢查cookie是否可讀寫。

var checkCookieWritable = function(domain) { try { // Create cookie document.cookie = 'cookietest=1' + (domain ? '; domain=' + domain : ''); var ret = document.cookie.indexOf('cookietest=') != -1; // Delete cookie document.cookie = 'cookietest=1; expires=Thu, 01-Jan-1970 00:00:01 GMT' + (domain ? '; domain=' + domain : ''); return ret; } catch (e) { return false; } };

檢查第三方 Cookie 可讀寫

檢查第三方cookie僅僅通過客戶端js是辦不到的,需要服務器端配合。

寫 讀 刪除 Cookie 代碼

代碼片段寫/讀/刪除cookie的腳本。

var cookie = { write: function(name, value, days, domain, path) { var date = new Date(); days = days || 730; // two years path = path || '/'; date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); var expires = '; expires=' + date.toGMTString(); var cookieValue = name + '=' + value + expires + '; path=' + path; if (domain) { cookieValue += '; domain=' + domain; } document.cookie = cookieValue; }, read: function(name) { var allCookie = '' + document.cookie; var index = allCookie.indexOf(name); if (name === undefined || name === '' || index === -1) return ''; var ind1 = allCookie.indexOf(';', index); if (ind1 == -1) ind1 = allCookie.length; return unescape(allCookie.substring(index + name.length + 1, ind1)); }, remove: function(name) { if (this.read(name)) { this.write(name, '', -1, '/'); } } };

Session

js寫不了session,需要服務器端寫。
一個頁面的session會一直保存着只要瀏覽器是開着的即使頁面重新加載。打開一個新頁面會生成一個新的session。子窗口會和父窗口共享一個session。

LocalStorage

存儲的數據沒有時間限制。存儲數據量大(至少5MB)並且信息不會傳送到服務器。而且同一個域名從http和https訪問localStorage是不共享的。你可以在你的網頁上創建個iframe,然后用postMessage方法去傳值到父頁面。HOW TO?

檢查 LocalStorage 可寫

window.localStorage 並不是任何瀏覽器都支持,SDK在用之前要檢查是否可用。

var testCanLocalStorage = function() { var mod = 'modernizr'; try { localStorage.setItem(mod, mod); localStorage.removeItem(mod); return true; } catch (e) { return false; } };

SessionStorage

針對一個 session 的數據存儲(當用戶關閉瀏覽器窗口后,數據會被刪除).

檢查 SessionStorage 可寫

var checkCanSessionStorage = function() { var mod = 'modernizr'; try { sessionStorage.setItem(mod, mod); sessionStorage.removeItem(mod); return true; } catch (e) { return false; } }

事件

在客戶端瀏覽器有很多事件加載、卸載、綁定等會存在兼容問題。polyfills是個解決不同平台事件綁定的不錯的解決方案。

Document Ready

確保整個頁面完成加載了再執行SDK方法。

// handle IE8+ function ready (fn) { if (document.readyState != 'loading') { fn(); } else if (window.addEventListener) { // window.addEventListener('load', fn); window.addEventListener('DOMContentLoaded', fn); } else { window.attachEvent('onreadystatechange', function() { if (document.readyState != 'loading') fn(); }); } }

DOMContentLoaded - 所有DOM解析完會觸發整個事件 不需要等到樣式表、圖片等加載完。

load 頁面完整加載。

Message Event

這里是實現iframe和父頁面之間的數據通信, 這里有文檔 API documentation.

// in the iframe parent.postMessage("Hello"); // string // ========================================== // in the iframe's parent // Create IE + others compatible event handler var eventMethod = window.addEventListener ? "addEventListener" : "attachEvent"; var eventer = window[eventMethod]; var messageEvent = eventMethod == "attachEvent" ? "onmessage" : "message"; // Listen to message from child window eventer(messageEvent,function(e) { // e.origin , check the message origin console.log('parent received message!: ',e.data); },false);

發送的數據是字符串, 對於使用更高級的json字符串. 不是所有的瀏覽器對支持 Structured Clone Algorithm on the parameter, (參數的結構化克隆)。

Orientation Change 橫屏事件

檢測設備橫屏

window.addEventListener('orientationchange', fn);

獲取旋轉方向和角度

window.orientation; // => 90, -90, 0

Screen portrait-primary(豎屏正方向), portrait-secondary(豎屏反方向), landscape-primary(橫屏正方向), landscape-secondary (橫屏反方向)(Experimental)

// https://developer.mozilla.org/en-US/docs/Web/API/Screen/orientation var orientation = screen.orientation || screen.mozOrientation || screen.msOrientation;

Request

我們的SDK和服務器之間通信通過Ajax請求,因為我們知道我們可以使用jQuery的Ajax 方法。但是有更好的方案來實現它。

圖片預加載

通過創建一個Image對象預加載一張圖片。為了防止瀏覽器緩存記得加上時間戳。

(new Image()).src = 'http://xxxxx.com/collect?id=1111';

要注意通過GET方式傳輸參數最大長度是2048個字節(取決於不同的瀏覽器和服務器)。這里要做一些處理如果超過長度。

if (length > 2048) { // do Multiple Post (form) } else { // do Image Beacon }

你可能遇到問題在使用encodeURI 還是 encodeURIComponent的時候,最好理解它們的區別。 See below.

對於圖像加載成功/錯誤回調

var img = new Image(); img.src = 'http://xxxxx.com/collect?id=1111'; img.onload = successCallback; img.onerror = errorCallback;

單個 Post 請求

普通表單發送一個對應元素和值

var form = document.createElement('form'); var input = document.createElement('input'); form.style.display = 'none'; form.setAttribute('method', 'POST'); form.setAttribute('action', 'http://xxxx.com/track'); input.name = 'username'; input.value = 'attacker'; form.appendChild(input); document.getElementsByTagName('body')[0].appendChild(form); form.submit();

多個 Post 請求

服務通常比較復雜,需要通過POST方法發送更多數據。

function requestWithoutAjax( url, params, method ){ params = params || {}; method = method || "post"; // function to remove the iframe var removeIframe = function( iframe ){ iframe.parentElement.removeChild(iframe); }; // make a iframe... var iframe = document.createElement('iframe'); iframe.style.display = 'none'; iframe.onload = function(){ var iframeDoc = this.contentWindow.document; // Make a invisible form var form = iframeDoc.createElement('form'); form.method = method; form.action = url; iframeDoc.body.appendChild(form); // pass the parameters for( var name in params ){ var input = iframeDoc.createElement('input'); input.type = 'hidden'; input.name = name; input.value = params[name]; form.appendChild(input); } form.submit(); // remove the iframe setTimeout( function(){ removeIframe(iframe); }, 500); }; document.body.appendChild(iframe); } requestWithoutAjax('url/to', { id: 2, price: 2.5, lastname: 'Gamez'});

Iframe

當你在需要在頁面中生成內容時候,你可以通過iframe嵌入。

var iframe = document.createElement('iframe'); var body = document.getElementsByTagName('body')[0]; iframe.style.display = 'none'; iframe.src = 'http://xxxx.com/page'; iframe.onreadystatechange = function () { if (iframe.readyState !== 'complete') { return; } }; iframe.onload = loadCallback; body.appendChild(iframe);

清除iframe的邊框,內部margin值。

<iframe src="..." marginwidth="0" marginheight="0" hspace="0" vspace="0" frameborder="0" scrolling="no"> </iframe>

iframe中插入html

<iframe id="iframe"></iframe> <script> var html_string= "content <script>alert(location.href); </script>"; document.getElementById('iframe').src = "data:text/html;charset=utf-8," + escape(html_string); // alert data:text/html;charset=utf-8..... // access cookie get ERROR var doc = document.getElementById('iframe').contentWindow.document; doc.open(); doc.write('<body>Test<script>alert(location.href);</script></body>'); doc.close(); // alert "top window url" var iframe = document.createElement('iframe'); iframe.src = 'javascript:;\\\\'' + encodeURI('<html><body> <script>alert(location.href);</body></html>') + '\\\\''; // iframe.src = 'javascript:;"' + encodeURI((html_tag).replace(/\\\\"/g, '\\\\\\\\\\\\"')) + '"'; document.body.appendChild(iframe); // alert "about:blank" </script>

jsonp

這種情況下,你的服務器需要響應JavaScript 代碼,並讓瀏覽器執行它,僅僅通過js腳本鏈接。

(function () { var s = document.createElement('script'); s.type = 'text/javascript'; s.async = true; s.src = '/yourscript? some=parameter&callback=jsonpCallback'; var x = document.getElementsByTagName('script')[0]; x.parentNode.insertBefore(s, x); })();

關於jsonp你需要了解:

  • JSONP 只能通過GET請求。
  • JSONP 缺少錯誤處理機制, 意味着你不能檢測代碼是否404還是500等狀態。
  • JSONP 請求是異步的。
  • 當心 CSRF 攻擊。
  • 跨域通信。腳本響應端(服務器端)不需要關心CORS。

XMLHttpRequest

自己寫XMLHttpRequest不是個好主意,因為你要浪費很多時間去做IE或者其它瀏覽器的兼容。這里提供一些現成的解決方案供大家參考:

1 - window.fetch - A window.fetch JavaScript polyfill.
2 - got - Simplified HTTP/HTTPS requests
3 - microjs - list of ajax lib
4 – more

Maximum Number of Connection

檢查不同瀏覽器的最大連接數 browserscope

調試

模擬多個域

你不需要注冊多個域名來模擬域,在本地搭建個虛擬服務器,綁定host的方式就可以:

$ sudo vim /etc/hosts

添加以下條目

#refer to localhost 127.0.0.1 publisher.net 127.0.0.1 sdk.net

然后你就可以訪問該頁面http://publisher.net和http://sdk.net

Developer Tools

用瀏覽器自帶的調試工具,Chrome Developer Tool 、Safari Developer Tools、Firebug都是不錯的選擇。

開發工具也簡稱為工具。

工具提供Web開發者深進入瀏覽器和Web應用程序的內部。使用工具來有效地追蹤布局問題,將JavaScript打斷點,並獲得代碼優化的建議。

控制台日志

用於測試和輸出文本和其他一般的調試, 控制台日志可通過瀏覽器的API log()輸出顯示。有各種各樣的方法和格式輸出你的信息,了解更多API: Console API.

調試代理

代理在你調試SDK的很多時候都很有用。 修改cookies, headers, cache, 編輯 http request/response, SSL Proxying, ajax 調試等等。
這里推薦一些代理工具:

  • FiddlerCore
  • Charles
  • Cellist

BrowserSync

Browsersync能讓瀏覽器實時、快速響應您的文件更改(html、js、css、sass、less等)並自動刷新頁面。更重要的是 Browsersync可以同時在PC、平板、手機等設備下進項調試。它真的很有幫助如果你需要跨平台測試你的SDK)。

提示和小技巧

Console Logs Polyfill(Polyfilling 是由 RemySharp 提出的一個術語,它是用來描述復制缺少的 API 和API 功能的行為)

這不是一個真正的polyfill,只是保證在調用console.log API的時候不拋出錯誤。

if (typeof console === "undefined") { var f = function() {}; console = { log: f, debug: f, error: f, info: f };}

EncodeURI or EncodeURIComponent

理解三者的不同 escape()、encodeURI()、encodeURIComponent()
here.
記住使用 encodeURI()和encodeURIComponent()有11個字符不同。 它們是: # $ & + , / : ; = ? @ more discussion。

你可能真的不需要JQuery

正如標題所說, 你可能真的不需要JQuery。如果你正在找一些公共的代碼那下面這些會很有用:- AJAX EFFECTS, ELEMENTS, EVENTS, UTILS

你不需要 jQuery

Free yourself from the chains of jQuery by embracing and understanding the modern Web API and discovering various directed libraries to help you fill in the gaps.

http://blog.garstasio.com/you-dont-need-jquery/
有用的 Tips
Selecting Elements
DOM Manipulation

回調函數加載腳本

類似於 異步加載腳本 增加回調函數。

function loadScript(url, callback) { var script = document.createElement('script'); script.async = true; script.src = url; var entry = document.getElementsByTagName('script')[0]; entry.parentNode.insertBefore(script, entry); script.onload = script.onreadystatechange = function () { var rdyState = script.readyState; if (!rdyState || /complete|loaded/.test(script.readyState)) { callback(); // detach the event handler to avoid memory leaks in IE (http://mng.bz/W8fx) script.onload = null; script.onreadystatechange = null; } }; }

執行一次函數

這里展示了如何實現函數只執行一次。

每當你想有一個只運行一次的函數。通常這些函數是以事件監聽的方式,很難管理。當然如果很容易管理,你只需要刪除監聽事件,但是這是個理想的狀態,很多時候你只需要允許一個函數執行一次。下面的代碼可以實現:

// Copy from DWB // http://davidwalsh.name/javascript-once function once(fn, context) { var result; return function() { if(fn) { result = fn.apply(context || this, arguments); fn = null; } return result; }; } // Usagevar canOnlyFireOnce = once(function() { console.log('Fired!');}); canOnlyFireOnce(); // "Fired!"canOnlyFireOnce(); // nada

獲取樣式

獲取行間樣式

<span id="black" style="color: black"> This is black color span </span> <script> document.getElementById('black').style.color; // => black</script>

獲取真正的樣式

<style> #black { color: red !important;} </style> <span id="black" style="color: black"> This is black color span </span> <script> document.getElementById('black').style.color; // => black // real var black = document.getElementById('black'); window.getComputedStyle(black, null).getPropertyValue('color'); // => rgb(255, 0, 0) </script>

ref:https://developer.mozilla.org/en-US/docs/Web/API/Window/getComputedStyle

檢測當前窗口

了解更多: here。

function isElementInViewport (el) { //special bonus for those using jQuery if (typeof jQuery === "function" && el instanceof jQuery) { el = el[0]; } var rect = el.getBoundingClientRect(); return ( rect.top >= 0 && rect.left >= 0 && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && /*or $(window).height() */ rect.right <= (window.innerWidth || document.documentElement.clientWidth) /*or $(window).width() */ ); }


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM