Skip to content

zhuqingyv/nova-dom

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

59 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Nova View

把 UI 写回 DOM,把更新交给 Signal,把产能交给 AI。

没有 JSX、没有虚拟 DOM:写下去的是原生 DOM,状态一变就精准更新。


  • ⚡️ 极简主义的反叛
    不依赖模板语法和虚拟 DOM 的黑盒;CDN 场景可零构建直用。链式 API 像搭积木一样构建 UI,AI 不用学框架语法也更不容易写错。

  • 🎯 手术刀级的更新
    基于 Signal 的细粒度依赖追踪。状态变化时,只更新对应的 TextNode 或属性,绝不触碰无关 DOM。

  • 🏎️ 性能控制权回归
    既有自动 Diff 的便捷,也有 ForController 的极致。需要榨干性能时,你可以绕过 Diff 算法,直接对列表进行增删改查。


⚡ 5 秒快速体验

复制这段代码到浏览器控制台,立即感受“原生 DOM + 响应式”的魔法:

const s=document.createElement('script');s.src='https://unpkg.com/nova-view/dist/lib/nova-dom.umd.min.js';s.onload=()=>{const{Dom:$,ref:r,mount:m}=window['nova-view'],{div:d,button:b,h1:h}=$,c=r(0),app=d().style('position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:#fff;padding:2rem;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.1);text-align:center;z-index:9999;')(h().style('color:#333;margin-bottom:1rem;')('Hello Nova View!'),b().style('background:#007bff;color:white;border:none;padding:0.5rem 1rem;border-radius:4px;cursor:pointer;').onclick(()=>c.value++)(()=>('点击次数: '+c.value)));m(app,document.body)};document.head.appendChild(s)

🤖 为什么它是“AI Native”的 UI 库?

并不是所有框架都适合让 AI 写。Nova View 的设计天然契合 LLM 的代码生成特性:

  1. 极低的语法“幻觉”率

    • 传统框架:AI 常搞混 Vue2/3 写法、React Hook 依赖数组或 JSX 闭合标签。
    • Nova View:纯函数调用链(div().class().onclick())。结构即逻辑,AI 极难写出语法错误的函数调用。
  2. 原生的调试反馈回路

    • AI 生成的代码直接返回 HTMLElement。如果出了问题,AI 可以直接生成原生 DOM API(如 querySelector)代码来自我修正,无需理解复杂的框架运行时内部状态。
  3. 统一的状态心智

    • 没有复杂的指令集。动态内容要么是函数 () => val,要么是 ref。这种高度统一的模式让 AI 生成逻辑时异常稳定。

💡 提示词模板"用 nova-view 写一个计数器,使用 ref 管理状态,div 作为容器,style 使用对象写法..." -> AI 一次通过率极高。


💡 为什么选择 Nova View?

原生 DOM API - 冗长且容易出错:

const div = document.createElement('div');
div.className = 'container';
div.style.padding = '1rem';
const span = document.createElement('span');
span.textContent = 'Hello';
div.appendChild(span);

Nova View - 优雅且强大:

// 底层基于 Proxy 实现按需拦截,内置属性缓存与 Style Diff
div().class('container').style({ padding: '1rem' })(span()('Hello'));

代码量减少 70%,却拥有了完整的响应式能力和 TypeScript 支持。


📦 安装

包管理器安装 (推荐)

npm install nova-view
# 或 yarn add nova-view
# 或 pnpm add nova-view

CDN 直接使用 (零构建)

无需安装,直接在 HTML 中引入(推荐使用 unpkg):

<!DOCTYPE html>
<html>
<head>
  <script type="importmap">
    {
      "imports": {
        "nova-view": "https://unpkg.com/nova-view/dist/lib/nova-dom.min.js"
      }
    }
  </script>
</head>
<body>
  <div id="app"></div>
  <script type="module">
    import { Dom, ref, component, mount } from 'nova-view';

    const { div, button, h1 } = Dom;

    const Counter = component(() => {
      const count = ref(0);
      return div()(
        h1()(() => `Count: ${count.value}`),
        button().onclick(() => count.value++)('点击')
      );
    });

    mount(Counter(), document.getElementById('app'));
  </script>
</body>
</html>

🚀 核心概念与 API

1. 链式 API:不仅仅是语法糖

Nova View 的链式 API 背后的 DomElement 做了大量脏活累活:

  • 智能属性缓存:同值不重复写入 DOM,减少 Layout/Paint。
  • Style Diff:对象写法支持细粒度更新,动态样式自动解绑副作用。
  • Proxy 懒加载:只有访问到的属性才会创建 Setter。
import { Dom, ref } from 'nova-view';
const { div, span, button } = Dom;

const count = ref(0);

