PAYATHON 2026

Google Apps Script 服务端无法读取 HTMLService 表单文件

排障赵师傅

结论

processForm() 没有被调用,原因是表单提交事件没有正确绑定。当前 HTML 有三个问题:

  1. document.querySelectorAll('importForm') 查找的是 <importForm> 标签,并非 id="importForm" 的元素。
  2. window.addEventListener('importForm', ...) 监听的是一个不会自动触发的自定义事件。
  3. handleFormSubmit() 没有绑定到表单的 submit 事件,因此 google.script.run 不会执行。

提交后,浏览器会按普通 HTML 表单处理,页面可能刷新,也可能看不出明显变化。由于请求没有发送到服务端,Executions 中不会出现 processForm 的执行记录。

修改表单提交逻辑

可以直接给表单绑定 onsubmit,并将整个表单元素传给服务端:

<!DOCTYPE html>
<html>
<head>
  <base target="_top">

  <script>
    function handleFormSubmit(form) {
      const fileInput = form.elements.file;
      const output = document.getElementById('output');

      if (!fileInput.files.length) {
        output.textContent = 'Please select a CSV file.';
        return false;
      }

      output.textContent = 'Importing...';

      google.script.run
        .withSuccessHandler(function (result) {
          output.textContent = result;
        })
        .withFailureHandler(function (error) {
          output.textContent = 'ERROR: ' + error.message;
        })
        .processForm(form);

      return false;
    }
  </script>
</head>

<body>
  <p>File must be in CSV format</p>

  <div id="output"></div>

  <form id="importForm" onsubmit="return handleFormSubmit(this);">
    <input
      type="file"
      name="file"
      id="file"
      accept=".csv,text/csv"
      required
    >
    <input type="submit" value="Import">
    <input
      type="button"
      value="Close"
      onclick="google.script.host.close();"
    >
  </form>
</body>
</html>

return false 会阻止浏览器按普通表单方式提交,google.script.run 仍可异步调用 Apps Script 服务端函数。

将 HTML 表单传给 google.script.run 时,表单必须是唯一参数。文件输入字段到达服务端后会变成 Blob,字段的 name="file" 决定了服务端可以通过 formObject.file 读取文件。

服务端读取 CSV

服务端先取得上传文件的 Blob,再读取其中的文本:

/** @OnlyCurrentDoc */

function onOpen(e) {
  const ui = SpreadsheetApp.getUi();

  ui.createMenu('📄 Import transactions')
    .addItem('Import Swissborg transactions', 'importSwissborgCsv')
    .addToUi();
}

function importSwissborgCsv() {
  displayImportDialog();
}

function displayImportDialog() {
  const htmlOutput = HtmlService
    .createHtmlOutputFromFile('importDialog.html')
    .setWidth(480)
    .setHeight(236);

  SpreadsheetApp.getUi()
    .showModalDialog(htmlOutput, 'Import transactions');
}

function processForm(formObject) {
  console.log('Processing the form...');

  if (!formObject || !formObject.file) {
    throw new Error('No file was received.');
  }

  const fileBlob = formObject.file;
  const csvText = fileBlob.getDataAsString('UTF-8');
  const rows = Utilities.parseCsv(csvText);

  console.log('File name: %s', fileBlob.getName());
  console.log('Number of rows: %s', rows.length);

  return 'Imported ' + rows.length + ' rows from ' + fileBlob.getName() + '.';
}

Blob.getAs('text/csv') 返回的仍是 Blob,并非 CSV 字符串。要分析 CSV,应先读取文本,再解析内容:

const csvText = fileBlob.getDataAsString('UTF-8');
const rows = Utilities.parseCsv(csvText);

如果源文件并非 UTF-8 编码,请将字符集改为文件实际使用的编码。

原代码中还需要处理的问题

querySelectorAll() 的选择器错误

原代码:

document.querySelectorAll('importForm');

这段代码会查找名为 <importForm> 的 HTML 标签。按 ID 查找应写成:

document.querySelectorAll('#importForm');

要选择页面上的所有表单,可以写成:

document.querySelectorAll('form');

addEventListener() 监听了错误的事件

原代码:

window.addEventListener('importForm', preventFormSubmit);

importForm 是元素 ID,并非浏览器的页面加载事件。应改为:

window.addEventListener('load', preventFormSubmit);

只改这两行还不能解决问题,仍需将 handleFormSubmit() 绑定到表单的 submit 事件。

如果不使用行内 onsubmit,可以统一通过 JavaScript 绑定:

<script>
  window.addEventListener('load', function () {
    const form = document.getElementById('importForm');

    form.addEventListener('submit', function (event) {
      event.preventDefault();

      google.script.run
        .withSuccessHandler(function (result) {
          document.getElementById('output').textContent = result;
        })
        .withFailureHandler(function (error) {
          document.getElementById('output').textContent =
            'ERROR: ' + error.message;
        })
        .processForm(form);
    });
  });
</script>

两种绑定方式任选其一。不要同时使用,否则点击一次可能会发送两次请求。

SpreadsheetApp.create() 缺少名称

原服务端代码还有这一行:

let tempSheet = SpreadsheetApp.create();

SpreadsheetApp.create() 需要传入电子表格名称,例如:

const tempSpreadsheet = SpreadsheetApp.create('CSV Import');

即使表单提交已经恢复正常,缺少这个参数仍会导致 processForm() 执行失败。

如果数据要写入当前绑定的 Google Sheet,则不必新建文件:

const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
const sheet = spreadsheet.insertSheet('Imported CSV');

同时需要留意 @OnlyCurrentDoc。它表示脚本只申请访问当前文档,而 SpreadsheetApp.create() 会创建新的电子表格,可能需要更大的授权范围。如果确实要新建文件,应按项目的实际权限配置处理。只操作当前表格时,继续使用 @OnlyCurrentDoc 更合适。

建议的排查顺序

先打开浏览器开发者工具,在 handleFormSubmit() 的第一行加入:

console.log('submit handler called');

点击 Import 后,根据结果继续检查:

  • 没有看到日志,说明客户端事件仍未绑定。
  • 看到了日志,但 Executions 中没有记录,检查调用 google.script.run 前是否发生了 JavaScript 异常。
  • Executions 中出现失败记录,查看 Apps Script 的异常信息,并确认页面上的 withFailureHandler() 是否显示了同一错误。
  • processForm() 已执行却没有收到文件,检查 <input> 是否保留了 name="file",以及传入的参数是否为整个 <form> 元素。

也可以先用一个最小的服务端函数测试通信:

function processForm(formObject) {
  console.log('processForm called');
  return formObject.file
    ? 'File received: ' + formObject.file.getName()
    : 'No file received';
}

确认文件能到达服务端后,再加入 CSV 解析和写入电子表格的逻辑,以便分别检查客户端提交、文件传输和 CSV 处理。

chrome-extension://invalid/ net::ERR_FAILED 通常来自浏览器扩展或扩展注入的页面资源,与这段表单代码没有直接关系。/wardeninit 错误本身也无法证明 Apps Script 服务端执行失败。当前代码已有明确的事件绑定问题,应先修正表单提交,再根据 Executions 和 withFailureHandler() 返回的具体异常继续排查。

备注:内容仅供参考。