Skip to content

Jiess 节点创建 ​

目录 ​

this.add() 是 Jiess 创建节点的统一入口,在 Vue 3、React 和 Web Components 中语法完全一致。支持文本、HTML 元素、第三方组件和 Jiess 自定义组件,搭配响应式数据驱动、条件/列表渲染,以纯配置对象描述整个 UI。

核心 API:this.add() ​

typescript
this.add(...nodes: (string | number | boolean | NodeConfig | Array<NodeConfig>)[]): this

返回 this,支持链式调用。配置采用扁平化设计——想给元素加什么属性,直接在对象中平铺即可。

基本用法 ​

javascript
function setup () {
  this.add({
    is: 'span',
    className: 'text-red-500',
    children: 'Hello, Jiess!',
    onClick: e => {
      e.stopPropagation()
      console.log('点击事件触发了!')
    },
    style: {
      color: 'red',
      fontSize: '16px'
    }
  })
}

省略 is 字段:当 is 省略时,框架自动使用 'div' 作为默认标签。因此 { children: '...' } 等价于 { is: 'div', children: '...' },在 div 容器场景下可省略 is 字段使代码更简洁。

配置项速查 ​

配置项类型必填默认值描述
isstring|Component否'div'标签名或组件
className / classstring|string[]否—CSS 类名,className 和 class 均直接设置(Shadow DOM 提供天然隔离,无需作用域前缀)
childrenstring|number|boolean|NodeConfig|NodeConfig[]否—子节点内容
styleobject否—内联样式,驼峰命名(如 fontSize)
keystring|number否—唯一标识,优化渲染性能
onClick / onInput 等function否—事件监听,前缀 on + 首字母大写事件名
$ifboolean|ReactiveValue否true条件渲染开关
$elseIfboolean|ReactiveValue否—条件渲染 else-if 分支
$refReactiveRef否—获取元素/组件引用
$slotsobject否—组件插槽内容
$modelReactiveRef否—表单双向绑定
$proxyReactiveValue否—响应式数据驱动属性
$attrsobject否—额外透传属性
$propsobject否—组件 props

基础节点创建 ​

1. 文本节点 ​

直接传入字符串,不包裹任何标签:

javascript
function setup () {
  this.add('Hello, Jiess!')
}

2. 数据渲染规则 ​

javascript
function setup () {
  this.add('字符串会显示')
  this.add(123)      // 数字
  this.add(0)        // 零
  this.add(true)     // 布尔值 true
  this.add(false)    // 布尔值 false

  this.add([])       // 空数组——忽略
  this.add(null)     // null——忽略
  this.add(NaN)      // NaN——忽略
  this.add(undefined)// undefined——忽略
}

3. 默认 div ​

不指定 is 时默认创建 div:

javascript
function setup () {
  this.add({
    children: '这是一个默认的 div 元素'
  })
}

4. 自定义标签 ​

javascript
function setup () {
  this.add({
    is: 'p',
    className: 'text-center',
    children: '这是一个段落标签'
  })
  this.add({
    is: 'button',
    children: '点击我',
    onClick: () => alert('按钮被点击了!')
  })
}

5. 类名设置 ​

className 和 class 均直接设置类名(Shadow DOM 提供天然隔离,无需作用域前缀),值可以是字符串或数组:

javascript
function setup () {
  // 字符串
  this.add({
    className: 'text-red-500 font-bold',
    children: '红色粗体文本'
  })
  // class 别名(同样直接设置,无作用域前缀)
  this.add({
    class: 'text-blue-500',
    children: '蓝色文本'
  })
  // 数组形式——适合动态组合
  this.add({
    className: ['bg-gray-100', 'p-4', 'rounded'],
    children: '带背景和边距的元素'
  })
  // 运行时动态类名
  const isActive = true
  this.add({
    className: ['base-class', isActive ? 'active' : 'inactive'],
    children: '动态类名示例'
  })
}

6. 多元素创建与链式调用 ​

javascript
function setup () {
  // 一次传入多个
  this.add(
    { is: 'h1', children: '标题' },
    { is: 'p', children: '段落内容' },
    { is: 'button', children: '按钮' }
  )
}

// 链式调用
function setup () {
  this.add({ is: 'h2', children: '链式添加示例' })
    .add({ is: 'p', children: '第一段落' })
    .add({ is: 'p', children: '第二段落' })
}

7. 节点嵌套 ​

通过 children 数组构建任意深度嵌套:

javascript
function setup () {
  this.add({
    className: 'card',
    children: [
      { className: 'card-header', children: '卡片标题' },
      { className: 'card-body', children: '卡片内容' },
      { className: 'card-footer', children: '卡片页脚' }
    ]
  })
}

