
如果你有javascript的组件开发经验,我这个层级的代码相信你一下子就能看明白。源码中我还给出了一个demo,这个demo模拟了一个比较贴近现实需求的一个场景:
1)用户点击界面上的某个按钮,打开之前定义的一个modal框:

2)用户在打开的modal框内填写一些表单,点击确定的时候,会触发一些校验:
没填email时:

填写了email之后:

这两个提示其实是为了演示Alert和Confirm的效果硬塞进去的,实际上可能没有这么别扭的功能。
3)在提示Password为空的时候,细心的人会发现那个确定按钮处于一个禁用的状态,这个考虑是因为确定按钮最终要完成的是一些异步任务,在异步任务成功完成之前,我希望modal组件都不要关闭,并且能够控制已点击的按钮不能重复点击;
4)我用setTimeout模拟了一个异步任务,这个异步任务在点击确定按钮之后,3s才会回调,并且:
当email输入admin@admin 的时候,会给出提交成功的提示,确定之后就会关闭所有的弹框:

当email输入其它值得时候,会给出提交失败的提示,并且modal框会依然显示在那里:


在组件定义里面,尤其是注册按钮这一块,我加了一些AOP编程的处理,同时利用了jquery的延迟对象,来实现我需要的异步编程,详情请阅读源码,有问题可以在评论区交流赐教。
2. 组件需求
有时候为了写一个好用的组件,只需要把它的大概原型和要对外部提供的接口确定下来,就已经完成这个组件编写最重要的工作了,虽然还没有开始编码。以本文要编写的这几个组件来说,我想要的这几个组件的原型和调用形式分别是这样的:
1)自定义alert框
原型是:

调用时最多需要两个参数,一个msg用来传递要显示的提示内容,一个onOk用来处理确定按钮点击时候的回调,调用形式有以下2种:
//1Alert("您选择的订单状态不符合当前操作的条件,请刷新列表显示最新数据后再继续操作!");//2Alert({msg: "您选择的订单状态不符合当前操作的条件,请刷新列表显示最新数据后再继续操作!",onOk: function(){}}); 第一种是没有回调的情况,那么直接传递msg即可,第二种是有回调的情况,用options对象的方式来传递msg和onOks回调这两个参数。不管onOk回调有没有,点击按钮的时候都要关闭弹框。
Confirm({msg: "您选择的订单状态不符合当前操作的条件,请确认是否要继续操作!",onOk: function(){},onCancel: function(){}}); onCancel是在点击取消按钮时候的回调。不管onOk和onCancel回调有没有,点击按钮的时候都要关闭弹框。onCancel回调可以没有。
调用形式:
var modal = new Modal({title: "",content: "",width: 600,buttons: [{html: "<button type="button" class="btn btn-sm btn-primary btn-ok">确定</button>",selector: ".btn-ok",callback: function(){//点击确定按钮的回调}},{html: "<button type="button" class="btn btn-sm btn-default btn-cancel">取消</button>",selector: ".btn-cancel",callback: function(){//点击取消按钮的回调}}],onContentReady: function(){//当modal添加到DOM并且初始化完毕时的事件回调,每个modal实例这个回调只会被触发一次},onContentChange: function(){//当调用modal.setContent类似的方法改变了modal内容时的事件回调},onModalShow: function(){//当调用modal.open类似方法显示modal时都会触发的事件回调},onModalHide: function(){//当调用modal.hide类似方法隐藏modal时都会触发的事件回调}});$("#btn-audit").click(function(){modal.open();}); 跟Alert和Confirm不同的是,一个页面里面只需要一个Alert和Confirm的实例,但是可能需要多个Modal的实例,所以每个Modal对象都需要单独new一下。由于每个Modal要完成的事情都不相同,所以:var modal = new Modal({title: "测试modal",content: $("#modal-tpl").html(),width: 500,onOk: function(){var $form = this.$modal.find("form");var data = $form.serializeArray();var postData = {};data.forEach(function(obj){postData[obj.name] = obj.value;});if(!postData.email) {Alert("请输入EMAIL!");return false;}var deferred = $.Deferred();if(!postData.password) {Confirm({msg: "Password为空,是否要继续?",onOk: function(){_post();},onCancel: function(){deferred.reject();}})} else {_post();}return $.when(deferred);function _post(){//模拟异步任务setTimeout(function(){if(postData.email === "admin@admin") {Alert({msg: "提交成功!",onOk: function(){deferred.resolve();}});} else {Alert({msg: "提交失败!",onOk: function(){deferred.reject();}});}},3000);}},onModalShow: function () {var $form = this.$modal.find("form");$form[0].reset();}});$("#btn-modal").click(function () {modal.open();}); 3. 实现要点var $body = $(document.body),BackDrop = (function () {var $backDrop,count = 0,create = function () {$backDrop = $("<div class="modal-backdrop fade in"></div>").appendTo($body);};return {show: function () {!$backDrop && create();$backDrop[0].style.display = "block";count++;},hide: function () {count--;if (!count) {$backDrop.remove();$backDrop = undefined;}}}})(); 这段代码中引入count变量的原因是因为BackDrop是一个全局的单例对象,当调用多个modal实例的open方法的时候,都会调用BackDrop的show方法,为了保证在调用BackDrop的hide方法时,能够确保在所有的modal实例都关闭之后再隐藏Backdrop,所以就加了一个count变量来记录BackDrop的show方法被调用了多少次,只有当count为0的时候,调用BackDrop的hide方法才会真正隐藏BackDrop。ModalDialog.defaults = {title: "",content: "",width: 600,buttons: [{html: "<button type="button" class="btn btn-sm btn-primary btn-ok">确定</button>",selector: ".btn-ok",callback: getDefaultBtnCallbackProxy("onOk")},{html: "<button type="button" class="btn btn-sm btn-default btn-cancel">取消</button>",selector: ".btn-cancel",callback: getDefaultBtnCallbackProxy("onCancel")}],onOk: $.noop,onCancel: $.noop,onContentReady: $.noop,onContentChange: $.noop,//content替换之后的回调onModalShow: $.noop,onModalHide: $.noop//modal关闭之后的回调}; 通过buttons配置两个默认的按钮,确定和取消,然后为了简化这两个默认按钮的回调配置,把这两个按钮的接口进一步扩展到了上一级别,onOk和onCancel分别会在点击确定和取消的时候被调用,这两个选项完全是函数回调,不像onContentReady这种属于事件回调。getDefaultBtnCallbackProxy用来辅助注册onOk和onCancel:var getDefaultBtnCallbackProxy = function (callbackName) {return function () {var opts = this.options,callback = opts[callbackName] && typeof opts[callbackName] === "function" ? opts[callbackName] : "";return callback && callback.apply(this, arguments);}}里面的this会被绑定到modal实例上。function ModalDialog(options) {this.options = this.getOptions(options);this.$modal = undefined;this.$modalTitle = undefined;this.$modalBody = undefined;this.$modalFooter = undefined;this.state = undefined;}这个主要是声明了用到的一些实例变量。open: function (state) {this.state = state;!this.$modal && initModal(this, this.options);BackDrop.show();this.$modal.modal("show");}这是个原型方法,组件的初始化也是在这个方法调用的时候执行的(延迟初始化)。initModal = function (that, opts) {var $modal = createModal(that);that.setTitle(opts.title);that.setContent(opts.content);that.addButtons(opts.buttons);that.setWidth(opts.width);bindHandler(that, opts);$modal.modal();//调用bootstrap的Modal组件$modal.trigger("contentReady");}这是个函数,用来初始化组件。其中:bindHandler = function (that, opts) {var $modal = that.$modal;typeof opts.onContentChange === "function" && $modal.on("contentChange", $.proxy(opts.onContentChange, that));typeof opts.onContentReady === "function" && $modal.on("contentReady", $.proxy(opts.onContentReady, that));typeof opts.onModalShow === "function" && $modal.on("modalShow", $.proxy(opts.onModalShow, that));typeof opts.onModalHide === "function" && $modal.on("modalHide", $.proxy(opts.onModalHide, that));$modal.on("show.bs.modal", function () {$modal.trigger("modalShow");});$modal.on("hidden.bs.modal", function () {$modal.trigger("modalHide");});}为了方便使用,我把onContentChange这几个回调的上下文绑定到了当前的modal实例。最后两个事件侦听就是把bootstrap的事件封装成了我定义的modal事件。addButtons: function (buttons) {var buttons = !$.isArray(buttons) ? [] : buttons,that = this,htmlS = [];buttons.forEach(function (btn) {htmlS.push(btn.html);btn.selector && that.$modal.on("click", btn.selector, $.proxy(function (e) {var self = this,$btn = $(e.currentTarget);//先禁用按钮$btn[0].disabled = true;var callback = typeof btn.callback === "function" ? btn.callback : "",ret = callback && callback.apply(self, arguments);if (ret === false) {$btn[0].disabled = false;return;}if (typeof(ret) === "object" && "done" in ret && typeof ret["done"] === "function") {//异步任务只有在成功回调的时候关闭Modalret.done(function () {that.hide();}).always(function () {$btn[0].disabled = false;});} else {$btn[0].disabled = false;that.hide();}}, that));});this.$modalFooter.prepend($(htmlS.join("")));}从这个代码可以看出:var Alert, Confirm;(function () {var modal,Proxy = function (isAlert) {return function () {if (arguments.length != 1) return;var msg = typeof arguments[0] === "string" && arguments[0] || arguments[0].msg || "",onOk = typeof arguments[0] === "object" && typeof arguments[0].onOk === "function" && arguments[0].onOk,onCancel = typeof arguments[0] === "object" && typeof arguments[0].onCancel === "function" && arguments[0].onCancel,width = typeof arguments[0] === "object" && arguments[0].width || 400,_onModalShow = function () {this.setWidth(width);this.setContent(msg);this[(isAlert ? "hide" : "show") + "Button"](".btn-cancel");},_onModalHide = function () {this.setContent("");};//延迟初始化modalif(!modal) {modal = new Modal({"title": "操作提示",onModalShow: _onModalShow,onModalHide: _onModalHide,onContentReady: function(){this.$modalBody.css({"padding-top": "30px","padding-bottom": "30px"})}});} else {//如果modal已经初始化则需要重新监听事件var $modal = modal.$modal;$modal.off("modalShow modalHide");$modal.off("modalShow modalHide");$modal.on("modalShow", $.proxy(_onModalShow, modal));$modal.on("modalHide", $.proxy(_onModalHide, modal));}modal.setOptions({onOk: onOk || $.noop,onCancel: onCancel || $.noop});modal.open();}};Alert = Proxy(true);Confirm = Proxy();})(); 这段代码里: