编写第一个任务组件

完成支持进度与取消的资料统计任务。

扩展 API v1按当前源码核对
本页目录

目标与最终效果#

这一章完成 资料统计自动化任务:接收文本、分批处理、显示实际进度、支持取消,完成后显示结果。它展示一个任务组件的最小闭环,后续可替换为自己的业务阶段。

示例不调用模型或外部进程,不持久化项目。取消通过运行身份停止后续批次;接入实际进程后,必须额外连接该进程真实的停止机制。

下载完整任务源码

目录和模块顺序#

text
my-example-task/
  manifest.json
  main.js
  modules/state.js
  modules/actions.js
  modules/view.js
  styles/workspace.css
  .tests/actions.test.cjs
  README.txt

先加载状态、再加载操作、再加载视图,最后注册入口。按环境准备放进开发版的任务扩展目录,重新扫描、启用并打开。

第一步:声明任务类别#

json
{
  "id": "my-example-task",
  "name": "资料统计示例",
  "version": "1.0.0",
  "apiVersion": 1,
  "minAppVersion": "2.7.5",
  "type": "thirdparty",
  "category": "task",
  "slot": "standalone",
  "description": "分批统计文本材料,演示进度、取消与清理",
  "main": "main.js",
  "scripts": [
    "modules/state.js",
    "modules/actions.js",
    "modules/view.js"
  ],
  "styles": [
    "styles/workspace.css"
  ],
  "permissions": [],
  "button": {
    "label": "资料统计",
    "order": 100
  }
}

这里明确使用 category: task。不要复制一个内置组件 ID,也不要添加未经宿主支持的 backend 字段。

第二步:把运行状态独立出来#

javascript
(function () {
  window.MyExampleTask = window.MyExampleTask || {};
  MyExampleTask.createState = function () {
    return { runId: 0, status: 'idle', stage: '等待输入', processed: 0,
      total: 0, result: null, error: '', disposed: false };
  };
})();

runId 用于识别一次运行;status 描述任务状态;stage 面向用户解释当前步骤。disposed 用于阻止关闭后的回调继续更新界面。

第三步:实现分批执行与取消#

javascript
(function () {
  var task = window.MyExampleTask;
  var pause = function () { return new Promise(function (r) { setTimeout(r, 0); }); };
  task.cancel = function (state, render) {
    if (state.status !== 'running') return;
    state.runId += 1;
    state.status = 'cancelled';
    state.stage = '已取消,已有输入保持不变';
    render();
  };
  task.run = async function (state, input, render) {
    if (state.disposed || state.status === 'running') return;
    var text = input.trim();
    if (!text) {
      state.status = 'failed'; state.error = '请先粘贴需要统计的材料';
      state.stage = '检查输入'; render(); return;
    }
    var id = ++state.runId;
    state.status = 'running'; state.stage = '分批统计材料';
    state.error = ''; state.result = null; state.processed = 0;
    state.total = text.length;
    var lines = 1, characters = 0;
    var current = function () { return !state.disposed && state.runId === id; };
    try {
      for (var offset = 0; offset < text.length; offset += 4096) {
        if (!current()) return;
        var chunk = text.slice(offset, offset + 4096);
        for (var i = 0; i < chunk.length; i++) {
          if (chunk[i] === '\n') lines++;
          if (!/\s/.test(chunk[i])) characters++;
        }
        state.processed = Math.min(text.length, offset + 4096);
        render();
        await pause(); // Give the renderer a chance to paint and receive cancel.
      }
      if (!current()) return;
      state.result = { lines: lines, characters: characters };
      state.status = 'completed'; state.stage = '统计完成';
    } catch (error) {
      if (!current()) return;
      state.status = 'failed';
      state.error = state.stage + '失败:' + String(error.message || error);
    } finally {
      if (current()) render();
    }
  };
})();

