本自具足,前端框架的终极形态就是浏览器本身?

57 阅读7分钟

现代前端的发展,已经走入一个奇特的阶段。过去十年中,前端社区创造了大量抽象层:虚拟 DOM、响应式系统、Hooks、模板编译器、构建器……每一层都声称可以简化开发,但这些“简化”叠加起来反而让整个生态愈发复杂。我们拥有了种类繁多的框架、完善的构建生态和自动化工具链,却在这个过程中失去了某种最初的简洁与确定。

在今天,一个简单的按钮都可能需要完整的工程流程:安装依赖、配置构建、引入组件、打包输出。我们在追求抽象与自动化的同时,也在不断远离浏览器原本提供的运行能力。dagger.js 的出现,并不是要否定这些进步,而是要重新提出一个被忽略的问题:如果浏览器已经“本自具足”,那么框架的角色究竟是什么?

一、浏览器:被遗忘的完备平台

我们对现代前端框架的使用如数家珍,却逐渐忘记了浏览器本身是如何运作的。

现代浏览器从功能角度来说,已经具备构建复杂应用的一切条件。它提供了 DOM 与 CSSOM 作为渲染核心,提供了模块系统、事件模型、历史管理、网络接口、存储机制。理论上,你完全可以只用原生 API 写出一个完整的前端应用。

问题在于,原生 API 虽然强大,但太底层、太冗长。开发者因此创造出各种“语法糖”与抽象层,最终这些抽象又演化成框架。dagger.js 则反其道而行:它不再创建新的抽象世界,而是通过极薄的一层“指令系统”,把浏览器现有的能力串联起来。

二、五个维度:从浏览器自身生长出的框架

dagger.js 的设计核心是一个简单的假设:浏览器并不是孤立的 API 集合,而是一个由五个对象模型(Object Model)构成的整体系统。这五个模型分别承担了现代应用所需的全部功能:

1. DOM(文档对象模型):负责结构与节点组织。

2. CSSOM(样式对象模型):控制视觉表现与响应式布局。

3. JSOM(JavaScript 对象模型):dagger.js 在传统 JS 的作用域体系之上,利用原型链继承构建出一棵层级化的 JSOM 树,使得组件之间的状态与逻辑可以自然地传递。

4. Module-OM(模块对象模型):基于 ES Module 构建的模块映射关系,让依赖管理和作用域隔离变得清晰自然。

5. Router-OM(路由对象模型):直接基于浏览器的 history.pushState与 history.popstate 事件实现,使路由控制回归原生。

这五个模型在浏览器内部本就存在,却彼此相对独立。dagger.js 并没有重新定义它们,而是通过“指令系统”让它们协同工作,使得一个 HTML 页面本身就具备了应用运行所需的全部逻辑与状态。

一个最小的 dagger.js 示例只需要几行代码:

<div +load="{ count: 0 }"><button +click="count++">Clicked: ${ count }</button></div>

这段代码没有任何构建步骤,也没有框架 API。+load 定义了作用域并初始化数据,+click 绑定事件,${ count } 实现模板插值。浏览器解析、执行并立即渲染,整个过程不依赖任何外部工具。

dagger.js 并不是在“模拟框架”,而是在“显化浏览器”。

三、JS-OM:作用域的结构化演化

在传统 JavaScript 中,作用域是一种线性概念:变量要么是局部的,要么是全局的。大型应用中,这种扁平作用域常常导致状态共享困难与层级失控。

dagger.js 通过原型链机制,将作用域组织成树形结构,每个 +load 指令都会生成一个 $scope 对象,并通过 Object.setPrototypeOf 继承上级作用域,从而形成一个天然的逻辑层级体系:

<div +load="{ a: 1 }">
   <div +load="{ b: 2 }">     <button +click="alert(a + b)">Show Sum</button>  </div></div>

在这个例子中,a 与 b 分别属于不同层级的作用域,内层可以访问外层的数据,而不会产生冲突或污染。

这种机制不依赖全局 store,也不需要额外的状态管理库。它通过浏览器原生的原型链,实现了一个轻量的、递归可继承的逻辑结构。这与虚拟 DOM 框架截然不同。dagger.js 并不维护一棵“镜像树”,它让浏览器的真实对象结构成为唯一的数据源。你写下的就是实际执行的。

换句话说,dagger.js 并未创造新的状态模型,而是让 JavaScript 自身的作用域机制变得可视化和可用。

四、DOM 与 CSSOM:恢复浏览器的原生更新链

React 的虚拟 DOM 在特定场景下极大提升了更新效率,但它的实现代价是割裂了浏览器原有的渲染流程。开发者不再直接操作 DOM,而是操作一棵抽象树,由框架负责“同步”变化。

dagger.js 放弃虚拟层,直接依托 DOM 与 CSSOM 的天然联动机制。

