跳转至

开发插件

插件的要求

在7.0版本中,插件通过JavaScript类实现。插件中需要实现:

模板

可以使用JavaScript或TypeScript编写插件。JavaScriptTypeScript的模板文件可以在jspsych-contrib仓库中找到。

插件的构成

constructor()

插件的constructor()会接收一个JsPsych的实例,constructor函数需要将其保存下来用于访问。

constructor(jsPsych){
  this.jsPsych = jsPsych;
}

trial()

插件的trial()方法用于运行试次。当jsPsych的时间线运行到了一个试次的时候,就会触发该插件的trial()方法。

trial方法接受以下三个参数:

  • display_element是用于渲染jsPsych的实验内容的DOM元素。该参数需要是一个HTMLElement,我们可以用这个参数控制将jsPsych呈现在文档额哪个部分。
  • trial是相应的时间线节点中设定的参数。
  • on_load是一个可选参数,包含了一个回调函数,会在trial()完成加载后触发。详见处理on_load事件

对于trial方法的唯一要求是在试次完成后调用jsPsych.finishTrial(),这样jsPsych才能知道什么时候进入下一个试次(如果是最后一个试次,则结束实验)。在那之前,插件爱干啥干啥。

静态的info

info属性是一个对象,必须有name属性和parameters属性。

const info = {
  name: 'my-awesome-plugin',
  parameters: { }
}

parameters参数包含了试次所需要的参数,每个参数都有typedefault属性。

const info = {
  name: 'my-awesome-plugin',
  parameters: { 
    image: {
      type: jspsych.ParameterType.IMAGE,
      default: undefined
    },
    image_duration: {
      type: jspsych.ParameterType.INT,
      default: 500
    }
  }
}

如果default值为undefined,则用户在使用这个插件的时候必须指定该参数的值,否则会在控制台报错。如果在info中指定了default值,则插件会在没有指定该参数的值的时候使用该默认值。

jsPsych多数情况下允许动态参数,即可以使用函数作为参数,这些函数会在试次开始前执行。但是,如果你希望插件的某一个参数是一个函数,且该函数 不需要 在试次开始前执行,就需要将参数的type设置为'FUNCTION',这样jsPsych就不会把它当作一个动态参数。参见canvas-*系列插件。

info对象需要时一个静态参数,详见下面的示例。

const info = {
  name: 'my-awesome-plugin',
  parameters: { 
    image: {
      type: jspsych.ParameterType.IMAGE,
      default: undefined
    },
    image_duration: {
      type: jspsych.ParameterType.INT,
      default: 500
    }
  }
}

class MyAwesomePlugin {
  constructor(...)

  trial(...)
}

MyAwesomePlugin.info = info;

插件的功能

.trial()方法中,我们可以做很多事情,比如修改DOM元素,创建事件监听器,创建异步的请求,等等。在这一部分,我们就来看一些常见的功能。

改变呈现的内容

改变呈现内容的方法有多种。trial方法的display_element参数包含了呈现实验内容的HTMLElement,所以我们可以使用JavaScript中的方法和这个元素进行交互。一种常见的方法是改变他的innerHTML。下面的例子中使用innerHTML呈现trial参数中设定的图片。

trial(display_element, trial){
  let html_content = `<img src="${trial.image}"></img>`;

  display_element.innerHTML = html_content;
}

jsPsych不会自动在试次开始前或结束后清空内容,所以我们一般需要在试次结束前使用innerHTML手动清空呈现的内容。

display_element.innerHTML = '';

等待一段时间

如果我们需要代码暂停运行一段时间,可以使用jsPsych封装的setTimeout()函数——jsPsych.pluginAPI.setTimeout()。在7.0版本中,使用这个方法的唯一优点在于它创建的定时器可以在试次末通过jsPsych.pluginAPI.clearAllTimeouts()清除。未来的版本中,我们可能会改变jsPsych.pluginAPI.setTimeout()的实现方式,并基于requestAnimationFrame加入一些计时相关的功能。

trial(display_element, trial){
  // show image
  display_element.innerHTML = `<img src="${trial.image}"></img>`;

  // hide image after trial.image_duration milliseconds
  this.jsPsych.pluginAPI.setTimeout(()=>{
    display_element.innerHTML = '';
  }, trial.image_duration);
}

处理键盘事件

插件允许我们监听很多事件,包括keyupkeydownjsPsych.pluginAPI模块包含了getKeyboardResponse函数,该函数可以很方便地处理实验中的按键。

下面是一个简单的例子。更多示例详见getKeyboardResponse处文档

trial(display_element, trial){
  // show image
  display_element.innerHTML = `<img src="${trial.image}"></img>`;

  const after_key_response = (info) => {
    // hide the image
    display_element.innerHTML = '';

    // record the response time as data
    let data = {
      rt: info.rt
    }

    // end the trial
    this.jsPsych.finishTrial(data);
  }

  // set up a keyboard event to respond only to the spacebar
  this.jsPsych.pluginAPI.getKeyboardResponse({
    callback_function: after_key_response,
    valid_responses: [' '],
    persist: false
  });
}

异步加载

试次事件中包括了on_load,该事件通常在.trial()方法完成后自动触发。在大多数情况下,这一事件发生在DOM元素的初始化(例如,渲染图片,创建事件监听器和定时器,等等)之后。但是,在某些情况下,插件可能会执行一个异步操作,该操作需要在插件的初始加载完成之前执行。例如audio-keyboard-response插件中,检查音频文件是否加载完成是异步操作,.trial()方法在音频文件初始化、DOM元素更新前就完成执行了。

如果您想手动触发插件的on_load事件,可以使用.trial()方法可选的第三个传入参数,该参数是加载完成时调用的回调函数。

为了告诉jsPsych在.trial()方法完成时 使用常规的回调,我们需要显式返回一个 Promise对象。从 7.0 版本开始,这个Promise对象仅用来告诉jsPsych不要触发on_load事件。在未来的版本中,我们可能会丰富这个Promise的功能,这样trial可以使用异步函数。

下面的示例中展示了如何在插件中使用on_load事件。请注意,此示例省略了加载和完成试次之间发生的所有事情。完整示例参见audio-keyboard-response插件的源代码。

trial(display_element, trial, on_load){
  let trial_complete;

  do_something_asynchronous().then(()=>{
    on_load();
  });

  const end_trial = () => {
    this.jsPsych.finishTrial({...})
    trial_complete(); // not strictly necessary, but doesn't hurt.
  }

  return new Promise((resolve)=>{
    trial_complete = resolve;
  })
}

保存数据

如果要将数据写入jsPsych的数据集,需要将数据对象传入jsPsych.finishTrial()

constructor(jsPsych){
  this.jsPsych = jsPsych;
}

trial(display_element, trial){
  let data = {
    correct: true,
    rt: 350
  }

  this.jsPsych.finishTrial(data);
}

记录的数据中,correcttruert350其他的一些数据也会由插件自动收集。

模拟模式

插件可以支持模拟模式

如果要插件支持模拟模式,插件需要有一个simulate()函数,该函数接受4个传入参数:

simulate(trial, simulation_mode, simulation_options, load_callback)

  • trial: 和传入插件的trial()方法的trial参数相同,包含了试次使用的参数。
  • simulation_mode: 字符串,可以是"data-only""visual",用来表示使用哪种模拟模式。插件是否支持"visual" mode. If "visual"模式是可选的。如果不支持"visual"模式,则插件会在被要求使用"visual"模式的时候自动使用"data-only"模式。
  • simulation_options: 该对象包含了模拟相关的参数。
  • load_callback: 该函数在模拟到on_load事件将要触发前调用。请在正确的时候调用这个回调,这样实验中的on_load事件才能正常运行。

一般来说,模拟模式的流程如下:

  1. 生成符合trial参数的数据。
  2. 将生成的数据和simulation_options中的数据合并。
  3. 验证最终的数据对象是否仍然符合trial参数。例如,检查反应时是否比试次的时长更长。
  4. data-only模式下,调用jsPsych.finishTrial()并传入生成的数据。
  5. visual模式下,调用trial()方法,并使用生成的数据触发相应的事件。Plugin API module提供了多种方法用来模拟诸如按键和点击鼠标这种事件。

我们计划在未来对模拟模式的开发进行更详细的讲解。不过目前,最好还是参考一下支持了模拟模式的插件的源代码,去看看上述流程是如何实现的。

关于编写插件的建议

如果你希望将开发的插件加入到jsPsych的主仓库中,请参照贡献代码指南

开发的插件最好能 适用于多种场景 。可以考虑通过参数方便用户进行自定义。例如,如果你的插件内会呈现文字,比如说按钮上的问题,可以将这些文字设置为参数。这样,适用其他语言的用户在适用插件的时候也可以对这些文字进行替换。