Jiess 节点创建
目录
this.add() 是 Jiess 创建节点的统一入口,在 Vue 3、React 和 Web Components 中语法完全一致。支持文本、HTML 元素、第三方组件和 Jiess 自定义组件,搭配响应式数据驱动、条件/列表渲染,以纯配置对象描述整个 UI。
核心 API:this.add()
this.add(...nodes: (string | number | boolean | NodeConfig | Array<NodeConfig>)[]): this返回 this,支持链式调用。配置采用扁平化设计——想给元素加什么属性,直接在对象中平铺即可。
基本用法
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字段使代码更简洁。
配置项速查
| 配置项 | 类型 | 必填 | 默认值 | 描述 |
|---|---|---|---|---|
is | string|Component | 否 | 'div' | 标签名或组件 |
className / class | string|string[] | 否 | — | CSS 类名,className 和 class 均直接设置(Shadow DOM 提供天然隔离,无需作用域前缀) |
children | string|number|boolean|NodeConfig|NodeConfig[] | 否 | — | 子节点内容 |
style | object | 否 | — | 内联样式,驼峰命名(如 fontSize) |
key | string|number | 否 | — | 唯一标识,优化渲染性能 |
onClick / onInput 等 | function | 否 | — | 事件监听,前缀 on + 首字母大写事件名 |
$if | boolean|ReactiveValue | 否 | true | 条件渲染开关 |
$elseIf | boolean|ReactiveValue | 否 | — | 条件渲染 else-if 分支 |
$ref | ReactiveRef | 否 | — | 获取元素/组件引用 |
$slots | object | 否 | — | 组件插槽内容 |
$model | ReactiveRef | 否 | — | 表单双向绑定 |
$proxy | ReactiveValue | 否 | — | 响应式数据驱动属性 |
$attrs | object | 否 | — | 额外透传属性 |
$props | object | 否 | — | 组件 props |
基础节点创建
1. 文本节点
直接传入字符串,不包裹任何标签:
function setup () {
this.add('Hello, Jiess!')
}2. 数据渲染规则
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:
function setup () {
this.add({
children: '这是一个默认的 div 元素'
})
}4. 自定义标签
function setup () {
this.add({
is: 'p',
className: 'text-center',
children: '这是一个段落标签'
})
this.add({
is: 'button',
children: '点击我',
onClick: () => alert('按钮被点击了!')
})
}5. 类名设置
className 和 class 均直接设置类名(Shadow DOM 提供天然隔离,无需作用域前缀),值可以是字符串或数组:
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. 多元素创建与链式调用
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 数组构建任意深度嵌套:
function setup () {
this.add({
className: 'card',
children: [
{ className: 'card-header', children: '卡片标题' },
{ className: 'card-body', children: '卡片内容' },
{ className: 'card-footer', children: '卡片页脚' }
]
})
}8. 多维数组自动展平
Jiess 自动展平多维数组为单层:
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-*)直接写在配置对象中:
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. 样式设置
支持驼峰命名和内联样式:
function setup () {
this.add({
is: 'div',
style: {
padding: '16px',
margin: '8px',
backgroundColor: '#f5f5f5',
borderRadius: '8px',
fontSize: '14px',
lineHeight: '1.5'
},
children: '带样式的元素'
})
}11. $ref 获取 DOM 引用
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 场景
三步上手:
- 引入 Three.js 命名空间,注册环境(
@jiess/core/three) - 在组件中
this.add({ is: 'threeCanvas', ... })声明根场景 - 在
children里按标签声明 3D 元素(几何体、材质、灯光、相机)
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 画布作为渲染环境:
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 为前缀,后接首字母大写的事件名。
基础事件
function setup () {
this.add({
is: 'button',
children: '点击我',
onClick: e => {
e.stopPropagation()
console.log('按钮被点击了!')
},
onMouseEnter: () => console.log('鼠标移入'),
onMouseLeave: () => console.log('鼠标移出')
})
}表单双向绑定——$model
$model 简化 onInput + value 组合:
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:
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 等修饰符语法,在回调中手动调用:
function setup () {
this.add({
is: 'a',
href: '#',
children: '阻止默认行为',
onClick: e => {
e.preventDefault()
e.stopPropagation()
console.log('已阻止默认行为和冒泡')
}
})
}自定义事件
function setup () {
this.add({
is: 'div',
children: '自定义事件示例',
onCustomEvent: (detail) => {
console.log('自定义事件触发', detail)
}
})
}使用组件
is 可传入 React 组件、Vue 3 组件、Web Components 组件或 Jiess 组件。
React 组件(Ant Design)
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 组件库:
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:
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 自定义组件
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: '卡片页脚'
})
}组件传参
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 传递插槽内容,支持字符串、对象、函数三种形式。
默认插槽
function setup () {
this.add({
is: 'div',
$slots: {
default: '这是默认插槽内容'
}
})
}Vue 3 组件插槽(以 Vant Field 为例)
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 内正常工作:
function setup() {
this.add({
is: 'my-card',
$slots: {
default: [{ is: 'p', children: '卡片内容' }],
header: [{ is: 'h3', children: '卡片标题' }]
}
})
}Jiess 组件插槽
Jiess 组件在 setup 第二个参数中获取 slots:
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 我的应用'
}
})
}动态插槽
根据运行时条件决定是否提供插槽:
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可选,仅在需要转换返回值时使用。
静态条件
function setup () {
this.add({ $if: true, children: '这个节点会显示' })
this.add({ $if: false, children: '这个节点不会显示' })
}响应式条件——$ref 本身就是 boolean(无需 done)
当绑定的 $ref 本身就是 boolean 时,直接用 $proxy,不需要 done:
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:
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 配合使用
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:
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' } }
]
}
]
})
}实际应用——登录状态切换
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,确保高效更新。
基本列表
function setup () {
const items = ['苹果', '香蕉', '橙子', '葡萄']
this.add({
is: 'ul',
children: items.map((item) => ({
is: 'li',
key: item,
children: item
}))
})
}响应式列表
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 将响应式数据绑定到节点属性,数据变化时自动更新视图。
动态属性
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)
}
})
}动态样式
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
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 可返回新的节点配置,实现子节点动态重建。常用于父子组件级联场景。
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
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 可以是简单引用(直接取值),也可以是包含以下字段的对象:
| 字段 | 类型 | 描述 |
|---|---|---|
$proxy | ReactiveValue | 必填,绑定的响应式数据源 |
field | string | 可选,指定读取对象中的字段 |
done | (value) => any | 可选,对值进行转换后输出 |
update | boolean | 可选,设为 true 时启用双向更新 |
$proxy 与 field 配合
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)
import { Input } from 'antd'
import { $ref } from '@jiess/core'
function setup () {
const inputValue = $ref('')
this.add({
is: Input,
value: { $proxy: inputValue, update: true },
placeholder: '请输入内容'
})
}// 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 实例缓存复用:
function setup () {
this.add({
is: 'header',
key: '_static_header',
children: '网站标题'
})
this.add({
is: 'main',
children: [/* 动态内容 */]
})
this.add({
is: 'footer',
key: '_static_footer',
children: '© 2024 网站版权'
})
}不再需要时手动清除缓存释放内存:
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,减少内存占用:
// 推荐:$proxy 绑定响应式条件,DOM 按需创建/销毁
this.add({ $if: { $proxy: isVisible }, children: '内容' })
// 不推荐:style.display 仍会创建 DOM 节点
this.add({ style: { display: isVisible.value ? 'block' : 'none' }, children: '内容' })3. 避免深层嵌套
过深的 children 嵌套会降低可读性和渲染性能,建议拆分为独立组件:
// 不推荐
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. 事件回调优化
避免在渲染循环中创建匿名函数,可提取到外部:
function setup () {
const handleClick = () => console.log('点击')
this.add({
is: 'button',
onClick: handleClick,
children: '按钮'
})
}6. 合理组织代码
- 将复杂组件拆分为独立 Jiess 组件
- 复用相同的节点结构时,封装为辅助函数
- 使用
$computed处理需要计算的属性
7. 性能注意事项
$proxy绑定应保持最小粒度,避免对整个大对象建立绑定- 静态内容使用
_static_缓存 - 大型列表考虑使用虚拟滚动
- 避免在渲染函数中进行复杂计算,使用
$computed提前计算