这是很多读者期待的一个激动人心的时刻,从本篇起,本系列将分多集连续全面介绍Drupal系统的AJAX实现,在阅读前需要掌握一些必须的前置知识,请先阅读本系列以下主题:
《全局设置与前端API》
《jQuery表单库jquery.form.js详解》
此外需要读者熟悉jQuery,如果JS编程不熟悉,云客为你准备了以下教程:
《PHP开发者的JavaScript快速教程(phper简明js教程)》
Drupal AJAX概述:
AJAX是在不刷新整个页面的前提下,基于服务器端返回的数据动态更新页面中部分内容或JS变量的过程;在drupal世界中从开发角度看AJAX,可分为两大类:
自定义AJAX:
和通常项目一样的由开发者自定义前端js代码,再从后端拉取数据实现的AJAX,这要求开发者自定义资源库,为了方便通常会依赖核心jquery库,用jquery提供的诸多方法进行AJAX通讯;这一类AJAX灵活自由,可以实现开发者想要的任意功能,是一种任意项目通用的底层操作,而不局限于drupal,缺点是需要开发者处理所有细节,不够简单和便捷,即便一个小功能也需要实现自定义库。
Drupal AJAX API:
为弥补第一类的不足,系统提供了AJAX API,非常便捷、简单,无需写JS代码,只需要依据API的要求为要应用AJAX行为的元素指定一个AJAX配置即可,无需涉及底层细节,系统会自动完成AJAX功能,这是一种更高层级的封装,支持任意元素。
通常最常见的AJAX操作是从服务器拉取html片段并替换页面元素,但也有很多其他操作,比如依据服务器返回的数据更新某个js变量、弹出一个警告框、设置一个css属性、移除一个元素等等,Drupal将各种操作进行了统一,透过现象直达本质,在实现上,将不同种类的操作抽象成一个个不同的命令,每个命令由一个对应的js函数来完成该种操作,比如替换html就是一个“insert”命令,参数是替换的html片段以及如何替换、替换谁等;这样一来,后端仅需以json方式返回一个纯数据对象即可,对象中包含了命令的名字,以及完成命令需要的参数数据,在前端该对象称为AJAX命令对象,后端仅返回AJAX命令对象,且在一次AJAX请求中可以返回多个命令,从而一次性进行多种操作,关于命令会在后续主题详细解释。
API的缺点是如果系统提供的AJAX命令不能满足需求,那么需要新增命令,但很棒的是系统默认提供的命令足够丰富且扩展简单。
在设计上AJAX API主要用于以下元素:
渲染数组中具备“#ajax”属性的表单元素
具备“use-ajax”类属性的链接元素
具备“use-ajax-submit” 类属性的表单按钮
但这并不是说仅能用于这些元素,准确的说法是最常用于这些元素,AJAX API是可以用于任意元素的,见下文示例三的说明。
准备:
为演示本篇的示例,需要一个模块来运行相关代码,这里以“yunke_help”模块为列,该模块是本系列的配套模块,专门用于研究学习drupal,有非常多的辅助功能,请先到云客的博客下载安装,须注意这里只是为了起演示作用,并不依赖特定模块,以下以“yunke_help”模块来说明示例步骤。
示例一:在表单非提交元素上运用AJAX
目的:一个表单元素输入改变,从服务器获取数据联动改变其他元素
第一步:
建立文件:yunke_help/src/Form/YunkeForm.php
内容如下:
'html_tag',
'#tag' => 'div',
'#value' => 'ajax获取的内容将显示在这里:',
'#attributes' => ['id' => 'content_one'],
];
$form['input'] = [
'#type' => 'textfield',
'#title' => '任意输入字符:',
'#description' => '输入的内容将通过ajax发送到服务器',
'#size' => '60',
'#ajax' => [
'callback' => '::yunkeTest',
'event' => 'blur',
'wrapper' => 'content_one',
'method' => 'append',
'effect' => 'slide',
'speed' => 1000,
'prevent' => 'click',
'progress' => [
'type' => 'throbber',
'message' => '正在进行ajax...',
],
],
];
$form['#attributes']['target'] = "_blank";
$form['actions']['#type'] = 'actions';
$form['actions']['submit'] = array(
'#type' => 'submit',
'#value' => $this->t('Submit'),
'#button_type' => 'primary',
);
return $form;
}
public function yunkeTest(array &$form, FormStateInterface $form_state)
{
//\Drupal::messenger()->addStatus('AJAX消息测试'); //默认也会发生消息管理器中的内容
$markup = '你的输入是:' . $form_state->getValue('input') . '
';
return ['#markup' => $markup];
}
public function validateForm(array & $form, FormStateInterface $form_state)
{
}
public function submitForm(array & $form, FormStateInterface $form_state)
{
$form_state->cleanValues();
print_r($form_state->getValues());
die;
}
}
第二步:
然后在控制器:\Drupal\yunke_help\Controller\Test::test中执行以下代码:
return \Drupal::formBuilder()->getForm("\Drupal\yunke_help\Form\YunkeForm");
访问连接:http://www.你的域名.com/yunke-help/test
或点击“yunke_help”模块主页的测试按钮
说明:
在该例中关键在于表单元素的“#ajax”项设置,她为前端提供了一个AJAX配置,前端系统会据此为具备该属性的元素附加AJAX行为,AJAX配置可用的设置项及解释见下文的AJAX配置说明。
示例二:在dialog弹框显示链接内容
目的:点击页面中的链接,然后将链接内容通过AJAX获取后显示到页面dialog弹框中:
第一步:
先在控制器:\Drupal\yunke_help\Controller\Test::test_1中放入以下内容供AJAX获取:
$msg = '我是ajax获取的内容' . time() . '';
return array(
'#markup' => $msg,
);
第二步:
然后在控制器:\Drupal\yunke_help\Controller\Test::test中执行以下示例代码:
$elements['#title'] = '示例二:用dialog显示链接目标';
$url = \Drupal\Core\Url::fromUri('internal:/yunke-help/test-1');
$elements['link'] = [
'#type' => 'link',
'#title' => '点击本链接将以dialog方式打开',
'#url' => $url,
'#attributes' => ['class' => ['use-ajax'],
'data-dialog-type' => 'dialog',
],
];
return $elements;
第三步:
这样就可以点击“yunke_help”模块主页的测试按钮查看示例效果了
说明:
该例没有像示例一那样用到“#ajax”属性,没有AJAX配置传递到前端,但是前端系统发现链接具备类属性“use-ajax”就会结合 “data-dialog-type” 属性、href属性自动产生AJAX配置并附加AJAX行为
示例三:点击链接后在任意位置显示
目的:示例二以dialog显示AJAX获取的内容,本例将获取的内容显示到特定的页面元素中
第一步:
示例二中控制器:\Drupal\yunke_help\Controller\Test::test_1中所放AJAX获取的内容不变
然后在控制器:\Drupal\yunke_help\Controller\Test::test中执行以下示例代码:
$elements['#title'] = '示例三:在特定元素中显示链接目标';
$elements['content_one'] = array(
'#type' => 'html_tag',
'#tag' => 'div',
'#value' => '容器元素',
'#attributes' => ['id' => 'content_one'],
);
$url = \Drupal\Core\Url::fromUri('internal:/yunke-help/test-1');
$elements['link'] = [
'#type' => 'link',
'#title' => '点击本链接将在容器元素中呈现目标内容',
'#url' => $url,
'#attributes' => ['id' => 'yunkeID'],
];
$elements['link']['#attached']['drupalSettings']['ajax']['yunkeID'] = [ //以链接ID做键名
'event' => 'click',
'wrapper' => 'content_one', //赋值容器元素ID,以使得显示位置指向容器元素
'method' => 'append',
'effect' => 'slide',
'speed' => 1000,
'prevent' => 'click',
'progress' => [
'type' => 'throbber',
'message' => '正在进行ajax...',
],
];
$elements['link']['#attached']['drupalSettings']['ajaxTrustedUrl'][$url->toString()] = TRUE;
$elements['link']['#attached']['library'][] = 'core/jquery.form';
$elements['link']['#attached']['library'][] = 'core/drupal.ajax';
return $elements;
第二步:
这样就可以点击“yunke_help”模块主页的测试按钮查看示例效果了
说明:
在该例中关键在于通过系统前端设置机制传递了一个AJAX配置,这以触发AJAX事件元素的id做键名(这里是yunkeID),键值是AJAX配置,该配置和示例一中的“#ajax”是一样的,实际上在内部,示例一中的“#ajax”也会被转化成这种形式,这里使用了更加底层的操作,由于没有采用自动处理,因此必须手动附加相关的库和可信AJAX请求链接设置
以示例三这种方法可以为任意前端元素指定AJAX行为,如图片、按钮、文字等等,只需要元素具备ID属性,并以ID属性值做AJAX配置的键名即可,但需要注意由于本例中触发AJAX的元素是一个链接元素(a标签),AJAX请求url可以被前端系统从链接上自动获取,因此可以省略,如果是其他元素,那么必须在配置中指定url项,其值必须是一个字符串值,不能是url对象,如果是url对象必须用“$url->toString()”进行转化
示例四:在任意元素上设置AJAX行为
目的:在任意元素上设置AJAX行为,并将结果显示在对话框中,这里以span元素为列
第一步:
前例控制器:\Drupal\yunke_help\Controller\Test::test_1中所放AJAX获取的内容不变
然后在控制器:\Drupal\yunke_help\Controller\Test::test中执行以下示例代码:
$elements['#title'] = '示例四:任意元素触发AJAX,并在dialog中显示内容';
$url = \Drupal\Core\Url::fromUri('internal:/yunke-help/test-1');
$elements['span'] = [
'#type' => 'html_tag',
'#tag' => 'span',
'#value' => '点击这里将在dialog中呈现ajax内容',
'#attributes' => ['id' => 'yunkeID'],
];
$elements['link']['#attached']['drupalSettings']['ajax']['yunkeID'] = [ //以触发元素ID做键名
'event' => 'click',
'dialogType' => 'dialog', //该项使得显示位置在对话框中
'url' => $url->toString(),
'method' => 'append',
'effect' => 'slide',
'speed' => 1000,
'prevent' => 'click',
'progress' => [
'type' => 'throbber',
'message' => '正在进行ajax...',
],
];
$elements['link']['#attached']['drupalSettings']['ajaxTrustedUrl'][$url->toString()] = TRUE;
$elements['link']['#attached']['library'][] = 'core/jquery.form';
$elements['link']['#attached']['library'][] = 'core/drupal.ajax';
return $elements;
第二步:
这样就可以点击“yunke_help”模块主页的测试按钮查看示例效果了
说明:
该例的目的是让读者明白drupal的AJAX API可以用于任意元素,这里仅以span元素的点击事件为例
示例五:通过AJAX提交表单
目的:在表单提交元素上设置AJAX行为,这将执行提交处理器,并将结果显示在特定位置
和示例一完全相同,仅将表单类替换如下:
'textfield',
'#title' => '标题,仅能输入数字',
'#pattern' => '[0-9]+',
);
$form['input'] = [
'#type' => 'textfield',
'#title' => '输入任意字符:',
'#id' => 'yunkeID',
'#required' => TRUE,
];
$form['actions']['#type'] = 'actions';
$form['actions']['content_one'] = [ //用于显示AJAX提交反馈
'#type' => 'html_tag',
'#tag' => 'div',
'#attributes' => ['id' => 'content_one'],
];
$form['actions']['submit'] = array(
'#type' => 'submit',
'#value' => 'AJAX提交',
'#button_type' => 'primary',
'#ajax' => [
'callback' => '::yunkeTest',
'event' => 'click',
'disable-refocus' => true,
'wrapper' => 'content_one',
'method' => 'html',
'effect' => 'fade',
'speed' => 1000,
'prevent' => 'click',//阻止非ajax提交,其实不需要这行,默认已阻止
'progress' => [
'type' => 'throbber',
'message' => '表单正在提交中...',
],
],
);
return $form;
}
public function yunkeTest(array &$form, FormStateInterface $form_state)
{
/*
//返回渲染数组,该方式将返回消息服务中的数据,以此显示错误
if($errors=$form_state->getErrors()){
return ['#markup' => '表单已成功提交'];
}else{
return ['#markup' => '提交失败,请检查错误'];
}
*/
//如果不想反回消息数据,则使用以下代码返回json响应:
if ($errors = $form_state->getErrors()) {
$message = ['提交失败,请检查错误:'];
foreach ($errors as $error) {
$message[] = $error;
}
//错误会沉积在消息系统中,体验不好故删除,但不要删除非表单错误的消息
$errorMessages = \Drupal::messenger()->deleteByType('error');
foreach ($errors as $key => $error) {
$errors[$key] = (string)$error;//可能是翻译对象
}
foreach ($errorMessages as $key => $error) {
if (!in_array((string)$error, $errors)) {
\Drupal::messenger()->addMessage($error, 'error');
}
}
} else {
$message = ['表单已成功提交'];
}
$response = new \Drupal\Core\Ajax\AjaxResponse();
$html = ['#markup' => implode('
', $message)];
$command = new \Drupal\Core\Ajax\InsertCommand(NULL, $html);
$response->addCommand($command);
return $response;
}
public function validateForm(array & $form, FormStateInterface $form_state)
{
if (empty($form_state->getValue('title'))) {
$form_state->setErrorByName('title', '标题不能为空');
}
file_put_contents('yunkeAJAX.txt', __CLASS__ . "验证器\n", FILE_APPEND);
//在根目录写入信息以便测试
}
public function submitForm(array & $form, FormStateInterface $form_state)
{
file_put_contents('yunkeAJAX.txt', __CLASS__ . "提交器\n", FILE_APPEND);
$form_state->cleanValues();
$d = print_r($form_state->getValues(), true);
file_put_contents('yunkeAJAX.txt', "提交内容\n:{$d}\n", FILE_APPEND);
}
}
说明:
该列演示了在提交元素上采用“#ajax”属性设置表单ajax提交,这和示例一不同之处在于该列会执行提交处理器
示例六:
目的:在提交元素上采用类属性“use-ajax-submit”进行提交与显示
很遗憾,目前(V8.7,2019.7)系统在该功能上存在BUG,如下:
1、在该种情况下,系统本应自动添加“core/jquery.form”和“core/drupal.ajax”库,但其并没有,该bug已经被社区成员提交,现处于修复阶段
2、在该种情况下,提交处理器需在表单状态对象中设置一个ajax响应,这在成功提交的情况下将工作的很好,但是如果验证失败(此时不会执行提交处理器),控制器将返回表单数组,接着由AJAX主内容渲染器将其渲染成AJAX响应,由于该响应中,insert命令缺乏指向错误显示元素的选择器,因此页面并不显示错误,这虽然可以在验证器中将表单数组的元素类型设置成“ajax”以在前端通过警告框进行错误提示,但显然这并不优雅
由于以上bug尚未修复,建议目前以示例五代替
AJAX配置:
AJAX配置是前端系统所需的用来为页面元素设置一个AJAX行为的信息(在下集原理篇中会有更深入的体会),在后端以数组形式存在,在前端以数据对象形式存在,通常通过JS配置设置机制传递到前端全局变量drupalSettings中,但有些情况为了更加便捷,系统支持部分元素只需要设置少许属性即可实现AJAX,此时不需要传递AJAX配置,典型的是设置了类属性“use-ajax”的链接和设置了类属性“use-ajax-submit”的表单按钮,前端系统会为它们自动产生AJAX配置并设置AJAX行为。
从系统流程角度看,AJAX配置在系统中有三种存在形式(或存在位置):
元素“#ajax”属性值:
也就是元素渲染数组的“#ajax”属性值,需要注意的是并不是所有元素都能使用“#ajax”属性,仅在渲染过程中能调用到以下方法的元素才可以:
\Drupal\Core\Render\Element\RenderElement::preRenderAjaxForm
这通常是表单元素,该方法会将“#ajax”属性值处理成JS设置中的AJAX配置后传递到前端(准确说是传递到附属物中)
JS设置值:
在后端这也称为位于渲染附属物中,也就是渲染数组中以下项的值:
$element ['#attached']['drupalSettings']['ajax']['元素ID']
渲染数组的$element ['#attached']['drupalSettings']用于给前端传递设置信息,位于这里的AJAX配置称为标准AJAX配置,或最终AJAX配置,不会再被处理,将直接传递给前端,在前端位于以下全局变量中:
drupalSettings.ajax.元素ID
注意:以JS设置值这种方式直接传递AJAX配置,看起来更加底层,但并不能完全代替表单元素的“#ajax”属性值方式传递,主要原因在于回调参数并不通过前端传递,在内部系统会先找到触发元素,再通过其:['#ajax']['callback']属性找到回调,这种机制要求回调仅设置在“#ajax”属性中
前端自动产生:
为一些特殊元素设置AJAX行为时,由前端自动产生,如前文提到的设置了类属性“use-ajax”的链接和设置了类属性“use-ajax-submit”的表单按钮
AJAX配置项说明:
AJAX配置可用键名及其解释如下(部分选项根据存在位置不同,可能会有所不同):
callback:
表单AJAX请求时服务器端的回调,仅用于表单元素,且仅用于表单元素的“#ajax”属性中,在该属性中是必须的,用在JS设置值的AJAX配置中无效(因为该项并不通过前端传递),和设置提交处理器或验证器方法一样,如“::yunkeTest”,可以是任意有效php回调,按次序接收三个参数:&$form、$form_state和请求对象,可以用他们获取用户当前的输入,回调应该返回渲染数组,字符串需用['#markup' => $markup]包装成渲染数组,此时除返回值外,消息管理器(\Drupal::messenger())中的全部内容也会被返回;回调也可以返回json响应对象:
\Drupal\Core\Ajax\AjaxResponse
此时不会自动返回消息管理器中的内容,但可以明确通过命令的方式添加。
注意:回调在表单流程之后执行,如果AJAX用在非提交元素中,在回调执行前,表单验证器会执行,但验证错误被镇压,提交处理器不会被执行,如果用在提交元素中,验证器会执行,错误不会镇压,验证通过时提交器已执行,提交器返回的响应被忽略,最后执行回调,响应以回调的返回值为准
url:
AJAX请求地址,如果AJAX配置是在#ajax属性中指定,那么表明元素是表单元素,那么该项可以省略,内部会采用当前表单地址,如果不省略的话,则必须是url对象(\Drupal\Core\Url),如果AJAX配置是在附属物中指定,那么该项是必须的,且必须是一个字符串值,在前端以此项作为AJAX请求地址,并不关注callback
options:
仅当AJAX配置是在#ajax属性中指定时才有用,在url为url对象时,用于url对象的选项参数,在附属物中,AJAX配置的url项已经被转变成字符串,此时该项已经不需要存在了
wrapper:
在AJAX请求返回html内容时,用来放置内容的元素的id属性值,将来会用“selector”代替,以便可使用任意选择器来指定元素,目前只支持id,注意其值不要有“#”前缀
dialogType:
可选值为dialog、modal,表示以这些方式之一显示AJAX内容,优先级高于wrapper,在渲染数组的“#ajax”属性中设置无效,因为内部实际上据此包装格式会有不同的主内容渲染器,而“#ajax”属性仅支持AJAX主内容渲染器
method:
在使用wrapper 放置内容时,采用的jQuery放置方法,可选值有:replaceWith(替换整个id元素,这是默认值)、html(替换子内容)、append(在里面尾部追加)、prepend(在里面头部追加)、before(在外部前面追加)、after(在外面后面追加)
effect:
在放置内容时,采用的jQuery动画效果,可选值有:none (默认值,无效果)、slide(下拉显示)、fade(慢慢淡出)
speed:
在使用放置动画时的动画速度,可选值有:slow(慢)、fast(快)、none(默认值,jQuery默认速度)或者一个以毫秒为单位的数字
event:
在该元素上触发AJAX的js事件(后称AJAX事件),有默认值时是可选的,默认会依据表单元素的类型自动选择,见下文,如果提供了值将以提供值为准,如果没有默认值则必须指定,不带on前缀,如change、keyup、input等,当有多个时使用空格分隔,最终将传递给jquery的on方法,可带名字空间
prevent:
当触发AJAX的js事件发生时要阻止的其他事件,比如触发事件是mousedown,此时你可能会想阻止click事件,当有多个时使用空格分隔,最终将传递给jquery的on方法,可带名字空间,事件传播和默认动作都会被阻止;可与AJAX事件相同,不会影响AJAX,仅会阻止该事件的传播和默认动作,但这没有必要,因为AJAX行为默认会阻止事件的传播和默认动作
progress:
用于配置进度指示器,当不需要显示进度指示时,可以设置为false,一个数组值,各子键为:
type:进度指示器的类型,可选值有:throbber(在元素后边显示一个转圈的等待动画图片,默认值)、bar(结合其他选项以进度条方式显示)、fullscreen(全屏居中显示一个等待动画图标)、false(禁用指示器,布尔值)
message:提示消息,字符串值,如“请稍后…”,应该是被翻译过的,默认为Drupal.t('Please wait...')
url:使用进度条时,获取进度数据的url,字符串值,在#ajax属性中也可以是url对象
method:请求进度条更新url时使用的http方法,get或post,默认为get
interval:使用进度条时,多长时间更新一次,单位毫秒,默认1500毫秒
当数组中仅有type时,该项可简写为type表示的字符串值
disable-refocus:
布尔值,指示ajax调用后,是否禁止重新获得焦点,默认为false,也就是会重新获得焦点,很多时候需要被设置为true,否则焦点会被反复拉回到元素导致用户无法使用
keypress:
布尔值,是否在元素上监听键盘事件,以便可以通过按压空格或回车键触发AJAX事件,默认为true,将在元素上监听“keypress”事件,注意有四种类型的表单元素:text、textarea、tel、number即便设置了该项,也不会通过空格触发AJAX事件,但回车可以
submit:
额外提交到服务器的数据,一个数组值,键名为name,键值为value;前端系统默认的并不会设置是哪一个元素触发了ajax请求(也可以理解成不会设置是哪个元素触发了表单AJAX提交),如果AJAX配置是在#ajax属性中指定,为了告诉后端触发元素,系统在该项添加了以下变量:
_triggering_element_name:触发元素名
_triggering_element_value:触发元素值,可选
如果AJAX配置是在附属物中指定,需要用户来设定触发元素,如果没有设置,那么后端在不知道触发元素的情况,会以第一个按钮元素当做触发元素
trigger_as:
是一个数组,用于指定一个表单提交元素(让后端知道是哪个元素触发了表单提交),键名name为触发元素名,value为触发元素值,该项是可选的,默认以本元素的name和value作为触发元素名和值,仅在#ajax属性中有效,该项在附属物中的AJAX配置里会被删除(信息已经提取到submit项中)
setClick:
布尔值,仅用于表单提交元素,如提交按钮,指示将当前元素当做触发表单提交的元素,并传递其名值到服务器,在内部普通表单元素该项默认为false,使用“.use-ajax-submit”类的表单元素默认为true ,因为普通表单元素本就会被传递,且“.use-ajax-submit”类的表单元素会自动设置为true,因此该项用户无需理会,这里列出仅供程序研究参考,注:不要将该项和trigger_as项混淆,任何表单元素的AJAX操作都会提交整个表单,trigger_as项常用在当前元素为非提交元素时,指定提交元素,而该项仅用于提交元素,如按钮,指示是否将本元素当做提交元素,当发生冲突时,该项优先级高于trigger_as项。
AJAX配置默认事件:
在没有指定事件名时,以下元素类型(渲染数组类型)将采用默认事件:
submit、button、image_button
默认事件名:mousedown,同时在没有设置阻止事情的情况下阻止click事件
password、textfield、number、tel、textarea
默认事件名:blur
radio、checkbox、select、date
默认事件名:change
link
默认事件名:click
注意:除以上这些元素类型外,其他元素类型必须设置事件名,否则不被设置ajax行为
官网参考文档:
AJAX API概述:
https://api.drupal.org/api/drupal/core!core.api.php/group/ajax/
创建Ajax菜单链接:
https://www.drupal.org/docs/8/api/menu-api/making-ajax-menu-links
AJAX表单示例:
https://www.drupal.org/docs/8/api/javascript-api/ajax-forms
命令:
https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Ajax%21CommandInterface.php/interface/implements/CommandInterface
补充:
1、如果在具备#ajax属性的元素上设置#ajax_processed,那么不论何值(NULL除外),只要设置了,那么就不会被附加AJAX行为,相当于没有给元素添加#ajax属性,调试时比较有用,被附加#ajax的元素处理后,会附加该属性,值为true。
2、在AJAX API中表单元素的AJAX操作总是提交整个表单值到后端,所有AJAX均以POST方式发起
我是云客,【云游天下,做客四方】,联系方式见主页,欢迎转载,但须注明出处