说实话,以前我也被“低代码”这几个字坑过。那时候觉得,既然叫低代码,那不就是拖拖拽拽就能搞定一切吗?结果呢?买了一套昂贵的SaaS平台,模板丑得像上世纪的网页,想改个按钮颜色得找供应商排队,想加个自定义逻辑发现根本插不上手,最后钱花了,效率没提上来,反而多了个维护负担。
直到我遇见了 ToolJet。
这玩意儿不一样。它不是那种把你关在笼子里跳舞的封闭系统,而是一把真正的瑞士军刀。更重要的是,它是开源的(Open Source)。这意味着什么?意味着你可以把它部署在自己的服务器上,或者哪怕只是本地跑着,都是零软件授权成本。对于咱们这种想搞内部提效、又不想给大厂交智商税的小团队或个人开发者来说,简直是福音。
今天我不跟你讲那些虚头巴脑的概念,咱们直接上手,看看怎么用 ToolJet 搭建一个真正能用的内部管理系统,并且教你怎么避开那些常见的坑。
为什么选 ToolJet?先看清它的底牌
在动手之前,你得明白 ToolJet 到底强在哪,不然容易拿着锤子找钉子。
真正的“低代码”,但不是“无代码”: 很多低代码工具限制你只能用他们提供的组件。ToolJet 不同,它允许你写 JavaScript。如果你的业务逻辑复杂到拖拽组件搞不定,你可以直接写 JS 代码块(Queries)来处理数据。这才是专业玩家该用的工具。
连接能力极强: 不管是 PostgreSQL、MySQL、MongoDB,还是 Google Sheets、Airtable、Slack,甚至是你自己写的 API,ToolJet 都能连。它就像一个万能适配器,把你的数据孤岛打通。
零成本的核心优势: 因为支持 Docker 部署,你只需要一台便宜的云服务器(甚至家里的 NAS 或旧电脑),就能拥有企业级的后台管理界面。没有订阅费,没有按用户数收费的噩梦。
前端体验现代化: 基于 React 构建,界面响应速度快,不像某些老旧的低代码平台那样卡顿。
第一步:环境准备——把工具装起来
别去注册什么云账号,咱们自己部署,才叫掌控感。
方案 A:本地开发测试(最快上手)
如果你只是想试试水,或者只是给自己公司小范围用,直接用 Docker Compose 是最简单的。
在你的电脑上创建一个文件夹 tooljet-demo,然后新建一个文件 docker-compose.yml,内容如下:
version: '3'
services:
tooljet-db:
image: postgres:15
restart: always
environment:
- POSTGRES_DB=tooljet
- POSTGRES_USER=tooljet
- POSTGRES_PASSWORD=secret_password
ports:
- "5432:5432"
volumes:
- tooljet-db:/var/lib/postgresql/data
tooljet-app:
image: tooljet/tooljet:latest
restart: always
depends_on:
- tooljet-db
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgresql://tooljet:secret_password@tooljet-db:5432/tooljet
- REDIS_HOST=redis
- SECRET_KEY_BASE=VerySecretKeyThatShouldBeChangedInProduction
volumes:
- tooljet-app:/usr/src/app/data
volumes:
tooljet-db:
tooljet-app:
注意:上面的 SECRET_KEY_BASE 在生产环境中一定要换成一个随机生成的长字符串,否则别人可能破解你的会话。
在终端运行:
docker-compose up -d
等个一两分钟,打开浏览器访问 http://localhost:3000。你会看到 ToolJet 的登录页面。首次注册的用户会自动成为管理员。
方案 B:生产环境部署(稍微进阶)
如果你要正式投入使用,建议把数据库和 Redis 单独部署,使用 Nginx 做反向代理,并配置 HTTPS。但这超出了本文“手把手”的范畴,核心逻辑是一样的。对于大多数中小团队,单服务器部署 Docker 完全够用。
第二步:实战演练——搭建一个“员工报销管理系统”
光说不练假把式。我们来做一个真实的场景:员工提交报销申请 -> 经理审批 -> 财务归档。
这个系统涉及三个核心部分:
- 数据源:存储报销单。
- 表单页:员工填写信息。
- 仪表盘/列表页:经理查看和审批。
1. 创建数据源
登录 ToolJet 后,点击左侧菜单的 Data Sources。
我们选择 PostgreSQL(刚才 Docker 里已经配好了)。 填入信息:
- Host:
tooljet-db(如果是本地Docker,且App和DB在同一网络,用这个;如果在宿主机访问,可能是localhost或127.0.0.1,取决于你的网络配置,通常 Docker Compose 内部互通用服务名) - Port:
5432 - Database:
tooljet - Username:
tooljet - Password:
secret_password
点击 Test Connection,显示绿色成功,保存。
接下来,我们需要一张表。虽然 ToolJet 可以自动建表,但为了演示规范,我们在数据库客户端里手动创建一张表(或者在 ToolJet 的 Query Builder 里执行 SQL):
CREATE TABLE reimbursements (
id SERIAL PRIMARY KEY,
employee_name VARCHAR(100),
amount DECIMAL(10, 2),
category VARCHAR(50), -- 如:餐饮、交通、办公
description TEXT,
status VARCHAR(20) DEFAULT 'pending', -- pending, approved, rejected
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
现在,回到 ToolJet,点击 New Query,选择刚才的数据源,类型选 SQL。
输入查询语句:
SELECT * FROM reimbursements ORDER BY created_at DESC;
命名为 getAllReimbursements。
再创建一个插入查询:
INSERT INTO reimbursements (employee_name, amount, category, description, status)
VALUES ('{{ $page.queryFormValues.employee_name }}', {{ $page.queryFormValues.amount }}, '{{ $page.queryFormValues.category }}', '{{ $page.queryFormValues.description }}', 'pending');
命名为 submitReimbursement。
注:这里的 {{ }} 是 ToolJet 的变量绑定语法,稍后我们会讲到。
2. 搭建员工提交页面(表单)
新建一个 App,命名为 ReimbursementSystem。
设计表单
从左侧组件库拖入一个 Form 组件。 在 Form 内部,添加以下输入控件:
- Text Input: 标签设为“姓名”,绑定变量名为
employee_name。 - Number Input: 标签设为“金额”,绑定变量名为
amount。 - Select Dropdown: 标签设为“类别”,选项设为
["餐饮", "交通", "办公用品", "其他"],绑定变量名为category。 - Textarea: 标签设为“备注”,绑定变量名为
description。 - Button: 标签设为“提交”。
绑定交互逻辑
选中“提交”按钮,在右侧属性面板找到 On Click 事件。
这里有一个关键步骤:验证与执行。
- 点击 Add Action -> Query -> 选择我们刚才创建的
submitReimbursement。 - ToolJet 会自动尝试映射参数。你需要确认:
employee_name对应表单中的employee_nameamount对应表单中的amount- …以此类推。
- 为了用户体验更好,添加一个 Toast Notification(提示框)。
- 在 On Click 事件中,先执行 Query,成功后触发 Toast:“提交成功!”。
- 如果失败,触发 Error Toast。
小技巧: 在 Query 设置里,你可以勾选 Run on Load 为 False,确保只有点按钮才提交。
现在,点击预览(Preview),试着填几个数据提交一下。去数据库里看看,数据是不是真的进来了?
3. 搭建经理审批页面(列表与操作)
这是最体现 ToolJet 灵活性的地方。我们需要一个表格展示所有申请,并提供“通过”和“拒绝”按钮。
创建表格
拖入一个 Table 组件。
设置 Data Source 为我们之前的 getAllReimbursements Query。
配置列:
- ID
- Employee Name
- Amount
- Category
- Status
- Created At
添加操作列(Actions)
默认表格只有数据。我们需要在最后一列加按钮。 在 Table 的属性里,找到 Columns 配置,添加一列自定义渲染(Custom Render)或者使用内置的 Action 功能(取决于版本,新版推荐用 Cell Actions 或 Toolbar)。
更简单的方式是使用 Button Group 在每一行操作。 但在 ToolJet 中,处理行级操作的最佳实践是使用 Row Actions。
- 在 Table 组件上右键,选择 Add Row Action -> Button。
- 设置按钮文本为“批准”。
- 设置 On Click 事件:
- 新建一个 Query,命名为
approveStatus。 - SQL:
UPDATE reimbursements SET status = 'approved' WHERE id = {{ $row.id }}; - 注意:这里的
$row.id是 ToolJet 的上下文变量,代表当前行的 ID。 - 执行完 Update 后,刷新列表:再次执行
getAllReimbursementsQuery。
- 新建一个 Query,命名为
同理,添加一个“拒绝”按钮:
- Query:
rejectStatus - SQL:
UPDATE reimbursements SET status = 'rejected' WHERE id = {{ $row.id }}; - 刷新列表。
视觉优化:状态颜色
为了让经理一眼看出哪些是待处理的,我们可以根据状态改变表格行的样式或文字颜色。
在 Table 的 Row Style 或 Cell Style 配置中,使用表达式:
{{ $row.status === 'pending' ? '#fff3cd' : ($row.status === 'approved' ? '#d4edda' : '#f8d7da') }}
这行代码的意思是:如果是 pending,背景黄色;approved 绿色;rejected 红色。
4. 高级技巧:如何用 JavaScript 处理复杂逻辑
假设老板说:“单笔超过 5000 元的报销,需要总经理二次审批。”
这时候,纯 SQL 就有点麻烦了,而且不利于维护。ToolJet 的强大之处在于它可以执行 JS 代码。
- 在 Query 编辑器中,不要选 SQL,而是选 JavaScript。
- 编写逻辑:
// 假设我们有一个函数 receiveData 接收传入的参数
function receiveData(data) {
const { id, amount, currentStatus } = data;
if (amount > 5000 && currentStatus === 'pending') {
// 如果需要二次审批,可以更新状态为 'manager_review'
return {
action: 'update_status',
payload: { id: id, new_status: 'manager_review' },
message: '金额较大,需总经理审批'
};
} else {
// 普通审批逻辑
return {
action: 'update_status',
payload: { id: id, new_status: 'approved' },
message: '审批通过'
};
}
}
// 这里的 data 可以从外部传入,比如在 Button 的 On Click 中传递 row 对象
在 ToolJet 的 UI 层面,你可以在按钮的 On Click 中选择这个 JS Query,并将 $row 作为参数传入。JS 返回的结果可以进一步触发其他 UI 动作,比如弹出不同的 Toast 提示。
这就是“低代码”与“无代码”的分水岭。当业务逻辑变得复杂时,你不需要跳出平台去找外挂,直接在平台内写代码,既保留了开发的灵活性,又享受了低代码的快速迭代。
避坑指南:那些年我踩过的雷
虽然 ToolJet 很强,但新手很容易犯以下几个错误,导致项目烂尾或性能极差。
1. 不要把所有数据都拉到前端
错误做法:在 Table 组件上直接绑定一个 SELECT * FROM huge_table,不管有没有分页。
后果:如果表里有 10 万条数据,前端会卡死,浏览器内存溢出,用户体验极差。
正确做法:
- 利用 ToolJet 的 Pagination 功能。
- 或者,在 SQL Query 中使用
LIMIT和OFFSET。 - 对于超大数据量,建议在数据库层建立索引,并使用后端聚合查询,只返回统计摘要。
2. 忽视安全性:权限控制
错误做法:直接把 App 链接发给所有人,谁都能看谁都能改。 后果:数据泄露,误删数据。 正确做法:
- ToolJet 本身有基本的用户认证。
- 更高级的做法是:在 SQL 查询中加入
WHERE user_id = {{ currentUser.id }}。这样员工只能看到自己的报销记录,经理能看到部门的,财务能看到所有的。 - 利用 ToolJet 的 Roles & Permissions 插件(如果有企业版功能)或者通过自定义 Query 逻辑来实现行级安全(RLS)。
3. 过度依赖拖拽,缺乏模块化
错误做法:在一个 App 里放 50 个页面,所有逻辑都写在同一个 Page 里。 后果:后期维护地狱,改一个按钮,整个页面报错。 正确做法:
- 善用 Components。把常用的表单、表格封装成独立组件。
- 把复杂的 SQL 查询封装成独立的 Queries,并在多处引用。
- 保持页面职责单一:提交页就是提交,列表页就是列表。
4. 忘记处理错误边界
错误做法:用户输入了非数字的金额,或者网络中断,页面直接白屏或报错信息晦涩难懂。 正确做法:
- 在每个 Query 的 Settings 中,开启 Error Handling。
- 使用 Try-Catch 逻辑(在 JS Query 中)捕获异常。
- 在前端 UI 上,给用户明确的反馈:“网络连接失败,请重试”或“金额格式不正确”。
进阶:如何让它看起来像“真产品”
ToolJet 默认的界面虽然干净,但略显单调。想要让非技术人员(比如你的老板或同事)觉得“哇,这系统好专业”,需要做一点 CSS 定制。
ToolJet 支持自定义 CSS。
- 在 App 设置中,找到 Theme 或 Custom CSS 区域。
- 编写一些简单的样式:
/* 修改主色调 */
:root {
--primary-color: #2563eb; /* 你喜欢的蓝色 */
--success-color: #10b981;
}
/* 让表格行悬停效果更明显 */
.tj-table-row:hover {
background-color: #f3f4f6 !important;
cursor: pointer;
}
/* 自定义按钮圆角 */
.tj-button {
border-radius: 8px !important;
font-weight: 600;
}
- 图标增强:
在按钮文字前加上 FontAwesome 图标。ToolJet 支持嵌入 HTML 或使用图标库。
例如,批准按钮可以用
<i class="fa fa-check"></i> 批准,这样视觉上更直观。
总结:从零成本到高效能
搭建这样一个系统,你不需要支付任何软件许可费。
- 服务器成本:如果你用家里旧电脑或最低配的云服务器(如 AWS EC2 t3.micro 或国内阿里云轻量应用服务器),每月可能只需几十块钱,甚至免费。
- 人力成本:原本需要外包开发花几万块做的内部工具,现在你自己半天就能搭出来原型,一周内完善功能。
- 维护成本:因为代码和逻辑都在你手里,随时可以修改,不用担心厂商涨价或停止服务。
ToolJet 的核心价值在于它平衡了速度和控制权。它不像 Excel 那样难以协作和维护,也不像定制开发那样昂贵和缓慢。
当你把这个报销系统跑通后,你会发现,这种成就感是无与伦比的。你可以接着做库存管理、CRM、项目管理……只要你想得到,ToolJet 就能帮你实现。
记住,低代码不是为了取代程序员,而是为了赋能每一个有想法的人。别被那些复杂的概念吓倒,打开 ToolJet,开始拖拽你的第一个组件吧。你的第一个内部神器,就在指尖。
