摘要:本文源码为版本。的代码结构也是一个很经典的定义结构构造函数实例修改函数原型共享实例方法,它提供事件通道上事件的订阅撤消订阅调用。
前言
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/
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/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的目标有三个,是针对...
阅读 820·2023-04-26 00:37
阅读 703·2021-11-24 09:39
阅读 2131·2021-11-23 09:51
阅读 3766·2021-11-22 15:24
阅读 732·2021-10-19 11:46
阅读 1866·2019-08-30 13:53
阅读 2406·2019-08-29 17:28
阅读 1312·2019-08-29 14:11