8. 多维数组自动展平 ​

Jiess 自动展平多维数组为单层:

javascript
function setup () {
  this.add([
    [{ is: 'h3', children: '层级1' }],
    [[{ is: 'p', children: '层级2' }]]
  ])

  this.add({
    is: 'div',
    children: [[['Hello', ' ', 'Jiess!']]]
  })

  this.add([
    '文本节点',
    { is: 'br' },
    '另一个文本节点'
  ])
}

9. 静态属性 ​

所有 HTML 属性(含 data-*)直接写在配置对象中:

javascript
function setup () {
  this.add({
    is: 'a',
    id: 'my-link',
    href: 'https://github.com',
    target: '_blank',
    'data-type': 'external',
    className: 'text-blue-500',
    children: '访问 GitHub'
  })

  this.add({
    is: 'input',
    type: 'text',
    placeholder: '请输入内容',
    disabled: false,
    required: true,
    defaultValue: '默认值'
  })
}

10. 样式设置 ​

支持驼峰命名和内联样式:

javascript
function setup () {
  this.add({
    is: 'div',
    style: {
      padding: '16px',
      margin: '8px',
      backgroundColor: '#f5f5f5',
      borderRadius: '8px',
      fontSize: '14px',
      lineHeight: '1.5'
    },
    children: '带样式的元素'
  })
}

11. $ref 获取 DOM 引用 ​

javascript
import { $ref } from '@jiess/core'

function setup () {
  const myDiv = $ref(null)

  this.add({
    is: 'div',
    $ref: myDiv,
    children: '可被引用的 DIV'
  })

  // myDiv.value 即为 DOM 元素
  console.log(myDiv.value)
}

Three.js 3D 场景与 Konva 2D 场景(v6.0+) ​

除了把配置渲染成网页 DOM,Jiess 还能把 Three.js 场景图和 Konva 画布当作渲染环境——同一套 this.add() 语法,声明式搭建 3D 或 2D 场景,响应式数据驱动属性级更新。

Three.js 3D 场景 ​

三步上手:

  1. 引入 Three.js 命名空间,注册环境(@jiess/core/three)
  2. 在组件中 this.add({ is: 'threeCanvas', ... }) 声明根场景
  3. 在 children 里按标签声明 3D 元素(几何体、材质、灯光、相机)
javascript
import * as THREE from 'three'
import { $entry, $ref } from '@jiess/core'
import JiessEnv from '@jiess/core/three'

// ① 注册环境:把 THREE 命名空间作为第二个参数传入
const jiess = $entry(JiessEnv, THREE, {})
const { JiessComponent } = jiess

// ② 调用 JiessComponent 创建组件,返回容器 div,塞进页面即可
const container = JiessComponent({
  setup() {
    const color = $ref('#4f8cff')

    this.add({
      is: 'threeCanvas',              // 根标签:自动创建 renderer + scene + camera + 渲染循环
      style: { width: '720px', height: '520px' },
      camera: {                       // 可选:自定义相机(缺省为透视相机 perspectiveCamera,位于 (0,0,5) 看向原点;is 可省略)
        args: [75, 720 / 520, 0.1, 1000],
        position: [0, 2.2, 6],
      },
      children: [
        { is: 'ambientLight', intensity: 0.6 },
        { is: 'directionalLight', position: [3, 5, 4], intensity: 1.2 },
        {
          is: 'mesh', key: 'cube',    // key:相同 key 只做属性级更新(复用实例,省 GPU 资源)
          position: [0, 1, 0],
          onClick: () => {            // 点击事件:Raycaster 自动拾取派发
            color.value = '#' + Math.floor(Math.random() * 0xffffff).toString(16).padStart(6, '0')
          },
          children: [
            { is: 'boxGeometry', args: [1, 1, 1] },                    // 几何体
            { is: 'meshStandardMaterial',                             // 材质(响应式绑定颜色)
              color: { $proxy: color, field: 'value', done: v => v } },
          ],
        },
      ],
    })
  },
})
document.getElementById('root').appendChild(container)

常用标签(is 字符串 → Three.js 类):

类别标签(is)
场景/容器threeCanvas(根)、scene、group、mesh、points、line、sprite
几何体boxGeometry、sphereGeometry、planeGeometry、cylinderGeometry、coneGeometry、torusGeometry、circleGeometry 等
材质meshBasicMaterial、meshStandardMaterial、meshPhongMaterial、meshLambertMaterial、meshNormalMaterial 等
灯光ambientLight、directionalLight、pointLight、spotLight、hemisphereLight
相机perspectiveCamera、orthographicCamera、cubeCamera
辅助axesHelper、gridHelper、arrowHelper

关键点:

  • 容器尺寸:threeCanvas 的 style.width/height 支持 px、百分比(100%)、vw/vh;容器尺寸变化时(ResizeObserver)自动重设渲染器尺寸并同步相机宽高比,无需手动处理
  • 相机热更新:forceUpdate 时 camera 配置的 is(类型切换,如透视↔正交)或 args(fov/宽高比等构造参数)变化会自动重建相机;fov 等投影属性变化自动刷新投影矩阵
  • 响应式更新:$proxy 绑定属性 → 数据变化只更新对应属性,不重建对象;args 构造参数变化才销毁重建(几何尺寸等无法局部更新)
  • 点击事件:onClick / onPointerMove / onPointerDown / onPointerUp 通过 Raycaster 拾取自动派发
  • 扩展对象:非场景图对象(如 OrbitControls)用 registerTag(name, cls, { isExtension: true }) 注册,在 ref 内手动安装
  • 资源释放:组件销毁(frame.dispose())时自动释放 GPU 资源(几何体/材质/贴图);共享 geometry/material 采用引用计数,引用归零才释放
  • 材质数组:mesh 的 children 中声明多个材质时,自动挂载为 material 数组(配合几何体 groups);几何体仅取第一个并告警

Konva 2D 场景 ​

与 Three.js 同理,Konva 适配器把 Canvas 2D 画布作为渲染环境:

javascript
import Konva from 'konva'
import { $entry, $ref, $reactive } from '@jiess/core'
import JiessKonva from '@jiess/core/konva'

// ① 注册环境:把 Konva 命名空间作为第二个参数传入
const jiess = $entry(JiessKonva, Konva, {})
const { JiessComponent } = jiess

const container = JiessComponent({
  setup() {
    const pos = $reactive({ x: 100, y: 80 })

    this.add({
      is: 'konvaStage',                        // 根标签:Stage + Layer + 自动重绘
      style: { width: '720px', height: '520px' },
      children: [
        { is: 'rect', x: 0, y: 0, width: 720, height: 520, fill: '#1a1d29' },  // 背景
        {
          is: 'rect', key: 'main-rect',        // key:相同 key 复用节点,只更新属性
          x: { $proxy: pos, field: 'x', done: v => v },   // 响应式:数据变化 → setAttrs 属性级更新
          y: { $proxy: pos, field: 'y', done: v => v },
          width: 100, height: 100, fill: '#4f8cff', stroke: '#fff', strokeWidth: 2,
          onClick: () => { pos.x = Math.random() * 500 },  // Konva 内置 hit testing,无需手动绑定
        },
        { is: 'text', x: 20, y: 10, text: 'Konva × Jiess', fontSize: 20, fill: '#fff' },
      ],
    })
  },
})
document.getElementById('root').appendChild(container)

常用标签(18 种内置映射):根标签为 konvaStage;stage/layer/group;rect/circle/ellipse/line/text/image/path/arc/ring/star/wedge;label/regularPolygon/transformer/arrow。

关键点:

  • 响应式更新:$proxy 绑定属性 → 只更新属性(setAttrs),key 相同的节点复用不重建
  • 事件:onClick/onMouseEnter 等直接映射 Konva 事件(on + 事件名首字母大写),无需 Raycaster;配置是事件的唯一来源——从配置移除 onXxx 后对应监听器自动解绑,不残留
  • 自定义标签:registerTag('myShape', MyKonvaClass) 注册后即可用 is: 'myShape'
  • 尺寸:style 宽高即 Stage 尺寸,支持 px、百分比(100%)、vw/vh;容器尺寸变化时(ResizeObserver)自动同步 Stage 并重绘

更多可运行示例见仓库 example-base/three 与 example-base/konva 目录(含 Vue3 共存、鼠标交互、小游戏等 demo)。

事件绑定 ​

事件以 on 为前缀,后接首字母大写的事件名。

基础事件 ​

javascript
function setup () {
  this.add({
    is: 'button',
    children: '点击我',
    onClick: e => {
      e.stopPropagation()
      console.log('按钮被点击了!')
    },
    onMouseEnter: () => console.log('鼠标移入'),
    onMouseLeave: () => console.log('鼠标移出')
  })
}

表单双向绑定——$model ​

$model 简化 onInput + value 组合:

javascript
import { $ref } from '@jiess/core'

function setup () {
  const username = $ref('')
  const email = $ref('')

  this.add({
    is: 'div',
    className: 'form',
    children: [
      {
        is: 'input',
        type: 'text',
        placeholder: '请输入用户名',
        $model: username
      },
      {
        is: 'input',
        type: 'email',
        placeholder: '请输入邮箱',
        $model: email
      },
      {
        is: 'button',
        type: 'submit',
        children: '提交',
        onClick: () => console.log('表单提交', { username: username.value, email: email.value })
      }
    ]
  })
}

手动表单绑定 ​

如果不使用 $model,可手动绑定 value + onInput:

javascript
import { $ref } from '@jiess/core'

function setup () {
  const username = $ref('')

  this.add({
    is: 'input',
    type: 'text',
    placeholder: '请输入用户名',
    value: username.value,
    onInput: e => { username.value = e.target.value }
  })
}

事件修饰符说明 ​

Jiess 不提供 .stop、.prevent 等修饰符语法,在回调中手动调用:

javascript
function setup () {
  this.add({
    is: 'a',
    href: '#',
    children: '阻止默认行为',
    onClick: e => {
      e.preventDefault()
      e.stopPropagation()
      console.log('已阻止默认行为和冒泡')
    }
  })
}

自定义事件 ​

javascript
function setup () {
  this.add({
    is: 'div',
    children: '自定义事件示例',
    onCustomEvent: (detail) => {
      console.log('自定义事件触发', detail)
    }
  })
}

使用组件 ​

is 可传入 React 组件、Vue 3 组件、Web Components 组件或 Jiess 组件。

React 组件(Ant Design) ​

javascript
import { Button, Input, List } from 'antd'
import { $reactive } from '@jiess/core'

function setup () {
  const items = $reactive([
    { id: 1, title: 'Item 1' },
    { id: 2, title: 'Item 2' },
    { id: 3, title: 'Item 3' }
  ])

  this.add({
    is: Button,
    size: 'small',
    type: 'primary',
    children: 'Ant Design 按钮',
    onClick: () => console.log('React 按钮被点击了!')
  })

  this.add({
    is: Input,
    placeholder: 'Ant Design Input',
    style: { width: 200, margin: '0 10px' }
  })

  this.add({
    is: List,
    dataSource: items,
    renderItem: (item) => ({
      is: List.Item,
      children: item.title
    })
  })
}

Web Components 组件 ​

使用原生 Web Components 或第三方 Web Components 组件库:

javascript
import { $ref } from '@jiess/core'

// 使用原生 Web Components 自定义元素
class MyInput extends HTMLElement {
  constructor() {
    super()
    const shadow = this.attachShadow({ mode: 'open' })
    shadow.innerHTML = `<input type="text" />`
  }
}
customElements.define('my-input', MyInput)

function setup() {
  const value = $ref('')
  this.add({
    is: 'my-input',
    $proxy: { value, update: true },
    onInput: (e) => { value.value = e.target.value }
  })
}

Jiess 适配器说明:Jiess 自身的 Web Components 适配器统一使用原生 attachShadow({ mode: 'open' }) API 实现 Shadow DOM 隔离,不使用 customElements.define,也不依赖任何第三方 Shadow DOM 库(如 react-shadow)。上例中的 customElements.define 是用户侧注册自定义元素的代码,与 Jiess 内部适配机制无关——Jiess 仅在渲染 Jiess 组件时通过原生 attachShadow 创建 Shadow Root 并完成样式分流。

第三方组件双向绑定 ​

与 Ant Design、Element Plus 配合使用 $proxy + update:

javascript
import { Input, Button } from 'antd'
import { $ref } from '@jiess/core'

function setup () {
  const value = $ref('初始内容')

  this.add({
    is: Input,
    placeholder: '使用 Ant Design Input 组件',
    value: { $proxy: value, update: true },
    onPressEnter: () => console.log('按下回车键,当前值:', value.value)
  })

  this.add({
    is: Button,
    type: 'primary',
    children: 'Ant Design 按钮',
    onClick: () => console.log('Ant Design 按钮被点击了!')
  })
}

Web Components 提示:与 Web Components 组件库配合时,使用原生事件名(如 onInput、onChange)进行绑定,$proxy + update 机制同样适用。

Jiess 自定义组件 ​

javascript
const CardComponent = {
  isJiess: true,
  name: 'CardComponent',
  props () {
    return {
      title: { type: String, default: '默认标题' },
      content: { type: String, default: '默认内容' },
      footer: { type: String, default: '' }
    }
  },
  setup (props) {
    this.add({
      is: 'div',
      className: 'card',
      children: [
        { is: 'h3', children: props.title },
        { is: 'p', children: props.content },
        props.footer ? { is: 'div', className: 'card-footer', children: props.footer } : null
      ]
    })
  },
  style: `
    .card { border: 1px solid #e0e0e0; padding: 16px; border-radius: 8px; }
    .card-footer { margin-top: 12px; padding-top: 12px; border-top: 1px solid #f0f0f0; }
  `
}