<div +load="{ active: false }" +click="active = !active" class="card">  Toggle</div><style>  .card { padding: 8px; border: 1px solid #aaa; cursor: pointer; }  .card[active] { background: #2196f3; color: white; }</style>

当状态变化时,dagger.js 直接更新 DOM 属性,CSSOM 自动触发重绘。没有 diff、没有 fiber,也不需要虚拟节点。浏览器的渲染引擎天生知道如何进行最小化更新。

这并非一种“简化”,而是一种信任——信任浏览器的自我优化能力。dagger.js 不再替浏览器决策,而是让浏览器恢复其本来的职责。

五、Module-OM:在没有构建的世界里运行

构建工具的出现曾是前端生产力的象征,如今却成为系统复杂度的主要来源。依赖链条过长、构建缓存错乱、npm 供应链攻击等问题,都与“构建层”紧密相关。

dagger.js 把模块加载完全交回浏览器,由原生 ES Module Loader 负责解析与缓存。

<div +load="load()">  <div *each="items">${ item }</div></div><script type="dagger/modules">  import { getList } from './utils.js'  export const load = () => ({    items: getList()  });</script>

模块即文件,加载即运行。开发环境与生产环境没有差异,不存在编译或转译步骤。每一行代码都可直接在浏览器中调试,源代码即运行时。这种方式减少了依赖链、提高了安全性,并重新定义了“部署”的含义:HTML 本身就是应用。

六、Router-OM:路径就是状态

路由系统是现代前端的重要组成部分,但多数框架通过引入独立的 router 实现状态与路径的同步,从而又构建出新的中间层。

dagger.js 直接使用浏览器的 history.pushState() 与 popstate 事件,以最小代价实现单页导航:

<nav>  <a +click="history.pushState({}, '', '/home')">Home</a>  <a +click="history.pushState({}, '', '/about')">About</a></nav><section>  <div *exist="$route.path === '/home'">Welcome Home</div>  <div *exist="$route.path === '/about'">About dagger.js</div></section>

这种方式看似朴素,却拥有天然的确定性与可预测性。浏览器自己维护路径、状态与回退栈,框架不再介入。这是对“控制权”的一次有意识的放手。

七、五个模型的协同与系统自洽

在 dagger.js 中,每一行指令都可能同时触发五个系统层面的协作:

DOM-OM 负责结构更新与事件绑定;

CSSOM-OM 响应状态变化进行重绘;

JS-OM 维护作用域与数据关系;

Module-OM 保证模块上下文一致;

Router-OM 在需要时同步路径状态。

这五个模型之间没有中间代理,也没有虚拟层。

dagger.js 的运行方式就像一场实时的编排,让浏览器自身的五个子系统重新以协调的方式共同工作。

这种“显式协作”模式,使浏览器第一次被完整地当作一个框架运行时,而不是被动的渲染容器。

八、“本自具足”:从技术到哲学的回归

dagger.js 的设计哲学可以用四个字概括——本自具足。

浏览器不是残缺的执行环境,它本身已经拥有渲染、交互、模块化、状态管理等全部能力。我们长期以来之所以需要框架,是因为浏览器的标准化进程滞后,而社区提前填补了空白。如今,ES Module、Proxy、MutationObserver、CustomElement 等特性已经成熟,浏览器再次成为一个自洽的应用运行平台。

dagger.js 做的不是创新,而是“去冗余”。它让我们重新相信,前端开发可以直接建立在浏览器的原生机制之上,而不需要额外的抽象层。

九、数据即状态:让逻辑与呈现重新合一

传统框架对“状态”的理解往往独立于数据本身。

React 需要调用 setState() 或 useState() 来显式触发更新;

Vue 使用ref和reactive来定义响应式数据。

在这些设计中,“数据”与“状态”是两件不同的事,

状态是数据的影子,需要维护同步关系。

dagger.js 取消了这种分裂。

在 dagger.js 的世界里,数据本身就是状态。

当你修改一个变量时,不需要告诉框架“它变了”,

浏览器通过指令系统自动完成更新:

<div +load="{ text: 'Hello dagger!' }">  <input *value="text">  <p>${ text }</p></div>

text 是一个普通变量,没有代理、没有观察器、也没有 setter。

当输入框的值改变时,*value 指令直接修改作用域中的数据,

而模板中的 ${ text } 立即重新渲染。

整个过程没有状态机、没有调度队列,也没有强制同步逻辑。

浏览器本身承担了“更新”的责任。

这种方式带来两个深远的变化:

1. 认知简化 —— 开发者不再需要思考“什么时候更新”,因为数据的修改即是更新。

2. 性能确定性 —— 没有代理或依赖追踪机制,更新范围由作用域天然限定,运行成本可预测。

在 dagger.js 中,页面状态不再是一棵需要维护的抽象树,而是由数据自然流动形成的逻辑投影。

这让前端重新回到了最初的直觉:页面内容随着关联数据的变化自动改变。

十、结语:框架的终点,是重返浏览器

前端的发展史,本质上是一场在“抽象”与“直观”之间的往复。每一次抽象带来生产力提升的同时,也让我们远离了原始的直觉。dagger.js 提供的不是新的 API,也不是新的编程模型,而是一种新的视角——让浏览器重新成为前端的第一原则。

当你写下每一个 *value、每一个 +click,你并不是在操作一个框架,而是在直接与浏览器对话。浏览器不再是工具链的终点,而是整个框架的起点。

也许,前端框架的“终极形态”,从来都不是超越浏览器,而是重返浏览器本身。