2025年6月

1、背景

最近接触到fastadmin,在处理表单验证方面,fastadmin使用的是Nice-validator验证插件,刚开始用着还挺爽,表单的验证也挺方便,毕竟人家已经提供了很多的验证规则,但是有这么个需求,那就是修改个人信息时需要确保账号的唯一性,也就是唯一性验证,这个人前端也想到了,给了你一个remote()远程验证,这个函数还可以传参数,逗号隔开即可,看起来也挺方便,但用起来有些困难。。。。
2、问题

先谈需求:1、账号唯一性验证   2、其它字段的验证  3、ajax提交  

再来看看具体遇到哪些问题:

1、远程验证remote 这玩意官网上说  (图在下面)    返回字符串说是可以直接显示,我后端用的php,我return 字符串也不行, 它说还有统一格式 $this->success('ok'),这样说也行,那行吧,人家官网上说格式统一,那咱试试,结果不显示信息,就是不管是否唯一,后端会返回一个msg,展示到前端页面上给用户看,但是$this->success()返回的是个json(我的这个success()返回的是json),好吧,json,人家官网也有说json数据咋处理,照着官网的代码走一遍,结果。。。。。。js报错,提示说validator 不是个函数。。。。。我也是醉了,看看jq,有啊,看看jQuery.validator.js也有,顺序也没错。。。。有点让人无语    然后想着用input的失去焦点blur函数来触发这个 验证 于是我就将 $('#form').validator()函数 写在了 blur函数里面,然后 还真别说,真的能用,,,,,

2、remote 能用了,也能显示msg信息了(如何显示msg信息?获取到了data中的msg然后用js添加到页面即可!有没有自动,fastadmin给我们准备好了的那种?不好意思,母鸡啊),但是其他字段的验证规则有失效了。。。。。然后没办法,在看看官方文档吧,就看到验证规则有两种绑定方式,一种是dom,一种是js,我好像两种都用了,那可以理解为啥其它字段的验证规则不能用了,冲突了,只能二选一,可是dom已经绑定了,不想再用js绑定了,这简单啊,把那个validator删掉,直接用jq,当input失去焦点时发起个ajax请求就完事了

3、ajax提交,发现验证成不成功都能发起ajax,毕竟ajax那玩意儿只要单击了就给你发起ajax了,而且ajax提交会导致验证只生效一次,第二次没反应。。。。

js加载图片:

3、解决办法
先贴出html
复制代码

<form id="changepwd-form" class="form-horizontal" role="form" data-toggle="validator"

                              autocomplete="off">

                            <div class="form-group">
                                <label for="username" class="control-label col-xs-12 col-sm-2">名称:</label>
                                <div class="col-xs-12 col-sm-4">
                                    <input type="text"
                                           data-rule="required;"
                                           id="username" name="username"
                                           class="layui-input form-control input-none" placeholder="姓名"
                                           value="{$username}">
                                    <div id="username_msg" style="color: red"></div>
                                </div></div>

                            <div class="form-group">
                                <label for="mobile" class="control-label col-xs-12 col-sm-2">联系手机号:</label>
                                <div class="col-xs-12 col-sm-4">
                                    <input type="text" data-rule="required;mobile" id="mobile" name="mobile"
                                           class="layui-input form-control input-none" placeholder="手机号"
                                           value="{$mobile}">
                                </div>
                            </div>

                  

按钮的重要性

按钮占据表格50%以上的功能,表头按钮和行按钮的渲染比较复杂。
从源码上来分析这些表格渲染和点击事件的行为。后续要修改或添加按钮就很容易了。
表头按钮

为什么会显示这些按钮?
如何去掉其中部分按钮?
如何添加新的表头按钮?
输入图片说明
为什么会显示这些表头按钮

是在html里的toolbar里定义了的,这个有定义就会出现,还是比较直接的,说明并不是根据什么配置数据来动态渲染的。
如果要添加新按钮或去掉一些按钮,直接修改这里的html就可以。
输入图片说明
按钮的事件

以上的html只定义了按钮的名称、位置,但是看不不出他们的动作事件。
各个按钮的动作事件在require-table.js的Table.api.bindevent来设定的。
各个按钮的className是约定好的,不能去修改,除非同步修改了Table.config里的默认配置。否则就找不到这些按钮来动态设置动作。
输入图片说明
行按钮

