Google Apps Script 服务端无法读取 HTMLService 表单文件
结论
processForm() 没有被调用,原因是表单提交事件没有正确绑定。当前 HTML 有三个问题:
document.querySelectorAll('importForm')查找的是<importForm>标签,并非id="importForm"的元素。window.addEventListener('importForm', ...)监听的是一个不会自动触发的自定义事件。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() 返回的具体异常继续排查。
备注:内容仅供参考。