// 第一个括号:配置属性(支持静态值、Ref、函数)
div()
  .class('card')
  .style(() => ({ 
    color: count.value > 5 ? 'red' : 'black', // 动态样式
    padding: '1rem' 
  }))
// 第二个括号:添加内容(支持字符串、节点、数组、函数)
(
  span()('Current count: '),
  span()(() => count.value), // 细粒度更新 TextNode
  button().onclick(() => count.value++)('+')
);

2. 响应式系统:Signal 驱动

基于 track/trigger 的响应式系统,支持 batch 批量更新。

import { ref, computed, watchEffect, batch } from 'nova-view';

const count = ref(0);
const double = computed(() => count.value * 2);

watchEffect(() => {
  console.log(`Count: ${count.value}, Double: ${double.value}`);
});

// 批量更新:多次修改状态,只触发一次 DOM 刷新
batch(() => {
  count.value++;
  count.value++;
});

3. 组件:返回真实 Node

组件本质上就是返回 DOM 节点的函数。

import { Dom, component, onMounted } from 'nova-view';
const { div } = Dom;

const MyButton = component(() => {
  // 这里的代码只在组件初始化时运行一次
  const btn = document.createElement('button'); // 甚至可以混合原生 DOM
  
  onMounted(() => console.log('Mounted!'));

  return div()(btn); // 返回 Node
});

🎮 控制流与列表:性能直通车

Show / Switch:精确的 DOM 替换

利用 节点池 (Node Pool) 技术复用 Text/Comment 节点,减少高频切换时的 GC 压力。

Show(() => isLoading.value, {
  when: () => div()('Loading...'),
  fallback: () => div()('Content'),
  keepAlive: true // 可选:使用 display:none 切换而非移除 DOM
});

For:智能列表渲染

内置 LIS (最长递增子序列) 算法,确保最小化 DOM 移动。

For(() => items.value, {
  key: item => item.id,
  children: item => div()(item.name)
});

🚀 ForController:绕过 Diff 的极致性能

哪怕是最高效的 Diff 算法也有开销。在处理万级数据或高频实时更新(如股票K线、即时日志)时,你可以使用 controller 获得“上帝权限”:

let listController: ForController<Item> | null = null;

For(() => state.items, {
  key: it => it.id,
  children: (it) => ItemComponent(it),
  controller: (ctrl) => { listController = ctrl; }
});

// ❌ 传统方式:修改数据 -> 触发 Diff -> 更新 DOM
// list.value = newList;

// ✅ Nova 方式:直接操作 DOM 映射,0 Diff 开销
// 比 React 快 20%~80%(视操作密度而定)
batch(() => {
  // 直接更新特定项,不触发整个列表的重渲染
  listController?.updateItem(itemId, item => ({ ...item, price: newPrice }));
  
  // 复用现有 DOM 节点进行全量替换
  listController?.replaceAll(newItems, { reuseDOM: true });
});

这是 Nova View 区别于大多数 MVVM 框架的硬核能力:我们允许你为了性能“作弊”。


⚡ 性能基准 (Benchmark)

我们在 js-framework-benchmark 的标准场景下进行了测试。得益于无 Virtual DOM 开销精细的更新策略,Nova View 在多项指标上表现优异。

  • DOM 创建与替换:直接操作原生节点,无额外对象创建开销。
  • 内存占用:得益于 TextNode/CommentNode 对象池 技术,内存波动极低。
  • 列表更新
    • 常规模式 (For):自动 Diff,性能与主流框架持平。
    • 极速模式 (Controller):在特定场景下(如全部替换、定点更新),通过绕过 Diff 逻辑,比 React/Vue 快 20%~84%视具体操作密度而定,详见 bench 报告)。

注:性能数据基于 M1/M2 芯片测试环境,实际收益取决于业务场景。推荐在大量数据更新场景使用 Controller 模式。


📚 更多功能

  • VirtualList: 开箱即用的虚拟滚动组件,支持动态高度。
  • ErrorBoundary: 优雅处理组件树错误。
  • Context API: provide / inject 依赖注入。
  • 异步组件: 支持 async/await 组件定义。

🎯 使用场景

  • AI 辅助开发 - 语法简单,AI 生成代码准确率高。
  • 中小型 Web 应用 - 快速构建,打包体积极小。
  • 极致性能场景 - 大屏数据展示、高频实时更新列表。
  • 微前端 / 嵌入式 - 无需全家桶,轻松嵌入现有项目。

📚 深入指南(细节版)

你可以把上面当作“快速入口”。下面这一段是面向想把 Nova View 用到生产环境的人:把常见能力、边界和技巧一次讲透。

🎨 链式 API:双括号 ()() 的完整心智模型

Nova View 把 UI 写法拆成两个阶段:

  • 第一段 ():创建元素并“配置”(属性、事件、样式、class…)
  • 第二段 ():添加内容(子节点/字符串/函数/Ref…)
