JexLang:一次编写,随处验证 — 跨平台表达式语言
Source: Dev.to
🎯 我们共同面临的问题
想象一下:你正在构建一个多平台应用——React Web 应用、Android 移动端、Spring Boot 后端,甚至可能还有 iOS 应用。一切看似可控,直到你碰到熟悉的墙——校验逻辑。
你的产品经理走进来说:
“我们需要校验用户年龄在 18 岁以上,邮箱必须是公司域名,如果他们选择 Premium 计划,则必须拥有有效的信用卡。”
听起来很简单,对吧?现在把它在各平台上实现:
// JavaScript (Web/React Native/NodeJS)
if (
user.age >= 18 &&
user.email.endsWith('@company.com') &&
(user.plan !== 'Premium' || user.hasValidCard)
) {
// valid
}
// Java (Backend/Android)
if (
user.getAge() >= 18 &&
user.getEmail().endsWith("@company.com") &&
(!user.getPlan().equals("Premium") || user.hasValidCard())
) {
// valid
}
// Swift (iOS)
if user.age >= 18 &&
user.email.hasSuffix("@company.com") &&
(user.plan != "Premium" || user.hasValidCard) {
// valid
}
现在想象你的应用里有 50+ 条校验规则。每个平台都有自己的实现,需要分别测试,而且每条规则都可能隐藏细微的 bug。
欢迎来到校验地狱。 🔥
😫 真正的痛点
1. 代码重复的噩梦
每条校验规则都会出现在多个地方。业务规则变更时,你需要在 3‑4 个不同的代码库里修改代码。漏掉一个?就会出现不一致的行为,折磨你的调试过程。
2. 微妙的平台差异
不同语言对同一件事的处理方式略有不同:
- 字符串比较:
===vs.equals()vs== - 空值处理:
undefinedvsnullvsOptional<> - 类型强制转换:JavaScript 那怪异的
"5" + 3 = "53"vs Java 的严格类型
这些差异会导致只在特定平台上出现的 bug——最棘手的那类 bug。
3. 硬编码逻辑 = 部署痛苦
修改一条校验规则意味着一次代码改动、PR 审核、测试、预发布以及部署——每个平台都要单独进行。如果校验逻辑可以放在数据库或配置里会怎样?如果非开发人员也能在不触碰代码的情况下修改简单规则会怎样?
4. 测试开销
50 条校验规则 × 4 个平台 = 至少 200 条测试用例。而且你还要确保它们的行为完全一致。祝你好运。
💡 如果有更好的办法会怎样?
如果你可以用一种简单、熟悉的语法一次性编写校验逻辑:
user.age >= 18 && endsWith(user.email, "@company.com") && (user.plan != "Premium" || user.hasValidCard)
并且它能够在以下环境完全相同地执行:
- ✅ JavaScript/TypeScript(浏览器 & Node.js)
- ✅ Java(Android & 后端)
- 🚧 Swift(iOS)— 敬请期待!
没有平台特有的怪癖。没有代码重复。没有部署负担。
这正是我创建 JexLang 的原因。
🚀 介绍 JexLang
JexLang 是一种轻量级、平台无关的表达式语言,采用 类 JavaScript 语法。它极其擅长解决一个问题:一次编写,处处运行,行为一致。
为什么采用类 JavaScript 语法?
大多数开发者都熟悉 JavaScript 语法。即使你主要是 Java 或 Swift 开发者,也一定在某个时刻接触过 JavaScript。使用熟悉的语法,JexLang 的学习成本几乎为 零。
如果你能写出:
user.age >= 18 && user.active === true
那么你已经会 JexLang 了。
🛠️ JexLang 工作原理
JexLang 由三个核心组件组成:
1. 词法分析器(Tokenizer)
将表达式拆分为 token:
"user.age >= 18" → [IDENTIFIER:user, DOT, IDENTIFIER:age, GTE, NUMBER:18]
2. 解析器(Parser)
把 token 转换为抽象语法树(AST):
{
"type": "BinaryExpression",
"left": {
"type": "MemberExpression",
"object": "user",
"property": "age"
},
"operator": ">=",
"right": {
"type": "NumericLiteral",
"value": 18
}
}
3. 解释器(Interpreter)
在给定的上下文数据上评估 AST 并返回结果。
神奇之处在于:相同的 AST 结构和求值逻辑在每种语言中都实现得完全一致——相同的解析规则、相同的运算符优先级、相同的类型强制转换、相同的结果。
📖 实际使用场景
1. 动态表单校验
将校验规则存放在后端,所有平台共享:
{
"fields": [
{
"name": "email",
"validations": [
{ "rule": "required(email)", "message": "Email is required" },
{ "rule": "contains(email, '@')", "message": "Please enter a valid email" }
]
},
{
"name": "age",
"validations": [
{ "rule": "age >= 18", "message": "You must be 18 or older" }
]
}
]
}
你的 React 应用、Android 应用以及后端都可以抓取这些规则并使用 JexLang 进行评估。唯一的真相来源。
2. 业务规则引擎
无需代码部署即可配置业务逻辑:
// Rules stored in database
const discountRules = [
{ condition: "customer.tier === 'Gold' && cart.total > 100", discount: 20 },
{ condition: "customer.tier === 'Silver' && cart.total > 200", discount: 10 },
{ condition: "cart.items.length > 5", discount: 5 }
];
// Evaluate at runtime
const applicableRule = discountRules.find(rule =>
jexlang.evaluate(rule.condition, { customer, cart })
);
营销团队想把 Gold 级别的阈值从 100 改成 150?只需更新数据库——无需部署。
3. 条件化 UI 渲染
根据可配置规则显示/隐藏 UI 元素:
{
"component": "PremiumBanner",
"showWhen": "user.subscription === 'free' && user.daysActive > 30"
}
4. 工作流自动化
构建条件可由用户自行配置的工作流引擎:
IF order.total > 1000 AND customer.verified === true
→ Route to priority fulfillment
IF order.items.length > 10
→ Apply bulk processing
IF contains(order.shippingAddress.country, 'EU')
→ Apply VAT calculation
5. 复杂逻辑的功能标记
超越简单布尔标记:
const featureRules = {
newCheckout: "user.region === 'US' && user.accountAge > 30 && !user.hasActiveIssues",
betaFeatures: "user.role === 'developer' || contains(user.email, '@internal.com')"
};
⚡ 快速示例
基础表达式
// Arithmetic
jexlang.evaluate("10 + 5 * 2"); // 20
// Comparisons
jexlang.evaluate("100 >= 50"); // true
// Logical operators
jexlang.evaluate("true && false || true"); // true
// String operations
jexlang.evaluate("'Hello' + ' ' + 'World'"); // "Hello World"
使用上下文
const context = {
user: {
name: "John Doe",
age: 28,
email: "john@company.com",
roles: ["admin", "editor"]
},
settings: {
maxUploadSize: 100,
allowedFormats: ["jpg", "png", "pdf"]
}
};
// Access nested properties
jexlang.evaluate("user.name", context); // "John Doe"
jexlang.evaluate("settings.maxUploadSize > 50", context); // true