function setup () {
  this.add({
    is: CardComponent,
    title: '自定义卡片',
    content: '这是卡片的内容',
    footer: '卡片页脚'
  })
}

组件传参 ​

javascript
import { $ref } from '@jiess/core'

const UserProfile = {
  isJiess: true,
  name: 'UserProfile',
  props () {
    return {
      user: { type: Object, default: () => ({}) },
      showDetails: { type: Boolean, default: false }
    }
  },
  setup (props) {
    this.add({
      is: 'div',
      className: 'user-profile',
      children: [
        { is: 'h3', children: props.user.name || '未知用户' },
        { is: 'p', children: `邮箱:${props.user.email || '未设置'}` },
        props.showDetails && props.user.age
          ? { is: 'p', children: `年龄:${props.user.age}` }
          : null
      ]
    })
  },
  style: `
    .user-profile { border: 1px solid #e0e0e0; padding: 16px; border-radius: 8px; }
  `
}

function setup () {
  const user = $ref({
    name: '张三',
    email: 'zhangsan@example.com',
    age: 25
  })
  const showDetails = $ref(true)

  this.add({
    is: UserProfile,
    user: user.value,
    showDetails: showDetails.value
  })
}

插槽系统 ​

通过 $slots 传递插槽内容,支持字符串、对象、函数三种形式。

默认插槽 ​

javascript
function setup () {
  this.add({
    is: 'div',
    $slots: {
      default: '这是默认插槽内容'
    }
  })
}

Vue 3 组件插槽(以 Vant Field 为例) ​

javascript
import { Field } from 'vant'

function setup () {
  this.add({
    is: Field,
    $slots: {
      label: '用户名',
      'left-icon': {
        is: 'span',
        className: 'icon-user',
        children: '👤'
      },
      'right-icon': () => ({
        is: 'span',
        className: 'icon-info',
        children: 'ℹ️'
      }),
      'error-message': (prop) => ({
        is: 'div',
        className: 'error',
        children: `错误信息:${prop.message || '输入格式有误'}`
      })
    }
  })
}

Web Components 组件插槽 ​

Web Components 使用 Shadow DOM 的 <slot> 元素实现插槽。Jiess 的 Web Components 适配器通过原生 attachShadow({ mode: 'open' }) 创建 Shadow Root(不使用 customElements.define,不依赖第三方 Shadow DOM 库),<slot> 元素在 Shadow Root 内正常工作:

javascript
function setup() {
  this.add({
    is: 'my-card',
    $slots: {
      default: [{ is: 'p', children: '卡片内容' }],
      header: [{ is: 'h3', children: '卡片标题' }]
    }
  })
}

Jiess 组件插槽 ​

Jiess 组件在 setup 第二个参数中获取 slots:

javascript
const LayoutComponent = {
  isJiess: true,
  name: 'LayoutComponent',
  setup (props, { slots }) {
    this.add({
      is: 'div',
      className: 'layout',
      children: [
        {
          is: 'header',
          className: 'layout-header',
          children: slots.header ? slots.header() : '默认头部'
        },
        {
          is: 'aside',
          className: 'layout-aside',
          children: slots.aside ? slots.aside() : '默认侧边栏'
        },
        {
          is: 'main',
          className: 'layout-main',
          children: slots.main
            ? slots.main({ title: '主内容区域', time: new Date().toLocaleString() })
            : '默认内容'
        },
        {
          is: 'footer',
          className: 'layout-footer',
          children: slots.footer ? slots.footer() : '默认底部'
        }
      ]
    })
  },
  style: `
    .layout { display: grid; grid-template-areas: "header header" "aside main" "footer footer"; gap: 16px; }
    .layout-header { grid-area: header; background: #f0f0f0; padding: 16px; }
    .layout-aside { grid-area: aside; background: #fafafa; padding: 16px; width: 200px; }
    .layout-main { grid-area: main; background: #ffffff; padding: 16px; }
    .layout-footer { grid-area: footer; background: #f0f0f0; padding: 16px; text-align: center; }
  `
}

function setup () {
  this.add({
    is: LayoutComponent,
    $slots: {
      header: '我的应用',
      aside: () => ({
        is: 'ul',
        children: [
          { is: 'li', children: '首页' },
          { is: 'li', children: '关于我们' },
          { is: 'li', children: '联系我们' }
        ]
      }),
      main: (scope) => ({
        is: 'div',
        children: [
          { is: 'h2', children: scope.title },
          { is: 'p', children: `当前时间:${scope.time}` },
          { is: 'p', children: '这是应用的主内容区域' }
        ]
      }),
      footer: '© 2024 我的应用'
    }
  })
}

动态插槽 ​

根据运行时条件决定是否提供插槽:

javascript
function setup () {
  const hasExtra = true

  this.add({
    is: 'div',
    $slots: {
      default: '默认内容',
      ...(hasExtra ? { extra: '额外内容' } : {})
    }
  })
}

插槽要点 ​

  • 默认插槽:键名为 default
  • 具名插槽:不同名称区分多个插槽
  • 作用域插槽:函数形式接收组件回传数据
  • 动态插槽:根据条件动态决定是否提供

条件渲染 ​

$if / $elseIf 控制节点是否渲染,条件为 false 时不产生 DOM。

核心原理:$if 本质就是标准的 $proxy 响应式绑定(详见响应式绑定 § $proxy)。$ref 自动读取 .value,真值→显示、假值→隐藏(DOM 中移除)。done 可选,仅在需要转换返回值时使用。

静态条件 ​

javascript
function setup () {
  this.add({ $if: true, children: '这个节点会显示' })
  this.add({ $if: false, children: '这个节点不会显示' })
}

响应式条件——$ref 本身就是 boolean(无需 done) ​

当绑定的 $ref 本身就是 boolean 时,直接用 $proxy,不需要 done:

javascript
import { $ref } from '@jiess/core'

function setup () {
  const showContent = $ref(true)

  this.add({
    is: 'div',
    children: [
      {
        $if: { $proxy: showContent },   // $ref 自动读取 .value,true→显示
        is: 'div',
        className: 'content',
        children: '这是可以动态显示/隐藏的内容'
      },
      {
        is: 'button',
        children: showContent.value ? '隐藏内容' : '显示内容',
        onClick: () => { showContent.value = !showContent.value }
      }
    ]
  })
}

响应式条件——非 boolean 值用 done 转换 ​

当绑定的值不是 boolean(如对象、数字)时,用 done 将其转换为 boolean:

javascript
import { $ref } from '@jiess/core'

function setup () {
  const currentNode = $ref(null)    // 树节点对象

  function isRootNode(node) {
    return node && (!node.parentId || node.parentId === 0 || node.parentId === '0')
  }

  this.add({
    is: 'div',
    children: [
      {
        $if: { $proxy: currentNode, done: v => !!v },
        children: { $proxy: currentNode, done: v => `当前节点:${v?.name}` }
      },
      {
        $if: { $proxy: currentNode, done: v => !isRootNode(v) },
        is: 'button',
        type: 'danger',
        children: '删除节点',
      },
    ]
  })
}

$elseIf 配合使用 ​

javascript
import { $ref } from '@jiess/core'

function setup () {
  const user = $ref(null)

  this.add({
    is: 'div',
    children: [
      {
        $if: { $proxy: user },
        children: { $proxy: user, done: (u) => `欢迎,${u?.name}` }
      },
      {
        $elseIf: true,
        children: '请登录'
      }
    ]
  })
  // user.value = { name: '张三' } => 显示欢迎信息
  // user.value = null => 显示"请登录"
}

⚠️ 注意:$if: user.value 仅取 setup 时的静态快照,不会随 ref 变化更新。务必使用 $proxy 或 $computed 来实现真正的响应式条件。

$elseIf 多分支 ​

分支按 $if → $elseIf → $elseIf 顺序排列,末尾用 $elseIf: true 充当 $else:

javascript
import { $ref, $computed } from '@jiess/core'

function setup () {
  const status = $ref('success')

  const statusConfig = $computed(() => {
    switch (status.value) {
      case 'success': return { className: 'status success', text: '✅ 操作成功' }
      case 'warning': return { className: 'status warning', text: '⚠️ 警告信息' }
      case 'error': return { className: 'status error', text: '❌ 错误信息' }
      default: return { className: 'status info', text: 'ℹ️ 提示信息' }
    }
  })

  this.add({
    is: 'div',
    className: 'status-indicator',
    children: [
      {
        is: 'div',
        className: { $proxy: statusConfig, done: cfg => cfg.className },
        children: { $proxy: statusConfig, done: cfg => cfg.text }
      },
      {
        is: 'div',
        className: 'buttons',
        children: [
          { is: 'button', children: '成功', onClick: () => { status.value = 'success' } },
          { is: 'button', children: '警告', onClick: () => { status.value = 'warning' } },
          { is: 'button', children: '错误', onClick: () => { status.value = 'error' } },
          { is: 'button', children: '信息', onClick: () => { status.value = 'info' } }
        ]
      }
    ]
  })
}

实际应用——登录状态切换 ​

javascript
import { $ref, $computed } from '@jiess/core'

