npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

htmj

v0.0.1

Published

DocProxy,声明式构建和配置html文档

Readme

DocProxy

简介

DocProxy是一个返璞归真的html文档构建和处理库。 提供声明式构建、链式配置、组件化、样式隔离、WebComponents集成五大核心功能。 DocProxy不是MVVM框架,无脚手架,无虚拟DOM,无数据响应式,专注于高性能和对DOM节点精细控制的场景。

安装

  • 原生脚本引入
<script src="doc-proxy.js"></script>
  • npm安装
npm i doc-proxy
  • 模块化导入
import {$$} from "doc-proxy"

快速上手

<!DOCTYPE html>
<html lang="en">
<head>
  <script src="doc-proxy.js"></script>
</head>
<body>
  <div id="app"></div>
  <script>

    //数据
    const data = [
      { id: 1, name: "张向明" },
      { id: 2, name: "蒋海艳" }
    ]

    //标签代理
    const { table, thead, tbody, tr, th, td } = $$

    //构建节点
    const node = table(
      { id: "table1", className: "list container", draggable: true },
      thead(
        tr(
          th("学号"),
          th("姓名")
        )
      ),
      tbody(
        data.map( item => tr(
          td(item.id),
          td(item.name)
        ))
      )
    )

    //挂载节点
    $$("#app").render(node)

  </script>
</body>
</html>

核心概念

  1. 【节点代理】节点代理是DOM节点的代理,通过节点代理可以链式配置DOM节点的属性、样式、事件。
  2. 【真节点】真节点是指【节点代理】背后的真实DOM节点,每个节点代理一定会有对应的【真节点】。
  3. 【标签代理】标签代理是用来构建【节点代理】的函数,调用标签代理时,可以传入【节点代理】数组,因此可以递归构建出一棵DOM树。
  4. 【规则属性】规则属性是一个JSON对象,其键值会按照约定的方式传递到【真节点】的对象属性、标签属性、样式属性、事件。
  5. 【属性代理】属性代理是指通过【节点代理】的方法来访问【真节点】的属性(包括对象属性、标签属性、样式属性、事件)。如:nodeProxy.prop("title","提示")用来配置【真节点】的title属性。
  6. 【动态属性代理】动态属性代理是指在【节点代理】上的调用函数时,函数名就是【真节点】的属性名,如nodeProxy.title("提示"),【节点代理】的title函数对应的是真节点的title属性。

//解构【标签代理】
const { ul, li } = $$

//构建【节点代理】
const node = ul(
  //【规则属性】的键值将传递到ul上
  { id:"ul1", className:"list container", draggable:true },
  //子节点
  li("项目1"),
  li("项目2").attr("title","项目2"),    //attr函数是一个通用的【属性代理】
  li("项目3").title("项目3")            //title函数是一个【动态属性代理】
)

//挂载节点
$$("#app").render(node)

标签代理和节点代理

【标签代理】是一种用于声明式构建DOM树的函数, 其函数名对应的是你想要构建的DOM元素的标签名。 标签代理的返回值是一个【节点代理】, 用于链式配置其代理的真节点的属性、样式、事件。 你可以从全局对象DocProxy(或别名$$)上解构出所有的标签代理。 标签代理可接收如下类型的参数:

  1. 【JSON】:为当前节点配置属性
  2. 【节点代理】:插入子节点
  3. 【DOM节点】:插入子节点
  4. 【字符串】:插入文本子节点
  5. 【数组】:自动展开,展开后根据元素本身的类型递归追加到上级节点
  6. 【函数】:自动运行,并根据返回值的类型递归追加到上级节点

//用标准语法从$$解构出标签代理
const { ul, li } = $$

//构建节点
const node = ul(
  { id:"ul1", className:"list" },   //JSON参数,表示为ul配置属性
  li("项目1"),                      //节点代理参数,表示将LI插入UL
  li("项目2"),                      //"项目2"是个字符串,表示配置LI的文本内容
  [li("项目3"),li("项目4")],        //数组将自动展开
  ()=>li("项目5"),                  //函数将自动运行,并将返回的LI插入到UL
  ()=>[li("项目6", li("项目7"))]    //函数的返回值是一个数组,将自动展开并插入到UL
)

//挂载节点
$$("#app").render(node)

规则属性

标签代理可接收JSON类型的参数, 用于配置本节点的对象属性、标签属性、样式属性、事件, 我们称这个JSON对象为【规则属性】。规则属性的key约定如下:

  1. 首字母是下划线:配置标签属性,属性名将去除首个下划线,其余下滑先将转换成中划线;
  2. 首字母是美元符号:配置样式属性,属性名将去除首个美元符号,其余部分使用小驼峰命名;
  3. 首字母为on:配置事件;
  4. 其他情况:配置对象属性;
  5. 如果标签属性和样式属性的value是一个函数,则以函数的返回值为属性的值,函数中的this指向DOM节点。
const { ul, li } = $$

const node = ul(

  //【规则属性】
  {
    id:"ul1",                              //对象属性id
    _title:"提示文字",                      //标签属性title,属性名去除第一个下划线
    _custom_attribute:"子定义的标签属性",   //标签属性,属性名去除第一个下划线,其余下划线将转换成中划线
    _draggable:()=>"true",                 //标签属性的value是函数,则以返回值为属性值
    $border:"1px solid gray",              //样式属性,属性名去除第一个美元符号
    $backgroundColor:"lightgray",          //样式属性,除第一美元符号外,其余部分用小驼峰命名
    onclick:event=>{                       //配置事件
      console.log(event.target)
    }
  },

  //配置子节点
  li("项目1"),
  li("项目2"),
  li("项目3")

)

$$("#app").render(node)

属性前置范式

【标签代理】可以接收JSON类型的参数, 用来配置节点的属性, 其返回值是一个【节点代理】。 节点代理是一个拥有对象和函数双重身份的Proxy, 函数身份用来配置子节点。 如此便形成了一个比较优美的范式:tag(attribute)(...chidren), 我们称其为【属性前置范式】。

const { ul, li } = $$

//ul(obj)返回了一个节点代理,节点代理本身也是一个函数,这个函数用来追加子节点
const node = ul({id:"ul1",className:"class1 class2",title:"提示"})(    
  
  //配置子节点
  li("项目1"),
  li("项目2"),
  li("项目3")

)

$$("#app").render(node)

节点语法糖

如果要构建的DOM节点是一个仅包含文本内容的节点(如script和style),可以利用模板字符语法来减少一对括号。


const { div, style, ul, li } = $$

const node = div(
  style`
    li{
      color:green;
      font-size:20px;
    }
  `,
  ul(    
    li`项目1`,   
    li`项目2`,     
    li`项目3`
  )
)

$$("#app").render(node)

加解代理

除了通过标签代理构建节点代理外, 还可以通过$$函数将真节点包装成节点代理, 这一过程称为【加代理】。 通过节点代理的下标0, 可以得到真节点, 这一过程称为【解代理】。


//将一个真节点包装为节点代理,这一过程称为【加代理】
let nodeProxy1 = $$(document.querySelector("#app")) 

//通过查询表达式简化加代理  
let nodeProxy2 = $$("#app") 

//通过节点代理的下标0,访问真节点,这一过程称为【解代理】                        
let node1 = nodeProxy1[0]                              
let node2 = nodeProxy2[0]

//两个代理是不相等的
console.log(nodeProxy1===nodeProxy2)

//两个代理代理了同一个真节点
console.log(node1 === node1)

属性代理

节点代理上提供了大量可链式调用的函数, 称为【属性代理】。 属性代理用来配置和获取节点的对象属性、标签属性、样式属性、事件。

  1. 【prop(name,value)】配置对象属性
  2. 【prop(name)】获取对象属性值
  3. 【prop(obj)】将obj的键值配置到节点对应的对象属性上
  4. 【arrt(name,value)】配置标签属性
  5. 【arrt(name,false)】移除标签属性
  6. 【arrt(name,fn)】以fn的返回值为value递归调用attr(name,value)
  7. 【attr(name)】获取标签属性值
  8. 【attr(obj)】将obj的键值配置到节点对应的标签属性上
  9. 【css(name,value)】配置样式属性
  10. 【css(name,fn)】以fn的返回值为value递归调用css(name,value)
  11. 【css(name)】获取样式属性值
  12. 【css(obj)】将obj的键值配置到节点对应的样式属性上
  13. 【class(class1)】添加class1
  14. 【class([class1,calss2])】添加class1和class2
  15. 【class(class1,false)】移除class1
  16. 【class(fn)】将fn的返回值添加到className
  17. 【class(class1,fn)】如果fn的返回值是false则移除class1,否则添加class1
  18. 【on(name,fn,option)】配置事件
  19. 特别注意:attr的第二个参数如何为false,会删除该标签属性,如果要将标签属性设置为false请使用字符串"false"