import { Dom } from 'nova-view';
const { div, button, span } = Dom;

div()                                    // 创建元素
  .class('container')                    // 配置:class / id / style / on*
  .id('app')
  .style({ padding: '1rem' })
  (                                       // 添加内容
    span()('Hello'),
    button().onclick(() => console.log('clicked'))('Click'),
  );

样式绑定:静态 / 混合 / 全响应式

import { Dom, ref } from 'nova-view';
const { div } = Dom;

// 1) 静态对象
div().style({ padding: '1rem', borderRadius: '8px' });

// 2) 混合:静态 + 响应式函数
const size = ref(16);
div().style({
  padding: '1rem',
  fontSize: () => `${size.value}px`,
  color: () => (size.value > 20 ? 'red' : '#111'),
});

// 3) 全响应式:函数返回对象
div().style(() => ({
  transform: `translateY(${size.value}px)`,
  opacity: size.value > 20 ? 0.8 : 1,
}));

响应式属性绑定:函数或 Ref

import { Dom, ref } from 'nova-view';
const { button } = Dom;

const count = ref(0);

button()
  .class(() => `btn ${count.value > 0 ? 'active' : ''}`)
  .disabled(() => count.value >= 10)
  .onclick(() => count.value++)
  (() => `Count: ${count.value}`);

嵌套结构:用代码保持层级清晰

import { Dom } from 'nova-view';
const { div, header, h1, nav, a, main, section } = Dom;

div().class('app')(
  header().class('header')(
    h1()('My App'),
    nav().class('nav')(
      a().href('/home')('Home'),
      a().href('/about')('About'),
    ),
  ),
  main().class('main')(
    section().class('content')('Content here'),
  ),
);

🎯 直接返回真实 DOM(这不是“抽象对象”)

Nova View 的元素就是浏览器原生节点,你可以直接调用任何 DOM API。

import { Dom, component, onMounted } from 'nova-view';
const { button } = Dom;

const EnhancedButton = component(() => {
  const el = button()
    .class('my-button')
    .onclick(() => console.log('clicked'))
    ('Click me');

  onMounted(() => {
    const rect = el.getBoundingClientRect();
    console.log('按钮位置:', rect.x, rect.y);
    el.style.transform = 'scale(1.05)';
  });

  return el;
});

⚡ 响应式系统:ref / reactive / computed / watch / batch

import { ref, reactive, computed, watch, watchEffect, batch } from 'nova-view';

const count = ref(0);
const user = reactive({ name: 'Alice', age: 25 });

const double = computed(() => count.value * 2);

watch(() => count.value, (n, o) => console.log('count:', o, '->', n));
watchEffect(() => console.log('double:', double.value));

batch(() => {
  user.name = 'Bob';
  count.value += 2;
});

🧩 组件系统(含插槽/事件/异步)

基础组件

import { Dom, ref, component, mount } from 'nova-view';
const { div, h1, button } = Dom;

const Counter = component(() => {
  const count = ref(0);
  return div().class('counter')(
    h1()(() => `Count: ${count.value}`),
    button().onclick(() => count.value++)('+'),
    button().onclick(() => count.value--)('-'),
  );
});

mount(Counter(), document.getElementById('app')!);

插槽(slots):默认插槽 / 具名插槽

import { Dom, component, type Slots } from 'nova-view';
const { div, button } = Dom;

const Card = component<{ title: string }>((props, { slots }) => {
  return div().class('card')(
    div().class('card-header')(props.title),
    div().class('card-body')(slots.default?.()),
    div().class('card-footer')(slots.footer?.()),
  );
});

const App = component(() =>
  Card({ title: '我的卡片' }, {
    default: () => div()('这是卡片内容'),
    footer: () => button()('确定'),
  })
);

作用域插槽:给插槽传数据

import { Dom, component } from 'nova-view';
const { div, span } = Dom;

type Item = { id: number; name: string; status: 'active' | 'inactive' };

const DataList = component<{ items: Item[] }>((props, { slots }) => {
  return div().class('data-list')(
    ...props.items.map(item =>
      slots.default?.(item) || div()(item.name)
    )
  );
});

const App = component(() => {
  const items: Item[] = [
    { id: 1, name: '项目1', status: 'active' },
    { id: 2, name: '项目2', status: 'inactive' },
  ];

  return DataList({ items }, {
    default: (item: Item) =>
      div().class(() => `item ${item.status}`)(
        span()(item.name),
        span().class('status')(item.status === 'active' ? '✓' : '✗'),
      ),
  });
});

自定义事件:emit + onXxx

import { Dom, component } from 'nova-view';
const { button } = Dom;

const CustomButton = component<{ variant?: 'primary' | 'secondary' }>((props, { emit }) => {
  return button()
    .class(() => `btn btn-${props.variant || 'primary'}`)
    .onclick(() => emit('click', { timestamp: Date.now() }))
    ('点击我');
});