function setup () {
  const user = $ref(null)
  const isLoggedIn = $computed(() => !!user.value)

  const toggleLogin = () => {
    user.value = user.value ? null : { name: '张三', role: 'admin' }
  }

  this.add({
    is: 'div',
    className: 'user-info',
    children: [
      {
        $if: isLoggedIn,
        children: [
          { is: 'div', children: { $proxy: user, done: (u) => `欢迎,${u.name} (${u.role})` } },
          { is: 'button', children: '退出登录', onClick: toggleLogin }
        ]
      },
      {
        $elseIf: true,
        children: [
          { is: 'div', children: '您还未登录' },
          { is: 'button', children: '登录', onClick: toggleLogin }
        ]
      }
    ]
  })
}

注意事项 ​

  • $if 和 $elseIf 必须在同一个 add 调用中按序排列
  • 条件不满足时不产生 DOM,比 display:none 性能更优

列表渲染 ​

使用 map 将数组转换为节点配置数组。必须为每个列表项提供唯一的 key,确保高效更新。

基本列表 ​

javascript
function setup () {
  const items = ['苹果', '香蕉', '橙子', '葡萄']

  this.add({
    is: 'ul',
    children: items.map((item) => ({
      is: 'li',
      key: item,
      children: item
    }))
  })
}

响应式列表 ​

javascript
import { $reactive, $computed } from '@jiess/core'

function setup () {
  const fruits = $reactive([
    { id: 1, name: '苹果', price: 10 },
    { id: 2, name: '香蕉', price: 5 },
    { id: 3, name: '橙子', price: 8 }
  ])

  const fruitList = $computed(() => fruits.map(fruit => ({
    is: 'li',
    key: fruit.id,
    children: `${fruit.name} - ¥${fruit.price}`
  })))

  const addFruit = () => {
    fruits.push({
      id: Date.now(),
      name: '新水果',
      price: Math.floor(Math.random() * 20) + 1
    })
  }

  this.add({
    is: 'div',
    children: [
      {
        is: 'ul',
        children: fruitList
      },
      {
        is: 'button',
        children: '添加水果',
        onClick: addFruit
      }
    ]
  })
}

响应式绑定 $proxy ​

$proxy 将响应式数据绑定到节点属性,数据变化时自动更新视图。

动态属性 ​

javascript
import { $ref } from '@jiess/core'

function setup () {
  const isDisabled = $ref(false)

  this.add({
    is: 'button',
    disabled: { $proxy: isDisabled },
    children: '动态禁用按钮',
    onClick: () => {
      isDisabled.value = true
      setTimeout(() => { isDisabled.value = false }, 2000)
    }
  })
}

动态样式 ​

javascript
import { $ref } from '@jiess/core'

function setup () {
  const fontSize = $ref(16)

  this.add({
    is: 'div',
    style: {
      fontSize: { $proxy: fontSize, done: (size) => `${size}px` },
      color: 'blue'
    },
    children: '动态字体大小'
  })

  const timer = setInterval(() => {
    fontSize.value += 1
    if (fontSize.value > 32) fontSize.value = 16
  }, 1000)

  this.onBeforeUnmount(() => clearInterval(timer))
}

动态内容——$proxy + done ​

javascript
import { $ref, $reactive } from '@jiess/core'

function setup () {
  const count = $ref(0)
  const user = $reactive({ name: '张三', role: 'admin' })

  this.add({
    is: 'div',
    children: [
      {
        is: 'p',
        children: { $proxy: count, done: (val) => `点击次数:${val}` }
      },
      {
        is: 'button',
        children: '点击+1',
        onClick: () => { count.value++ }
      },
      {
        is: 'p',
        children: { $proxy: user, done: (u) => `${u.name} - ${u.role}` }
      }
    ]
  })
}

children 数组中的 $proxy 子节点绑定 ​

当子节点配置对象中直接使用 $proxy + field + done 时,done 可返回新的节点配置,实现子节点动态重建。常用于父子组件级联场景。

javascript
import { $reactive } from '@jiess/core'

function setup() {
    const state = $reactive({ active: true, count: 0 })

    this.add({
        is: 'div',
        children: [
            { is: 'p', children: { $proxy: state, field: 'count', done: (v) => `计数: ${v}` } },
            // ★ 子节点配置中直接绑定 $proxy
            {
                is: 'span',
                $proxy: state,
                field: 'active',
                done: (active) => ({
                    is: 'span',
                    class: active ? 'stat-ok' : 'stat-err',
                    children: active ? '运行中' : '已停止'
                })
            },
        ]
    })
}

$proxy 配合 $if ​

javascript
import { $ref } from '@jiess/core'

