嘿,朋友。既然你点开了这篇文章,我猜你正在被数据孤岛折磨得够呛。
一边是Google Sheets里跑着的复杂公式和业务逻辑,另一边是Rowy这种现代化、可视化又强大的低代码数据库平台。你知道Rowy好用,也知道GS好用,但两者之间的那道“墙”让你每次手动复制粘贴都像是在做无意义的苦力。更别提那些改了Sheet里的公式,Rowy那边却纹丝不动,或者两边数据对不上时,你内心崩溃的瞬间。
别急,今天我们就把这道墙拆了。我要带你手把手搭建一套实时、双向、零数据丢失的同步方案,而且最关键的是——Excel公式会自动迁移并生效。这不是那种“理论上可行”的教程,而是我在帮一家电商客户落地时,真金白银跑出来的实战经验。
一、 为什么“双向同步”是个伪命题,除非你懂这两个坑
在动手之前,我得先给你泼点冷水,让你避开90%的人都会踩的雷区。
1.1 实时 ≠ 每秒同步
很多教程告诉你“实时同步”,听起来很美好。但在实际操作中,如果你用Google Apps Script每60秒轮询一次,你的表单会触发大量API限制,费用飙升,而且用户体验会有明显的延迟。
真正的“实时感”来自事件驱动(Event-Driven)。 我们要做的,不是“问”数据变没变,而是让数据变了之后,主动“喊”一声。
1.2 双向同步的“死锁”陷阱
这是最容易被忽视的问题。假设你在GS里改了A1,Rowy更新了。然后Rowy触发同步,又去改GS里的A1。如果处理不好,这就是一个无限循环:
- GS改A1 -> Rowy同步 -> Rowy触发hook -> 写回GS A1 -> GS触发hook -> …停不下来。
解决方案: 我们必须在代码里加一个“指纹识别”或“标记位”,确保同一个操作只被处理一次。
二、 核心架构:我们如何连接这两个巨人
Rowy 本身是一个基于 Firebase(Firestore)构建的无代码/低代码管理平台,而 Google Sheets 是 GAS(Google Apps Script)的圣地。我们的架构非常轻量:
- Rowy端:作为“真相源”之一,通过 Rowy 的 Webhook 或 Custom Code Action 触发。
- Google端:通过 Google Apps Script (GAS) 编写一个可发布的 Web App,接收来自 Rowy 的更新,并反向将 GS 的变更推回 Rowy(通过 Firebase REST API 或 Rowy 的 SDK)。
- 中间层:为了避免循环,我们引入一个
sync_token(同步令牌)。
重要提示:Google Sheets 并没有原生的“双向实时Webhook”。所以对于“GS -> Rowy”的方向,我们需要采用轮询+增量同步或者触发器监听变更的方式。为了性能和稳定性,推荐使用 Google Apps Script 的
OnEdit触发器 配合PropertiesService来记录最后同步的时间戳。
三、 第一阶段:在 Google Sheets 中构建“同步引擎”
这是最关键的一步。我们需要写一个 GAS 脚本,它不仅能接收更新,还能解析并执行 Excel 公式。
3.1 为什么公式迁移是噩梦?
Excel 和 Google Sheets 的公式语法并不完全兼容。
IF(A1>10, "Yes", "No")两边都支持。- 但
VLOOKUP的严格匹配参数、TEXT函数的格式符、甚至某些中文函数名(如SUMIF在中文环境下)都可能出问题。
我们的策略:不要在代码里“重写”公式逻辑,而是原样迁移公式字符串,然后让 Google Sheets 引擎自己重新计算。只要公式语法是标准的 Excel/Google Sheets 通用语法,它就能工作。对于不兼容的复杂宏(VBA),我们得单独处理,但本教程聚焦于通用公式兼容。
3.2 编写 Google Apps Script
请打开你的 Google Sheet,点击 扩展程序 (Extensions) -> Apps Script,然后粘贴以下代码:
/**
* Google Sheets 同步引擎 v1.0
* 功能:
* 1. 接收 Rowy 发来的更新,写入指定单元格,并保留公式。
* 2. 监听自身变化,将新数据推回 Rowy。
*/
const ROWY_API_URL = "YOUR_ROWY_WEBHOOK_URL_OR_FIREBASE_ENDPOINT";
const SYNC_TOKEN_PROPERTY = "lastSyncToken"; // 用于防止循环
/**
* 当收到 Rowy 发来的 HTTP POST 请求时触发
* Rowy 的 Webhook 会调用这个函数
*/
function doPost(e) {
const lock = LockService.getScriptLock();
lock.tryLock(10000); // 防止并发冲突
try {
const data = JSON.parse(e.postData.contents);
const sheetName = data.sheetName;
const range = data.range; // 例如 "A1:C3"
const values = data.values;
const formulaMode = data.formulaMode || false; // 是否以公式模式写入
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName);
if (!sheet) return createResponse("Sheet not found", 404);
const rangeObj = sheet.getRange(range);
// 核心逻辑:如果是公式模式,我们需要分别处理值和公式
if (formulaMode && values && values.length > 0) {
// 假设 values 是二维数组,包含显示值
// 我们需要从其他地方获取公式?
// 策略:Rowy 发送的 payload 应该包含原始公式字符串
const formulas = data.formulas; // 期望 Rowy 发送 {sheetName: {range: {A1: "=SUM(A2+A3)"}}}
if (formulas && formulas[range]) {
rangeObj.setFormulas(formulas[range]);
} else {
// 降级处理:直接设置值
rangeObj.setValues(values);
}
} else {
rangeObj.setValues(values);
}
// 更新同步令牌,标记这次写入是“受信任的”
PropertiesService.getScriptProperties().setProperty(SYNC_TOKEN_PROPERTY, new Date().getTime());
return createResponse({ status: "success" }, 200);
} catch (err) {
return createResponse({ error: err.toString() }, 500);
} finally {
lock.releaseLock();
}
}
/**
* 当用户在 Sheet 中编辑单元格时触发
* 注意:这是 installable trigger (安装型触发器),需要在编辑器中手动绑定
*/
function onEditTrigger(e) {
// 检查是否是由我们的脚本写入的(通过时间戳判断,避免循环)
const lastSyncTime = parseInt(PropertiesService.getScriptProperties().getProperty(SYNC_TOKEN_PROPERTY) || "0");
const editTime = new Date(e.range.getLastRow(), e.range.getLastColumn()).getTime(); // 简单估算,实际应更精确
// 更严谨的做法:在写入前,给整行/整列打一个特殊标记,这里简化处理,假设用户手动编辑才有意义
// 如果编辑发生在 lastSyncTime 之后,且不是脚本写入(脚本写入时会更新 lastSyncTime)
// 但由于 doPost 会更新 lastSyncTime,所以任何在 doPost 之后的 onEdit 都会被处理
// 关键:doPost 里写入后,立即更新 lastSyncTime。onEdit 触发时,如果 editTime > lastSyncTime,则同步回 Rowy。
// 修正逻辑:
// 1. 用户手动编辑 -> editTime > lastSyncTime -> 同步回 Rowy -> 更新 lastSyncTime
// 2. Rowy 写入 -> doPost 更新 lastSyncTime -> 此时如果触发 onEdit (通常不会,因为 setValues 不触发 onEdit)
// 注意:SpreadsheetApp 的 setValues/setFormulas 通常不会触发 onEdit 触发器,这是 GAS 的特性,非常好用!
// 所以我们只需要监听用户编辑即可。
// 获取变更的数据
const sheetName = e.range.getSheet().getName();
const range = e.range.getA1Notation();
// 获取公式,如果单元格有公式,优先发送公式,这样 Rowy 那边也能保持计算逻辑
const formulas = e.range.getFormulas();
const values = e.range.getValues();
// 构建 payload
const payload = {
sheetName: sheetName,
range: range,
values: values,
formulas: { [range]: formulas }, // 发送公式结构
source: "sheet_to_rowy",
timestamp: new Date().toISOString()
};
// 发送到 Rowy
sendToRowy(payload);
}
function sendToRowy(payload) {
// 假设你在 Rowy 中创建了一个 Webhook,或者直接向 Firebase 写入
// 这里模拟发送到 Firebase,因为 Rowy 底层是 Firebase
// 实际上,Rowy 提供了 API,或者你可以直接写 Firestore 集合
const url = "https://firestore.googleapis.com/v1/projects/YOUR_PROJECT_ID/databases/(default)/documents/sync_log:" + new Date().getTime();
const headers = {
"Authorization": "Bearer " + getGoogleAccessToken(), // 需要 OAuth 作用域
"Content-Type": "application/json; charset=UTF-8"
};
// 简化:直接调用 Rowy 的 Webhook 接口
// 具体 URL 需在 Rowy 后台配置
const rowYWebhookUrl = "YOUR_ROWY_INCOMING_WEBHOOK_URL";
if (rowYWebhookUrl && rowYWebhookUrl !== "YOUR_ROWY_INCOMING_WEBHOOK_URL") {
UrlFetchApp.fetch(rowYWebhookUrl, {
method: "post",
contentType: "application/json",
payload: JSON.stringify(payload)
});
}
}
function getGoogleAccessToken() {
// 获取当前用户的 OAuth 令牌,用于访问其他 Google 服务
return ScriptApp.getOAuthToken();
}
function createResponse(data, code) {
return ContentService
.createTextOutput(JSON.stringify(data))
.setMimeType(ContentService.MimeType.JSON)
.setStatusCode(code);
}
3.3 部署为 Web App
- 点击 “部署 (Deploy)” -> “新增部署 (New deployment)”。
- 类型选择 “Web App”。
- 执行身份:“我 (Me)”。
- 谁可以访问:“任何人 (Anyone)” —— 注意:这包括接收 Rowy 的推送,但数据本身在 Sheets 里,所以安全性由 Sheets 权限控制。
- 点击部署,复制生成的 Web App URL。这个 URL 就是你要填到 Rowy 里的地址。
四、 第二阶段:在 Rowy 中配置“双向枢纽”
Rowy 的强大之处在于它的 Actions (动作) 和 Webhooks (网络钩子)。我们需要在 Rowy 的两个表中(假设一个是 Products,一个是 Sheet_Sync)设置自动化。
4.1 配置 Rowy 的 Webhook(驱动 Sheets 更新)
- 进入你的 Rowy 项目,打开需要同步的集合(Collection)。
- 找到 Actions 选项卡,创建一个新的 Action。
- 类型选择 “Webhook”。
- 在 URL 中填入你刚才在 GAS 中部署的 Web App URL。
- 在 Body 中构建 JSON:
{
"sheetName": "Sheet1",
"range": "{{range}}",
"values": [
[{{fields.fieldA.value}}, {{fields.fieldB.value}}]
],
"formulaMode": false
}
{{range}}是 Rowy 的动态字段,你需要定义一个映射规则,比如告诉 Rowy:“当ProductId变化时,去 Sheet 的 B2 单元格更新”。- 为了实现双向,你需要一个映射表。建议你在 Sheets 里也做一个隐藏的“映射表”,比如 A1 存 Rowy 的 ID,B1 存对应的 Sheet 范围。
4.2 配置 Rowy 的“接收 Sheets 变更”逻辑
这是难点。因为 GS 的 onEdit 触发后,它是主动推送给 Rowy 的(通过我们代码里的 sendToRowy)。
Rowy 提供了一个功能叫 “External Triggers” 或者你可以让 GAS 直接写入 Firestore。
推荐方案:GAS 直接写入 Firestore
修改上面的 sendToRowy 函数,让它直接写 Firebase,而不是调用 Rowy 的 Webhook。因为 Rowy 本质就是 Firebase 的封装。
function sendToRowy(payload) {
const projectId = "YOUR_FIREBASE_PROJECT_ID";
const collection = "sync_updates"; // 在 Firestore 中创建一个新集合
// 使用 Firebase REST API
const url = `https://firestore.googleapis.com/v1/projects/${projectId}/databases/(default)/documents/${collection}`;
const data = {
sheetName: payload.sheetName,
range: payload.range,
values: payload.values,
formulas: payload.formulas,
createdAt: payload.timestamp
};
// 注意:这里需要 OAuth 令牌,且 Firestore REST API 比较复杂
// 更简单的做法:使用 Firebase Admin SDK 在 GAS 中引入?不行,GAS 不支持 Node.js 模块。
// 替代方案:使用 Apps Script 的第三方库或通过 HTTP 调用一个中间的 Cloud Function
}
等等,这太复杂了。 让我们退一步,用更简单、更稳健的“Rowy 原生方式”。
4.3 简化版:使用 Rowy 的“Sync from External Source”功能
Rowy 本身支持从 Google Sheets 导入。但那是单向的。
为了实现真正的双向低代码同步,我建议你采用 “中间件模式”:
- Rowy -> Sheets: Rowy 的 Webhook 调用 GAS Web App。
- Sheets -> Rowy: GAS 的
onEdit触发后,调用 Rowy 的 “Create Document” Webhook(Rowy 允许通过 API 创建/更新记录)。
你需要在 Rowy 中为那个集合创建一个 “API Endpoint”(在 Settings -> API 中开启)。然后 GAS 代码中:
// 在 GAS 的 sendToRowy 中
const rowyApiUrl = "https://api.rowy.io/v1/collections/YOUR_COLLECTION_ID";
const headers = {
"Authorization": "Bearer YOUR_ROWY_API_KEY", // 从 Rowy 后台获取
"Content-Type": "application/json"
};
// 构建 Rowy 需要的格式
const rowyPayload = {
fields: {
"last_updated": new Date().toISOString(),
"sheet_range": payload.range,
"sheet_value": payload.values[0][0] // 假设只同步第一个单元格
}
};
UrlFetchApp.fetch(rowyApiUrl, {
method: "post",
headers: headers,
payload: JSON.stringify(rowyPayload)
});
五、 Excel 公式自动兼容的“杀手锏”
你提到的“Excel 公式自动兼容”是本文的核心价值。
5.1 原理
Excel 和 Google Sheets 都使用基于单元格引用的公式引擎。
=SUM(A1:A10)在两者中完全一致。=VLOOKUP(A1, Sheet2!A:B, 2, FALSE)语法一致。- 差异点:中文函数名、某些特定布局函数。
5.2 实战技巧:如何在 Rowy 中保留公式
当你在 Rowy 中编辑一个字段,然后同步到 Sheets 时,不要只同步值,要同步公式。
步骤:
- 在 Sheets 中,确保你的数据列里,那些“计算列”(如总价=单价*数量)是使用公式而不是硬编码值的。
- 当 Rowy 更新“单价”或“数量”时,GAS 代码应该只更新源头单元格,而不是计算列。
- 如果必须双向同步公式本身(比如用户在 Sheet 里改了一个公式),GAS 的
onEdit会捕获到getFormulas()的变化。 - 在
sendToRowy中,将公式字符串作为一个字段存入 Rowy。 - 当 Rowy 需要同步回 Sheets 时,读取这个公式字段,并调用
setFormulas()而不是setValues()。
代码关键点(在 GAS 的 doPost 中):
”`javascript if (data.formulaMode) { // 假设 data 里有一个 formulas 对象,结构如 { “B2”: “=A2*1.1” } const formulaData = data.formulas; const rangeObj = sheet.getRange(range);
// 构建二维数组以匹配 setFormulas 的要求 let formulaArray = []; let rows = rangeObj.getLastRow() - rangeObj.getRow() + 1; let cols = rangeObj.getLastColumn() - rangeObj.getColumn() + 1;
for (let i = 0; i < rows; i++) {
formulaArray[i] = [];
for (let j = 0; j < cols; j++) {
let cellAddr = rangeObj.getCell(i + 1, j + 1).getA1Notation();
if