const App = component(() =>
  CustomButton({ variant: 'primary' }, {
    onClick: (data) => console.log('按钮被点击', data),
  })
);

异步组件:直接 async 返回 Node

import { Dom, component } from 'nova-view';
const { div, h2, p } = Dom;

const AsyncData = component(async () => {
  const resp = await fetch('/api/data');
  const data = await resp.json();
  return div().class('data-view')(
    h2()('异步数据'),
    p()(data.message),
  );
});

注意:await 与生命周期/上下文的关系(务必看)
在当前实现下,async component 会在第一次 await 之后丢失“当前组件实例上下文”
这意味着:

  • await 之前调用 onMounted/onUpdated/onUnmounted/provide/inject 等“依赖当前实例”的 API 是可靠的
  • ⚠️ await 之后再调用这些 API,通常不会被注册到该组件实例上(表现为钩子不触发 / provide 不生效)

推荐写法:把钩子注册和 provide/inject 放在 await 之前;await 之后只做数据获取,然后更新 ref/reactive 来驱动 UI。


🧷 Hook 组件:把组件也做成链式 API

import { Dom, component, hook, mount } from 'nova-view';
const { div } = Dom;

const MyCard = component<{ title: string; active?: boolean }>((props) => {
  return div().class(() => `card ${props.active ? 'active' : ''}`)(
    div().class('title')(props.title),
  );
});

const Card = hook(MyCard);
mount(Card().title('Hello').active(true)(), document.body);

🧠 条件渲染 / 多分支 / 列表渲染(更完整示例)

Show:支持 keepAlive

import { Dom, ref, Show } from 'nova-view';
const { div } = Dom;

const isLoading = ref(true);

Show(() => isLoading.value, {
  when: () => div()('加载中...'),
  fallback: () => div()('内容'),
  keepAlive: true,
});

Switch:多分支更直观

import { Dom, ref, Switch } from 'nova-view';
const { div } = Dom;

const status = ref<'loading' | 'error' | 'success'>('loading');

Switch(() => status.value, [
  [() => status.value === 'loading', () => div()('加载中')],
  [() => status.value === 'error', () => div()('出错了!')],
  [true, () => div()('成功')],
]);

For:keyed 列表 + controller

import { Dom, ref, For, type ForController } from 'nova-view';
const { div } = Dom;

type Row = { id: number; name: string };
const items = ref<Row[]>([
  { id: 1, name: 'A' },
  { id: 2, name: 'B' },
]);

let controller: ForController<Row> | null = null;

For(() => items.value, {
  key: (it) => it.id,
  children: (it) => div().class('row')(it.name),
  controller: (ctrl) => { controller = ctrl; },
});

// controller?.updateItem(1, (it) => ({ ...it, name: 'A+' }));
// controller?.removeItem(2);
// controller?.replaceAll(newItems, { reuseDOM: true });

🧱 VirtualList:虚拟滚动(配置说明)

适合超大列表,只渲染可视区内容,保持滚动流畅。

核心属性:

  • items() => T[] 列表数据(必填)
  • height:容器高度(必填)
  • itemHeight:固定高度或 (item, index) => number
  • overscan:额外渲染的行数,减少闪烁
  • key:为每行生成 key
  • children:渲染函数(必填)
  • onReachEnd / threshold:触底加载

🛡️ ErrorBoundary:错误边界(配置说明)

捕获子组件树中的错误,避免整个应用崩溃。

核心属性:

  • children() => Node(必填)
  • fallback(error, reset) => Node(可选)
  • onError:错误上报回调(可选)

💡 最佳实践(更贴近真实项目)

  • 批量更新:高频更新时优先用 batch() 合并刷新
  • 列表性能:常规用 For;明确知道更新路径/追求极限时用 ForController
  • KeepAliveShow(..., { keepAlive: true }) 适合昂贵组件的频繁切换
  • 组织方式:把业务逻辑抽到组合函数(useXxx),UI 保持纯粹

🚚 迁移指南(从 React / Vue / 原生)

迁移的核心不是“语法替换”,而是心智转换:

  • 从 ReactuseState -> ref / reactiveuseEffect -> watchEffectref + useEffect 才能拿 DOM -> 直接返回 DOM
  • 从 Vueref/reactive/computed/watch 基本同构;模板 -> 链式 DSL
  • 从原生:命令式创建 -> 声明式组合;事件监听 -> .onclick/.oninput...

📖 文档与资源

🤝 贡献

欢迎提交 Issue 和 Pull Request!我们非常欢迎社区的贡献。

📄 许可证

MIT


Made with ❤️ by the Nova View team

官网 | npm | GitHub

About

轻量级别的纯运行时前端构建库

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors