在class出现之前,我们实现一个类通常通过定义1个function A,并在A.prototype上定义方法和属性,例如:
function Browser (soft) { ///<summary>软件api</summary> ///<field name="parent" type="ZXYSoft">soft根</field> ///<field name="Size" type="Sizes">各种窗体大小值</field> ///<field name="Location" type="Locations">各种窗体坐标值</field> this.parent = soft; this._ver = undefined; this.Size = new Sizes(); this.Location = new Locations(); }; Browser.prototype._webkitVer = function () { /// <summary>webkit底层版本号(1000表示旧版本,1001表示qt-webkit)</summary> /// <returns type="Number" /> if (this._ver == null) { if (this.parent.isApp) { var ver = this.parent.APP.ver; this._ver = typeof ver === "number" ? ver : 1000; } else { this._ver = 1000; } } return this._ver; }; Browser.prototype.appname = function () { ///<summary>应用名(exe名)</summary> ///<returns type="String" /> if (this.parent.isApp) { var appname = this.parent.APP.appname; return typeof appname === "string" ? appname : ""; } return ""; };
在2014年的时候,公司接入了第一个Hybrid App-新浪页游助手(webkit[vc]+html+js),前端基础库由我负责实现,当时的基础库的编写大量应用了prototype,我按功能模块将他们封装在不同使用闭包中,下面为截取的一部分源码:
//窗体设置相关 Browser.prototype.SetWindowPos = function (hWndlnsertAfter, x, y, cx, cy, ulflag) { ///<param type="Number" name="hWndlnsertAfter">在z序中的位于被置位的窗口前的窗口句柄。 参看枚举》Enum.SetWindowPosOrder</param> ///<param type="Number" name="x">以客户坐标指定窗口新位置的左边界</param> ///<param type="Number" name="y">以客户坐标指定窗口新位置的顶边界</param> ///<param type="Number" name="cx">以像素指定窗口的新的宽度</param> ///<param type="Number" name="cy">以像素指定窗口的新的高度</param> ///<param type="Number" name="ulflag">窗口尺寸和定位的标志。 参看枚举》Enum.SetWindowPosUlflag</param> ///<returns type="Boolean">此处无返回值</returns> if (this.parent.isApp/* && typeof this.parent.APP.setWindowPos === "function"*/) { return this.parent.APP.setWindowPos(hWndlnsertAfter, x, y, cx, cy, ulflag) === 1; } return false; }; Browser.prototype.setTopMost = function (isTopMost) { /// <summary>设置窗体置顶</summary> /// <param name="isTopMost" type="Boolean">是否置顶</param> /// <returns type="Boolean">是否设置成功</returns> var type = isTopMost ? Enum.SetWindowPosOrder.HWND_TOPMOST : Enum.SetWindowPosOrder.HWND_NOTOPMOST; return this.SetWindowPos(type, 0, 0, 0, 0, Enum.SetWindowPosUlflag.SWP_NOMOVE | Enum.SetWindowPosUlflag.SWP_NOSIZE | Enum.SetWindowPosUlflag.SWP_NOACTIVATE); }; Browser.prototype.setWindowSize = function (x, y, width, height) { ///<summary>设置窗体位置和大小</summary> ///<param type="Number" name="x">以客户坐标指定窗口新位置的左边界。</param> ///<param type="Number" name="y">以客户坐标指定窗口新位置的顶边界。</param> ///<param type="Number" name="width">以像素指定窗口的新的宽度。</param> ///<param type="Number" name="height">以像素指定窗口的新的高度。</param> ///<returns type="Boolean">是否设置成功</returns> if (this.parent.isApp/* && typeof this.parent.APP.setWindowSize === "function"*/) { return this.parent.APP.setWindowSize(x, y, width, height) === 1; } return false; };
//坐标相关 Browser.prototype.getScreenCenterLocation = function (width, height) { ///<summary>根据指定的矩形窗口大小返回居中于屏幕(包含任务栏)的坐标</summary> ///<returns type="Object" /> var s = this.Size.screen; var x = parseInt((s.width() - width) / 2); var y = parseInt((s.height() - height) / 2); return { x: x, y: y } }; Browser.prototype.getDesktopCenterLocation = function (width, height) { ///<summary>根据指定的矩形窗口大小返回居中于桌面(不含任务栏)的坐标</summary> ///<returns type="Object" /> var s = this.Size.desktop; var x = parseInt((s.width() - width) / 2); var y = parseInt((s.height() - height) / 2); return { x: x, y: y } }; Browser.prototype.getAppCenterLocation = function (width, height) { ///<summary>根据指定的矩形窗口大小返回居中于当前窗口的坐标(若窗体居中位置在屏幕外会自动被修正到屏幕边缘)</summary> ///<returns type="Object" /> var s = this.Size.window; var p = this.Location.window; var x_max = this.Size.desktop.width() - width; var y_max = this.Size.desktop.height() - height; var x = p.x() + parseInt((s.width() - width) / 2); var y = p.y() + parseInt((s.height() - height) / 2); if (x < 0) { x = 0; } else if (x > x_max) { x = x_max; } if (y < 0) { y = 0; } else if (y > y_max) { y = y_max; } return { x: x, y: y } };
前端模块化兴起,webpack大行其道。2019年5月,终于到了需要在代码层重构该库的时候,如何将这超过3400行的代码重构成符合ES6规范的并且能拆解成若干模块的文件成为了第一道拦路虎。
我们知道Class可以通过extends关键字继承来自其他类的所有属性和方法,参看:http://es6.ruanyifeng.com/#docs/class-extends ,但这也产生了根本问题,我可能按功能模块拆分出了N个js文件(N>2),如何将他们合并到1个Class供外部统一使用?最直观的方法可能如下
//child2.js class Child2 { constructor(){} } export {Child2};
//child1.js import { Child2 } from "./child2.js"; class Child1 extends Child2 { constructor(){} } export {Child1};
//browser.js import { Child1 } from "./child1.js"; class Browser extends Child1{ constructor(){} }
上述代码虽然按模块拆分了功能,但是各子模块间存在extends,没有真正的分离,被我抛弃了,我在阮一峰大拿关于Class的继承章节的结尾找到了关于Mixin 模式的实现,做了一些细微的修改:
//utils.js /*** * mixins class * @param {...Class} mixins */ function mixinsClass(...mixins) { class MixinClass { constructor() { for (let mixin of mixins) { copyProperties(this, new mixin(this)); // 拷贝实例属性 同时执行内部初始化 } } }; let proto = MixinClass.prototype; for (let mixin of mixins) { copyProperties(MixinClass, mixin); // 拷贝静态属性 copyProperties(proto, mixin.prototype); // 拷贝原型属性 } return MixinClass; } function copyProperties(target, source) { for (let key of Reflect.ownKeys(source)) { if (key !== 'constructor' && key !== 'prototype' && key !== 'name' ) { let desc = Object.getOwnPropertyDescriptor(source, key); Object.defineProperty(target, key, desc); } } } export { mixinsClass, }
和阮一峰提供的范例版本相比,唯一差别在
copyProperties(this, new mixin(this)); // 我将this指针传入到各组成成员的constructor中,这样方便子模块对新对象做一些数据绑定。
以下为一些代码选段:
//soft.js import { mixinsClass } from "./utils.js"; import { Enum } from "./Enum.js"; //mixins-import import { DataFormat } from "./soft-mixins/DataFormat.js"; import { Version } from "./soft-mixins/Version.js"; import { Debug } from "./soft-mixins/Debug.js"; import { Method } from "./soft-mixins/Method.js"; import { IframeListener } from "./soft-mixins/IframeListener.js"; import { BossKey } from "./soft-mixins/BossKey.js"; import { JSONP } from "./soft-mixins/JSONP.js"; import { GetIframeData } from "./soft-mixins/GetIframeData.js"; import { Ajax } from "./soft-mixins/Ajax.js"; import { PostMessage } from "./soft-mixins/PostMessage.js"; /** * 软件框架级接口层 * @augments Method * @augments DataFormat * @augments Version * @augments Debug * @augments IframeListener * @augments BossKey * @augments JSONP * @augments GetIframeData * @augments Ajax * @augments PostMessage * */ class Soft extends mixinsClass( Method, DataFormat, Version, Debug, IframeListener, BossKey, JSONP, GetIframeData, Ajax, PostMessage ) { /** * 当前页面是否运行在软件中 */ isApp = false; /** * 软件接口根 * @type {Object} */ APP = null; /** * 枚举、常量值 */ Enum = Enum; /** * url模块 */ Url = new Url(); /** * 软件层交互 * * @type {Browser} */ get Browser(){ if(this._Browser==null){ this._Browser=new Browser(this); } return this._Browser; } } export { Soft, Enum, }
//Version.js //flash检测版本比对模块 class Version { /** * 获取Flash版本号 不存在返回undefined * @returns {String|Undefined} */ getFlashVersion () { let result; for (let i =0,length=navigator.plugins.length;i<length;i++) { let tmp = navigator.plugins[i]; if (tmp && typeof tmp.name === "string" && tmp.name.toLowerCase() === "shockwave flash") { let filename = tmp.filename; if (typeof filename === "string") { if (/^NPSWF\d+(_\d+)+.dll$/gi.test(filename)) { filename = filename.replace(/^NPSWF\d+((?:(?:_|\.|-)\d+)+).dll$/gi, "$1").replace(/_/gi, "."); return filename.substr(1); } else { let description = tmp.description; if (/^Shockwave Flash \d+(\.\d+)+ /gi.test(description)) {//可能有多版本 不能直接return result = description.replace(/^Shockwave Flash (\d+(?:\.\d+)+) .*$/gi, "$1"); } } } }; } return result; }; /** * 比较versionA和versionB的版本,如果A的版本高于或等于B版本,返回true * @param {String} versionA 版本versionA eg:1.1 1.1.1 1.1.1.1 * @param {String} versionB 版本versionB eg:1.1 1.1.1 1.1.1.1 * @returns {Boolean} */ compareVersion (versionA, versionB) { if (typeof versionA !== "string") return false; if (typeof versionB !== "string") return true; let verReg = /^\d{1,7}(\.\d{1,7})*$/i; if (!verReg.test(versionA)) return false; if (!verReg.test(versionB)) return true; versionA = versionA.split("."); versionB = versionB.split("."); let lengthA = versionA.length; let lengthB = versionB.length; if (lengthA > lengthB) { for (let i = lengthB; i < lengthA; i++) { versionB.push("0"); } lengthB = lengthA; } else if (lengthA < lengthB) { for (let i = lengthA; i < lengthB; i++) { versionA.push("0"); } lengthA = lengthB; } for (let i = 0; i < lengthA; i++) { let tmpA = parseInt(versionA[i]); let tmpB = parseInt(versionB[i]); if (tmpA > tmpB) { return true; } else if (tmpA < tmpB) { return false; } } return true; }; } export {Version};
//method.js /** * 提供给软件的交互api */ let openapi = {}; //软件交互相关方法 class Method{ /** * * @param {*} extendCls 最终继承类 */ constructor(extendCls){ /** * 注册到window提供给软件端调用 */ window.invokeMethod = function (modulename, methodname, parm, parmIsJSON) { return this.data2String(this.invokeWebMethod(modulename, methodname, parm, parmIsJSON)); }.bind(extendCls); } /** * 获取指定的已注册的接口 * @param {String} modulename 模块名 * @param {String} methodname 方法名 * @returns {Function|Undefined} */ getWebMethod(modulename, methodname){ if (typeof modulename !== "string" || modulename.length<1 || typeof methodname !== "string"||methodname.length<1) return; let fun = openapi[modulename]; if (typeof fun != "object") return; fun = fun[methodname]; if (typeof fun !== "function") return; return fun; } //web提供给软件端的接口 /** * 插入1个可供软件端调用(invokeMethod)的api * @param {String} modulename 模块名 * @param {String} methodname 方法名 * @param {Function} method 函数 */ addWebMethod (modulename, methodname, method) { if (typeof modulename !== "string" || typeof methodname !== "string" || typeof method !== "function") return false; if (!openapi[modulename]) openapi[modulename] = {}; delete openapi[modulename][methodname]; if (modulename !== "ServerData" && modulename !== "ServerJSONP" && modulename !== "DEBUG") { let name = "WM_" + modulename + "_" + methodname; delete this[name]; this[name] = method; name = undefined; } openapi[modulename][methodname] = method; modulename = methodname = method = null; return true; }; /** * 移除1个可供软件端调用(invokeMethod)的api * @param {String} modulename 模块名 * @param {String} methodname 方法名 */ removeWebMethod (modulename, methodname) { if (typeof modulename !== "string" || typeof methodname !== "string") return false; if (!openapi[modulename]) return false; if (openapi[modulename][methodname] === undefined) return false; if (!delete openapi[modulename][methodname]) { openapi[modulename][methodname] = null; } if (modulename !== "ServerData" && modulename !== "ServerJSONP" && modulename !== "DEBUG") { let name = "WM_" + modulename + "_" + methodname; delete this[name]; name = undefined; } return true; } /** * 引发addWebMethod的方法 * @param {String} modulename 模块名 * @param {String} methodname 方法名 * @param {*} parm 传递的参数信息 * @param {Boolean} [parmIsJSON] 指示parm是否就是字符串,无需转义(当遇到base64图片流的时候请使用该项),默认为true,将尝试将JSON.parse(parm) */ invokeWebMethod (modulename, methodname, parm, parmIsJSON) { if (typeof modulename !== "string" || typeof methodname !== "string") return undefined; let fun = openapi[modulename]; if (!fun) { return undefined; } fun = fun[methodname]; if (typeof fun !== "function") { return undefined; } (parmIsJSON === undefined || parmIsJSON === true) && (parm = this.string2Data(parm)); return fun.call(this, parm); }; //软件端提供给web的接口 /** 调用软件端方法 (异步,无返回值) * @param {String} modulename 模块名 * @param {String} methodname 方法名 * @param {Object} parm 传递的参数信息 */ invokeSoftMethodAsync(modulename, methodname, parm) { }; /** * 调用软件端方法 * @param {String} modulename 模块名 * @param {String} methodname 方法名 * @param {Object} parm 传递的参数信息 * @param {Boolean} [resultIsJSON] 指示是否尝试转义软件返回值成json,若无需转义(当遇到base64图片流的时候请使用该项)则设置成false,默认为true */ invokeSoftMethod (modulename, methodname, parm, resultIsJSON) { }; /** * 跨窗体调用其他窗体前端注册的方法(addWebMethod) (异步,无返回值) * @param {Number} winType 方法所在窗体类型 Enum.WINDOW_TYPE 枚举值 如 Enum.WINDOW_TYPE.MAIN * @param {String} modulename 模块名 * @param {String} methodname 方法名 * @param {*} parm 传递的参数信息 * @param {Boolean} [parmIsJSON] parm是否是JSON,如果为true则接收方将尝试JSON.parse(parm) 默认为true */ crossWindowInvokeWebMethodAsync = function (winType, modulename, methodname, parm, parmIsJSON) { }; /** * 跨窗体调用其他窗体前端注册的方法(addWebMethod) * @param {Number} winType 方法所在窗体类型 Enum.WINDOW_TYPE 枚举值 如 Enum.WINDOW_TYPE.MAIN * @param {String} modulename 模块名 * @param {String} methodname 方法名 * @param {*} parm 传递的参数信息 * @param {Boolean} [parmIsJSON] parm是否是JSON,如果为true则接收方将尝试JSON.parse(parm) 默认为true * @param {Boolean} [resultIsJSON] 指示是否尝试转义返回值成json,若无需转义(当遇到base64图片流的时候请使用该项)则设置成false,默认为true */ crossWindowInvokeWebMethod (winType, modulename, methodname, parm, parmIsJSON, resultIsJSON) { }; /** * 跨窗体调用其他窗体的指定Iframe下前端注册的方法(addWebMethod) (异步,无返回值) * @param {Number} winType 方法所在窗体类型 Enum.WINDOW_TYPE 枚举值 如 Enum.WINDOW_TYPE.MAIN * @param {String} frameId iframe的id (请注意,不支持孙级iframe,仅支持子级) * @param {String} modulename 模块名 * @param {String} methodname 方法名 * @param {*} parm 传递的参数信息 * @param {Boolean} [parmIsJSON] parm是否是JSON,如果为true则接收方将尝试JSON.parse(parm) 默认为true */ crossWindowInvokeWebMethod2Async(winType, frameId, modulename, methodname, parm, parmIsJSON) { }; /** * 跨窗体调用其他窗体的指定Iframe下前端注册的方法(addWebMethod) * @param {Number} winType 方法所在窗体类型 Enum.WINDOW_TYPE 枚举值 如 Enum.WINDOW_TYPE.MAIN * @param {String} frameId iframe的id (请注意,不支持孙级iframe,仅支持子级) * @param {String} modulename 模块名 * @param {String} methodname 方法名 * @param {*} parm 传递的参数信息 * @param {Boolean} [parmIsJSON] parm是否是JSON,如果为true则接收方将尝试JSON.parse(parm) 默认为true * @param {Boolean} [resultIsJSON] 指示是否尝试转义返回值成json,若无需转义(当遇到base64图片流的时候请使用该项)则设置成false,默认为true */ crossWindowInvokeWebMethod2(winType, frameId, modulename, methodname, parm, parmIsJSON, resultIsJSON) { }; } export {Method};
不幸的是,JSDoc 似乎目前仅支持标记1个 @augments(@extends) ,智能提示方面可能需要编写.d.ts