摘要:本文源碼為版本。的代碼結構也是一個很經典的定義結構構造函數實例修改函數原型共享實例方法,它提供事件通道上事件的訂閱撤消訂閱調用。
前言
cordova(PhoneGap) 是一個優秀的經典的中間件框架,網上對其源代碼解讀的文章確實不多,本系列文章試著解讀一下,以便對cordova 框架的原理理解得更深入。本文源碼為cordova android版本6.1.2。
源碼結構我們使用IDE的代碼折疊功能先從整體上把握代碼結構。
/* * 版權申明及注釋部分 */ ;(function() { ... })();
;是保證導入的其它js腳本,使用工具壓縮js文件時不出錯。一個自執行匿名函數包裹,防止內部變量污染到外部命名空間。閱讀過jQuery源碼的人都知道,jQuery的也是相同的結構,只是jQuery定義的匿名函數多了兩個參數window和undefined,然后調用的時候只傳入window,這樣,window可以在jQuery內部安全使用,而undefined也的確表示未定義(有些瀏覽器實現允許重定義undefined)。
繼續展開代碼,可以看到如下的結構:
;(function() { var PLATFORM_VERSION_BUILD_LABEL = "6.1.2"; // 模塊化系統 /* ------------------------------------------------------------- */ var require, // 加載使用module define; // 定義注冊module // require|define 的邏輯 (function () { ... })(); // Export for use in node if (typeof module === "object" && typeof require === "function") { module.exports.require = require; module.exports.define = define; } /* ------------------------------------------------------------- */ // 事件的處理和回調,外部訪問cordova.js的入口 define("cordova", function(require, exports, module) { ... } // JS->Native的具體交互形式 define("cordova/android/nativeapiprovider", function(require, exports, module) { ... } // 通過prompt()和Native交互 define("cordova/android/promptbasednativeapi", function(require, exports, module) { ... } // 用于plugin中校驗參數,比如argscheck.checkArgs("fFO", "Camera.getPicture", arguments); 參數應該是2個函數1個對象 define("cordova/argscheck", function(require, exports, module) { ... } // JS->Native交互時對ArrayBuffer進行uint8ToBase64(WebSockets二進制流) define("cordova/base64", function(require, exports, module) { ... } // 對象屬性操作,比如把一個對象的屬性Merge到另外一個對象 define("cordova/builder", function(require, exports, module) { ... } // 事件通道 define("cordova/channel", function(require, exports, module) { ... } // 執行JS->Native交互 define("cordova/exec", function(require, exports, module) { ... } // 用于Plugin中往已經有的模塊上添加方法 define("cordova/exec/proxy", function(require, exports, module) { ... } // 初始化處理 define("cordova/init", function(require, exports, module) { ... } define("cordova/init_b", function(require, exports, module) { ... } // 把定義的模塊clobber到一個對象,在初始化的時候會賦給window define("cordova/modulemapper", function(require, exports, module) { ... } define("cordova/modulemapper_b", function(require, exports, module) { ... } // 平臺啟動處理 define("cordova/platform", function(require, exports, module) { ... } // 清緩存、loadUrl、退出程序等 define("cordova/plugin/android/app", function(require, exports, module) { ... } // 載所有cordova_plugins.js中定義的模塊,執行完成后會觸發 define("cordova/pluginloader", function(require, exports, module) { ... } define("cordova/pluginloader_b", function(require, exports, module) { ... } // 獲取絕對URL,InAppBrowser中會用到 define("cordova/urlutil", function(require, exports, module) { ... } // 工具類 define("cordova/utils", function(require, exports, module) { ... } // 所有模塊注冊完之后,導入cordova至全局環境中 window.cordova = require("cordova"); // 初始化啟動 require("cordova/init"); })();
從上可以清晰的看出,在cordova內部,首先是定義了兩個公共的require和define函數,然后是使用define注冊所有模塊,再通過window.cordova=require("cordova")導入庫文件至全局執行環境中。
模塊機制類似于Java的package/import,在JavaScript中也有類似的define/require,它用來異步加載module化的js,從而提高運行效率。模塊化加載的必要性,起源于nodejs的出現。但是JavaScript并沒有內置模塊系統,所以就出現了很多規范。?主要有2種:CommonJS 和 AMD(Asynchronous Module Definition)。還有國內興起的CMD(Common Module Definition)?。CommonJS主要面對的是服務器,代表是Node.js;AMD針對瀏覽器進行了優化,主要實現require.js;CMD是seajs。?
cordova-js最開始采用的是require.js作者寫的almond.js(兼容AMD和CommonJS),但之后由于特殊需求(比如模塊不存在的時候要throw異常),最終從almond.js fork過來實現了一個簡易CommonJS風格的模塊系統,同時提供了和nodejs之間很好的交互。在cordova.js中可以直接使用define()和require(),在其他文件可以通過cordova.define()和cordova.require()來調用。所以src/scripts/require.js中定義的就是一個精簡的JavaScript模塊系統。?
// cordova.js內部使用的全局函數require/define var require, define; (function () { // 初始化一個空對象,緩存所有的模塊 var modules = {}, // 正在build中的模塊ID的棧 requireStack = [], // 標示正在build中模塊ID的Map inProgressModules = {}, SEPARATOR = "."; // 模塊build function build(module) { // 備份工廠方法 var factory = module.factory, // 對require對象進行特殊處理 localRequire = function (id) { var resultantId = id; //Its a relative path, so lop off the last portion and add the id (minus "./") if (id.charAt(0) === ".") { resultantId = module.id.slice(0, module.id.lastIndexOf(SEPARATOR)) + SEPARATOR + id.slice(2); } return require(resultantId); }; // 給模塊定義一個空的exports對象,防止工廠類方法中的空引用 module.exports = {}; // 刪除工廠方法 delete module.factory; // 調用備份的工廠方法(參數必須是require,exports,module) factory(localRequire, module.exports, module); // 返回工廠方法中實現的module.exports對象 return module.exports; } // 加載使用模塊 require = function (id) { // 如果模塊不存在拋出異常 if (!modules[id]) { throw "module " + id + " not found"; // 如果模塊正在build中拋出異常 } else if (id in inProgressModules) { var cycle = requireStack.slice(inProgressModules[id]).join("->") + "->" + id; throw "Cycle in require graph: " + cycle; } // 如果模塊存在工廠方法說明還未進行build(require嵌套) if (modules[id].factory) { try { // 標示該模塊正在build inProgressModules[id] = requireStack.length; // 將該模塊壓入請求棧 requireStack.push(id); // 模塊build,成功后返回module.exports return build(modules[id]); } finally { // build完成后刪除當前請求 delete inProgressModules[id]; requireStack.pop(); } } // build完的模塊直接返回module.exports return modules[id].exports; }; // 定義注冊模塊 define = function (id, factory) { // 如果已經存在拋出異常 if (modules[id]) { throw "module " + id + " already defined"; } // 模塊以ID為索引包含ID和工廠方法 modules[id] = { id: id, factory: factory }; }; // 移除模塊 define.remove = function (id) { delete modules[id]; }; // 返回所有模塊 define.moduleMap = modules; })();
首先在外部cordova環境中定義require和define兩個變量,用來存儲實現導入功能的函數和實現注冊功能的函數。然后用一個立即調用的匿名函數來實例化這兩個變量,在這個匿名函數內部,緩存了所有的功能模塊。注冊模塊時,如果已經注冊了,就直接拋出異常,防止無意中重定義,如確實需要重定義,可先調用define.remove。
從內部私有函數build中,可以看出,調用工廠函數時, factory(localRequire, module.exports, module); 第一個參數localRequire實質還是調用全局的require()函數,只是把ID稍微加工了一下支持相對路徑。cordova.js沒有用到相對路徑的require,但在一些Plugin的js中有,比如Contact.js 中 ContactError = require("./ContactError");
這里我們寫個測試用例:
注:module.js為上述cordova的模塊代碼。
上面例子中我們定義了兩個模塊,這里是寫在同一個頁面下,在實際中我們自然希望寫在兩個不同的文件中,然后按需加載。我們上一篇文章中說明了cordova的插件使用方法,我們會發現cordova_plugins.js中定義了cordova插件的id、路徑等變量,并且該文件定義了一個id為cordova/plugin_list的模塊,我們在cordova.js中可以看到有這個模塊的引用。
定義了require和define并賦值后,是將cordova所有模塊一一注冊,例如:
define("cordova",function(require,exports,module){ // 工廠函數內部實現代碼 });
這里需要注意的是,define只是注冊模塊,不會調用其factory。factory函數在這個時候并沒有實際執行,而只是定義,并作為一個參數傳遞給define函數。所有模塊注冊完之后,通過:
window.cordova = require("cordova");
導入至全局環境。
因為是注冊后第一次導入,所以在執行require("cordova")時,modules["cordova"].factory的值是注冊時的工廠函數,轉變為boolean值時為true,從而在這里會通過build調用這個工廠函數,并將這個工廠函數從注冊緩存里面刪除,接下來的就是去執行cordova的這個factory函數了。
事件通道作為觀察者模式(Observer)的一種變形,很多MV*框架(比如:Vue.js、Backbone.js)中都提供發布/訂閱模型來對代碼進行解耦。cordova.js中也提供了一個自定義的pub-sub模型,基于該模型提供了一些事件通道,用來控制通道中的事件什么時候以什么樣的順序被調用,以及各個事件通道的調用。
src/common/channel.js的代碼結構也是一個很經典的定義結構(構造函數、實例、修改函數原型共享實例方法),它提供事件通道上事件的訂閱(subscribe)、撤消訂閱(unsubscribe)、調用(fire)。pub-sub模型用于定義和控制對cordova初始化的事件的觸發以及此后的自定義事件。
頁面加載和Cordova啟動期間的事件順序如下:
onDOMContentLoaded ——(內部事件通道)頁面加載后DOM解析完成
onNativeReady ——(內部事件通道)Cordova的native準備完成
onCordovaReady ——(內部事件通道)所有Cordova的JavaScript對象被創建完成可以開始加載插件
onDeviceReady —— Cordova全部準備完成
onResume —— 應用重新返回前臺
onPause —— 應用暫停退到后臺
可以通過下面的事件進行監聽:
document.addEventListener("deviceready", myDeviceReadyListener, false); document.addEventListener("resume", myResumeListener, false); document.addEventListener("pause", myPauseListener, false);
DOM生命周期事件應用于保存和恢復狀態:
window.onload
window.onunload
define("cordova/channel", function(require, exports, module) { var utils = require("cordova/utils"), nextGuid = 1; // 事件通道的構造函數 var Channel = function(type, sticky) { // 通道名稱 this.type = type; // 通道上的所有事件處理函數Map(索引為guid) this.handlers = {}; // 通道的狀態(0:非sticky, 1:sticky但未調用, 2:sticky已調用) this.state = sticky ? 1 : 0; // 對于sticky事件通道備份傳給fire()的參數 this.fireArgs = null; // 當前通道上的事件處理函數的個數 this.numHandlers = 0; // 訂閱第一個事件或者取消訂閱最后一個事件時調用自定義的處理 this.onHasSubscribersChange = null; }, // 事件通道外部接口 channel = { // 把指定的函數h訂閱到c的各個通道上,保證h在每個通道的最后被執行 join: function(h, c) { var len = c.length, i = len, f = function() { if (!(--i)) h(); }; for (var j=0; j我們可以寫一個測試用例:
但是很多時候我們希望能夠傳遞參數,通過閱讀上面的源碼可以得知:
if (eventListenerOrFunction && typeof eventListenerOrFunction === "object") { // 接收到一個實現handleEvent接口的EventListener對象 handleEvent = eventListenerOrFunction.handleEvent; eventListener = eventListenerOrFunction; } else { // 接收到處理事件的回調函數 handleEvent = eventListenerOrFunction; }我們上面的例子中我們傳遞的是一個方法,這里我們也可以傳遞一個EventListener對象。
// 創建事件通道 channel.create("onTest"); // 訂閱事件 channel.onTest.subscribe(function (event) { console.log(event); console.log(event.data.name+" fire"); }); // 創建 Event 對象 var event = document.createEvent("Events"); // 初始化事件 event.initEvent("onTest", false, false); // 綁定數據 event.data = {name: "test"}; // 觸發事件 channel.onTest.fire(event);工具模塊我們在寫插件的時候如果熟悉cordova自帶的工具函數,可以更加方便的拓展自己的插件。
define("cordova/utils", function(require, exports, module) { var utils = exports; // 定義對象屬性(或方法)的setter/getter utils.defineGetterSetter = function(obj, key, getFunc, opt_setFunc) {...} // 定義對象屬性(或方法)的getter utils.defineGetter = utils.defineGetterSetter; // Array IndexOf 方法 utils.arrayIndexOf = function(a, item) {...} // Array remove 方法 utils.arrayRemove = function(a, item) {...} // 類型判斷 utils.typeName = function(val) {...} // 數組判斷 utils.isArray = Array.isArray || function(a) {return utils.typeName(a) == "Array";}; // Date判斷 utils.isDate = function(d) {...} // 深度拷貝 utils.clone = function(obj) {...} // 函數包裝調用 utils.close = function(context, func, params) {...} // 內部私有函數,產生隨機數 function UUIDcreatePart(length) {...} // 創建 UUID (通用唯一識別碼) utils.createUUID = function() {...} // 繼承 utils.extend = function() {...} // 調試 utils.alert = function(msg) {...} });UUIDcreatePart函數用來隨機產生一個16進制的號碼,接受一個表示號碼長度的參數(實際上是最終號碼長度的一半),一般用途是做為元素的唯一ID。
utils.isArray 在這里不使用instanceof來判斷是不是Array類型,主要是考慮到跨域或者多個frame的情況,多個frame時每個frame都會有自己的Array構造函數,從而得出不正確的結論。使用"[object Array]"來判斷是根據ECMA標準中的返回值來進行的,事實上,這里不需要類型轉換,而可以用全等“===”來判斷。
utils.close函數,封裝函數的調用,將執行環境作為一個參數,調用的函數為第二個參數,調用函數本身的參數為后續參數。
原型繼承實現詳解
utils.extend = (function() { // proxy used to establish prototype chain var F = function() {}; // extend Child from Parent return function(Child, Parent) { F.prototype = Parent.prototype; Child.prototype = new F(); Child.__super__ = Parent.prototype; Child.prototype.constructor = Child; }; }());這里的繼承是通過原型鏈的方式實現,我們可以通過下述方式調用:
var Parent = function () { this.name = "Parent"; } Parent.prototype.getName = function () { return this.name; } var Child = function () { this.name = "Child"; } utils.extend(Child, Parent); var child = new Child(); console.log(child.getName())ES5中有一個Object.create方法,我們可以使用這個函數實現繼承:
// 創建一個新的對象 Object.create = Object.create || function (proto) { var F = function () {}; F.prototype = proto; return new F(); } // 實現繼承 var extend = function(Child, Parent) { // 拷貝Parent原型對象 Child.prototype = Object.create(Parent.prototype); // 將Child構造函數賦值給Child的原型對象 Child.prototype.constructor = Child; } // 實例 var Parent = function () { this.name = "Parent"; } Parent.prototype.getName = function () { return this.name; } var Child = function () { this.name = "Child"; } extend(Child, Parent); var child = new Child(); console.log(child.getName())原型鏈的概念對于初學者而言可能有點繞,但是我們把握構造函數、實例化對象、原型對象三者的關系就很簡單了。我們以此為例說明:
// 構造函數 var Child = function () { this.name = "Child"; } // 原型對象Child.prototype Child.prototype.getName = function () { return this.name; } // 實例化對象 var child = new Child();原型對象是構造函數的prototype屬性,是所有實例化對象共享屬性和方法的原型對象。
實例化對象通過new構造函數得到,都繼承了原型對象的屬性和方法。
如何訪(qiu)問(jie)原型對象?若已知構造函數Child,則可以通過Child.prototype得到;若已知實例化對象child,則可以通過child.__proto__或者Object.getPrototypeOf(child)得到,也通過Object.setPrototypeOf方法來重寫對象的原型。
Child.prototype === child.__proto__ // true child.__proto__ === Object.getPrototypeOf(child) // true原型對象中有個隱式的constructor,指向了構造函數本身,也就是我們可以通過Child.prototype.constructor(雖然看似多此一舉,但是經常需要重新設置構造函數)或child.__proto__.constructor或者Object.getPrototypeOf(child).constructor得到構造函數。
instanceof和Object.isPrototypeOf()可以判斷兩個對象是否是繼承關系
child instanceof Child // true Child.prototype.isPrototypeOf(child) // true至此構造函數、實例化對象、原型對象三者的關系我們已經很清除了,再回過頭看看上面繼承的實現就很簡單了。
我們可以通過instanceof來檢驗是否滿足繼承關系:
child instanceof Child && child instanceof Parent // true其實上述繼承的思路很簡單:
1.首先獲得父類原型對象的方法,這里的F對象作為中間變量引用拷貝Parent.prototype對象(即和Parent.prototype共享同一內存空間);例如我們修改上述的Object.create為:Object.create = function (proto) { var F = function () {}; F.prototype = proto; F.prototype.setName = function(name){ this.name = name; } return new F(); }此時Parent.prototype、Child.prototype、child都擁有的setName方法,但是我們應當避免這樣做,這也是為什么我們不直接通過Child.prototype = Parent.prototype獲得;通過實例化中間對象F間接得到Parent.prototype的方法,此時通過Object.create方法獲得的對象和Parent.prototype不再是共享內存空間。Child通過extend(Child, Parent)從Parent.prototype對象獲得一份新的拷貝。實質是因為我們通過new一個構造函數獲得的實例化對象是獲得了一個新的內存空間,子對象互不影響;
2.對子類進行修正,我們通過拷貝獲得了父類的一個備份,此時子類原型對象下的constructor屬性依然是父類的構造函數,顯然不符合我們的要求,我們需要重置,同時有時候我們希望保留對父類的引用,如cordova這里用一個__super__屬性保存。Child.__super__ = Parent.prototype; Child.prototype.constructor = Child;其實繼承的本質我們是希望能實現以下功能:
父類有的我都有,我也能重載,但不至于影響到父類的屬性和方法
除了繼承之外,我也能添加自己的方法和屬性
我們可以利用es6新特性實現同樣的效果:
class Parent { constructor () { this.name = "Parent"; } getName () { return this.name; } } class Child extends Parent { constructor () { super(); this.name = "Child"; } } var child = new Child(); console.log(child.getName())super關鍵字在這里表示父類的構造函數,用來新建父類的this對象。在子類的構造函數中,只有調用super之后,才可以使用this關鍵字,否則會報錯。這是因為子類實例的構建,是基于對父類實例加工,只有super方法才能返回父類實例。
cordova 模塊本文最后一部分我們來看看cordova模塊,cordova模塊是事件的處理和回調,外部訪問cordova.js的入口。
define("cordova", function(require, exports, module) { if (window.cordova && !(window.cordova instanceof HTMLElement)) { throw new Error("cordova already defined"); } // 導入事件通道模塊 var channel = require("cordova/channel"); // 導入平臺模塊 var platform = require("cordova/platform"); // 保存addEventListener、removeEventListener的原生實現 var m_document_addEventListener = document.addEventListener; var m_document_removeEventListener = document.removeEventListener; var m_window_addEventListener = window.addEventListener; var m_window_removeEventListener = window.removeEventListener; // 緩存所有的事件處理函數 var documentEventHandlers = {}, windowEventHandlers = {}; // 重新定義addEventListener、removeEventListener,方便后面注冊添加pause、resume、deviceReady等事件 document.addEventListener = function(evt, handler, capture) {...} window.addEventListener = function(evt, handler, capture) {...} document.removeEventListener = function(evt, handler, capture) {...} window.removeEventListener = function(evt, handler, capture) {...} function createEvent(type, data) {...} var cordova = { define: define, require: require, version: PLATFORM_VERSION_BUILD_LABEL, platformVersion: PLATFORM_VERSION_BUILD_LABEL, platformId: platform.id, addWindowEventHandler: function(event) {...}, addStickyDocumentEventHandler: function(event) {...}, addDocumentEventHandler: function(event) {...}, removeWindowEventHandler: function(event) {...}, removeDocumentEventHandler: function(event) {...}, getOriginalHandlers: function() {...}, fireDocumentEvent: function(type, data, bNoDetach) {...}, fireWindowEvent: function(type, data) {...}, callbackId: Math.floor(Math.random() * 2000000000), callbacks: {}, callbackStatus: {}, callbackSuccess: function(callbackId, args) {...}, callbackError: function(callbackId, args) {...}, callbackFromNative: function(callbackId, isSuccess, status, args, keepCallback) {...}, addConstructor: function(func) {...} } // 暴露cordova對象給外部 module.exports = cordova; });這里我們以document Event為例說明一下cordova模塊中關于事件的處理:
// 保存addEventListener、removeEventListener的原生實現 var m_document_addEventListener = document.addEventListener; var m_document_removeEventListener = document.removeEventListener; // 緩存事件處理函數 var documentEventHandlers = {}; // 重新定義addEventListener document.addEventListener = function(evt, handler, capture) { var e = evt.toLowerCase(); if (typeof documentEventHandlers[e] != "undefined") { documentEventHandlers[e].subscribe(handler); } else { m_document_addEventListener.call(document, evt, handler, capture); } }; // 重新定義removeEventListener document.removeEventListener = function(evt, handler, capture) { var e = evt.toLowerCase(); // If unsubscribing from an event that is handled by a plugin if (typeof documentEventHandlers[e] != "undefined") { documentEventHandlers[e].unsubscribe(handler); } else { m_document_removeEventListener.call(document, evt, handler, capture); } }; // 創建 Event 對象 function createEvent(type, data) { var event = document.createEvent("Events"); event.initEvent(type, false, false); if (data) { for (var i in data) { if (data.hasOwnProperty(i)) { event[i] = data[i]; } } } return event; } var codova = { ... // 創建事件通道 addStickyDocumentEventHandler:function(event) { return (documentEventHandlers[event] = channel.createSticky(event)); }, addDocumentEventHandler:function(event) { return (documentEventHandlers[event] = channel.create(event)); }, // 取消事件通道 removeDocumentEventHandler:function(event) { delete documentEventHandlers[event]; }, // 發布事件消息 fireDocumentEvent: function(type, data, bNoDetach) { var evt = createEvent(type, data); if (typeof documentEventHandlers[type] != "undefined") { if( bNoDetach ) { documentEventHandlers[type].fire(evt); } else { setTimeout(function() { // Fire deviceready on listeners that were registered before cordova.js was loaded. if (type == "deviceready") { document.dispatchEvent(evt); } documentEventHandlers[type].fire(evt); }, 0); } } else { document.dispatchEvent(evt); } }, ... } module.exports = cordova;在初始化啟動模塊cordova/init中有這樣的代碼:
// 注冊pause、resume、deviceReady事件 channel.onPause = cordova.addDocumentEventHandler("pause"); channel.onResume = cordova.addDocumentEventHandler("resume"); channel.onActivated = cordova.addDocumentEventHandler("activated"); channel.onDeviceReady = cordova.addStickyDocumentEventHandler("deviceready"); // 監聽DOMContentLoaded事件并發布事件消息 if (document.readyState == "complete" || document.readyState == "interactive") { channel.onDOMContentLoaded.fire(); } else { document.addEventListener("DOMContentLoaded", function() { channel.onDOMContentLoaded.fire(); }, false); } // 原生層加載完成事件 if (window._nativeReady) { channel.onNativeReady.fire(); } // 加載完成發布時間事件消息 channel.join(function() { modulemapper.mapModules(window); platform.initialize && platform.initialize(); channel.onCordovaReady.fire(); channel.join(function() { require("cordova").fireDocumentEvent("deviceready"); }, channel.deviceReadyChannelsArray); }, platformInitChannelsArray);這里通過addDocumentEventHandler及addStickyDocumentEventHandler創建了事件通道,并通過fireDocumentEvent或者fire發布事件消息,這樣我們就可以通過document.addEventListener訂閱監聽事件了。
如果我們要創建一個自定義事件Test,我們可以這樣做:
// 創建事件通道 cordova.addWindowEventHandler("Test"); // 發布事件消息 cordova.fireWindowEvent("Test", { name: "test", data: { time: new Date() } }) // 訂閱事件消息 window.addEventListener("Test", function (evt) { console.log(evt); });參考Cordova 3.x 入門 - 目錄
寫在后面
PhoneGap源碼分析本文至此已經說完了cordova的模塊機制和事件機制,已經cordova的工具模塊,了解這些后寫起插件來才能得心應手,對于原理實現部分不屬于本文的范疇,下一篇會詳細講解cordova原理實現。敬請關注,不過近來在寫畢設,估計一時半會兒也不會寫完,本文前前后后已是拖了半個月。如果覺得本文對您有幫助,不妨打賞支持以此鼓勵。
轉載需標注本文原始地址:https://zhaomenghuan.github.io/
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/82904.html
摘要:任何初始化任務應該在文件中的事件的事件處理函數中。這個配置文件有幾個地方很關鍵,一開始沒有認真看,將插件導進工程跑的時候各種問題,十分頭痛,不得不重新認真看看文檔。 前言 來新公司的第一個任務,研究hybrid App中間層實現原理,做中間層插件開發。這個任務挺有意思,也很有挑戰性,之前在DCloud雖然做過5+ App開發,但是中間層的東西確實涉及不多。本系列文章屬于系列開篇cord...
摘要:之前做過一點前端的小項目所以前端還算熟練因為最近在準備所以想能不能寫一個背單詞軟件正好這學期有個開發課,就用來當大作業了前端后端如何在下調試當然是代理啦在之前兩個項目中為了不用代理強行在后端啟用了事實證明這是個愚蠢的決定因為完全不適合做后端 之前做過一點前端的小項目所以前端還算熟練因為最近在準備GRE所以想能不能寫一個背單詞軟件正好這學期有個Android開發課,就用來當大作業了 前端...
摘要:第期新聞發布是熱門的通用服務器端的延伸框架,近日發布了版本。官方博客相關報導官網全新改版工具的官網近日全新改版上線,文檔與首頁都有全新的版面與改善。官方網站研習會報導是在月底,于歐洲的社群的一個研習會活動。 第022期 (2017.04.02) 新聞 Next.js發布2.0 Next.js是熱門的通用(服務器端)的React延伸框架,近日發布了2.0版本。2.0的目標有三個,是針對...
閱讀 821·2023-04-26 00:37
閱讀 706·2021-11-24 09:39
閱讀 2132·2021-11-23 09:51
閱讀 3769·2021-11-22 15:24
閱讀 734·2021-10-19 11:46
閱讀 1868·2019-08-30 13:53
閱讀 2410·2019-08-29 17:28
閱讀 1314·2019-08-29 14:11