function setup () {
  const userRole = $ref('guest')

  this.add({
    is: 'div',
    children: [
      {
        $if: { $proxy: userRole, done: (role) => role === 'admin' },
        is: 'button',
        children: '管理面板',
        onClick: () => console.log('进入管理面板')
      },
      {
        $if: { $proxy: userRole, done: (role) => role === 'user' },
        is: 'button',
        children: '用户中心',
        onClick: () => console.log('进入用户中心')
      },
      {
        $if: { $proxy: userRole, done: (role) => role === 'guest' },
        is: 'button',
        children: '登录',
        onClick: () => console.log('请登录')
      }
    ]
  })
}

$proxy 配置字段 ​

$proxy 可以是简单引用(直接取值),也可以是包含以下字段的对象:

字段类型描述
$proxyReactiveValue必填,绑定的响应式数据源
fieldstring可选,指定读取对象中的字段
done(value) => any可选,对值进行转换后输出
updateboolean可选,设为 true 时启用双向更新

$proxy 与 field 配合 ​

javascript
import { $ref } from '@jiess/core'

function setup () {
  const user = $ref({ name: '张三', role: 'admin' })

  this.add({
    is: 'div',
    children: [
      { is: 'p', children: { $proxy: user, field: 'name' } },
      { is: 'p', children: { $proxy: user, field: 'role' } }
    ]
  })
}

$proxy 双向绑定第三方组件(update) ​

javascript
import { Input } from 'antd'
import { $ref } from '@jiess/core'

function setup () {
  const inputValue = $ref('')

  this.add({
    is: Input,
    value: { $proxy: inputValue, update: true },
    placeholder: '请输入内容'
  })
}
javascript
// Web Components 组件双向绑定
function setup() {
  const value = $ref('')
  this.add({
    is: 'my-input',
    $proxy: { value, update: true },
    onInput: (e) => { value.value = e.target.value }
  })
}

静态 JsRender 实例 ​

对固定不变的节点,通过 _static_ 前缀的 key 将 JsRender 实例缓存复用:

javascript
function setup () {
  this.add({
    is: 'header',
    key: '_static_header',
    children: '网站标题'
  })

  this.add({
    is: 'main',
    children: [/* 动态内容 */]
  })

  this.add({
    is: 'footer',
    key: '_static_footer',
    children: '© 2024 网站版权'
  })
}

不再需要时手动清除缓存释放内存:

javascript
import { JsRender } from '@jiess/core'

JsRender.clearStatic('_static_header')
// 或清除全部
JsRender.clearAllStatic()

最佳实践与性能优化 ​

1. key 的使用 ​

  • 列表渲染:始终提供唯一 key,避免使用 index
  • 静态节点:使用 _static_ 前缀缓存实例
  • 普通节点:仅在需要优化更新时设置 key

2. 条件渲染 vs 动态显示 ​

优先使用 $if 而非 style.display 控制显隐——$if 为 false 时不产生 DOM,减少内存占用:

javascript
// 推荐:$proxy 绑定响应式条件,DOM 按需创建/销毁
this.add({ $if: { $proxy: isVisible }, children: '内容' })

// 不推荐:style.display 仍会创建 DOM 节点
this.add({ style: { display: isVisible.value ? 'block' : 'none' }, children: '内容' })

3. 避免深层嵌套 ​

过深的 children 嵌套会降低可读性和渲染性能,建议拆分为独立组件:

javascript
// 不推荐
function setup () {
  this.add({
    is: 'div',
    children: [
      { is: 'div', children: [
        { is: 'div', children: [
          { is: 'div', children: '深层嵌套' }
        ]}
      ]}
    ]
  })
}

// 推荐——拆分为组件
const DeepNested = {
  isJiess: true,
  name: 'DeepNested',
  setup () {
    this.add({ children: '深层嵌套' })
  }
}

function setup () {
  this.add({ is: DeepNested })
}

4. 合理使用响应式 ​

  • $ref:用于基本类型和需要整体替换的对象
  • $reactive:用于数组和需要深层响应式的对象
  • $computed:用于依赖其他响应式数据的计算属性

5. 事件回调优化 ​

避免在渲染循环中创建匿名函数,可提取到外部:

javascript
function setup () {
  const handleClick = () => console.log('点击')
  this.add({
    is: 'button',
    onClick: handleClick,
    children: '按钮'
  })
}

6. 合理组织代码 ​

  • 将复杂组件拆分为独立 Jiess 组件
  • 复用相同的节点结构时,封装为辅助函数
  • 使用 $computed 处理需要计算的属性

7. 性能注意事项 ​

  • $proxy 绑定应保持最小粒度,避免对整个大对象建立绑定
  • 静态内容使用 _static_ 缓存
  • 大型列表考虑使用虚拟滚动
  • 避免在渲染函数中进行复杂计算,使用 $computed 提前计算