$$("#app")                              //加代理
  .prop("innerHTML","内容")             //配置对象属性
  .attr("draggable","true")             //配置标签属性                     
  .class("class1")                      //配置class
  .class("class2 class3 class4")        //添加多个class
  .class("class3 class4",false)         //移除class
  .css("display","block")               //配置样式
  .on("click",()=>console.log("单击"))  //配置事件
  
  .attr("title",function(){             //attr和css两个链函数的第二个参数可以是函数,函数的返回值就是属性值      
    let title = this.className ;        //this指向当前真节点 
    return title;
  })

//当prop、attr、css三个函数只有一个参数时,用来获取属性值
const node = $$("#app")
console.log(node.prop("innerHTML"))
console.log(node.attr("draggable"))
console.log(node.css("display"))

动态属性代理

可以通过【属性代理】配置节点的对象属性、标签属性、样式属性、事件。 除此之外DocProxy还提供了一套按函数名来配置属性的动态函数, 由于这套函数是完全由你的意图动态生成的, 因此我们称为【动态属性代理】。 动态属性代理的函数名约定如下:

  1. 以_开始的动态链函数用来配置节点的标签属性;
  2. 以$开始的动态链函数用来配置节点的样式;
  3. 以on开始的动态链函数用来配置节点的事件;
  4. 注意on与动态onxxx的区别,on使用的是addEventListener,而动态的onxxx使用的是属性赋值;
  5. 其他动态链函数用来配置节点的对象属性。

除了使用函数调用语法配置节点的属性外, 也可以直接使用等号赋值语法为这些属性赋值, 赋值语法同样遵守上述命名规则。 如nodeProxy.innerHTML("内容")与nodeProxy.innerHTML="内容"是同样的效果, 采用函数调用语法时,返回的是节点代理本身,因此支持链式写法。


$$("#app")  

  //配置对象属性,innerHTML既是函数名,也是对象属性名,函数的参数是对象属性值          
  .innerHTML("当前时间:"+new Date())    
  
  //配置标签属性,以下划线开始
  ._title("提示文字") 
  
  //配置样式,美元符号+小驼峰命名
  .$padding(20).$backgroundColor("lightgray")
  
  //以属性赋值的方式配置click事件
  .onclick(()=>console.log("onclick"))

  //以addEventListener方式配置事件
  .on(
    "click",
    ()=>console.log("addEventListener"),
    {once:true}
  )
  
  //标签属性代理和样式属性代理的参数可以是一个函数,函数的返回值就是属性值
  ._draggable(function(){
    let time = new Date().getTime()
    return `\${time%2===0}`
  });

//当动态属性代理无参时,将返回对应的属性值
const node = $$("#app");
console.log(node.innerHTML())

含值属性

对象的属性和函数可以用点号访问(obj.property), 也可以用中括号访问(obj["property"]), 用中括号访问可以脱离变量命名规则。 于是便为节点代理的【含值属性】打开了大门。 所谓含值属性是指将属性名和属性值整体打包成一个字符串, 然后用中括号表达式去访问这个字符串, 框架会拦截这个字符串, 将其解析成属性名和属性值。 节点代理包含如下七种含值属性:

  1. nodeProxy["#myid"] 将节点的id配置为myid
  2. nodeProxy[".myclass"] 将myclass追加到节点的className
  3. nodeProxy["title=提示"] 等号左边是对象属性名,右边是属性值
  4. nodeProxy["_draggable=true"] 等号左边是标签属性名(丢弃首个下划线),右边是属性值
  5. nodeProxy["$color=red"] 等号左边是样式属性名(丢弃首个美元号),右边是样式值
  6. nodeProxy["onclick==event=>console.log(event.target)"] 双等号左边是事件名含on,右边是事件处理程序
  7. nodeProxy["customPropery=={a:'aa',b:1}"] 双等号左边是属性名,右边是一个对象

$$(".app")
  ["#app"]              //用#号配置id
  [".class1"]           //用.号追加className  
  ["title=提示"]        //等号左边是对象属性名,等号右边是属性值
  ["_draggable=true"]   //等号左边是标签属性名(不含首个下划线),等号右边是属性值
  ["$color=red"]        //等号左边是样式属性名(不含首个美元符),等号右边是样式值
  ["p1=1"]              //使用单等号时,p1的值是字符串""1
  ["p2==1"]             //使用双等号时,p2的值是数字1

console.log($$(".app")[0].p1.constructor)
console.log($$(".app")[0].p2.constructor)

const clickHandle = function(event){console.log(this)}

//通常我们会搭配【属性前置范式】
const { ul, li } = $$
$$(".app")["onclick==event=>console.log(this)"](  //框架会使用new Function(内容)将双等号后面的内容生成一个新的函数,并不推荐这种方式
  ul.onclick(clickHandle)(                        //事件的配置建议使用属性代理,其他简单属性才使用含值属性
    li("项目1"),
    li("项目2")
  )
)

标签代理含值属性

前面的章节我们已经学习到【节点代理】具备函数和对象双重身份。 本章节我们学习【标签代理】的函数和对象双重身份。 之前的章节我们一直将【标签代理】当函数使用,用于追加子节点, 其实【标签代理】也是一个对象,是对象就可以有自己的方法。

调用【标签代理】的方法,实质上是调用其构建的【节点代理】的同名的【属性代理】。 如:

//此时将div当对象使用,调用div的id方法,将返回一个id为mydiv的节点代理
div.id("mydiv")

上例中已经创建了一个【节点代理】,后续可以链式的调用【节点代理】的其他方法,如:

//此处的id是【标签代理】的方法,会生成一个【节点代理】,title是这个【节点代理】的方法
div.id("mydiv").title("提示")

上例中我们最终得到了一个id为"mydiv",title为"提示"的【节点代理】,而节点代理又可以当函数使用,于是便形成了如下经典的【属性前置】范式:

div.id("mydiv").title("提示")(
  div("子节点1"),
  div("子节点2")
)

此外,我们还学习过节点代理的【含值属性】,标签代理同样支持【含值属性】,请看如下经典范式:


const { ul, li } = $$

const node = ul["#container"][".list"]["_style=background-color:yellow;color:red;"](
  li["#item1"][".item"]("项目1"),
  li["#item2"][".item"]("项目2"),
  li["#item3"][".item"]("项目3")
)

$$("#app").render(node)

插入html字符串

虽然提倡用DocProxy声明式构建DOM树, 但也难免需要在节点上直接插入一个待解析的html字符串, 特别是在webpack等工程化环境中。 此时可以使用【节点代理】的html方法。

$$("#app").html(`
  <h3>营利项目</h3>
  <ul>
    <li>项目1</li>
    <li>项目2</li>
    <li>项目3</li>
  </ul>
`)

事件处理

DocProxy的事件处理有两种方式:

1. 将事件当属性配置

nodeProxy
  .onmousedown(event=>console.log("鼠标按下"))
  .onmouseup(event=>console.log("鼠标松开"))

2. 用on(type,fn,option)配置事件

【节点代理】的on函数是原生addEventListener的加强版, 会在内部生成一个新的事件处理函数, 由于该内部事件处理函数对外不可见, 因此无法通过removeEventListener移除。 需要使用AbortController的abort方法来移除该事件处理函数。

const controller = new AbortController()
$$("#text").on(  
  "keydown",                   //事件类型
  function(event){             //事件处理函数
    console.log(event.key)
  },
  {                            //事件行为配置,每一项默认值为false
    capture: false,            //true表示只在捕获节点处理
    once: false,               //true表示只触发一次
    passive: false,            //true表示阻止调用event.preventDefault()
    signal: controller.signal, //将来调用controller的abort函数时将移除事件
    prevent: true,             //true表示阻止默认行为
    stop: true,                //true表示阻止冒泡
    self: true,                //true表示只在本节点上触发,子节点不触发
    letf: true,                //true表示只在按下鼠标左键时才触发事件
    right: false,              //true表示只在按下鼠标右键时才触发事件
    ctrl: true,                //true表示只在按下了ctrl键不松才触发事件
    alt: false,                //true表示只在按下了alt键不松才触发事件
    shift: false,              //true表示只在按下了shift键不松才触发事件
    meta: false,               //true表示只在按下了meta键不松才触发事件
    debounce: 200              //200毫秒防抖
  }
)