每批最多处理 4096 个 UTF-16 单元,然后把执行权交回事件循环,使界面能重绘和响应取消。统计的 characters 是非空白 UTF-16 单元数,不是词数或字形数;文本首尾空白先被移除。

不要把这里的分批文本处理当作通用后台线程。更重的计算应使用目标宿主支持的后台执行路径,而不是持续占用渲染线程。

第四步:让界面只反映当前状态#

javascript
(function () {
  var task = window.MyExampleTask;
  var cleanup = null;
  task.dispose = function () { if (cleanup) cleanup(); cleanup = null; };
  task.mount = function (host) {
    task.dispose();
    if (!host) throw new Error('任务窗口容器不可用');
    var state = task.createState();
    var root = document.createElement('section'); root.className = 'my-example-task';
    var title = document.createElement('h2'); title.textContent = '资料统计';
    var input = document.createElement('textarea');
    input.setAttribute('aria-label', '待统计材料'); input.placeholder = '粘贴文本材料';
    var start = document.createElement('button'); start.textContent = '开始统计';
    var stop = document.createElement('button'); stop.textContent = '取消';
    start.type = stop.type = 'button';
    var status = document.createElement('p'); status.setAttribute('role', 'status');
    var progress = document.createElement('progress'); progress.max = 1;
    var output = document.createElement('pre');
    function render() {
      if (state.disposed) return;
      var running = state.status === 'running';
      start.disabled = running; stop.disabled = !running; input.disabled = running;
      status.textContent = state.error || state.stage;
      progress.value = state.total ? state.processed / state.total : 0;
      output.textContent = state.result ? JSON.stringify(state.result, null, 2) : '';
    }
    function run() { task.run(state, input.value, render); }
    function cancel() { task.cancel(state, render); }
    start.addEventListener('click', run); stop.addEventListener('click', cancel);
    root.append(title, input, start, stop, status, progress, output);
    host.append(root); render();
    cleanup = function () {
      state.disposed = true; state.runId++;
      start.removeEventListener('click', run); stop.removeEventListener('click', cancel);
      root.remove();
    };
  };
})();

开始、取消、进度和结果都由同一份状态驱动。旧运行失效后不能把新任务改成完成;清理时移除事件和根节点,不碰其他组件的监听。

第五步:注册与样式#

javascript
(function () {
  Mrite.plugins.register({
    id: 'my-example-task',
    activate: function (api, hostElement) { MyExampleTask.mount(hostElement); },
    deactivate: function () { MyExampleTask.dispose(); }
  });
})();
css
.my-example-task { padding:24px; color:#152139; font:14px/1.7 system-ui; }
.my-example-task textarea { display:block; box-sizing:border-box; width:100%; min-height:180px; padding:12px; font:inherit; }
.my-example-task button { margin:12px 8px 0 0; padding:8px 18px; border:0; border-radius:8px; background:#245dba; color:white; }
.my-example-task button:disabled { opacity:.5; }
.my-example-task progress { width:100%; }
.my-example-task pre { white-space:pre-wrap; overflow-wrap:anywhere; }

第六步:按操作验收#

操作 预期结果
输入“甲乙”,换行“丙” lines: 2,characters: 3
空输入直接开始 提示需要材料,可以继续编辑
长文本运行时取消 显示已取消,输入恢复可编辑
取消后换新文本运行 只显示新运行的结果
运行时关闭或停用 不再回写旧界面,不留下重复监听

在示例目录运行:

powershell
node --test .tests/actions.test.cjs

扩展为真实任务#

将 actions.js 中的本地统计替换为实际步骤时,继续保持运行身份、可解释阶段和清理链。输入文件先登记引用,确实需要时再读取;输出写入成功后才登记成果;外部请求、进程和任务取消分别接入自己的真实机制。

继续阅读阶段、进度与取消、工作区与恢复和桌面验收。不要在尚未完成保存和结果核对时把状态改为 completed。

下载本页 Markdown