119. 系统AJAX(二):前端原理
前端概述:
drupal AJAX API前端系统主要是指核心库:core/drupal.ajax,下文简称AJAX库,前端AJAX行为均由该库完成,她主要依赖以下几个重要的库:
jquery库core/jquery:整个前端系统是建立在jquery基础之上的
jquery表单库core/jquery.form:为表单提交提供支持功能,请见本系列番外篇对此的介绍
core/drupal:为AJAX行为提供初始化等
core/drupalSettings:用于接收后端传递的AJAX配置
在流程上,渲染数组“#ajax”属性中的AJAX配置首先经过以下方法处理:
\Drupal\Core\Render\Element\RenderElement::preRenderAjaxForm
然后传递给后端JS配置系统,也就是如下变量:
$element['#attached']['drupalSettings']['ajax'][$element['#id']] = $AJAX_Settings;
其中$element['#id']是要触发AJAX请求的元素的ID属性值,任何一个表单元素都有id,手动指定优先,如果没有指定,则默认会在表单构建器中做如下处理:
$unprocessed_id = 'edit-' . implode('-', $element['#parents']);
$element['#id'] = Html::getUniqueId($unprocessed_id);
后端$element['#attached']['drupalSettings']变量会传递给前端的drupalSettings变量,然后AJAX库从该变量中获取AJAX配置进行AJAX行为设置,除从这里获取的配置外,AJAX库也会为便捷AJAX设置的元素生成AJAX配置(见后),最后统一由AJAX库的以下入口方法处理:
Drupal.ajax(elementSettings);
这里参数elementSettings就是指AJAX配置,在内部会实例化以下方法:
new Drupal.Ajax(base, element, settings);
这得到AJAX对象,在实例化过程中设置了AJAX行为,这里“AJAX配置”、“AJAX对象”是很重要的概念,后续多次用到。
注意:
AJAX请求均是POST请求,服务器只返回命令,即便返回的是html片段也是通过命令实现替换的
可信任AJAX地址:
为了安全,前端并不允许向任意地址发起AJAX请求,在后端可通过以下变量设置一个可信任的地址:
$element['#attached']['drupalSettings']['ajaxTrustedUrl'][$settings['url']] = TRUE;
在前端可信任的AJAX地址保存在全局对象drupalSettings.ajaxTrustedUrl中,以url做属性名,属性值为true,如果AJAX请求的url不在该对象中,且不是站内地址,则ajax API不允许发起ajax操作,如果是站内地址但不在该对象中,则允许发起AJAX请求,但会通过“X-Drupal-Ajax-Token”响应头来判断安全性,其值全等于1表示合法,否则响应不被接受
注意:该可信任AJAX地址不影响自定义js,也就是非AJAX api之外的内容
前端AJAX库:
在前端,AJAX功能封装在以下库中:
核心库:core/drupal.ajax
文件:core/misc/ajax.es6.js
这是系统ajax API的唯一前端js程序,入口方法是:
Drupal.ajax(elementSettings)
该方法接收一个配置对象(AJAX配置对象),依据其配置给元素添加AJAX功能,主要为三类元素提供AJAX功能,不同元素传递不同的配置对象,当我们自定义的前端js需要为元素添加ajax时也可以调用该方法,但前提是要明白配置对象的结构,在以下方法中为这三类元素准备了AJAX配置对象并调用了以上方法:
Drupal.behaviors.AJAX.attach(context, settings)
该方法在文档就绪后自动执行(见本系列《前端JavaScript(一)全局设置与前端API》主题),三类元素及其传递的配置对象结构如下:
声明在全局配置对象drupalSettings.ajax中的元素:
这一类元素通常是后端设置了“#ajax”属性的渲染数组元素,配置对象的值便来自“#ajax”属性,见:
\Drupal\Core\Render\Element\RenderElement::preRenderAjaxForm
确保了以下配置键存在(在前端用elementSettings变量代表AJAX配置):
elementSettings.selector:
该元素的jquery选择器,默认为id选择器,也可在后端“#ajax”属性中直接指定“selector”值
elementSettings.element:
该元素的DOM对象,非jquery对象,将在其上绑定事件
elementSettings.base:
该元素的id值(字符串值),不带“#”前缀
所有具备类属性“use-ajax”的元素:
通常是链接元素,为其AJAX配置elementSettings自动生成以下属性:
progress:
进度指示,值默认为:{ type: 'throbber' }
dialogType:
对话框类型,值:$linkElement.data('dialog-type'),在后端将决定采用的主内容渲染器
dialog:
对话框选项,值为:$linkElement.data('dialog-options'),
dialogRenderer:
对话框子渲染器,值为:$linkElement.data('dialog-renderer'),和dialogType选项共同决定后端主内容渲染器
base:
元素id属性值,字符串值,来自$linkElement.attr('id')
element:
链接元素DOM对象,非jquery对象,将在其上绑定事件
url:
链接url,仅在链接有href属性时才存在
event:
要阻止的默认事件,默认为:“click”,仅在链接有href属性时才存在
所有具备类属性“use-ajax-submit”的元素:
通常是表单提交元素,如按钮,自动为其产生elementSettings,属性如下:
url:
来自表单元素的action属性
setClick:
布尔值,默认true,告诉后端表单是哪个元素提交的,这样才能执行相应验证、提交处理器
event:
要阻止的默认事件,默认为:“click”
progress:
进度指示,值默认为:{ type: 'throbber' }
base:
元素id属性值,字符串值
按钮元素DOM对象,非jquery对象,将在其上绑定事件
Ajax对象
添加AJAX的行为全部在实例化Ajax对象的过程中:
const ajax = new Drupal.Ajax(base, element, settings);
实例化的Ajax对象保存在以下全局数组中:
Drupal.ajax.instances
Ajax对象的instanceIndex属性为在该数组中的索引
Ajax对象的属性:
合并了所有的AJAX配置属性,此外新增有以下属性
commands:
命令对象,可通过该对象运行命令对应的方法,是后端返回命令被运行的关键
element:
元素的DOM对象,非jquery对象,在其上设置AJAX事件
elementSettings:
保存AJAX配置对象,别名element_settings
$form:
如果元素是一个表单元素,将存在该属性,保存包含该元素的表单的jquery对象
url:
发送AJAX的请求地址,不过已经被处理过,如果路径中存在“/nojs/”、“/nojs$”、“/nojs?”、“/nojs#”,那么会将其“nojs”替换成“ajax”,以此向服务器表明是通过ajax发送的请求,并将新的url添加到可信ajax路径中,(在后端需要关注是否为ajax请求的路由,在设置中可以以此设置占位变量,通常设置为“js”,通过判断其值为“nojs”还是“ajax”进行识别)
options:
用于设置jquery的ajax方法的选项对象,指定了AJAX请求发送行为
Ajax对象的方法:
她们定义在原型对象上(Drupal.Ajax.prototype),如下:
Drupal.Ajax.prototype.execute()
开发者可以在Ajax对象上调用该方法以手动执行ajax请求,而不需要任何事件触发,注意该方法是直接采用jquery的ajax方法,而没有使用jquery表单库,因此并不调用提交前方法,默认的提交前方法是一个空方法,这没有什么影响,但有覆写提交前方法时,需要注意覆写并不会产生什么影响
Drupal.Ajax.prototype.keypressResponse(element, event)
当配置对象上设置了keypress属性,且其值为true时(默认值为true),表示可以通过按压空格或回车键触发配置对象中设置的AJAX触发事件(后称AJAX事件),程序会在元素上侦听keypress事件,当发生该事件时便会运行该方法,这里键盘代码(event.which):回车键是13,空格键是32,当按压回车时会触发AJAX事件,如果按压的是空格则需要排除四种元素:text、textarea、tel、number,该方法在触发AJAX事件的同时也会阻止默认动作和事件传播
Drupal.Ajax.prototype.eventResponse(element, event)
元素AJAX事件的处理函数,如果元素是一个表单元素,如按钮、输入框等,那么将立即提交表单,这里用到了jQuery表单库(jquery.form.js),详见本系列番外篇对该库的介绍,传递给ajaxSubmit方法的是 Ajax对象的options属性,表单库将依次执行Ajax对象原型(Drupal.Ajax.prototype)上的以下方法:
beforeSerialize(element, options);
beforeSubmit(formValues, element, options);
beforeSend(xmlhttprequest, options);
success(response, status);
如果元素不是表单元素,如链接,那么将采用jquery本身的方法($.ajax(ajax.options);)执行ajax,此时将依次运行Ajax对象原型(Drupal.Ajax.prototype)上的以下方法:
beforeSerialize(element, options);
beforeSend(xmlhttprequest, options);
success(response, status);
注意:以上均没有执行error回调,这是因为会在complete回调中执行错误处理,最终会执行Ajax对象原型(Drupal.Ajax.prototype)上的以下方法:
error(xmlhttprequest, ajax.url);
这个方法抛出的异常会采用window.alert方法弹框给用户(云客备注:此处没有用到翻译机制,须改进)
Drupal.Ajax.prototype.beforeSerialize(element, options)
表单序列化之前执行的回调函数,在取回值之前提供一个机会去操纵表单,参数含义如下:
element:jquery包装的表单对象
options:Ajax对象中的选项属性
注:如果该方法返回false将阻止表单提交,该特性可用方法覆写
主要工作是为AJAX提供额外请求参数,参数如下:
_drupal_ajax:值为1,告诉后端这是一个ajax请求
ajax_page_state[theme]:页面所用的主题
ajax_page_state[theme_token]:主题token
ajax_page_state[libraries]:页面已加载的资源库,避免ajax重复加载
如果元素是表单元素且在DOM中,那么还会执行全局方法:
Drupal.detachBehaviors(form, settings, ‘serialize’);
这样其他js组件就可以定义drupal行为对象(Drupal.behaviors)来修改表单的内容,参数含义如下:
form:表单DOM对象
settings:如果后端“#ajax”数组中设置了该项便采用,否则采用drupal全局设置对象drupalSettings
Drupal.Ajax.prototype.beforeSubmit(formValues, element, options)
仅用于表单元素,提供一个机会在提交前修改整个表单的值或选项对象,如果返回false将阻止表单提交,默认该方法是空的,如有需要可以覆写,三个参数为:数组格式的表单数据、jquery包装的表单对象和选项对象
Drupal.Ajax.prototype.beforeSend(xmlhttprequest, options)
在该方法中,变量“options.extraData”用到了jquery表单库的一个内部功能,在老旧浏览器中通过AJAX上传文件时,jquery表单库会采用iframe模拟提交,变量“options.extraData”中的值仅在采用iframe模拟提交时才会被提交给服务器,否则并不被提交,现代浏览器已经不需要iframe模拟了,该方法在变量“options.extraData”中设置了“ajax_iframe_upload”参数,这样做的结果是:当服务器发现POST数据中有“ajax_iframe_upload”参数,那么就知道客户端是通过iframe模拟提交的,此时服务器可将JSON响应包装在TEXTAREA元素中返回;同理由于iframe模拟提交是浏览器的原生提交,因此不会提交被禁用的元素,所以对当前元素进行了特殊处理以得到提交;此外该方法做以下工作:
将元素设置为禁用,防止重复操作(在ajax完成或出错时在其他方法中会重新启用元素);设置进度指示器
注意:在非iframe模拟提交的情况下,该方法已经无法给请求添加额外的参数了
Drupal.Ajax.prototype.setProgressIndicatorBar()
Drupal.Ajax.prototype.setProgressIndicatorThrobber()
Drupal.Ajax.prototype.setProgressIndicatorFullscreen()
默认提供的三个进度指示器设置方法,用户可自定义进度指示器,设置方法名为“setProgressIndicator”前缀加类型名,其中类型名首字母大写,其他全小写,里面的this指向Ajax对象
Drupal.Ajax.prototype.success(response, status)
在ajax成功后执行,去除进度条,去掉触发元素禁用状态,在drupal的ajax API中响应均是json对象,包含着各种命令,命令即是Drupal.AjaxCommands.prototype中的各种方法,调用是传递如下参数:
command(Ajax, response[i], status);
分别为:Ajax对象、命令对象、请求状态信息
请求状态信息为字符串值,可能值有"success", "notmodified", "nocontent", "error", "timeout", "abort", or "parsererror"
发送给后端的额外参数:
在进行AJAX请求时有些额外参数会传递给后端,她们有很重要的含义,影响着后端执行,如下:
ajax_form:
GET查询参数,布尔值1,用于指示该AJAX是一个表单提交AJAX,如果在“#ajax”属性中AJAX配置仅有回调没有url时,后端会自动附加该参数到URL中,该参数名定义在以下常量中:
\Drupal\Core\Form\FormBuilderInterface::AJAX_FORM_REQUEST
前端并不处理该参数。该参数对于后端来说非常重要,直接影响系统执行流程(有该参数时表单流程采用异常控制流)
_wrapper_format:
GET查询参数,用于在后端指定主内容渲染器(关于主内容渲染器见后端原理篇),AJAX前端无条件附加,必定存在,默认值为:“drupal_ajax”,该参数名后端定义在以下常量中:
\Drupal\Core\EventSubscriber\MainContentViewSubscriber::WRAPPER_FORMAT
前端定义在Drupal.ajax.WRAPPER_FORMAT变量中,并没有进行前后端传递。
_drupal_ajax:
POST请求参数,用于告诉后端请求是一个AJAX请求(不一定是表单AJAX请求,而是所有类型的AJAX请求),AJAX前端无条件附加,必定存在,值为1,该参数名定义在以下常量中:
\Drupal\Core\EventSubscriber\AjaxResponseSubscriber::AJAX_REQUEST_PARAMETER
前端定义在Drupal.Ajax.AJAX_REQUEST_PARAMETER变量中,并没有进行前后端传递。
ajax_iframe_upload:
POST请求参数,用于告诉后端AJAX是借助iframe模拟发起的;在web早期浏览器并不支持文件等AJAX上传,人们变通的借助iframe元素模拟AJAX(可参考本系列番外篇jquery表单库了解这种技术),随着现代浏览器的支持现在已经不会这样做了,但有部分老旧浏览器还在使用,此时仍将采用iframe模拟,这种情况下便会传递该参数给服务器,值为“1”,当页面在现代浏览器中运行时,通常不会传递该参数,除非故意指定采用iframe模拟;在后端如果服务器检测到该参数,那么就可以进行回退处理,实际上当drupal 在格式协商阶段发现该参数时会将请求格式设置为“iframeupload”。该参数在前后端均未采用常量定义。
补充:
1、在一些路由中,会用“js”作为路径占位变量,其值为“nojs”时表示客户端非js提交,为“ajax”表示通过AJAX提交,AJAX前端将替换路径中的“nojs”为“ajax”,这样后端便可借此侦测客户端环境
2、服务器的ajax响应均以命令方式返回,前端命令函数定义在Drupal.AjaxCommands.prototype上,关于命令因为涉及前后端配合,本系列将在专门的主题中详解,
3、throbber进度条默认样式(由系统模块原始定义):
原始定义:core/modules/system/css/components/ajax-progress.module.css
覆写定义:core/themes/stable/css/system/components/ajax-progress.module.css
4、在前端AJAX配置中,并无ajax回调概念(callback选项),仅有url,回调并不通过前端传递。