方法代理

节点代理的首字母大写的方法称为【方法代理】, 调用方法代理时,内部会先将首字母小写,然后调用真节点的同名方法。 特别注意【方法代理】的返回值是当前【节点代理】,而不是真节点的方法返回值。

//链式调用真节点的setAttribute方法
$$("#app")
  .SetAttribute("title","提示文字")    
  .SetAttribute("draggable",true)

属性后置范式

之前的章节已经介绍了【属性前置范式】和【属性代理】。 本章节介绍另一种范式。 当节点没有子节点时, 可以先配置节点的文本内容, 然后在利用属性代理链式配置节点的属性, 如此便形成一种范式:tag(content).property1(value).property2(value), 我们称其为【属性后置范式】。 并没有强制要求使用哪种范式, 两种范式搭配使用, 可以使DOM树的构建过程更美观清晰。 作范式抉择时建议从以下几点考虑:层次分明、尽量扁平、避免头重脚轻、整齐划一。


const { ul, li } = $$

//【属性前置范式】ul(obj)返回了一个节点代理,节点代理本身也是一个函数,这个函数用来配置子节点
const node = ul({ id:"ul1", className:"class1 class2", title:"提示" })(    
  
  //【属性后置范式】li(string)返回了一个节点代理,节点代理也是一个对象,这个对象上的函数用来配置属性
  li("项目1").id("li1").className("item"),               //链式写法
  li("项目2").prop({ id:"li2", className:"item" }),      //对象写法
  li("项目3")({ id:"li3", className:"item" })            //使用规则属性

)

$$("#app").render(node)

动态构建

标签代理的参数可以是一个节点数组, 也可以是一个能返回节点(或节点数组)的函数, 从而实现对子节点的动态构建。 动态构建为组件化提供天然的支持。


//解构标签函数
const { div, label, ul, li } = $$

//准备数据
const data = ["item1","item2"]

//构建节点    
const node = div(
  div(
    "当前时间:",
    ()=>label(new Date())         //无参函数会自动运行,并将返回值插入当前节点
  ),
  ul(
    append=>{                     //函数会自动运行,参数append是一个钩子函数,指向上级节点代理的append方法
      for( let i=0; i<5; i++ ){
        append(li("项目"+i))      //此处append钩子函数实际调用的是ul节点代理的append方法
      }
    }
  ),
  ul( 
    data.map(item => li(item)),  //数组会自动展开
  )
  
) 

//挂载节点
$$("#app").render(node)

无参函数组件

所谓【函数组件】就是一个有返回值的函数, 命名为函数组件是为了区别于WebComponents组件。 函数组件的返回值通常是节点代理、字符串、属性配置对象, 以及上述类型的数组。

const { ul, li, div } = $$

//创建组件
function component() {
  return ul(
    li`项目1`,
    li`项目2`,
    li`项目3`
  )
}

//使用组件
const node = div(
  div("项目列表"),
  component   //等价与component(),框架会自动调用无参的函数组件
)

//渲染节点
$$("#app").render(node)

有参函数组件

无论是无参函数组件还是有参函数组件,其本质都是一个能返回节点代理(或节点数组)的函数,唯一的不同点是有参的函数组件在使用时必须手动传参调用。


const { ul, li, div, span } = $$

//定义组件
function component(data) {
  return ul(
    data.map(item =>
      li(span(item.id),span(item.name))
    )
  )
}

//准备数据
const data = [{id:1,name:"项目1"},{id:2,name:"项目2"},{id:3,name:"项目3"}]

//使用组件
const node = div(
  div("项目列表"),
  component(data)  //调用组件函数并传入参数
)

//渲染节点
$$("#app").render(node)

文档碎片

fragment是一个特殊的【标签代理】,其用途是创建一个文档碎片节点代理,其真节点是一个DocumentFragment实例。

const { fragment, ul, li } = $$  

//创建文档碎片
const content = fragment(
  li("项目1"),
  li("项目2"),
  li("项目3")
)

//使用文档碎片
const node = ul(
  content
)

//渲染节点
$$("#app").render(node)

shadowRoot

DocProxy实现了对WebComponents API的全面支持。 你可以使用customElement这种重型的实现, 也可以仅使用shadowRoot这种轻型实现。 shadowRoot可以做到最完美的样式隔离和样式穿透。 可以使用以下几种方式实现shadowRoot:

  1. 调用容器节点代理的shadowNode(node)函数
  2. 调用容器节点代理的shadowHTML(html)函数
  3. 设置子节点的root=true,声明式启用shadowRoot

shadowNode函数

const { ul, li } = $$
$$("#container").shadowNode(
  ul(
    li("项目1"),
    li("项目2")
  )
)

shadowHTML函数

$$("#container").shadowHTML(`
  <ul>
    <li>项目1</li>
    <li>项目1</li>
  </ul>
`)

声明式启用shadowRoot

const { ul, li } = $$
//ul设置了root属性,将先在容器上创建shadowRoot,然后再将ul插入容器。
//注意root属性要配置到容器的子节点上,而不是配置到容器本身
$$("#container")(
  ul.shadow(true)(    
    li("项目1"),
    li("项目2")
  )
)

样式隔离

<!DOCTYPE html>
<html lang="en">
  <head>
    <script src="../doc-proxy.js"></script>
  </head>
  <body>
    <div>这组件外边的div,样式没有被污染</div>
    <div id="app"></div>
    <script>
      const { template, style, ul, li } = $$
      const node = template.shadow(true)(
        style`
          li{
            color:var(--color);   /*使用外部定义的css变量*/
          }`,
        ul(
          li`项目1`,
          li`项目2`,
          li`项目3`
        )
      )
      $$("#app").render(node)
  </script>
  </body>
</html>

样式穿透

shadowRoot将隔离容器外部与内部的样式, 在通常情况下这是优点, 但有时确实需要在容器内部使用外部的样式。 使用浏览器原生支持的css变量,可以深度穿透多层ShadowRoot, 是目前最优的样式穿透方案。

<!DOCTYPE html>
<html lang="en">
  <head>
    <style>
      :root{--color:green;}
    </style>
    <script src="../doc-proxy.js"></script>
  </head>
  <body>
    <div id="app"></div>
    <script>
    const { fragment, style, ul, li } = $$
    const node = fragment.shadow(true)(
      style`
        li{
          color:var(--color);   /*使用外部定义的css变量*/
        }`,
      ul(
        li`项目1`,
        li`项目2`,
        li`项目3`
      )
    )
    $$("#app").render(node)
  </script>
  </body>
</html>

托管脚本

shadowRoot的子节点可以包含script块, 当子节点被插入到容器的shadowRoot时, 这些脚本块将被托管运行, 脚本块中的this指向容器, 脚本块中的root指向容器的shadowRoot。

const { template, ul, li, script } = $$
const node = template["shadow=true"](
  ul(
    li`项目1`,
    li`项目2`,
    li`项目3`
  ),
  script`
    this.onclick = ()=>{
      console.log("this is ",this)
      console.log("root is ",root)
    }
  `
)
$$("#app").render(node)

插槽

const { style, template, header, main, footer, slot, div } = $$

//创建一个包含插槽的template,并配置roott为true
const temp = template["shadow=true"](
  style`
    ::slotted(*){
      padding:10px;
      border:1px solid lightgray;
    }
  `,
  header("页头"),
  main(
    slot    //没有命名的插槽是一个默认插槽
  ),
  footer(
    slot["name=slot1"]
  )
)

$$("#app")(temp)(             //将template的内容插入shadowRoot
  div("主体"),                //插入默认插槽
  div["slot=slot1"]("页脚")   //插入具名插槽
)

CustomElement

//定义CustomElement类,请参考w3c官网
//以下示例省略了生命周期函数
//此处完全使用原生代码来创建CustomElement,
//其实完全可以用DocProxy来构建shadowRoot的内容
class MyDiv extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({mode:"open"})
    this.shadowRoot.innerHTML = `
      <style>
        :host{display:block;border:1px solid lightgray;}
        *{padding:10px;}
        header{border-bottom:1px solid lightgray;font-weight:bolder;s}
      </style>
      <header>这是一个webcomponent组件</header>
      <main>
        <slot></slot>
      </main>`
  }
}

//为CustomElement定义标签
customElements.define("my-div",MyDiv)