为什么会默认显示这些按钮?
如何去掉其中部分按钮?
如何添加新的行按钮?
输入图片说明
默认的行按钮 Table.api.formatter.operate

从如下的js可以看到操作列是使用了Table.api.formatter.operate来格式化。

var Controller = {

    index: function () {
        // 初始化表格
        table.bootstrapTable({
            columns: [
            ...
            {field: 'operate', title: __('Operate'), table: table, 
                        
                events: Table.api.events.operate,                            
                formatter: Table.api.formatter.operate
            }
        })
        ...
    
    }

}

Table.api.formatter.operate这个函数实现了默认的三个按钮的渲染。
从这个函数分析下去可以看到,如果对应的url路径不为空才显示这个行按钮,否则就不显示

operate: function (value, row, index) {

var table = this.table;
// 操作配置
var options = table ? table.bootstrapTable('getOptions') : {};
// 默认按钮组


var buttons = $.extend([], this.buttons || []);
// 所有按钮名称
var names = [];
buttons.forEach(function (item) {
    names.push(item.name);
});
if (options.extend.dragsort_url !== '' && names.indexOf('dragsort') === -1) {
    buttons.push(Table.button.dragsort);
}
if (options.extend.edit_url !== '' && names.indexOf('edit') === -1) {
    Table.button.edit.url = options.extend.edit_url;
    buttons.push(Table.button.edit);
}
if (options.extend.del_url !== '' && names.indexOf('del') === -1) {
    buttons.push(Table.button.del);
}
return Table.api.buttonlink(this, buttons, value, row, index, 'operate');

}

从这里可以看到,如果你想要屏蔽行按钮中的删除按钮,只需要删除edit_url: 'studycase/c1001project/edit',

var Controller = {

index: function () {
    // 初始化表格参数配置
    Table.api.init({
        extend: {
            index_url: 'studycase/c1001project/index' + location.search,
            add_url: 'studycase/c1001project/add',
            edit_url: 'studycase/c1001project/edit',
            del_url: 'studycase/c1001project/del',
            multi_url: 'studycase/c1001project/multi',
            import_url: 'studycase/c1001project/import',
            table: 'c1001_project',
        }
    });

}

如果想要删除拖拽按钮,发现上面的代码里没有拖拽的url。那就要去删除require-table.js里定义的dragsort_url: 'ajax/weigh'
总不能到删除核心文件里的东西,会影响到其他的页面的。可以在上面的 Table.api.init函数前执行,表示动态去删除这个dragsort_url。

delete Table.defaults.extend.dragsort_url;

以上通过删除url的定义的方式来影响默认行按钮的渲染行为,是一种方式。下面说另外一种,自定义行按钮。
自定义行按钮 Table.api.formatter.bottons

我们知道默认的行按钮是通过Table.api.formatter.operate这个格式化函数来实现的,那自定义的话就可以自定义一个格式化函数。
require-table.js其实也为我们准备了比较便捷的自定义的方式:Table.api.formatter.bottons函数

var Controller = {

    index: function () {
        // 初始化表格
        table.bootstrapTable({
            columns: [
            ...
            {field: 'operate', title: __('Operate'), table: table, 
                        
                events: Table.api.events.operate,                            
                buttons: [
                    {
                        name: 'dragsort',
                        icon: 'fa fa-arrows',
                        title: __('Drag to sort'),
                        extend: 'data-toggle="tooltip"',
                        classname: 'btn btn-xs btn-primary btn-dragsort'
                    },
                    
                     {
                        name: 'del',
                        icon: 'fa fa-trash',
                        title: __('Del'),
                        extend: 'data-toggle="tooltip"',
                        classname: 'btn btn-xs btn-danger btn-delone'
                    },
                   
                ],
                formatter: Table.api.formatter.buttons
        })
        ...
    
    }

}

注意使用:
1、要定义 buttons
默认的三个按钮,就拷贝这些,可以删减,调整顺序:

buttons:[

 {
    name: 'edit',
    icon: 'fa fa-pencil',
    title: __('Edit'),
    extend: 'data-toggle="tooltip"',
    classname: 'btn btn-xs btn-success btn-editone'
},
 {
    name: 'del',
    icon: 'fa fa-trash',
    title: __('Del'),
    extend: 'data-toggle="tooltip"',
    classname: 'btn btn-xs btn-danger btn-delone'
},
 {
    name: 'dragsort',
    icon: 'fa fa-arrows',
    title: __('Drag to sort'),
    extend: 'data-toggle="tooltip"',
    classname: 'btn btn-xs btn-primary btn-dragsort'
}

]

2、要使用formatter: Table.api.formatter.buttons 而不能再使用 formatter: Table.api.formatter.operate
行按钮的url渲染 Table.api.buttonlink

修改按钮的url是如何增加上去?
Table.api.buttonlink这个函数负责渲染行按钮的url,formatter: Table.api.formatter.buttons 和 formatter: Table.api.formatter.operate 最终都会调用这个函数来渲染按钮。
这个函数,检查到每个button定义里,如果有url,就渲染一个url;如果没有,这href就给个javascript:
输入图片说明
行按钮的行为设置

1、点击修改按钮会弹窗,在哪里定义了弹窗呢,是在 Table.api.events.operate
输入图片说明
2、点击删除按钮会提示二次确认,在哪里定义了二次确认呢,也是在 Table.api.events.operate
输入图片说明
3、拖拽按钮可以支持拖拽,在哪里增加的事件呢,
输入图片说明

特别注意:自定义的行按钮(非默认三个),他们的className就不要和默认的一样。
自定义行按钮自定义url

我们从上面的自定义按钮的bottons属性可以看到,也没有体现url属性。修改、删除、拖拽这三个默认的url是从约定好的:

// 初始化表格参数配置
Table.api.init({
    extend: {
        index_url: 'studycase/c1001project/index' + location.search,
        add_url: 'studycase/c1001project/add',
        edit_url: 'studycase/c1001project/edit?name={name}',
        del_url: 'studycase/c1001project/del',
        multi_url: 'studycase/c1001project/multi',
        import_url: 'studycase/c1001project/import',
        table: 'c1001_project',
    }
});

拖拽的url是在require-table.js里约定了的,如果业务侧js没重新定义,就取require-table.js里的。

那自定义的行按钮的url就要显式的体现:

buttons:[

 {
    name: 'edit',
    icon: 'fa fa-pencil',
    title: __('Edit'),
    extend: 'data-toggle="tooltip"',
    classname: 'btn btn-xs btn-success btn-editone'
},
{
    name: 'AddSub',
    text: __('Edit'),
    classname: 'btn btn-xs btn-success btn-addsub btn-dialog',
    icon: 'fa fa-plus',
    url: $.fn.bootstrapTable.defaults.extend.addsub_url,
    extend: 'data-area=\'["98%","98%"]\'', //这是控制弹窗的弹出的尺寸
    refresh: true
},
 {
    name: 'del',
    icon: 'fa fa-trash',
    title: __('Del'),
    extend: 'data-toggle="tooltip"',
    classname: 'btn btn-xs btn-danger btn-delone'
},
 {
    name: 'dragsort',
    icon: 'fa fa-arrows',
    title: __('Drag to sort'),
    extend: 'data-toggle="tooltip"',
    classname: 'btn btn-xs btn-primary btn-dragsort'
}

]

里面的AddSub就是自定义的按钮,自定义的url。
如果要为这个行自定义按钮增加点击事件,在页面js里增加:

Table.api.events.operate = $.extend(Table.api.events.operate, {

'click .btn-diy': function (e, value, row, index){
    console.log(row);
}               

})

总结

1、删减行按钮:屏蔽url
2、增加行按钮:显式配置buttons属性,并使用formatter: Table.api.formatter.buttons
3、按钮url:行按钮buttons属性有url属性则显示超链接,无url显示javascrip:
4、按钮classname要注意:和默认的区别开来
5、按钮增加JS事件:扩展Table.api.events.operate

带着问题开始

经常看到fa代码里有$("#table").bootstrapTable()
这个bootstrapTable函数是bootstrap-table 这个jquery插件的能力,是一个jquery对象为什么就具备了这个bootstrap-table的能力呢?是什么时机赋予这个能力?
jquery插件如何注入能力

输入图片说明
jquery插件其能力注入到jquery原型对象里,由于jquery对象自然也具备了jquery原型对象里的能力。
那如何注入呢?
jQuery.extend和jQuery.fn.extend

在jQuery插件开发中,jQuery.extend和jQuery.fn.extend有着不同的用途和效果。
jQuery.extend

jQuery.extend的原理示意代码,主要就是在jq这个函数对象上增加其他的属性或方法。增加的这些,使用时,是通过jquery.XX来使用。就是类似java里某个类的静态方法的调用方式

function jquery(){}
jquery.add = function(){console.log(33);}
jquery.add();

以上是简化后的示意原理,但是jQuery.extend({...})其实做的就是在jquery的函数对象上增加extend({...})里的属性或方法。
jQuery.fn.extend

jQuery.fn.extend的原理示意代码,主要就是在jq这个原型对象上增加其他的属性或方法。增加的这些,使用时,是通过jquery对象(即$()产生的对象)来调用。

function jquery(){}
jquery.fn = jquery.prototype;
jquery.fn.add = function(){console.log(44);}
var b = new jquery();
b.add();

以上是简化后的示意原理,但是jQuery.fn.extend({...})其实做的就是在jquery的原型对象上增加extend({...})里的属性或方法。

jquery里 $('XX') 其实底层就是new jquery() 创建了一个对象。jQuery.fn.extend({...})实现了扩展后,这个创建出来的对象自动就具备了扩展的能力。例如$('XX').bootstrapTable(), 这个bootstrapTable()就是jq插件 往jquery的原型对象上注入进来的能力。

背景

FA框架大量使用了requrieJS。要理解框架如何使用的,代码藏得比较深,不易于理解。
本案例就脱离FA框架,没有一行框架的代码,基于requireJS从零实现一个简单的功能,包括:
1、演示一:封装一个模块,实现两位数的加法。输入数字,就实时显示计算结果。
2、演示二:封装一个模块,实现输入什么文字,就显示:机器人回复:您好,我是飞哥。我收到你的消息:XXX
html

<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <title>requireJs加载原理</title>
</head>
<body>
    <h1>requireJs加载原理</h1>

    <h2>演示一:调用math.js的函数</h2>
    <br/>
    <div>
        <span>第一个数</span>
        <input id="num1" type="text" value=""/>
    </div>
    <div>
        <span>第二个数</span>
        <input id="num2" type="text" value=""/>
    </div>
    <div>
        <span>求和:</span>
        <span id="result"></span>            
    </div>

    <h2>演示二:调用sayhi.js的函数</h2>
    <br/>
    <div>
        <span>输入文字</span>
        <input id="msg" type="text" value=""/>
    </div>

    <div>
        <span>机器人回复:</span>
        <span id="reply"></span>      
    </div>
    
</body>

主js

就是上面data-main="main"执行是main.js

require.config({

baseUrl: "js", // 指定根目录。表示本main.js文件的相当路径下的js文件夹为根目录
paths: { 
    'jquery': '/assets/libs/jquery/dist/jquery.min',  //如果不具体到js后缀,默认或加上js后缀的
    'feige_robot': 'sayhi'//特意演示:给sayhi.js文件起了个别名feige_robot。
}

});

//注意require函数第一个入参是数组,里面的元素表示要加载的js文件。
//先到require.config中的paths配置找路径,找不到再到根目录找。
//例如如下的jquery、feige_robot在paths中配置了路径,就会从这些路径加载对应的js文件
//例如如下的math在paths中没有配置路径,就会从根目录找,就会找到js/math.js
require(['jquery', 'feige_robot','math'], function($, feige_robot,math) {

$("#num1").keyup(function() {
    var num1 = $("#num1").val();
    var num2 = $("#num2").val();
    if(num1=="") num1 = 0;
    if(num2=="") num2 = 0;

    //调用了math的函数
    var result = math.add(num1, num2);
    $("#result").text(result);
})

$("#num2").keyup(function() {
    var num1 = $("#num1").val();
    var num2 = $("#num2").val();
    if(num1=="") num1 = 0;
    if(num2=="") num2 = 0;

    //调用了math的函数
    var result = math.add(num1, num2);
    $("#result").text(result);
})

$("#msg").keyup(function() {
    var msg = $("#msg").val();

    //调用了feige_robot的函数
    var reply = feige_robot.say(msg);
   
    $("#reply").text(reply);
})   

})

从以上的代码,可以看出加载了如下的模块'jquery', 'feige_robot'(其实就是下面的模块二:sayhi.js,feige_robot是起来个别名),'math'(其实就是下面的模块一:math.js)。
加载了这些模块后,在函数里就可以直接调用模块暴露出来的方法,例如math.add(num1, num2)或者feige_robot.say(msg)。
模块一:math.js

define(function (){

var add = function (x,y){    
    //x 字符转数字
    x = parseFloat(x);
    //y 字符转数字
    y = parseFloat(y);
    return x+y;    
};

return {    
    add: add
};    

});

模块二:sayhi.js

define(function (){

var say = function (msg){    
    return "您好,我是飞哥。我收到你的消息:"+msg;    
};

return {    
    say: say
};    

});

运行效果

image.png

背景

requrejs 是fastadmin 前端框架的基础和核心。如果不了解requirejs,在编写代码时将会磕磕碰碰。
我们带着如下的问题来学习:

页面上没找到script标签引用js文件的位置?如何添加上的JS文件?
js里大量的require函数是什么意思?

define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefined, Backend, Table, Form) {

···
})

)

requrejs的作用

按需加载JS文件,避免在页面初始化时就逐一将全部的JS都下载下来,提升页面的性能;
管理JS之间的依赖性,便于代码的编写和维护。

AMD规范

AMD是“Asynchronous Module Definition”的缩写,即“异步模块定义”。主要用于浏览器端开发。在浏览器端,由于网络延迟等因素,模块的加载时间可能会比较长,AMD的异步加载特性可以有效避免页面渲染被阻塞,提高页面的加载速度和用户体验。例如,在大型的前端项目中,使用AMD规范可以实现按需加载模块,减少初始加载时间,提升应用的性能。其核心是define和require两个函数。
define 声明依赖

使用define函数来定义模块,显式声明了依赖的模块,并将这些依赖模块以参数的形式传递给本模块函数。
格式如下:
如下是没有依赖其他模块的时候:

define(function (){
...
});

如果这个模块还依赖其他模块,那么define()函数的第一个参数,必须是一个数组,指明该模块的依赖性:

define(['ff'],function (ff){
...
});

以上表示本模块依赖了ff模块
模块暴露其属性或方法

通过return 的方式暴露其属性或方法。一般调用者可以调用此模块的属性或方法。

define(function (){

var add = function (x,y){    
    ...
};

return {    
    add: add
};    

});

上面代码就是将add方法暴露出去。
require的方式来加载模块

上面define函数是定义了模块,如果要调用这些模块,就需要使用require函数,这函数是requireJS提供的,等下详细讲解。

require(['jquery', 'feige_robot','math'], function($, feige_robot,math) {
...
})

注意require函数第一个入参是数组,里面的元素表示要加载的模块。这些模块对应的js路径定义在哪里呢,等下看。
表示依次调用jquery、feige_robot、math,三个模块初始化后赋值给$、 feige_robot、math三个变量,后续函数就可以可以通过这些变量来调用各种模块暴露出来的方式和属性。
requireJs

RequireJS是一个JavaScript模块加载器,它实现了AMD规范,是AMD规范的具体实现之一。DefineJS、curl.js、Dojo等其他的前端框架也实现了AMD规范。
html引入requrejs文件并加载主js

使用script 加载requrejs,加载后要加载一个主js文件。这个文件会第一个被require.js加载。
如下data-main="main",注意不需要带上.js后缀,表示主js文件为本html相同目录下的main.js。

配置信息 require.config

设置requirejs加载的配置信息,关键几个:

baseUrl:指定根目录。因为要加载很多其他的模块js,如果使用的是相当路径,加载时就会拼上这个根目录。
paths:维护了模块js的路径和别名的关系,在其他模块使用时就使用别名就可以,到时就通过别名来加载相应的js。

require.config({

baseUrl: "js", // 指定根目录。表示本main.js文件的相当路径下的js文件夹为根目录
paths: { 
    'jquery': '/assets/libs/jquery/dist/jquery.min',  //如果不具体到js后缀,默认或加上js后缀的
    'feige_robot': 'sayhi'//特意演示:给sayhi.js文件起了个别名feige_robot。
}    

});

其中注意,
paths里的条目,路径不需要体现js后缀,默认会加上js后缀的。如果加了,反而重复了。
require函数加载模块

require函数第一个入参是数组,里面的元素表示要加载的模块。
如果模块的js文件的路径require.config的paths有映射,就去这里的路径来加载js文件。
如果paths没映射,就require.config的baseUrl拼上模块名称的js文件。

require(['jquery', 'feige_robot','math'],function($, feige_robot,math) {

    ...
}

)

例如,如果math在paths中没有配置路径,就会从根目录找,就会找到js/math.js