用 30 行代码写出一个计算器:从 dagger.js 的指令哲学谈起(并与 React/Vue 对比)
关键词:dagger.js / HTML-First / 指令驱动 / 表单交互 / 前端范式
适读人群:前端开发者、对低样板代码有强需求的同学
在线示例(CodePen)
📌 导读
这篇文章带你拆解一个超轻量计算器示例:仅用一份 HTML + 少量脚本,通过 dagger.js 的几条指令就能完成状态初始化、双向绑定、事件拦截、列表渲染等工作。文末附上 React/Vue 等价实现,便于横向对比开发心智与样板代码量。

01|先看成品:结构像 HTML,行为写在模板上
在示例里,几个关键 dagger.js 指令把“行为”直接声明在 HTML 上:
<form dg-cloak name="calculator" class="calc" +load +reset="value = ''" +submit#prevent="value = eval(value)">
<p>
<input type="textfield" placeholder="0" *value>
<input type="reset" value="C">
</p>
<p *each#item:button-Row="buttonRows">
<input *each#item:button="resolve(buttonRow)" *type="button.type" *value="button.showValue" +click="value += button.realValue">
</p>
</form>
+load:载入初始状态(如value、buttonRows)。*value:输入框与作用域变量双向绑定。*each:把buttonRows展成按钮矩阵。+submit#prevent、+reset、+click:表单提交/重置/点击事件在模板上完成声明。
读者要点:dagger.js 倾向于“HTML 优先”,让你在模板属性层就把 80% 的交互说清楚,减少 JS 样板与组件胶水。
02|状态从哪里来?用 +load 一步到位
示例中用 +load="load()" 将 load() 返回对象注入到模板作用域,因此模板里可以直接使用返回的字段与函数(如 value、buttonRows、resolve):
const load = () => ({
value: '',
buttonRows: [
[7, 8, 9, ['/', '÷']],
[4, 5, 6, ['*', '×']],
[1, 2, 3, '-'],
[0, '.', ['', '='], '+']
]
});
const resolve = buttonRow => buttonRow.map(item => ({
type: item[0] === '' ? 'submit' : 'button',
showValue: item[1] || item,
realValue: item[1] ? item[0] : item + ''
}));
对比传统做法:在很多框架中,状态初始化需要 Hook/Store/组件数据等;在 dagger.js,把“数据边界”声明回模板即可。
03|绑定与事件:一眼看懂的“指令语义”
- 双向绑定:
*value="value"(阻止默认提交并计算) - 表单提交:
+submit#prevent="value = eval(value)"(阻止默认提交并计算) - 重置按钮:
+reset="value = ''" - 按钮点击:
+click="value += button.realValue"
🔒 安全提醒:示例中
eval仅用于演示。生产请改用受限表达式解析(白名单字符/运算符或专用解析器)。
04|命名小细节:更贴近直觉
外层循环形如 *each#item:button-Row="buttonRows",模板里可用驼峰形式 buttonRow 继续传参(如 resolve(buttonRow))。这种直观的一致性降低了心智负担,让模板更可读。
05|同题双解:React / Vue 等价实现
下面给出“功能对齐”的极简版本(同样仅作演示,求值方式请替换为安全解析)。
✅ React(Hooks)版
import { useState } from "react";
const rows = [
[7, 8, 9, ["/", "÷"]],
[4, 5, 6, ["*", "×"]],
[1, 2, 3, "-"],
[0, ".", ["", "="], "+"],
];
export default function Calc() {
const [value, setValue] = useState("");
const resolve = (row) =>
row.map((it) => ({
type: Array.isArray(it) && it[0] === "" ? "submit" : "button",
showValue: Array.isArray(it) ? it[1] : it,
realValue: Array.isArray(it) ? it[0] : String(it),
}));
const onSubmit = (e) => {
e.preventDefault();
try { setValue(String(Function(`return (${value})`)())); } catch {}
};
return (
<form onSubmit={onSubmit} onReset={() => setValue("")} className="calc">
<p>
<input value={value} onChange={(e) => setValue(e.target.value)} />
<input type="reset" value="C" />
</p>
{rows.map((r, i) => (
<p key={i}>
{resolve(r).map((b, j) =>
b.type === "submit" ? (
<input key={j} type="submit" value={b.showValue} />
) : (
<input
key={j}
type="button"
value={b.showValue}
onClick={() => setValue((v) => v + b.realValue)}
/>
)
)}
</p>
))}
</form>
);
}
观察:UI 结构与交互主要由 JSX/JS 表达,需要 useState、事件回调、map、key 等样板。
✅ Vue 3(组合式)版
<form class="calc" @submit.prevent="onSubmit" @reset="value = ''">
<p>
<input v-model="value" placeholder="0" />
<input type="reset" value="C" />
</p>
<p v-for="(row, i) in rows" :key="i">
<input
v-for="(b, j) in resolve(row)"
:key="j"
:type="b.type"
:value="b.showValue"
@click="b.type==='button' && (value += b.realValue)"
/>
</p>
</form>
<script setup>
import { ref } from "vue";
const value = ref("");
const rows = [
[7, 8, 9, ["/", "÷"]],
[4, 5, 6, ["*", "×"]],
[1, 2, 3, "-"],
[0, ".", ["", "="], "+"],
];
const resolve = (row) =>
row.map((it) => ({
type: Array.isArray(it) && it[0] === "" ? "submit" : "button",
showValue: Array.isArray(it) ? it[1] : it,
realValue: Array.isArray(it) ? it[0] : String(it),
}));
const onSubmit = () => {
try { value.value = String(Function(`return (${value.value})`)()); } catch {}
};
</script>
观察:v-model、事件修饰符与dagger.js的指令范式接近,但仍需组件化上下文与实例化。
06|对照清单:三家同题的“心智与样板”
- 状态初始化
- dagger.js:
+load返回对象 → 直接成为模板作用域 - React:
useState/ 上层状态管理 - Vue:
ref/reactive
- dagger.js:
- 双向绑定
- dagger.js:
*value - React:受控组件 +
onChange - Vue:
v-model
- dagger.js:
- 事件拦截
- dagger.js:
+submit#prevent - React:手写
e.preventDefault() - Vue:
@submit.prevent
- dagger.js:
- 列表渲染
- dagger.js:
*each - React:
array.map - Vue:
v-for
- dagger.js:
- 模板位置与语义
- dagger.js:原生 HTML 属性层表意
- React:JSX(JS 主导)
- Vue:模板/单文件组件
一句话总结:dagger.js 让你“少写样板,模板能动”,尤其适合简单到中等复杂的表单/工具型页面与后台面板。
07|进阶练习(自行扩展)
- 🔒 替换
eval:使用受限表达式解析(仅允许0–9 . + - * / ())。 - ⌨️ 键盘输入:在容器上加
+keydown,过滤无效按键并#prevent。 - 🧾 历史记录:在
+load增加history: [],提交后history.push(...)并渲染。 - 📱 移动端体验:按钮尺寸/间距、触控反馈与误触容错。
08|结语
这个小计算器例子展示了 dagger.js 的HTML-First、指令即行为:把交互与数据边界写回模板,让页面更易读、更易改、更易落地。当需求增长时,再引入模块化与生命周期钩子(如 +loaded、+unload、+unloaded)也很顺滑。
本文内容就到这里,后续文章将为大家带来更多案例和讲解。
如果对dagger.js感兴趣的话,请您点赞收藏、分享本系列文章,也欢迎留言或者私信作者提出问题和建议,您的关注是对我最大的支持和鼓励。感谢您的阅读,祝工作学习顺利!
👉 在线示例(CodePen)
更多推荐


所有评论(0)