//解构webcomponent标签代理,注意my_div的下划线对应是my-div的中划线
const { my_div } = $$

//构建节点 
const node = my_div("这是组件的内容")

//渲染节点
$$("#app").render(node)

文档处理

DocProxy对常用的html文档处理函数做了轻量封装。

  1. 【nodeProxy[0]】通过节点代理的下标0可以获取代理的真实节点。
  2. 【nodeProxy[-1]】通过节点代理的下标-1可以获得真节点的shadowRoot。
  3. 【nodeProxy.html(value)】设置节点的innerHTML。
  4. 【nodeProxy.text(value)】设置节点的innerText。
  5. 【nodeProxy.append(...children)】添加子节点或配置属性。
  6. 【nodeProxy.prepend(...children)】在节点部的第一个子元素之前插入子节点
  7. 【nodeProxy.appendTo(container)】将当前节点追加到容器
  8. 【nodeProxy.prependTo(container)】将当前节点插入到容器的第一个子节点的前面
  9. 【nodeProxy.render(...chidren)】先清空本节点,在追加子节点
  10. 【nodeProxy.insert(value,where)】向当前节点插入子节点或html字符串内容
  11. 【nodeProxy.before(value)】在当前节点之前插入兄弟节点,value为空表示获取当前节点的上一个兄弟节点
  12. 【nodeProxy.after(value)】在当前节点之后插入兄弟节点,value为空表示获取当前节点的下一个兄弟节点
  13. 【nodeProxy.wrap(value)】用value来包裹当前节点
  14. 【nodeProxy.unwrap()】剥离当前节点的外层节点
  15. 【nodeProxy.up()】如果存在上一个兄弟节点,则与之交换位置
  16. 【nodeProxy.down()】如果存在下一个兄弟节点,则与之交换位置
  17. 【nodeProxy.top()】与父节点的第一个子节点交换位置
  18. 【nodeProxy.bottom()】与父节点的最后一个子节点交换位置
  19. 【nodeProxy.swap(target)】与父节点的指定子节点交互位置,target可以是下标、查询字符串、节点、节点代理
  20. 【nodeProxy.replace(target)】用当前节点替换目标节点

svg

const { svg, svg_path } = $$  //注意:除svg本身之外,其他svg元素的标签名都必须加svg_前缀
//构建节点
const node = svg["_width=100"]["_height=100"](
  svg_path({
    _d:"M0,0 L100,100",
    _stroke:"red",
    _stroke_width:1
  })
)
//渲染节点
$$("#app").render(node)

canvas集成

canvas节点本身与其他节点代理同样操作, canvas节点代理的context2d可以获取canvas的2d上下文(CanvasRenderingContext2D)的代理。 2d上下文代理上的方法用于配置与方法名同名的上下文属性, 若方法名首字母大写,则用来调用上下文方法。

$$("canvasid");
  .width(200)                 //设置画布属性
  .height(200)                //设置画布属性
  .context2d()                //获取2d上下文代理
  .lineWidth("1")             //配置上下文属性 
  .strokeStyle("red")         //配置上下文属性   
  .BeginPath()                //调用上下文方法,首字母大写,无参
  .MoveTo(50,50)              //调用上下文方法,首字母大写,有参
  .LineTo(100,100)
  .ClosePath()
  .Stroke()

如果不喜欢链式配置, 或不喜欢方法名首字母大写, 或者其他框架在CanvasRenderingContext2D的prototype上定义的首字母大写的属性和方法, 则可使用nodeProxy.context2d(...steps)配置, steps是剩余参数,要求steps的每一个项的constructor都严格等于Array。

$$("canvasid")
  .context2d(
    1, "a", true, false, {},  //要求每个参数都是数组,这些不合法的参数不会抛出异常,但会被忽略
    ["lineWidth",1],          //第[1]个元素不是数组,表示配置属性
    ["strokeStyle","red"],    //第[1]个元素不是数组,表示配置属性
    ["beginPath"],            //只有一个元素,表示调用无参方法,也可以写成["beginPath",[]]
    ["moveTo",[50,50]],       //第[1]个元素是数组,表示以第[0]个元素为方法名,以第[1]个元素为参数(展开),调用方法
    ["lineTo",[100,100]],
    ["closePath"],
    ["stroke"]
  )